inventoresed
Version:
Z-Wave driver written entirely in JavaScript/TypeScript
273 lines (250 loc) • 7.98 kB
text/typescript
import { ZWaveError, ZWaveErrorCodes } from "../error/ZWaveError";
import {
getBitMaskWidth,
getMinimumShiftForBitMask,
validatePayload,
} from "../util/misc";
type Brand<K, T> = K & { __brand: T };
type BrandedUnknown<T> = Brand<"unknown", T>;
export type Maybe<T> = T | BrandedUnknown<T>;
export const unknownNumber = "unknown" as Maybe<number>;
export const unknownBoolean = "unknown" as Maybe<boolean>;
/** Parses a boolean that is encoded as a single byte and might also be "unknown" */
export function parseMaybeBoolean(
val: number,
preserveUnknown: boolean = true,
): Maybe<boolean> | undefined {
return val === 0xfe
? preserveUnknown
? unknownBoolean
: undefined
: parseBoolean(val);
}
/** Parses a boolean that is encoded as a single byte */
export function parseBoolean(val: number): boolean | undefined {
return val === 0 ? false : val === 0xff ? true : undefined;
}
/** Encodes a boolean that is encoded as a single byte */
export function encodeBoolean(val: boolean): number {
return val ? 0xff : 0;
}
/** Encodes a boolean that is encoded as a single byte and might also be "unknown" */
export function encodeMaybeBoolean(val: Maybe<boolean>): number {
return val === "unknown" ? 0xfe : val ? 0xff : 0;
}
/** Parses a single-byte number from 0 to 99, which might also be "unknown" */
export function parseMaybeNumber(val: number): Maybe<number> | undefined {
return val === 0xfe ? unknownNumber : parseNumber(val);
}
/** Parses a single-byte number from 0 to 99 */
export function parseNumber(val: number): number | undefined {
return val <= 99 ? val : val === 0xff ? 99 : undefined;
}
/**
* Parses a floating point value with a scale from a buffer.
*/
export function parseFloatWithScale(
payload: Buffer,
allowEmpty?: false,
): {
value: number;
scale: number;
bytesRead: number;
};
/**
* Parses a floating point value with a scale from a buffer.
* @param allowEmpty Whether empty floats (precision = scale = size = 0 no value) are accepted
*/
export function parseFloatWithScale(
payload: Buffer,
allowEmpty: true,
): {
value?: number;
scale?: number;
bytesRead: number;
};
/**
* Parses a floating point value with a scale from a buffer.
* @param allowEmpty Whether empty floats (precision = scale = size = 0 no value) are accepted
*/
export function parseFloatWithScale(
payload: Buffer,
allowEmpty: boolean = false,
): {
value?: number;
scale?: number;
bytesRead: number;
} {
validatePayload(payload.length >= 1);
const precision = (payload[0] & 0b111_00_000) >>> 5;
const scale = (payload[0] & 0b000_11_000) >>> 3;
const size = payload[0] & 0b111;
if (allowEmpty && size === 0) {
validatePayload(precision === 0, scale === 0);
return { bytesRead: 1 };
} else {
validatePayload(size >= 1, size <= 4, payload.length >= 1 + size);
const value = payload.readIntBE(1, size) / Math.pow(10, precision);
return { value, scale, bytesRead: 1 + size };
}
}
function getPrecision(num: number): number {
if (!Number.isFinite(num)) return 0;
let e = 1;
let p = 0;
while (Math.round(num * e) / e !== num) {
e *= 10;
p++;
}
return p;
}
/** The minimum and maximum values that can be stored in each numeric value type */
export const IntegerLimits = Object.freeze({
UInt8: Object.freeze({ min: 0, max: 0xff }),
UInt16: Object.freeze({ min: 0, max: 0xffff }),
UInt24: Object.freeze({ min: 0, max: 0xffffff }),
UInt32: Object.freeze({ min: 0, max: 0xffffffff }),
Int8: Object.freeze({ min: -0x80, max: 0x7f }),
Int16: Object.freeze({ min: -0x8000, max: 0x7fff }),
Int24: Object.freeze({ min: -0x800000, max: 0x7fffff }),
Int32: Object.freeze({ min: -0x80000000, max: 0x7fffffff }),
});
export function getMinIntegerSize(
value: number,
signed: boolean,
): 1 | 2 | 4 | undefined {
if (signed) {
if (value >= IntegerLimits.Int8.min && value <= IntegerLimits.Int8.max)
return 1;
else if (
value >= IntegerLimits.Int16.min &&
value <= IntegerLimits.Int16.max
)
return 2;
else if (
value >= IntegerLimits.Int32.min &&
value <= IntegerLimits.Int32.max
)
return 4;
} else if (value >= 0) {
if (value <= IntegerLimits.UInt8.max) return 1;
if (value <= IntegerLimits.UInt16.max) return 2;
if (value <= IntegerLimits.UInt32.max) return 4;
}
// Not a valid size
}
export function getIntegerLimits(
size: 1 | 2 | 3 | 4,
signed: boolean,
): { min: number; max: number } {
return (IntegerLimits as any)[`${signed ? "" : "U"}Int${size * 8}`];
}
/**
* Encodes a floating point value with a scale into a buffer
* @param override can be used to overwrite the automatic computation of precision and size with fixed values
*/
export function encodeFloatWithScale(
value: number,
scale: number,
override: {
size?: number;
precision?: number;
} = {},
): Buffer {
const precision = override.precision ?? Math.min(getPrecision(value), 7);
value = Math.round(value * Math.pow(10, precision));
let size: number | undefined = getMinIntegerSize(value, true);
if (size == undefined) {
throw new ZWaveError(
`Cannot encode the value ${value} because its too large or too small to fit into 4 bytes`,
ZWaveErrorCodes.Arithmetic,
);
} else if (override.size != undefined && override.size > size) {
size = override.size;
}
const ret = Buffer.allocUnsafe(1 + size);
ret[0] =
((precision & 0b111) << 5) | ((scale & 0b11) << 3) | (size & 0b111);
ret.writeIntBE(value, 1, size);
return ret;
}
/** Parses a bit mask into a numeric array */
export function parseBitMask(mask: Buffer, startValue: number = 1): number[] {
const numBits = mask.length * 8;
const ret: number[] = [];
for (let index = 1; index <= numBits; index++) {
const byteNum = (index - 1) >>> 3; // id / 8
const bitNum = (index - 1) % 8;
if ((mask[byteNum] & (2 ** bitNum)) !== 0)
ret.push(index + startValue - 1);
}
return ret;
}
/** Serializes a numeric array with a given maximum into a bit mask */
export function encodeBitMask(
values: readonly number[],
maxValue: number,
startValue: number = 1,
): Buffer {
const numBytes = Math.ceil((maxValue - startValue + 1) / 8);
const ret = Buffer.alloc(numBytes, 0);
for (let val = startValue; val <= maxValue; val++) {
if (values.indexOf(val) === -1) continue;
const byteNum = (val - startValue) >>> 3; // id / 8
const bitNum = (val - startValue) % 8;
ret[byteNum] |= 2 ** bitNum;
}
return ret;
}
/**
* Parses a partial value from a "full" value. Example:
* ```txt
* Value = 01110000
* Mask = 00110000
* ----------------
* 11 => 3 (unsigned) or -1 (signed)
* ```
*
* @param value The full value the partial should be extracted from
* @param bitMask The bit mask selecting the partial value
* @param signed Whether the partial value should be interpreted as signed
*/
export function parsePartial(
value: number,
bitMask: number,
signed: boolean,
): number {
const shift = getMinimumShiftForBitMask(bitMask);
const width = getBitMaskWidth(bitMask);
let ret = (value & bitMask) >>> shift;
// If the high bit is set and this value should be signed, we need to convert it
if (signed && !!(ret & (2 ** (width - 1)))) {
// To represent a negative partial as signed, the high bits must be set to 1
ret = ~(~ret & (bitMask >>> shift));
}
return ret;
}
/**
* Encodes a partial value into a "full" value. Example:
* ```txt
* Value = 01··0000
* + Partial = 10 (2 or -2 depending on signed interpretation)
* Mask = 00110000
* ------------------
* 01100000
* ```
*
* @param fullValue The full value the partial should be merged into
* @param partialValue The partial to be merged
* @param bitMask The bit mask selecting the partial value
*/
export function encodePartial(
fullValue: number,
partialValue: number,
bitMask: number,
): number {
const ret =
(fullValue & ~bitMask) |
((partialValue << getMinimumShiftForBitMask(bitMask)) & bitMask);
return ret >>> 0; // convert to unsigned if necessary
}