diff --git a/packages/core/src/error/ZWaveError.ts b/packages/core/src/error/ZWaveError.ts index ebcea83ad51..af3ed919fc3 100644 --- a/packages/core/src/error/ZWaveError.ts +++ b/packages/core/src/error/ZWaveError.ts @@ -87,6 +87,8 @@ export enum ZWaveErrorCodes { NVM_InvalidFormat, /** Not enough space in the NVM */ NVM_NoSpace, + /** The NVM hasn't been opened yet */ + NVM_NotOpen, CC_Invalid = 300, CC_NoNodeID, diff --git a/packages/nvmedit/src/cli.ts b/packages/nvmedit/src/cli.ts index d83631604f0..40319719390 100644 --- a/packages/nvmedit/src/cli.ts +++ b/packages/nvmedit/src/cli.ts @@ -47,10 +47,10 @@ void yargs const buffer = await fs.readFile(argv.in); let json: any; try { - json = nvmToJSON(buffer, argv.verbose); + json = await nvmToJSON(buffer, argv.verbose); } catch (e) { try { - json = nvm500ToJSON(buffer); + json = await nvm500ToJSON(buffer); } catch (ee) { console.error(e); process.exit(1); @@ -118,8 +118,8 @@ Create a backup of the target stick, use the nvm2json command to convert it to J } const nvm = versionIs500 - ? jsonToNVM500(json, protocolVersion) - : jsonToNVM(json, protocolVersion); + ? await jsonToNVM500(json, protocolVersion) + : await jsonToNVM(json, protocolVersion); await fs.writeFile(argv.out, nvm); console.error(`NVM (binary) written to ${argv.out}`); @@ -217,7 +217,7 @@ Create a backup of the target stick, use the nvm2json command to convert it to J async (argv) => { const source = await fs.readFile(argv.source); const target = await fs.readFile(argv.target); - const output = migrateNVM(source, target); + const output = await migrateNVM(source, target); await fs.writeFile(argv.out, output); console.error(`Converted NVM written to ${argv.out}`); diff --git a/packages/nvmedit/src/convert.test.ts b/packages/nvmedit/src/convert.test.ts index e36ab7fba06..ad3dfd6b8e9 100644 --- a/packages/nvmedit/src/convert.test.ts +++ b/packages/nvmedit/src/convert.test.ts @@ -22,7 +22,7 @@ import type { NVM500JSON } from "./nvm500/NVMParser"; for (const file of files) { test(`${suite} -> ${file}`, async (t) => { const data = await fs.readFile(path.join(fixturesDir, file)); - const json = nvmToJSON(data); + const json = await nvmToJSON(data); t.snapshot(json); }); } @@ -36,14 +36,14 @@ import type { NVM500JSON } from "./nvm500/NVMParser"; for (const file of files) { test(`${suite} -> ${file}`, async (t) => { - const jsonInput: Required = await fs.readJson( + const jsonInput: NVMJSON = await fs.readJson( path.join(fixturesDir, file), ); - const nvm = jsonToNVM( + const nvm = await jsonToNVM( jsonInput, jsonInput.controller.applicationVersion, ); - const jsonOutput = nvmToJSON(nvm); + const jsonOutput = await nvmToJSON(nvm); // @ts-expect-error if (!("meta" in jsonInput)) delete jsonOutput.meta; @@ -66,8 +66,8 @@ import type { NVM500JSON } from "./nvm500/NVMParser"; const nvmIn = await fs.readFile(path.join(fixturesDir, file)); const version = /_(\d+\.\d+\.\d+)[_.]/.exec(file)![1]; - const json = nvmToJSON(nvmIn); - const nvmOut = jsonToNVM(json, version); + const json = await nvmToJSON(nvmIn); + const nvmOut = await jsonToNVM(json, version); t.deepEqual(nvmOut, nvmIn); }); @@ -83,7 +83,7 @@ import type { NVM500JSON } from "./nvm500/NVMParser"; for (const file of files) { test(`${suite} -> ${file}`, async (t) => { const data = await fs.readFile(path.join(fixturesDir, file)); - const json = nvm500ToJSON(data); + const json = await nvm500ToJSON(data); t.snapshot(json); }); } @@ -119,8 +119,11 @@ import type { NVM500JSON } from "./nvm500/NVMParser"; const nvmIn = await fs.readFile(path.join(fixturesDir, file)); // const lib = /_(static|bridge)_/.exec(file)![1]; - const json = nvm500ToJSON(nvmIn); - const nvmOut = jsonToNVM500(json, json.controller.protocolVersion); + const json = await nvm500ToJSON(nvmIn); + const nvmOut = await jsonToNVM500( + json, + json.controller.protocolVersion, + ); t.deepEqual(nvmOut, nvmIn); }); @@ -190,7 +193,7 @@ test("700 to 700 migration shortcut", async (t) => { const nvmTarget = await fs.readFile( path.join(fixturesDir, "ctrlr_backup_700_7.16_1.bin"), ); - const converted = migrateNVM(nvmSource, nvmTarget); + const converted = await migrateNVM(nvmSource, nvmTarget); t.deepEqual(converted, nvmSource); }); diff --git a/packages/nvmedit/src/convert.test.ts.md b/packages/nvmedit/src/convert.test.ts.md index a4c27259f56..826824858ac 100644 --- a/packages/nvmedit/src/convert.test.ts.md +++ b/packages/nvmedit/src/convert.test.ts.md @@ -6984,6 +6984,7 @@ Generated by [AVA](https://avajs.dev). > Snapshot 1 { + applicationFileFormat: 4, controller: { applicationData: 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', applicationName: null, diff --git a/packages/nvmedit/src/convert.test.ts.snap b/packages/nvmedit/src/convert.test.ts.snap index 4a6b6a846e1..ed38022ea37 100644 Binary files a/packages/nvmedit/src/convert.test.ts.snap and b/packages/nvmedit/src/convert.test.ts.snap differ diff --git a/packages/nvmedit/src/convert.ts b/packages/nvmedit/src/convert.ts index 4b0167d6607..d051ae6183a 100644 --- a/packages/nvmedit/src/convert.ts +++ b/packages/nvmedit/src/convert.ts @@ -1,6 +1,7 @@ import { type CommandClasses, ControllerCapabilityFlags, + MAX_NODES, NodeIDType, type NodeProtocolInfo, NodeType, @@ -14,10 +15,25 @@ import { cloneDeep, num2hex, pick } from "@zwave-js/shared/safe"; import { isObject } from "alcalzone-shared/typeguards"; import semver from "semver"; import { MAX_PROTOCOL_FILE_FORMAT, SUC_MAX_UPDATES } from "./consts"; +import { NVM3, type NVM3Meta } from "./lib/NVM3"; +import { NVM500 } from "./lib/NVM500"; +import { + type Route, + type RouteCache, + getEmptyRoute, +} from "./lib/common/routeCache"; +import { type SUCUpdateEntry } from "./lib/common/sucUpdateEntry"; +import { NVMMemoryIO } from "./lib/io/NVMMemoryIO"; +import { NVM3Adapter } from "./lib/nvm3/adapter"; +import { + ZWAVE_APPLICATION_NVM_SIZE, + ZWAVE_PROTOCOL_NVM_SIZE, + ZWAVE_SHARED_NVM_SIZE, +} from "./lib/nvm3/consts"; import { ApplicationCCsFile, ApplicationCCsFileID, - ApplicationDataFile, + type ApplicationDataFile, ApplicationDataFileID, ApplicationRFConfigFile, ApplicationRFConfigFileID, @@ -31,77 +47,71 @@ import { ControllerInfoFileID, type ControllerInfoFileOptions, type LRNodeInfo, - LRNodeInfoFileV5, + type LRNodeInfoFileV5, NVMFile, type NodeInfo, - NodeInfoFileV0, - NodeInfoFileV1, - ProtocolAppRouteLockNodeMaskFile, + type NodeInfoFileV0, + type NodeInfoFileV1, + type ProtocolAppRouteLockNodeMaskFile, ProtocolAppRouteLockNodeMaskFileID, - ProtocolLRNodeListFile, + type ProtocolLRNodeListFile, ProtocolLRNodeListFileID, - ProtocolNodeListFile, + type ProtocolNodeListFile, ProtocolNodeListFileID, - ProtocolPendingDiscoveryNodeMaskFile, + type ProtocolPendingDiscoveryNodeMaskFile, ProtocolPendingDiscoveryNodeMaskFileID, - ProtocolPreferredRepeatersFile, + type ProtocolPreferredRepeatersFile, ProtocolPreferredRepeatersFileID, ProtocolRouteCacheExistsNodeMaskFile, ProtocolRouteCacheExistsNodeMaskFileID, - ProtocolRouteSlaveSUCNodeMaskFile, + type ProtocolRouteSlaveSUCNodeMaskFile, ProtocolRouteSlaveSUCNodeMaskFileID, - ProtocolSUCPendingUpdateNodeMaskFile, + type ProtocolSUCPendingUpdateNodeMaskFile, ProtocolSUCPendingUpdateNodeMaskFileID, ProtocolVersionFile, ProtocolVersionFileID, - ProtocolVirtualNodeMaskFile, + type ProtocolVirtualNodeMaskFile, ProtocolVirtualNodeMaskFileID, - type Route, - type RouteCache, - RouteCacheFileV0, - RouteCacheFileV1, + type RouteCacheFileV0, + type RouteCacheFileV1, SUCUpdateEntriesFileIDV0, - SUCUpdateEntriesFileV0, - SUCUpdateEntriesFileV5, - type SUCUpdateEntry, + type SUCUpdateEntriesFileV0, + type SUCUpdateEntriesFileV5, SUC_UPDATES_PER_FILE_V5, - getEmptyRoute, nodeIdToLRNodeInfoFileIDV5, nodeIdToNodeInfoFileIDV0, nodeIdToNodeInfoFileIDV1, nodeIdToRouteCacheFileIDV0, nodeIdToRouteCacheFileIDV1, sucUpdateIndexToSUCUpdateEntriesFileIDV5, -} from "./files"; +} from "./lib/nvm3/files"; import { - ApplicationNameFile, + type ApplicationNameFile, ApplicationNameFileID, -} from "./files/ApplicationNameFile"; -import { - type NVM3Objects, - type NVM3Pages, - type NVMMeta, - encodeNVM, - getNVMMeta, - parseNVM, -} from "./nvm3/nvm"; -import type { NVM3Object } from "./nvm3/object"; -import { mapToObject } from "./nvm3/utils"; +} from "./lib/nvm3/files/ApplicationNameFile"; +import type { NVM3Object } from "./lib/nvm3/object"; +import { dumpNVM, mapToObject } from "./lib/nvm3/utils"; +import { NVM500Adapter } from "./lib/nvm500/adapter"; +import { nvm500Impls } from "./lib/nvm500/impls"; +import { resolveLayout } from "./lib/nvm500/shared"; import { type NVM500JSON, - NVMSerializer, - createParser as createNVM500Parser, - nmvDetails500, + type NVM500JSONController, + type NVM500JSONNode, + type NVM500Meta, } from "./nvm500/NVMParser"; export interface NVMJSON { - format: number; - meta?: NVMMeta; + format: number; // protocol file format + applicationFileFormat?: number; + meta?: NVM3Meta; controller: NVMJSONController; nodes: Record; lrNodes?: Record; } +export type NVMJSONWithMeta = NVMJSON & { meta: NVM3Meta }; + export interface NVMJSONController { protocolVersion: string; applicationVersion: string; @@ -188,7 +198,7 @@ type ParsedNVM = } | { type: 700; - json: Required; + json: NVMJSONWithMeta; } | { type: "unknown"; @@ -285,7 +295,7 @@ export function nvmObjectsToJSON( fileVersion: string, ): T => { const obj = getObjectOrThrow(id); - return NVMFile.from(obj, fileVersion) as T; + return NVMFile.from(obj.key, obj.data!, fileVersion) as T; }; const getFile = ( @@ -293,8 +303,8 @@ export function nvmObjectsToJSON( fileVersion: string, ): T | undefined => { const obj = getObject(id); - if (!obj) return undefined; - return NVMFile.from(obj, fileVersion) as T; + if (!obj || !obj.data) return undefined; + return NVMFile.from(obj.key, obj.data, fileVersion) as T; }; // === Protocol NVM files === @@ -571,7 +581,8 @@ export function nvmObjectsToJSON( } : {}), sucUpdateEntries, - applicationData: applicationDataFile?.data.toString("hex") ?? null, + applicationData: applicationDataFile?.applicationData.toString("hex") + ?? null, applicationName: applicationNameFile?.name ?? null, }; @@ -685,293 +696,544 @@ function nvmJSONControllerToFileOptions( return ret; } -function serializeCommonApplicationObjects(nvm: NVMJSON): NVM3Object[] { - const ret: NVM3Object[] = []; - const applTypeFile = new ApplicationTypeFile({ - ...pick(nvm.controller, [ - "isListening", - "optionalFunctionality", - "genericDeviceClass", - "specificDeviceClass", - ]), - fileVersion: nvm.controller.applicationVersion, - }); - ret.push(applTypeFile.serialize()); +/** Reads an NVM buffer of a 700+ series stick and returns its JSON representation */ +export async function nvmToJSON( + buffer: Buffer, + debugLogs: boolean = false, +): Promise { + const io = new NVMMemoryIO(buffer); + const nvm3 = new NVM3(io); + const info = await nvm3.init(); - const applCCsFile = new ApplicationCCsFile({ - ...pick(nvm.controller.commandClasses, [ - "includedInsecurely", - "includedSecurelyInsecureCCs", - "includedSecurelySecureCCs", - ]), - fileVersion: nvm.controller.applicationVersion, - }); - ret.push(applCCsFile.serialize()); - - if (nvm.controller.rfConfig) { - const applRFConfigFile = new ApplicationRFConfigFile({ - ...pick(nvm.controller.rfConfig, [ - "rfRegion", - "txPower", - "measured0dBm", - ]), - enablePTI: nvm.controller.rfConfig.enablePTI ?? undefined, - maxTXPower: nvm.controller.rfConfig.maxTXPower ?? undefined, - nodeIdType: nvm.controller.rfConfig.nodeIdType ?? undefined, - fileVersion: nvm.controller.applicationVersion, - }); - ret.push(applRFConfigFile.serialize()); - } + const adapter = new NVM3Adapter(nvm3); - if (nvm.controller.applicationData) { - // TODO: ensure this is 512 bytes long - const applDataFile = new ApplicationDataFile({ - data: Buffer.from(nvm.controller.applicationData, "hex"), - fileVersion: nvm.controller.applicationVersion, - }); - ret.push(applDataFile.serialize()); + if (debugLogs) { + // Dump all pages, all raw objects in each page, and each object in its final state + await dumpNVM(nvm3); } - if (nvm.controller.applicationName && nvm.meta?.sharedFileSystem) { - // The application name only seems to be used with the shared file system - const applNameFile = new ApplicationNameFile({ - name: nvm.controller.applicationName, - fileVersion: nvm.controller.applicationVersion, - }); - ret.push(applNameFile.serialize()); - } - - return ret; -} + const firstPageHeader = info.isSharedFileSystem + ? info.sections.all.pages[0] + : info.sections.protocol.pages[0]; + + const meta: NVM3Meta = { + sharedFileSystem: info.isSharedFileSystem, + ...pick(firstPageHeader, [ + "pageSize", + "writeSize", + "memoryMapped", + "deviceFamily", + ]), + }; -function serializeCommonProtocolObjects(nvm: NVMJSON): NVM3Object[] { - const ret: NVM3Object[] = []; + const nodes = new Map(); + const getNode = (id: number): NVMJSONNode => { + if (!nodes.has(id)) nodes.set(id, createEmptyPhysicalNode()); + return nodes.get(id)!; + }; - const appRouteLock = new Set(); - const routeSlaveSUC = new Set(); - const sucPendingUpdate = new Set(); - const isVirtual = new Set(); - const pendingDiscovery = new Set(); + const lrNodes = new Map(); + const getLRNode = (id: number): NVMJSONLRNode => { + if (!lrNodes.has(id)) lrNodes.set(id, createEmptyLRNode()); + return lrNodes.get(id)!; + }; - for (const [id, node] of Object.entries(nvm.nodes)) { - const nodeId = parseInt(id); - if (!nodeHasInfo(node)) { - isVirtual.add(nodeId); - continue; - } else { - if (node.isVirtual) isVirtual.add(nodeId); - if (node.appRouteLock) appRouteLock.add(nodeId); - if (node.routeSlaveSUC) routeSlaveSUC.add(nodeId); - if (node.sucPendingUpdate) sucPendingUpdate.add(nodeId); - if (node.pendingDiscovery) pendingDiscovery.add(nodeId); - } - } + const protocolFileFormat = await adapter.get({ + domain: "controller", + type: "protocolFileFormat", + }, true); - ret.push( - new ControllerInfoFile( - nvmJSONControllerToFileOptions(nvm.controller), - ).serialize(), - ); - - ret.push( - new ProtocolAppRouteLockNodeMaskFile({ - nodeIds: [...appRouteLock], - fileVersion: nvm.controller.protocolVersion, - }).serialize(), - ); - ret.push( - new ProtocolRouteSlaveSUCNodeMaskFile({ - nodeIds: [...routeSlaveSUC], - fileVersion: nvm.controller.protocolVersion, - }).serialize(), - ); - ret.push( - new ProtocolSUCPendingUpdateNodeMaskFile({ - nodeIds: [...sucPendingUpdate], - fileVersion: nvm.controller.protocolVersion, - }).serialize(), - ); - ret.push( - new ProtocolVirtualNodeMaskFile({ - nodeIds: [...isVirtual], - fileVersion: nvm.controller.protocolVersion, - }).serialize(), - ); - ret.push( - new ProtocolPendingDiscoveryNodeMaskFile({ - nodeIds: [...pendingDiscovery], - fileVersion: nvm.controller.protocolVersion, - }).serialize(), - ); - - // TODO: format >= 2: { .key = FILE_ID_LRANGE_NODE_EXIST, .size = FILE_SIZE_LRANGE_NODE_EXIST, .name = "LRANGE_NODE_EXIST"}, - - if (nvm.controller.preferredRepeaters?.length) { - ret.push( - new ProtocolPreferredRepeatersFile({ - nodeIds: nvm.controller.preferredRepeaters, - fileVersion: nvm.controller.protocolVersion, - }).serialize(), + // Bail early if the NVM uses a protocol file format that's newer than we support + if (protocolFileFormat > MAX_PROTOCOL_FILE_FORMAT) { + throw new ZWaveError( + `Unsupported protocol file format: ${protocolFileFormat}`, + ZWaveErrorCodes.NVM_NotSupported, + { protocolFileFormat }, ); } - if (nvm.format < 5) { - ret.push( - new SUCUpdateEntriesFileV0({ - updateEntries: nvm.controller.sucUpdateEntries, - fileVersion: nvm.controller.protocolVersion, - }).serialize(), - ); - } else { - // V5 has split the SUC update entries into multiple files - for ( - let index = 0; - index < SUC_MAX_UPDATES; - index += SUC_UPDATES_PER_FILE_V5 - ) { - const slice = nvm.controller.sucUpdateEntries.slice( - index, - index + SUC_UPDATES_PER_FILE_V5, - ); - if (slice.length === 0) break; - const file = new SUCUpdateEntriesFileV5({ - updateEntries: slice, - fileVersion: nvm.controller.protocolVersion, - }); - file.fileId = sucUpdateIndexToSUCUpdateEntriesFileIDV5(index); - ret.push(file.serialize()); - } - } + const protocolVersion = await adapter.get({ + domain: "controller", + type: "protocolVersion", + }, true); - return ret; -} + // Read all flags for all nodes + const appRouteLock = new Set( + await adapter.get({ + domain: "controller", + type: "appRouteLock", + }, true), + ); + const routeSlaveSUC = new Set( + await adapter.get({ + domain: "controller", + type: "routeSlaveSUC", + }, true), + ); + const sucPendingUpdate = new Set( + await adapter.get({ + domain: "controller", + type: "sucPendingUpdate", + }, true), + ); + const virtualNodeIds = new Set( + await adapter.get({ + domain: "controller", + type: "virtualNodeIds", + }, true), + ); + const pendingDiscovery = new Set( + await adapter.get({ + domain: "controller", + type: "pendingDiscovery", + }, true), + ); -export function jsonToNVMObjects_v7_0_0( - json: NVMJSON, - targetSDKVersion: semver.SemVer, -): NVM3Objects { - const target = cloneDeep(json); + // Figure out which nodes exist + const nodeIds = await adapter.get({ + domain: "controller", + type: "nodeIds", + }, true); - target.controller.protocolVersion = "7.0.0"; - target.format = 0; - target.controller.applicationVersion = targetSDKVersion.format(); + // And create each node entry, including virtual ones + for (const id of nodeIds) { + const node = getNode(id) as NVMJSONNodeWithInfo; - const applicationObjects = new Map(); - const protocolObjects = new Map(); + // Find node info + const nodeInfo = await adapter.get({ + domain: "node", + nodeId: id, + type: "info", + }, true); - const addApplicationObjects = (...objects: NVM3Object[]) => { - for (const o of objects) { - applicationObjects.set(o.key, o); - } - }; - const addProtocolObjects = (...objects: NVM3Object[]) => { - for (const o of objects) { - protocolObjects.set(o.key, o); + Object.assign(node, nodeInfo); + + // Evaluate flags + node.isVirtual = virtualNodeIds.has(id); + node.appRouteLock = appRouteLock.has(id); + node.routeSlaveSUC = routeSlaveSUC.has(id); + node.sucPendingUpdate = sucPendingUpdate.has(id); + node.pendingDiscovery = pendingDiscovery.has(id); + + const routes = await adapter.get({ + domain: "node", + nodeId: id, + type: "routes", + }); + if (routes) { + node.lwr = routes.lwr; + node.nlwr = routes.nlwr; } - }; - // Application files - const applVersionFile = new ApplicationVersionFile({ - // The SDK compares 4-byte values where the format is set to 0 to determine whether a migration is needed - format: 0, - major: targetSDKVersion.major, - minor: targetSDKVersion.minor, - patch: targetSDKVersion.patch, - fileVersion: target.controller.applicationVersion, // does not matter for this file + // @ts-expect-error Some fields include a nodeId, but we don't need it + delete node.nodeId; + } + + // If they exist, read info about LR nodes + const lrNodeIds = await adapter.get({ + domain: "controller", + type: "lrNodeIds", }); - addApplicationObjects(applVersionFile.serialize()); + if (lrNodeIds) { + for (const id of lrNodeIds) { + const node = getLRNode(id); - addApplicationObjects(...serializeCommonApplicationObjects(target)); + // Find node info + const nodeInfo = await adapter.get({ + domain: "lrnode", + nodeId: id, + type: "info", + }, true); - // Protocol files + Object.assign(node, nodeInfo); + } + } - const protocolVersionFile = new ProtocolVersionFile({ - format: target.format, - major: 7, - minor: 0, - patch: 0, - fileVersion: target.controller.protocolVersion, // does not matter for this file + // Read info about the controller + const sucUpdateEntries = await adapter.get({ + domain: "controller", + type: "sucUpdateEntries", + }, true); + + const applicationVersion = await adapter.get({ + domain: "controller", + type: "applicationVersion", + }, true); + + const applicationFileFormat = await adapter.get({ + domain: "controller", + type: "applicationFileFormat", + }, true); + + const applicationData = await adapter.get({ + domain: "controller", + type: "applicationData", }); - addProtocolObjects(protocolVersionFile.serialize()); - const nodeInfoFiles = new Map(); - const routeCacheFiles = new Map(); - const nodeInfoExists = new Set(); - const routeCacheExists = new Set(); + const applicationName = await adapter.get({ + domain: "controller", + type: "applicationName", + }); - for (const [id, node] of Object.entries(target.nodes)) { - const nodeId = parseInt(id); - if (!nodeHasInfo(node)) continue; - - nodeInfoExists.add(nodeId); - - // Create/update node info file - const nodeInfoFileIndex = nodeIdToNodeInfoFileIDV0(nodeId); - nodeInfoFiles.set( - nodeInfoFileIndex, - new NodeInfoFileV0({ - nodeInfo: nvmJSONNodeToNodeInfo(nodeId, node), - fileVersion: target.controller.protocolVersion, - }), - ); + const preferredRepeaters = await adapter.get({ + domain: "controller", + type: "preferredRepeaters", + }); - // Create/update route cache file (if there is a route) - if (node.lwr || node.nlwr) { - routeCacheExists.add(nodeId); - - const routeCacheFileIndex = nodeIdToRouteCacheFileIDV0(nodeId); - routeCacheFiles.set( - routeCacheFileIndex, - new RouteCacheFileV0({ - routeCache: { - nodeId, - lwr: node.lwr ?? getEmptyRoute(), - nlwr: node.nlwr ?? getEmptyRoute(), - }, - fileVersion: target.controller.protocolVersion, - }), - ); - } + // The following are a bit awkward to read one by one, so we just take the files + const controllerInfoFile = await adapter.getFile( + ControllerInfoFileID, + true, + ); + const rfConfigFile = await adapter.getFile( + ApplicationRFConfigFileID, + ); + const applicationCCsFile = await adapter.getFile( + ApplicationCCsFileID, + true, + ); + const applicationTypeFile = await adapter.getFile( + ApplicationTypeFileID, + true, + ); + + const controller: NVMJSONController = { + protocolVersion, + applicationVersion, + homeId: `0x${controllerInfoFile.homeId.toString("hex")}`, + ...pick(controllerInfoFile, [ + "nodeId", + "lastNodeId", + "staticControllerNodeId", + "sucLastIndex", + "controllerConfiguration", + "sucAwarenessPushNeeded", + "maxNodeId", + "reservedId", + "systemState", + "lastNodeIdLR", + "maxNodeIdLR", + "reservedIdLR", + "primaryLongRangeChannelId", + "dcdcConfig", + ]), + ...pick(applicationTypeFile, [ + "isListening", + "optionalFunctionality", + "genericDeviceClass", + "specificDeviceClass", + ]), + commandClasses: pick(applicationCCsFile, [ + "includedInsecurely", + "includedSecurelyInsecureCCs", + "includedSecurelySecureCCs", + ]), + preferredRepeaters, + ...(rfConfigFile + ? { + rfConfig: { + rfRegion: rfConfigFile.rfRegion, + txPower: rfConfigFile.txPower, + measured0dBm: rfConfigFile.measured0dBm, + enablePTI: rfConfigFile.enablePTI ?? null, + maxTXPower: rfConfigFile.maxTXPower ?? null, + nodeIdType: rfConfigFile.nodeIdType ?? null, + }, + } + : {}), + sucUpdateEntries, + applicationData: applicationData?.toString("hex") ?? null, + applicationName: applicationName ?? null, + }; + + // Make sure all props are defined + const optionalControllerProps = [ + "sucAwarenessPushNeeded", + "lastNodeIdLR", + "maxNodeIdLR", + "reservedIdLR", + "primaryLongRangeChannelId", + "dcdcConfig", + "rfConfig", + "preferredRepeaters", + "applicationData", + ] as const; + for (const prop of optionalControllerProps) { + if (controller[prop] === undefined) controller[prop] = null; + } + + const ret: NVMJSONWithMeta = { + format: protocolFileFormat, + controller, + nodes: mapToObject(nodes), + meta, + }; + if (applicationFileFormat !== 0) { + ret.applicationFileFormat = applicationFileFormat; + } + if (lrNodes.size > 0) { + ret.lrNodes = mapToObject(lrNodes); } + return ret; +} - addProtocolObjects(...serializeCommonProtocolObjects(target)); +/** Reads an NVM buffer of a 500-series stick and returns its JSON representation */ +export async function nvm500ToJSON( + buffer: Buffer, +): Promise> { + const io = new NVMMemoryIO(buffer); + const nvm = new NVM500(io); + + const info = await nvm.init(); + const meta: NVM500Meta = { + library: info.library, + ...pick(info.nvmDescriptor, [ + "manufacturerID", + "firmwareID", + "productType", + "productID", + ]), + }; - addProtocolObjects( - new ProtocolNodeListFile({ - nodeIds: [...nodeInfoExists], - fileVersion: target.controller.protocolVersion, - }).serialize(), + const adapter = new NVM500Adapter(nvm); + + // Read all flags for all nodes + const appRouteLock = new Set( + await adapter.get({ + domain: "controller", + type: "appRouteLock", + }, true), ); - addProtocolObjects( - new ProtocolRouteCacheExistsNodeMaskFile({ - nodeIds: [...routeCacheExists], - fileVersion: target.controller.protocolVersion, - }).serialize(), + const routeSlaveSUC = new Set( + await adapter.get({ + domain: "controller", + type: "routeSlaveSUC", + }, true), + ); + const sucPendingUpdate = new Set( + await adapter.get({ + domain: "controller", + type: "sucPendingUpdate", + }, true), + ); + const virtualNodeIds = new Set( + await adapter.get({ + domain: "controller", + type: "virtualNodeIds", + }) ?? [], + ); + const pendingDiscovery = new Set( + await adapter.get({ + domain: "controller", + type: "pendingDiscovery", + }, true), ); - if (nodeInfoFiles.size > 0) { - addProtocolObjects( - ...[...nodeInfoFiles.values()].map((f) => f.serialize()), - ); + // Figure out which nodes exist along with their info + const nodes: Record = {}; + for (let nodeId = 1; nodeId <= MAX_NODES; nodeId++) { + const nodeInfo = await adapter.get({ + domain: "node", + nodeId, + type: "info", + }); + const isVirtual = virtualNodeIds.has(nodeId); + if (!nodeInfo) { + if (isVirtual) { + nodes[nodeId] = { isVirtual: true }; + } + continue; + } + + const routes = await adapter.get({ + domain: "node", + nodeId, + type: "routes", + }); + + // @ts-expect-error Some fields include a nodeId, but we don't need it + delete nodeInfo.nodeId; + + nodes[nodeId] = { + ...nodeInfo, + specificDeviceClass: nodeInfo.specificDeviceClass ?? null, + isVirtual, + + appRouteLock: appRouteLock.has(nodeId), + routeSlaveSUC: routeSlaveSUC.has(nodeId), + sucPendingUpdate: sucPendingUpdate.has(nodeId), + pendingDiscovery: pendingDiscovery.has(nodeId), + + lwr: routes?.lwr ?? null, + nlwr: routes?.nlwr ?? null, + }; } - if (routeCacheFiles.size > 0) { - addProtocolObjects( - ...[...routeCacheFiles.values()].map((f) => f.serialize()), - ); + + // Read info about the controller + const ownNodeId = await adapter.get({ + domain: "controller", + type: "nodeId", + }, true); + + const ownHomeId = await adapter.get({ + domain: "controller", + type: "homeId", + }, true); + + let learnedHomeId = await adapter.get({ + domain: "controller", + type: "learnedHomeId", + }); + if (learnedHomeId?.equals(Buffer.alloc(4, 0))) { + learnedHomeId = undefined; } + const lastNodeId = await adapter.get({ + domain: "controller", + type: "lastNodeId", + }, true); + + const maxNodeId = await adapter.get({ + domain: "controller", + type: "maxNodeId", + }, true); + + const reservedId = await adapter.get({ + domain: "controller", + type: "reservedId", + }, true); + + const staticControllerNodeId = await adapter.get({ + domain: "controller", + type: "staticControllerNodeId", + }, true); + + const sucLastIndex = await adapter.get({ + domain: "controller", + type: "sucLastIndex", + }, true); + + const controllerConfiguration = await adapter.get({ + domain: "controller", + type: "controllerConfiguration", + }, true); + + const commandClasses = await adapter.get({ + domain: "controller", + type: "commandClasses", + }, true); + + const sucUpdateEntries = await adapter.get({ + domain: "controller", + type: "sucUpdateEntries", + }, true); + + const applicationData = await adapter.get({ + domain: "controller", + type: "applicationData", + }); + + const preferredRepeaters = await adapter.get({ + domain: "controller", + type: "preferredRepeaters", + }, true); + + const systemState = await adapter.get({ + domain: "controller", + type: "systemState", + }, true); + + const watchdogStarted = await adapter.get({ + domain: "controller", + type: "watchdogStarted", + }, true); + + const powerLevelNormal = await adapter.get({ + domain: "controller", + type: "powerLevelNormal", + }, true); + const powerLevelLow = await adapter.get({ + domain: "controller", + type: "powerLevelLow", + }, true); + const powerMode = await adapter.get({ + domain: "controller", + type: "powerMode", + }, true); + const powerModeExtintEnable = await adapter.get({ + domain: "controller", + type: "powerModeExtintEnable", + }, true); + const powerModeWutTimeout = await adapter.get({ + domain: "controller", + type: "powerModeWutTimeout", + }, true); + + const controller: NVM500JSONController = { + protocolVersion: info.nvmDescriptor.protocolVersion, + applicationVersion: info.nvmDescriptor.firmwareVersion, + ownHomeId: `0x${ownHomeId.toString("hex")}`, + learnedHomeId: learnedHomeId + ? `0x${learnedHomeId.toString("hex")}` + : null, + nodeId: ownNodeId, + lastNodeId, + staticControllerNodeId, + sucLastIndex, + controllerConfiguration, + sucUpdateEntries, + maxNodeId, + reservedId, + systemState, + watchdogStarted, + rfConfig: { + powerLevelNormal, + powerLevelLow, + powerMode, + powerModeExtintEnable, + powerModeWutTimeout, + }, + preferredRepeaters, + commandClasses, + applicationData: applicationData?.toString("hex") ?? null, + }; + return { - applicationObjects, - protocolObjects, + format: 500, + meta, + controller, + nodes, }; } -export function jsonToNVMObjects_v7_11_0( +export async function jsonToNVM( json: NVMJSON, - targetSDKVersion: semver.SemVer, -): NVM3Objects { - const target = cloneDeep(json); + targetSDKVersion: string, +): Promise { + const parsedVersion = semver.parse(targetSDKVersion); + if (!parsedVersion) { + throw new ZWaveError( + `Invalid SDK version: ${targetSDKVersion}`, + ZWaveErrorCodes.Argument_Invalid, + ); + } + + // Erase the NVM + const sharedFileSystem = json.meta?.sharedFileSystem; + const nvmSize = sharedFileSystem + ? ZWAVE_SHARED_NVM_SIZE + : (ZWAVE_APPLICATION_NVM_SIZE + ZWAVE_PROTOCOL_NVM_SIZE); + const ret = Buffer.allocUnsafe(nvmSize); + const io = new NVMMemoryIO(ret); + const nvm3 = new NVM3(io); + await nvm3.erase(json.meta); + + const serializeFile = async (file: NVMFile) => { + const { key, data } = file.serialize(); + await nvm3.set(key, data); + }; + // Figure out which SDK version we are targeting let targetApplicationVersion: semver.SemVer; let targetProtocolVersion: semver.SemVer; let targetProtocolFormat: number; @@ -999,43 +1261,72 @@ export function jsonToNVMObjects_v7_11_0( } else if (semver.gte(targetSDKVersion, "7.12.0")) { targetProtocolVersion = semver.parse("7.12.0")!; targetProtocolFormat = 2; - } else { - // All versions below 7.11.0 are handled in the _v7_0_0 method + } else if (semver.gte(targetSDKVersion, "7.11.0")) { targetProtocolVersion = semver.parse("7.11.0")!; targetProtocolFormat = 1; + } else { + targetProtocolVersion = semver.parse("7.0.0")!; + targetProtocolFormat = 0; } - target.format = targetProtocolFormat; - target.controller.applicationVersion = targetApplicationVersion.format(); + const target = cloneDeep(json); target.controller.protocolVersion = targetProtocolVersion.format(); + target.format = targetProtocolFormat; + target.controller.applicationVersion = parsedVersion.format(); - const applicationObjects = new Map(); - const protocolObjects = new Map(); - - const addApplicationObjects = (...objects: NVM3Object[]) => { - for (const o of objects) { - applicationObjects.set(o.key, o); - } - }; - const addProtocolObjects = (...objects: NVM3Object[]) => { - for (const o of objects) { - protocolObjects.set(o.key, o); - } - }; - - // Application files - const ApplicationVersionConstructor = json.meta?.sharedFileSystem + // Write application and protocol version files, because they are required + // for the NVM3 adapter to work. + const ApplicationVersionConstructor = sharedFileSystem ? ApplicationVersionFile800 : ApplicationVersionFile; const applVersionFile = new ApplicationVersionConstructor({ - // The SDK compares 4-byte values where the format is set to 0 to determine whether a migration is needed format: 0, major: targetApplicationVersion.major, minor: targetApplicationVersion.minor, patch: targetApplicationVersion.patch, - fileVersion: target.controller.applicationVersion, // does not matter for this file + fileVersion: targetProtocolVersion.format(), // does not matter for this file }); - addApplicationObjects(applVersionFile.serialize()); + await serializeFile(applVersionFile); + + const protocolVersionFile = new ProtocolVersionFile({ + format: targetProtocolFormat, + major: targetProtocolVersion.major, + minor: targetProtocolVersion.minor, + patch: targetProtocolVersion.patch, + fileVersion: targetProtocolVersion.format(), // does not matter for this file + }); + await serializeFile(protocolVersionFile); + { + const { key, data } = protocolVersionFile.serialize(); + await nvm3.set(key, data); + } + + // Now use the adapter where possible. Some properties have to be set together though, + // so we set the files directly + const adapter = new NVM3Adapter(nvm3); + + // Start with the application data + + const applTypeFile = new ApplicationTypeFile({ + ...pick(target.controller, [ + "isListening", + "optionalFunctionality", + "genericDeviceClass", + "specificDeviceClass", + ]), + fileVersion: target.controller.applicationVersion, + }); + adapter.setFile(applTypeFile); + + const applCCsFile = new ApplicationCCsFile({ + ...pick(target.controller.commandClasses, [ + "includedInsecurely", + "includedSecurelyInsecureCCs", + "includedSecurelySecureCCs", + ]), + fileVersion: target.controller.applicationVersion, + }); + adapter.setFile(applCCsFile); // When converting it can be that the rfConfig doesn't exist. Make sure // that it is initialized with proper defaults. @@ -1058,93 +1349,104 @@ export function jsonToNVMObjects_v7_11_0( target.controller.rfConfig.nodeIdType ??= NodeIDType.Short; } - addApplicationObjects(...serializeCommonApplicationObjects(target)); + const applRFConfigFile = new ApplicationRFConfigFile({ + ...pick(target.controller.rfConfig, [ + "rfRegion", + "txPower", + "measured0dBm", + ]), + enablePTI: target.controller.rfConfig.enablePTI ?? undefined, + maxTXPower: target.controller.rfConfig.maxTXPower ?? undefined, + nodeIdType: target.controller.rfConfig.nodeIdType ?? undefined, + fileVersion: target.controller.applicationVersion, + }); + adapter.setFile(applRFConfigFile); - // Protocol files + if (target.controller.applicationData) { + await adapter.set( + { domain: "controller", type: "applicationData" }, + Buffer.from(target.controller.applicationData, "hex"), + ); + } - const protocolVersionFile = new ProtocolVersionFile({ - format: targetProtocolFormat, - major: targetProtocolVersion.major, - minor: targetProtocolVersion.minor, - patch: targetProtocolVersion.patch, - fileVersion: target.controller.protocolVersion, // does not matter for this file - }); - addProtocolObjects(protocolVersionFile.serialize()); + // The application name only seems to be used on 800 series with the shared file system + if (target.controller.applicationName && target.meta?.sharedFileSystem) { + await adapter.set( + { domain: "controller", type: "applicationName" }, + target.controller.applicationName, + ); + } + + // Now the protocol data - const nodeInfoFiles = new Map(); - const lrNodeInfoFiles = new Map(); - const routeCacheFiles = new Map(); + // TODO: node IDs and LR node IDs should probably be handled by the NVM adapter when + // setting the node info. But then we need to make sure here that the files are guaranteed to exist const nodeInfoExists = new Set(); const lrNodeInfoExists = new Set(); - const routeCacheExists = new Set(); + const virtualNodeIds = new Set(); + + const appRouteLock = new Set(); + const routeSlaveSUC = new Set(); + const sucPendingUpdate = new Set(); + const pendingDiscovery = new Set(); + + // Ensure that the route cache exists nodemask is written, even when no routes exist + adapter.setFile( + new ProtocolRouteCacheExistsNodeMaskFile({ + nodeIds: [], + fileVersion: target.controller.protocolVersion, + }), + ); for (const [id, node] of Object.entries(target.nodes)) { const nodeId = parseInt(id); - if (!nodeHasInfo(node)) continue; - - nodeInfoExists.add(nodeId); - - // Create/update node info file - const nodeInfoFileIndex = nodeIdToNodeInfoFileIDV1(nodeId); - if (!nodeInfoFiles.has(nodeInfoFileIndex)) { - nodeInfoFiles.set( - nodeInfoFileIndex, - new NodeInfoFileV1({ - nodeInfos: [], - fileVersion: target.controller.protocolVersion, - }), - ); + if (!nodeHasInfo(node)) { + virtualNodeIds.add(nodeId); + continue; + } else { + nodeInfoExists.add(nodeId); + if (node.isVirtual) virtualNodeIds.add(nodeId); + if (node.appRouteLock) appRouteLock.add(nodeId); + if (node.routeSlaveSUC) routeSlaveSUC.add(nodeId); + if (node.sucPendingUpdate) sucPendingUpdate.add(nodeId); + if (node.pendingDiscovery) pendingDiscovery.add(nodeId); } - const nodeInfoFile = nodeInfoFiles.get(nodeInfoFileIndex)!; - nodeInfoFile.nodeInfos.push(nvmJSONNodeToNodeInfo(nodeId, node)); + await adapter.set( + { domain: "node", nodeId, type: "info" }, + nvmJSONNodeToNodeInfo(nodeId, node), + ); - // Create/update route cache file (if there is a route) if (node.lwr || node.nlwr) { - routeCacheExists.add(nodeId); - const routeCacheFileIndex = nodeIdToRouteCacheFileIDV1(nodeId); - if (!routeCacheFiles.has(routeCacheFileIndex)) { - routeCacheFiles.set( - routeCacheFileIndex, - new RouteCacheFileV1({ - routeCaches: [], - fileVersion: target.controller.protocolVersion, - }), - ); - } - const routeCacheFile = routeCacheFiles.get(routeCacheFileIndex)!; - routeCacheFile.routeCaches.push({ - nodeId, - lwr: node.lwr ?? getEmptyRoute(), - nlwr: node.nlwr ?? getEmptyRoute(), - }); + await adapter.set( + { domain: "node", nodeId, type: "routes" }, + { + lwr: node.lwr ?? getEmptyRoute(), + nlwr: node.nlwr ?? getEmptyRoute(), + }, + ); } } + await adapter.set( + { domain: "controller", type: "nodeIds" }, + [...nodeInfoExists], + ); if (target.lrNodes) { for (const [id, node] of Object.entries(target.lrNodes)) { const nodeId = parseInt(id); - lrNodeInfoExists.add(nodeId); - // Create/update node info file - const nodeInfoFileIndex = nodeIdToLRNodeInfoFileIDV5(nodeId); - if (!lrNodeInfoFiles.has(nodeInfoFileIndex)) { - lrNodeInfoFiles.set( - nodeInfoFileIndex, - new LRNodeInfoFileV5({ - nodeInfos: [], - fileVersion: target.controller.protocolVersion, - }), - ); - } - const nodeInfoFile = lrNodeInfoFiles.get(nodeInfoFileIndex)!; - - nodeInfoFile.nodeInfos.push( + await adapter.set( + { domain: "lrnode", nodeId, type: "info" }, nvmJSONLRNodeToLRNodeInfo(nodeId, node), ); } } + await adapter.set( + { domain: "controller", type: "lrNodeIds" }, + [...lrNodeInfoExists], + ); // For v3+ targets, the ControllerInfoFile must contain the LongRange properties // or the controller will ignore the file and not have a home ID @@ -1155,129 +1457,57 @@ export function jsonToNVMObjects_v7_11_0( target.controller.primaryLongRangeChannelId ??= 0; target.controller.dcdcConfig ??= 255; } + adapter.setFile( + new ControllerInfoFile( + nvmJSONControllerToFileOptions(target.controller), + ), + ); - addProtocolObjects(...serializeCommonProtocolObjects(target)); - - addProtocolObjects( - new ProtocolNodeListFile({ - nodeIds: [...nodeInfoExists], - fileVersion: target.controller.protocolVersion, - }).serialize(), + await adapter.set( + { domain: "controller", type: "appRouteLock" }, + [...appRouteLock], ); - addProtocolObjects( - new ProtocolRouteCacheExistsNodeMaskFile({ - nodeIds: [...routeCacheExists], - fileVersion: target.controller.protocolVersion, - }).serialize(), + await adapter.set( + { domain: "controller", type: "routeSlaveSUC" }, + [...routeSlaveSUC], + ); + await adapter.set( + { domain: "controller", type: "sucPendingUpdate" }, + [...sucPendingUpdate], + ); + await adapter.set( + { domain: "controller", type: "virtualNodeIds" }, + [...virtualNodeIds], + ); + await adapter.set( + { domain: "controller", type: "pendingDiscovery" }, + [...pendingDiscovery], ); - if (nodeInfoFiles.size > 0) { - addProtocolObjects( - ...[...nodeInfoFiles.values()].map((f) => f.serialize()), - ); - } - if (routeCacheFiles.size > 0) { - addProtocolObjects( - ...[...routeCacheFiles.values()].map((f) => f.serialize()), - ); - } - - if (lrNodeInfoFiles.size > 0) { - addProtocolObjects( - new ProtocolLRNodeListFile({ - nodeIds: [...lrNodeInfoExists], - fileVersion: target.controller.protocolVersion, - }).serialize(), - ); - addProtocolObjects( - ...[...lrNodeInfoFiles.values()].map((f) => f.serialize()), - ); - } - - return { - applicationObjects, - protocolObjects, - }; -} - -/** Reads an NVM buffer and returns its JSON representation */ -export function nvmToJSON( - buffer: Buffer, - debugLogs: boolean = false, -): Required { - const nvm = parseNVM(buffer, debugLogs); - return parsedNVMToJSON(nvm); -} - -function parsedNVMToJSON( - nvm: NVM3Pages & NVM3Objects, -): Required { - const objects = new Map([ - ...nvm.applicationObjects, - ...nvm.protocolObjects, - ]); - // 800 series doesn't distinguish between the storage for application and protocol objects - const sharedFileSystem = nvm.applicationObjects.size > 0 - && nvm.protocolObjects.size === 0; - const ret = nvmObjectsToJSON(objects); - const firstPage = sharedFileSystem - ? nvm.applicationPages[0] - : nvm.protocolPages[0]; - ret.meta = getNVMMeta(firstPage, sharedFileSystem); - return ret as Required; -} - -/** Reads an NVM buffer of a 500-series stick and returns its JSON representation */ -export function nvm500ToJSON(buffer: Buffer): Required { - const parser = createNVM500Parser(buffer); - if (!parser) { - throw new ZWaveError( - "Did not find a matching NVM 500 parser implementation! Make sure that the NVM data belongs to a controller with Z-Wave SDK 6.61 or higher.", - ZWaveErrorCodes.NVM_NotSupported, - ); - } - return parser.toJSON(); -} - -/** Takes a JSON represented NVM and converts it to binary */ -export function jsonToNVM( - json: Required, - targetSDKVersion: string, -): Buffer { - const parsedVersion = semver.parse(targetSDKVersion); - if (!parsedVersion) { - throw new ZWaveError( - `Invalid SDK version: ${targetSDKVersion}`, - ZWaveErrorCodes.Argument_Invalid, - ); - } - - let objects: NVM3Objects; - if (semver.gte(parsedVersion, "7.11.0")) { - objects = jsonToNVMObjects_v7_11_0(json, parsedVersion); - } else if (semver.gte(parsedVersion, "7.0.0")) { - objects = jsonToNVMObjects_v7_0_0(json, parsedVersion); - } else { - throw new ZWaveError( - "jsonToNVM cannot convert to a pre-7.0 NVM version. Use jsonToNVM500 instead.", - ZWaveErrorCodes.Argument_Invalid, + if (target.controller.preferredRepeaters?.length) { + await adapter.set( + { domain: "controller", type: "preferredRepeaters" }, + target.controller.preferredRepeaters, ); } - return encodeNVM( - objects.applicationObjects, - objects.protocolObjects, - json.meta, + await adapter.set( + { domain: "controller", type: "sucUpdateEntries" }, + target.controller.sucUpdateEntries, ); + + await adapter.commit(); + await io.close(); + return ret; } /** Takes a JSON represented 500 series NVM and converts it to binary */ -export function jsonToNVM500( +export async function jsonToNVM500( json: Required, protocolVersion: string, -): Buffer { +): Promise { // Try to find a matching implementation - const impl = nmvDetails500.find( + const impl = nvm500Impls.find( (p) => p.protocolVersions.includes(protocolVersion) && p.name.toLowerCase().startsWith(json.meta.library), @@ -1290,9 +1520,190 @@ export function jsonToNVM500( ); } - const serializer = new NVMSerializer(impl); - serializer.parseJSON(json, protocolVersion); - return serializer.serialize(); + const { layout, nvmSize } = resolveLayout(impl.layout); + + // Erase the NVM and set some basic information + const ret = Buffer.allocUnsafe(nvmSize); + const io = new NVMMemoryIO(ret); + const nvm = new NVM500(io); + await nvm.erase({ + layout, + nvmSize, + library: impl.library, + nvmDescriptor: { + ...pick(json.meta, [ + "manufacturerID", + "productType", + "productID", + "firmwareID", + ]), + // Override the protocol version with the specified one + protocolVersion, + firmwareVersion: json.controller.applicationVersion, + }, + }); + + const adapter = new NVM500Adapter(nvm); + + // Set controller infos + const c = json.controller; + + await adapter.set( + { domain: "controller", type: "homeId" }, + Buffer.from(c.ownHomeId.replace(/^0x/, ""), "hex"), + ); + await adapter.set( + { domain: "controller", type: "learnedHomeId" }, + c.learnedHomeId + ? Buffer.from(c.learnedHomeId.replace(/^0x/, ""), "hex") + : undefined, + ); + + await adapter.set( + { domain: "controller", type: "nodeId" }, + c.nodeId, + ); + await adapter.set( + { domain: "controller", type: "lastNodeId" }, + c.lastNodeId, + ); + await adapter.set( + { domain: "controller", type: "maxNodeId" }, + c.maxNodeId, + ); + await adapter.set( + { domain: "controller", type: "reservedId" }, + c.reservedId, + ); + await adapter.set( + { domain: "controller", type: "staticControllerNodeId" }, + c.staticControllerNodeId, + ); + + await adapter.set( + { domain: "controller", type: "controllerConfiguration" }, + c.controllerConfiguration, + ); + + await adapter.set( + { domain: "controller", type: "sucUpdateEntries" }, + c.sucUpdateEntries, + ); + + await adapter.set( + { domain: "controller", type: "sucLastIndex" }, + c.sucLastIndex, + ); + + await adapter.set( + { domain: "controller", type: "systemState" }, + c.systemState, + ); + + await adapter.set( + { domain: "controller", type: "watchdogStarted" }, + c.watchdogStarted, + ); + + await adapter.set( + { domain: "controller", type: "powerLevelNormal" }, + c.rfConfig.powerLevelNormal, + ); + await adapter.set( + { domain: "controller", type: "powerLevelLow" }, + c.rfConfig.powerLevelLow, + ); + await adapter.set( + { domain: "controller", type: "powerMode" }, + c.rfConfig.powerMode, + ); + await adapter.set( + { domain: "controller", type: "powerModeExtintEnable" }, + c.rfConfig.powerModeExtintEnable, + ); + await adapter.set( + { domain: "controller", type: "powerModeWutTimeout" }, + c.rfConfig.powerModeWutTimeout, + ); + + await adapter.set( + { domain: "controller", type: "preferredRepeaters" }, + c.preferredRepeaters, + ); + + await adapter.set( + { domain: "controller", type: "commandClasses" }, + c.commandClasses, + ); + + if (c.applicationData) { + await adapter.set( + { domain: "controller", type: "applicationData" }, + Buffer.from(c.applicationData, "hex"), + ); + } + + // Set node infos + const appRouteLock: number[] = []; + const routeSlaveSUC: number[] = []; + const pendingDiscovery: number[] = []; + const sucPendingUpdate: number[] = []; + const virtualNodeIds: number[] = []; + + for (const [id, node] of Object.entries(json.nodes)) { + const nodeId = parseInt(id); + if (!nodeHasInfo(node)) { + virtualNodeIds.push(nodeId); + continue; + } + if (node.appRouteLock) appRouteLock.push(nodeId); + if (node.routeSlaveSUC) routeSlaveSUC.push(nodeId); + if (node.pendingDiscovery) pendingDiscovery.push(nodeId); + if (node.sucPendingUpdate) sucPendingUpdate.push(nodeId); + + await adapter.set( + { domain: "node", nodeId, type: "info" }, + { + nodeId, + ...node, + }, + ); + + if (node.lwr || node.nlwr) { + await adapter.set( + { domain: "node", nodeId, type: "routes" }, + { + lwr: node.lwr ?? undefined, + nlwr: node.nlwr ?? undefined, + }, + ); + } + } + + await adapter.set( + { domain: "controller", type: "appRouteLock" }, + [...appRouteLock], + ); + await adapter.set( + { domain: "controller", type: "routeSlaveSUC" }, + [...routeSlaveSUC], + ); + await adapter.set( + { domain: "controller", type: "sucPendingUpdate" }, + [...sucPendingUpdate], + ); + await adapter.set( + { domain: "controller", type: "virtualNodeIds" }, + [...virtualNodeIds], + ); + await adapter.set( + { domain: "controller", type: "pendingDiscovery" }, + [...pendingDiscovery], + ); + + await adapter.commit(); + await io.close(); + return ret; } export function json500To700( @@ -1487,30 +1898,27 @@ export function json700To500(json: NVMJSON): NVM500JSON { } /** Converts the given source NVM into a format that is compatible with the given target NVM */ -export function migrateNVM(sourceNVM: Buffer, targetNVM: Buffer): Buffer { +export async function migrateNVM( + sourceNVM: Buffer, + targetNVM: Buffer, +): Promise { let source: ParsedNVM; let target: ParsedNVM; - let sourceObjects: Map | undefined; let sourceProtocolFileFormat: number | undefined; let targetProtocolFileFormat: number | undefined; try { - const nvm = parseNVM(sourceNVM); source = { type: 700, - json: parsedNVMToJSON(nvm), + json: await nvmToJSON(sourceNVM), }; sourceProtocolFileFormat = source.json.format; - sourceObjects = new Map([ - ...nvm.applicationObjects, - ...nvm.protocolObjects, - ]); } catch (e) { if (isZWaveError(e) && e.code === ZWaveErrorCodes.NVM_InvalidFormat) { // This is not a 700 series NVM, maybe it is a 500 series one? source = { type: 500, - json: nvm500ToJSON(sourceNVM), + json: await nvm500ToJSON(sourceNVM), }; } else if ( isZWaveError(e) @@ -1529,7 +1937,7 @@ export function migrateNVM(sourceNVM: Buffer, targetNVM: Buffer): Buffer { try { target = { type: 700, - json: nvmToJSON(targetNVM), + json: await nvmToJSON(targetNVM), }; targetProtocolFileFormat = target.json.format; } catch (e) { @@ -1537,7 +1945,7 @@ export function migrateNVM(sourceNVM: Buffer, targetNVM: Buffer): Buffer { // This is not a 700 series NVM, maybe it is a 500 series one? target = { type: 500, - json: nvm500ToJSON(targetNVM), + json: await nvm500ToJSON(targetNVM), }; } else if ( isZWaveError(e) @@ -1597,7 +2005,7 @@ export function migrateNVM(sourceNVM: Buffer, targetNVM: Buffer): Buffer { && semver.lt(sourceApplicationVersion, "255.0.0") && semver.lt(targetApplicationVersion, "255.0.0") // and avoid restoring a backup with a shifted 800 series application version file - && (!sourceObjects || !hasShiftedAppVersion800File(sourceObjects)) + && (!hasShiftedAppVersion800File(source.json)) ) { return sourceNVM; } @@ -1647,7 +2055,7 @@ export function migrateNVM(sourceNVM: Buffer, targetNVM: Buffer): Buffer { return jsonToNVM500(json, target.json.controller.protocolVersion); } else if (source.type === 500 && target.type === 700) { // We need to upgrade the source to 700 series - const json: Required = { + const json: NVMJSONWithMeta = { lrNodes: {}, ...json500To700(source.json, true), meta: target.json.meta, @@ -1667,9 +2075,9 @@ export function migrateNVM(sourceNVM: Buffer, targetNVM: Buffer): Buffer { return jsonToNVM500(json, target.json.controller.protocolVersion); } else { // Both are 700, so we just need to update the metadata to match the target - const json: Required = { - ...(source.json as Required), - meta: (target.json as Required).meta, + const json: NVMJSONWithMeta = { + ...(source.json as NVMJSONWithMeta), + meta: (target.json as NVMJSONWithMeta).meta, }; // 700 series distinguishes the NVM format by the application version return jsonToNVM(json, target.json.controller.applicationVersion); @@ -1680,53 +2088,25 @@ export function migrateNVM(sourceNVM: Buffer, targetNVM: Buffer): Buffer { * Detects whether the app version file on a 800 series controller is shifted by 1 byte */ function hasShiftedAppVersion800File( - objects: Map, + json: NVMJSONWithMeta, ): boolean { - const getObject = ( - id: number | ((id: number) => boolean), - ): NVM3Object | undefined => { - if (typeof id === "number") { - return objects.get(id); - } else { - for (const [key, obj] of objects) { - if (id(key)) return obj; - } - } - }; + // We can only detect this on 800 series controllers with the shared FS + if (!json.meta.sharedFileSystem) return false; - const getFile = ( - id: number | ((id: number) => boolean), - fileVersion: string, - ): T | undefined => { - const obj = getObject(id); - if (!obj) return undefined; - return NVMFile.from(obj, fileVersion) as T; - }; - - const protocolVersionFile = getFile( - ProtocolVersionFileID, - "7.0.0", // We don't know the version here yet - ); - // File not found, cannot fix anything - if (!protocolVersionFile) return false; - - const protocolVersion = - `${protocolVersionFile.major}.${protocolVersionFile.minor}.${protocolVersionFile.patch}`; - - const applVersionFile800 = getFile( - ApplicationVersionFile800ID, - protocolVersion, - ); + const protocolVersion = semver.parse(json.controller.protocolVersion); + // Invalid protocol version, cannot fix anything + if (!protocolVersion) return false; - // File not found, cannot fix anything - if (!applVersionFile800) return false; + const applicationVersion = semver.parse(json.controller.applicationVersion); + // Invalid application version, cannot fix anything + if (!applicationVersion) return false; // We consider the version shifted if: // - the app version format is the major protocol version // - the app version major is the minor protocol version +/- 3 - if (applVersionFile800.format !== protocolVersionFile.major) return false; - if (Math.abs(applVersionFile800.major - protocolVersionFile.minor) > 3) { + if (json.applicationFileFormat !== protocolVersion.major) return false; + if (Math.abs(applicationVersion.major - protocolVersion.minor) > 3) { return false; } diff --git a/packages/nvmedit/src/files/ProtocolNodeMaskFiles.ts b/packages/nvmedit/src/files/ProtocolNodeMaskFiles.ts deleted file mode 100644 index 7c9b385b960..00000000000 --- a/packages/nvmedit/src/files/ProtocolNodeMaskFiles.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { NODE_ID_MAX, encodeBitMask, parseBitMask } from "@zwave-js/core/safe"; -import type { NVM3Object } from "../nvm3/object"; -import { - NVMFile, - type NVMFileCreationOptions, - type NVMFileDeserializationOptions, - getNVMFileIDStatic, - gotDeserializationOptions, - nvmFileID, -} from "./NVMFile"; - -export interface ProtocolNodeMaskFileOptions extends NVMFileCreationOptions { - nodeIds: number[]; -} - -export class ProtocolNodeMaskFile extends NVMFile { - public constructor( - options: NVMFileDeserializationOptions | ProtocolNodeMaskFileOptions, - ) { - super(options); - if (gotDeserializationOptions(options)) { - this.nodeIds = parseBitMask(this.payload); - } else { - this.nodeIds = options.nodeIds; - } - } - - public nodeIds: number[]; - - public serialize(): NVM3Object { - this.payload = encodeBitMask(this.nodeIds, NODE_ID_MAX); - return super.serialize(); - } - - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - public toJSON() { - return { - ...super.toJSON(), - "node IDs": this.nodeIds.join(", "), - }; - } -} - -@nvmFileID(0x50002) -export class ProtocolPreferredRepeatersFile extends ProtocolNodeMaskFile {} -export const ProtocolPreferredRepeatersFileID = getNVMFileIDStatic( - ProtocolPreferredRepeatersFile, -); - -@nvmFileID(0x50005) -export class ProtocolNodeListFile extends ProtocolNodeMaskFile {} -export const ProtocolNodeListFileID = getNVMFileIDStatic(ProtocolNodeListFile); - -@nvmFileID(0x50006) -export class ProtocolAppRouteLockNodeMaskFile extends ProtocolNodeMaskFile {} -export const ProtocolAppRouteLockNodeMaskFileID = getNVMFileIDStatic( - ProtocolAppRouteLockNodeMaskFile, -); - -@nvmFileID(0x50007) -export class ProtocolRouteSlaveSUCNodeMaskFile extends ProtocolNodeMaskFile {} -export const ProtocolRouteSlaveSUCNodeMaskFileID = getNVMFileIDStatic( - ProtocolRouteSlaveSUCNodeMaskFile, -); - -@nvmFileID(0x50008) -export class ProtocolSUCPendingUpdateNodeMaskFile - extends ProtocolNodeMaskFile -{} -export const ProtocolSUCPendingUpdateNodeMaskFileID = getNVMFileIDStatic( - ProtocolSUCPendingUpdateNodeMaskFile, -); - -@nvmFileID(0x50009) -export class ProtocolVirtualNodeMaskFile extends ProtocolNodeMaskFile {} -export const ProtocolVirtualNodeMaskFileID = getNVMFileIDStatic( - ProtocolVirtualNodeMaskFile, -); - -@nvmFileID(0x5000a) -export class ProtocolPendingDiscoveryNodeMaskFile - extends ProtocolNodeMaskFile -{} -export const ProtocolPendingDiscoveryNodeMaskFileID = getNVMFileIDStatic( - ProtocolPendingDiscoveryNodeMaskFile, -); - -@nvmFileID(0x5000b) -export class ProtocolRouteCacheExistsNodeMaskFile - extends ProtocolNodeMaskFile -{} -export const ProtocolRouteCacheExistsNodeMaskFileID = getNVMFileIDStatic( - ProtocolRouteCacheExistsNodeMaskFile, -); - -@nvmFileID(0x5000c) -export class ProtocolLRNodeListFile extends NVMFile { - public constructor( - options: NVMFileDeserializationOptions | ProtocolNodeMaskFileOptions, - ) { - super(options); - if (gotDeserializationOptions(options)) { - this.nodeIds = parseBitMask(this.payload, 256); - } else { - this.nodeIds = options.nodeIds; - } - } - - public nodeIds: number[]; - - public serialize(): NVM3Object { - // There are only 128 bytes for the bitmask, so the LR node IDs only go up to 1279 - this.payload = encodeBitMask(this.nodeIds, 1279, 256); - return super.serialize(); - } - - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - public toJSON() { - return { - ...super.toJSON(), - payload: this.payload.toString("hex"), - "node IDs": this.nodeIds.join(", "), - }; - } -} -export const ProtocolLRNodeListFileID = getNVMFileIDStatic( - ProtocolLRNodeListFile, -); diff --git a/packages/nvmedit/src/index.ts b/packages/nvmedit/src/index.ts index 733263ae845..5b92de7b5d9 100644 --- a/packages/nvmedit/src/index.ts +++ b/packages/nvmedit/src/index.ts @@ -17,18 +17,39 @@ export type { NVMJSONNodeWithInfo, NVMJSONVirtualNode, } from "./convert"; +export { NVM3, type NVM3EraseOptions, type NVM3Meta } from "./lib/NVM3"; +export { NVM500, type NVM500EraseOptions, type NVM500Info } from "./lib/NVM500"; +export { NVMAccess } from "./lib/common/definitions"; +export type { + ControllerNVMProperty, + ControllerNVMPropertyToDataType, + LRNodeNVMProperty, + LRNodeNVMPropertyToDataType, + NVM, + NVMAdapter, + NVMIO, + NVMProperty, + NVMPropertyToDataType, + NodeNVMProperty, + NodeNVMPropertyToDataType, +} from "./lib/common/definitions"; +export { BufferedNVMReader } from "./lib/io/BufferedNVMReader"; +export { NVMFileIO } from "./lib/io/NVMFileIO"; +export { NVM3Adapter } from "./lib/nvm3/adapter"; export { FragmentType, ObjectType, PageStatus, PageWriteSize, -} from "./nvm3/consts"; -export type { NVMMeta } from "./nvm3/nvm"; -export type { NVM3Object as NVMObject } from "./nvm3/object"; -export type { - NVM3Page as NVMPage, - NVM3PageHeader as PageHeader, -} from "./nvm3/page"; +} from "./lib/nvm3/consts"; +export { + ControllerInfoFile, + ControllerInfoFileID, + NVMFile, +} from "./lib/nvm3/files"; +export type { NVM3Object } from "./lib/nvm3/object"; +export type { NVM3Page, NVM3PageHeader } from "./lib/nvm3/page"; +export { NVM500Adapter } from "./lib/nvm500/adapter"; export type { NVM500JSON, NVM500JSONController, diff --git a/packages/nvmedit/src/index_safe.ts b/packages/nvmedit/src/index_safe.ts index 9abfad738f4..fcd7e330e67 100644 --- a/packages/nvmedit/src/index_safe.ts +++ b/packages/nvmedit/src/index_safe.ts @@ -10,18 +10,30 @@ export type { NVMJSONNodeWithInfo, NVMJSONVirtualNode, } from "./convert"; +export type { NVM3EraseOptions, NVM3Meta } from "./lib/NVM3"; +export type { NVM500EraseOptions, NVM500Info } from "./lib/NVM500"; +export { NVMAccess } from "./lib/common/definitions"; +export type { + ControllerNVMProperty, + ControllerNVMPropertyToDataType, + LRNodeNVMProperty, + LRNodeNVMPropertyToDataType, + NVM, + NVMAdapter, + NVMIO, + NVMProperty, + NVMPropertyToDataType, + NodeNVMProperty, + NodeNVMPropertyToDataType, +} from "./lib/common/definitions"; export { FragmentType, ObjectType, PageStatus, PageWriteSize, -} from "./nvm3/consts"; -export type { NVMMeta } from "./nvm3/nvm"; -export type { NVM3Object as NVMObject } from "./nvm3/object"; -export type { - NVM3Page as NVMPage, - NVM3PageHeader as PageHeader, -} from "./nvm3/page"; +} from "./lib/nvm3/consts"; +export type { NVM3Object } from "./lib/nvm3/object"; +export type { NVM3Page, NVM3PageHeader } from "./lib/nvm3/page"; export type { NVM500JSON, NVM500JSONController, diff --git a/packages/nvmedit/src/lib/NVM3.ts b/packages/nvmedit/src/lib/NVM3.ts new file mode 100644 index 00000000000..bb675ab1502 --- /dev/null +++ b/packages/nvmedit/src/lib/NVM3.ts @@ -0,0 +1,891 @@ +import { ZWaveError, ZWaveErrorCodes } from "@zwave-js/core"; +import { getEnumMemberName, num2hex } from "@zwave-js/shared"; +import { type NVM, NVMAccess, type NVMIO } from "./common/definitions"; +import { nvmReadBuffer, nvmReadUInt32LE, nvmWriteBuffer } from "./common/utils"; +import { + FLASH_MAX_PAGE_SIZE_700, + FLASH_MAX_PAGE_SIZE_800, + FragmentType, + NVM3_CODE_LARGE_SHIFT, + NVM3_CODE_SMALL_SHIFT, + NVM3_COUNTER_SIZE, + NVM3_MAX_OBJ_SIZE_SMALL, + NVM3_OBJ_FRAGTYPE_MASK, + NVM3_OBJ_FRAGTYPE_SHIFT, + NVM3_OBJ_HEADER_SIZE_LARGE, + NVM3_OBJ_KEY_MASK, + NVM3_OBJ_KEY_SHIFT, + NVM3_OBJ_LARGE_LEN_MASK, + NVM3_OBJ_TYPE_MASK, + NVM3_PAGE_COUNTER_MASK, + NVM3_PAGE_COUNTER_SIZE, + NVM3_PAGE_HEADER_SIZE, + NVM3_PAGE_MAGIC, + ObjectType, + PageStatus, + PageWriteSize, + ZWAVE_APPLICATION_NVM_SIZE, +} from "./nvm3/consts"; +import { + ApplicationVersionFile800ID, + type NVMSection, + getNVMSectionByFileID, +} from "./nvm3/files"; +import { + type NVM3Object, + type NVM3ObjectHeader, + fragmentLargeObject, + getAlignedSize, + getObjectHeader, + getRequiredSpace, + serializeObject, +} from "./nvm3/object"; +import { + type NVM3PageHeader, + pageSizeFromBits, + serializePageHeader, +} from "./nvm3/page"; +import { validateBergerCode, validateBergerCodeMulti } from "./nvm3/utils"; + +// TODO: Possible optimizations: +// Investigate if there is a better way to determine whether the NVM +// uses a shared FS or not. The current implementation scans all objects +// to find the 800 series application version file. +// Alternatively, we could simply check if each page starts with an object header. +// If yes, read the objects lazily when needed. If not, remember that the page is empty. + +export type NVM3PageInfo = NVM3PageHeader & { + objects: NVM3ObjectHeader[]; +}; + +export interface NVM3SectionInfo { + pages: NVM3PageInfo[]; + /** The index of the current page */ + currentPage: number; + /** The next byte to write in the current page */ + offsetInPage: number; + /** A map of file IDs and page indizes in which their last copy resides */ + objectLocations: Map; +} + +export type NVM3FileSystemInfo = { + isSharedFileSystem: true; + sections: Record<"all", NVM3SectionInfo>; +} | { + isSharedFileSystem: false; + sections: Record; +}; + +export interface NVM3Meta { + sharedFileSystem: boolean; + pageSize: number; + deviceFamily: number; + writeSize: PageWriteSize; + memoryMapped: boolean; +} + +export type NVM3EraseOptions = Partial; + +export class NVM3 implements NVM { + public constructor(io: NVMIO) { + this._io = io; + } + + private _io: NVMIO; + private _access: NVMAccess = NVMAccess.None; + + private _info: NVM3FileSystemInfo | undefined; + public get info(): NVM3FileSystemInfo | undefined { + return this._info; + } + + private async ensureReadable(): Promise { + if ( + this._access === NVMAccess.Read + || this._access === NVMAccess.ReadWrite + ) { + return; + } + if (this._access === NVMAccess.Write) { + await this._io.close(); + } + this._access = await this._io.open(NVMAccess.Read); + } + + private async ensureWritable(): Promise { + if ( + this._access === NVMAccess.Write + || this._access === NVMAccess.ReadWrite + ) { + return; + } + if (this._access === NVMAccess.Read) { + await this._io.close(); + } + this._access = await this._io.open(NVMAccess.Write); + } + + public async init(): Promise { + await this.ensureReadable(); + + let pageOffset = 0; + // Determine NVM size, scan pages + const pages: NVM3PageInfo[] = []; + let isSharedFileSystem = false; + while (pageOffset < this._io.size) { + // console.debug( + // `NVM3 init() - reading page header at offset ${ + // num2hex(pageOffset) + // }`, + // ); + const header = await readPageHeader(this._io, pageOffset); + pages.push({ + ...header, + objects: [], + }); + pageOffset += header.pageSize; + } + + // Scan each page for objects + for (const page of pages) { + // Scan objects in this page + let objectOffset = page.offset + NVM3_PAGE_HEADER_SIZE; + const nextPageOffset = page.offset + page.pageSize; + while (objectOffset < nextPageOffset) { + // console.debug( + // `NVM3 init() - reading object header. page offset ${ + // num2hex(page.offset) + // }, object offset ${num2hex(objectOffset)}`, + // ); + const objectHeader = await readObjectHeader( + this._io, + objectOffset, + ); + if (objectHeader) { + page.objects.push(objectHeader); + objectOffset += objectHeader.alignedSize; + + // Detect the 800 series shared protocol & application NVM file system + // by looking for the 800 series application version file + if (objectHeader.key === ApplicationVersionFile800ID) { + isSharedFileSystem = true; + } + } else { + // Reached the end of the data in this page + break; + } + } + } + + // By convention, we only use the applicationPages in that case + let applicationPages: NVM3PageInfo[]; + let protocolPages: NVM3PageInfo[]; + + if (isSharedFileSystem) { + applicationPages = pages; + protocolPages = []; + } else { + applicationPages = pages.filter( + (p) => p.offset < ZWAVE_APPLICATION_NVM_SIZE, + ); + protocolPages = pages.filter( + (p) => p.offset >= ZWAVE_APPLICATION_NVM_SIZE, + ); + } + + // NVM3 layouts pages in a ring buffer. Pages are written from front to back, then occupied pages + // are erased and overwritten. Pages at the start of the memory section may have an erase count that's 1 higher + // than the pages at the end. + const pageInfoToSectionInfo = ( + pages: NVM3PageInfo[], + ): NVM3SectionInfo => { + // Find the current page, which is either: + // - The last page with the high erase count that contains an object + const maxEraseCount = Math.max(...pages.map((p) => p.eraseCount)); + let currentPageIndex = pages.findLastIndex((p) => + p.eraseCount === maxEraseCount && p.objects.length > 0 + ); + // - or if there is none, the last page with the lower erase count that contains an object + if (currentPageIndex === -1) { + currentPageIndex = pages.findLastIndex((p) => + p.objects.length > 0 + ); + } + // - Or if no objects exist at all, the beginning of the section + if (currentPageIndex === -1) currentPageIndex = 0; + + // Find the next free byte of the current page + const currentPage = pages[currentPageIndex]; + let offset = NVM3_PAGE_HEADER_SIZE; + for (const object of currentPage.objects) { + offset += object.alignedSize; + } + + const objectLocations = new Map(); + for (let i = 0; i < pages.length; i++) { + const page = pages[i]; + for (const object of page.objects) { + const location = objectLocations.get(object.key); + if (location == undefined) { + // Object seen for the first time, remember the page it is in + objectLocations.set(object.key, i); + } else if ( + (object.fragmentType === FragmentType.None + || object.fragmentType === FragmentType.First) + && (page.eraseCount >= pages[location].eraseCount) + ) { + // Object was seen before. Only remember it if it is the only + // or first fragment and the object appears in a later location + // of the ring buffer + objectLocations.set(object.key, i); + } + } + } + + return { + pages, + offsetInPage: offset, + currentPage: currentPageIndex, + objectLocations, + }; + }; + + if (isSharedFileSystem) { + this._info = { + isSharedFileSystem: true, + sections: { + all: pageInfoToSectionInfo(applicationPages), + }, + }; + } else { + this._info = { + isSharedFileSystem: false, + sections: { + application: pageInfoToSectionInfo(applicationPages), + protocol: pageInfoToSectionInfo(protocolPages), + }, + }; + } + + return this._info; + } + + private getNVMSectionForFile(fileId: number): NVM3SectionInfo { + // Determine which ring buffer to read in + return this._info!.isSharedFileSystem + ? this._info!.sections.all + : this._info!.sections[getNVMSectionByFileID(fileId)]; + } + + public async has(fileId: number): Promise { + this._info ??= await this.init(); + + // Determine which ring buffer to read in + const section = this.getNVMSectionForFile(fileId); + + return section.objectLocations.has(fileId); + } + + public readObjectData(object: NVM3ObjectHeader): Promise { + return nvmReadBuffer( + this._io, + object.offset + object.headerSize, + object.fragmentSize, + ); + } + + public async get(fileId: number): Promise { + this._info ??= await this.init(); + + // Determine which ring buffer to read in + const section = this.getNVMSectionForFile(fileId); + + const pages = section.pages; + + // TODO: There should be no need for scanning, since we know the object locations after init(). + + // Start scanning backwards through the pages ring buffer, starting with the current page + let parts: Buffer[] | undefined; + let complete = false; + let objType: ObjectType | undefined; + const resetFragments = () => { + // if (parts?.length) { + // console.debug("Resetting fragmented object"); + // } + parts = undefined; + complete = false; + }; + pages: for (let offset = 0; offset < pages.length; offset++) { + const index = (section.currentPage - offset + pages.length) + % pages.length; + const page = pages[index]; + // console.debug( + // `NVM3.get(${fileId}): scanning page ${index} at offset ${ + // num2hex(page.offset) + // }`, + // ); + // Scan objects in this page, read backwards. + // The last non-deleted object wins + objects: for (let j = page.objects.length - 1; j >= 0; j--) { + const object = page.objects[j]; + + const readObject = () => this.readObjectData(object); + + if (object.key !== fileId) { + // Reset any fragmented objects when encountering a different key + resetFragments(); + continue objects; + } + + if (object.type === ObjectType.Deleted) { + // Last action for this object was a deletion. There is no data. + return; + } else if (object.fragmentType === FragmentType.None) { + // console.debug( + // `NVM3.get(${fileId}): found complete object - header offset ${ + // num2hex(object.offset) + // }, content offset ${ + // num2hex(object.offset + object.headerSize) + // }, length ${object.fragmentSize}`, + // ); + // This is a complete object + parts = [await readObject()]; + objType = object.type; + complete = true; + break pages; + } else if (object.fragmentType === FragmentType.Last) { + // console.debug( + // `NVM3.get(${fileId}): found LAST fragment - header offset ${ + // num2hex(object.offset) + // }, content offset ${ + // num2hex(object.offset + object.headerSize) + // }, length ${object.fragmentSize}`, + // ); + parts = [await readObject()]; + objType = object.type; + complete = false; + } else if (object.fragmentType === FragmentType.Next) { + if (parts?.length && objType === object.type) { + // console.debug( + // `NVM3.get(${fileId}): found NEXT fragment - header offset ${ + // num2hex(object.offset) + // }, content offset ${ + // num2hex(object.offset + object.headerSize) + // }, length ${object.fragmentSize}`, + // ); + parts.unshift(await readObject()); + } else { + // This shouldn't be here + resetFragments(); + } + } else if (object.fragmentType === FragmentType.First) { + if (parts?.length && objType === object.type) { + // console.debug( + // `NVM3.get(${fileId}): found FIRST fragment - header offset ${ + // num2hex(object.offset) + // }, content offset ${ + // num2hex(object.offset + object.headerSize) + // }, length ${object.fragmentSize}`, + // ); + parts.unshift(await readObject()); + complete = true; + break pages; + } else { + // This shouldn't be here + resetFragments(); + } + } + } + } + + if (!parts?.length || !complete || objType == undefined) return; + + return Buffer.concat(parts); + } + + private async writeObjects(objects: NVM3Object[]): Promise { + const section = this.getNVMSectionForFile(objects[0].key); + + let page = section.pages[section.currentPage]; + let remainingSpace = page.pageSize + - NVM3_PAGE_HEADER_SIZE + - section.offsetInPage; + + // TODO: See if we can avoid double writes on a page change + + /** Moves to the next page and erases it if necessary */ + const nextPage = async () => { + section.currentPage = (section.currentPage + 1) + % section.pages.length; + page = section.pages[section.currentPage]; + + // Find headers of objects that need to be preserved + const toPreserve = [...section.objectLocations].filter(( + [, pageIndex], + ) => pageIndex === section.currentPage) + .map(([fileID]) => + page.objects.findLast((h) => h.key === fileID) + ) + .filter((h) => h != undefined) + .filter((h) => h.type !== ObjectType.Deleted); + // And add the objects to the TODO list + for (const header of toPreserve) { + const data = await this.get(header.key); + console.error(`Need to preserve object ${num2hex(header.key)} + page index: ${section.currentPage} + object type: ${getEnumMemberName(ObjectType, header.type)} + data: ${data != undefined ? `${data.length} bytes` : "(no data)"}`); + objects.push({ + key: header.key, + type: header.type, + fragmentType: FragmentType.None, + data, + }); + } + + if (page.objects.length > 0) { + // The page needs to be erased + page.eraseCount++; + page.objects = []; + + const pageHeaderBuffer = serializePageHeader(page); + const pageBuffer = Buffer.alloc(page.pageSize, 0xff); + pageHeaderBuffer.copy(pageBuffer, 0); + + await nvmWriteBuffer(this._io, page.offset, pageBuffer); + } + + section.offsetInPage = NVM3_PAGE_HEADER_SIZE; + remainingSpace = page.pageSize - NVM3_PAGE_HEADER_SIZE; + }; + + // Go through the list of objects and write all of them to the NVM + for (const object of objects) { + const isLargeObject = object.type === ObjectType.DataLarge + || object.type === ObjectType.CounterLarge; + + let fragments: NVM3Object[] | undefined; + + if (isLargeObject) { + // Large objects may be fragmented + + // We need to start a new page, if the remaining space is not enough for + // the object header plus additional data + if (remainingSpace <= NVM3_OBJ_HEADER_SIZE_LARGE) { + await nextPage(); + } + + fragments = fragmentLargeObject( + object as any, + remainingSpace, + page.pageSize - NVM3_PAGE_HEADER_SIZE, + ); + } else { + // Small objects cannot be fragmented. If they don't fit, + // they need to go on the next page. + const requiredSpace = getRequiredSpace(object); + if (requiredSpace > remainingSpace) { + await nextPage(); + } + fragments = [object]; + } + + // Write each fragment to the NVM. If there are multiple fragments, + // each one but the first needs to be written at the beginning of a new page + for (let i = 0; i < fragments.length; i++) { + if (i > 0) await nextPage(); + const fragment = fragments[i]; + + const objBuffer = serializeObject(fragment); + const objOffset = page.offset + section.offsetInPage; + await this._io.write(objOffset, objBuffer); + const requiredSpace = getRequiredSpace(fragment); + section.offsetInPage += requiredSpace; + remainingSpace -= requiredSpace; + + // Remember which objects exist in this page + page.objects.push(getObjectHeader(object, objOffset)); + + // And remember where this object lives + if (object.type === ObjectType.Deleted) { + section.objectLocations.delete(object.key); + } else if ( + fragment.fragmentType === FragmentType.None + || fragment.fragmentType === FragmentType.First + ) { + section.objectLocations.set( + fragment.key, + section.currentPage, + ); + } + } + } + } + + public async set(property: number, value: Buffer): Promise { + if (!this._info) await this.init(); + await this.ensureWritable(); + + await this.writeObjects([{ + key: property, + type: value.length <= NVM3_MAX_OBJ_SIZE_SMALL + ? ObjectType.DataSmall + : ObjectType.DataLarge, + // writeObject deals with fragmentation + fragmentType: FragmentType.None, + data: value, + }]); + } + + /** Writes multiple values to the NVM at once. `null` / `undefined` cause the value to be deleted */ + public async setMany( + values: [number, Buffer | null | undefined][], + ): Promise { + if (!this._info) await this.init(); + await this.ensureWritable(); + + // Group objects by their NVM section + const objectsBySection = new Map< + number, /* offset */ + [number, Buffer | null | undefined][] + >(); + for (const [key, value] of values) { + const sectionOffset = + this.getNVMSectionForFile(key).pages[0].offset; + if (!objectsBySection.has(sectionOffset)) { + objectsBySection.set(sectionOffset, []); + } + objectsBySection.get(sectionOffset)!.push([key, value]); + } + + // And call writeObjects for each group + for (const objectGroups of objectsBySection.values()) { + await this.writeObjects( + objectGroups.map(([key, value]) => (value + ? { + key, + type: value.length <= NVM3_MAX_OBJ_SIZE_SMALL + ? ObjectType.DataSmall + : ObjectType.DataLarge, + // writeObject deals with fragmentation + fragmentType: FragmentType.None, + data: value, + } + : { + key, + type: ObjectType.Deleted, + fragmentType: FragmentType.None, + }) + ), + ); + } + } + + public async delete(property: number): Promise { + if (!this._info) await this.init(); + await this.ensureWritable(); + + await this.writeObjects([{ + key: property, + type: ObjectType.Deleted, + fragmentType: FragmentType.None, + }]); + } + + public async erase(options?: NVM3EraseOptions): Promise { + const { + deviceFamily = 2047, + writeSize = PageWriteSize.WRITE_SIZE_16, + memoryMapped = true, + sharedFileSystem = false, + } = options ?? {}; + const maxPageSize = sharedFileSystem + ? FLASH_MAX_PAGE_SIZE_800 + : FLASH_MAX_PAGE_SIZE_700; + const pageSize = Math.min( + options?.pageSize ?? maxPageSize, + maxPageSize, + ); + + // Make sure we won't be writing incomplete pages + if (this._io.size % pageSize !== 0) { + throw new ZWaveError( + `Invalid page size. NVM size ${this._io.size} must be a multiple of the page size ${pageSize}.`, + ZWaveErrorCodes.Argument_Invalid, + ); + } else if ( + !sharedFileSystem && ZWAVE_APPLICATION_NVM_SIZE % pageSize !== 0 + ) { + throw new ZWaveError( + `Invalid page size. The application NVM size ${ZWAVE_APPLICATION_NVM_SIZE} must be a multiple of the page size ${pageSize}.`, + ZWaveErrorCodes.Argument_Invalid, + ); + } else if ( + !sharedFileSystem + && (this._io.size - ZWAVE_APPLICATION_NVM_SIZE) % pageSize !== 0 + ) { + throw new ZWaveError( + `Invalid page size. The protocol NVM size ${ + this._io.size + - ZWAVE_APPLICATION_NVM_SIZE + } must be a multiple of the page size ${pageSize}.`, + ZWaveErrorCodes.Argument_Invalid, + ); + } + + await this.ensureWritable(); + + // Create empty pages, write them to the NVM + const applicationPages: NVM3PageInfo[] = []; + const protocolPages: NVM3PageInfo[] = []; + + const numPages = this._io.size / pageSize; + for (let i = 0; i < numPages; i++) { + const offset = i * pageSize; + const pageBuffer = Buffer.alloc(pageSize, 0xff); + const pageHeader: NVM3PageHeader = { + offset, + version: 0x01, + eraseCount: 0, + encrypted: false, + deviceFamily, + memoryMapped, + pageSize, + status: PageStatus.OK, + writeSize, + }; + serializePageHeader(pageHeader).copy(pageBuffer, 0); + await nvmWriteBuffer(this._io, offset, pageBuffer); + + if (sharedFileSystem || offset < ZWAVE_APPLICATION_NVM_SIZE) { + applicationPages.push({ ...pageHeader, objects: [] }); + } else { + protocolPages.push({ ...pageHeader, objects: [] }); + } + } + + // Remember the pages we just created for further use + this._info = sharedFileSystem + ? { + isSharedFileSystem: true, + sections: { + all: { + currentPage: 0, + objectLocations: new Map(), + offsetInPage: NVM3_PAGE_HEADER_SIZE, + pages: applicationPages, + }, + }, + } + : { + isSharedFileSystem: false, + sections: { + application: { + currentPage: 0, + objectLocations: new Map(), + offsetInPage: NVM3_PAGE_HEADER_SIZE, + pages: applicationPages, + }, + protocol: { + currentPage: 0, + objectLocations: new Map(), + offsetInPage: NVM3_PAGE_HEADER_SIZE, + pages: protocolPages, + }, + }, + }; + } +} + +async function readPageHeader( + io: NVMIO, + offset: number, +): Promise { + if (offset > io.size - NVM3_PAGE_HEADER_SIZE) { + throw new ZWaveError( + "Incomplete page in buffer!", + ZWaveErrorCodes.NVM_InvalidFormat, + ); + } + + const buffer = (await io.read(offset, NVM3_PAGE_HEADER_SIZE)).buffer; + + const { version, eraseCount } = tryGetVersionAndEraseCount(buffer); + + // Page status + const status = buffer.readUInt32LE(12); + + const devInfo = buffer.readUInt16LE(16); + const deviceFamily = devInfo & 0x7ff; + const writeSize = (devInfo >> 11) & 0b1; + const memoryMapped = !!((devInfo >> 12) & 0b1); + let pageSize = pageSizeFromBits((devInfo >> 13) & 0b111); + + if (pageSize > 0xffff) { + // Some controllers have no valid info in the page size bits, resulting + // in an impossibly large page size. To try and figure out the actual page + // size without knowing the hardware, we scan the buffer for the next valid + // page start. + for (let exponent = 0; exponent < 0b111; exponent++) { + const testPageSize = pageSizeFromBits(exponent); + const nextOffset = offset + testPageSize; + if ( + // exactly end of NVM OR + io.size === nextOffset + // next page + || await isValidPageHeaderAtOffset(io, nextOffset) + ) { + pageSize = testPageSize; + break; + } + } + } + if (pageSize > 0xffff) { + throw new ZWaveError( + "Could not determine page size!", + ZWaveErrorCodes.NVM_InvalidFormat, + ); + } + + if (io.size < offset + pageSize) { + throw new ZWaveError( + `NVM contains incomplete page at offset ${num2hex(offset)}!`, + ZWaveErrorCodes.NVM_InvalidFormat, + ); + } + + const formatInfo = buffer.readUInt16LE(18); + const encrypted = !(formatInfo & 0b1); + + return { + offset, + version, + eraseCount, + status, + encrypted, + pageSize, + writeSize, + memoryMapped, + deviceFamily, + }; +} + +function tryGetVersionAndEraseCount( + header: Buffer, +): { version: number; eraseCount: number } { + const version = header.readUInt16LE(0); + const magic = header.readUInt16LE(2); + if (magic !== NVM3_PAGE_MAGIC) { + throw new ZWaveError( + "Not a valid NVM3 page!", + ZWaveErrorCodes.NVM_InvalidFormat, + ); + } + if (version !== 0x01) { + throw new ZWaveError( + `Unsupported NVM3 page version: ${version}`, + ZWaveErrorCodes.NVM_NotSupported, + ); + } + + // The erase counter is saved twice, once normally, once inverted + let eraseCount = header.readUInt32LE(4); + const eraseCountCode = eraseCount >>> NVM3_PAGE_COUNTER_SIZE; + eraseCount &= NVM3_PAGE_COUNTER_MASK; + validateBergerCode(eraseCount, eraseCountCode, NVM3_PAGE_COUNTER_SIZE); + + let eraseCountInv = header.readUInt32LE(8); + const eraseCountInvCode = eraseCountInv >>> NVM3_PAGE_COUNTER_SIZE; + eraseCountInv &= NVM3_PAGE_COUNTER_MASK; + validateBergerCode( + eraseCountInv, + eraseCountInvCode, + NVM3_PAGE_COUNTER_SIZE, + ); + + if (eraseCount !== (~eraseCountInv & NVM3_PAGE_COUNTER_MASK)) { + throw new ZWaveError( + "Invalid erase count!", + ZWaveErrorCodes.NVM_InvalidFormat, + ); + } + + return { version, eraseCount }; +} + +async function isValidPageHeaderAtOffset( + io: NVMIO, + offset: number, +): Promise { + if (offset > io.size - NVM3_PAGE_HEADER_SIZE) { + return false; + } + + const { buffer } = await io.read(offset, NVM3_PAGE_HEADER_SIZE); + + try { + tryGetVersionAndEraseCount(buffer); + return true; + } catch { + return false; + } +} + +async function readObjectHeader( + io: NVMIO, + offset: number, +): Promise { + let headerSize = 4; + const hdr1 = await nvmReadUInt32LE(io, offset); + + // Skip over blank page areas + if (hdr1 === 0xffffffff) return; + + const key = (hdr1 >> NVM3_OBJ_KEY_SHIFT) & NVM3_OBJ_KEY_MASK; + let objType: ObjectType = hdr1 & NVM3_OBJ_TYPE_MASK; + let fragmentSize = 0; + let hdr2: number | undefined; + const isLarge = objType === ObjectType.DataLarge + || objType === ObjectType.CounterLarge; + if (isLarge) { + hdr2 = await nvmReadUInt32LE(io, offset + 4); + headerSize += 4; + fragmentSize = hdr2 & NVM3_OBJ_LARGE_LEN_MASK; + } else if (objType > ObjectType.DataSmall) { + // In small objects with data, the length and object type are stored in the same value + fragmentSize = objType - ObjectType.DataSmall; + objType = ObjectType.DataSmall; + } else if (objType === ObjectType.CounterSmall) { + fragmentSize = NVM3_COUNTER_SIZE; + } + + const fragmentType: FragmentType = isLarge + ? (hdr1 >>> NVM3_OBJ_FRAGTYPE_SHIFT) & NVM3_OBJ_FRAGTYPE_MASK + : FragmentType.None; + + if (isLarge) { + validateBergerCodeMulti([hdr1, hdr2!], 32 + NVM3_CODE_LARGE_SHIFT); + } else { + validateBergerCodeMulti([hdr1], NVM3_CODE_SMALL_SHIFT); + } + + if (io.size < offset + headerSize + fragmentSize) { + throw new ZWaveError( + `NVM contains incomplete object at offset ${num2hex(offset)}!`, + ZWaveErrorCodes.NVM_InvalidFormat, + ); + } + + const alignedFragmentSize = getAlignedSize(fragmentSize); + const alignedSize = headerSize + alignedFragmentSize; + + return { + key, + offset, + type: objType, + fragmentType, + headerSize, + fragmentSize, + alignedSize, + }; +} diff --git a/packages/nvmedit/src/lib/NVM500.ts b/packages/nvmedit/src/lib/NVM500.ts new file mode 100644 index 00000000000..23b6905f637 --- /dev/null +++ b/packages/nvmedit/src/lib/NVM500.ts @@ -0,0 +1,612 @@ +import { + MAX_NODES, + ZWaveError, + ZWaveErrorCodes, + encodeBitMask, + parseBitMask, +} from "@zwave-js/core"; +import { type NVM, NVMAccess, type NVMIO } from "./common/definitions"; +import { type Route, encodeRoute, parseRoute } from "./common/routeCache"; +import { + type SUCUpdateEntry, + encodeSUCUpdateEntry, + parseSUCUpdateEntry, +} from "./common/sucUpdateEntry"; +import { nvmReadBuffer, nvmReadUInt16BE, nvmWriteBuffer } from "./common/utils"; +import { + type NVM500NodeInfo, + type NVMDescriptor, + type NVMModuleDescriptor, + encodeNVM500NodeInfo, + encodeNVMDescriptor, + encodeNVMModuleDescriptor, + parseNVM500NodeInfo, + parseNVMDescriptor, + parseNVMModuleDescriptor, +} from "./nvm500/EntryParsers"; +import { nvm500Impls } from "./nvm500/impls"; +import { + CONFIGURATION_VALID_0, + CONFIGURATION_VALID_1, + MAGIC_VALUE, + type NVM500Impl, + type NVMData, + type NVMEntryName, + NVMEntrySizes, + NVMEntryType, + NVMModuleType, + ROUTECACHE_VALID, + type ResolvedNVMEntry, + type ResolvedNVMLayout, +} from "./nvm500/shared"; + +export interface NVM500Info { + layout: ResolvedNVMLayout; + library: NVM500Impl["library"]; + moduleDescriptors: Map; + nvmDescriptor: NVMDescriptor; +} + +export type NVM500EraseOptions = { + layout: ResolvedNVMLayout; + nvmSize: number; + library: NVM500Impl["library"]; + nvmDescriptor: NVMDescriptor; +}; + +export class NVM500 implements NVM { + public constructor(io: NVMIO) { + this._io = io; + } + + private _io: NVMIO; + private _access: NVMAccess = NVMAccess.None; + + private _info: NVM500Info | undefined; + public get info(): NVM500Info | undefined { + return this._info; + } + + private async ensureReadable(): Promise { + if ( + this._access === NVMAccess.Read + || this._access === NVMAccess.ReadWrite + ) { + return; + } + if (this._access === NVMAccess.Write) { + await this._io.close(); + } + this._access = await this._io.open(NVMAccess.Read); + } + + private async ensureWritable(): Promise { + if ( + this._access === NVMAccess.Write + || this._access === NVMAccess.ReadWrite + ) { + return; + } + if (this._access === NVMAccess.Read) { + await this._io.close(); + } + this._access = await this._io.open(NVMAccess.Write); + } + + public async init(): Promise { + await this.ensureReadable(); + + // Try the different known layouts to find one that works + for (const impl of nvm500Impls) { + try { + const info = await this.resolveLayout(impl); + if (await this.isLayoutValid(info, impl.protocolVersions)) { + this._info = info; + } + break; + } catch { + continue; + } + } + + if (!this._info) { + throw new ZWaveError( + "Did not find a matching NVM 500 parser implementation! Make sure that the NVM data belongs to a controller with Z-Wave SDK 6.61 or higher.", + ZWaveErrorCodes.NVM_NotSupported, + ); + } + + return this._info; + } + + private async resolveLayout(impl: NVM500Impl): Promise { + const resolvedLayout = new Map(); + let nvmDescriptor: NVMDescriptor | undefined; + const moduleDescriptors = new Map(); + + let offset = 0; + let moduleStart = -1; + let moduleSize = -1; + const nvmEnd = await nvmReadUInt16BE(this._io, 0); + + for (const entry of 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 = await nvmReadUInt16BE(this._io, 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 resolvedEntry: ResolvedNVMEntry = { + ...entry, + offset, + size, + }; + + if (entry.type === NVMEntryType.NVMDescriptor) { + const entryData = await this.readRawEntry(resolvedEntry); + // NVMDescriptor is always a single entry + nvmDescriptor = parseNVMDescriptor(entryData[0]); + } else if (entry.type === NVMEntryType.NVMModuleDescriptor) { + const entryData = await this.readRawEntry(resolvedEntry); + // NVMModuleDescriptor is always a single entry + const descriptor = parseNVMModuleDescriptor(entryData[0]); + if (descriptor.size !== moduleSize) { + throw new ZWaveError( + "NVM module descriptor size does not match module size!", + ZWaveErrorCodes.NVM_InvalidFormat, + ); + } + moduleDescriptors.set(entry.name, descriptor); + } + + resolvedLayout.set(entry.name, resolvedEntry); + + // Skip forward + offset += size * entry.count; + if (offset >= nvmEnd) break; + } + + if (!nvmDescriptor) { + throw new ZWaveError( + "NVM descriptor not found in NVM!", + ZWaveErrorCodes.NVM_InvalidFormat, + ); + } + + return { + layout: resolvedLayout, + library: impl.library, + moduleDescriptors, + nvmDescriptor, + }; + } + + private async isLayoutValid( + info: NVM500Info, + protocolVersions: string[], + ): Promise { + // Checking if an NVM is valid requires checking multiple bytes at different locations + const eeoffset_magic_entry = info.layout.get("EEOFFSET_MAGIC_far"); + if (!eeoffset_magic_entry) return false; + const eeoffset_magic = + (await this.readEntry(eeoffset_magic_entry))[0] as number; + + const configuration_valid_0_entry = info.layout.get( + "NVM_CONFIGURATION_VALID_far", + ); + if (!configuration_valid_0_entry) return false; + const configuration_valid_0 = + (await this.readEntry(configuration_valid_0_entry))[0] as number; + + const configuration_valid_1_entry = info.layout.get( + "NVM_CONFIGURATION_REALLYVALID_far", + ); + if (!configuration_valid_1_entry) return false; + const configuration_valid_1 = + (await this.readEntry(configuration_valid_1_entry))[0] as number; + + const routecache_valid_entry = info.layout.get( + "EX_NVM_ROUTECACHE_MAGIC_far", + ); + if (!routecache_valid_entry) return false; + const routecache_valid = + (await this.readEntry(routecache_valid_entry))[0] as number; + + const endMarker_entry = info.layout.get("nvmModuleSizeEndMarker"); + if (!endMarker_entry) return false; + const endMarker = (await this.readEntry(endMarker_entry))[0] as number; + + return ( + eeoffset_magic === MAGIC_VALUE + && configuration_valid_0 === CONFIGURATION_VALID_0 + && configuration_valid_1 === CONFIGURATION_VALID_1 + && routecache_valid === ROUTECACHE_VALID + && protocolVersions.includes(info.nvmDescriptor.protocolVersion) + && endMarker === 0 + ); + } + + async has(property: NVMEntryName): Promise { + this._info ??= await this.init(); + return this._info.layout.has(property); + } + + private async readSingleRawEntry( + entry: ResolvedNVMEntry, + index: number, + ): Promise { + if (index >= entry.count) { + throw new ZWaveError( + `Index out of range. Tried to read entry ${index} of ${entry.count}.`, + ZWaveErrorCodes.Argument_Invalid, + ); + } + return nvmReadBuffer( + this._io, + entry.offset + index * entry.size, + entry.size, + ); + } + + private async readRawEntry( + entry: ResolvedNVMEntry, + ): Promise { + const ret: Buffer[] = []; + const nvmData = await nvmReadBuffer( + this._io, + entry.offset, + entry.count * entry.size, + ); + for (let i = 0; i < entry.count; i++) { + ret.push( + nvmData.subarray(i * entry.size, (i + 1) * entry.size), + ); + } + return ret; + } + + private parseEntry(type: NVMEntryType, data: Buffer): NVMData { + switch (type) { + case NVMEntryType.Byte: + return data.readUInt8(0); + case NVMEntryType.Word: + case NVMEntryType.NVMModuleSize: + return data.readUInt16BE(0); + case NVMEntryType.DWord: + return data.readUInt32BE(0); + case NVMEntryType.NodeInfo: + if (data.every((byte) => byte === 0)) { + return undefined; + } + return parseNVM500NodeInfo(data, 0); + case NVMEntryType.NodeMask: + return parseBitMask(data); + case NVMEntryType.SUCUpdateEntry: + if (data.every((byte) => byte === 0)) { + return undefined; + } + return parseSUCUpdateEntry(data, 0); + case NVMEntryType.Route: + if (data.every((byte) => byte === 0)) { + return undefined; + } + return parseRoute(data, 0); + case NVMEntryType.NVMModuleDescriptor: { + return parseNVMModuleDescriptor(data); + } + case NVMEntryType.NVMDescriptor: + return parseNVMDescriptor(data); + default: + // This includes NVMEntryType.BUFFER + return data; + } + } + + private async readEntry( + entry: ResolvedNVMEntry, + ): Promise { + const data: Buffer[] = await this.readRawEntry(entry); + return data.map((buffer) => this.parseEntry(entry.type, buffer)); + } + + private async readSingleEntry( + entry: ResolvedNVMEntry, + index: number, + ): Promise { + const data: Buffer = await this.readSingleRawEntry(entry, index); + return this.parseEntry(entry.type, data); + } + + public async get(property: NVMEntryName): Promise { + this._info ??= await this.init(); + await this.ensureReadable(); + + const entry = this._info.layout.get(property); + if (!entry) return undefined; + + return this.readEntry(entry); + } + + public async getSingle( + property: NVMEntryName, + index: number, + ): Promise { + this._info ??= await this.init(); + await this.ensureReadable(); + + const entry = this._info.layout.get(property); + if (!entry) return undefined; + + return this.readSingleEntry(entry, index); + } + + private encodeEntry( + type: NVMEntryType, + data: NVMData, + entrySize?: number, + ): Buffer { + const size = entrySize ?? NVMEntrySizes[type]; + + switch (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; + } + } + + private async writeSingleRawEntry( + entry: ResolvedNVMEntry, + index: number, + data: Buffer, + ): Promise { + if (index >= entry.count) { + throw new ZWaveError( + `Index out of range. Tried to write entry ${index} of ${entry.count}.`, + ZWaveErrorCodes.Argument_Invalid, + ); + } + return nvmWriteBuffer( + this._io, + entry.offset + index * entry.size, + data, + ); + } + + private async writeRawEntry( + entry: ResolvedNVMEntry, + data: Buffer[], + ): Promise { + await nvmWriteBuffer( + this._io, + entry.offset, + Buffer.concat(data), + ); + } + + private async writeEntry( + entry: ResolvedNVMEntry, + data: NVMData[], + ): Promise { + const buffers = data.map((d) => + this.encodeEntry(entry.type, d, entry.size) + ); + await this.writeRawEntry(entry, buffers); + } + + private async writeSingleEntry( + entry: ResolvedNVMEntry, + index: number, + data: NVMData, + ): Promise { + const buffer = this.encodeEntry(entry.type, data, entry.size); + await this.writeSingleRawEntry(entry, index, buffer); + } + + public async set(property: NVMEntryName, value: NVMData[]): Promise { + this._info ??= await this.init(); + await this.ensureWritable(); + + const entry = this._info.layout.get(property); + if (!entry) return; + + await this.writeEntry(entry, value); + } + + public async setSingle( + property: NVMEntryName, + index: number, + value: NVMData, + ): Promise { + this._info ??= await this.init(); + await this.ensureWritable(); + + const entry = this._info.layout.get(property); + if (!entry) return undefined; + + await this.writeSingleEntry(entry, index, value); + } + + private async fill(key: NVMEntryName, value: number) { + this._info ??= await this.init(); + await this.ensureWritable(); + + const entry = this._info.layout.get(key); + // Skip entries not present in this layout + if (!entry) return; + + const size = entry.size ?? NVMEntrySizes[entry.type]; + + const data: NVMData[] = []; + 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: + data.push(new Array(size).fill(value)); + break; + case NVMEntryType.NodeInfo: + case NVMEntryType.Route: + data.push(undefined); + break; + default: + throw new Error( + `Cannot fill entry of type ${NVMEntryType[entry.type]}`, + ); + } + } + + await this.writeEntry(entry, data); + } + + // eslint-disable-next-line @typescript-eslint/require-await + public async delete(_property: NVMEntryName): Promise { + throw new Error( + "Deleting entries is not supported for 500 series NVMs", + ); + } + + public async erase( + options: NVM500EraseOptions, + ): Promise { + // Blank NVM with 0xff + await nvmWriteBuffer(this._io, 0, Buffer.alloc(options.nvmSize, 0xff)); + + // Compute module sizes + const layoutEntries = Array.from(options.layout.values()); + const moduleSizeEntries = layoutEntries + .filter((entry) => entry.type === NVMEntryType.NVMModuleSize); + const moduleDescriptorEntries = layoutEntries + .filter((entry) => entry.type === NVMEntryType.NVMModuleDescriptor); + const moduleDescriptors = new Map(); + // Each module starts with a size marker and ends with a descriptor + for (let i = 0; i < moduleSizeEntries.length; i++) { + const sizeEntry = moduleSizeEntries[i]; + const descriptorEntry = moduleDescriptorEntries[i]; + const size = descriptorEntry.offset + + descriptorEntry.size + - sizeEntry.offset; + + // Write each module size to their NVMModuleSize marker + await this.writeEntry(sizeEntry, [size]); + + // Write each module size, type and version to the NVMModuleDescriptor at the end + const moduleType = descriptorEntry.name === "nvmZWlibraryDescriptor" + ? NVMModuleType.ZW_LIBRARY + : descriptorEntry.name === "nvmApplicationDescriptor" + ? NVMModuleType.APPLICATION + : descriptorEntry.name === "nvmHostApplicationDescriptor" + ? NVMModuleType.HOST_APPLICATION + : descriptorEntry.name === "nvmDescriptorDescriptor" + ? NVMModuleType.NVM_DESCRIPTOR + : 0; + + const moduleDescriptor: NVMModuleDescriptor = { + size, + type: moduleType, + version: descriptorEntry.name === "nvmZWlibraryDescriptor" + ? options.nvmDescriptor.protocolVersion + : options.nvmDescriptor.firmwareVersion, + }; + moduleDescriptors.set(descriptorEntry.name, moduleDescriptor); + await this.writeEntry(descriptorEntry, [moduleDescriptor]); + } + + // Initialize this._info, so the following works + this._info = { + ...options, + moduleDescriptors, + }; + + // Write NVM size to nvmTotalEnd + // the value points to the last byte, therefore subtract 1 + await this.set("nvmTotalEnd", [options.nvmSize - 1]); + + // Set some entries that are always identical + await this.set("NVM_CONFIGURATION_VALID_far", [CONFIGURATION_VALID_0]); + await this.set("NVM_CONFIGURATION_REALLYVALID_far", [ + CONFIGURATION_VALID_1, + ]); + await this.set("EEOFFSET_MAGIC_far", [MAGIC_VALUE]); + await this.set("EX_NVM_ROUTECACHE_MAGIC_far", [ROUTECACHE_VALID]); + await this.set("nvmModuleSizeEndMarker", [0]); + + // Set NVM descriptor + await this.set("nvmDescriptor", [options.nvmDescriptor]); + + // Set dummy entries we're never going to fill + await this.fill("NVM_INTERNAL_RESERVED_1_far", 0); + await this.fill("NVM_INTERNAL_RESERVED_2_far", 0xff); + await this.fill("NVM_INTERNAL_RESERVED_3_far", 0); + await this.fill("NVM_RTC_TIMERS_far", 0); + await this.fill("EX_NVM_SUC_ACTIVE_START_far", 0); + await this.fill("EX_NVM_ZENSOR_TABLE_START_far", 0); + await this.fill("NVM_SECURITY0_KEY_far", 0); + + // And blank fields that are not supposed to be filled with 0xff + await this.fill("EX_NVM_SUC_CONTROLLER_LIST_START_far", 0xfe); + await this.fill("EX_NVM_NODE_TABLE_START_far", 0); + await this.fill("EX_NVM_ROUTING_TABLE_START_far", 0); + // For routes the value does not matter + await this.fill("EX_NVM_ROUTECACHE_START_far", 0); + await this.fill("EX_NVM_ROUTECACHE_NLWR_SR_START_far", 0); + } +} diff --git a/packages/nvmedit/src/lib/common/definitions.ts b/packages/nvmedit/src/lib/common/definitions.ts new file mode 100644 index 00000000000..ee104b5a1f0 --- /dev/null +++ b/packages/nvmedit/src/lib/common/definitions.ts @@ -0,0 +1,246 @@ +import { type CommandClasses } from "@zwave-js/core"; +import { type Expand } from "@zwave-js/shared"; +import { + type ApplicationCCsFile, + type ApplicationRFConfigFile, + type ApplicationTypeFile, + type ControllerInfoFile, + type LRNodeInfo, + type NodeInfo, +} from "../nvm3/files"; +import { type Route } from "./routeCache"; +import { type SUCUpdateEntry } from "./sucUpdateEntry"; + +export enum NVMAccess { + None, + Read, + Write, + ReadWrite, +} + +/** Provides an abstraction to access the contents of an NVM at the binary level */ +export interface NVMIO { + /** + * Opens the NVM for reading and/or writing. + * Since different NVM implementations may or may not allow reading and writing at the same time, + * the returned value indicates which access patterns are actually allowed. + */ + open(access: NVMAccess.Read | NVMAccess.Write): Promise; + + /** Returns the size of the NVM, after it has been opened */ + get size(): number; + /** Returns which access is currently allowed for this NVM implementation */ + get accessMode(): NVMAccess; + + /** + * Determines the size of the data chunks that can be used for writing. + * Requires the NVM to be readable. + */ + determineChunkSize(): Promise; + + /** + * Reads a chunk of data with the given length from the NVM. + * If the length is longer than the chunk size, or the end of the NVM is reached, + * the returned buffer will be shorter than the requested length. + */ + read( + offset: number, + length: number, + ): Promise<{ buffer: Buffer; endOfFile: boolean }>; + + /** + * Writes a chunk of data with the given length from the NVM. + * The returned value indicates how many bytes were actually written. + */ + write( + offset: number, + data: Buffer, + ): Promise<{ bytesWritten: number; endOfFile: boolean }>; + + /** Closes the NVM */ + close(): Promise; +} + +/** A specific NVM implementation */ +export interface NVM { + /** Checks if a property exists in the NVM */ + has(property: ID): Promise; + + /** Reads a property from the NVM */ + get(property: ID): Promise; + + /** Writes a property to the NVM */ + set(property: ID, value: Data): Promise; + + /** Deletes the property from the NVM */ + delete(property: ID): Promise; +} + +/** + * Provides an application-level abstraction over an NVM implementation + */ +export interface NVMAdapter { + /** Reads a property from the NVM */ + get( + property: T, + required?: R, + ): Promise< + R extends true ? NVMPropertyToDataType + : (NVMPropertyToDataType | undefined) + >; + + /** + * Changes a property to be written to the NVM later + */ + set( + property: T, + value: NVMPropertyToDataType, + ): Promise; + + /** + * Marks a property for deletion from the NVM. In some implementations, + * deleting one property may delete multiple properties that are stored together. + */ + delete(property: NVMProperty): Promise; + + /** Returns whether there are pending changes that weren't written to the NVM yet */ + hasPendingChanges(): boolean; + + /** Writes all pending changes to the NVM */ + commit(): Promise; +} + +export type ControllerNVMPropertyTypes = Expand< + & { + protocolVersion: string; + protocolFileFormat: number; + applicationVersion: string; + applicationData: Buffer; + preferredRepeaters?: number[]; + sucUpdateEntries: SUCUpdateEntry[]; + appRouteLock: number[]; + routeSlaveSUC: number[]; + sucPendingUpdate: number[]; + pendingDiscovery: number[]; + virtualNodeIds: number[]; + nodeIds: number[]; + } + // 700+ series only + & Partial<{ + applicationFileFormat: number; + applicationName: string; + lrNodeIds: number[]; + }> + // 500 series only + & Partial<{ + learnedHomeId: Buffer; + commandClasses: CommandClasses[]; + systemState: number; + watchdogStarted: number; + powerLevelNormal: number[]; + powerLevelLow: number[]; + powerMode: number; + powerModeExtintEnable: number; + powerModeWutTimeout: number; + }> + & Pick< + ControllerInfoFile, + | "homeId" + | "nodeId" + | "lastNodeId" + | "staticControllerNodeId" + | "sucLastIndex" + | "controllerConfiguration" + | "sucAwarenessPushNeeded" + | "maxNodeId" + | "reservedId" + | "systemState" + | "lastNodeIdLR" + | "maxNodeIdLR" + | "reservedIdLR" + | "primaryLongRangeChannelId" + | "dcdcConfig" + > + // 700+ series only + & Partial< + Pick< + ApplicationCCsFile, + | "includedInsecurely" + | "includedSecurelyInsecureCCs" + | "includedSecurelySecureCCs" + > + > + // 700+ series only + & Partial< + Pick< + ApplicationRFConfigFile, + | "rfRegion" + | "txPower" + | "measured0dBm" + | "enablePTI" + | "maxTXPower" + | "nodeIdType" + > + > + // 700+ series only + & Partial< + Pick< + ApplicationTypeFile, + | "isListening" + | "optionalFunctionality" + | "genericDeviceClass" + | "specificDeviceClass" + > + > +>; + +export interface NodeNVMPropertyTypes { + info: NodeInfo; + routes: { lwr?: Route; nlwr?: Route }; +} + +export interface LRNodeNVMPropertyTypes { + info: LRNodeInfo; +} + +export type ControllerNVMProperty = { + domain: "controller"; + type: keyof ControllerNVMPropertyTypes; + nodeId?: undefined; +}; + +export type ControllerNVMPropertyToDataType

= + ControllerNVMPropertyTypes[P["type"]]; + +export type NodeNVMProperty = { + domain: "node"; + type: keyof NodeNVMPropertyTypes; + nodeId: number; +}; + +export type NodeNVMPropertyToDataType

= + P["type"] extends keyof NodeNVMPropertyTypes + ? NodeNVMPropertyTypes[P["type"]] + : never; + +export type LRNodeNVMProperty = { + domain: "lrnode"; + type: keyof LRNodeNVMPropertyTypes; + nodeId: number; +}; + +export type LRNodeNVMPropertyToDataType

= + P["type"] extends keyof LRNodeNVMPropertyTypes + ? LRNodeNVMPropertyTypes[P["type"]] + : never; + +export type NVMProperty = + | ControllerNVMProperty + | NodeNVMProperty + | LRNodeNVMProperty; + +export type NVMPropertyToDataType

= P extends + ControllerNVMProperty ? ControllerNVMPropertyToDataType

+ : P extends NodeNVMProperty ? NodeNVMPropertyToDataType

+ : P extends LRNodeNVMProperty ? LRNodeNVMPropertyToDataType

+ : never; diff --git a/packages/nvmedit/src/lib/common/routeCache.ts b/packages/nvmedit/src/lib/common/routeCache.ts new file mode 100644 index 00000000000..2e78f1c62bb --- /dev/null +++ b/packages/nvmedit/src/lib/common/routeCache.ts @@ -0,0 +1,75 @@ +import { + type FLiRS, + MAX_REPEATERS, + RouteProtocolDataRate, + protocolDataRateMask, +} from "@zwave-js/core/safe"; + +const ROUTE_SIZE = MAX_REPEATERS + 1; +export const ROUTECACHE_SIZE = 2 * ROUTE_SIZE; +export const EMPTY_ROUTECACHE_FILL = 0xff; +export const emptyRouteCache = Buffer.alloc( + ROUTECACHE_SIZE, + EMPTY_ROUTECACHE_FILL, +); + +enum Beaming { + "1000ms" = 0x40, + "250ms" = 0x20, +} + +export interface Route { + beaming: FLiRS; + protocolRate: RouteProtocolDataRate; + repeaterNodeIDs?: number[]; +} + +export interface RouteCache { + nodeId: number; + lwr: Route; + nlwr: Route; +} + +export function parseRoute(buffer: Buffer, offset: number): Route { + const routeConf = buffer[offset + MAX_REPEATERS]; + const ret: Route = { + beaming: (Beaming[routeConf & 0x60] ?? false) as FLiRS, + protocolRate: routeConf & protocolDataRateMask, + repeaterNodeIDs: [ + ...buffer.subarray(offset, offset + MAX_REPEATERS), + ].filter((id) => id !== 0), + }; + if (ret.repeaterNodeIDs![0] === 0xfe) delete ret.repeaterNodeIDs; + return ret; +} + +export function encodeRoute(route: Route | undefined): Buffer { + const ret = Buffer.alloc(ROUTE_SIZE, 0); + if (route) { + if (route.repeaterNodeIDs) { + for ( + let i = 0; + i < MAX_REPEATERS && i < route.repeaterNodeIDs.length; + i++ + ) { + ret[i] = route.repeaterNodeIDs[i]; + } + } else { + ret[0] = 0xfe; + } + let routeConf = 0; + if (route.beaming) routeConf |= Beaming[route.beaming] ?? 0; + routeConf |= route.protocolRate & protocolDataRateMask; + ret[ROUTE_SIZE - 1] = routeConf; + } + + return ret; +} + +export function getEmptyRoute(): Route { + return { + beaming: false, + protocolRate: RouteProtocolDataRate.ZWave_40k, + repeaterNodeIDs: undefined, + }; +} diff --git a/packages/nvmedit/src/lib/common/sucUpdateEntry.ts b/packages/nvmedit/src/lib/common/sucUpdateEntry.ts new file mode 100644 index 00000000000..f9ad86cce42 --- /dev/null +++ b/packages/nvmedit/src/lib/common/sucUpdateEntry.ts @@ -0,0 +1,55 @@ +import { + type CommandClasses, + ZWaveError, + ZWaveErrorCodes, + encodeCCList, + parseCCList, +} from "@zwave-js/core/safe"; +import { SUC_UPDATE_ENTRY_SIZE, SUC_UPDATE_NODEPARM_MAX } from "../../consts"; + +export interface SUCUpdateEntry { + nodeId: number; + changeType: number; // TODO: This is some kind of enum + supportedCCs: CommandClasses[]; + controlledCCs: CommandClasses[]; +} + +export function parseSUCUpdateEntry( + buffer: Buffer, + offset: number, +): SUCUpdateEntry | undefined { + const slice = buffer.subarray(offset, offset + SUC_UPDATE_ENTRY_SIZE); + if (slice.every((b) => b === 0x00 || b === 0xff)) { + return; + } + const nodeId = slice[0]; + const changeType = slice[1]; + const { supportedCCs, controlledCCs } = parseCCList( + slice.subarray(2, SUC_UPDATE_ENTRY_SIZE), + ); + return { + nodeId, + changeType, + supportedCCs: supportedCCs.filter((cc) => cc > 0), + controlledCCs: controlledCCs.filter((cc) => cc > 0), + }; +} + +export function encodeSUCUpdateEntry( + entry: SUCUpdateEntry | undefined, +): Buffer { + const ret = Buffer.alloc(SUC_UPDATE_ENTRY_SIZE, 0); + if (entry) { + ret[0] = entry.nodeId; + ret[1] = entry.changeType; + const ccList = encodeCCList(entry.supportedCCs, entry.controlledCCs); + if (ccList.length > SUC_UPDATE_NODEPARM_MAX) { + throw new ZWaveError( + "Cannot encode SUC update entry, too many CCs", + ZWaveErrorCodes.Argument_Invalid, + ); + } + ccList.copy(ret, 2); + } + return ret; +} diff --git a/packages/nvmedit/src/lib/common/utils.ts b/packages/nvmedit/src/lib/common/utils.ts new file mode 100644 index 00000000000..c1ecbf171cf --- /dev/null +++ b/packages/nvmedit/src/lib/common/utils.ts @@ -0,0 +1,75 @@ +import type { NVMIO } from "./definitions"; + +export async function nvmReadUInt32LE( + io: NVMIO, + position: number, +): Promise { + const { buffer } = await io.read(position, 4); + return buffer.readUInt32LE(0); +} + +export async function nvmReadUInt16LE( + io: NVMIO, + position: number, +): Promise { + const { buffer } = await io.read(position, 2); + return buffer.readUInt16LE(0); +} + +export async function nvmReadUInt32BE( + io: NVMIO, + position: number, +): Promise { + const { buffer } = await io.read(position, 4); + return buffer.readUInt32BE(0); +} + +export async function nvmReadUInt16BE( + io: NVMIO, + position: number, +): Promise { + const { buffer } = await io.read(position, 2); + return buffer.readUInt16BE(0); +} + +export async function nvmReadUInt8( + io: NVMIO, + position: number, +): Promise { + const { buffer } = await io.read(position, 1); + return buffer.readUInt8(0); +} + +export async function nvmWriteBuffer( + io: NVMIO, + position: number, + buffer: Buffer, +): Promise { + const chunkSize = await io.determineChunkSize(); + let offset = 0; + while (offset < buffer.length) { + const chunk = buffer.subarray(offset, offset + chunkSize); + const { bytesWritten } = await io.write(position + offset, chunk); + offset += bytesWritten; + } +} + +export async function nvmReadBuffer( + io: NVMIO, + position: number, + length: number, +): Promise { + const ret = Buffer.allocUnsafe(length); + const chunkSize = await io.determineChunkSize(); + let offset = 0; + while (offset < length) { + const { buffer, endOfFile } = await io.read( + position + offset, + Math.min(chunkSize, length - offset), + ); + buffer.copy(ret, offset); + offset += buffer.length; + if (endOfFile) break; + } + return ret.subarray(0, offset); +} diff --git a/packages/nvmedit/src/lib/io/BufferedNVMReader.ts b/packages/nvmedit/src/lib/io/BufferedNVMReader.ts new file mode 100644 index 00000000000..84fa990773a --- /dev/null +++ b/packages/nvmedit/src/lib/io/BufferedNVMReader.ts @@ -0,0 +1,111 @@ +import { type NVMAccess, type NVMIO } from "../common/definitions"; + +interface BufferedChunk { + offset: number; + data: Buffer; +} + +export class BufferedNVMReader implements NVMIO { + public constructor(inner: NVMIO) { + this._inner = inner; + } + + private _inner: NVMIO; + // Already-read chunks. There are a few rules to follow: + // - Offsets MUST be multiples of the chunk size + // - The size of each chunk must be exactly the chunk size + private _buffer: BufferedChunk[] = []; + + open(access: NVMAccess.Read | NVMAccess.Write): Promise { + return this._inner.open(access); + } + get size(): number { + return this._inner.size; + } + get accessMode(): NVMAccess { + return this._inner.accessMode; + } + determineChunkSize(): Promise { + return this._inner.determineChunkSize(); + } + + private async readBuffered( + alignedOffset: number, + chunkSize: number, + ): Promise { + let buffered = this._buffer.find((chunk) => + chunk.offset === alignedOffset + ); + if (!buffered) { + const { buffer: data } = await this._inner.read( + alignedOffset, + chunkSize, + ); + buffered = { data, offset: alignedOffset }; + this._buffer.push(buffered); + } + return buffered.data; + } + + async read( + offset: number, + length: number, + ): Promise<{ buffer: Buffer; endOfFile: boolean }> { + // Limit the read size to the chunk size. This ensures we have to deal with maximum 2 chunks or read requests + const chunkSize = await this.determineChunkSize(); + length = Math.min(length, chunkSize); + + // Figure out at which offsets to read + const firstChunkStart = offset - offset % chunkSize; + const secondChunkStart = (offset + length) + - (offset + length) % chunkSize; + + // Read one or two chunks, depending on how many are needed + const chunks: Buffer[] = []; + chunks.push(await this.readBuffered(firstChunkStart, chunkSize)); + if (secondChunkStart > firstChunkStart) { + chunks.push(await this.readBuffered(secondChunkStart, chunkSize)); + } + const alignedBuffer = Buffer.concat(chunks); + + // Then slice out the section we need + const endOfFile = offset + length >= this.size; + const buffer = alignedBuffer.subarray( + offset - firstChunkStart, + offset - firstChunkStart + length, + ); + + return { + buffer, + endOfFile, + }; + } + + async write( + offset: number, + data: Buffer, + ): Promise<{ bytesWritten: number; endOfFile: boolean }> { + const ret = await this._inner.write(offset, data); + + // Invalidate cached chunks + const chunkSize = await this.determineChunkSize(); + // Figure out at which offsets to read + const firstChunkStart = offset - offset % chunkSize; + const lastChunkStart = (offset + ret.bytesWritten) + - (offset + ret.bytesWritten) % chunkSize; + + // TODO: We should update existing chunks where possible + for (let i = firstChunkStart; i <= lastChunkStart; i += chunkSize) { + const index = this._buffer.findIndex((chunk) => chunk.offset === i); + if (index !== -1) { + this._buffer.splice(index, 1); + } + } + + return ret; + } + + close(): Promise { + return this._inner.close(); + } +} diff --git a/packages/nvmedit/src/lib/io/NVMFileIO.ts b/packages/nvmedit/src/lib/io/NVMFileIO.ts new file mode 100644 index 00000000000..8c683b351c6 --- /dev/null +++ b/packages/nvmedit/src/lib/io/NVMFileIO.ts @@ -0,0 +1,112 @@ +import { ZWaveError, ZWaveErrorCodes } from "@zwave-js/core"; +import fs, { type FileHandle } from "node:fs/promises"; +import { NVMAccess, type NVMIO } from "../common/definitions"; + +/** An implementation of NVMIO for the filesystem */ +export class NVMFileIO implements NVMIO { + public constructor(path: string) { + this._path = path; + this._accessMode = NVMAccess.None; + } + + private _path: string; + private _handle: FileHandle | undefined; + private _chunkSize = 16 * 1024; // We could read more, but 16 KB chunks are more than enough for reading NVM contents + + async open(access: NVMAccess): Promise { + let flags: string; + switch (access) { + case NVMAccess.Read: + flags = "r"; + break; + case NVMAccess.Write: + case NVMAccess.ReadWrite: + // Read/Write, don't create, don't truncate + flags = "r+"; + access = NVMAccess.ReadWrite; + break; + default: + throw new Error("Invalid access mode"); + } + this._handle = await fs.open(this._path, flags); + this._size = (await this._handle.stat()).size; + + this._accessMode = access; + return access; + } + + private _size: number | undefined; + get size(): number { + if (this._size == undefined) { + throw new ZWaveError( + "The NVM file is not open", + ZWaveErrorCodes.NVM_NotOpen, + ); + } + return this._size; + } + + private _accessMode: NVMAccess; + get accessMode(): NVMAccess { + return this._accessMode; + } + + determineChunkSize(): Promise { + return Promise.resolve(this._chunkSize); + } + + async read( + offset: number, + length: number, + ): Promise<{ buffer: Buffer; endOfFile: boolean }> { + if (this._handle == undefined) { + throw new ZWaveError( + "The NVM file is not open", + ZWaveErrorCodes.NVM_NotOpen, + ); + } + const readResult = await this._handle.read({ + buffer: Buffer.alloc(length), + position: offset, + }); + + const endOfFile = offset + readResult.bytesRead >= this.size; + return { + buffer: readResult.buffer.subarray(0, readResult.bytesRead), + endOfFile, + }; + } + + async write( + offset: number, + data: Buffer, + ): Promise<{ bytesWritten: number; endOfFile: boolean }> { + if (this._handle == undefined) { + throw new ZWaveError( + "The NVM file is not open", + ZWaveErrorCodes.NVM_NotOpen, + ); + } + if (offset + data.length > this.size) { + throw new ZWaveError( + "Write would exceed the NVM size", + ZWaveErrorCodes.NVM_NoSpace, + ); + } + const writeResult = await this._handle.write( + data, + 0, + data.length, + offset, + ); + const endOfFile = offset + writeResult.bytesWritten >= this.size; + return { bytesWritten: writeResult.bytesWritten, endOfFile }; + } + + async close(): Promise { + await this._handle?.close(); + this._handle = undefined; + this._accessMode = NVMAccess.None; + this._size = undefined; + } +} diff --git a/packages/nvmedit/src/lib/io/NVMMemoryIO.ts b/packages/nvmedit/src/lib/io/NVMMemoryIO.ts new file mode 100644 index 00000000000..01f34a88ada --- /dev/null +++ b/packages/nvmedit/src/lib/io/NVMMemoryIO.ts @@ -0,0 +1,62 @@ +import { ZWaveError, ZWaveErrorCodes } from "@zwave-js/core"; +import { NVMAccess, type NVMIO } from "../common/definitions"; + +/** An im-memory implementation of NVMIO */ +export class NVMMemoryIO implements NVMIO { + public constructor(buffer: Buffer) { + this._buffer = buffer; + } + + private _buffer: Buffer; + + open(_access: NVMAccess.Read | NVMAccess.Write): Promise { + // Nothing to do + return Promise.resolve(NVMAccess.ReadWrite); + } + + get size(): number { + return this._buffer.length; + } + + get accessMode(): NVMAccess { + return NVMAccess.ReadWrite; + } + + determineChunkSize(): Promise { + // We can read the entire buffer at once + return Promise.resolve(this._buffer.length); + } + + read( + offset: number, + length: number, + ): Promise<{ buffer: Buffer; endOfFile: boolean }> { + return Promise.resolve({ + buffer: this._buffer.subarray(offset, offset + length), + endOfFile: offset + length >= this._buffer.length, + }); + } + + write( + offset: number, + data: Buffer, + ): Promise<{ bytesWritten: number; endOfFile: boolean }> { + if (offset + data.length > this.size) { + throw new ZWaveError( + "Write would exceed the NVM size", + ZWaveErrorCodes.NVM_NoSpace, + ); + } + + data.copy(this._buffer, offset, 0, data.length); + return Promise.resolve({ + bytesWritten: data.length, + endOfFile: offset + data.length >= this._buffer.length, + }); + } + + close(): Promise { + // Nothing to do + return Promise.resolve(); + } +} diff --git a/packages/nvmedit/src/lib/nvm3/adapter.ts b/packages/nvmedit/src/lib/nvm3/adapter.ts new file mode 100644 index 00000000000..ed5e6a13da0 --- /dev/null +++ b/packages/nvmedit/src/lib/nvm3/adapter.ts @@ -0,0 +1,1289 @@ +import { ZWaveError, ZWaveErrorCodes } from "@zwave-js/core"; +import { num2hex } from "@zwave-js/shared"; +import { assertNever } from "alcalzone-shared/helpers"; +import { SUC_MAX_UPDATES } from "../../consts"; +import { type NVM3 } from "../NVM3"; +import { + type ControllerNVMProperty, + type LRNodeNVMProperty, + type NVMAdapter, + type NVMProperty, + type NVMPropertyToDataType, + type NodeNVMProperty, +} from "../common/definitions"; +import { type RouteCache } from "../common/routeCache"; +import { + type ApplicationCCsFile, + ApplicationCCsFileID, + ApplicationDataFile, + ApplicationDataFileID, + ApplicationNameFile, + ApplicationNameFileID, + type ApplicationRFConfigFile, + ApplicationRFConfigFileID, + type ApplicationTypeFile, + ApplicationTypeFileID, + type ApplicationVersionFile, + type ApplicationVersionFile800, + ApplicationVersionFile800ID, + ApplicationVersionFileID, + type ControllerInfoFile, + ControllerInfoFileID, + LRNodeInfoFileV5, + NVMFile, + NodeInfoFileV0, + NodeInfoFileV1, + ProtocolAppRouteLockNodeMaskFile, + ProtocolAppRouteLockNodeMaskFileID, + ProtocolLRNodeListFile, + ProtocolLRNodeListFileID, + ProtocolNodeListFile, + ProtocolNodeListFileID, + ProtocolPendingDiscoveryNodeMaskFile, + ProtocolPendingDiscoveryNodeMaskFileID, + ProtocolPreferredRepeatersFile, + ProtocolPreferredRepeatersFileID, + ProtocolRouteCacheExistsNodeMaskFile, + ProtocolRouteCacheExistsNodeMaskFileID, + ProtocolRouteSlaveSUCNodeMaskFile, + ProtocolRouteSlaveSUCNodeMaskFileID, + ProtocolSUCPendingUpdateNodeMaskFile, + ProtocolSUCPendingUpdateNodeMaskFileID, + type ProtocolVersionFile, + ProtocolVersionFileID, + ProtocolVirtualNodeMaskFile, + ProtocolVirtualNodeMaskFileID, + RouteCacheFileV0, + RouteCacheFileV1, + SUCUpdateEntriesFileIDV0, + SUCUpdateEntriesFileV0, + SUCUpdateEntriesFileV5, + SUCUpdateEntriesFileV5IDBase, + SUCUpdateEntriesFileV5IDMax, + SUC_UPDATES_PER_FILE_V5, + getNVMSectionByFileID, + nodeIdToLRNodeInfoFileIDV5, + nodeIdToNodeInfoFileIDV0, + nodeIdToNodeInfoFileIDV1, + nodeIdToRouteCacheFileIDV0, + nodeIdToRouteCacheFileIDV1, + sucUpdateIndexToSUCUpdateEntriesFileIDV5, +} from "./files"; + +const DEFAULT_FILE_VERSION = "7.0.0"; + +export class NVM3Adapter implements NVMAdapter { + public constructor(nvm: NVM3) { + this._nvm = nvm; + } + + private _nvm: NVM3; + private _initialized: boolean = false; + + private _protocolInfo: { + version: string; + format: number; + } | undefined; + private _applicationInfo: { + version: string; + format: number; + } | undefined; + + /** A list of pending changes that haven't been written to the NVM yet. `null` indicates a deleted entry. */ + private _pendingChanges: Map = new Map(); + + private getFileVersion(fileId: number): string { + if ( + fileId === ProtocolVersionFileID + || fileId === ApplicationVersionFileID + || fileId === ApplicationVersionFile800ID + ) { + return DEFAULT_FILE_VERSION; + } + const section = getNVMSectionByFileID(fileId); + if (section === "application") { + return this._applicationInfo?.version ?? DEFAULT_FILE_VERSION; + } else if (section === "protocol") { + return this._protocolInfo?.version ?? DEFAULT_FILE_VERSION; + } + + return DEFAULT_FILE_VERSION; + } + + private async init(): Promise { + if (!this._protocolInfo) { + const protocolVersionFile = await this._getFile< + ProtocolVersionFile + >( + ProtocolVersionFileID, + true, + ); + if (protocolVersionFile) { + const version = + `${protocolVersionFile.major}.${protocolVersionFile.minor}.${protocolVersionFile.patch}`; + this._protocolInfo = { + version, + format: protocolVersionFile.format, + }; + } + } + + if (!this._applicationInfo) { + const applicationVersionFile700 = await this._getFile< + ApplicationVersionFile + >(ApplicationVersionFileID, true); + const applicationVersionFile800 = await this._getFile< + ApplicationVersionFile800 + >(ApplicationVersionFile800ID, true); + const applicationVersionFile = applicationVersionFile700 + ?? applicationVersionFile800; + + if (applicationVersionFile) { + const version = + `${applicationVersionFile.major}.${applicationVersionFile.minor}.${applicationVersionFile.patch}`; + this._applicationInfo = { + version, + format: applicationVersionFile.format, + }; + } + } + + this._initialized = true; + } + + /** Adds a complete file to the list of pending changes */ + public setFile(file: NVMFile): void { + const { key, data } = file.serialize(); + this._pendingChanges.set(key, data); + } + + public async hasFile(fileId: number): Promise { + if (!this._initialized) await this.init(); + + if (this._pendingChanges.has(fileId)) { + return this._pendingChanges.get(fileId) !== null; + } else { + return this._nvm.has(fileId); + } + } + + private async _getFile( + fileId: number, + skipInit: boolean = false, + ): Promise { + if (!skipInit && !this._initialized) await this.init(); + + // Prefer pending changes over the actual NVM, so changes can be composed + let data: Buffer | null | undefined; + if (this._pendingChanges.has(fileId)) { + data = this._pendingChanges.get(fileId); + } else { + data = await this._nvm.get(fileId); + } + if (!data) return; + + const fileVersion = this.getFileVersion(fileId); + return NVMFile.from(fileId, data, fileVersion) as T; + } + + private async _expectFile( + fileId: number, + skipInit: boolean = false, + ): Promise { + const file = await this._getFile(fileId, skipInit); + if (!file) { + throw new ZWaveError( + `NVM file ${num2hex(fileId)} not found`, + ZWaveErrorCodes.NVM_ObjectNotFound, + ); + } + return file; + } + + public getFile( + fileId: number, + required: true, + ): Promise; + + public getFile( + fileId: number, + required?: false, + ): Promise; + + public getFile( + fileId: number, + required?: boolean, + ): Promise { + if (required) { + return this._expectFile(fileId) as any; + } else { + return this._getFile(fileId) as any; + } + } + + public get( + property: T, + required?: R, + ): Promise< + R extends true ? NVMPropertyToDataType + : (NVMPropertyToDataType | undefined) + > { + if (property.domain === "controller") { + return this.getControllerNVMProperty(property, !!required) as any; + } else if (property.domain === "lrnode") { + return this.getLRNodeNVMProperty(property, !!required) as any; + } else { + return this.getNodeNVMProperty(property, !!required) as any; + } + } + + private async getControllerNVMProperty( + property: ControllerNVMProperty, + required: boolean, + ): Promise { + const getFile = (fileId: number) => { + if (required) { + return this._expectFile(fileId); + } else { + return this._getFile(fileId); + } + }; + + switch (property.type) { + case "protocolVersion": { + const file = await getFile( + ProtocolVersionFileID, + ); + if (!file) return; + return `${file.major}.${file.minor}.${file.patch}`; + } + case "protocolFileFormat": { + const file = await getFile( + ProtocolVersionFileID, + ); + return file?.format; + } + case "applicationVersion": + case "applicationFileFormat": { + const file700 = await this._getFile( + ApplicationVersionFileID, + ); + const file800 = await this._getFile( + ApplicationVersionFile800ID, + ); + const file = file700 ?? file800; + + if (!file) { + if (required) { + throw new ZWaveError( + "ApplicationVersionFile not found!", + ZWaveErrorCodes.NVM_ObjectNotFound, + ); + } else { + return; + } + } + + if (property.type === "applicationVersion") { + return `${file.major}.${file.minor}.${file.patch}`; + } else if (property.type === "applicationFileFormat") { + return file?.format; + } + } + + case "applicationData": { + const file = await getFile( + ApplicationDataFileID, + ); + return file?.applicationData; + } + + case "applicationName": { + const file = await getFile( + ApplicationNameFileID, + ); + return file?.name; + } + + case "homeId": + case "nodeId": + case "lastNodeId": + case "staticControllerNodeId": + case "sucLastIndex": + case "controllerConfiguration": + case "sucAwarenessPushNeeded": + case "maxNodeId": + case "reservedId": + case "systemState": + case "lastNodeIdLR": + case "maxNodeIdLR": + case "reservedIdLR": + case "primaryLongRangeChannelId": + case "dcdcConfig": { + const file = await getFile( + ControllerInfoFileID, + ); + return file?.[property.type]; + } + + case "includedInsecurely": + case "includedSecurelyInsecureCCs": + case "includedSecurelySecureCCs": { + const file = await getFile( + ApplicationCCsFileID, + ); + return file?.[property.type]; + } + + case "rfRegion": + case "txPower": + case "measured0dBm": + case "enablePTI": + case "maxTXPower": + case "nodeIdType": { + const file = await getFile( + ApplicationRFConfigFileID, + ); + return file?.[property.type]; + } + + case "isListening": + case "optionalFunctionality": + case "genericDeviceClass": + case "specificDeviceClass": { + const file = await getFile( + ApplicationTypeFileID, + ); + return file?.[property.type]; + } + + case "preferredRepeaters": { + const file = await getFile( + ProtocolPreferredRepeatersFileID, + ); + return file?.nodeIds; + } + + case "appRouteLock": { + const file = await getFile< + ProtocolAppRouteLockNodeMaskFile + >( + ProtocolAppRouteLockNodeMaskFileID, + ); + return file?.nodeIds; + } + case "routeSlaveSUC": { + const file = await getFile< + ProtocolRouteSlaveSUCNodeMaskFile + >( + ProtocolRouteSlaveSUCNodeMaskFileID, + ); + return file?.nodeIds; + } + case "sucPendingUpdate": { + const file = await getFile< + ProtocolSUCPendingUpdateNodeMaskFile + >( + ProtocolSUCPendingUpdateNodeMaskFileID, + ); + return file?.nodeIds; + } + case "pendingDiscovery": { + const file = await getFile< + ProtocolPendingDiscoveryNodeMaskFile + >( + ProtocolPendingDiscoveryNodeMaskFileID, + ); + return file?.nodeIds; + } + + case "nodeIds": { + const file = await getFile( + ProtocolNodeListFileID, + ); + return file?.nodeIds; + } + + case "lrNodeIds": { + const file = await getFile( + ProtocolLRNodeListFileID, + ); + return file?.nodeIds; + } + + case "virtualNodeIds": { + const file = await getFile( + ProtocolVirtualNodeMaskFileID, + ); + return file?.nodeIds; + } + + case "sucUpdateEntries": { + if (this._protocolInfo!.format < 5) { + const file = await getFile( + SUCUpdateEntriesFileIDV0, + ); + return file?.updateEntries; + } else { + // V5 has split the entries into multiple files + const updateEntries = []; + for ( + let index = 0; + index < SUC_MAX_UPDATES; + index += SUC_UPDATES_PER_FILE_V5 + ) { + // None of the files are required + const file = await this._getFile< + SUCUpdateEntriesFileV5 + >( + sucUpdateIndexToSUCUpdateEntriesFileIDV5(index), + ); + if (!file) break; + updateEntries.push(...file.updateEntries); + } + return updateEntries; + } + } + + case "learnedHomeId": + case "commandClasses": + case "watchdogStarted": + case "powerLevelNormal": + case "powerLevelLow": + case "powerMode": + case "powerModeExtintEnable": + case "powerModeWutTimeout": + // 500 series only, not supported on 700+ + return; + + default: + assertNever(property.type); + } + } + + private async getNodeNVMProperty( + property: NodeNVMProperty, + required: boolean, + ): Promise { + const getFile = (fileId: number) => { + if (required) { + return this._expectFile(fileId); + } else { + return this._getFile(fileId); + } + }; + + switch (property.type) { + case "info": { + if (this._protocolInfo!.format < 1) { + const file = await getFile( + nodeIdToNodeInfoFileIDV0( + property.nodeId, + ), + ); + return file?.nodeInfo; + } else { + const file = await getFile( + nodeIdToNodeInfoFileIDV1( + property.nodeId, + ), + ); + return file?.nodeInfos.find((info) => + info.nodeId === property.nodeId + ); + } + } + + case "routes": { + // The existence of routes is stored separately + const nodeMaskFile = await this.getFile< + ProtocolRouteCacheExistsNodeMaskFile + >(ProtocolRouteCacheExistsNodeMaskFileID); + + // If the node is not marked as having routes, don't try to read them + if (!nodeMaskFile) return; + if (!nodeMaskFile.nodeIdSet.has(property.nodeId)) return; + + let routeCache: RouteCache | undefined; + if (this._protocolInfo!.format < 1) { + const file = await getFile( + nodeIdToRouteCacheFileIDV0(property.nodeId), + ); + routeCache = file?.routeCache; + } else { + const file = await getFile( + nodeIdToRouteCacheFileIDV1(property.nodeId), + ); + routeCache = file?.routeCaches.find((route) => + route.nodeId === property.nodeId + ); + } + if (!routeCache) return; + return { + lwr: routeCache.lwr, + nlwr: routeCache.nlwr, + }; + } + + default: + assertNever(property.type); + } + } + + private async getLRNodeNVMProperty( + property: LRNodeNVMProperty, + required: boolean, + ): Promise { + const getFile = (fileId: number) => { + if (required) { + return this._expectFile(fileId); + } else { + return this._getFile(fileId); + } + }; + + switch (property.type) { + case "info": { + const file = await getFile( + nodeIdToLRNodeInfoFileIDV5(property.nodeId), + ); + return file?.nodeInfos.find((info) => + info.nodeId === property.nodeId + ); + } + + default: + assertNever(property.type); + } + } + + public async set( + property: T, + value: NVMPropertyToDataType, + ): Promise { + if (!this._initialized) await this.init(); + + if (property.domain === "controller") { + return this.setControllerNVMProperty(property, value); + } else if (property.domain === "lrnode") { + return this.setLRNodeNVMProperty(property, value); + } else { + return this.setNodeNVMProperty(property, value); + } + } + + private async setControllerNVMProperty( + property: ControllerNVMProperty, + value: any, + ): Promise { + const failFileMissing = (): never => { + throw new ZWaveError( + "Cannot set property in NVM for non-existing file", + ZWaveErrorCodes.NVM_ObjectNotFound, + ); + }; + + const expectFile = async ( + fileId: number, + ): Promise => { + const file = await this._getFile(fileId); + if (!file) throw failFileMissing(); + return file; + }; + + const changedFiles: NVMFile[] = []; + const deletedFiles: number[] = []; + + switch (property.type) { + case "protocolVersion": { + const file = await expectFile( + ProtocolVersionFileID, + ); + const [major, minor, patch] = (value as string).split(".") + .map((part) => parseInt(part, 10)); + file.major = major; + file.minor = minor; + file.patch = patch; + changedFiles.push(file); + break; + } + case "protocolFileFormat": { + const file = await expectFile( + ProtocolVersionFileID, + ); + file.format = value; + changedFiles.push(file); + break; + } + case "applicationVersion": { + const file700 = await this._getFile( + ApplicationVersionFileID, + ); + const file800 = await this._getFile( + ApplicationVersionFile800ID, + ); + const file = file700 ?? file800; + if (!file) { + throw new ZWaveError( + "ApplicationVersionFile not found!", + ZWaveErrorCodes.NVM_ObjectNotFound, + ); + } + + const [major, minor, patch] = (value as string).split(".") + .map((part) => parseInt(part, 10)); + file.major = major; + file.minor = minor; + file.patch = patch; + changedFiles.push(file); + break; + } + case "applicationFileFormat": { + const file = await expectFile( + ApplicationVersionFileID, + ); + file.format = value; + changedFiles.push(file); + break; + } + + case "applicationData": { + const file = new ApplicationDataFile({ + applicationData: value, + fileVersion: this.getFileVersion(ApplicationDataFileID), + }); + file.applicationData = value; + changedFiles.push(file); + break; + } + + case "applicationName": { + const file = new ApplicationNameFile({ + name: value, + fileVersion: this.getFileVersion(ApplicationNameFileID), + }); + changedFiles.push(file); + break; + } + + case "homeId": + case "nodeId": + case "lastNodeId": + case "staticControllerNodeId": + case "sucLastIndex": + case "controllerConfiguration": + case "sucAwarenessPushNeeded": + case "maxNodeId": + case "reservedId": + case "systemState": + case "lastNodeIdLR": + case "maxNodeIdLR": + case "reservedIdLR": + case "primaryLongRangeChannelId": + case "dcdcConfig": { + const file = await expectFile( + ControllerInfoFileID, + ); + file[property.type] = value; + changedFiles.push(file); + break; + } + + case "includedInsecurely": + case "includedSecurelyInsecureCCs": + case "includedSecurelySecureCCs": { + const file = await expectFile( + ApplicationCCsFileID, + ); + file[property.type] = value; + changedFiles.push(file); + break; + } + + case "rfRegion": + case "txPower": + case "measured0dBm": + case "enablePTI": + case "maxTXPower": + case "nodeIdType": { + const file = await expectFile( + ApplicationRFConfigFileID, + ); + (file as any)[property.type] = value; + changedFiles.push(file); + break; + } + + case "isListening": + case "optionalFunctionality": + case "genericDeviceClass": + case "specificDeviceClass": { + const file = await expectFile( + ApplicationTypeFileID, + ); + (file as any)[property.type] = value; + changedFiles.push(file); + break; + } + + case "nodeIds": { + const file = await this._getFile( + ProtocolNodeListFileID, + ) ?? new ProtocolNodeListFile({ + nodeIds: [], + fileVersion: this.getFileVersion(ProtocolNodeListFileID), + }); + file.nodeIds = value; + changedFiles.push(file); + break; + } + + case "lrNodeIds": { + const file = await this._getFile( + ProtocolLRNodeListFileID, + ) ?? new ProtocolLRNodeListFile({ + nodeIds: [], + fileVersion: this.getFileVersion(ProtocolLRNodeListFileID), + }); + file.nodeIds = value; + changedFiles.push(file); + break; + } + + case "virtualNodeIds": { + const file = await this._getFile( + ProtocolVirtualNodeMaskFileID, + ) ?? new ProtocolVirtualNodeMaskFile({ + nodeIds: [], + fileVersion: this.getFileVersion( + ProtocolVirtualNodeMaskFileID, + ), + }); + file.nodeIds = value; + changedFiles.push(file); + break; + } + + case "preferredRepeaters": { + const file = new ProtocolPreferredRepeatersFile({ + nodeIds: value, + fileVersion: this.getFileVersion( + ProtocolPreferredRepeatersFileID, + ), + }); + changedFiles.push(file); + break; + } + + case "appRouteLock": { + const file = new ProtocolAppRouteLockNodeMaskFile({ + nodeIds: value, + fileVersion: this.getFileVersion( + ProtocolAppRouteLockNodeMaskFileID, + ), + }); + changedFiles.push(file); + break; + } + case "routeSlaveSUC": { + const file = new ProtocolRouteSlaveSUCNodeMaskFile({ + nodeIds: value, + fileVersion: this.getFileVersion( + ProtocolRouteSlaveSUCNodeMaskFileID, + ), + }); + changedFiles.push(file); + break; + } + case "sucPendingUpdate": { + const file = new ProtocolSUCPendingUpdateNodeMaskFile({ + nodeIds: value, + fileVersion: this.getFileVersion( + ProtocolSUCPendingUpdateNodeMaskFileID, + ), + }); + changedFiles.push(file); + break; + } + case "pendingDiscovery": { + const file = new ProtocolPendingDiscoveryNodeMaskFile({ + nodeIds: value, + fileVersion: this.getFileVersion( + ProtocolPendingDiscoveryNodeMaskFileID, + ), + }); + changedFiles.push(file); + break; + } + + case "sucUpdateEntries": { + if (this._protocolInfo!.format < 5) { + const file = new SUCUpdateEntriesFileV0({ + updateEntries: value, + fileVersion: this.getFileVersion( + SUCUpdateEntriesFileIDV0, + ), + }); + changedFiles.push(file); + break; + } else { + // V5 has split the entries into multiple files + for ( + let index = 0; + index < SUC_MAX_UPDATES; + index += SUC_UPDATES_PER_FILE_V5 + ) { + const fileId = sucUpdateIndexToSUCUpdateEntriesFileIDV5( + index, + ); + const fileExists = await this.hasFile(fileId); + const fileVersion = this.getFileVersion(fileId); + const slice = value.slice( + index, + index + SUC_UPDATES_PER_FILE_V5, + ); + if (slice.length > 0) { + const file = new SUCUpdateEntriesFileV5({ + updateEntries: slice, + fileVersion, + }); + changedFiles.push(file); + } else if (fileExists) { + deletedFiles.push(fileId); + } + } + } + break; + } + + case "learnedHomeId": + case "commandClasses": + case "watchdogStarted": + case "powerLevelNormal": + case "powerLevelLow": + case "powerMode": + case "powerModeExtintEnable": + case "powerModeWutTimeout": + // 500 series only, not supported on 700+ + return; + + default: + assertNever(property.type); + } + + for (const file of changedFiles) { + const { key, data } = file.serialize(); + this._pendingChanges.set(key, data); + } + for (const file of deletedFiles) { + this._pendingChanges.set(file, null); + } + } + + private async setLRNodeNVMProperty( + property: LRNodeNVMProperty, + value: any, + ): Promise { + const changedFiles: NVMFile[] = []; + const deletedFiles: number[] = []; + + switch (property.type) { + case "info": { + const fileId = nodeIdToLRNodeInfoFileIDV5(property.nodeId); + let file = await this._getFile( + fileId, + ); + if (value) { + // Info added or modified + file ??= new LRNodeInfoFileV5({ + nodeInfos: [], + fileVersion: this.getFileVersion(fileId), + }); + const existingIndex = file.nodeInfos.findIndex( + (info) => info.nodeId === property.nodeId, + ); + if (existingIndex !== -1) { + file.nodeInfos[existingIndex] = value; + } else { + file.nodeInfos.push(value); + } + changedFiles.push(file); + } else if (file) { + // info deleted + const existingIndex = file.nodeInfos.findIndex( + (info) => info.nodeId === property.nodeId, + ); + if (existingIndex !== -1) { + file.nodeInfos.splice(existingIndex, 1); + if (file.nodeInfos.length === 0) { + deletedFiles.push(fileId); + } else { + changedFiles.push(file); + } + } + } + + break; + } + + default: + assertNever(property.type); + } + + for (const file of changedFiles) { + const { key, data } = file.serialize(); + this._pendingChanges.set(key, data); + } + for (const file of deletedFiles) { + this._pendingChanges.set(file, null); + } + } + + private async setNodeNVMProperty( + property: NodeNVMProperty, + value: any, + ): Promise { + const changedFiles: NVMFile[] = []; + const deletedFiles: number[] = []; + + switch (property.type) { + case "info": { + if (this._protocolInfo!.format < 1) { + // V0, single node info per file + const fileId = nodeIdToNodeInfoFileIDV0(property.nodeId); + let file = await this._getFile(fileId); + if (value) { + // Info added or modified + file ??= new NodeInfoFileV0({ + nodeInfo: undefined as any, + fileVersion: this.getFileVersion(fileId), + }); + file.nodeInfo = value; + changedFiles.push(file); + } else { + // info deleted + deletedFiles.push(fileId); + } + } else { + // V1+, multiple node infos per file + const fileId = nodeIdToNodeInfoFileIDV1(property.nodeId); + let file = await this._getFile( + fileId, + ); + if (value) { + // Info added or modified + file ??= new NodeInfoFileV1({ + nodeInfos: [], + fileVersion: this.getFileVersion(fileId), + }); + const existingIndex = file.nodeInfos.findIndex( + (info) => info.nodeId === property.nodeId, + ); + if (existingIndex !== -1) { + file.nodeInfos[existingIndex] = value; + } else { + file.nodeInfos.push(value); + } + changedFiles.push(file); + } else if (file) { + // info deleted + const existingIndex = file.nodeInfos.findIndex( + (info) => info.nodeId === property.nodeId, + ); + if (existingIndex !== -1) { + file.nodeInfos.splice(existingIndex, 1); + if (file.nodeInfos.length === 0) { + deletedFiles.push(fileId); + } else { + changedFiles.push(file); + } + } + } + } + + break; + } + + case "routes": { + if (this._protocolInfo!.format < 1) { + // V0, single route per file + const fileId = nodeIdToRouteCacheFileIDV0(property.nodeId); + let file = await this._getFile(fileId); + if (value) { + // Route added or modified + file ??= new RouteCacheFileV0({ + routeCache: undefined as any, + fileVersion: this.getFileVersion(fileId), + }); + file.routeCache = { + nodeId: property.nodeId, + lwr: value.lwr, + nlwr: value.nlwr, + }; + changedFiles.push(file); + } else if (file) { + // Route deleted + deletedFiles.push(fileId); + } + } else { + // V1+, multiple routes per file + const fileId = nodeIdToRouteCacheFileIDV1(property.nodeId); + const file = await this._getFile( + fileId, + ) ?? new RouteCacheFileV1({ + routeCaches: [], + fileVersion: this.getFileVersion(fileId), + }); + const existingIndex = file.routeCaches.findIndex( + (route) => route.nodeId === property.nodeId, + ); + const newRoute: RouteCache = { + nodeId: property.nodeId, + lwr: value.lwr, + nlwr: value.nlwr, + }; + if (existingIndex !== -1) { + file.routeCaches[existingIndex] = newRoute; + } else { + file.routeCaches.push(newRoute); + } + changedFiles.push(file); + } + + // The existence of routes is stored separately + const nodeMaskFile = await this._getFile< + ProtocolRouteCacheExistsNodeMaskFile + >(ProtocolRouteCacheExistsNodeMaskFileID) + ?? new ProtocolRouteCacheExistsNodeMaskFile({ + nodeIds: [], + fileVersion: this.getFileVersion( + ProtocolRouteCacheExistsNodeMaskFileID, + ), + }); + + if (!value && nodeMaskFile.nodeIdSet.has(property.nodeId)) { + nodeMaskFile.nodeIdSet.delete(property.nodeId); + changedFiles.push(nodeMaskFile); + } else if ( + value && !nodeMaskFile.nodeIdSet.has(property.nodeId) + ) { + nodeMaskFile.nodeIdSet.add(property.nodeId); + changedFiles.push(nodeMaskFile); + } + + break; + } + + default: + assertNever(property.type); + } + + for (const file of changedFiles) { + const { key, data } = file.serialize(); + this._pendingChanges.set(key, data); + } + for (const file of deletedFiles) { + this._pendingChanges.set(file, null); + } + } + + public async delete( + property: NVMProperty, + ): Promise { + if (property.domain === "controller") { + switch (property.type) { + case "protocolVersion": + case "protocolFileFormat": { + this._pendingChanges.set(ProtocolVersionFileID, null); + return; + } + + case "applicationVersion": + case "applicationFileFormat": { + if (await this.hasFile(ApplicationVersionFileID)) { + this._pendingChanges.set( + ApplicationVersionFileID, + null, + ); + } + if (await this.hasFile(ApplicationVersionFile800ID)) { + this._pendingChanges.set( + ApplicationVersionFile800ID, + null, + ); + } + return; + } + + case "applicationData": { + this._pendingChanges.set(ApplicationDataFileID, null); + return; + } + + case "applicationName": { + this._pendingChanges.set(ApplicationNameFileID, null); + return; + } + + case "homeId": + case "nodeId": + case "lastNodeId": + case "staticControllerNodeId": + case "sucLastIndex": + case "controllerConfiguration": + case "sucAwarenessPushNeeded": + case "maxNodeId": + case "reservedId": + case "systemState": + case "lastNodeIdLR": + case "maxNodeIdLR": + case "reservedIdLR": + case "primaryLongRangeChannelId": + case "dcdcConfig": { + this._pendingChanges.set(ControllerInfoFileID, null); + return; + } + + case "includedInsecurely": + case "includedSecurelyInsecureCCs": + case "includedSecurelySecureCCs": { + this._pendingChanges.set(ApplicationCCsFileID, null); + return; + } + + case "rfRegion": + case "txPower": + case "measured0dBm": + case "enablePTI": + case "maxTXPower": + case "nodeIdType": { + this._pendingChanges.set(ApplicationRFConfigFileID, null); + return; + } + + case "isListening": + case "optionalFunctionality": + case "genericDeviceClass": + case "specificDeviceClass": { + this._pendingChanges.set(ApplicationTypeFileID, null); + return; + } + + case "nodeIds": { + this._pendingChanges.set( + ProtocolNodeListFileID, + null, + ); + return; + } + + case "lrNodeIds": { + this._pendingChanges.set( + ProtocolLRNodeListFileID, + null, + ); + return; + } + + case "virtualNodeIds": { + this._pendingChanges.set( + ProtocolVirtualNodeMaskFileID, + null, + ); + return; + } + + case "preferredRepeaters": { + this._pendingChanges.set( + ProtocolPreferredRepeatersFileID, + null, + ); + return; + } + + case "appRouteLock": { + this._pendingChanges.set( + ProtocolAppRouteLockNodeMaskFileID, + null, + ); + return; + } + case "routeSlaveSUC": { + this._pendingChanges.set( + ProtocolRouteSlaveSUCNodeMaskFileID, + null, + ); + return; + } + case "sucPendingUpdate": { + this._pendingChanges.set( + ProtocolSUCPendingUpdateNodeMaskFileID, + null, + ); + return; + } + case "pendingDiscovery": { + this._pendingChanges.set( + ProtocolPendingDiscoveryNodeMaskFileID, + null, + ); + return; + } + + case "sucUpdateEntries": { + if (this._protocolInfo!.format < 5) { + this._pendingChanges.set( + SUCUpdateEntriesFileIDV0, + null, + ); + } else { + for ( + let id = SUCUpdateEntriesFileV5IDBase; + id <= SUCUpdateEntriesFileV5IDMax; + id++ + ) { + if (await this.hasFile(id)) { + this._pendingChanges.set(id, null); + } + } + } + return; + } + + case "learnedHomeId": + case "commandClasses": + case "watchdogStarted": + case "powerLevelNormal": + case "powerLevelLow": + case "powerMode": + case "powerModeExtintEnable": + case "powerModeWutTimeout": + // 500 series only, not supported on 700+ + return; + + default: + assertNever(property); + } + } else if ( + property.domain === "lrnode" + ) { + // Node properties are handled by set(..., undefined) because + // it requires both modifying and deleting files + return this.setLRNodeNVMProperty(property, undefined); + } else if ( + property.domain === "node" + ) { + // Node properties are handled by set(..., undefined) because + // it requires both modifying and deleting files + return this.setNodeNVMProperty(property, undefined); + } + } + + public hasPendingChanges(): boolean { + return this._pendingChanges.size > 0; + } + + public async commit(): Promise { + await this._nvm.setMany([...this._pendingChanges]); + } +} diff --git a/packages/nvmedit/src/nvm3/consts.ts b/packages/nvmedit/src/lib/nvm3/consts.ts similarity index 100% rename from packages/nvmedit/src/nvm3/consts.ts rename to packages/nvmedit/src/lib/nvm3/consts.ts diff --git a/packages/nvmedit/src/files/ApplicationCCsFile.ts b/packages/nvmedit/src/lib/nvm3/files/ApplicationCCsFile.ts similarity index 91% rename from packages/nvmedit/src/files/ApplicationCCsFile.ts rename to packages/nvmedit/src/lib/nvm3/files/ApplicationCCsFile.ts index e6b8b9ac3e9..fc8cb797c5c 100644 --- a/packages/nvmedit/src/files/ApplicationCCsFile.ts +++ b/packages/nvmedit/src/lib/nvm3/files/ApplicationCCsFile.ts @@ -1,12 +1,12 @@ import { CommandClasses } from "@zwave-js/core/safe"; -import type { NVM3Object } from "../nvm3/object"; +import type { NVM3Object } from "../object"; import { NVMFile, type NVMFileCreationOptions, type NVMFileDeserializationOptions, - getNVMFileIDStatic, gotDeserializationOptions, nvmFileID, + nvmSection, } from "./NVMFile"; export interface ApplicationCCsFileOptions extends NVMFileCreationOptions { @@ -17,7 +17,10 @@ export interface ApplicationCCsFileOptions extends NVMFileCreationOptions { const MAX_CCs = 35; -@nvmFileID(103) +export const ApplicationCCsFileID = 103; + +@nvmFileID(ApplicationCCsFileID) +@nvmSection("application") export class ApplicationCCsFile extends NVMFile { public constructor( options: NVMFileDeserializationOptions | ApplicationCCsFileOptions, @@ -52,7 +55,7 @@ export class ApplicationCCsFile extends NVMFile { public includedSecurelyInsecureCCs: CommandClasses[]; public includedSecurelySecureCCs: CommandClasses[]; - public serialize(): NVM3Object { + public serialize(): NVM3Object & { data: Buffer } { this.payload = Buffer.alloc((1 + MAX_CCs) * 3); let offset = 0; for ( @@ -85,4 +88,3 @@ export class ApplicationCCsFile extends NVMFile { }; } } -export const ApplicationCCsFileID = getNVMFileIDStatic(ApplicationCCsFile); diff --git a/packages/nvmedit/src/files/ApplicationDataFile.ts b/packages/nvmedit/src/lib/nvm3/files/ApplicationDataFile.ts similarity index 65% rename from packages/nvmedit/src/files/ApplicationDataFile.ts rename to packages/nvmedit/src/lib/nvm3/files/ApplicationDataFile.ts index f1e5a2c52f2..031d0c24077 100644 --- a/packages/nvmedit/src/files/ApplicationDataFile.ts +++ b/packages/nvmedit/src/lib/nvm3/files/ApplicationDataFile.ts @@ -2,32 +2,34 @@ import { NVMFile, type NVMFileCreationOptions, type NVMFileDeserializationOptions, - getNVMFileIDStatic, gotDeserializationOptions, nvmFileID, + nvmSection, } from "./NVMFile"; export interface ApplicationDataFileOptions extends NVMFileCreationOptions { - data: Buffer; + applicationData: Buffer; } -@nvmFileID(200) +export const ApplicationDataFileID = 200; + +@nvmFileID(ApplicationDataFileID) +@nvmSection("application") export class ApplicationDataFile extends NVMFile { public constructor( options: NVMFileDeserializationOptions | ApplicationDataFileOptions, ) { super(options); if (!gotDeserializationOptions(options)) { - this.payload = options.data; + this.payload = options.applicationData; } } // Just binary data - public get data(): Buffer { + public get applicationData(): Buffer { return this.payload; } - public set data(value: Buffer) { + public set applicationData(value: Buffer) { this.payload = value; } } -export const ApplicationDataFileID = getNVMFileIDStatic(ApplicationDataFile); diff --git a/packages/nvmedit/src/files/ApplicationNameFile.ts b/packages/nvmedit/src/lib/nvm3/files/ApplicationNameFile.ts similarity index 80% rename from packages/nvmedit/src/files/ApplicationNameFile.ts rename to packages/nvmedit/src/lib/nvm3/files/ApplicationNameFile.ts index a40bcf0f855..db5ba967aa8 100644 --- a/packages/nvmedit/src/files/ApplicationNameFile.ts +++ b/packages/nvmedit/src/lib/nvm3/files/ApplicationNameFile.ts @@ -1,19 +1,22 @@ import { cpp2js } from "@zwave-js/shared"; -import { type NVMObject } from ".."; +import { type NVM3Object } from "../object"; import { NVMFile, type NVMFileCreationOptions, type NVMFileDeserializationOptions, - getNVMFileIDStatic, gotDeserializationOptions, nvmFileID, + nvmSection, } from "./NVMFile"; export interface ApplicationNameFileOptions extends NVMFileCreationOptions { name: string; } -@nvmFileID(0x4100c) +export const ApplicationNameFileID = 0x4100c; + +@nvmFileID(ApplicationNameFileID) +@nvmSection("application") export class ApplicationNameFile extends NVMFile { public constructor( options: NVMFileDeserializationOptions | ApplicationNameFileOptions, @@ -28,7 +31,7 @@ export class ApplicationNameFile extends NVMFile { public name: string; - public serialize(): NVMObject { + public serialize(): NVM3Object & { data: Buffer } { // Return a zero-terminated string with a fixed length of 30 bytes const nameAsString = Buffer.from(this.name, "utf8"); this.payload = Buffer.alloc(30, 0); @@ -36,4 +39,3 @@ export class ApplicationNameFile extends NVMFile { return super.serialize(); } } -export const ApplicationNameFileID = getNVMFileIDStatic(ApplicationNameFile); diff --git a/packages/nvmedit/src/files/ApplicationRFConfigFile.ts b/packages/nvmedit/src/lib/nvm3/files/ApplicationRFConfigFile.ts similarity index 94% rename from packages/nvmedit/src/files/ApplicationRFConfigFile.ts rename to packages/nvmedit/src/lib/nvm3/files/ApplicationRFConfigFile.ts index 5c824be842b..9a096d3639c 100644 --- a/packages/nvmedit/src/files/ApplicationRFConfigFile.ts +++ b/packages/nvmedit/src/lib/nvm3/files/ApplicationRFConfigFile.ts @@ -6,14 +6,14 @@ import { } from "@zwave-js/core/safe"; import { type AllOrNone, getEnumMemberName } from "@zwave-js/shared/safe"; import semver from "semver"; -import type { NVM3Object } from "../nvm3/object"; +import type { NVM3Object } from "../object"; import { NVMFile, type NVMFileCreationOptions, type NVMFileDeserializationOptions, - getNVMFileIDStatic, gotDeserializationOptions, nvmFileID, + nvmSection, } from "./NVMFile"; export type ApplicationRFConfigFileOptions = @@ -31,7 +31,10 @@ export type ApplicationRFConfigFileOptions = nodeIdType?: number; }; -@nvmFileID(104) +export const ApplicationRFConfigFileID = 104; + +@nvmFileID(ApplicationRFConfigFileID) +@nvmSection("application") export class ApplicationRFConfigFile extends NVMFile { public constructor( options: NVMFileDeserializationOptions | ApplicationRFConfigFileOptions, @@ -82,7 +85,7 @@ export class ApplicationRFConfigFile extends NVMFile { public maxTXPower?: number; public nodeIdType?: NodeIDType; - public serialize(): NVM3Object { + public serialize(): NVM3Object & { data: Buffer } { if (semver.lt(this.fileVersion, "7.18.1")) { this.payload = Buffer.alloc( semver.gte(this.fileVersion, "7.15.3") ? 6 : 3, @@ -137,6 +140,3 @@ export class ApplicationRFConfigFile extends NVMFile { return ret; } } -export const ApplicationRFConfigFileID = getNVMFileIDStatic( - ApplicationRFConfigFile, -); diff --git a/packages/nvmedit/src/files/ApplicationTypeFile.ts b/packages/nvmedit/src/lib/nvm3/files/ApplicationTypeFile.ts similarity index 88% rename from packages/nvmedit/src/files/ApplicationTypeFile.ts rename to packages/nvmedit/src/lib/nvm3/files/ApplicationTypeFile.ts index c67da1e5c0a..112a79cedcb 100644 --- a/packages/nvmedit/src/files/ApplicationTypeFile.ts +++ b/packages/nvmedit/src/lib/nvm3/files/ApplicationTypeFile.ts @@ -1,11 +1,11 @@ -import type { NVM3Object } from "../nvm3/object"; +import type { NVM3Object } from "../object"; import { NVMFile, type NVMFileCreationOptions, type NVMFileDeserializationOptions, - getNVMFileIDStatic, gotDeserializationOptions, nvmFileID, + nvmSection, } from "./NVMFile"; export interface ApplicationTypeFileOptions extends NVMFileCreationOptions { @@ -15,7 +15,10 @@ export interface ApplicationTypeFileOptions extends NVMFileCreationOptions { specificDeviceClass: number; } -@nvmFileID(102) +export const ApplicationTypeFileID = 102; + +@nvmFileID(ApplicationTypeFileID) +@nvmSection("application") export class ApplicationTypeFile extends NVMFile { public constructor( options: NVMFileDeserializationOptions | ApplicationTypeFileOptions, @@ -39,7 +42,7 @@ export class ApplicationTypeFile extends NVMFile { public genericDeviceClass: number; public specificDeviceClass: number; - public serialize(): NVM3Object { + public serialize(): NVM3Object & { data: Buffer } { this.payload = Buffer.from([ (this.isListening ? 0b1 : 0) | (this.optionalFunctionality ? 0b10 : 0), @@ -60,4 +63,3 @@ export class ApplicationTypeFile extends NVMFile { }; } } -export const ApplicationTypeFileID = getNVMFileIDStatic(ApplicationTypeFile); diff --git a/packages/nvmedit/src/files/ControllerInfoFile.ts b/packages/nvmedit/src/lib/nvm3/files/ControllerInfoFile.ts similarity index 96% rename from packages/nvmedit/src/files/ControllerInfoFile.ts rename to packages/nvmedit/src/lib/nvm3/files/ControllerInfoFile.ts index e8e92e93594..c1a4bfd0b10 100644 --- a/packages/nvmedit/src/files/ControllerInfoFile.ts +++ b/packages/nvmedit/src/lib/nvm3/files/ControllerInfoFile.ts @@ -4,14 +4,14 @@ import { stripUndefined, } from "@zwave-js/core/safe"; import { buffer2hex } from "@zwave-js/shared"; -import type { NVM3Object } from "../nvm3/object"; +import type { NVM3Object } from "../object"; import { NVMFile, type NVMFileCreationOptions, type NVMFileDeserializationOptions, - getNVMFileIDStatic, gotDeserializationOptions, nvmFileID, + nvmSection, } from "./NVMFile"; export type ControllerInfoFileOptions = @@ -40,7 +40,10 @@ export type ControllerInfoFileOptions = } ); -@nvmFileID(0x50004) +export const ControllerInfoFileID = 0x50004; + +@nvmFileID(ControllerInfoFileID) +@nvmSection("protocol") export class ControllerInfoFile extends NVMFile { public constructor( options: NVMFileDeserializationOptions | ControllerInfoFileOptions, @@ -117,7 +120,7 @@ export class ControllerInfoFile extends NVMFile { public primaryLongRangeChannelId?: number; public dcdcConfig?: number; - public serialize(): NVM3Object { + public serialize(): NVM3Object & { data: Buffer } { if (this.lastNodeIdLR != undefined) { this.payload = Buffer.allocUnsafe(22); this.homeId.copy(this.payload, 0); @@ -176,4 +179,3 @@ export class ControllerInfoFile extends NVMFile { }); } } -export const ControllerInfoFileID = getNVMFileIDStatic(ControllerInfoFile); diff --git a/packages/nvmedit/src/files/NVMFile.ts b/packages/nvmedit/src/lib/nvm3/files/NVMFile.ts similarity index 62% rename from packages/nvmedit/src/files/NVMFile.ts rename to packages/nvmedit/src/lib/nvm3/files/NVMFile.ts index 2db10d1a3e2..4717bf6792b 100644 --- a/packages/nvmedit/src/files/NVMFile.ts +++ b/packages/nvmedit/src/lib/nvm3/files/NVMFile.ts @@ -1,24 +1,22 @@ +import { createSimpleReflectionDecorator } from "@zwave-js/core"; import { ZWaveError, ZWaveErrorCodes } from "@zwave-js/core/safe"; -import type { TypedClassDecorator } from "@zwave-js/shared"; -import { - FragmentType, - NVM3_MAX_OBJ_SIZE_SMALL, - ObjectType, -} from "../nvm3/consts"; -import type { NVM3Object } from "../nvm3/object"; +import { type TypedClassDecorator, num2hex } from "@zwave-js/shared"; +import { FragmentType, NVM3_MAX_OBJ_SIZE_SMALL, ObjectType } from "../consts"; +import type { NVM3Object } from "../object"; export interface NVMFileBaseOptions { fileId?: number; fileVersion: string; } export interface NVMFileDeserializationOptions extends NVMFileBaseOptions { - object: NVM3Object; + fileId: number; + data: Buffer; } export function gotDeserializationOptions( options: NVMFileOptions, ): options is NVMFileDeserializationOptions { - return "object" in options; + return "data" in options && Buffer.isBuffer(options.data); } export interface NVMFileCreationOptions extends NVMFileBaseOptions {} @@ -32,61 +30,54 @@ export class NVMFile { this.fileVersion = options.fileVersion; if (gotDeserializationOptions(options)) { - this.fileId = options.object.key; - this.object = options.object; + this.fileId = options.fileId; + this.payload = options.data; } else { const fileId = getNVMFileID(this); if (typeof fileId === "number") { this.fileId = fileId; } - - this.object = { - key: this.fileId, - fragmentType: FragmentType.None, - type: ObjectType.DataLarge, - }; + this.payload = Buffer.allocUnsafe(0); } - - this.payload = this.object.data ?? Buffer.allocUnsafe(0); } - protected object: NVM3Object; protected payload: Buffer; public fileId: number = 0; public fileVersion: string; /** - * Creates an instance of the CC that is serialized in the given buffer + * Creates an instance of the NVM file that is contained in the given NVM object */ - public static from(object: NVM3Object, fileVersion: string): NVMFile { - // Fall back to unspecified command class in case we receive one that is not implemented - const Constructor = getNVMFileConstructor(object.key)!; + public static from( + fileId: number, + data: Buffer, + fileVersion: string, + ): NVMFile { + const Constructor = getNVMFileConstructor(fileId)!; return new Constructor({ - fileId: object.key, + fileId, fileVersion, - object, + data, }); } /** * Serializes this NVMFile into an NVM Object */ - public serialize(): NVM3Object { + public serialize(): NVM3Object & { data: Buffer } { if (!this.fileId) { throw new Error("The NVM file ID must be set before serializing"); } - this.object.key = this.fileId; - this.object.data = this.payload; - // We only support large and small data objects for now - if (this.payload.length <= NVM3_MAX_OBJ_SIZE_SMALL) { - this.object.type = ObjectType.DataSmall; - } else { - this.object.type = ObjectType.DataLarge; - } - // By default output unfragmented objects, they will be split later - this.object.fragmentType = FragmentType.None; - - return this.object; + return { + key: this.fileId, + data: this.payload, + // We only support large and small data objects for now + type: this.payload.length <= NVM3_MAX_OBJ_SIZE_SMALL + ? ObjectType.DataSmall + : ObjectType.DataLarge, + // By default output unfragmented objects, they will be split later + fragmentType: FragmentType.None, + }; } public toJSON(): Record { @@ -183,3 +174,38 @@ export function getNVMFileIDStatic>( } return ret; } + +export type NVMSection = "application" | "protocol"; + +const nvmSectionDecorator = createSimpleReflectionDecorator< + NVMFile, + [section: NVMSection] +>({ + name: "nvmSection", +}); + +/** Defines in which section an NVM file is stored */ +export const nvmSection = nvmSectionDecorator.decorator; + +/** Returns in which section an NVM file is stored (using an instance of the file) */ +export const getNVMSection = nvmSectionDecorator.lookupValue; + +/** Returns in which section an NVM file is stored (using the constructor of the file) */ +export const getNVMSectionStatic = nvmSectionDecorator.lookupValueStatic; + +/** Returns in which NVM section the file with the given ID resides in */ +export function getNVMSectionByFileID(fileId: number): NVMSection { + const File = getNVMFileConstructor(fileId); + let ret: NVMSection | undefined; + if (File) { + ret = getNVMSectionStatic(File); + } + if (ret) return ret; + + throw new ZWaveError( + `NVM section for file with ID ${ + num2hex(fileId) + } could not be determined`, + ZWaveErrorCodes.Argument_Invalid, + ); +} diff --git a/packages/nvmedit/src/files/NodeInfoFiles.ts b/packages/nvmedit/src/lib/nvm3/files/NodeInfoFiles.ts similarity index 97% rename from packages/nvmedit/src/files/NodeInfoFiles.ts rename to packages/nvmedit/src/lib/nvm3/files/NodeInfoFiles.ts index 7d79b8da5f9..a49d39698eb 100644 --- a/packages/nvmedit/src/files/NodeInfoFiles.ts +++ b/packages/nvmedit/src/lib/nvm3/files/NodeInfoFiles.ts @@ -12,13 +12,14 @@ import { parseNodeProtocolInfo, } from "@zwave-js/core/safe"; import { pick } from "@zwave-js/shared/safe"; -import type { NVM3Object } from "../nvm3/object"; +import type { NVM3Object } from "../object"; import { NVMFile, type NVMFileCreationOptions, type NVMFileDeserializationOptions, gotDeserializationOptions, nvmFileID, + nvmSection, } from "./NVMFile"; export const NODEINFOS_PER_FILE_V1 = 4; @@ -195,6 +196,7 @@ export function nodeIdToNodeInfoFileIDV0(nodeId: number): number { @nvmFileID( (id) => id >= NodeInfoFileV0IDBase && id < NodeInfoFileV0IDBase + MAX_NODES, ) +@nvmSection("protocol") export class NodeInfoFileV0 extends NVMFile { public constructor( options: NVMFileDeserializationOptions | NodeInfoFileV0Options, @@ -213,7 +215,7 @@ export class NodeInfoFileV0 extends NVMFile { public nodeInfo: NodeInfo; - public serialize(): NVM3Object { + public serialize(): NVM3Object & { data: Buffer } { this.fileId = nodeIdToNodeInfoFileIDV0(this.nodeInfo.nodeId); this.payload = encodeNodeInfo(this.nodeInfo); return super.serialize(); @@ -244,6 +246,7 @@ export function nodeIdToNodeInfoFileIDV1(nodeId: number): number { id >= NodeInfoFileV1IDBase && id < NodeInfoFileV1IDBase + MAX_NODES / NODEINFOS_PER_FILE_V1, ) +@nvmSection("protocol") export class NodeInfoFileV1 extends NVMFile { public constructor( options: NVMFileDeserializationOptions | NodeInfoFileV1Options, @@ -277,7 +280,7 @@ export class NodeInfoFileV1 extends NVMFile { public nodeInfos: NodeInfo[]; - public serialize(): NVM3Object { + public serialize(): NVM3Object & { data: Buffer } { // The infos must be sorted by node ID this.nodeInfos.sort((a, b) => a.nodeId - b.nodeId); const minNodeId = this.nodeInfos[0].nodeId; @@ -329,6 +332,7 @@ export function nodeIdToLRNodeInfoFileIDV5(nodeId: number): number { && id < LRNodeInfoFileV5IDBase + MAX_NODES_LR / LR_NODEINFOS_PER_FILE_V5, ) +@nvmSection("protocol") export class LRNodeInfoFileV5 extends NVMFile { public constructor( options: NVMFileDeserializationOptions | LRNodeInfoFileV5Options, @@ -362,7 +366,7 @@ export class LRNodeInfoFileV5 extends NVMFile { public nodeInfos: LRNodeInfo[]; - public serialize(): NVM3Object { + public serialize(): NVM3Object & { data: Buffer } { // The infos must be sorted by node ID this.nodeInfos.sort((a, b) => a.nodeId - b.nodeId); const minNodeId = this.nodeInfos[0].nodeId; diff --git a/packages/nvmedit/src/lib/nvm3/files/ProtocolNodeMaskFiles.ts b/packages/nvmedit/src/lib/nvm3/files/ProtocolNodeMaskFiles.ts new file mode 100644 index 00000000000..a9e0b2eee6d --- /dev/null +++ b/packages/nvmedit/src/lib/nvm3/files/ProtocolNodeMaskFiles.ts @@ -0,0 +1,141 @@ +import { NODE_ID_MAX, encodeBitMask, parseBitMask } from "@zwave-js/core/safe"; +import type { NVM3Object } from "../object"; +import { + NVMFile, + type NVMFileCreationOptions, + type NVMFileDeserializationOptions, + gotDeserializationOptions, + nvmFileID, + nvmSection, +} from "./NVMFile"; + +export interface ProtocolNodeMaskFileOptions extends NVMFileCreationOptions { + nodeIds: number[]; +} + +export class ProtocolNodeMaskFile extends NVMFile { + public constructor( + options: NVMFileDeserializationOptions | ProtocolNodeMaskFileOptions, + ) { + super(options); + if (gotDeserializationOptions(options)) { + this.nodeIdSet = new Set(parseBitMask(this.payload)); + } else { + this.nodeIdSet = new Set(options.nodeIds); + } + } + + public nodeIdSet: Set; + public get nodeIds(): number[] { + return [...this.nodeIdSet]; + } + public set nodeIds(value: number[]) { + this.nodeIdSet = new Set(value); + } + + public serialize(): NVM3Object & { data: Buffer } { + this.payload = encodeBitMask([...this.nodeIdSet], NODE_ID_MAX); + return super.serialize(); + } + + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + public toJSON() { + return { + ...super.toJSON(), + "node IDs": [...this.nodeIdSet].join(", "), + }; + } +} + +export const ProtocolPreferredRepeatersFileID = 0x50002; + +@nvmFileID(ProtocolPreferredRepeatersFileID) +@nvmSection("protocol") +export class ProtocolPreferredRepeatersFile extends ProtocolNodeMaskFile {} + +export const ProtocolNodeListFileID = 0x50005; + +@nvmFileID(ProtocolNodeListFileID) +@nvmSection("protocol") +export class ProtocolNodeListFile extends ProtocolNodeMaskFile {} + +export const ProtocolAppRouteLockNodeMaskFileID = 0x50006; + +@nvmFileID(ProtocolAppRouteLockNodeMaskFileID) +@nvmSection("protocol") +export class ProtocolAppRouteLockNodeMaskFile extends ProtocolNodeMaskFile {} + +export const ProtocolRouteSlaveSUCNodeMaskFileID = 0x50007; + +@nvmFileID(ProtocolRouteSlaveSUCNodeMaskFileID) +@nvmSection("protocol") +export class ProtocolRouteSlaveSUCNodeMaskFile extends ProtocolNodeMaskFile {} + +export const ProtocolSUCPendingUpdateNodeMaskFileID = 0x50008; + +@nvmFileID(ProtocolSUCPendingUpdateNodeMaskFileID) +@nvmSection("protocol") +export class ProtocolSUCPendingUpdateNodeMaskFile + extends ProtocolNodeMaskFile +{} + +export const ProtocolVirtualNodeMaskFileID = 0x50009; + +@nvmFileID(ProtocolVirtualNodeMaskFileID) +@nvmSection("protocol") +export class ProtocolVirtualNodeMaskFile extends ProtocolNodeMaskFile {} + +export const ProtocolPendingDiscoveryNodeMaskFileID = 0x5000a; + +@nvmFileID(ProtocolPendingDiscoveryNodeMaskFileID) +@nvmSection("protocol") +export class ProtocolPendingDiscoveryNodeMaskFile + extends ProtocolNodeMaskFile +{} + +export const ProtocolRouteCacheExistsNodeMaskFileID = 0x5000b; + +@nvmFileID(ProtocolRouteCacheExistsNodeMaskFileID) +@nvmSection("protocol") +export class ProtocolRouteCacheExistsNodeMaskFile + extends ProtocolNodeMaskFile +{} + +export const ProtocolLRNodeListFileID = 0x5000c; + +@nvmFileID(ProtocolLRNodeListFileID) +@nvmSection("protocol") +export class ProtocolLRNodeListFile extends NVMFile { + public constructor( + options: NVMFileDeserializationOptions | ProtocolNodeMaskFileOptions, + ) { + super(options); + if (gotDeserializationOptions(options)) { + this.nodeIdSet = new Set(parseBitMask(this.payload, 256)); + } else { + this.nodeIdSet = new Set(options.nodeIds); + } + } + + public nodeIdSet: Set; + public get nodeIds(): number[] { + return [...this.nodeIdSet]; + } + public set nodeIds(value: number[]) { + this.nodeIdSet = new Set(value); + } + + public serialize(): NVM3Object & { data: Buffer } { + // There are only 128 bytes for the bitmask, so the LR node IDs only go up to 1279 + this.payload = encodeBitMask([...this.nodeIdSet], 1279, 256); + return super.serialize(); + } + + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + public toJSON() { + return { + ...super.toJSON(), + "node IDs": [...this.nodeIdSet].join(", "), + }; + } +} diff --git a/packages/nvmedit/src/files/RouteCacheFiles.ts b/packages/nvmedit/src/lib/nvm3/files/RouteCacheFiles.ts similarity index 67% rename from packages/nvmedit/src/files/RouteCacheFiles.ts rename to packages/nvmedit/src/lib/nvm3/files/RouteCacheFiles.ts index 116c7b367c3..cb396325836 100644 --- a/packages/nvmedit/src/files/RouteCacheFiles.ts +++ b/packages/nvmedit/src/lib/nvm3/files/RouteCacheFiles.ts @@ -1,85 +1,23 @@ +import { MAX_NODES, MAX_REPEATERS } from "@zwave-js/core/safe"; import { - type FLiRS, - MAX_NODES, - MAX_REPEATERS, - RouteProtocolDataRate, - protocolDataRateMask, -} from "@zwave-js/core/safe"; -import type { NVM3Object } from "../nvm3/object"; + EMPTY_ROUTECACHE_FILL, + ROUTECACHE_SIZE, + type RouteCache, + emptyRouteCache, + encodeRoute, + parseRoute, +} from "../../common/routeCache"; +import type { NVM3Object } from "../object"; import { NVMFile, type NVMFileCreationOptions, type NVMFileDeserializationOptions, gotDeserializationOptions, nvmFileID, + nvmSection, } from "./NVMFile"; export const ROUTECACHES_PER_FILE_V1 = 8; -const ROUTE_SIZE = MAX_REPEATERS + 1; -const ROUTECACHE_SIZE = 2 * ROUTE_SIZE; -const EMPTY_ROUTECACHE_FILL = 0xff; -const emptyRouteCache = Buffer.alloc(ROUTECACHE_SIZE, EMPTY_ROUTECACHE_FILL); - -enum Beaming { - "1000ms" = 0x40, - "250ms" = 0x20, -} - -export interface Route { - beaming: FLiRS; - protocolRate: RouteProtocolDataRate; - repeaterNodeIDs?: number[]; -} - -export interface RouteCache { - nodeId: number; - lwr: Route; - nlwr: Route; -} - -export function parseRoute(buffer: Buffer, offset: number): Route { - const routeConf = buffer[offset + MAX_REPEATERS]; - const ret: Route = { - beaming: (Beaming[routeConf & 0x60] ?? false) as FLiRS, - protocolRate: routeConf & protocolDataRateMask, - repeaterNodeIDs: [ - ...buffer.subarray(offset, offset + MAX_REPEATERS), - ].filter((id) => id !== 0), - }; - if (ret.repeaterNodeIDs![0] === 0xfe) delete ret.repeaterNodeIDs; - return ret; -} - -export function encodeRoute(route: Route | undefined): Buffer { - const ret = Buffer.alloc(ROUTE_SIZE, 0); - if (route) { - if (route.repeaterNodeIDs) { - for ( - let i = 0; - i < MAX_REPEATERS && i < route.repeaterNodeIDs.length; - i++ - ) { - ret[i] = route.repeaterNodeIDs[i]; - } - } else { - ret[0] = 0xfe; - } - let routeConf = 0; - if (route.beaming) routeConf |= Beaming[route.beaming] ?? 0; - routeConf |= route.protocolRate & protocolDataRateMask; - ret[ROUTE_SIZE - 1] = routeConf; - } - - return ret; -} - -export function getEmptyRoute(): Route { - return { - beaming: false, - protocolRate: RouteProtocolDataRate.ZWave_40k, - repeaterNodeIDs: undefined, - }; -} export interface RouteCacheFileV0Options extends NVMFileCreationOptions { routeCache: RouteCache; @@ -94,6 +32,7 @@ export function nodeIdToRouteCacheFileIDV0(nodeId: number): number { (id) => id >= RouteCacheFileV0IDBase && id < RouteCacheFileV0IDBase + MAX_NODES, ) +@nvmSection("protocol") export class RouteCacheFileV0 extends NVMFile { public constructor( options: NVMFileDeserializationOptions | RouteCacheFileV0Options, @@ -111,7 +50,7 @@ export class RouteCacheFileV0 extends NVMFile { public routeCache: RouteCache; - public serialize(): NVM3Object { + public serialize(): NVM3Object & { data: Buffer } { this.fileId = nodeIdToRouteCacheFileIDV0(this.routeCache.nodeId); this.payload = Buffer.concat([ encodeRoute(this.routeCache.lwr), @@ -146,6 +85,7 @@ export function nodeIdToRouteCacheFileIDV1(nodeId: number): number { id >= RouteCacheFileV1IDBase && id < RouteCacheFileV1IDBase + MAX_NODES / ROUTECACHES_PER_FILE_V1, ) +@nvmSection("protocol") export class RouteCacheFileV1 extends NVMFile { public constructor( options: NVMFileDeserializationOptions | RouteCacheFileV1Options, @@ -180,7 +120,7 @@ export class RouteCacheFileV1 extends NVMFile { public routeCaches: RouteCache[]; - public serialize(): NVM3Object { + public serialize(): NVM3Object & { data: Buffer } { // The route infos must be sorted by node ID this.routeCaches.sort((a, b) => a.nodeId - b.nodeId); const minNodeId = this.routeCaches[0].nodeId; diff --git a/packages/nvmedit/src/files/SUCUpdateEntriesFile.ts b/packages/nvmedit/src/lib/nvm3/files/SUCUpdateEntriesFile.ts similarity index 61% rename from packages/nvmedit/src/files/SUCUpdateEntriesFile.ts rename to packages/nvmedit/src/lib/nvm3/files/SUCUpdateEntriesFile.ts index 49ef6c6d3f2..07a44d7c86b 100644 --- a/packages/nvmedit/src/files/SUCUpdateEntriesFile.ts +++ b/packages/nvmedit/src/lib/nvm3/files/SUCUpdateEntriesFile.ts @@ -1,23 +1,17 @@ +import { SUC_MAX_UPDATES, SUC_UPDATE_ENTRY_SIZE } from "../../../consts"; import { - type CommandClasses, - ZWaveError, - ZWaveErrorCodes, - encodeCCList, - parseCCList, -} from "@zwave-js/core/safe"; -import { - SUC_MAX_UPDATES, - SUC_UPDATE_ENTRY_SIZE, - SUC_UPDATE_NODEPARM_MAX, -} from "../consts"; -import type { NVM3Object } from "../nvm3/object"; + type SUCUpdateEntry, + encodeSUCUpdateEntry, + parseSUCUpdateEntry, +} from "../../common/sucUpdateEntry"; +import type { NVM3Object } from "../object"; import { NVMFile, type NVMFileCreationOptions, type NVMFileDeserializationOptions, - getNVMFileIDStatic, gotDeserializationOptions, nvmFileID, + nvmSection, } from "./NVMFile"; export const SUC_UPDATES_PER_FILE_V5 = 8; @@ -26,54 +20,10 @@ export interface SUCUpdateEntriesFileOptions extends NVMFileCreationOptions { updateEntries: SUCUpdateEntry[]; } -export interface SUCUpdateEntry { - nodeId: number; - changeType: number; // TODO: This is some kind of enum - supportedCCs: CommandClasses[]; - controlledCCs: CommandClasses[]; -} - -export function parseSUCUpdateEntry( - buffer: Buffer, - offset: number, -): SUCUpdateEntry | undefined { - const slice = buffer.subarray(offset, offset + SUC_UPDATE_ENTRY_SIZE); - if (slice.every((b) => b === 0x00 || b === 0xff)) { - return; - } - const nodeId = slice[0]; - const changeType = slice[1]; - const { supportedCCs, controlledCCs } = parseCCList( - slice.subarray(2, SUC_UPDATE_ENTRY_SIZE), - ); - return { - nodeId, - changeType, - supportedCCs: supportedCCs.filter((cc) => cc > 0), - controlledCCs: controlledCCs.filter((cc) => cc > 0), - }; -} +export const SUCUpdateEntriesFileIDV0 = 0x50003; -export function encodeSUCUpdateEntry( - entry: SUCUpdateEntry | undefined, -): Buffer { - const ret = Buffer.alloc(SUC_UPDATE_ENTRY_SIZE, 0); - if (entry) { - ret[0] = entry.nodeId; - ret[1] = entry.changeType; - const ccList = encodeCCList(entry.supportedCCs, entry.controlledCCs); - if (ccList.length > SUC_UPDATE_NODEPARM_MAX) { - throw new ZWaveError( - "Cannot encode SUC update entry, too many CCs", - ZWaveErrorCodes.Argument_Invalid, - ); - } - ccList.copy(ret, 2); - } - return ret; -} - -@nvmFileID(0x50003) +@nvmFileID(SUCUpdateEntriesFileIDV0) +@nvmSection("protocol") export class SUCUpdateEntriesFileV0 extends NVMFile { public constructor( options: NVMFileDeserializationOptions | SUCUpdateEntriesFileOptions, @@ -93,7 +43,7 @@ export class SUCUpdateEntriesFileV0 extends NVMFile { public updateEntries: SUCUpdateEntry[]; - public serialize(): NVM3Object { + public serialize(): NVM3Object & { data: Buffer } { this.payload = Buffer.alloc(SUC_MAX_UPDATES * SUC_UPDATE_ENTRY_SIZE, 0); for (let i = 0; i < this.updateEntries.length; i++) { const offset = i * SUC_UPDATE_ENTRY_SIZE; @@ -112,11 +62,10 @@ export class SUCUpdateEntriesFileV0 extends NVMFile { } } -export const SUCUpdateEntriesFileIDV0 = getNVMFileIDStatic( - SUCUpdateEntriesFileV0, -); - export const SUCUpdateEntriesFileV5IDBase = 0x54000; +export const SUCUpdateEntriesFileV5IDMax = SUCUpdateEntriesFileV5IDBase + + SUC_MAX_UPDATES / SUC_UPDATES_PER_FILE_V5 + - 1; export function sucUpdateIndexToSUCUpdateEntriesFileIDV5( index: number, ): number { @@ -129,10 +78,9 @@ export function sucUpdateIndexToSUCUpdateEntriesFileIDV5( @nvmFileID( (id) => id >= SUCUpdateEntriesFileV5IDBase - && id - < SUCUpdateEntriesFileV5IDBase - + SUC_MAX_UPDATES / SUC_UPDATES_PER_FILE_V5, + && id <= SUCUpdateEntriesFileV5IDMax, ) +@nvmSection("protocol") export class SUCUpdateEntriesFileV5 extends NVMFile { public constructor( options: NVMFileDeserializationOptions | SUCUpdateEntriesFileOptions, @@ -152,7 +100,7 @@ export class SUCUpdateEntriesFileV5 extends NVMFile { public updateEntries: SUCUpdateEntry[]; - public serialize(): NVM3Object { + public serialize(): NVM3Object & { data: Buffer } { this.payload = Buffer.alloc( SUC_UPDATES_PER_FILE_V5 * SUC_UPDATE_ENTRY_SIZE, 0xff, diff --git a/packages/nvmedit/src/files/VersionFiles.ts b/packages/nvmedit/src/lib/nvm3/files/VersionFiles.ts similarity index 76% rename from packages/nvmedit/src/files/VersionFiles.ts rename to packages/nvmedit/src/lib/nvm3/files/VersionFiles.ts index 6ef0d0467c2..7a8d12adbb6 100644 --- a/packages/nvmedit/src/files/VersionFiles.ts +++ b/packages/nvmedit/src/lib/nvm3/files/VersionFiles.ts @@ -1,11 +1,11 @@ -import type { NVM3Object } from "../nvm3/object"; +import type { NVM3Object } from "../object"; import { NVMFile, type NVMFileCreationOptions, type NVMFileDeserializationOptions, - getNVMFileIDStatic, gotDeserializationOptions, nvmFileID, + nvmSection, } from "./NVMFile"; export interface VersionFileOptions extends NVMFileCreationOptions { @@ -38,7 +38,7 @@ export class VersionFile extends NVMFile { public minor: number; public patch: number; - public serialize(): NVM3Object { + public serialize(): NVM3Object & { data: Buffer } { this.payload = Buffer.from([ this.patch, this.minor, @@ -58,20 +58,22 @@ export class VersionFile extends NVMFile { } } -@nvmFileID(0x51000) +export const ApplicationVersionFileID = 0x51000; + +@nvmFileID(ApplicationVersionFileID) +@nvmSection("application") export class ApplicationVersionFile extends VersionFile {} -export const ApplicationVersionFileID = getNVMFileIDStatic( - ApplicationVersionFile, -); // The 800 series has a shared application/protocol file system // and uses a different ID for the application version file -@nvmFileID(0x41000) +export const ApplicationVersionFile800ID = 0x41000; + +@nvmFileID(ApplicationVersionFile800ID) +@nvmSection("application") export class ApplicationVersionFile800 extends VersionFile {} -export const ApplicationVersionFile800ID = getNVMFileIDStatic( - ApplicationVersionFile800, -); -@nvmFileID(0x50000) +export const ProtocolVersionFileID = 0x50000; + +@nvmFileID(ProtocolVersionFileID) +@nvmSection("protocol") export class ProtocolVersionFile extends VersionFile {} -export const ProtocolVersionFileID = getNVMFileIDStatic(ProtocolVersionFile); diff --git a/packages/nvmedit/src/files/index.ts b/packages/nvmedit/src/lib/nvm3/files/index.ts similarity index 91% rename from packages/nvmedit/src/files/index.ts rename to packages/nvmedit/src/lib/nvm3/files/index.ts index 510f8185d0b..5cc62c2c61a 100644 --- a/packages/nvmedit/src/files/index.ts +++ b/packages/nvmedit/src/lib/nvm3/files/index.ts @@ -2,6 +2,7 @@ import "reflect-metadata"; export * from "./ApplicationCCsFile"; export * from "./ApplicationDataFile"; +export * from "./ApplicationNameFile"; export * from "./ApplicationRFConfigFile"; export * from "./ApplicationTypeFile"; export * from "./ControllerInfoFile"; diff --git a/packages/nvmedit/src/lib/nvm3/object.ts b/packages/nvmedit/src/lib/nvm3/object.ts new file mode 100644 index 00000000000..3eea81b0636 --- /dev/null +++ b/packages/nvmedit/src/lib/nvm3/object.ts @@ -0,0 +1,172 @@ +import { + FragmentType, + NVM3_CODE_LARGE_SHIFT, + NVM3_CODE_SMALL_SHIFT, + NVM3_COUNTER_SIZE, + NVM3_OBJ_FRAGTYPE_MASK, + NVM3_OBJ_FRAGTYPE_SHIFT, + NVM3_OBJ_HEADER_SIZE_LARGE, + NVM3_OBJ_HEADER_SIZE_SMALL, + NVM3_OBJ_KEY_MASK, + NVM3_OBJ_KEY_SHIFT, + NVM3_OBJ_LARGE_LEN_MASK, + NVM3_OBJ_TYPE_MASK, + NVM3_WORD_SIZE, + ObjectType, +} from "./consts"; +import { computeBergerCode, computeBergerCodeMulti } from "./utils"; + +export interface NVM3ObjectHeader { + offset: number; + type: ObjectType; + key: number; + fragmentType: FragmentType; + /** The length of the header */ + headerSize: number; + /** The length of the object data */ + fragmentSize: number; + /** The total length of the object in the NVM */ + alignedSize: number; +} + +export interface NVM3Object { + type: ObjectType; + fragmentType: FragmentType; + key: number; + data?: Buffer; +} + +export function serializeObject(obj: NVM3Object): Buffer { + const isLarge = obj.type === ObjectType.DataLarge + || obj.type === ObjectType.CounterLarge; + const headerSize = isLarge + ? NVM3_OBJ_HEADER_SIZE_LARGE + : NVM3_OBJ_HEADER_SIZE_SMALL; + const dataLength = obj.data?.length ?? 0; + const ret = Buffer.allocUnsafe(dataLength + headerSize); + + // Write header + if (isLarge) { + let hdr2 = dataLength & NVM3_OBJ_LARGE_LEN_MASK; + + const hdr1 = (obj.type & NVM3_OBJ_TYPE_MASK) + | ((obj.key & NVM3_OBJ_KEY_MASK) << NVM3_OBJ_KEY_SHIFT) + | ((obj.fragmentType & NVM3_OBJ_FRAGTYPE_MASK) + << NVM3_OBJ_FRAGTYPE_SHIFT); + + const bergerCode = computeBergerCodeMulti( + [hdr1, hdr2], + 32 + NVM3_CODE_LARGE_SHIFT, + ); + hdr2 |= bergerCode << NVM3_CODE_LARGE_SHIFT; + + ret.writeInt32LE(hdr1, 0); + ret.writeInt32LE(hdr2, 4); + } else { + let typeAndLen = obj.type; + if (typeAndLen === ObjectType.DataSmall && dataLength > 0) { + typeAndLen += dataLength; + } + let hdr1 = (typeAndLen & NVM3_OBJ_TYPE_MASK) + | ((obj.key & NVM3_OBJ_KEY_MASK) << NVM3_OBJ_KEY_SHIFT); + const bergerCode = computeBergerCode(hdr1, NVM3_CODE_SMALL_SHIFT); + hdr1 |= bergerCode << NVM3_CODE_SMALL_SHIFT; + + ret.writeInt32LE(hdr1, 0); + } + + // Write data + if (obj.data) { + obj.data.copy(ret, headerSize); + } + return ret; +} + +export function fragmentLargeObject( + obj: NVM3Object & { type: ObjectType.DataLarge | ObjectType.CounterLarge }, + maxFirstFragmentSizeWithHeader: number, + maxFragmentSizeWithHeader: number, +): NVM3Object[] { + const ret: NVM3Object[] = []; + + if ( + obj.data!.length + NVM3_OBJ_HEADER_SIZE_LARGE + <= maxFirstFragmentSizeWithHeader + ) { + return [obj]; + } + + let offset = 0; + while (offset < obj.data!.length) { + const fragmentSize = offset === 0 + ? maxFirstFragmentSizeWithHeader - NVM3_OBJ_HEADER_SIZE_LARGE + : maxFragmentSizeWithHeader - NVM3_OBJ_HEADER_SIZE_LARGE; + const data = obj.data!.subarray(offset, offset + fragmentSize); + + ret.push({ + type: obj.type, + key: obj.key, + fragmentType: offset === 0 + ? FragmentType.First + : data.length + NVM3_OBJ_HEADER_SIZE_LARGE + < maxFragmentSizeWithHeader + ? FragmentType.Last + : FragmentType.Next, + data, + }); + + offset += fragmentSize; + } + + return ret; +} + +export function getAlignedSize(size: number): number { + return (size + NVM3_WORD_SIZE - 1) & ~(NVM3_WORD_SIZE - 1); +} + +export function getHeaderSize(obj: NVM3Object): number { + switch (obj.type) { + case ObjectType.Deleted: + case ObjectType.CounterSmall: + case ObjectType.DataSmall: + return NVM3_OBJ_HEADER_SIZE_SMALL; + case ObjectType.CounterLarge: + case ObjectType.DataLarge: + return NVM3_OBJ_HEADER_SIZE_LARGE; + } +} + +export function getFragmentSize(obj: NVM3Object): number { + switch (obj.type) { + case ObjectType.Deleted: + return 0; + case ObjectType.CounterSmall: + return NVM3_COUNTER_SIZE; + case ObjectType.DataSmall: + case ObjectType.DataLarge: + case ObjectType.CounterLarge: + return obj.data?.length ?? 0; + } +} + +export function getRequiredSpace(obj: NVM3Object): number { + return getHeaderSize(obj) + getAlignedSize(getFragmentSize(obj)); +} + +export function getObjectHeader( + obj: NVM3Object, + offset: number, +): NVM3ObjectHeader { + const headerSize = getHeaderSize(obj); + const fragmentSize = getFragmentSize(obj); + return { + offset, + key: obj.key, + type: obj.type, + fragmentType: obj.fragmentType, + headerSize, + fragmentSize, + alignedSize: headerSize + getAlignedSize(fragmentSize), + }; +} diff --git a/packages/nvmedit/src/lib/nvm3/page.ts b/packages/nvmedit/src/lib/nvm3/page.ts new file mode 100644 index 00000000000..21880a02521 --- /dev/null +++ b/packages/nvmedit/src/lib/nvm3/page.ts @@ -0,0 +1,75 @@ +import { + NVM3_MIN_PAGE_SIZE, + NVM3_PAGE_COUNTER_MASK, + NVM3_PAGE_COUNTER_SIZE, + NVM3_PAGE_HEADER_SIZE, + NVM3_PAGE_MAGIC, + type PageStatus, + type PageWriteSize, +} from "./consts"; +import { type NVM3Object } from "./object"; +import { computeBergerCode } from "./utils"; + +export interface NVM3PageHeader { + offset: number; + version: number; + eraseCount: number; + status: PageStatus; + encrypted: boolean; + pageSize: number; + writeSize: PageWriteSize; + memoryMapped: boolean; + deviceFamily: number; +} + +export interface NVM3Page { + header: NVM3PageHeader; + objects: NVM3Object[]; +} + +// The page size field has a value from 0 to 7 describing page sizes from 512 to 65536 bytes +export function pageSizeToBits(pageSize: number): number { + return Math.ceil(Math.log2(pageSize) - Math.log2(NVM3_MIN_PAGE_SIZE)); +} + +export function pageSizeFromBits(bits: number): number { + return NVM3_MIN_PAGE_SIZE * Math.pow(2, bits); +} + +export function serializePageHeader( + header: Omit, +): Buffer { + const ret = Buffer.alloc(NVM3_PAGE_HEADER_SIZE); + + ret.writeUInt16LE(header.version, 0); + ret.writeUInt16LE(NVM3_PAGE_MAGIC, 2); + + let eraseCount = header.eraseCount & NVM3_PAGE_COUNTER_MASK; + const eraseCountCode = computeBergerCode( + eraseCount, + NVM3_PAGE_COUNTER_SIZE, + ); + eraseCount |= eraseCountCode << NVM3_PAGE_COUNTER_SIZE; + ret.writeInt32LE(eraseCount, 4); + + let eraseCountInv = ~header.eraseCount & NVM3_PAGE_COUNTER_MASK; + const eraseCountInvCode = computeBergerCode( + eraseCountInv, + NVM3_PAGE_COUNTER_SIZE, + ); + eraseCountInv |= eraseCountInvCode << NVM3_PAGE_COUNTER_SIZE; + ret.writeInt32LE(eraseCountInv, 8); + + ret.writeUInt32LE(header.status, 12); + + const devInfo = (header.deviceFamily & 0x7ff) + | ((header.writeSize & 0b1) << 11) + | ((header.memoryMapped ? 1 : 0) << 12) + | (pageSizeToBits(header.pageSize) << 13); + ret.writeUInt16LE(devInfo, 16); + + const formatInfo = header.encrypted ? 0xfffe : 0xffff; + ret.writeUInt16LE(formatInfo, 18); + + return ret; +} diff --git a/packages/nvmedit/src/nvm3/utils.test.ts b/packages/nvmedit/src/lib/nvm3/utils.test.ts similarity index 100% rename from packages/nvmedit/src/nvm3/utils.test.ts rename to packages/nvmedit/src/lib/nvm3/utils.test.ts diff --git a/packages/nvmedit/src/nvm3/utils.ts b/packages/nvmedit/src/lib/nvm3/utils.ts similarity index 50% rename from packages/nvmedit/src/nvm3/utils.ts rename to packages/nvmedit/src/lib/nvm3/utils.ts index 0780607bfdf..5a32fd0b316 100644 --- a/packages/nvmedit/src/nvm3/utils.ts +++ b/packages/nvmedit/src/lib/nvm3/utils.ts @@ -1,8 +1,9 @@ import { ZWaveError, ZWaveErrorCodes } from "@zwave-js/core/safe"; -import { NVMFile } from "../files/NVMFile"; +import { num2hex } from "@zwave-js/shared"; +import { type NVM3 } from "../NVM3"; import { FragmentType, ObjectType, PageStatus } from "./consts"; +import { NVMFile } from "./files/NVMFile"; import type { NVM3Object } from "./object"; -import type { NVM3Page } from "./page"; /** Counts the number of unset bits in the given word */ export function computeBergerCode(word: number, numBits: number = 32): number { @@ -81,29 +82,23 @@ export function mapToObject>( return obj; } -export function dumpPage(page: NVM3Page, json: boolean = false): void { - console.log(` `); - console.log(`read page (offset 0x${page.header.offset.toString(16)}):`); - console.log(` version: ${page.header.version}`); - console.log(` eraseCount: ${page.header.eraseCount}`); - console.log(` status: ${PageStatus[page.header.status]}`); - console.log(` encrypted: ${page.header.encrypted}`); - console.log(` pageSize: ${page.header.pageSize}`); - console.log(` writeSize: ${page.header.writeSize}`); - console.log(` memoryMapped: ${page.header.memoryMapped}`); - console.log(` deviceFamily: ${page.header.deviceFamily}`); - console.log(""); - console.log(` objects:`); - for (const obj of page.objects) { - dumpObject(obj, json); - } -} - -export function dumpObject(obj: NVM3Object, json: boolean = false): void { +function dumpObject( + obj: NVM3Object & { offset: number }, + json: boolean = false, +): void { try { if (json) { - const file = NVMFile.from(obj, "7.0.0"); - console.log(`${JSON.stringify(file.toJSON(), null, 2)}`); + const file = NVMFile.from(obj.key, obj.data!, "7.0.0"); + console.log( + JSON.stringify( + { + offset: num2hex(obj.offset), + ...file.toJSON(), + }, + null, + 2, + ), + ); console.log(); return; } @@ -111,7 +106,8 @@ export function dumpObject(obj: NVM3Object, json: boolean = false): void { // ignore } const prefix = json ? "" : " "; - console.log(`${prefix}· key: 0x${obj.key.toString(16)}`); + console.log(`${prefix}· offset: ${num2hex(obj.offset)}`); + console.log(`${prefix} key: 0x${obj.key.toString(16)}`); console.log(`${prefix} type: ${ObjectType[obj.type]}`); console.log(`${prefix} fragment type: ${FragmentType[obj.fragmentType]}`); if (obj.data) { @@ -123,3 +119,62 @@ export function dumpObject(obj: NVM3Object, json: boolean = false): void { } console.log(); } + +export async function dumpNVM(nvm: NVM3): Promise { + for (const [name, section] of Object.entries(nvm.info!.sections)) { + console.log(`NVM section: ${name}`); + + for (const page of section.pages) { + console.log(""); + console.log(`page (offset 0x${page.offset.toString(16)}):`); + console.log(` version: ${page.version}`); + console.log(` eraseCount: ${page.eraseCount}`); + console.log(` status: ${PageStatus[page.status]}`); + console.log(` encrypted: ${page.encrypted}`); + console.log(` pageSize: ${page.pageSize}`); + console.log(` writeSize: ${page.writeSize}`); + console.log(` memoryMapped: ${page.memoryMapped}`); + console.log(` deviceFamily: ${page.deviceFamily}`); + console.log(""); + if (page.objects.length) { + console.log(` raw objects:`); + + for (const objectHeader of page.objects) { + const objectData = objectHeader.type !== ObjectType.Deleted + ? await nvm.readObjectData(objectHeader) + : undefined; + dumpObject({ + offset: objectHeader.offset, + key: objectHeader.key, + type: objectHeader.type, + fragmentType: objectHeader.fragmentType, + data: objectData, + }, false); + } + } + } + + console.log(); + console.log(); + } + + for (const [name, section] of Object.entries(nvm.info!.sections)) { + console.log(`${name} objects:`); + for (const [fileId, pageIndex] of section.objectLocations) { + const page = section.pages[pageIndex]; + const objectHeader = page.objects.findLast((o) => o.key === fileId); + if (!objectHeader) continue; + const objectData = await nvm.get(fileId); + + dumpObject({ + offset: objectHeader.offset, + key: fileId, + type: objectHeader.type, + fragmentType: FragmentType.None, + data: objectData, + }, true); + } + + console.log(); + } +} diff --git a/packages/nvmedit/src/nvm500/EntryParsers.ts b/packages/nvmedit/src/lib/nvm500/EntryParsers.ts similarity index 100% rename from packages/nvmedit/src/nvm500/EntryParsers.ts rename to packages/nvmedit/src/lib/nvm500/EntryParsers.ts diff --git a/packages/nvmedit/src/lib/nvm500/adapter.ts b/packages/nvmedit/src/lib/nvm500/adapter.ts new file mode 100644 index 00000000000..900a4109ae4 --- /dev/null +++ b/packages/nvmedit/src/lib/nvm500/adapter.ts @@ -0,0 +1,605 @@ +import { + type CommandClasses, + ZWaveError, + ZWaveErrorCodes, +} from "@zwave-js/core"; +import { assertNever } from "alcalzone-shared/helpers"; +import { SUC_MAX_UPDATES } from "../../consts"; +import { type NVM500, type NVM500Info } from "../NVM500"; +import { + type ControllerNVMProperty, + type NVMAdapter, + type NVMProperty, + type NVMPropertyToDataType, + type NodeNVMProperty, +} from "../common/definitions"; +import { type Route } from "../common/routeCache"; +import { type SUCUpdateEntry } from "../common/sucUpdateEntry"; +import { type NodeInfo } from "../nvm3/files"; +import { type NVM500NodeInfo } from "./EntryParsers"; +import { + APPL_NODEPARM_MAX, + type NVMData, + type NVMEntryName, + NVM_SERIALAPI_HOST_SIZE, +} from "./shared"; + +export class NVM500Adapter implements NVMAdapter { + public constructor(nvm: NVM500) { + this._nvm = nvm; + } + + private _nvm: NVM500; + + public async get( + property: T, + required?: R, + ): Promise< + R extends true ? NonNullable> + : (NVMPropertyToDataType | undefined) + > { + const info = this._nvm.info ?? await this._nvm.init(); + + let ret: unknown; + if (property.domain === "controller") { + ret = await this.getControllerNVMProperty(info, property); + } else if (property.domain === "lrnode") { + throw new ZWaveError( + `500 series NVM has no support for Long Range node information`, + ZWaveErrorCodes.NVM_ObjectNotFound, + ); + } else { + ret = await this.getNodeNVMProperty(property); + } + if (required && ret === undefined) { + throw new ZWaveError( + `NVM data for property ${JSON.stringify(property)} not found`, + ZWaveErrorCodes.NVM_ObjectNotFound, + ); + } + return ret as any; + } + + private async getOnly( + property: NVMEntryName, + ): Promise { + const data = await this._nvm.get(property); + return data?.[0] as T | undefined; + } + + private async getSingle( + property: NVMEntryName, + index: number, + ): Promise { + const data = await this._nvm.getSingle(property, index); + return data as T | undefined; + } + + private getAll( + property: NVMEntryName, + ): Promise { + return this._nvm.get(property) as any; + } + + private async getControllerNVMProperty( + info: NVM500Info, + property: ControllerNVMProperty, + ): Promise { + switch (property.type) { + case "protocolVersion": + return info.nvmDescriptor.protocolVersion; + case "applicationVersion": + return info.nvmDescriptor.firmwareVersion; + + case "protocolFileFormat": + case "applicationFileFormat": + // Not supported in 500 series, but we use 500 in JSON to designate a 500 series NVM + return 500; + + case "applicationData": + return this.getOnly("EEOFFSET_HOST_OFFSET_START_far"); + + case "applicationName": + // Not supported in 500 series + return; + + case "homeId": { + // 500 series stores the home ID as a number + const homeId = await this.getOnly( + "EX_NVM_HOME_ID_far", + ); + if (homeId == undefined) return; + const ret = Buffer.alloc(4, 0); + // FIXME: BE? LE? + ret.writeUInt32BE(homeId, 0); + return ret; + } + + case "learnedHomeId": { + // 500 series stores the home ID as a number + const homeId = await this.getOnly("NVM_HOMEID_far"); + if (homeId == undefined) return; + const ret = Buffer.alloc(4, 0); + // FIXME: BE? LE? + ret.writeUInt32BE(homeId, 0); + return ret; + } + + case "nodeId": + return this.getOnly("NVM_NODEID_far"); + + case "lastNodeId": + return this.getOnly( + "EX_NVM_LAST_USED_NODE_ID_START_far", + ); + + case "staticControllerNodeId": + return this.getOnly( + "EX_NVM_STATIC_CONTROLLER_NODE_ID_START_far", + ); + case "sucLastIndex": + return this.getOnly( + "EX_NVM_SUC_LAST_INDEX_START_far", + ); + case "controllerConfiguration": + return this.getOnly( + "EX_NVM_CONTROLLER_CONFIGURATION_far", + ); + + case "maxNodeId": + return this.getOnly("EX_NVM_MAX_NODE_ID_far"); + case "reservedId": + return this.getOnly("EX_NVM_RESERVED_ID_far"); + case "systemState": + return this.getOnly("NVM_SYSTEM_STATE"); + + case "commandClasses": { + const numCCs = await this.getOnly( + "EEOFFSET_CMDCLASS_LEN_far", + ); + const ret = await this.getAll( + "EEOFFSET_CMDCLASS_far", + ); + return ret?.slice(0, numCCs); + } + + case "preferredRepeaters": + return this.getOnly("NVM_PREFERRED_REPEATERS_far"); + + case "appRouteLock": { + return this.getOnly( + "EX_NVM_ROUTECACHE_APP_LOCK_far", + ); + } + case "routeSlaveSUC": { + return this.getOnly( + "EX_NVM_SUC_ROUTING_SLAVE_LIST_START_far", + ); + } + case "sucPendingUpdate": { + return this.getOnly("EX_NVM_PENDING_UPDATE_far"); + } + case "pendingDiscovery": { + return this.getOnly("NVM_PENDING_DISCOVERY_far"); + } + + case "nodeIds": { + const nodeInfos = await this.getAll( + "EX_NVM_NODE_TABLE_START_far", + ); + return nodeInfos + ?.map((info, index) => info ? (index + 1) : undefined) + .filter((id) => id != undefined); + } + + case "virtualNodeIds": { + const ret = await this.getOnly( + "EX_NVM_BRIDGE_NODEPOOL_START_far", + ); + return ret ?? []; + } + + case "sucUpdateEntries": { + const ret = await this.getAll( + "EX_NVM_SUC_NODE_LIST_START_far", + ); + return ret?.filter(Boolean); + } + + case "watchdogStarted": + return this.getOnly("EEOFFSET_WATCHDOG_STARTED_far"); + + case "powerLevelNormal": + return this.getAll( + "EEOFFSET_POWERLEVEL_NORMAL_far", + ); + case "powerLevelLow": + return this.getAll( + "EEOFFSET_POWERLEVEL_LOW_far", + ); + case "powerMode": + return this.getOnly( + "EEOFFSET_MODULE_POWER_MODE_far", + ); + case "powerModeExtintEnable": + return this.getOnly( + "EEOFFSET_MODULE_POWER_MODE_EXTINT_ENABLE_far", + ); + case "powerModeWutTimeout": + return this.getOnly( + "EEOFFSET_MODULE_POWER_MODE_WUT_TIMEOUT_far", + ); + + case "sucAwarenessPushNeeded": + case "lastNodeIdLR": + case "maxNodeIdLR": + case "reservedIdLR": + case "primaryLongRangeChannelId": + case "dcdcConfig": + case "lrNodeIds": + case "includedInsecurely": + case "includedSecurelyInsecureCCs": + case "includedSecurelySecureCCs": + case "rfRegion": + case "txPower": + case "measured0dBm": + case "enablePTI": + case "maxTXPower": + case "nodeIdType": + case "isListening": + case "optionalFunctionality": + case "genericDeviceClass": + case "specificDeviceClass": + // Not supported on 500 series, 700+ series only + return; + + default: + assertNever(property.type); + } + } + + private async getNodeNVMProperty( + property: NodeNVMProperty, + ): Promise { + switch (property.type) { + case "info": { + const nodeId = property.nodeId; + const nodeInfo = await this.getSingle( + "EX_NVM_NODE_TABLE_START_far", + nodeId - 1, + ); + const sucUpdateIndex = await this.getSingle( + "EX_NVM_SUC_CONTROLLER_LIST_START_far", + nodeId - 1, + ) ?? 0xff; + const neighbors = await this.getSingle( + "EX_NVM_ROUTING_TABLE_START_far", + nodeId - 1, + ) ?? []; + + if (!nodeInfo) return; + + return { + nodeId, + ...nodeInfo, + neighbors, + sucUpdateIndex, + } satisfies NodeInfo; + } + + case "routes": { + const lwr = await this.getSingle( + "EX_NVM_ROUTECACHE_START_far", + property.nodeId - 1, + ); + const nlwr = await this.getSingle( + "EX_NVM_ROUTECACHE_NLWR_SR_START_far", + property.nodeId - 1, + ); + return { lwr, nlwr }; + } + } + } + + private setOnly( + property: NVMEntryName, + value: NVMData, + ): Promise { + return this._nvm.set(property, [value]); + } + + private setSingle( + property: NVMEntryName, + index: number, + value: NVMData, + ): Promise { + return this._nvm.setSingle(property, index, value); + } + + private setAll( + property: NVMEntryName, + value: NVMData[], + ): Promise { + return this._nvm.set(property, value); + } + + set( + property: T, + value: NVMPropertyToDataType, + ): Promise { + if (property.domain === "controller") { + return this.setControllerNVMProperty(property, value); + } else if (property.domain === "lrnode") { + throw new ZWaveError( + `500 series NVM has no support for Long Range node information`, + ZWaveErrorCodes.NVM_ObjectNotFound, + ); + } else { + return this.setNodeNVMProperty(property, value); + } + } + + private async setControllerNVMProperty( + property: ControllerNVMProperty, + value: any, + ): Promise { + switch (property.type) { + case "protocolVersion": + case "applicationVersion": + // Only written during erase + return; + + case "protocolFileFormat": + case "applicationFileFormat": + // Cannot be written + return; + + case "applicationData": + return this.setOnly( + "EEOFFSET_HOST_OFFSET_START_far", + value ?? Buffer.alloc(NVM_SERIALAPI_HOST_SIZE, 0xff), + ); + + case "applicationName": + // Not supported in 500 series + return; + + case "homeId": { + // 500 series stores the home ID as a number + const homeId = value.readUInt32BE(0); + return this.setOnly("EX_NVM_HOME_ID_far", homeId); + } + + case "learnedHomeId": { + // 500 series stores the home ID as a number + const learnedHomeId = value?.readUInt32BE(0) ?? 0x00000000; + return this.setOnly("NVM_HOMEID_far", learnedHomeId); + } + + case "nodeId": + return this.setOnly("NVM_NODEID_far", value); + + case "lastNodeId": + return this.setOnly( + "EX_NVM_LAST_USED_NODE_ID_START_far", + value, + ); + + case "staticControllerNodeId": + return this.setOnly( + "EX_NVM_STATIC_CONTROLLER_NODE_ID_START_far", + value, + ); + case "sucLastIndex": + return this.setOnly( + "EX_NVM_SUC_LAST_INDEX_START_far", + value, + ); + case "controllerConfiguration": + return this.setOnly( + "EX_NVM_CONTROLLER_CONFIGURATION_far", + value, + ); + + case "maxNodeId": + return this.setOnly("EX_NVM_MAX_NODE_ID_far", value); + case "reservedId": + return this.setOnly("EX_NVM_RESERVED_ID_far", value); + case "systemState": + return this.setOnly("NVM_SYSTEM_STATE", value); + + case "commandClasses": { + await this.setOnly( + "EEOFFSET_CMDCLASS_LEN_far", + value.length, + ); + const CCs = new Array(APPL_NODEPARM_MAX).fill(0xff); + for (let i = 0; i < value.length; i++) { + if (i < APPL_NODEPARM_MAX) { + CCs[i] = value[i]; + } + } + await this.setAll( + "EEOFFSET_CMDCLASS_far", + CCs, + ); + return; + } + + case "preferredRepeaters": + return this.setOnly("NVM_PREFERRED_REPEATERS_far", value); + + case "appRouteLock": { + return this.setOnly( + "EX_NVM_ROUTECACHE_APP_LOCK_far", + value, + ); + } + case "routeSlaveSUC": { + return this.setOnly( + "EX_NVM_SUC_ROUTING_SLAVE_LIST_START_far", + value, + ); + } + case "sucPendingUpdate": { + return this.setOnly("EX_NVM_PENDING_UPDATE_far", value); + } + case "pendingDiscovery": { + return this.setOnly("NVM_PENDING_DISCOVERY_far", value); + } + + case "nodeIds": + // Cannot be written. Is implied by the node info table + return; + + case "virtualNodeIds": { + return this.setOnly( + "EX_NVM_BRIDGE_NODEPOOL_START_far", + value, + ); + } + + case "sucUpdateEntries": { + const entries = value as SUCUpdateEntry[]; + const sucUpdateEntries = new Array(SUC_MAX_UPDATES).fill( + undefined, + ); + for (let i = 0; i < entries.length; i++) { + if (i < SUC_MAX_UPDATES) { + sucUpdateEntries[i] = entries[i]; + } + } + return this.setAll( + "EX_NVM_SUC_NODE_LIST_START_far", + sucUpdateEntries, + ); + } + + case "watchdogStarted": + return this.setOnly("EEOFFSET_WATCHDOG_STARTED_far", value); + + case "powerLevelNormal": + return this.setAll( + "EEOFFSET_POWERLEVEL_NORMAL_far", + value, + ); + case "powerLevelLow": + return this.setAll( + "EEOFFSET_POWERLEVEL_LOW_far", + value, + ); + case "powerMode": + return this.setOnly( + "EEOFFSET_MODULE_POWER_MODE_far", + value, + ); + case "powerModeExtintEnable": + return this.setOnly( + "EEOFFSET_MODULE_POWER_MODE_EXTINT_ENABLE_far", + value, + ); + case "powerModeWutTimeout": + return this.setOnly( + "EEOFFSET_MODULE_POWER_MODE_WUT_TIMEOUT_far", + value, + ); + + case "sucAwarenessPushNeeded": + case "lastNodeIdLR": + case "maxNodeIdLR": + case "reservedIdLR": + case "primaryLongRangeChannelId": + case "dcdcConfig": + case "lrNodeIds": + case "includedInsecurely": + case "includedSecurelyInsecureCCs": + case "includedSecurelySecureCCs": + case "rfRegion": + case "txPower": + case "measured0dBm": + case "enablePTI": + case "maxTXPower": + case "nodeIdType": + case "isListening": + case "optionalFunctionality": + case "genericDeviceClass": + case "specificDeviceClass": + // Not supported on 500 series, 700+ series only + return; + + default: + assertNever(property.type); + } + } + + private async setNodeNVMProperty( + property: NodeNVMProperty, + value: any, + ): Promise { + switch (property.type) { + case "info": { + const nodeId = property.nodeId; + const node = value as NodeInfo; + await this.setSingle( + "EX_NVM_NODE_TABLE_START_far", + nodeId - 1, + node + ? { + isListening: node.isListening, + isFrequentListening: node.isFrequentListening, + isRouting: node.isRouting, + supportedDataRates: node.supportedDataRates, + protocolVersion: node.protocolVersion, + optionalFunctionality: node.optionalFunctionality, + nodeType: node.nodeType, + supportsSecurity: node.supportsSecurity, + supportsBeaming: node.supportsBeaming, + genericDeviceClass: node.genericDeviceClass, + specificDeviceClass: node.specificDeviceClass + ?? null, + } satisfies NVM500NodeInfo + : undefined, + ); + await this.setSingle( + "EX_NVM_SUC_CONTROLLER_LIST_START_far", + nodeId - 1, + node?.sucUpdateIndex ?? 0xfe, + ); + await this.setSingle( + "EX_NVM_ROUTING_TABLE_START_far", + nodeId - 1, + node?.neighbors, + ); + } + + case "routes": { + const nodeId = property.nodeId; + const routes = value as { lwr?: Route; nlwr?: Route }; + await this.setSingle( + "EX_NVM_ROUTECACHE_START_far", + nodeId - 1, + routes.lwr, + ); + await this.setSingle( + "EX_NVM_ROUTECACHE_NLWR_SR_START_far", + property.nodeId - 1, + routes.nlwr, + ); + } + } + } + + // eslint-disable-next-line @typescript-eslint/require-await + public async delete(_property: NVMProperty): Promise { + throw new Error("Method not implemented."); + } + + public hasPendingChanges(): boolean { + // We don't buffer changes + return false; + } + + public async commit(): Promise { + // We don't buffer changes at the moment + } +} diff --git a/packages/nvmedit/src/nvm500/parsers/Bridge_6_6x.ts b/packages/nvmedit/src/lib/nvm500/impls/Bridge_6_6x.ts similarity index 97% rename from packages/nvmedit/src/nvm500/parsers/Bridge_6_6x.ts rename to packages/nvmedit/src/lib/nvm500/impls/Bridge_6_6x.ts index 87ad9874e82..55a00de55c0 100644 --- a/packages/nvmedit/src/nvm500/parsers/Bridge_6_6x.ts +++ b/packages/nvmedit/src/lib/nvm500/impls/Bridge_6_6x.ts @@ -1,6 +1,6 @@ import { MAX_NODES, NUM_NODEMASK_BYTES } from "@zwave-js/core/safe"; -import { SUC_MAX_UPDATES } from "../../consts"; -import type { NVM500Details } from "../NVMParser"; +import { SUC_MAX_UPDATES } from "../../../consts"; +import type { NVM500Impl } from "../shared"; import { APPL_NODEPARM_MAX, NVMEntryType, @@ -182,7 +182,7 @@ const NVM_Layout_Bridge_6_6x: NVMLayout = [ { name: "nvmModuleSizeEndMarker", type: NVMEntryType.Word, count: 1 }, ]; -export const Bridge_6_6x: NVM500Details = { +export const Bridge_6_6x: NVM500Impl = { name: "Bridge 6.6x", library: "bridge", protocolVersions: ["4.33", "4.62"], diff --git a/packages/nvmedit/src/nvm500/parsers/Bridge_6_7x.ts b/packages/nvmedit/src/lib/nvm500/impls/Bridge_6_7x.ts similarity index 97% rename from packages/nvmedit/src/nvm500/parsers/Bridge_6_7x.ts rename to packages/nvmedit/src/lib/nvm500/impls/Bridge_6_7x.ts index 38784e31290..669b01aaf3d 100644 --- a/packages/nvmedit/src/nvm500/parsers/Bridge_6_7x.ts +++ b/packages/nvmedit/src/lib/nvm500/impls/Bridge_6_7x.ts @@ -1,6 +1,6 @@ import { MAX_NODES, NUM_NODEMASK_BYTES } from "@zwave-js/core/safe"; -import { SUC_MAX_UPDATES } from "../../consts"; -import type { NVM500Details } from "../NVMParser"; +import { SUC_MAX_UPDATES } from "../../../consts"; +import type { NVM500Impl } from "../shared"; import { APPL_NODEPARM_MAX, NVMEntryType, @@ -199,7 +199,7 @@ const NVM_Layout_Bridge_6_7x: NVMLayout = [ { name: "nvmModuleSizeEndMarker", type: NVMEntryType.Word, count: 1 }, ]; -export const Bridge_6_7x: NVM500Details = { +export const Bridge_6_7x: NVM500Impl = { name: "Bridge 6.7x", library: "bridge", protocolVersions: ["4.60", "4.61", "5.02", "5.03"], diff --git a/packages/nvmedit/src/nvm500/parsers/Bridge_6_8x.ts b/packages/nvmedit/src/lib/nvm500/impls/Bridge_6_8x.ts similarity index 97% rename from packages/nvmedit/src/nvm500/parsers/Bridge_6_8x.ts rename to packages/nvmedit/src/lib/nvm500/impls/Bridge_6_8x.ts index c99a240ff66..cd88be4698f 100644 --- a/packages/nvmedit/src/nvm500/parsers/Bridge_6_8x.ts +++ b/packages/nvmedit/src/lib/nvm500/impls/Bridge_6_8x.ts @@ -1,6 +1,6 @@ import { MAX_NODES, NUM_NODEMASK_BYTES } from "@zwave-js/core/safe"; -import { SUC_MAX_UPDATES } from "../../consts"; -import type { NVM500Details } from "../NVMParser"; +import { SUC_MAX_UPDATES } from "../../../consts"; +import type { NVM500Impl } from "../shared"; import { APPL_NODEPARM_MAX, NVMEntryType, @@ -215,7 +215,7 @@ const NVM_Layout_Bridge_6_8x: NVMLayout = [ { name: "nvmModuleSizeEndMarker", type: NVMEntryType.Word, count: 1 }, ]; -export const Bridge_6_8x: NVM500Details = { +export const Bridge_6_8x: NVM500Impl = { name: "Bridge 6.8x", library: "bridge", protocolVersions: [ diff --git a/packages/nvmedit/src/nvm500/parsers/Static_6_6x.ts b/packages/nvmedit/src/lib/nvm500/impls/Static_6_6x.ts similarity index 96% rename from packages/nvmedit/src/nvm500/parsers/Static_6_6x.ts rename to packages/nvmedit/src/lib/nvm500/impls/Static_6_6x.ts index 661698c5f54..1ea466a9ad8 100644 --- a/packages/nvmedit/src/nvm500/parsers/Static_6_6x.ts +++ b/packages/nvmedit/src/lib/nvm500/impls/Static_6_6x.ts @@ -1,6 +1,6 @@ import { MAX_NODES, NUM_NODEMASK_BYTES } from "@zwave-js/core/safe"; -import { SUC_MAX_UPDATES } from "../../consts"; -import type { NVM500Details } from "../NVMParser"; +import { SUC_MAX_UPDATES } from "../../../consts"; +import type { NVM500Impl } from "../shared"; import { APPL_NODEPARM_MAX, NVMEntryType, @@ -177,7 +177,7 @@ const NVM_Layout_Static_6_6x: NVMLayout = [ { name: "nvmModuleSizeEndMarker", type: NVMEntryType.Word, count: 1 }, ]; -export const Static_6_6x: NVM500Details = { +export const Static_6_6x: NVM500Impl = { name: "Static 6.6x", library: "static", protocolVersions: ["4.33", "4.62"], diff --git a/packages/nvmedit/src/nvm500/parsers/Static_6_7x.ts b/packages/nvmedit/src/lib/nvm500/impls/Static_6_7x.ts similarity index 97% rename from packages/nvmedit/src/nvm500/parsers/Static_6_7x.ts rename to packages/nvmedit/src/lib/nvm500/impls/Static_6_7x.ts index 88d28497424..a3343ee874c 100644 --- a/packages/nvmedit/src/nvm500/parsers/Static_6_7x.ts +++ b/packages/nvmedit/src/lib/nvm500/impls/Static_6_7x.ts @@ -1,6 +1,6 @@ import { MAX_NODES, NUM_NODEMASK_BYTES } from "@zwave-js/core/safe"; -import { SUC_MAX_UPDATES } from "../../consts"; -import type { NVM500Details } from "../NVMParser"; +import { SUC_MAX_UPDATES } from "../../../consts"; +import type { NVM500Impl } from "../shared"; import { APPL_NODEPARM_MAX, NVMEntryType, @@ -194,7 +194,7 @@ const NVM_Layout_Static_6_7x: NVMLayout = [ { name: "nvmModuleSizeEndMarker", type: NVMEntryType.Word, count: 1 }, ]; -export const Static_6_7x: NVM500Details = { +export const Static_6_7x: NVM500Impl = { name: "Static 6.7x", library: "static", protocolVersions: ["4.60", "4.61", "5.02", "5.03"], diff --git a/packages/nvmedit/src/nvm500/parsers/Static_6_8x.ts b/packages/nvmedit/src/lib/nvm500/impls/Static_6_8x.ts similarity index 97% rename from packages/nvmedit/src/nvm500/parsers/Static_6_8x.ts rename to packages/nvmedit/src/lib/nvm500/impls/Static_6_8x.ts index 6cdbc0e3ee3..c48db4068ed 100644 --- a/packages/nvmedit/src/nvm500/parsers/Static_6_8x.ts +++ b/packages/nvmedit/src/lib/nvm500/impls/Static_6_8x.ts @@ -1,6 +1,6 @@ import { MAX_NODES, NUM_NODEMASK_BYTES } from "@zwave-js/core/safe"; -import { SUC_MAX_UPDATES } from "../../consts"; -import type { NVM500Details } from "../NVMParser"; +import { SUC_MAX_UPDATES } from "../../../consts"; +import type { NVM500Impl } from "../shared"; import { APPL_NODEPARM_MAX, NVMEntryType, @@ -211,7 +211,7 @@ const NVM_Layout_Static_6_8x: NVMLayout = [ { name: "nvmModuleSizeEndMarker", type: NVMEntryType.Word, count: 1 }, ]; -export const Static_6_8x: NVM500Details = { +export const Static_6_8x: NVM500Impl = { name: "Static 6.8x", library: "static", protocolVersions: [ diff --git a/packages/nvmedit/src/lib/nvm500/impls/index.ts b/packages/nvmedit/src/lib/nvm500/impls/index.ts new file mode 100644 index 00000000000..f49192628c8 --- /dev/null +++ b/packages/nvmedit/src/lib/nvm500/impls/index.ts @@ -0,0 +1,15 @@ +import { Bridge_6_6x } from "./Bridge_6_6x"; +import { Bridge_6_7x } from "./Bridge_6_7x"; +import { Bridge_6_8x } from "./Bridge_6_8x"; +import { Static_6_6x } from "./Static_6_6x"; +import { Static_6_7x } from "./Static_6_7x"; +import { Static_6_8x } from "./Static_6_8x"; + +export const nvm500Impls = [ + Bridge_6_6x, + Bridge_6_7x, + Bridge_6_8x, + Static_6_6x, + Static_6_7x, + Static_6_8x, +] as const; diff --git a/packages/nvmedit/src/nvm500/shared.ts b/packages/nvmedit/src/lib/nvm500/shared.ts similarity index 77% rename from packages/nvmedit/src/nvm500/shared.ts rename to packages/nvmedit/src/lib/nvm500/shared.ts index b7cf0a40e52..cbea9b150c8 100644 --- a/packages/nvmedit/src/nvm500/shared.ts +++ b/packages/nvmedit/src/lib/nvm500/shared.ts @@ -1,6 +1,7 @@ import { NUM_NODEMASK_BYTES } from "@zwave-js/core/safe"; -import { SUC_UPDATE_ENTRY_SIZE } from "../consts"; -import type { Route, SUCUpdateEntry } from "../files"; +import { SUC_UPDATE_ENTRY_SIZE } from "../../consts"; +import { type Route } from "../common/routeCache"; +import { type SUCUpdateEntry } from "../common/sucUpdateEntry"; import type { NVM500NodeInfo, NVMDescriptor, @@ -91,6 +92,9 @@ export interface NVMEntry { count: number; } +/** The NVM entry as it appears in a valid layout, with all sizes and offsets resolved */ +export type ResolvedNVMEntry = Required; + export type NVMData = | Buffer | number @@ -106,7 +110,9 @@ export interface ParsedNVMEntry extends NVMEntry { data: NVMData[]; } -export type NVMLayout = NVMEntry[]; +export type NVMLayout = readonly Readonly[]; + +export type ResolvedNVMLayout = ReadonlyMap; export const NVMEntrySizes: Record = { [NVMEntryType.NVMModuleSize]: 2, // Marks the start of an NVM module @@ -150,3 +156,37 @@ export const CONFIGURATION_VALID_0 = 0x54; export const CONFIGURATION_VALID_1 = 0xa5; export const ROUTECACHE_VALID = 0x4a; export const MAGIC_VALUE = 0x42; + +export interface NVM500Impl { + name: string; + library: "static" | "bridge"; + protocolVersions: string[]; + layout: NVMLayout; +} + +export type ResolvedNVM500Impl = Omit & { + layout: ResolvedNVMLayout; +}; + +export function resolveLayout(layout: NVMLayout): { + layout: ResolvedNVMLayout; + nvmSize: number; +} { + const ret: Map> = new Map(); + let offset = 0; + for (const entry of layout) { + const size = entry.size ?? NVMEntrySizes[entry.type]; + const resolvedEntry: ResolvedNVMEntry = { + ...entry, + size, + offset: entry.offset ?? offset, + }; + ret.set(resolvedEntry.name, resolvedEntry); + offset += size * entry.count; + } + + const endMarker = ret.get("nvmModuleSizeEndMarker")!; + const nvmSize = endMarker.offset + endMarker.size; + + return { layout: ret, nvmSize }; +} diff --git a/packages/nvmedit/src/nvm3/nvm.ts b/packages/nvmedit/src/nvm3/nvm.ts deleted file mode 100644 index 8646a9e3ad0..00000000000 --- a/packages/nvmedit/src/nvm3/nvm.ts +++ /dev/null @@ -1,281 +0,0 @@ -import { ZWaveError, ZWaveErrorCodes } from "@zwave-js/core/safe"; -import { pick } from "@zwave-js/shared/safe"; -import { ApplicationVersionFile800ID } from "../files"; -import { - FLASH_MAX_PAGE_SIZE_700, - FLASH_MAX_PAGE_SIZE_800, - NVM3_COUNTER_SIZE, - NVM3_OBJ_HEADER_SIZE_LARGE, - NVM3_OBJ_HEADER_SIZE_SMALL, - NVM3_PAGE_HEADER_SIZE, - NVM3_WORD_SIZE, - ObjectType, - PageStatus, - PageWriteSize, - ZWAVE_APPLICATION_NVM_SIZE, - ZWAVE_PROTOCOL_NVM_SIZE, - ZWAVE_SHARED_NVM_SIZE, -} from "./consts"; -import { - type NVM3Object, - compressObjects, - fragmentLargeObject, - writeObject, -} from "./object"; -import { type NVM3Page, readPage, writePageHeader } from "./page"; -import { dumpObject, dumpPage } from "./utils"; - -function comparePages(p1: NVM3Page, p2: NVM3Page) { - if (p1.header.eraseCount === p2.header.eraseCount) { - return p1.header.offset - p2.header.offset; - } else { - return p1.header.eraseCount - p2.header.eraseCount; - } -} - -export interface NVMMeta { - sharedFileSystem: boolean; - pageSize: number; - deviceFamily: number; - writeSize: PageWriteSize; - memoryMapped: boolean; -} - -export interface NVM3Pages { - /** All application pages in the NVM */ - applicationPages: NVM3Page[]; - /** All application pages in the NVM */ - protocolPages: NVM3Page[]; -} - -export interface NVM3Objects { - /** A compressed map of application-level NVM objects */ - applicationObjects: Map; - /** A compressed map of protocol-level NVM objects */ - protocolObjects: Map; -} - -export function parseNVM( - buffer: Buffer, - verbose: boolean = false, -): NVM3Pages & NVM3Objects { - let offset = 0; - const pages: NVM3Page[] = []; - while (offset < buffer.length) { - const { page, bytesRead } = readPage(buffer, offset); - if (verbose) dumpPage(page); - pages.push(page); - offset += bytesRead; - } - - // 800 series has a shared NVM for protocol and application data. - // We can distinguish between the two, because the application version is stored in a different file ID - - const isSharedFileSystem = pages.some( - (p) => p.objects.some((o) => o.key === ApplicationVersionFile800ID), - ); - // By convention, we only use the applicationPages in that case - let applicationPages: NVM3Page[]; - let protocolPages: NVM3Page[]; - - if (isSharedFileSystem) { - applicationPages = pages; - protocolPages = []; - } else { - applicationPages = pages.filter( - (p) => p.header.offset < ZWAVE_APPLICATION_NVM_SIZE, - ); - protocolPages = pages.filter( - (p) => p.header.offset >= ZWAVE_APPLICATION_NVM_SIZE, - ); - } - - // The pages are written in a ring buffer, find the one with the lowest erase count and start reading from there in order - applicationPages.sort(comparePages); - protocolPages.sort(comparePages); - - // Build a compressed view of the NVM objects - const applicationObjects = compressObjects( - applicationPages.reduce( - (acc, page) => acc.concat(page.objects), - [], - ), - ); - - const protocolObjects = compressObjects( - protocolPages.reduce( - (acc, page) => acc.concat(page.objects), - [], - ), - ); - - if (verbose) { - console.log(); - console.log(); - console.log("Application objects:"); - applicationObjects.forEach((obj) => dumpObject(obj, true)); - console.log(); - console.log("Protocol objects:"); - protocolObjects.forEach((obj) => dumpObject(obj, true)); - } - - return { - applicationPages, - protocolPages, - applicationObjects, - protocolObjects, - }; -} - -export type EncodeNVMOptions = Partial; - -export function encodeNVM( - /** A compressed map of application-level NVM objects */ - applicationObjects: Map, - /** A compressed map of protocol-level NVM objects */ - protocolObjects: Map, - options?: EncodeNVMOptions, -): Buffer { - const { - deviceFamily = 2047, - writeSize = PageWriteSize.WRITE_SIZE_16, - memoryMapped = true, - } = options ?? {}; - const maxPageSize = options?.sharedFileSystem - ? FLASH_MAX_PAGE_SIZE_800 - : FLASH_MAX_PAGE_SIZE_700; - const pageSize = Math.min( - options?.pageSize ?? maxPageSize, - maxPageSize, - ); - - const createEmptyPage = (): Buffer => { - const ret = Buffer.alloc(pageSize, 0xff); - writePageHeader({ - version: 0x01, - eraseCount: 0, - encrypted: false, - deviceFamily, - memoryMapped, - pageSize, - status: PageStatus.OK, - writeSize, - }).copy(ret, 0); - return ret; - }; - - const writeObjects = ( - pages: Buffer[], - objects: Map, - ) => { - // Keep track where we are at with writing in the pages - let pageIndex = -1; - let offsetInPage = -1; - let remainingSpace = -1; - let currentPage!: Buffer; - const nextPage = () => { - pageIndex++; - if (pageIndex >= pages.length) { - throw new ZWaveError( - "Not enough pages!", - ZWaveErrorCodes.NVM_NoSpace, - ); - } - currentPage = pages[pageIndex]; - offsetInPage = NVM3_PAGE_HEADER_SIZE; - remainingSpace = pageSize - offsetInPage; - }; - const incrementOffset = (by: number) => { - const alignedDelta = (by + NVM3_WORD_SIZE - 1) - & ~(NVM3_WORD_SIZE - 1); - - offsetInPage += alignedDelta; - remainingSpace = pageSize - offsetInPage; - }; - - nextPage(); - for (const obj of objects.values()) { - let fragments: NVM3Object[] | undefined; - - if (obj.type === ObjectType.Deleted) continue; - if ( - (obj.type === ObjectType.CounterSmall - && remainingSpace - < NVM3_OBJ_HEADER_SIZE_SMALL + NVM3_COUNTER_SIZE) - || (obj.type === ObjectType.DataSmall - && remainingSpace - < NVM3_OBJ_HEADER_SIZE_SMALL + (obj.data?.length ?? 0)) - ) { - // Small objects cannot be fragmented and need to go on the next page - nextPage(); - } else if ( - obj.type === ObjectType.CounterLarge - || obj.type === ObjectType.DataLarge - ) { - // Large objects may be fragmented - - // We need to start a new page, if the remaining space is not enough for - // the object header plus additional data - if (remainingSpace <= NVM3_OBJ_HEADER_SIZE_LARGE) nextPage(); - - fragments = fragmentLargeObject( - obj as any, - remainingSpace, - pageSize - NVM3_PAGE_HEADER_SIZE, - ); - } - if (!fragments) fragments = [obj]; - - for (const fragment of fragments) { - const objBuffer = writeObject(fragment); - objBuffer.copy(currentPage, offsetInPage); - incrementOffset(objBuffer.length); - - // Each following fragment needs to be written to a different page^ - if (fragments.length > 1) nextPage(); - } - } - }; - - if (options?.sharedFileSystem) { - const pages: Buffer[] = []; - for (let i = 0; i < ZWAVE_SHARED_NVM_SIZE / pageSize; i++) { - pages.push(createEmptyPage()); - } - - const objects = new Map([ - ...applicationObjects, - ...protocolObjects, - ]); - writeObjects(pages, objects); - - return Buffer.concat(pages); - } else { - const applicationPages: Buffer[] = []; - for (let i = 0; i < ZWAVE_APPLICATION_NVM_SIZE / pageSize; i++) { - applicationPages.push(createEmptyPage()); - } - - const protocolPages: Buffer[] = []; - for (let i = 0; i < ZWAVE_PROTOCOL_NVM_SIZE / pageSize; i++) { - protocolPages.push(createEmptyPage()); - } - - writeObjects(applicationPages, applicationObjects); - writeObjects(protocolPages, protocolObjects); - - return Buffer.concat([...applicationPages, ...protocolPages]); - } -} - -export function getNVMMeta(page: NVM3Page, sharedFileSystem: boolean): NVMMeta { - return { - sharedFileSystem, - ...pick(page.header, [ - "pageSize", - "writeSize", - "memoryMapped", - "deviceFamily", - ]), - }; -} diff --git a/packages/nvmedit/src/nvm3/object.ts b/packages/nvmedit/src/nvm3/object.ts deleted file mode 100644 index 44db5245253..00000000000 --- a/packages/nvmedit/src/nvm3/object.ts +++ /dev/null @@ -1,260 +0,0 @@ -import { ZWaveError, ZWaveErrorCodes } from "@zwave-js/core/safe"; -import { - FragmentType, - NVM3_CODE_LARGE_SHIFT, - NVM3_CODE_SMALL_SHIFT, - NVM3_COUNTER_SIZE, - NVM3_OBJ_FRAGTYPE_MASK, - NVM3_OBJ_FRAGTYPE_SHIFT, - NVM3_OBJ_HEADER_SIZE_LARGE, - NVM3_OBJ_HEADER_SIZE_SMALL, - NVM3_OBJ_KEY_MASK, - NVM3_OBJ_KEY_SHIFT, - NVM3_OBJ_LARGE_LEN_MASK, - NVM3_OBJ_TYPE_MASK, - NVM3_WORD_SIZE, - ObjectType, -} from "./consts"; -import { - computeBergerCode, - computeBergerCodeMulti, - validateBergerCodeMulti, -} from "./utils"; - -export interface NVM3Object { - type: ObjectType; - fragmentType: FragmentType; - key: number; - data?: Buffer; -} - -export function readObject( - buffer: Buffer, - offset: number, -): - | { - object: NVM3Object; - bytesRead: number; - } - | undefined -{ - let headerSize = 4; - const hdr1 = buffer.readUInt32LE(offset); - - // Skip over blank page areas - if (hdr1 === 0xffffffff) return; - - const key = (hdr1 >> NVM3_OBJ_KEY_SHIFT) & NVM3_OBJ_KEY_MASK; - let objType: ObjectType = hdr1 & NVM3_OBJ_TYPE_MASK; - let fragmentLength = 0; - let hdr2: number | undefined; - const isLarge = objType === ObjectType.DataLarge - || objType === ObjectType.CounterLarge; - if (isLarge) { - hdr2 = buffer.readUInt32LE(offset + 4); - headerSize += 4; - fragmentLength = hdr2 & NVM3_OBJ_LARGE_LEN_MASK; - } else if (objType > ObjectType.DataSmall) { - // In small objects with data, the length and object type are stored in the same value - fragmentLength = objType - ObjectType.DataSmall; - objType = ObjectType.DataSmall; - } else if (objType === ObjectType.CounterSmall) { - fragmentLength = NVM3_COUNTER_SIZE; - } - - const fragmentType: FragmentType = isLarge - ? (hdr1 >>> NVM3_OBJ_FRAGTYPE_SHIFT) & NVM3_OBJ_FRAGTYPE_MASK - : FragmentType.None; - - if (isLarge) { - validateBergerCodeMulti([hdr1, hdr2!], 32 + NVM3_CODE_LARGE_SHIFT); - } else { - validateBergerCodeMulti([hdr1], NVM3_CODE_SMALL_SHIFT); - } - - if (buffer.length < offset + headerSize + fragmentLength) { - throw new ZWaveError( - "Incomplete object in buffer!", - ZWaveErrorCodes.NVM_InvalidFormat, - ); - } - - let data: Buffer | undefined; - if (fragmentLength > 0) { - data = buffer.subarray( - offset + headerSize, - offset + headerSize + fragmentLength, - ); - } - - const alignedLength = (fragmentLength + NVM3_WORD_SIZE - 1) - & ~(NVM3_WORD_SIZE - 1); - const bytesRead = headerSize + alignedLength; - - const obj: NVM3Object = { - type: objType, - fragmentType, - key, - data, - }; - return { - object: obj, - bytesRead, - }; -} - -export function readObjects(buffer: Buffer): { - objects: NVM3Object[]; - bytesRead: number; -} { - let offset = 0; - const objects: NVM3Object[] = []; - while (offset < buffer.length) { - const result = readObject(buffer, offset); - if (!result) break; - - const { object, bytesRead } = result; - objects.push(object); - - offset += bytesRead; - } - - return { - objects, - bytesRead: offset, - }; -} - -export function writeObject(obj: NVM3Object): Buffer { - const isLarge = obj.type === ObjectType.DataLarge - || obj.type === ObjectType.CounterLarge; - const headerSize = isLarge - ? NVM3_OBJ_HEADER_SIZE_LARGE - : NVM3_OBJ_HEADER_SIZE_SMALL; - const dataLength = obj.data?.length ?? 0; - const ret = Buffer.allocUnsafe(dataLength + headerSize); - - // Write header - if (isLarge) { - let hdr2 = dataLength & NVM3_OBJ_LARGE_LEN_MASK; - - const hdr1 = (obj.type & NVM3_OBJ_TYPE_MASK) - | ((obj.key & NVM3_OBJ_KEY_MASK) << NVM3_OBJ_KEY_SHIFT) - | ((obj.fragmentType & NVM3_OBJ_FRAGTYPE_MASK) - << NVM3_OBJ_FRAGTYPE_SHIFT); - - const bergerCode = computeBergerCodeMulti( - [hdr1, hdr2], - 32 + NVM3_CODE_LARGE_SHIFT, - ); - hdr2 |= bergerCode << NVM3_CODE_LARGE_SHIFT; - - ret.writeInt32LE(hdr1, 0); - ret.writeInt32LE(hdr2, 4); - } else { - let typeAndLen = obj.type; - if (typeAndLen === ObjectType.DataSmall && dataLength > 0) { - typeAndLen += dataLength; - } - let hdr1 = (typeAndLen & NVM3_OBJ_TYPE_MASK) - | ((obj.key & NVM3_OBJ_KEY_MASK) << NVM3_OBJ_KEY_SHIFT); - const bergerCode = computeBergerCode(hdr1, NVM3_CODE_SMALL_SHIFT); - hdr1 |= bergerCode << NVM3_CODE_SMALL_SHIFT; - - ret.writeInt32LE(hdr1, 0); - } - - // Write data - if (obj.data) { - obj.data.copy(ret, headerSize); - } - return ret; -} - -export function fragmentLargeObject( - obj: NVM3Object & { type: ObjectType.DataLarge | ObjectType.CounterLarge }, - maxFirstFragmentSizeWithHeader: number, - maxFragmentSizeWithHeader: number, -): NVM3Object[] { - const ret: NVM3Object[] = []; - - if ( - obj.data!.length + NVM3_OBJ_HEADER_SIZE_LARGE - <= maxFirstFragmentSizeWithHeader - ) { - return [obj]; - } - - let offset = 0; - while (offset < obj.data!.length) { - const fragmentSize = offset === 0 - ? maxFirstFragmentSizeWithHeader - NVM3_OBJ_HEADER_SIZE_LARGE - : maxFragmentSizeWithHeader - NVM3_OBJ_HEADER_SIZE_LARGE; - const data = obj.data!.subarray(offset, offset + fragmentSize); - - ret.push({ - type: obj.type, - key: obj.key, - fragmentType: offset === 0 - ? FragmentType.First - : data.length + NVM3_OBJ_HEADER_SIZE_LARGE - < maxFragmentSizeWithHeader - ? FragmentType.Last - : FragmentType.Next, - data, - }); - - offset += fragmentSize; - } - - return ret; -} - -/** - * Takes the raw list of objects from the pages ring buffer and compresses - * them so that each object is only stored once. - */ -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -export function compressObjects(objects: NVM3Object[]) { - const ret = new Map(); - // Only insert valid objects. This means non-fragmented ones, non-deleted ones - // and fragmented ones in the correct and complete order - outer: for (let i = 0; i < objects.length; i++) { - const obj = objects[i]; - if (obj.type === ObjectType.Deleted) { - ret.delete(obj.key); - continue; - } else if (obj.fragmentType === FragmentType.None) { - ret.set(obj.key, obj); - continue; - } else if (obj.fragmentType !== FragmentType.First || !obj.data) { - // This is the broken rest of an overwritten object, skip it - continue; - } - - // We're looking at the first fragment of a fragmented object - const parts: Buffer[] = [obj.data]; - for (let j = i + 1; j < objects.length; j++) { - // The next objects must have the same key and either be the - // next or the last fragment with data - const next = objects[j]; - if (next.key !== obj.key || !next.data) { - // Invalid object, skipping - continue outer; - } else if (next.fragmentType === FragmentType.Next) { - parts.push(next.data); - } else if (next.fragmentType === FragmentType.Last) { - parts.push(next.data); - break; - } - } - // Combine all fragments into a single readable object - ret.set(obj.key, { - key: obj.key, - fragmentType: FragmentType.None, - type: obj.type, - data: Buffer.concat(parts), - }); - } - return ret; -} diff --git a/packages/nvmedit/src/nvm3/page.ts b/packages/nvmedit/src/nvm3/page.ts deleted file mode 100644 index 4e967f1b80a..00000000000 --- a/packages/nvmedit/src/nvm3/page.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { ZWaveError, ZWaveErrorCodes } from "@zwave-js/core/safe"; -import { - NVM3_MIN_PAGE_SIZE, - NVM3_PAGE_COUNTER_MASK, - NVM3_PAGE_COUNTER_SIZE, - NVM3_PAGE_HEADER_SIZE, - NVM3_PAGE_MAGIC, - type PageStatus, - type PageWriteSize, -} from "./consts"; -import { type NVM3Object, readObjects } from "./object"; -import { computeBergerCode, validateBergerCode } from "./utils"; - -export interface NVM3PageHeader { - offset: number; - version: number; - eraseCount: number; - status: PageStatus; - encrypted: boolean; - pageSize: number; - writeSize: PageWriteSize; - memoryMapped: boolean; - deviceFamily: number; -} - -export interface NVM3Page { - header: NVM3PageHeader; - objects: NVM3Object[]; -} - -// The page size field has a value from 0 to 7 describing page sizes from 512 to 65536 bytes -export function pageSizeToBits(pageSize: number): number { - return Math.ceil(Math.log2(pageSize) - Math.log2(NVM3_MIN_PAGE_SIZE)); -} - -export function pageSizeFromBits(bits: number): number { - return NVM3_MIN_PAGE_SIZE * Math.pow(2, bits); -} - -export function readPage( - buffer: Buffer, - offset: number, -): { page: NVM3Page; bytesRead: number } { - const { version, eraseCount } = tryGetVersionAndEraseCount(buffer, offset); - - // Page status - const status = buffer.readUInt32LE(offset + 12); - - const devInfo = buffer.readUInt16LE(offset + 16); - const deviceFamily = devInfo & 0x7ff; - const writeSize = (devInfo >> 11) & 0b1; - const memoryMapped = !!((devInfo >> 12) & 0b1); - let pageSize = pageSizeFromBits((devInfo >> 13) & 0b111); - - if (pageSize > 0xffff) { - // Some controllers have no valid info in the page size bits, resulting - // in an impossibly large page size. To try and figure out the actual page - // size without knowing the hardware, we scan the buffer for the next valid - // page start. - for (let exponent = 0; exponent < 0b111; exponent++) { - const testPageSize = pageSizeFromBits(exponent); - const nextOffset = offset + testPageSize; - if ( - // exactly end of NVM OR - buffer.length === nextOffset - // next page - || isValidPageHeaderAtOffset(buffer, nextOffset) - ) { - pageSize = testPageSize; - break; - } - } - } - if (pageSize > 0xffff) { - throw new ZWaveError( - "Could not determine page size!", - ZWaveErrorCodes.NVM_InvalidFormat, - ); - } - - if (buffer.length < offset + pageSize) { - throw new ZWaveError( - "Incomplete page in buffer!", - ZWaveErrorCodes.NVM_InvalidFormat, - ); - } - - const formatInfo = buffer.readUInt16LE(offset + 18); - - const encrypted = !(formatInfo & 0b1); - - const header: NVM3PageHeader = { - offset, - version, - eraseCount, - status, - encrypted, - pageSize, - writeSize, - memoryMapped, - deviceFamily, - }; - const bytesRead = pageSize; - const data = buffer.subarray(offset + 20, offset + bytesRead); - - const { objects } = readObjects(data); - - return { - page: { header, objects }, - bytesRead, - }; -} - -function tryGetVersionAndEraseCount( - buffer: Buffer, - offset: number, -): { version: number; eraseCount: number } { - if (offset > buffer.length - NVM3_PAGE_HEADER_SIZE) { - throw new ZWaveError( - "Incomplete page in buffer!", - ZWaveErrorCodes.NVM_InvalidFormat, - ); - } - - const version = buffer.readUInt16LE(offset); - const magic = buffer.readUInt16LE(offset + 2); - if (magic !== NVM3_PAGE_MAGIC) { - throw new ZWaveError( - "Not a valid NVM3 page!", - ZWaveErrorCodes.NVM_InvalidFormat, - ); - } - if (version !== 0x01) { - throw new ZWaveError( - `Unsupported NVM3 page version: ${version}`, - ZWaveErrorCodes.NVM_NotSupported, - ); - } - - // The erase counter is saved twice, once normally, once inverted - let eraseCount = buffer.readUInt32LE(offset + 4); - const eraseCountCode = eraseCount >>> NVM3_PAGE_COUNTER_SIZE; - eraseCount &= NVM3_PAGE_COUNTER_MASK; - validateBergerCode(eraseCount, eraseCountCode, NVM3_PAGE_COUNTER_SIZE); - - let eraseCountInv = buffer.readUInt32LE(offset + 8); - const eraseCountInvCode = eraseCountInv >>> NVM3_PAGE_COUNTER_SIZE; - eraseCountInv &= NVM3_PAGE_COUNTER_MASK; - validateBergerCode( - eraseCountInv, - eraseCountInvCode, - NVM3_PAGE_COUNTER_SIZE, - ); - - if (eraseCount !== (~eraseCountInv & NVM3_PAGE_COUNTER_MASK)) { - throw new ZWaveError( - "Invalid erase count!", - ZWaveErrorCodes.NVM_InvalidFormat, - ); - } - - return { version, eraseCount }; -} - -function isValidPageHeaderAtOffset( - buffer: Buffer, - offset: number, -): boolean { - try { - tryGetVersionAndEraseCount(buffer, offset); - return true; - } catch { - return false; - } -} - -export function writePageHeader( - header: Omit, -): Buffer { - const ret = Buffer.alloc(NVM3_PAGE_HEADER_SIZE); - - ret.writeUInt16LE(header.version, 0); - ret.writeUInt16LE(NVM3_PAGE_MAGIC, 2); - - let eraseCount = header.eraseCount & NVM3_PAGE_COUNTER_MASK; - const eraseCountCode = computeBergerCode( - eraseCount, - NVM3_PAGE_COUNTER_SIZE, - ); - eraseCount |= eraseCountCode << NVM3_PAGE_COUNTER_SIZE; - ret.writeInt32LE(eraseCount, 4); - - let eraseCountInv = ~header.eraseCount & NVM3_PAGE_COUNTER_MASK; - const eraseCountInvCode = computeBergerCode( - eraseCountInv, - NVM3_PAGE_COUNTER_SIZE, - ); - eraseCountInv |= eraseCountInvCode << NVM3_PAGE_COUNTER_SIZE; - ret.writeInt32LE(eraseCountInv, 8); - - ret.writeUInt32LE(header.status, 12); - - const devInfo = (header.deviceFamily & 0x7ff) - | ((header.writeSize & 0b1) << 11) - | ((header.memoryMapped ? 1 : 0) << 12) - | (pageSizeToBits(header.pageSize) << 13); - ret.writeUInt16LE(devInfo, 16); - - const formatInfo = header.encrypted ? 0xfffe : 0xffff; - ret.writeUInt16LE(formatInfo, 18); - - return ret; -} diff --git a/packages/nvmedit/src/nvm500/NVMParser.ts b/packages/nvmedit/src/nvm500/NVMParser.ts index 1fb81943592..1770a680c70 100644 --- a/packages/nvmedit/src/nvm500/NVMParser.ts +++ b/packages/nvmedit/src/nvm500/NVMParser.ts @@ -1,726 +1,9 @@ -import { - type CommandClasses, - MAX_NODES, - ZWaveError, - ZWaveErrorCodes, - encodeBitMask, - parseBitMask, -} 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 { - type Route, - type SUCUpdateEntry, - encodeRoute, - encodeSUCUpdateEntry, - parseRoute, - parseSUCUpdateEntry, -} from "../files"; -import { - type NVM500NodeInfo, - type NVMDescriptor, - type NVMModuleDescriptor, - encodeNVM500NodeInfo, - encodeNVMDescriptor, - encodeNVMModuleDescriptor, - 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, - type NVMData, - type NVMEntryName, - NVMEntrySizes, - NVMEntryType, - type NVMLayout, - NVMModuleType, - NVM_SERIALAPI_HOST_SIZE, - type ParsedNVMEntry, - ROUTECACHE_VALID, -} from "./shared"; +import { type CommandClasses } from "@zwave-js/core/safe"; +import { type Route } from "../lib/common/routeCache"; +import { type SUCUpdateEntry } from "../lib/common/sucUpdateEntry"; +import { type NVM500NodeInfo } from "../lib/nvm500/EntryParsers"; +import { type NVM500Impl } from "../lib/nvm500/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(); - - 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.subarray(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(key: NVMEntryName): T { - return this.cache.get(key)?.data[0] as T; - } - - private getAll( - key: NVMEntryName, - ): T extends Buffer ? T : T[] { - return this.cache.get(key)?.data as any; - } - - public toJSON(): Required { - const nvmDescriptor = this.getOne("nvmDescriptor"); - const ownHomeId = this.getOne("EX_NVM_HOME_ID_far"); - const learnedHomeId = this.getOne("NVM_HOMEID_far"); - - const lastNodeId = this.getOne( - "EX_NVM_LAST_USED_NODE_ID_START_far", - ); - const maxNodeId = this.getOne("EX_NVM_MAX_NODE_ID_far"); - - const nodeInfos = this.getAll( - "EX_NVM_NODE_TABLE_START_far", - ); - const sucUpdateIndizes = this.getAll( - "EX_NVM_SUC_CONTROLLER_LIST_START_far", - ); - const appRouteLock = new Set( - this.getOne("EX_NVM_ROUTECACHE_APP_LOCK_far"), - ); - const routeSlaveSUC = new Set( - this.getOne("EX_NVM_SUC_ROUTING_SLAVE_LIST_START_far"), - ); - const pendingDiscovery = new Set( - this.getOne("NVM_PENDING_DISCOVERY_far"), - ); - const sucPendingUpdate = new Set( - this.getOne("EX_NVM_PENDING_UPDATE_far"), - ); - const virtualNodes = new Set( - this.getOne("EX_NVM_BRIDGE_NODEPOOL_START_far") ?? [], - ); - const lwr = this.getAll("EX_NVM_ROUTECACHE_START_far"); - const nlwr = this.getAll("EX_NVM_ROUTECACHE_NLWR_SR_START_far"); - const neighbors = this.getAll( - "EX_NVM_ROUTING_TABLE_START_far", - ); - - const numCCs = this.getOne("EEOFFSET_CMDCLASS_LEN_far"); - const commandClasses = this.getAll( - "EEOFFSET_CMDCLASS_far", - ).slice(0, numCCs); - - const nodes: Record = {}; - 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("NVM_NODEID_far"), - lastNodeId, - staticControllerNodeId: this.getOne( - "EX_NVM_STATIC_CONTROLLER_NODE_ID_START_far", - ), - sucLastIndex: this.getOne( - "EX_NVM_SUC_LAST_INDEX_START_far", - ), - controllerConfiguration: this.getOne( - "EX_NVM_CONTROLLER_CONFIGURATION_far", - ), - sucUpdateEntries: this.getAll( - "EX_NVM_SUC_NODE_LIST_START_far", - ).filter(Boolean), - maxNodeId, - reservedId: this.getOne("EX_NVM_RESERVED_ID_far"), - systemState: this.getOne("NVM_SYSTEM_STATE"), - watchdogStarted: this.getOne( - "EEOFFSET_WATCHDOG_STARTED_far", - ), - rfConfig: { - powerLevelNormal: this.getAll( - "EEOFFSET_POWERLEVEL_NORMAL_far", - ), - powerLevelLow: this.getAll( - "EEOFFSET_POWERLEVEL_LOW_far", - ), - powerMode: this.getOne( - "EEOFFSET_MODULE_POWER_MODE_far", - ), - powerModeExtintEnable: this.getOne( - "EEOFFSET_MODULE_POWER_MODE_EXTINT_ENABLE_far", - ), - powerModeWutTimeout: this.getOne( - "EEOFFSET_MODULE_POWER_MODE_WUT_TIMEOUT_far", - ), - }, - preferredRepeaters: this.getOne( - "NVM_PREFERRED_REPEATERS_far", - ), - - commandClasses, - applicationData: this.getOne( - "EEOFFSET_HOST_OFFSET_START_far", - ).toString("hex"), - }, - nodes, - }; - } -} - -export class NVMSerializer { - public constructor(private readonly impl: NVM500Details) {} - public readonly entries = new Map(); - private nvmSize: number = 0; - - private setOne(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(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( - key: NVMEntryName, - map: Map, - 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, - 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( - "EEOFFSET_HOST_OFFSET_START_far", - Buffer.from(c.applicationData, "hex"), - ); - } else { - this.setOne( - "EEOFFSET_HOST_OFFSET_START_far", - Buffer.alloc(NVM_SERIALAPI_HOST_SIZE, 0xff), - ); - } - - // Set node infos - const nodeInfos = new Map(); - const sucUpdateIndizes = new Map(); - const appRouteLock: number[] = []; - const routeSlaveSUC: number[] = []; - const pendingDiscovery: number[] = []; - const sucPendingUpdate: number[] = []; - const virtualNodes: number[] = []; - const lwr = new Map(); - const nlwr = new Map(); - const neighbors = new Map(); - - 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( - "EX_NVM_NODE_TABLE_START_far", - nodeInfos, - ); - this.setFromNodeMap( - "EX_NVM_SUC_CONTROLLER_LIST_START_far", - sucUpdateIndizes, - 0xfe, - ); - this.setOne("EX_NVM_ROUTECACHE_APP_LOCK_far", appRouteLock); - this.setOne( - "EX_NVM_SUC_ROUTING_SLAVE_LIST_START_far", - routeSlaveSUC, - ); - this.setOne("NVM_PENDING_DISCOVERY_far", pendingDiscovery); - this.setOne("EX_NVM_PENDING_UPDATE_far", sucPendingUpdate); - this.setOne("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", { - ...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(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(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; @@ -734,7 +17,7 @@ export interface NVM500Meta { firmwareID: number; productType: number; productID: number; - library: NVM500Details["library"]; + library: NVM500Impl["library"]; } export interface NVM500JSONController { diff --git a/packages/nvmedit/src/shared.ts b/packages/nvmedit/src/shared.ts deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.11.0.bin b/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.11.0.bin index 558b933d251..3a99941edba 100644 Binary files a/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.11.0.bin and b/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.11.0.bin differ diff --git a/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.12.0.bin b/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.12.0.bin index b3b718476d7..9a45fe805e5 100644 Binary files a/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.12.0.bin and b/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.12.0.bin differ diff --git a/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.15.4.bin b/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.15.4.bin index b55f5feeb6a..07b413cded2 100644 Binary files a/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.15.4.bin and b/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.15.4.bin differ diff --git a/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.16.2_1.bin b/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.16.2_1.bin index 36522cce083..dad76349ffd 100644 Binary files a/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.16.2_1.bin and b/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.16.2_1.bin differ diff --git a/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.16.2_2.bin b/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.16.2_2.bin index 388048e38e1..afd823ee10c 100644 Binary files a/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.16.2_2.bin and b/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.16.2_2.bin differ diff --git a/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.17.2.bin b/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.17.2.bin index 8bd68231b34..a919ef041bb 100644 Binary files a/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.17.2.bin and b/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.17.2.bin differ diff --git a/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.18.1.bin b/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.18.1.bin index b76cd65be25..c307f38ad3b 100644 Binary files a/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.18.1.bin and b/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.18.1.bin differ diff --git a/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.18.2.bin b/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.18.2.bin index 0633149d398..beb4f23a439 100644 Binary files a/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.18.2.bin and b/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.18.2.bin differ diff --git a/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.18.3.bin b/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.18.3.bin index 29669edf148..836e268fab7 100644 Binary files a/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.18.3.bin and b/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.18.3.bin differ diff --git a/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.18.4.bin b/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.18.4.bin index cc9fcbd8f60..56e00048dd9 100644 Binary files a/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.18.4.bin and b/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.18.4.bin differ diff --git a/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.19.0.bin b/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.19.0.bin index d5f69d481b0..850ce515da2 100644 Binary files a/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.19.0.bin and b/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.19.0.bin differ diff --git a/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.19.1.bin b/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.19.1.bin index 7998524d2af..e4a64b24b2c 100644 Binary files a/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.19.1.bin and b/packages/nvmedit/test/fixtures/nvm_700_invariants/ctrlr_backup_700_7.19.1.bin differ diff --git a/packages/zwave-js/src/lib/controller/Controller.ts b/packages/zwave-js/src/lib/controller/Controller.ts index 3237443350a..1edda6e9990 100644 --- a/packages/zwave-js/src/lib/controller/Controller.ts +++ b/packages/zwave-js/src/lib/controller/Controller.ts @@ -99,7 +99,15 @@ import { securityClassIsS2, securityClassOrder, } from "@zwave-js/core"; -import { migrateNVM } from "@zwave-js/nvmedit"; +import { + BufferedNVMReader, + NVM3, + NVM3Adapter, + NVM500, + NVM500Adapter, + type NVMAdapter, + migrateNVM, +} from "@zwave-js/nvmedit"; import { type BootloaderChunk, BootloaderChunkType, @@ -153,7 +161,6 @@ import { ApplicationUpdateRequestSmartStartHomeIDReceived, ApplicationUpdateRequestSmartStartLongRangeHomeIDReceived, } from "../serialapi/application/ApplicationUpdateRequest"; - import { ShutdownRequest, type ShutdownResponse, @@ -409,6 +416,7 @@ import { SecurityBootstrapFailure, type SmartStartProvisioningEntry, } from "./Inclusion"; +import { SerialNVMIO500, SerialNVMIO700 } from "./NVMIO"; import { determineNIF } from "./NodeInformationFrame"; import { protocolVersionToSDKVersion } from "./ZWaveSDKVersions"; import { @@ -7040,6 +7048,24 @@ ${associatedNodes.join(", ")}`, } } + private _nvm: NVMAdapter | undefined; + /** Provides access to the controller's non-volatile memory */ + public get nvm(): NVMAdapter { + if (!this._nvm) { + if (this.sdkVersionGte("7.0")) { + const io = new BufferedNVMReader(new SerialNVMIO700(this)); + const nvm3 = new NVM3(io); + this._nvm = new NVM3Adapter(nvm3); + } else { + const io = new BufferedNVMReader(new SerialNVMIO500(this)); + const nvm = new NVM500(io); + this._nvm = new NVM500Adapter(nvm); + } + } + + return this._nvm; + } + /** * **Z-Wave 500 series only** * @@ -7680,7 +7706,7 @@ ${associatedNodes.join(", ")}`, } else { targetNVM = await this.backupNVMRaw500(convertProgress); } - const convertedNVM = migrateNVM(nvmData, targetNVM); + const convertedNVM = await migrateNVM(nvmData, targetNVM); this.driver.controllerLog.print("Restoring NVM backup..."); if (this.sdkVersionGte("7.0")) { diff --git a/packages/zwave-js/src/lib/controller/NVMIO.ts b/packages/zwave-js/src/lib/controller/NVMIO.ts new file mode 100644 index 00000000000..07ebc78e2c9 --- /dev/null +++ b/packages/zwave-js/src/lib/controller/NVMIO.ts @@ -0,0 +1,260 @@ +import { ZWaveError, ZWaveErrorCodes } from "@zwave-js/core"; +import { NVMAccess, type NVMIO } from "@zwave-js/nvmedit"; +import { FunctionType } from "@zwave-js/serial"; +import { nvmSizeToBufferSize } from "../serialapi/nvm/GetNVMIdMessages"; +import { type ZWaveController } from "./Controller"; + +/** NVM IO over serial for 500 series controllers */ +export class SerialNVMIO500 implements NVMIO { + public constructor(controller: ZWaveController) { + this._controller = controller; + } + + private _controller: ZWaveController; + private _size: number | undefined; + private _chunkSize: number | undefined; + + public async open(_access: NVMAccess): Promise { + this._size = nvmSizeToBufferSize( + (await this._controller.getNVMId()).memorySize, + ); + if (!this._size) { + throw new ZWaveError( + "Unknown NVM size - cannot backup!", + ZWaveErrorCodes.Controller_NotSupported, + ); + } + return NVMAccess.ReadWrite; + } + + get size(): number { + if (this._size == undefined) { + throw new ZWaveError( + "The NVM is not open", + ZWaveErrorCodes.NVM_NotOpen, + ); + } + return this._size; + } + + get accessMode(): NVMAccess { + if (this._size == undefined) { + return NVMAccess.None; + } else { + return NVMAccess.ReadWrite; + } + } + + async determineChunkSize(): Promise { + if (this._size == undefined) { + throw new ZWaveError( + "The NVM is not open", + ZWaveErrorCodes.NVM_NotOpen, + ); + } + + if (!this._chunkSize) { + // Try reading the maximum size at first, the Serial API should return chunks in a size it supports + // For some reason, there is no documentation and no official command for this + const chunk = await this._controller.externalNVMReadBuffer( + 0, + 0xffff, + ); + // Some SDK versions return an empty buffer when trying to read a buffer that is too long + // Fallback to a sane (but maybe slow) size + this._chunkSize = chunk.length || 48; + } + return this._chunkSize; + } + + async read( + offset: number, + length: number, + ): Promise<{ buffer: Buffer; endOfFile: boolean }> { + // Ensure we're not reading out of bounds + const size = this.size; + if (offset < 0 || offset >= size) { + throw new ZWaveError( + "Cannot read outside of the NVM", + ZWaveErrorCodes.Argument_Invalid, + ); + } + + const chunkSize = await this.determineChunkSize(); + const readSize = Math.min(length, chunkSize, size - offset); + + const buffer = await this._controller.externalNVMReadBuffer( + offset, + readSize, + ); + const endOfFile = offset + readSize >= size; + return { + buffer, + endOfFile, + }; + } + + async write( + offset: number, + data: Buffer, + ): Promise<{ bytesWritten: number; endOfFile: boolean }> { + // Ensure we're not writing out of bounds + const size = this.size; + if (offset < 0 || offset >= size) { + throw new ZWaveError( + "Cannot read outside of the NVM", + ZWaveErrorCodes.Argument_Invalid, + ); + } + + // Write requests need 5 bytes more than read requests, which limits our chunk size + const chunkSize = await this.determineChunkSize() - 5; + const writeSize = Math.min(data.length, chunkSize, size - offset); + + await this._controller.externalNVMWriteBuffer( + offset, + data.subarray(0, writeSize), + ); + const endOfFile = offset + writeSize >= size; + return { + bytesWritten: writeSize, + endOfFile, + }; + } + + close(): Promise { + // Nothing to do really + return Promise.resolve(); + } +} + +/** NVM IO over serial for 700+ series controllers */ +export class SerialNVMIO700 implements NVMIO { + public constructor(controller: ZWaveController) { + this._controller = controller; + if ( + controller.isFunctionSupported( + FunctionType.ExtendedNVMOperations, + ) + ) { + this._open = async () => { + const { size } = await controller.externalNVMOpenExt(); + return size; + }; + this._read = (offset, length) => + controller.externalNVMReadBufferExt(offset, length); + this._write = (offset, buffer) => + controller.externalNVMWriteBufferExt(offset, buffer); + this._close = () => controller.externalNVMCloseExt(); + } else { + this._open = () => controller.externalNVMOpen(); + this._read = (offset, length) => + controller.externalNVMReadBuffer700(offset, length); + this._write = (offset, buffer) => + controller.externalNVMWriteBuffer700(offset, buffer); + this._close = () => controller.externalNVMClose(); + } + } + + private _controller: ZWaveController; + + private _open: () => Promise; + private _read: ( + offset: number, + length: number, + ) => Promise<{ buffer: Buffer; endOfFile: boolean }>; + private _write: ( + offset: number, + buffer: Buffer, + ) => Promise<{ endOfFile: boolean }>; + private _close: () => Promise; + + private _size: number | undefined; + private _chunkSize: number | undefined; + private _accessMode: NVMAccess = NVMAccess.None; + + public async open( + access: NVMAccess.Read | NVMAccess.Write, + ): Promise { + this._size = await this._open(); + // We only support reading or writing, not both + this._accessMode = access; + return access; + } + + get size(): number { + if (this._size == undefined) { + throw new ZWaveError( + "The NVM is not open", + ZWaveErrorCodes.NVM_NotOpen, + ); + } + return this._size; + } + + get accessMode(): NVMAccess { + return this._accessMode; + } + + async determineChunkSize(): Promise { + if (!this._chunkSize) { + // The write requests have the same size as the read response - if this yields no + // data, default to a sane (but maybe slow) size + this._chunkSize = (await this._read(0, 0xff)).buffer.length || 48; + } + + return this._chunkSize; + } + + async read( + offset: number, + length: number, + ): Promise<{ buffer: Buffer; endOfFile: boolean }> { + // Ensure we're not reading out of bounds + const size = this.size; + if (offset < 0 || offset >= size) { + throw new ZWaveError( + "Cannot read outside of the NVM", + ZWaveErrorCodes.Argument_Invalid, + ); + } + + const chunkSize = await this.determineChunkSize(); + + return this._read( + offset, + Math.min(length, chunkSize, size - offset), + ); + } + + async write( + offset: number, + data: Buffer, + ): Promise<{ bytesWritten: number; endOfFile: boolean }> { + // Ensure we're not writing out of bounds + const size = this.size; + if (offset < 0 || offset >= size) { + throw new ZWaveError( + "Cannot read outside of the NVM", + ZWaveErrorCodes.Argument_Invalid, + ); + } + + const chunkSize = await this.determineChunkSize(); + const writeSize = Math.min(data.length, chunkSize, size - offset); + + const { endOfFile } = await this._write( + offset, + data.subarray(0, writeSize), + ); + return { + bytesWritten: writeSize, + endOfFile, + }; + } + + close(): Promise { + this._accessMode = NVMAccess.None; + return this._close(); + } +} diff --git a/test/run_duplex.ts b/test/run_duplex.ts index b00ae82f8f5..b788fdbb165 100644 --- a/test/run_duplex.ts +++ b/test/run_duplex.ts @@ -1,8 +1,7 @@ -import { SecurityClass } from "@zwave-js/core"; import { wait as _wait } from "alcalzone-shared/async"; import path from "node:path"; import "reflect-metadata"; -import { Driver, InclusionStrategy, RFRegion } from "zwave-js"; +import { Driver, RFRegion } from "zwave-js"; const wait = _wait; @@ -22,7 +21,7 @@ const port_primary = const port_secondary = "/dev/serial/by-id/usb-Silicon_Labs_CP2102N_USB_to_UART_Bridge_Controller_ca4d95064355ee118d4d1294de9da576-if00-port0"; -let pin: string | undefined; +// let pin: string | undefined; const driver_primary = new Driver(port_primary, { logConfig: { @@ -71,36 +70,36 @@ const driver_primary = new Driver(port_primary, { .on("error", console.error) .once("driver ready", async () => { // Test code goes here - await wait(1000); - await driver_primary.hardReset(); - await wait(5000); - await driver_primary.controller.beginInclusion({ - strategy: InclusionStrategy.Default, - userCallbacks: { - abort() {}, - async grantSecurityClasses(requested) { - return { - clientSideAuth: false, - securityClasses: [ - SecurityClass.S0_Legacy, - SecurityClass.S2_Unauthenticated, - SecurityClass.S2_Authenticated, - SecurityClass.S2_AccessControl, - ], - }; - }, - async validateDSKAndEnterPIN(dsk) { - // Try to read PIN from the file pin.txt - for (let i = 0; i < 100; i++) { - if (typeof pin === "string" && pin?.length === 5) { - return pin; - } - await wait(1000); - } - return false; - }, - }, - }); + // await wait(1000); + // await driver_primary.hardReset(); + // await wait(5000); + // await driver_primary.controller.beginInclusion({ + // strategy: InclusionStrategy.Default, + // userCallbacks: { + // abort() {}, + // async grantSecurityClasses(requested) { + // return { + // clientSideAuth: false, + // securityClasses: [ + // SecurityClass.S0_Legacy, + // SecurityClass.S2_Unauthenticated, + // SecurityClass.S2_Authenticated, + // SecurityClass.S2_AccessControl, + // ], + // }; + // }, + // async validateDSKAndEnterPIN(dsk) { + // // Try to read PIN from the file pin.txt + // for (let i = 0; i < 100; i++) { + // if (typeof pin === "string" && pin?.length === 5) { + // return pin; + // } + // await wait(1000); + // } + // return false; + // }, + // }, + // }); }) .once("bootloader ready", async () => { // What to do when stuck in the bootloader @@ -151,22 +150,22 @@ const driver_secondary = new Driver(port_secondary, { lockDir: path.join(__dirname, "cache2/locks"), }, allowBootloaderOnly: true, - joinNetworkUserCallbacks: { - showDSK(dsk) { - pin = dsk.split("-")[0]; - }, - done() { - pin = undefined; - }, - }, + // joinNetworkUserCallbacks: { + // showDSK(dsk) { + // pin = dsk.split("-")[0]; + // }, + // done() { + // pin = undefined; + // }, + // }, }) .on("error", console.error) .once("driver ready", async () => { // Test code goes here - await wait(5000); - await driver_secondary.hardReset(); - await wait(5000); - await driver_secondary.controller.beginJoiningNetwork(); + // await wait(5000); + // await driver_secondary.hardReset(); + // await wait(5000); + // await driver_secondary.controller.beginJoiningNetwork(); }) .once("bootloader ready", async () => { // What to do when stuck in the bootloader