Skip to content

Commit

Permalink
fix: support 800 series NVM layout and new NVM3 files (#6670)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlCalzone authored Feb 10, 2024
1 parent 72cae6d commit b98399a
Show file tree
Hide file tree
Showing 20 changed files with 407 additions and 108 deletions.
38 changes: 37 additions & 1 deletion packages/nvmedit/src/convert.test.ts.md

Large diffs are not rendered by default.

Binary file modified packages/nvmedit/src/convert.test.ts.snap
Binary file not shown.
104 changes: 82 additions & 22 deletions packages/nvmedit/src/convert.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
type CommandClasses,
ControllerCapabilityFlags,
NodeIDType,
type NodeProtocolInfo,
NodeType,
RFRegion,
Expand All @@ -9,7 +10,7 @@ import {
isZWaveError,
stripUndefined,
} from "@zwave-js/core/safe";
import { cloneDeep, pick } from "@zwave-js/shared/safe";
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";
Expand All @@ -23,6 +24,8 @@ import {
ApplicationTypeFile,
ApplicationTypeFileID,
ApplicationVersionFile,
ApplicationVersionFile800,
ApplicationVersionFile800ID,
ApplicationVersionFileID,
ControllerInfoFile,
ControllerInfoFileID,
Expand Down Expand Up @@ -65,6 +68,10 @@ import {
nodeIdToRouteCacheFileIDV1,
sucUpdateIndexToSUCUpdateEntriesFileIDV5,
} from "./files";
import {
ApplicationNameFile,
ApplicationNameFileID,
} from "./files/ApplicationNameFile";
import {
type NVM3Objects,
type NVMMeta,
Expand Down Expand Up @@ -122,6 +129,7 @@ export interface NVMJSONController {
};

applicationData?: string | null;
applicationName?: string | null;
}

export interface NVMJSONControllerRFConfig {
Expand All @@ -130,6 +138,7 @@ export interface NVMJSONControllerRFConfig {
measured0dBm: number;
enablePTI: number | null;
maxTXPower: number | null;
nodeIdType: NodeIDType | null;
}

export interface NVMJSONNodeWithInfo
Expand Down Expand Up @@ -202,8 +211,7 @@ function createEmptyPhysicalNode(): NVMJSONNodeWithInfo {

/** Converts a compressed set of NVM objects to a JSON representation */
export function nvmObjectsToJSON(
applicationObjects: ReadonlyMap<number, NVM3Object>,
protocolObjects: ReadonlyMap<number, NVM3Object>,
objects: ReadonlyMap<number, NVM3Object>,
): NVMJSON {
const nodes = new Map<number, NVMJSONNode>();
const getNode = (id: number): NVMJSONNode => {
Expand All @@ -215,12 +223,9 @@ export function nvmObjectsToJSON(
id: number | ((id: number) => boolean),
): NVM3Object | undefined => {
if (typeof id === "number") {
return protocolObjects.get(id) ?? applicationObjects.get(id);
return objects.get(id);
} else {
for (const [key, obj] of protocolObjects) {
if (id(key)) return obj;
}
for (const [key, obj] of applicationObjects) {
for (const [key, obj] of objects) {
if (id(key)) return obj;
}
}
Expand All @@ -232,7 +237,9 @@ export function nvmObjectsToJSON(
const ret = getObject(id);
if (ret) return ret;
throw new ZWaveError(
`Object${typeof id === "number" ? ` ${id}` : ""} not found!`,
`Object${
typeof id === "number" ? ` ${num2hex(id)} (${id})` : ""
} not found!`,
ZWaveErrorCodes.NVM_ObjectNotFound,
);
};
Expand Down Expand Up @@ -414,10 +421,22 @@ export function nvmObjectsToJSON(

// === Application NVM files ===

const applicationVersionFile = getFileOrThrow<ApplicationVersionFile>(
const applicationVersionFile700 = getFile<ApplicationVersionFile>(
ApplicationVersionFileID,
"7.0.0", // We don't know the version here yet
);
const applicationVersionFile800 = getFile<ApplicationVersionFile800>(
ApplicationVersionFile800ID,
"7.0.0", // We don't know the version here yet
);
const applicationVersionFile = applicationVersionFile700
?? applicationVersionFile800;
if (!applicationVersionFile) {
throw new ZWaveError(
"ApplicationVersionFile not found!",
ZWaveErrorCodes.NVM_ObjectNotFound,
);
}
const applicationVersion =
`${applicationVersionFile.major}.${applicationVersionFile.minor}.${applicationVersionFile.patch}`;

Expand All @@ -437,6 +456,11 @@ export function nvmObjectsToJSON(
ApplicationTypeFileID,
applicationVersion,
);
const applicationNameFile = getFile<ApplicationNameFile>(
ApplicationNameFileID,
applicationVersion,
);

const preferredRepeaters = getFile<ProtocolPreferredRepeatersFile>(
ProtocolPreferredRepeatersFileID,
applicationVersion,
Expand All @@ -459,10 +483,8 @@ export function nvmObjectsToJSON(
"dcdcConfig",
] as const;
const controller: NVMJSONController = {
protocolVersion:
`${protocolVersionFile.major}.${protocolVersionFile.minor}.${protocolVersionFile.patch}`,
applicationVersion:
`${applicationVersionFile.major}.${applicationVersionFile.minor}.${applicationVersionFile.patch}`,
protocolVersion,
applicationVersion,
homeId: `0x${controllerInfoFile.homeId.toString("hex")}`,
...pick(controllerInfoFile, controllerProps),
...pick(applicationTypeFile, [
Expand All @@ -485,11 +507,13 @@ export function nvmObjectsToJSON(
measured0dBm: rfConfigFile.measured0dBm,
enablePTI: rfConfigFile.enablePTI ?? null,
maxTXPower: rfConfigFile.maxTXPower ?? null,
nodeIdType: rfConfigFile.nodeIdType ?? null,
},
}
: {}),
sucUpdateEntries,
applicationData: applicationDataFile?.data.toString("hex") ?? null,
applicationName: applicationNameFile?.name ?? null,
};

// Make sure all props are defined
Expand Down Expand Up @@ -609,6 +633,7 @@ function serializeCommonApplicationObjects(nvm: NVMJSON): NVM3Object[] {
]),
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());
Expand All @@ -623,6 +648,15 @@ function serializeCommonApplicationObjects(nvm: NVMJSON): NVM3Object[] {
ret.push(applDataFile.serialize());
}

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;
}

Expand Down Expand Up @@ -858,12 +892,13 @@ export function jsonToNVMObjects_v7_11_0(
let targetProtocolVersion: semver.SemVer;
let targetProtocolFormat: number;

// We currently support application version migrations up to 7.19.1
// We currently support application version migrations up to:
const HIGHEST_SUPPORTED_SDK_VERSION = "7.21.0";
// For all higher ones, set the highest version we support and let the controller handle the migration itself
if (semver.lte(targetSDKVersion, "7.19.1")) {
if (semver.lte(targetSDKVersion, HIGHEST_SUPPORTED_SDK_VERSION)) {
targetApplicationVersion = semver.parse(targetSDKVersion)!;
} else {
targetApplicationVersion = semver.parse("7.19.1")!;
targetApplicationVersion = semver.parse(HIGHEST_SUPPORTED_SDK_VERSION)!;
}

// The protocol version file only seems to be updated when the format of the protocol file system changes
Expand Down Expand Up @@ -905,7 +940,10 @@ export function jsonToNVMObjects_v7_11_0(
};

// Application files
const applVersionFile = new ApplicationVersionFile({
const ApplicationVersionConstructor = json.meta?.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,
Expand All @@ -923,6 +961,7 @@ export function jsonToNVMObjects_v7_11_0(
measured0dBm: +3.3,
enablePTI: null,
maxTXPower: null,
nodeIdType: null,
};

// Make sure the RF config format matches the application version.
Expand All @@ -931,6 +970,9 @@ export function jsonToNVMObjects_v7_11_0(
target.controller.rfConfig.enablePTI ??= 0;
target.controller.rfConfig.maxTXPower ??= 14.0;
}
if (semver.gte(targetSDKVersion, "7.21.0")) {
target.controller.rfConfig.nodeIdType ??= NodeIDType.Short;
}

addApplicationObjects(...serializeCommonApplicationObjects(target));

Expand Down Expand Up @@ -1041,8 +1083,18 @@ export function nvmToJSON(
debugLogs: boolean = false,
): Required<NVMJSON> {
const nvm = parseNVM(buffer, debugLogs);
const ret = nvmObjectsToJSON(nvm.applicationObjects, nvm.protocolObjects);
ret.meta = getNVMMeta(nvm.protocolPages[0]);
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<NVMJSON>;
}

Expand Down Expand Up @@ -1374,12 +1426,20 @@ export function migrateNVM(sourceNVM: Buffer, targetNVM: Buffer): Buffer {
&& targetProtocolFileFormat > MAX_PROTOCOL_FILE_FORMAT
&& sourceProtocolFileFormat
&& sourceProtocolFileFormat <= targetProtocolFileFormat
&& sourceNVM.length === targetNVM.length
) {
// ...both the source and the target are 700 series, but at least the target uses an unsupported protocol version.
// We can be sure hwoever that the target can upgrade any 700 series NVM to its protocol version, as long as the
// We can be sure however that the target can upgrade any 700 series NVM to its protocol version, as long as the
// source protocol version is not higher than the target's
return sourceNVM;
} else if (source.type === 700 && target.type === 700) {
} else if (
source.type === 700
&& target.type === 700
// ...the source and target NVMs have the same size and structure
&& sourceNVM.length === targetNVM.length
&& source.json.meta.sharedFileSystem
=== target.json.meta.sharedFileSystem
) {
// ... the source and target protocol versions are compatible without conversion
const sourceProtocolVersion = source.json.controller.protocolVersion;
const targetProtocolVersion = target.json.controller.protocolVersion;
Expand Down
39 changes: 39 additions & 0 deletions packages/nvmedit/src/files/ApplicationNameFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { cpp2js } from "@zwave-js/shared";
import { type NVMObject } from "..";
import {
NVMFile,
type NVMFileCreationOptions,
type NVMFileDeserializationOptions,
getNVMFileIDStatic,
gotDeserializationOptions,
nvmFileID,
} from "./NVMFile";

export interface ApplicationNameFileOptions extends NVMFileCreationOptions {
name: string;
}

@nvmFileID(0x4100c)
export class ApplicationNameFile extends NVMFile {
public constructor(
options: NVMFileDeserializationOptions | ApplicationNameFileOptions,
) {
super(options);
if (gotDeserializationOptions(options)) {
this.name = cpp2js(this.payload.toString("utf8"));
} else {
this.name = options.name;
}
}

public name: string;

public serialize(): NVMObject {
// 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);
nameAsString.subarray(0, this.payload.length - 1).copy(this.payload);
return super.serialize();
}
}
export const ApplicationNameFileID = getNVMFileIDStatic(ApplicationNameFile);
37 changes: 34 additions & 3 deletions packages/nvmedit/src/files/ApplicationRFConfigFile.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { RFRegion, ZWaveError, ZWaveErrorCodes } from "@zwave-js/core/safe";
import {
NodeIDType,
RFRegion,
ZWaveError,
ZWaveErrorCodes,
} from "@zwave-js/core/safe";
import { type AllOrNone, getEnumMemberName } from "@zwave-js/shared/safe";
import semver from "semver";
import type { NVM3Object } from "../nvm3/object";
Expand All @@ -21,7 +26,10 @@ export type ApplicationRFConfigFileOptions =
& AllOrNone<{
enablePTI?: number;
maxTXPower?: number;
}>;
}>
& {
nodeIdType?: number;
};

@nvmFileID(104)
export class ApplicationRFConfigFile extends NVMFile {
Expand All @@ -44,6 +52,13 @@ export class ApplicationRFConfigFile extends NVMFile {
this.measured0dBm = this.payload.readInt16LE(3) / 10;
this.enablePTI = this.payload[5];
this.maxTXPower = this.payload.readInt16LE(6) / 10;
} else if (this.payload.length === 9) {
this.rfRegion = this.payload[0];
this.txPower = this.payload.readInt16LE(1) / 10;
this.measured0dBm = this.payload.readInt16LE(3) / 10;
this.enablePTI = this.payload[5];
this.maxTXPower = this.payload.readInt16LE(6) / 10;
this.nodeIdType = this.payload[8];
} else {
throw new ZWaveError(
`ApplicationRFConfigFile has unsupported length ${this.payload.length}`,
Expand All @@ -56,6 +71,7 @@ export class ApplicationRFConfigFile extends NVMFile {
this.measured0dBm = options.measured0dBm;
this.enablePTI = options.enablePTI;
this.maxTXPower = options.maxTXPower;
this.nodeIdType = options.nodeIdType;
}
}

Expand All @@ -64,6 +80,7 @@ export class ApplicationRFConfigFile extends NVMFile {
public measured0dBm: number;
public enablePTI?: number;
public maxTXPower?: number;
public nodeIdType?: NodeIDType;

public serialize(): NVM3Object {
if (semver.lt(this.fileVersion, "7.18.1")) {
Expand All @@ -78,13 +95,21 @@ export class ApplicationRFConfigFile extends NVMFile {
this.payload[3] = this.enablePTI ?? 0;
this.payload.writeInt16LE((this.maxTXPower ?? 0) * 10, 4);
}
} else {
} else if (semver.lt(this.fileVersion, "7.21.0")) {
this.payload = Buffer.alloc(8, 0);
this.payload[0] = this.rfRegion;
this.payload.writeInt16LE(this.txPower * 10, 1);
this.payload.writeInt16LE(this.measured0dBm * 10, 3);
this.payload[5] = this.enablePTI ?? 0;
this.payload.writeInt16LE((this.maxTXPower ?? 0) * 10, 6);
} else {
this.payload = Buffer.alloc(9, 0);
this.payload[0] = this.rfRegion;
this.payload.writeInt16LE(this.txPower * 10, 1);
this.payload.writeInt16LE(this.measured0dBm * 10, 3);
this.payload[5] = this.enablePTI ?? 0;
this.payload.writeInt16LE((this.maxTXPower ?? 0) * 10, 6);
this.payload[8] = this.nodeIdType ?? NodeIDType.Short;
}
return super.serialize();
}
Expand All @@ -103,6 +128,12 @@ export class ApplicationRFConfigFile extends NVMFile {
if (this.maxTXPower != undefined) {
ret["max TX power"] = `${this.maxTXPower.toFixed(1)} dBm`;
}
if (this.nodeIdType != undefined) {
ret["node ID type"] = getEnumMemberName(
NodeIDType,
this.nodeIdType,
);
}
return ret;
}
}
Expand Down
Loading

0 comments on commit b98399a

Please sign in to comment.