UNPKG

@w2a-iiot/parsers

Version:
1,425 lines (1,418 loc) 48.5 kB
import * as v2 from 'valibot'; // ../parsers/src/NETRIS2/parser.ts var DEFAULT_ROUNDING_DECIMALS = 4; function numberToIntArray(value, byteLength) { const hexString = value.toString(16).padStart(byteLength * 2, "0"); const arr = Array.from({ length: byteLength }); let i = 0; let j = 0; while (i < hexString.length) { arr[j] = Number.parseInt(hexString.slice(i, i + 2), 16); i += 2; j++; } return arr; } function hexStringToIntArray(hexString) { let adjustedString = hexString.replaceAll(" ", ""); adjustedString = adjustedString.startsWith("0x") ? adjustedString.slice(2) : adjustedString; const schema = v2.pipe(v2.string(), v2.hexadecimal()); const result = v2.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; } function useBaseParser(config) { const DEVICE_NAME = config.deviceName; const roundingDecimalsSchema = v2.pipe(v2.number(), v2.integer(), v2.minValue(0)); let ROUNDING_DECIMALS = v2.parse(v2.fallback(roundingDecimalsSchema, DEFAULT_ROUNDING_DECIMALS), config.roundingDecimals); function adjustRoundingDecimals(newDecimals) { ROUNDING_DECIMALS = v2.parse(v2.fallback(roundingDecimalsSchema, ROUNDING_DECIMALS), newDecimals); } function roundValue(value) { const factor = 10 ** ROUNDING_DECIMALS; return Math.round(value * factor) / factor; } function createErrorMessage(newErrors) { return { errors: newErrors.map((error) => `${DEVICE_NAME} (JS): ${error}`) }; } function addWarningMessages(existingWarnings, newWarnings) { newWarnings.forEach((warning) => { existingWarnings.push(`${DEVICE_NAME} (JS): ${warning}`); }); } function validateUplinkInput(input) { const inputSchema = v2.object({ bytes: v2.array(v2.pipe(v2.number(), v2.integer(), v2.minValue(0), v2.maxValue(255))), fPort: v2.pipe(v2.number(), v2.integer(), v2.minValue(1), v2.maxValue(255)), recvTime: v2.optional(v2.string()) }); const res = v2.safeParse(inputSchema, input); if (!res.success) { return createErrorMessage(res.issues.map((issue) => issue.message)); } return res.output; } function checkSemVerVersions(semVers) { const warnings = []; for (const semVer of semVers) { const parts = semVer.split("."); if (parts.length !== 3) { warnings.push(`Invalid semantic version format: ${semVer}`); return warnings; } const [majorStr, minorStr, patchStr] = parts; const major = Number.parseInt(majorStr); const minor = Number.parseInt(minorStr); const patch = Number.parseInt(patchStr); if (major.toString() !== majorStr || minor.toString() !== minorStr || patch.toString() !== patchStr) { warnings.push(`Semantic version contains non-integer value: ${semVer}`); return warnings; } if (major < 0 || major > 16) { warnings.push(`Major version ${major} is out of range for semver ${semVer}`); } if (minor < 0 || minor > 16) { warnings.push(`Minor version ${minor} is out of range for semver ${semVer}`); } if (patch < 0 || patch > 255) { warnings.push(`Patch version ${patch} is out of range for semver ${semVer}`); } } return warnings.length > 0 ? warnings : null; } const channels = config.channels.map((channel) => ({ name: channel.name, start: channel.start, end: channel.end, span: channel.end - channel.start })); function getChannel(channelId) { const channel = channels[channelId]; if (!channel) { return `Channel ${channel} is not defined in the configuration`; } return channel; } function adjustMeasurementRange(channelId, newBounds) { const channel = getChannel(channelId); if (typeof channel === "string") return channel; channel.start = newBounds.start; channel.end = newBounds.end; channel.span = newBounds.end - newBounds.start; } function getRealMeasurementValue(channelId, value) { const channel = getChannel(channelId); if (typeof channel === "string") return channel; const realVal = (value - 2500) / 1e4 * channel.span + channel.start; const roundedVal = roundValue(realVal); return roundedVal; } function getRealSlopeValue(channelId, value) { const channel = getChannel(channelId); if (typeof channel === "string") return channel; const realVal = value / 1e4 * channel.span; const roundedVal = roundValue(realVal); return roundedVal; } function createChannelData(channelId, value) { const v3 = getRealMeasurementValue(channelId, value); if (typeof v3 === "string") return v3; const channelName = getChannel(channelId).name; return { channelId, value: v3, channelName }; } return { validateUplinkInput, adjustRoundingDecimals, createErrorMessage, addWarningMessages, createChannelData, checkSemVerVersions, getRealSlopeValue, getRealMeasurementValue, adjustMeasurementRange }; } // ../parsers/src/NETRIS2/parser.ts var ALARM_EVENT_NAMES_DICTIONARY = ["triggered", "disappeared"]; var PROCESS_ALARM_TYPE_NAMES_DICTIONARY = [ "low threshold", "high threshold", "falling slope", "rising slope", "low threshold with delay", "high threshold with delay" ]; var TECHNICAL_ALARM_CAUSE_OF_FAILURE_NAMES_DICTIONARY = [ "no alarm", "open condition", "short condition", "saturated low", "saturated high", "ADC communication error" ]; var CONFIGURATION_STATUS_NAMES_DICTIONARY = { 32: "configuration successful", 48: "configuration rejected", 96: "command successful", 112: "command failed" }; function useParser() { const { validateUplinkInput, createErrorMessage, addWarningMessages, createChannelData, checkSemVerVersions, getRealSlopeValue, getRealMeasurementValue, adjustRoundingDecimals } = useBaseParser({ deviceName: "NETRIS2", channels: [ { start: 4, end: 20, name: "Electrical current" }, { start: 4, end: 20, name: "Electrical current" } ] }); function decodeUplink(input) { return decode(input); } function decodeHexUplink(input) { const inputSchema = v2.object({ bytes: v2.unknown() }); const inputParseResult = v2.safeParse(inputSchema, input); if (!inputParseResult.success) { return createErrorMessage(["Input must be an object with a `bytes` property"]); } const bytesSchema = v2.union([ v2.string("Input `bytes` must be a hex string") ]); const parseResult = v2.safeParse(bytesSchema, inputParseResult.output.bytes); if (!parseResult.success) { return createErrorMessage(["`bytes` must either be a hex string or an array of hex strings"]); } const intArray = hexStringToIntArray(parseResult.output); if (!intArray) { return createErrorMessage([ `Invalid hex string: ${inputParseResult.output.bytes}` ]); } return decodeUplink({ ...input, bytes: intArray }); } function encodeDownlink(input) { return encode(input); } function decode(input) { const validationResult = validateUplinkInput(input); if ("errors" in validationResult) { return validationResult; } input = validationResult; const firstByte = input.bytes[0]; switch (firstByte) { // Data message with no alarm ongoing case 1: return decodeDataMessage(input); // Data message with at least one alarm ongoing case 2: return decodeDataMessage(input); // Process alarm message case 3: return decodeProcessAlarmMessage(input); // Technical alarm message case 4: return decodeTechnicalAlarmMessage(input); // Configuration status message case 6: return decodeConfigurationStatusMessage(input); // Radio unit identification message case 7: return decodeRadioUnitIdentificationMessage(input); // Keep alive message case 8: return decodeKeepAliveMessage(input); // Unsupported message type default: return createErrorMessage([ `Data message type ${firstByte} is not supported` ]); } } function decodeDataMessage(input) { const minLengthForData = 5; const maxLengthForData = 7; const warnings = []; const [haschannel0, haschannel1, overflows] = [ input.bytes.length >= minLengthForData, input.bytes.length >= maxLengthForData, input.bytes.length > maxLengthForData ]; if (!haschannel0) { return createErrorMessage([ `Data message${input.bytes[0] ? ` ${input.bytes[0]}` : ""} must contain at least ${minLengthForData} bytes. Contains ${input.bytes.length} bytes.` ]); } if (overflows) { addWarningMessages(warnings, [ `Data message contains more than ${maxLengthForData} bytes. Data might been decoded incorrectly. Contains ${input.bytes.length} bytes.` ]); } const messageType = input.bytes[0]; const configurationId = input.bytes[1]; const channelMask = input.bytes[2]; const isChannelMaskValid = [0, 1, 2, 3].includes( channelMask ); if (!isChannelMaskValid) { return createErrorMessage([ `Data message contains an invalid channel mask: ${channelMask}, expected 0x00, 0x01, 0x02 or 0x03` ]); } const [channel0Valid, channel1Valid] = [ channelMask & 1, channelMask & 2 ]; const channelData = []; if (haschannel0 && channel0Valid) { const measurementValue = input.bytes[3] << 8 | input.bytes[4]; const data = createChannelData(0, measurementValue); if (typeof data === "string") { return createErrorMessage([data]); } channelData.push(data); } if (haschannel1 && channel1Valid) { const measurementValue = input.bytes[5] << 8 | input.bytes[6]; const data = createChannelData(1, measurementValue); if (typeof data === "string") { return createErrorMessage([data]); } channelData.push(data); } if (channelData.length !== 1 && channelData.length !== 2) { return createErrorMessage([ `Invalid number of channels: ${channelData.length}` ]); } const res = { data: { messageType, configurationId, measurements: { channels: channelData } } }; if (warnings.length > 0) { res.warnings = warnings; } return res; } function decodeProcessAlarmMessage(input) { const minLengthBytes = 6; if (input.bytes.length < minLengthBytes) { return createErrorMessage([ `Process alarm message must contain at least ${minLengthBytes} bytes. Contains ${input.bytes.length} bytes.` ]); } if (input.bytes.length % 3 !== 0) { return createErrorMessage([ `Process alarm message must contain a multiple of 3 bytes.` ]); } const messageType = input.bytes[0]; const configurationId = input.bytes[1]; const processAlarms = []; for (let i = 3; i < input.bytes.length; i += 3) { const alarmTypeByte = input.bytes[i]; const processAlarmType = getProcessAlarmType(alarmTypeByte); if ("errors" in processAlarmType) { return processAlarmType; } const processAlarmRelatedValue = getProcessAlarmRelatedValue( input.bytes[i + 1], input.bytes[i + 2], processAlarmType.channelId, processAlarmType.alarmType ); if (typeof processAlarmRelatedValue === "string") { return createErrorMessage([processAlarmRelatedValue]); } processAlarms.push({ channelId: processAlarmType.channelId, channelName: `Electrical current`, alarmType: processAlarmType.alarmType, alarmTypeName: PROCESS_ALARM_TYPE_NAMES_DICTIONARY[processAlarmType.alarmType], event: processAlarmType.sense, eventName: ALARM_EVENT_NAMES_DICTIONARY[processAlarmType.sense], value: processAlarmRelatedValue }); } return { data: { messageType, configurationId, processAlarms } }; } function decodeTechnicalAlarmMessage(input) { const minLengthBytes = 4; const maxLengthBytes = 5; const warnings = []; if (input.bytes.length < minLengthBytes) { return createErrorMessage([ `Technical alarm message must contain at least ${minLengthBytes} bytes` ]); } if (input.bytes.length > maxLengthBytes) { addWarningMessages(warnings, [ `Technical alarm message contains more than ${maxLengthBytes} bytes. Data might been decoded incorrectly. Contains ${input.bytes.length} bytes.` ]); } const messageType = input.bytes[0]; const configurationId = input.bytes[1]; const bitMask = input.bytes[2]; if (![1, 2, 3].includes(bitMask)) { return createErrorMessage([ `Technical alarm message contains an invalid bit mask: ${bitMask}, expected 0x01, 0x02 or 0x03` ]); } const requiredBitMaskLength = bitMask === 3 ? 5 : 4; if (input.bytes.length < requiredBitMaskLength) { return createErrorMessage([ `Technical alarm message with bit mask ${bitMask} must contain atleast ${requiredBitMaskLength} bytes. Contains ${input.bytes.length} bytes.` ]); } if (input.bytes.length > requiredBitMaskLength) { addWarningMessages(warnings, [ `Technical alarm message with bit mask ${bitMask} contains more than ${requiredBitMaskLength} bytes. Data might been decoded incorrectly. Contains ${input.bytes.length} bytes.` ]); } const technicalAlarms = []; switch (bitMask) { case 1: { const technicalAlarm = getTechnicalAlarmCauseOfFailure( input.bytes[3] ); if ("errors" in technicalAlarm) { return technicalAlarm; } technicalAlarms.push({ channelId: 0, channelName: `Electrical current`, event: technicalAlarm.sense, eventName: ALARM_EVENT_NAMES_DICTIONARY[technicalAlarm.sense], causeOfFailure: technicalAlarm.causeOfFailure, causeOfFailureName: TECHNICAL_ALARM_CAUSE_OF_FAILURE_NAMES_DICTIONARY[technicalAlarm.causeOfFailure] }); break; } case 2: { const technicalAlarm = getTechnicalAlarmCauseOfFailure( input.bytes[3] ); if ("errors" in technicalAlarm) { return technicalAlarm; } technicalAlarms.push({ channelId: 1, channelName: `Electrical current`, event: technicalAlarm.sense, eventName: ALARM_EVENT_NAMES_DICTIONARY[technicalAlarm.sense], causeOfFailure: technicalAlarm.causeOfFailure, causeOfFailureName: TECHNICAL_ALARM_CAUSE_OF_FAILURE_NAMES_DICTIONARY[technicalAlarm.causeOfFailure] }); break; } case 3: { const technicalAlarm1 = getTechnicalAlarmCauseOfFailure( input.bytes[3] ); if ("errors" in technicalAlarm1) { return technicalAlarm1; } const technicalAlarm2 = getTechnicalAlarmCauseOfFailure( input.bytes[4] ); if ("errors" in technicalAlarm2) { return technicalAlarm2; } technicalAlarms.push( { channelId: 0, channelName: `Electrical current`, event: technicalAlarm1.sense, eventName: ALARM_EVENT_NAMES_DICTIONARY[technicalAlarm1.sense], causeOfFailure: technicalAlarm1.causeOfFailure, causeOfFailureName: TECHNICAL_ALARM_CAUSE_OF_FAILURE_NAMES_DICTIONARY[technicalAlarm1.causeOfFailure] }, { channelId: 1, channelName: `Electrical current`, event: technicalAlarm2.sense, eventName: ALARM_EVENT_NAMES_DICTIONARY[technicalAlarm2.sense], causeOfFailure: technicalAlarm2.causeOfFailure, causeOfFailureName: TECHNICAL_ALARM_CAUSE_OF_FAILURE_NAMES_DICTIONARY[technicalAlarm2.causeOfFailure] } ); break; } } const res = { data: { messageType, configurationId, technicalAlarms } }; if (warnings.length > 0) { res.warnings = warnings; } return res; } function decodeConfigurationStatusMessage(input) { const minLength = 3; const validStatuses = [32, 48, 96, 112]; const warnings = []; if (input.bytes.length < minLength) { return createErrorMessage([ `Configuration status message must contain at least ${minLength} bytes. Contains ${input.bytes.length} bytes.` ]); } if (input.bytes.length > minLength) { addWarningMessages(warnings, [ `Configuration status message contains more than ${minLength} bytes. Data might been decoded incorrectly. Contains ${input.bytes.length} bytes.` ]); } const messageType = input.bytes[0]; const configurationId = input.bytes[1]; const status = input.bytes[2]; if (!validStatuses.includes(status)) { return createErrorMessage([ `Configuration status message contains an invalid status: ${status}` ]); } const res = { data: { messageType, configurationStatus: { configurationId, statusId: status, status: CONFIGURATION_STATUS_NAMES_DICTIONARY[status] } } }; if (warnings.length > 0) { res.warnings = warnings; } return res; } function decodeRadioUnitIdentificationMessage(input) { const minLength = 24; const warnings = []; if (input.bytes.length < minLength) { return createErrorMessage([ `Radio unit identification message must contain at least ${minLength} bytes. Contains ${input.bytes.length} bytes.` ]); } if (input.bytes.length > minLength) { addWarningMessages(warnings, [ `Radio unit identification message contains more than ${minLength} bytes. Data might been decoded incorrectly. Contains ${input.bytes.length} bytes.` ]); } const messageType = input.bytes[0]; const configurationId = input.bytes[1]; const productId = input.bytes[2]; if (productId !== 14) { return createErrorMessage([ `Radio unit identification message contains an invalid product ID: ${productId}, expected 0x0e (14).` ]); } const productSubId = input.bytes[3]; if (productSubId !== 0) { return createErrorMessage([ `Radio unit identification message contains an invalid product sub ID: ${productSubId}, expected 0x00 (0).` ]); } const radioUnitModemFirmwareVersion = `${input.bytes[4] >> 4}.${input.bytes[4] & 15}.${input.bytes[5]}`; const radioUnitModemHardwareVersion = `${input.bytes[6] >> 4}.${input.bytes[6] & 15}.${input.bytes[7]}`; const radioUnitFirmwareVersion = `${input.bytes[8] >> 4}.${input.bytes[8] & 15}.${input.bytes[9]}`; const radioUnitHardwareVersion = `${input.bytes[10] >> 4}.${input.bytes[10] & 15}.${input.bytes[11]}`; const checkSemVerVersionsErrors = checkSemVerVersions([ radioUnitModemFirmwareVersion, radioUnitModemHardwareVersion, radioUnitFirmwareVersion, radioUnitHardwareVersion ]); if (checkSemVerVersionsErrors) { addWarningMessages(warnings, checkSemVerVersionsErrors); } const serialNumberASCII = input.bytes.slice(12, 23).map((byte) => String.fromCharCode(byte)).join(""); const res = { data: { messageType, configurationId, radioUnitIdentification: { productId, productSubId, radioUnitModemFirmwareVersion, radioUnitModemHardwareVersion, radioUnitFirmwareVersion, radioUnitHardwareVersion, serialNumber: serialNumberASCII } } }; if (warnings.length > 0) { res.warnings = warnings; } return res; } function decodeKeepAliveMessage(input) { const minLength = 12; const warnings = []; if (input.bytes.length !== minLength) { return createErrorMessage([ `Keep alive message must contain at least ${minLength} bytes. Contains ${input.bytes.length} bytes.` ]); } if (input.bytes.length > minLength) { addWarningMessages(warnings, [ `Keep alive message contains more than ${minLength} bytes. Data might been decoded incorrectly. Contains ${input.bytes.length} bytes.` ]); } const messageType = input.bytes[0]; const configurationId = input.bytes[1]; const numberOfMeasurements = input.bytes[2] << 24 | input.bytes[3] << 16 | input.bytes[4] << 8 | input.bytes[5]; const numberOfTransmissions = input.bytes[6] << 24 | input.bytes[7] << 16 | input.bytes[8] << 8 | input.bytes[9]; const batteryResetSinceLastKeepAlive = !!(input.bytes[10] & 128); const estimatedBatteryPercent = input.bytes[10] & 127; const batteryCalculationError = estimatedBatteryPercent === 127; const radioUnitTemperatureLevel_C = input.bytes[11]; const res = { data: { messageType, configurationId, deviceStatistic: { numberOfMeasurements, numberOfTransmissions, batteryResetSinceLastKeepAlive, estimatedBatteryPercent, batteryCalculationError, radioUnitTemperatureLevel_C } } }; if (warnings.length > 0) { res.warnings = warnings; } return res; } function getTechnicalAlarmCauseOfFailure(byte) { const causeOfFailure = { // sense is bit 7 sense: (byte & 128) >> 7, // causeOfFailure is bit 6 to 4 causeOfFailure: byte & 7 }; if (![0, 1, 2, 3, 4, 5].includes(causeOfFailure.causeOfFailure)) { return createErrorMessage([ `Invalid causeOfFailure in technical alarm: ${causeOfFailure.causeOfFailure}` ]); } return causeOfFailure; } function getProcessAlarmType(byte) { const alarmType = { // sense is bit 7 sense: (byte & 128) >> 7, // channelId is bit 6 to 3 as 0 (0b0000) or 1 (0b0001) channelId: (byte & 120) >> 3, // alarmType is bit 2 to 0 alarmType: byte & 7 }; if (alarmType.channelId !== 0 && alarmType.channelId !== 1) { return createErrorMessage([ `Invalid channelId in process alarm: ${alarmType.channelId}` ]); } if (![0, 1, 2, 3, 4, 5].includes(alarmType.alarmType)) { return createErrorMessage([ `Invalid alarmType in process alarm: ${alarmType.alarmType}` ]); } return alarmType; } function getProcessAlarmRelatedValue(byte1, byte2, channelId, alarmType) { const value = byte1 << 8 | byte2; switch (alarmType) { case 2: case 3: return getRealSlopeValue(channelId, value); case 0: case 1: case 4: case 5: return getRealMeasurementValue(channelId, value); } } const DEFAULT_DOWNLINK_FPORT = 10; const DEFAULT_CONFIGURATION_ID = 1; function encode(input) { const parsedInput = validateDownlinkInput(input); if ("errors" in parsedInput) { return parsedInput; } switch (parsedInput.deviceAction) { case "resetToFactory": return encodeResetToFactory(); case "resetBatteryIndicator": return encodeResetBatteryIndicator(parsedInput); case "disableChannel": return encodeDisableChannel(parsedInput); case "setMainConfiguration": return encodeSetMainConfiguration(parsedInput); case "setProcessAlarmConfiguration": return encodeSetProcessAlarmConfiguration(parsedInput); case "setMeasureOffsetConfiguration": return encodeSetMeasureOffsetConfiguration(parsedInput); case "setStartUpTimeConfiguration": return encodeSetStartUpTimeConfiguration(parsedInput); default: return createErrorMessage([ `Downlink type ${parsedInput.type} is not supported` ]); } } const configurationIdSchema = v2.optional( v2.pipe( v2.number("configurationId needs to be a number"), v2.integer("configurationId needs to be a number"), v2.minValue(1, "configurationId needs to be at least 1"), v2.maxValue(31, "configurationId needs to be at most 31") ), DEFAULT_CONFIGURATION_ID ); const resetToFactorySchema = v2.object({ deviceAction: v2.literal("resetToFactory") }); const resetBatteryIndicatorSchema = v2.object({ configurationId: configurationIdSchema, deviceAction: v2.literal("resetBatteryIndicator") }); const disableChannelSchema = v2.object({ configurationId: configurationIdSchema, deviceAction: v2.literal("disableChannel"), configuration: v2.object({ channel0: v2.optional( v2.object({ disable: v2.literal(true) }) ), channel1: v2.optional(v2.object({ disable: v2.literal(true) })) }) }); const setMainConfigurationSchema = v2.object({ configurationId: configurationIdSchema, deviceAction: v2.literal("setMainConfiguration"), configuration: v2.object({ measuringRateWhenNoAlarm: v2.pipe( v2.number("measuringRateWhenNoAlarm needs to be a number"), v2.integer("measuringRateWhenNoAlarm needs to be an integer"), v2.minValue( 60, "measuringRateWhenNoAlarm needs to be at least 60" ), v2.maxValue( 86400, "measuringRateWhenNoAlarm needs to be at most 86,400" ) ), publicationFactorWhenNoAlarm: v2.pipe( v2.number("publicationFactorWhenNoAlarm needs to be a number"), v2.integer( "publicationFactorWhenNoAlarm needs to be an integer" ), v2.minValue( 1, "publicationFactorWhenNoAlarm needs to be at least 1" ), v2.maxValue( 2880, "publicationFactorWhenNoAlarm needs to be at most 2,880" ) ), measuringRateWhenAlarm: v2.pipe( v2.number("measuringRateWhenAlarm needs to be a number"), v2.integer("measuringRateWhenAlarm needs to be an integer"), v2.minValue( 60, "measuringRateWhenAlarm needs to be at least 60" ), v2.maxValue( 86400, "measuringRateWhenAlarm needs to be at most 86,400" ) ), publicationFactorWhenAlarm: v2.pipe( v2.number("publicationFactorWhenAlarm needs to be a number"), v2.integer("publicationFactorWhenAlarm needs to be an integer"), v2.minValue( 1, "publicationFactorWhenAlarm needs to be at least 1" ), v2.maxValue( 2880, "publicationFactorWhenAlarm needs to be at most 2,880" ) ) }) }); const channelConfigSchema = v2.object({ deadBand: v2.pipe( v2.number("deadBand needs to be a number"), v2.minValue(0, "deadBand needs to be at least 0"), v2.maxValue(20, "deadBand needs to be at most 20") ), alarms: v2.optional(v2.object({ lowThreshold: v2.optional(v2.pipe( v2.number("lowThreshold needs to be a number"), v2.minValue(0, "lowThreshold needs to be at least 0"), v2.maxValue(100, "lowThreshold needs to be at most 100") )), highThreshold: v2.optional(v2.pipe( v2.number("highThreshold needs to be a number"), v2.minValue(0, "highThreshold needs to be at least 0"), v2.maxValue(100, "highThreshold needs to be at most 100") )), lowThresholdWithDelay: v2.optional(v2.object({ value: v2.pipe( v2.number( "value of lowThresholdWithDelay needs to be a number" ), v2.minValue( 0, "value of lowThresholdWithDelay needs to be at least 0" ), v2.maxValue( 100, "value of lowThresholdWithDelay needs to be at most 100" ) ), delay: v2.pipe( v2.number( "delay of lowThresholdWithDelay needs to be a number" ), v2.integer( "delay of lowThresholdWithDelay needs to be an integer" ), v2.minValue( 0, "delay of lowThresholdWithDelay needs to be at least 0" ), v2.maxValue( 65535, "delay of lowThresholdWithDelay needs to be at most 65,535" ) ) })), highThresholdWithDelay: v2.optional(v2.object({ value: v2.pipe( v2.number( "value of highThresholdWithDelay needs to be a number" ), v2.minValue( 0, "value of highThresholdWithDelay needs to be at least 0" ), v2.maxValue( 100, "value of highThresholdWithDelay needs to be at most 100" ) ), delay: v2.pipe( v2.number( "delay of highThresholdWithDelay needs to be a number" ), v2.integer( "delay of highThresholdWithDelay needs to be an integer" ), v2.minValue( 0, "delay of highThresholdWithDelay needs to be at least 0" ), v2.maxValue( 65535, "delay of highThresholdWithDelay needs to be at most 65,535" ) ) })), fallingSlope: v2.optional(v2.pipe( v2.number("fallingSlope needs to be a number"), v2.minValue(0, "fallingSlope needs to be at least 0"), v2.maxValue(50, "fallingSlope needs to be at most 50") )), risingSlope: v2.optional(v2.pipe( v2.number("risingSlope needs to be a number"), v2.minValue(0, "risingSlope needs to be at least 0"), v2.maxValue(50, "risingSlope needs to be at most 50") )) })) }); const setProcessAlarmConfigurationSchema = v2.object({ configurationId: configurationIdSchema, deviceAction: v2.literal("setProcessAlarmConfiguration"), configuration: v2.object({ channel0: v2.optional(channelConfigSchema), channel1: v2.optional(channelConfigSchema) }) }); const setMeasureOffsetConfigurationSchema = v2.object({ configurationId: configurationIdSchema, deviceAction: v2.literal("setMeasureOffsetConfiguration"), configuration: v2.object({ channel0: v2.optional( v2.object({ measureOffset: v2.pipe( v2.number( "measureOffset of channel0 needs to be a number" ), v2.minValue( -5, "measureOffset of channel0 needs to be at least -5" ), v2.maxValue( 5, "measureOffset of channel0 needs to be at most 5" ) ) }) ), channel1: v2.optional( v2.object({ measureOffset: v2.pipe( v2.number( "measureOffset of channel1 needs to be a number" ), v2.minValue( -5, "measureOffset of channel1 needs to be at least -5" ), v2.maxValue( 5, "measureOffset of channel1 needs to be at most 5" ) ) }) ) }) }); const startUpTimeSchema = v2.object({ configurationId: configurationIdSchema, deviceAction: v2.literal("setStartUpTimeConfiguration"), configuration: v2.object({ channel0: v2.optional( v2.object({ startUpTime: v2.pipe( v2.number( "startUpTime of channel0 needs to be a number" ), v2.minValue( 0.1, "startUpTime of channel0 needs to be at least 0.1" ), v2.maxValue( 15, "startUpTime of channel0 needs to be at most 15" ) ) }) ), channel1: v2.optional( v2.object({ startUpTime: v2.pipe( v2.number( "startUpTime of channel1 needs to be a number" ), v2.minValue( 0.1, "startUpTime of channel1 needs to be at least 0.1" ), v2.maxValue( 15, "startUpTime of channel1 needs to be at most 15" ) ) }) ) }) }); const downlinkInputSchema = v2.variant("deviceAction", [ resetToFactorySchema, resetBatteryIndicatorSchema, disableChannelSchema, setMainConfigurationSchema, setProcessAlarmConfigurationSchema, setMeasureOffsetConfigurationSchema, startUpTimeSchema ]); function validateDownlinkInput(input) { const res = v2.safeParse(downlinkInputSchema, input); if (res.success) { return res.output; } return createErrorMessage(res.issues.map((i) => { return i.message; })); } function encodeResetToFactory(_input) { return { bytes: [0, 1], fPort: DEFAULT_DOWNLINK_FPORT }; } function encodeResetBatteryIndicator(input) { return { fPort: DEFAULT_DOWNLINK_FPORT, bytes: [...numberToIntArray(input.configurationId, 1), 5] }; } function encodeDisableChannel(input) { const bitMask = (input.configuration.channel0 ? 1 : 0) | (input.configuration.channel1 ? 2 : 0); if (bitMask === 0) { return createErrorMessage([ "At least one channel must be present when disabling channels" ]); } return { fPort: DEFAULT_DOWNLINK_FPORT, bytes: [ ...numberToIntArray(input.configurationId, 1), 17, bitMask ] }; } function encodeSetMainConfiguration(input) { const { measuringRateWhenNoAlarm, publicationFactorWhenNoAlarm, measuringRateWhenAlarm, publicationFactorWhenAlarm } = input.configuration; const TRANSMISSION_PERIOD = 172800; if (measuringRateWhenNoAlarm * publicationFactorWhenNoAlarm > TRANSMISSION_PERIOD || measuringRateWhenAlarm * publicationFactorWhenAlarm > TRANSMISSION_PERIOD) { return createErrorMessage([ "Measuring rate when no alarm * publication factor must be less than or equal to 172800" ]); } const bytes = [ ...numberToIntArray(input.configurationId, 1), 2, ...numberToIntArray(measuringRateWhenNoAlarm, 4), ...numberToIntArray(publicationFactorWhenNoAlarm, 2), ...numberToIntArray(measuringRateWhenAlarm, 4), ...numberToIntArray(publicationFactorWhenAlarm, 2) ]; return { fPort: DEFAULT_DOWNLINK_FPORT, bytes }; } function encodeSetMeasureOffsetConfiguration(input) { const { channel0, channel1 } = input.configuration; const bitMask = (channel0 !== void 0 ? 1 : 0) | (channel1 !== void 0 ? 2 : 0); if (bitMask === 0) { return createErrorMessage([ "At least one channel offset must be present when configuring offsets" ]); } const CONVERSION_FACTOR = 100; function int16ToBytes(value) { const int16 = value & 65535; return [int16 >> 8 & 255, int16 & 255]; } function int16C(value) { if (value < 0) { return 65535 + value + 1; } return value; } const channel0Bytes = channel0 !== void 0 ? int16ToBytes( int16C(Math.floor(channel0.measureOffset * CONVERSION_FACTOR)) ) : []; const channel1Bytes = channel1 !== void 0 ? int16ToBytes( int16C(Math.floor(channel1.measureOffset * CONVERSION_FACTOR)) ) : []; return { fPort: DEFAULT_DOWNLINK_FPORT, bytes: [ ...numberToIntArray(input.configurationId, 1), 48, bitMask, ...channel0Bytes, ...channel1Bytes ] }; } function encodeSetStartUpTimeConfiguration(input) { const { channel0, channel1 } = input.configuration; const bitMask = (channel0 !== void 0 ? 1 : 0) | (channel1 !== void 0 ? 2 : 0); if (bitMask === 0) { return createErrorMessage([ "At least one channel start up time must be present when configuring start up times" ]); } const CONVERSION_FACTOR = 10; const channel0Bytes = channel0 !== void 0 ? numberToIntArray( Math.floor(channel0.startUpTime * CONVERSION_FACTOR), 2 ) : []; const channel1Bytes = channel1 !== void 0 ? numberToIntArray( Math.floor(channel1.startUpTime * CONVERSION_FACTOR), 2 ) : []; return { fPort: DEFAULT_DOWNLINK_FPORT, bytes: [ ...numberToIntArray(input.configurationId, 1), 96, bitMask, ...channel0Bytes, ...channel1Bytes ] }; } function encodeSetProcessAlarmConfiguration(input) { const { channel0, channel1 } = input.configuration; if (!channel0 && !channel1) { return createErrorMessage([ "At least one channel must be present when configuring process alarms" ]); } const channel0BitMap = 0; const channel1BitMap = 1; const channel0Bytes = channel0 ? [ 32, 0, channel0BitMap, ...encodeChannelConfiguration(channel0) ] : []; const channel1Bytes = channel1 ? [ 32, 0, channel1BitMap, ...encodeChannelConfiguration(channel1) ] : []; return { fPort: DEFAULT_DOWNLINK_FPORT, bytes: [ ...numberToIntArray(input.configurationId, 1), ...channel0Bytes, ...channel1Bytes ] }; } function encodeChannelConfiguration(channel) { const DEAD_BAND_FACTOR = 100; const deadBandBytes = numberToIntArray( Math.floor(channel.deadBand * DEAD_BAND_FACTOR), 2 ); const alarmBitMap = (channel.alarms?.lowThreshold ? 128 : 0) | (channel.alarms?.highThreshold ? 64 : 0) | (channel.alarms?.fallingSlope ? 32 : 0) | (channel.alarms?.risingSlope ? 16 : 0) | (channel.alarms?.lowThresholdWithDelay ? 8 : 0) | (channel.alarms?.highThresholdWithDelay ? 4 : 0); function thresholdValueConversion(v3) { return Math.floor(v3 * 100) + 2500; } function slopeConversion(v3) { return Math.floor(v3 * 100); } const lowThresholdBytes = channel.alarms?.lowThreshold ? numberToIntArray( thresholdValueConversion(channel.alarms.lowThreshold), 2 ) : []; const highThresholdBytes = channel.alarms?.highThreshold ? numberToIntArray( thresholdValueConversion(channel.alarms.highThreshold), 2 ) : []; const fallingSlopeBytes = channel.alarms?.fallingSlope ? numberToIntArray(slopeConversion(channel.alarms.fallingSlope), 2) : []; const risingSlopeBytes = channel.alarms?.risingSlope ? numberToIntArray(slopeConversion(channel.alarms.risingSlope), 2) : []; const lowThresholdWithDelayValueBytes = channel.alarms?.lowThresholdWithDelay ? numberToIntArray( thresholdValueConversion( channel.alarms.lowThresholdWithDelay.value ), 2 ) : []; const lowThresholdWithDelayDelayBytes = channel.alarms?.lowThresholdWithDelay ? numberToIntArray( channel.alarms.lowThresholdWithDelay.delay, 2 ) : []; const highThresholdWithDelayValueBytes = channel.alarms?.highThresholdWithDelay ? numberToIntArray( thresholdValueConversion( channel.alarms.highThresholdWithDelay.value ), 2 ) : []; const highThresholdWithDelayDelayBytes = channel.alarms?.highThresholdWithDelay ? numberToIntArray( channel.alarms.highThresholdWithDelay.delay, 2 ) : []; const bytes = [ ...deadBandBytes, alarmBitMap, ...lowThresholdBytes, ...highThresholdBytes, ...fallingSlopeBytes, ...risingSlopeBytes, ...lowThresholdWithDelayValueBytes, ...lowThresholdWithDelayDelayBytes, ...highThresholdWithDelayValueBytes, ...highThresholdWithDelayDelayBytes ]; return bytes; } return { decodeUplink, decodeHexUplink, encodeDownlink, adjustRoundingDecimals }; } // src/ffd.ts function FirstFitDecreasing(packets, limit) { limit = Math.floor(limit); if (limit <= 0) { throw new Error("Limit must be greater than 0"); } const bins = []; const sorted = packets.sort((a, b) => b.length - a.length); sorted.forEach((packet) => { const packetLength = packet.length; if (packetLength > limit) { throw new Error("Packet is too big to fit in any bin"); } let found = false; for (const bin of bins) { if (limit - bin.length >= packetLength) { bin.push(...packet); found = true; break; } } if (!found) { bins.push(packet); } }); return bins; } // src/shared.ts function concatFrames(frames, byteLimit, configurationId, maxConfigId) { const actualLimit = byteLimit - 1; const framesWithoutConfigurationId = frames.map((frame) => frame.slice(1)); const stitchedFrames = FirstFitDecreasing(framesWithoutConfigurationId, actualLimit); let currentConfigurationId = configurationId; return stitchedFrames.map((stitchedFrame) => { const frame = [currentConfigurationId, ...stitchedFrame]; currentConfigurationId++; if (currentConfigurationId > maxConfigId) { currentConfigurationId = 1; } return frame; }); } // src/wrappers/NETRIS2.ts function getNecessaryFrames(input) { const channel0StartUpTimeNecessary = typeof input.configuration.channel0 === "object" ? input.configuration.channel0.startUpTime !== void 0 : false; const channel0OffsetNecessary = typeof input.configuration.channel0 === "object" ? input.configuration.channel0.measureOffset !== void 0 : false; const channel1StartUpTimeNecessary = typeof input.configuration.channel1 === "object" ? input.configuration.channel1.startUpTime !== void 0 : false; const channel1OffsetNecessary = typeof input.configuration.channel1 === "object" ? input.configuration.channel1.measureOffset !== void 0 : false; const channel0Configuration = input.configuration.channel0 === void 0 ? "none" : input.configuration.channel0 === false ? "disabled" : input.configuration.channel0 === true ? "enabled" : "configured"; const channel1Configuration = input.configuration.channel1 === void 0 ? "none" : input.configuration.channel1 === false ? "disabled" : input.configuration.channel1 === true ? "enabled" : "configured"; const channel0Disabled = input.configuration.channel0 === false; const channel1Disabled = input.configuration.channel1 === false; const isMainConfigurationNecessary = !!input.configuration.mainConfiguration; return { channel0StartUpTimeNecessary, channel0OffsetNecessary, channel1StartUpTimeNecessary, channel1OffsetNecessary, channel1Configuration, channel0Configuration, channel0Disabled, channel1Disabled, isMainConfigurationNecessary }; } var spreadingFactorLookUp = { SF7: 222, 7: 222, SF8: 222, 8: 222, SF9: 115, 9: 115, SF10: 51, 10: 51, SF11: 51, 11: 51, SF12: 51, 12: 51 }; function NETRIS2Parser() { const { adjustRoundingDecimals, decodeUplink, decodeHexUplink, ...parser } = useParser(); function encodeMainConfiguration(input, necessaryFrames) { if (!necessaryFrames.isMainConfigurationNecessary) return null; return parser.encodeDownlink({ deviceAction: "setMainConfiguration", configuration: input.configuration.mainConfiguration }); } function encodeStartUpTimeFrame(input, necessaryFrames) { if (necessaryFrames.channel0StartUpTimeNecessary || necessaryFrames.channel1StartUpTimeNecessary) { return parser.encodeDownlink({ deviceAction: "setStartUpTimeConfiguration", configuration: { channel0: necessaryFrames.channel0StartUpTimeNecessary ? { startUpTime: input.configuration.channel0.startUpTime } : void 0, channel1: necessaryFrames.channel1StartUpTimeNecessary ? { startUpTime: input.configuration.channel1.startUpTime } : void 0 } }); } return null; } function encodeOffsetFrame(input, necessaryFrames) { if (necessaryFrames.channel0OffsetNecessary || necessaryFrames.channel1OffsetNecessary) { return parser.encodeDownlink({ deviceAction: "setMeasureOffsetConfiguration", configuration: { ...necessaryFrames.channel0OffsetNecessary ? { channel0: { measureOffset: input.configuration.channel0.measureOffset } } : {}, ...necessaryFrames.channel1OffsetNecessary ? { channel1: { measureOffset: input.configuration.channel1.measureOffset } } : {} } }); } return null; } function encodeDisableChannelFrame(necessaryFrames) { if (necessaryFrames.channel0Disabled || necessaryFrames.channel1Disabled) { return parser.encodeDownlink({ deviceAction: "disableChannel", configuration: { channel0: necessaryFrames.channel0Disabled === true ? { disable: true } : void 0, channel1: necessaryFrames.channel1Disabled === true ? { disable: true } : void 0 } }); } return null; } function encodeConfigurationFrame(input, necessaryFrames) { const isChannel0DisabledOrNone = necessaryFrames.channel0Configuration === "disabled" || necessaryFrames.channel0Configuration === "none"; const isChannel1DisabledOrNone = necessaryFrames.channel1Configuration === "disabled" || necessaryFrames.channel1Configuration === "none"; if (isChannel0DisabledOrNone && isChannel1DisabledOrNone) { return null; } return parser.encodeDownlink({ deviceAction: "setProcessAlarmConfiguration", configuration: { channel0: necessaryFrames.channel0Configuration === "disabled" || necessaryFrames.channel0Configuration === "none" ? void 0 : necessaryFrames.channel0Configuration === "enabled" ? { deadBand: 0 } : input.configuration.channel0, channel1: necessaryFrames.channel1Configuration === "disabled" || necessaryFrames.channel0Configuration === "none" ? void 0 : necessaryFrames.channel1Configuration === "enabled" ? { deadBand: 0 } : input.configuration.channel1 } }); } function encodeDownlink(input) { switch (input.deviceAction) { case "resetToFactory": case "resetBatteryIndicator": { const res = parser.encodeDownlink(input); if ("errors" in res) { return { success: false, errors: res.errors }; } return { success: true, data: { frames: [res.bytes] } }; } case "downlinkConfiguration": { const byteLimit = spreadingFactorLookUp[input.spreadingFactor ?? "SF12"]; const necessaryFrames = getNecessaryFrames(input); const startUpTimeFrame = encodeStartUpTimeFrame(input, necessaryFrames); const offSetFrame = encodeOffsetFrame(input, necessaryFrames); const disableChannelFrame = encodeDisableChannelFrame(necessaryFrames); const configurationFrame = encodeConfigurationFrame(input, necessaryFrames); const mainConfiguration = encodeMainConfiguration(input, necessaryFrames); const frames = [mainConfiguration, disableChannelFrame, configurationFrame, offSetFrame, startUpTimeFrame].filter((frame) => frame !== null); const errors = frames.map((frame) => "errors" in frame ? frame.errors : []).flat(); if (errors.length > 0) { return { success: false, errors }; } return { success: true, data: { frames: concatFrames(frames.map((frame) => frame.bytes), byteLimit, input.configurationId ?? input.transactionId ?? 1, 31) } }; } default: return { success: false, errors: [`Unknown device action: ${input.deviceAction}`] }; } } return { encodeDownlink, decodeUplink, decodeHexUplink, adjustRoundingDecimals }; } export { NETRIS2Parser };