@w2a-iiot/parsers
Version:
WIKA JavaScript parsers
1,411 lines (1,396 loc) • 527 kB
JavaScript
import * as v from "valibot";
import { safeParse } from "valibot";
//#region ../parsers/src/codecs/utils.ts
/**
* Checks the validity of multiple channels.
* @param channels The channels to check.
* @param codecName Optional codec name for more detailed error messages.
* @throws Will throw an error if start is equal to or greater than end.
* @throws Will throw an error if duplicate channel names are found.
*/
function checkChannelsValidity(channels, codecName) {
const channelNameSet = /* @__PURE__ */ new Set();
channels.forEach((channel) => {
if (channel.start >= channel.end) {
if (codecName) throw new Error(`Invalid channel range in codec ${codecName}: ${channel.start} >= ${channel.end} in channel ${channel.name}`);
throw new Error(`Invalid channel range: ${channel.start} >= ${channel.end} in channel ${channel.name}`);
}
if (channelNameSet.has(channel.name)) {
if (codecName) throw new Error(`Duplicate channel name found in codec ${codecName}: ${channel.name}`);
throw new Error(`Duplicate channel name found: ${channel.name}`);
}
channelNameSet.add(channel.name);
});
}
/**
* Checks the validity of multiple codecs and ensures they are compatible.
* Validates that all codecs have unique names, matching channel names, consistent channel ranges,
* and consistent adjustMeasurementRangeDisallowed settings.
*
* @param codecs The array of codecs to validate.
* @returns A record mapping channel names to their adjustment permission status.
* `true` means adjustment is disallowed, `false` means adjustment is allowed.
* @throws Will throw an error if no codecs are provided.
* @throws Will throw an error if codec names are not unique.
* @throws Will throw an error if codecs have different channel names.
* @throws Will throw an error if channels with the same name have different ranges across codecs.
* @throws Will throw an error if channels with the same name have inconsistent adjustMeasurementRangeDisallowed settings.
*/
function checkCodecsValidity(codecs) {
if (codecs.length === 0) throw new Error("At least one codec must be provided");
codecs.forEach((codec) => {
checkChannelsValidity(codec.getChannels(), codec.name);
});
codecs.forEach((codec, index) => {
if (codecs.filter((_, i) => i !== index).some((c) => c.name === codec.name)) throw new Error(`Codec names must be unique. Duplicate found: ${codec.name}`);
});
const firstCodecChannelNames = codecs[0].getChannels().map((c) => c.name);
codecs.forEach((codec, index) => {
if (index === 0) return;
const currentCodecChannelNames = codec.getChannels().map((c) => c.name);
const check = {
missing: [],
extra: []
};
currentCodecChannelNames.forEach((name) => {
if (!firstCodecChannelNames.includes(name)) check.extra.push(name);
});
firstCodecChannelNames.forEach((name) => {
if (!currentCodecChannelNames.includes(name)) check.missing.push(name);
});
if (check.extra.length > 0) throw new Error(`Codec ${codec.name} has extra channels not present in other codecs: ${check.extra.join(", ")}`);
if (check.missing.length > 0) throw new Error(`Codec ${codec.name} is missing channels present in other codecs: ${check.missing.join(", ")}`);
});
const channelRanges = /* @__PURE__ */ new Map();
const channelAdjustmentPermissions = {};
codecs.forEach((codec) => {
codec.getChannels().forEach((channel) => {
const range = [channel.start, channel.end];
const isDisallowed = channel.adjustMeasurementRangeDisallowed ?? false;
if (channelRanges.has(channel.name)) {
const existingRange = channelRanges.get(channel.name);
if (existingRange[0] !== range[0] || existingRange[1] !== range[1]) throw new Error(`Channel ${channel.name} has inconsistent ranges across codecs: ${JSON.stringify(existingRange)} vs ${JSON.stringify(range)}`);
const existingDisallowed = channelAdjustmentPermissions[channel.name];
if (existingDisallowed !== isDisallowed) throw new Error(`Channel ${channel.name} has inconsistent adjustMeasurementRangeDisallowed settings across codecs: ${existingDisallowed} vs ${isDisallowed}`);
} else {
channelRanges.set(channel.name, range);
channelAdjustmentPermissions[channel.name] = isDisallowed;
}
});
});
return channelAdjustmentPermissions;
}
//#endregion
//#region ../parsers/src/schemas/index.ts
function createFPortSchema() {
return v.pipe(v.number(), v.minValue(1), v.maxValue(255), v.integer());
}
function createRecvTimeSchema() {
return v.optional(v.string());
}
function createUplinkInputSchema() {
return v.object({
bytes: v.array(v.pipe(v.number(), v.minValue(0), v.maxValue(255), v.integer())),
fPort: v.optional(v.pipe(v.number(), v.minValue(1), v.maxValue(224), v.integer())),
recvTime: v.optional(v.string())
}, "Uplink input should be an object with `bytes` and optional `fPort` and `recvTime` properties.");
}
function createHexUplinkInputSchema() {
return v.pipe(v.union([v.object({
bytes: v.pipe(v.string()),
fPort: v.optional(createFPortSchema()),
recvTime: createRecvTimeSchema()
}), v.string()]), v.transform((input) => {
if (typeof input === "string") return { bytes: input };
return input;
}));
}
//#endregion
//#region ../../node_modules/.pnpm/nstr@0.1.3/node_modules/nstr/dist/index.mjs
/**
* Automatically determines the appropriate precision for a number by detecting
* floating point artifacts (consecutive 0s or 9s) and truncating accordingly.
*/ function nstr(value, options = {}) {
const { threshold = 4, maxDecimals = 10 } = options;
if (Number.isNaN(value)) return "NaN";
if (!isFinite(value)) return value.toString();
if (Number.isInteger(value)) return value.toString();
const str = value.toFixed(maxDecimals);
let patternStart = -1;
let patternChar = "";
const decimalIndex = str.indexOf(".");
const startIndex = decimalIndex >= 0 ? decimalIndex + 1 : str.length;
for (let i = startIndex; i < str.length; i++) {
const char = str[i];
if (char === "0" || char === "9") {
let consecutiveCount = 1;
let j = i + 1;
while (j < str.length && str[j] === char) {
consecutiveCount++;
j++;
}
if (consecutiveCount >= threshold) {
patternStart = i;
patternChar = char;
break;
}
i = j - 1;
}
}
if (patternStart !== -1) {
let truncated = str.substring(0, patternStart);
if (patternChar === "9") {
const beforeNines = truncated;
const rounded = Number.parseFloat(beforeNines);
const decimalPos = beforeNines.indexOf(".");
const decimalPlaces = decimalPos >= 0 ? beforeNines.length - decimalPos - 1 : 0;
const increment = Math.pow(10, -decimalPlaces);
const roundedUp = rounded < 0 ? rounded - increment : rounded + increment;
if (decimalPlaces > 0) truncated = roundedUp.toFixed(decimalPlaces);
else truncated = roundedUp.toString();
}
let result = truncated.replace(/\.?0+$/, "");
if (result.endsWith(".")) result = result.slice(0, -1);
if (result === "-0." || result === "0." || result === "-0" || result === "0") result = "0";
return result;
}
let result = str.replace(/\.?0+$/, "");
if (result.endsWith(".")) result = result.slice(0, -1);
if (result === "-0." || result === "0." || result === "-0" || result === "0") result = "0";
return result;
}
//#endregion
//#region ../parsers/src/utils.ts
const DEFAULT_ROUNDING_DECIMALS = 4;
/**
* Returns the number of decimals to use for rounding, with robust handling for edge cases.
*
* @param roundingDecimals The requested number of decimals (may be undefined, NaN, Infinity, etc.)
* @param currentDecimals The fallback number of decimals if roundingDecimals is not valid
* @returns The number of decimals to use for rounding
*
* - If roundingDecimals is undefined, NaN, or -Infinity, returns fallback (currentDecimals or DEFAULT_ROUNDING_DECIMALS)
* - If roundingDecimals is Infinity, returns Infinity
* - If Math.floor(roundingDecimals) >= 0, returns Math.floor(roundingDecimals)
* - Otherwise, returns fallback
*/
function getRoundingDecimals(roundingDecimals, currentDecimals) {
const fallbackDecimals = currentDecimals ?? DEFAULT_ROUNDING_DECIMALS;
if (roundingDecimals === void 0 || Number.isNaN(roundingDecimals) || roundingDecimals === -Infinity) return fallbackDecimals;
if (roundingDecimals === Infinity) return Infinity;
return Math.floor(roundingDecimals) >= 0 ? Math.floor(roundingDecimals) : fallbackDecimals;
}
/**
* Converts a hexadecimal string to an array of integer bytes.
* The function handles various hex string formats including prefixes and spaces.
* Returns null if the input string contains invalid hexadecimal characters.
*
* @param hexString The hexadecimal string to convert (supports '0x' prefix and spaces)
* @returns Array of integer bytes or null if invalid hex string
* @example
* hexStringToIntArray('1234') // Returns: [0x12, 0x34]
* hexStringToIntArray('0x1234') // Returns: [0x12, 0x34]
* hexStringToIntArray('12 34') // Returns: [0x12, 0x34]
* hexStringToIntArray('xyz') // Returns: null (invalid hex)
*/
function hexStringToIntArray(hexString) {
let adjustedString = hexString.replaceAll(" ", "");
adjustedString = adjustedString.startsWith("0x") ? adjustedString.slice(2) : adjustedString;
const schema = v.pipe(v.string(), v.hexadecimal());
const result = v.safeParse(schema, adjustedString);
if (!result.success) return null;
const intArray = [];
for (let i = 0; i < result.output.length; i += 2) {
const byte = Number.parseInt(result.output.slice(i, i + 2), 16);
intArray.push(byte);
}
return intArray;
}
/**
* Converts a percentage value to a real value within a specified range.
* The function performs linear interpolation between the minimum and maximum values
* based on the given percentage.
*
* @param percentage The percentage value (must be between 0 and 100)
* @param range Object containing min and max values for the target range
* @param range.start The minimum value of the range
* @param range.end The maximum value of the range
* @returns The calculated value within the specified range
* @throws {RangeError} When range.start is not less than range.end
* @example
* percentageToValue(50, {min: 0, max: 100}) // Returns: 50
* percentageToValue(25, {min: -10, max: 10}) // Returns: -5
* percentageToValue(0, {min: 5, max: 15}) // Returns: 5
* percentageToValue(100, {min: 5, max: 15}) // Returns: 15
*/
function percentageToValue(percentage, range) {
if (range.start >= range.end) throw new RangeError("Range start must be less than range end");
return (range.end - range.start) * (percentage / 100) + range.start;
}
/**
* Converts a TULIP scale value to a real value within a specified range.
* TULIP scale uses values from 2500 to 12500, where:
* - 2500 represents 0% (minimum value)
* - 7500 represents 50% (middle value)
* - 12500 represents 100% (maximum value)
*
* The function first converts the TULIP value to a percentage, then maps
* that percentage to the specified range using linear interpolation.
*
* @param tulipValue The TULIP scale value (must be between 2500 and 12500)
* @param range Object containing min and max values for the target range
* @param range.start The minimum value of the range
* @param range.end The maximum value of the range
* @returns The calculated value within the specified range
* Uses {@link percentageToValue} internally, which may throw errors itself.
* @example
* TULIPValueToValue(2500, {min: 0, max: 100}) // Returns: 0 (0%)
* TULIPValueToValue(7500, {min: 0, max: 100}) // Returns: 50 (50%)
* TULIPValueToValue(12500, {min: 0, max: 100}) // Returns: 100 (100%)
* TULIPValueToValue(5000, {min: -10, max: 10}) // Returns: -5 (25%)
*/
function TULIPValueToValue(tulipValue, range) {
return percentageToValue((tulipValue - 2500) * 100 / 1e4 + 0, range);
}
/**
* Converts a slope scale value to a real value within a specified range.
*
* The slope scale uses values from 0 to 10_000 (inclusive). This maps linearly
* to a percentage between 0 and 100 by dividing the slope value by 100, and
* then uses {@link percentageToValue} to map that percentage into the provided
* range using linear interpolation.
*
* @param slopeValue The slope scale value (must be between 0 and 10_000 inclusive)
* @param range Object containing min and max values for the target range
* @param range.start The minimum value of the range
* @param range.end The maximum value of the range
* @returns The calculated value within the specified range
* @throws {RangeError} When slopeValue is not between 0 and 10_000
* Uses {@link percentageToValue} internally, which may throw errors for invalid ranges or percentages.
* @example
* slopeValueToValue(0, {min: 0, max: 100}) // Returns: 0 (0%)
* slopeValueToValue(5000, {min: 0, max: 100}) // Returns: 50 (50%)
* slopeValueToValue(10000, {min: 0, max: 100}) // Returns: 100 (100%)
* slopeValueToValue(2500, {min: -10, max: 10}) // Returns: 5 (25% of span -> 20)
*/
function slopeValueToValue(slopeValue, range) {
if (slopeValue < 0 || slopeValue > 1e4) throw new RangeError(`Slope value must be between 0 and 10_000, is ${slopeValue}`);
const percentage = slopeValue / 100;
const span = range.end - range.start;
return percentage / 100 * span;
}
/**
* Rounds a number to a specified number of decimal places.
*
* This function attempts to smartly handle floating point imprecisions that can occur due to repeating values
* (such as 0.998 actually being 0.99799999... etc.), which are not exactly representable in binary floating point.
*
* @param value The number to round.
* @param decimals The number of decimal places to round to (default is 0). Negative values are treated as 0.
* Values above 100 are clamped to 100.
* @returns The rounded number.
* @example
* roundValue(3.14159, 2) // Returns: 3.14
* roundValue(123.456, 0) // Returns: 123
* roundValue(1.005, 2) // Returns: 1.01
*/
function roundValue(value, decimals) {
decimals = typeof decimals === "number" ? Math.min(Math.max(0, Math.floor(decimals)), 100) : void 0;
if (Number.isInteger(value)) return value;
const v = nstr(value + 5 * Number.EPSILON, { maxDecimals: decimals });
if (v === "") return 0;
return Number.parseFloat(v);
}
/**
* Converts a 4-byte tuple to a 32-bit IEEE 754 float (big-endian).
*
* This is a small, dependency-free helper that converts four bytes into
* a JavaScript Number using a DataView. It returns the raw IEEE-754
* float value as produced by the platform.
*
* @param data Tuple containing exactly 4 bytes in big-endian order
* @returns 32-bit floating point number (raw IEEE-754 value)
*/
function intTuple4ToFloat32(data) {
const buffer = /* @__PURE__ */ new ArrayBuffer(4);
const view = new DataView(buffer);
view.setUint8(0, data[0] & 255);
view.setUint8(1, data[1] & 255);
view.setUint8(2, data[2] & 255);
view.setUint8(3, data[3] & 255);
return view.getFloat32(0);
}
/**
* Converts a 4-byte tuple to a 32-bit IEEE 754 float and returns a
* cleaned numeric value suitable for UI display.
*
* This wrapper applies a small post-processing step using `nstr` to
* remove common floating-point precision artifacts (for example:
* 0.30000000000000004 → "0.3"). The `threshold` parameter controls
* detection sensitivity (non-negative integer). Higher values make the
* function less eager to trim trailing digits.
*
* Note: `nstr` is used here centrally in `utils` so other modules do
* not need to import it directly.
*
* @param data Tuple containing exactly 4 bytes in big-endian order
* @param threshold Non-negative integer controlling artifact detection sensitivity (default: 3)
* @returns Cleaned numeric value (parsed from `nstr` output)
*/
function intTuple4ToFloat32WithThreshold(data, threshold = 3) {
threshold = Math.max(0, Math.floor(threshold));
const value = intTuple4ToFloat32(data);
return Number.parseFloat(nstr(value, { threshold }));
}
//#endregion
//#region ../parsers/src/parser.ts
function defineParser(options) {
const { codecs, throwOnMultipleDecode = true, parserName } = options;
const roundingDecimals = getRoundingDecimals(options.roundingDecimals);
const channelAdjustmentPermissions = checkCodecsValidity(options.codecs);
function createError(message) {
return { errors: [addPrefixToMessage(message)] };
}
function addPrefixToMessage(message) {
return `${parserName} (JS): ${message}`;
}
function decode(input) {
let c;
codecs.forEach((codec) => {
if (codec.canTryDecode(input)) {
if (!c) c = codec;
else if (throwOnMultipleDecode) throw new Error(`Message could not be uniquely decoded. Multiple codecs matched the input.`);
}
});
if (!c) throw new Error(`Message could not be decoded. No codec matched the input.`);
return c.decode(input);
}
function decodeUplink(input) {
try {
const validatedInput = safeParse(createUplinkInputSchema(), input);
if (!validatedInput.success) throw new Error(`Input is not a valid for decoding. Check your input data.`);
const result = decode(validatedInput.output);
if (result.warnings) result.warnings = result.warnings.map(addPrefixToMessage);
return result;
} catch (error) {
if (error instanceof Error) return createError(error.message);
return createError(`Unknown error occurred during decoding in parser ${parserName} with input ${JSON.stringify(input)}`);
}
}
function decodeHexUplink(input) {
const validatedInput = safeParse(createHexUplinkInputSchema(), input);
if (!validatedInput.success) return createError(`Input is not a valid for decoding. Check your input data.`);
const intArray = hexStringToIntArray(validatedInput.output.bytes);
if (!intArray) return createError(`Input bytes is not a valid hexadecimal string.`);
return decodeUplink({
bytes: intArray,
fPort: validatedInput.output.fPort,
recvTime: validatedInput.output.recvTime
});
}
function encodeDownlink(input) {
try {
const codec = codecs.find((c) => c.protocol === input.protocol);
if (!codec) throw new Error(`Codec with protocol ${input.protocol} not found in parser. Available protocols: ${codecs.map((c) => c.protocol).join(", ")}`);
if (!("encode" in codec)) throw new Error(`Codec with protocol ${input.protocol} does not support encoding. Input could not be encoded.`);
return codec.encode(input.input);
} catch (error) {
if (error instanceof Error) return createError(error.message);
return createError(`Unknown error occurred during encoding in parser ${parserName} with input ${JSON.stringify(input)}`);
}
}
function encodeMultipleDownlinks(input) {
try {
const codec = codecs.find((c) => c.protocol === input.protocol);
if (!codec) throw new Error(`Codec with protocol ${input.protocol} not found in parser. Available protocols: ${codecs.map((c) => c.protocol).join(", ")}`);
if (!("encodeMultiple" in codec)) throw new Error(`Codec with protocol ${input.protocol} does not support multiple encoding. Input could not be encoded.`);
return codec.encodeMultiple(input.input);
} catch (error) {
if (error instanceof Error) return createError(error.message);
return createError(`Unknown error occurred during multiple encoding in parser ${parserName} with input ${JSON.stringify(input)}`);
}
}
return {
decodeUplink,
decodeHexUplink,
encodeDownlink,
encodeMultipleDownlinks,
adjustMeasuringRange: (name, range) => {
if (!(name in channelAdjustmentPermissions)) throw new Error(`Channel ${name} does not exist in parser ${parserName}. Cannot adjust measuring range.`);
if (channelAdjustmentPermissions[name]) throw new Error(`Channel ${name} does not allow adjusting the measuring range in parser ${parserName}.`);
codecs.forEach((codec) => {
codec.adjustMeasuringRange(name, range);
});
},
adjustRoundingDecimals: (decimals) => {
const corrected = getRoundingDecimals(decimals, roundingDecimals);
codecs.forEach((codec) => {
codec.adjustRoundingDecimals(corrected);
});
}
};
}
//#endregion
//#region ../parsers/src/codecs/protocols.ts
/**
* TULIP2 protocol identifier.
* Used for legacy devices with basic message handling (0x00-0x09 message types).
*/
const TULIP2_PROTOCOL = "TULIP2";
/**
* TULIP3 protocol identifier.
* Used for advanced devices with comprehensive sensor support (0x10-0x17 message types).
*/
const TULIP3_PROTOCOL = "TULIP3";
//#endregion
//#region ../parsers/src/codecs/tulip2/index.ts
/**
* Creates a TULIP2 protocol codec for decoding and encoding IoT device messages.
*
* TULIP2 is a protocol that uses the first byte (0x00-0x09) as a message type identifier,
* followed by payload data. Each message type is handled by a specific handler function.
*
*
* @param options - Configuration object for the TULIP2 codec, see {@link TULIP2CodecOptions}
*
* @returns A fully configured TULIP2 codec with decode, encode, and adjustment capabilities
*
* @throws {Error} When channel names are duplicated
* @throws {Error} When channel IDs are duplicated
* @throws {Error} When channel ranges are invalid (start >= end)
*
* @example
* ```typescript
* // Define channels with unique names and IDs
* const defineChannels () => ([
* { name: 'temperature', start: -40, end: 125, channelId: 0 },
* { name: 'humidity', start: 0, end: 100, channelId: 1 }
* ] as const);
*
* // Define message handlers
* const handlers = {
* 0x01: (input, { roundingDecimals, channels }) => ({
* data: { temperature: 22.5, timestamp: Date.now() }
* }),
* 0x02: (input, { roundingDecimals, channels }) => ({
* data: { humidity: 55.0, timestamp: Date.now() }
* })
* };
*
* // Create codec
* const codec = defineTULIP2Codec({
* deviceName: 'SensorDevice',
* channels: defineChannels(),
* handlers,
* encodeHandler: (data) => [0x01, 0x42] // Optional encoder
* });
* ```
*
* @warning **ANTI-PATTERN**: Do not reuse the same channel array reference across multiple codec instances.
* The channels array is mutated when `adjustMeasuringRange` is called, which can cause unexpected
* behavior when multiple parsers share the same channel references. Always create fresh channel
* arrays for each codec instance to avoid channel pollution:
*
* ```typescript
* // ❌ WRONG - Reusing channel reference
* const sharedChannels = [{ name: 'temp', start: 0, end: 100, channelId: 0 }];
* const codec1 = defineTULIP2Codec({ channels: sharedChannels, ... });
* const codec2 = defineTULIP2Codec({ channels: sharedChannels, ... }); // Will share mutations!
*
* // ✅ CORRECT - Fresh channel arrays
* const codec1 = defineTULIP2Codec({
* channels: [{ name: 'temp', start: 0, end: 100, channelId: 0 }], ...
* });
* const codec2 = defineTULIP2Codec({
* channels: [{ name: 'temp', start: 0, end: 100, channelId: 0 }], ...
* });
* ```
*
* @see {@link TULIP2CodecOptions} for detailed options interface
* @see {@link MessageHandlers} for handler function signatures
* @see {@link TULIP2Channel} for channel configuration interface
*/
function defineTULIP2Codec(options) {
const codecName = `${options.deviceName}TULIP2`;
let roundingDecimals = getRoundingDecimals(options.roundingDecimals);
checkChannelsValidity(options.channels);
options.channels.forEach((channel, index, array) => {
const duplicate = array.find((c, i) => c.channelId === channel.channelId && i !== index);
if (duplicate) throw new Error(`Duplicate channel ID found: ${channel.channelId} for channels ${channel.name} and ${duplicate.name} in ${codecName} Codec`);
});
function canTryDecode(input) {
const firstByte = input.bytes[0];
return typeof firstByte === "number" && firstByte in options.handlers;
}
function decode(input) {
const firstByte = input.bytes[0];
if (typeof firstByte !== "number") throw new TypeError(`Input must have at least one byte for ${codecName} Codec`);
const handler = options.handlers[firstByte];
if (handler) return handler(input, {
roundingDecimals,
channels: options.channels
});
throw new TypeError(`No handler registered for byte ${firstByte} in ${codecName} Codec`);
}
function getChannels() {
return options.channels.map((c) => ({
end: c.end,
name: c.name,
start: c.start,
...c.adjustMeasurementRangeDisallowed ? { adjustMeasurementRangeDisallowed: true } : {}
}));
}
const codec = {
name: codecName,
protocol: TULIP2_PROTOCOL,
getChannels,
canTryDecode,
decode,
adjustMeasuringRange: (name, range) => {
const channel = options.channels.find((channel) => channel.name === name);
if (!channel) throw new Error(`Channel ${name} not found in ${options.deviceName}TULIP2 Codec`);
channel.start = range.start;
channel.end = range.end;
},
adjustRoundingDecimals: (decimals) => {
roundingDecimals = getRoundingDecimals(decimals, roundingDecimals);
}
};
if (options.encoderFactory) codec.encode = options.encoderFactory({ getChannels: () => options.channels });
if (options.multipleEncodeFactory) codec.encodeMultiple = options.multipleEncodeFactory({ getChannels: () => options.channels });
return codec;
}
//#endregion
//#region ../parsers/src/devices/A2G/parser/tulip2/lookups.ts
const MEASUREMENT_CHANNELS$1 = {
pressure: 0,
flow: 1,
input_1: 2,
input_2: 3,
input_3: 4,
input_4: 5,
relay_status_1: 6,
relay_status_2: 7
};
const PRODUCT_SUB_ID_NAMES$6 = {
0: "LoRaWAN",
1: "MIOTY"
};
const HARDWARE_ASSEMBLY_TYPE_NAMES = {
0: "A2G HE0 Full Assembly",
1: "A2G HE1 1AO Assembly",
2: "A2G HE2 Modbus Assembly",
3: "A2G HE3 Modular Assembly",
128: "A2G LC1 LC1VAO",
129: "A2G LC2 CT",
130: "A2G LC3 BAT"
};
const LPP_MEASURAND_NAMES_PRESSURE = {
3: "Pressure (gauge)",
4: "Pressure (absolute)",
5: "Pressure (differential)"
};
const LPP_MEASURAND_NAMES_FLOW = {
6: "Flow (vol.)",
7: "Flow (mass)"
};
const LPP_MEASURAND_NAMES_INPUT = {
70: "Input 1",
71: "Input 2",
72: "Input 3",
73: "Input 4"
};
const LPP_MEASURAND_NAMES = {
1: "Temperature",
2: "Temperature difference",
...LPP_MEASURAND_NAMES_PRESSURE,
...LPP_MEASURAND_NAMES_FLOW,
8: "Force",
9: "Mass",
10: "Level",
11: "Length",
12: "Volume",
13: "Current",
14: "Voltage",
15: "Resistance",
16: "Capacitance",
17: "Inductance",
18: "Relative",
19: "Time",
20: "Frequency",
21: "Speed",
22: "Acceleration",
23: "Density",
24: "Density (gauge pressure at 20 °C)",
25: "Density (absolute pressure at 20 °C)",
26: "Humidity (relative)",
27: "Humidity (absolute)",
28: "Angle of rotation / inclination",
60: "Device specific",
61: "Device specific",
62: "Device specific",
...LPP_MEASURAND_NAMES_INPUT,
75: "Relay Status 1",
76: "Relay Status 2"
};
const LPP_UNIT_NAMES_PRESSURE = {
1: "Pa",
2: "kPa",
3: "mbar",
4: "mmWC",
5: "inWC"
};
const LPP_UNIT_NAMES_FLOW = {
10: "[m³/s] cubic metre per second",
11: "[m³/h] cubic metre per hour (cbm/h)",
12: "[l/s] litre per second",
13: "[cfm] cubic feet per minute",
14: "[m/s]",
15: "[ft/min]"
};
const LPP_UNIT_NAMES_INPUT = {
0: "None",
20: "% rH",
21: "[g/m³]",
22: "[g/ft³]",
23: "[kJ/kg]",
24: "[BTU/lb]",
30: "normalized",
31: "ppm",
32: "[%] percent",
40: "°C",
41: "°F",
45: "V",
46: "bin"
};
const LPP_UNIT_NAMES = {
...LPP_UNIT_NAMES_PRESSURE,
...LPP_UNIT_NAMES_FLOW,
...LPP_UNIT_NAMES_INPUT
};
const TECHNICAL_ALARM_FLAGS = [
{
name: "PressureSignalOverload",
mask: 1
},
{
name: "AnalogOutput1SignalOverload",
mask: 2
},
{
name: "AnalogOutput2SignalOverload",
mask: 4
},
{
name: "ModbusCommunicationError",
mask: 8
},
{
name: "VoltageInput1SignalOverload",
mask: 16
},
{
name: "VoltageInput2SignalOverload",
mask: 32
},
{
name: "TemperatureInput3SignalOverload",
mask: 64
},
{
name: "TemperatureInput4SignalOverload",
mask: 128
}
];
const DEVICE_ALARM_FLAGS = [
{
name: "ADCConverterError",
mask: 128,
byteIndex: 2
},
{
name: "PressureSensorNoResponseError",
mask: 64,
byteIndex: 2
},
{
name: "PressureSensorTimeoutError",
mask: 32,
byteIndex: 2
},
{
name: "FactoryOptionsWriteError",
mask: 16,
byteIndex: 2
},
{
name: "FactoryOptionsDeleteError",
mask: 8,
byteIndex: 2
},
{
name: "InvalidFactoryOptionsError",
mask: 4,
byteIndex: 2
},
{
name: "UserSettingsInvalidError",
mask: 2,
byteIndex: 2
},
{
name: "UserSettingsReadWriteError",
mask: 1,
byteIndex: 2
},
{
name: "ZeroOffsetOverRangeError",
mask: 128,
byteIndex: 3
},
{
name: "InvalidSignalSourceSpecifiedError",
mask: 64,
byteIndex: 3
},
{
name: "AnalogOutput2OverTemperatureError",
mask: 32,
byteIndex: 3
},
{
name: "AnalogOutput2LoadFaultError",
mask: 16,
byteIndex: 3
},
{
name: "AnalogOutput2OverRangeError",
mask: 8,
byteIndex: 3
},
{
name: "AnalogOutput1OverTemperatureError",
mask: 4,
byteIndex: 3
},
{
name: "AnalogOutput1LoadFaultError",
mask: 2,
byteIndex: 3
},
{
name: "AnalogOutput1OverRangeError",
mask: 1,
byteIndex: 3
}
];
//#endregion
//#region ../parsers/src/devices/A2G/parser/tulip2/index.ts
const ERROR_VALUE_TUPLE = [
255,
255,
255,
255
];
function isErrorTuple(tuple) {
return tuple[0] === ERROR_VALUE_TUPLE[0] && tuple[1] === ERROR_VALUE_TUPLE[1] && tuple[2] === ERROR_VALUE_TUPLE[2] && tuple[3] === ERROR_VALUE_TUPLE[3];
}
function createTULIP2A2GChannels() {
return [
{
channelId: MEASUREMENT_CHANNELS$1.pressure,
name: "pressure",
start: 0,
end: 100,
adjustMeasurementRangeDisallowed: true
},
{
channelId: MEASUREMENT_CHANNELS$1.flow,
name: "flow",
start: 0,
end: 100,
adjustMeasurementRangeDisallowed: true
},
{
channelId: MEASUREMENT_CHANNELS$1.input_1,
name: "input_1",
start: 0,
end: 100,
adjustMeasurementRangeDisallowed: true
},
{
channelId: MEASUREMENT_CHANNELS$1.input_2,
name: "input_2",
start: 0,
end: 100,
adjustMeasurementRangeDisallowed: true
},
{
channelId: MEASUREMENT_CHANNELS$1.input_3,
name: "input_3",
start: 0,
end: 100,
adjustMeasurementRangeDisallowed: true
},
{
channelId: MEASUREMENT_CHANNELS$1.input_4,
name: "input_4",
start: 0,
end: 1,
adjustMeasurementRangeDisallowed: true
},
{
channelId: MEASUREMENT_CHANNELS$1.relay_status_1,
name: "relay_status_1",
start: 0,
end: 1,
adjustMeasurementRangeDisallowed: true
},
{
channelId: MEASUREMENT_CHANNELS$1.relay_status_2,
name: "relay_status_2",
start: 0,
end: 1,
adjustMeasurementRangeDisallowed: true
}
];
}
function createFloatFromTuple(tuple, roundingDecimals) {
if (isErrorTuple(tuple)) throw new Error("Invalid data for channel - measurement: 0xffff, 65535");
return roundValue(intTuple4ToFloat32WithThreshold(tuple), roundingDecimals);
}
const handleDataMessage$12 = (input, options) => {
if (input.bytes.length !== 6 && input.bytes.length !== 27) throw new Error(`Data message (0x01) requires 6 or 27 bytes, but received ${input.bytes.length} bytes`);
const [messageType, configurationId] = [input.bytes[0], input.bytes[1]];
if (input.bytes.length === 6) {
const value = createFloatFromTuple([
input.bytes[2],
input.bytes[3],
input.bytes[4],
input.bytes[5]
], options.roundingDecimals);
return { data: {
messageType,
configurationId,
measurement: { channels: [{
channelId: MEASUREMENT_CHANNELS$1.pressure,
channelName: "pressure",
value
}] }
} };
}
const pressureTuple = [
input.bytes[2],
input.bytes[3],
input.bytes[4],
input.bytes[5]
];
const flowTuple = [
input.bytes[6],
input.bytes[7],
input.bytes[8],
input.bytes[9]
];
const input1Tuple = [
input.bytes[10],
input.bytes[11],
input.bytes[12],
input.bytes[13]
];
const input2Tuple = [
input.bytes[14],
input.bytes[15],
input.bytes[16],
input.bytes[17]
];
const input3Tuple = [
input.bytes[18],
input.bytes[19],
input.bytes[20],
input.bytes[21]
];
const input4Tuple = [
input.bytes[22],
input.bytes[23],
input.bytes[24],
input.bytes[25]
];
const pressureValue = createFloatFromTuple(pressureTuple, options.roundingDecimals);
const flowValue = createFloatFromTuple(flowTuple, options.roundingDecimals);
const input1Value = createFloatFromTuple(input1Tuple, options.roundingDecimals);
const input2Value = createFloatFromTuple(input2Tuple, options.roundingDecimals);
const input3Value = createFloatFromTuple(input3Tuple, options.roundingDecimals);
const input4Value = createFloatFromTuple(input4Tuple, options.roundingDecimals);
const relayByte = input.bytes[26];
return { data: {
messageType,
configurationId,
measurement: { channels: [
{
channelId: MEASUREMENT_CHANNELS$1.pressure,
channelName: "pressure",
value: pressureValue
},
{
channelId: MEASUREMENT_CHANNELS$1.flow,
channelName: "flow",
value: flowValue
},
{
channelId: MEASUREMENT_CHANNELS$1.input_1,
channelName: "input_1",
value: input1Value
},
{
channelId: MEASUREMENT_CHANNELS$1.input_2,
channelName: "input_2",
value: input2Value
},
{
channelId: MEASUREMENT_CHANNELS$1.input_3,
channelName: "input_3",
value: input3Value
},
{
channelId: MEASUREMENT_CHANNELS$1.input_4,
channelName: "input_4",
value: input4Value
},
{
channelId: MEASUREMENT_CHANNELS$1.relay_status_1,
channelName: "relay_status_1",
value: relayByte & 1
},
{
channelId: MEASUREMENT_CHANNELS$1.relay_status_2,
channelName: "relay_status_2",
value: relayByte >> 1 & 1
}
] }
} };
};
const handleTechnicalAlarmMessage$11 = (input) => {
if (input.bytes.length !== 3) throw new Error(`Technical alarm message (0x04) requires 3 bytes, but received ${input.bytes.length} bytes`);
const configurationId = input.bytes[1];
const statusByte = input.bytes[2];
return { data: {
messageType: 4,
configurationId,
technicalAlarms: Object.fromEntries(TECHNICAL_ALARM_FLAGS.map(({ name, mask }) => [name, (statusByte & mask) === mask]))
} };
};
const handleDeviceAlarmMessage$11 = (input) => {
if (input.bytes.length !== 4) throw new Error(`Device alarm message (0x05) requires 4 bytes, but received ${input.bytes.length} bytes`);
const configurationId = input.bytes[1];
const alarmByte1 = input.bytes[2];
const alarmByte2 = input.bytes[3];
return { data: {
messageType: 5,
configurationId,
deviceAlarms: Object.fromEntries(DEVICE_ALARM_FLAGS.map(({ name, mask, byteIndex }) => {
return [name, ((byteIndex === 2 ? alarmByte1 : alarmByte2) & mask) === mask];
}))
} };
};
const handleDeviceIdentificationMessage$11 = (input) => {
if (input.bytes.length !== 33 && input.bytes.length !== 38) throw new Error(`Device identification message (0x07) requires 33 or 38 bytes, but received ${input.bytes.length} bytes`);
const configurationId = input.bytes[1];
const productId = input.bytes[2];
if (productId !== 13) throw new Error(`Invalid productId ${productId} in device identification message. Expected 13 (A2G).`);
const productSubId = input.bytes[3];
const productSubIdName = PRODUCT_SUB_ID_NAMES$6[productSubId];
const sensorFirmwareVersion = `${(input.bytes[4] >> 4).toString(10)}.${(input.bytes[4] & 15).toString(10)}.${input.bytes[5].toString(10)}`;
const sensorHardwareVersion = input.bytes[6].toString(10);
const hardwareAssemblyTypeId = input.bytes[7];
const hardwareAssemblyTypeName = HARDWARE_ASSEMBLY_TYPE_NAMES[hardwareAssemblyTypeId];
if (!hardwareAssemblyTypeName) throw new Error(`Unknown hardware assembly type ${hardwareAssemblyTypeId} in device identification message`);
let serialNumber = "";
for (let i = 8; i < 24; i++) {
const byte = input.bytes[i];
if (byte === 0) break;
serialNumber += String.fromCharCode(byte);
}
const measurementRangeStartPressure = intTuple4ToFloat32WithThreshold([
input.bytes[24],
input.bytes[25],
input.bytes[26],
input.bytes[27]
]);
const measurementRangeEndPressure = intTuple4ToFloat32WithThreshold([
input.bytes[28],
input.bytes[29],
input.bytes[30],
input.bytes[31]
]);
const pressureUnit = input.bytes[32];
const pressureUnitName = LPP_UNIT_NAMES[pressureUnit];
if (!pressureUnitName) throw new Error(`Unknown pressure unit ${pressureUnit} in device identification message`);
const channelConfigurations = [{
measurand: 3,
measurandName: LPP_MEASURAND_NAMES_PRESSURE[3],
measurementRangeStart: measurementRangeStartPressure,
measurementRangeEnd: measurementRangeEndPressure,
unit: pressureUnit,
unitName: pressureUnitName
}];
if (input.bytes.length === 33) return { data: {
messageType: 7,
configurationId,
deviceInformation: {
productId,
productIdName: A2G_NAME,
productSubId,
productSubIdName,
sensorFirmwareVersion,
sensorHardwareVersion,
hardwareAssemblyTypeId,
hardwareAssemblyTypeName,
serialNumber,
channelConfigurations
}
} };
const flowUnit = input.bytes[33];
const flowUnitName = LPP_UNIT_NAMES[flowUnit];
if (!flowUnitName) throw new Error(`Unknown flow unit ${flowUnit} in device identification message`);
const input1Unit = input.bytes[34];
const input1UnitName = LPP_UNIT_NAMES[input1Unit];
if (!input1UnitName) throw new Error(`Unknown input 1 unit ${input1Unit} in device identification message`);
const input2Unit = input.bytes[35];
const input2UnitName = LPP_UNIT_NAMES[input2Unit];
if (!input2UnitName) throw new Error(`Unknown input 2 unit ${input2Unit} in device identification message`);
const input3Unit = input.bytes[36];
const input3UnitName = LPP_UNIT_NAMES[input3Unit];
if (!input3UnitName) throw new Error(`Unknown input 3 unit ${input3Unit} in device identification message`);
const input4Unit = input.bytes[37];
const input4UnitName = LPP_UNIT_NAMES[input4Unit];
if (!input4UnitName) throw new Error(`Unknown input 4 unit ${input4Unit} in device identification message`);
const completeChannelConfiguration = [
channelConfigurations[0],
{
measurand: 6,
measurandName: LPP_MEASURAND_NAMES_FLOW[6],
unit: flowUnit,
unitName: flowUnitName
},
{
measurand: 70,
measurandName: LPP_MEASURAND_NAMES[70],
unit: input1Unit,
unitName: input1UnitName
},
{
measurand: 71,
measurandName: LPP_MEASURAND_NAMES[71],
unit: input2Unit,
unitName: input2UnitName
},
{
measurand: 72,
measurandName: LPP_MEASURAND_NAMES[72],
unit: input3Unit,
unitName: input3UnitName
},
{
measurand: 73,
measurandName: LPP_MEASURAND_NAMES[73],
unit: input4Unit,
unitName: input4UnitName
}
];
return { data: {
messageType: 7,
configurationId,
deviceInformation: {
productId,
productIdName: A2G_NAME,
productSubId,
productSubIdName,
sensorFirmwareVersion,
sensorHardwareVersion,
hardwareAssemblyTypeId,
hardwareAssemblyTypeName,
serialNumber,
channelConfigurations: completeChannelConfiguration
}
} };
};
const handleKeepAliveMessage$11 = (input) => {
if (input.bytes.length !== 3) throw new Error(`Keep alive message (0x08) requires 3 bytes, but received ${input.bytes.length} bytes`);
const configurationId = input.bytes[1];
const statusByte = input.bytes[2];
if (statusByte === 127) throw new Error("Keep Alive message 08: The device reports an error during the calculation of the battery capacity.");
return { data: {
messageType: 8,
configurationId,
deviceStatistic: {
batteryLevelNewEvent: (statusByte & 128) >> 7 === 1,
batteryLevelPercent: statusByte & 127
}
} };
};
function createTULIP2A2GCodec() {
return defineTULIP2Codec({
deviceName: A2G_NAME,
roundingDecimals: DEFAULT_ROUNDING_DECIMALS,
channels: createTULIP2A2GChannels(),
handlers: {
1: handleDataMessage$12,
4: handleTechnicalAlarmMessage$11,
5: handleDeviceAlarmMessage$11,
7: handleDeviceIdentificationMessage$11,
8: handleKeepAliveMessage$11
}
});
}
//#endregion
//#region ../parsers/src/devices/A2G/parser/index.ts
const A2G_NAME = "A2G";
function useParser() {
return defineParser({
parserName: A2G_NAME,
codecs: [createTULIP2A2GCodec()]
});
}
//#endregion
//#region ../parsers/src/schemas/tulip2/downlink.ts
function createConfigurationIdSchema(maxConfigId) {
return v.optional(v.pipe(v.number("configurationId needs to be a number"), v.minValue(1, "configurationId needs to be at least 1"), v.maxValue(maxConfigId, `configurationId needs to be at most ${maxConfigId}`), v.integer("configurationId needs to be an integer")));
}
function createByteLimitSchema() {
return v.optional(v.pipe(v.number(), v.minValue(0), v.integer()));
}
function createGenericTULIP2DownlinkActionSchema(i) {
const includeConfigurationId = i.meta?.configurationId ?? true;
const includeByteLimit = i.meta?.byteLimit ?? true;
const extension = i.extension ?? {};
if (includeConfigurationId && includeByteLimit) return v.object({
deviceAction: v.literal(i.action),
configurationId: createConfigurationIdSchema(i.maxConfigId),
byteLimit: createByteLimitSchema(),
...extension
});
if (includeConfigurationId) return v.object({
deviceAction: v.literal(i.action),
configurationId: createConfigurationIdSchema(i.maxConfigId),
...extension
});
if (includeByteLimit) return v.object({
deviceAction: v.literal(i.action),
byteLimit: createByteLimitSchema(),
...extension
});
return v.object({
deviceAction: v.literal(i.action),
...extension
});
}
function createTULIP2DownlinkActionSchemaFactory(maxConfigId) {
return (i) => {
return createGenericTULIP2DownlinkActionSchema({
action: i.action,
maxConfigId,
extension: i.extension,
meta: i.meta
});
};
}
function createTulip2DownlinkMainConfigurationSchema(featureFlags) {
const baseSchema = {
publicationFactorWhenAlarm: v.pipe(v.number(), v.minValue(1), v.maxValue(2880), v.integer()),
publicationFactorWhenNoAlarm: v.pipe(v.number(), v.minValue(1), v.maxValue(2880), v.integer())
};
if (featureFlags.mainConfigBLE) Object.assign(baseSchema, { isBLEEnabled: v.boolean() });
if (featureFlags.mainConfigSingleMeasuringRate) Object.assign(baseSchema, { measuringRate: v.pipe(v.number(), v.minValue(60), v.maxValue(86400), v.integer()) });
else Object.assign(baseSchema, {
measuringRateWhenNoAlarm: v.pipe(v.number(), v.minValue(60), v.maxValue(86400), v.integer()),
measuringRateWhenAlarm: v.pipe(v.number(), v.minValue(60), v.maxValue(86400), v.integer())
});
return v.object(baseSchema);
}
function createTulip2DownlinkChannelSchema(channel, featureFlags, spanLimitFactors) {
const span = channel.end - channel.start;
const deadBandMaxSpanFactor = spanLimitFactors?.deadBandMaxSpanFactor ?? 1;
const slopeMaxSpanFactor = spanLimitFactors?.slopeMaxSpanFactor ?? 1;
const measureOffsetMinSpanFactor = spanLimitFactors?.measureOffsetMinSpanFactor ?? 1;
const measureOffsetMaxSpanFactor = spanLimitFactors?.measureOffsetMaxSpanFactor ?? 1;
const baseSchema = { alarms: v.optional(v.object({
deadBand: v.pipe(v.number(), v.minValue(0), v.maxValue(span * deadBandMaxSpanFactor), v.transform((v) => Math.round(v / span * 100 * 100))),
lowThreshold: v.optional(v.pipe(v.number(), v.minValue(channel.start), v.maxValue(channel.end), v.transform((v) => Math.round((v - channel.start) / span * 100 * 100 + 2500)))),
highThreshold: v.optional(v.pipe(v.number(), v.minValue(channel.start), v.maxValue(channel.end), v.transform((v) => Math.round((v - channel.start) / span * 100 * 100 + 2500)))),
lowThresholdWithDelay: v.optional(v.object({
value: v.pipe(v.number(), v.minValue(channel.start), v.maxValue(channel.end), v.transform((v) => Math.round((v - channel.start) / span * 100 * 100 + 2500))),
delay: v.pipe(v.number(), v.minValue(0), v.maxValue(65535), v.integer())
})),
highThresholdWithDelay: v.optional(v.object({
value: v.pipe(v.number(), v.minValue(channel.start), v.maxValue(channel.end), v.transform((v) => Math.round((v - channel.start) / span * 100 * 100 + 2500))),
delay: v.pipe(v.number(), v.minValue(0), v.maxValue(65535), v.integer())
})),
risingSlope: v.optional(v.pipe(v.number(), v.minValue(0), v.maxValue(span * slopeMaxSpanFactor), v.transform((v) => Math.round(v / span * 100 * 100)))),
fallingSlope: v.optional(v.pipe(v.number(), v.minValue(0), v.maxValue(span * slopeMaxSpanFactor), v.transform((v) => Math.round(v / span * 100 * 100))))
})) };
if (featureFlags.channelsStartupTime) Object.assign(baseSchema, { startUpTime: v.optional(v.pipe(v.number(), v.minValue(.1), v.maxValue(15), v.transform((v) => Math.round(v * 10)))) });
if (featureFlags.channelsMeasureOffset) Object.assign(baseSchema, { measureOffset: v.optional(v.pipe(v.number(), v.minValue(-measureOffsetMinSpanFactor * span), v.maxValue(measureOffsetMaxSpanFactor * span), v.transform((v) => Math.round(v / span * 100 * 100)))) });
return v.object(baseSchema);
}
function createTULIP2DownlinkConfigurationActionSchema(channels, featureFlags, spanLimitFactors) {
const createActionSchema = createTULIP2DownlinkActionSchemaFactory(featureFlags.maxConfigId);
const channelsBooleanOnly = new Set(featureFlags.channelsBooleanOnly ?? []);
const channelSchemas = channels.reduce((acc, channel) => {
const channelKey = `channel${channel.channelId}`;
if (channelsBooleanOnly.has(channelKey)) {
acc[channelKey] = v.optional(v.union([v.literal(false), v.literal(true)]));
return acc;
}
acc[channelKey] = v.optional(v.union([
v.literal(false),
v.literal(true),
createTulip2DownlinkChannelSchema(channel, featureFlags, spanLimitFactors)
]));
return acc;
}, {});
return createActionSchema({
action: "configuration",
extension: {
mainConfiguration: v.optional(createTulip2DownlinkMainConfigurationSchema(featureFlags)),
...channelSchemas
}
});
}
function createDownlinkResetToFactorySchema() {
return v.object({ deviceAction: v.literal("resetToFactory") });
}
function createDownlinkResetBatteryIndicatorSchema(featureFlags) {
return createTULIP2DownlinkActionSchemaFactory(featureFlags.maxConfigId)({
action: "resetBatteryIndicator",
meta: { byteLimit: false }
});
}
function createTULIP2DownlinkSchema(channels, featureFlags, extraActions, spanLimitFactors) {
return v.variant("deviceAction", [
createDownlinkResetToFactorySchema(),
createTULIP2DownlinkConfigurationActionSchema(channels, featureFlags, spanLimitFactors),
...extraActions ?? []
]);
}
/**
* Validate TULIP2 downlink input for a given set of channels and feature flags.
* Has to be used as dynamic valibot schemas cannot be correctly inferred by TypeScript.
* @param input Input to validate
* @param channels TULIP2 channels
* @param featureFlags Feature flags for the downlink
* @returns Validated downlink input
* @throws Error if the input is invalid. Should be caught and handled by the caller (parser that uses codec that uses this).
*/
function validateTULIP2DownlinkInput(input, channels, featureFlags, extraActions, spanLimitFactors) {
const schema = createTULIP2DownlinkSchema(channels, featureFlags, extraActions, spanLimitFactors);
const res = v.safeParse(schema, input);
if (!res.success) throw new Error(v.summarize(res.issues));
return res.output;
}
//#endregion
//#region ../parsers/src/devices/FLRU_NETRIS3/parser/tulip2/channels.ts
const FLRUTULIP2_LEVEL_CHANNEL = {
name: "level",
channelId: 0,
defaultRange: {
start: 0,
end: 1e3
}
};
function createTULIP2FLRUChannels() {
return [{
channelId: FLRUTULIP2_LEVEL_CHANNEL.channelId,
name: FLRUTULIP2_LEVEL_CHANNEL.name,
start: FLRUTULIP2_LEVEL_CHANNEL.defaultRange.start,
end: FLRUTULIP2_LEVEL_CHANNEL.defaultRange.end
}];
}
//#endregion
//#region ../parsers/src/devices/FLRU_NETRIS3/parser/tulip2/constants.ts
const FLRU_DOWNLINK_FEATURE_FLAGS = {
maxConfigId: 31,
channelsStartupTime: false,
channelsMeasureOffset: true,
mainConfigBLE: false,
mainConfigSingleMeasuringRate: false
};
const FLRU_DOWNLINK_SPAN_LIMIT_FACTORS = {
deadBandMaxSpanFactor: 1,
slopeMaxSpanFactor: 1,
measureOffsetMinSpanFactor: 1,
measureOffsetMaxSpanFactor: 1
};
const FLRU_COMMANDS = {
RESET_FACTORY: 1,
SET_MAIN_CONFIG: 2,
DISABLE_CHANNEL: 17,
SET_PROCESS_ALARM: 32,
SET_CHANNEL_PROPERTY: 48
};
const FLRU_DEFAULT_CONFIGURATION_ID = 1;
const FLRU_DEFAULT_BYTE_LIMIT = 51;
//#endregion
//#region ../parsers/src/formatters/index.ts
function getChannelKeys(input) {
return Object.keys(input).filter((key) => /^channel\d+$/.test(key));
}
function formatMainConfigurationInput(input) {
return input.mainConfiguration;
}
function formatDisableChannelInput(input) {
const config = {};
let hasConfig = false;
const channelKeys = getChannelKeys(input);
for (const key of channelKeys) if (input[key] === false) {
config[key] = true;
hasConfig = true;
}
return hasConfig ? config : void 0;
}
function formatProcessAlarmInput(input) {
const config = {};
let hasConfig = false;
const channelKeys = getChannelKeys(input);
for (const key of channelKeys) {
const channelValue = input[key];
if (!channelValue) continue;
if (channelValue === true) {
config[key] = true;
hasConfig = true;
} else if (typeof channelValue === "object" && "alarms" in channelValue && typeof channelValue.alarms === "object") {
config[key] = channelValue.alarms;
hasConfig = true;
}
}
return hasConfig ? config : void 0;
}
function formatMeasureOffsetInput(input, featureFlags) {
if (featureFlags?.channelsMeasureOffset === false) return;
const config = {};
let hasConfig = false;
const channelKeys = getChannelKeys(input);
for (const key of channelKeys) {
const channelValue = input[key];
if (channelValue && typeof channelValue === "object" && "measureOffset" in channelValue && channelValue.measureOffset !== void 0) {
config[key] = { offset: channelValue.measureOffset };
hasConfig = true;
}
}
return hasConfig ? config : void 0;
}
function formatStartupTimeInput(input, featureFlags) {
if (featureFlags?.channelsStartupTime === false) return;
const config = {};
let hasConfig = false;
const channelKeys = getChannelKeys(input);
for (const key of channelKeys) {
const channelValue = input[key];
if (channelValue && typeof channelValue === "object" && "startUpTime" in channelValue && channelValue.startUpTime !== void 0) {
config[key] = { startUpTime: channelValue.startUpTime };
hasConfig = true;
}
}
return hasConfig ? config : void 0;
}
//#endregion
//#region ../parsers/src/utils/encoding/ff