inventoresed
Version:
Z-Wave driver written entirely in JavaScript/TypeScript
265 lines (235 loc) • 6.72 kB
text/typescript
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.slice(
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!.slice(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<number, NVM3Object>();
// 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;
}