I got a subflow to handle modbus retries. Because modbus requests are highly unstable and fails multiple times each hour. Quick and dirty solution is to run the request again 1 or 2 more times until it succeeds.
For this to work, I need precise handling of errors/exceptions. It is also good to have a 2nd error ouput to support the case when the request that failed was part of a group to be joined later in parent flow.
Modbus flex getter has many different options:
- Empty msg on Modbus fail
- Keep Msg Properties
- Show Activities
- Show Errors
- Show Warnings
Then the context node also has a set of options:
- Log states changes
- Log failures
- Show Errors
- Show Warnings
- Show Logs
I don't understand the details of all these options, for example what is the difference between Show Errors
and Log failures
? Furthermore, I don't understand what happens if I set a specific setting in the node and/or the context node.
I monitor 12 values to export every minute. When these settings are disabled, I get no exception in parent's catch (set to catch all). When I enable them, I also don't get any exceptions when data is missing. If I set Empty msg on Modbus fail
, I get an output message which I use to throw an exception (to be logged and possibly retried). But that empty message doesn't have the Msg Properties, so all meta-data about the context of that particular msg is lost. And so the retry fails.
How is it possible to make a retry mechanic for modbus? How to precisely capture every msg which request doesn't return successfully with a value?
This was very easy to do with for example http node. I need some help to understand how it's possible to manage with modbus. Thanks in advance for any insight.
[
{
"id": "a5184c5aab2f6cda",
"type": "subflow",
"name": "log",
"info": "# Writes log to file\r\nMust set env variable `logTopic` in parent flow to determine filename.\r\nThis can't be done as input variable because the parent may also be a subflow.\r\n\r\n### Inputs\r\n\r\n: logMessage (string) : the log message to write to file can be provided in the node configuration, or as a message property. By default it will use `msg.logMessage`.\r\n: logTopic (string) : determines parts of the file name. Typically enter flow name for this purpose.\r\n: logCompleteMsgObj (boolean): Add the entire msg object to the log message.\r\n: stdOut (boolean) : Also log to standard out (using debug node).\r\n\r\n### Details\r\n\r\nLogs are written to /data/logs/`msg.logTopic`_yyyy-MM-dd.txt. `msg.logTopic` is used as filename prefix. Typically use flow name for this purpose. Creates file and folder if they don't exist.\r\n\r\n`msg.logMessage` is used as the main content of the log to write. Datetime is automatically added as prefix. Each log is appended with newline.",
"category": "",
"in": [
{
"x": 100,
"y": 80,
"wires": [
{
"id": "0db769e63c22272e"
}
]
}
],
"out": [
{
"x": 1040,
"y": 80,
"wires": [
{
"id": "5f7d3b7030f7bb02",
"port": 0
},
{
"id": "0db769e63c22272e",
"port": 0
}
]
}
],
"env": [
{
"name": "logMessage",
"type": "str",
"value": "",
"ui": {
"label": {
"en-US": "Log message"
},
"type": "input",
"opts": {
"types": [
"str"
]
}
}
},
{
"name": "logCompleteMsgObj",
"type": "bool",
"value": "false",
"ui": {
"label": {
"en-US": "Log complete msg"
},
"type": "checkbox"
}
},
{
"name": "stdOut",
"type": "bool",
"value": "false",
"ui": {
"label": {
"en-US": "Debug (sidebar)"
},
"type": "checkbox"
}
},
{
"name": "logTopic",
"type": "env",
"value": "$parent.logTopic",
"ui": {
"type": "hide"
}
}
],
"meta": {},
"color": "#C7E9C0",
"icon": "font-awesome/fa-file-text"
},
{
"id": "b1add847796af765",
"type": "function",
"z": "a5184c5aab2f6cda",
"name": "prepare log",
"func": "msg._backup = RED.util.cloneMessage(msg);\n\nconst date = new Date().toISOString().replaceAll(\"-\", \"-\").replaceAll(\"T\", \"-\").replaceAll(\":\", \"-\").replaceAll(\".\", \"-\").replaceAll(\"Z\", \"-\");\nconst list = date.split(\"-\");\nconst year = list[0];\nconst month = list[1];\nconst day = list[2];\nconst hour = list[3];\nconst minute = list[4];\nconst second = list[5];\n\nlet logTopic;\ntry{\n logTopic = env.get(\"logTopic\").toLowerCase();\n if(!logTopic){\n node.error(\"ERROR: Log failed getting logTopic env from parent. Did you remember to set it?\");\n return;\n }\n} catch (error) {\n node.error(\"ERROR: Log failed getting logTopic env from parent. Did you remember to set it?\");\n return;\n}\n\nmsg.stdOut = env.get(\"stdOut\");\n\nlet logMessage = env.get(\"logMessage\") ? env.get(\"logMessage\") : msg.logMessage;\nlet logCompleteMsgObj = env.get(\"logCompleteMsgObj\");\nif(!logCompleteMsgObj){\n logCompleteMsgObj = msg?.logCompleteMsgObj ?? false;\n}\n\nif(logCompleteMsgObj){\n logMessage += \"\\n\";\n logMessage += JSON.stringify(msg._backup);\n}\n\nmsg.filename = `/data/logs/${logTopic}/${logTopic}_${year}-${month}-${day}.log`;\nmsg.payload = `${year}-${month}-${day} ${hour}:${minute}:${second} - ${logMessage}`;\n\nconst maxLineLength = 10000;\nif(msg.payload.length > maxLineLength){\n // Safeguard against excessively large logs\n msg.payload = msg.payload.slice(0, maxLineLength) + '... [TRUNCATED]';\n}\n\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 470,
"y": 140,
"wires": [
[
"3ea911242530d241",
"2c7e47352b7df809"
]
]
},
{
"id": "3ea911242530d241",
"type": "file",
"z": "a5184c5aab2f6cda",
"name": "write log file",
"filename": "filename",
"filenameType": "msg",
"appendNewline": true,
"createDir": true,
"overwriteFile": "false",
"encoding": "utf8",
"x": 670,
"y": 140,
"wires": [
[
"5f7d3b7030f7bb02"
]
]
},
{
"id": "e96bad283c240487",
"type": "catch",
"z": "a5184c5aab2f6cda",
"name": "",
"scope": null,
"uncaught": false,
"x": 660,
"y": 260,
"wires": [
[
"5ac9480a70f4d93a"
]
]
},
{
"id": "5ac9480a70f4d93a",
"type": "debug",
"z": "a5184c5aab2f6cda",
"name": "log failed error",
"active": true,
"tosidebar": true,
"console": true,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 850,
"y": 260,
"wires": []
},
{
"id": "fac2f56d19607dc3",
"type": "debug",
"z": "a5184c5aab2f6cda",
"name": "log std out",
"active": true,
"tosidebar": true,
"console": true,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 830,
"y": 200,
"wires": []
},
{
"id": "2c7e47352b7df809",
"type": "switch",
"z": "a5184c5aab2f6cda",
"name": "std out?",
"property": "stdOut",
"propertyType": "msg",
"rules": [
{
"t": "true"
},
{
"t": "else"
}
],
"checkall": "true",
"repair": false,
"outputs": 2,
"x": 660,
"y": 200,
"wires": [
[
"fac2f56d19607dc3"
],
[]
],
"inputLabels": [
"msg.stdOut"
],
"outputLabels": [
"true",
"false"
]
},
{
"id": "5f7d3b7030f7bb02",
"type": "function",
"z": "a5184c5aab2f6cda",
"name": "revert",
"func": "return msg._backup;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 810,
"y": 140,
"wires": [
[]
]
},
{
"id": "0db769e63c22272e",
"type": "function",
"z": "a5184c5aab2f6cda",
"name": "logMessage?",
"func": "const logMessage = env.get(\"logMessage\") ? env.get(\"logMessage\") : msg.logMessage;\n\nconst skip = logMessage ? null : msg;\nconst log = logMessage ? msg : null;\nreturn [skip, log];",
"outputs": 2,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 220,
"y": 80,
"wires": [
[],
[
"b1add847796af765"
]
],
"outputLabels": [
"Skip",
"Log"
]
},
{
"id": "71082c29770ea643",
"type": "inject",
"z": "a5184c5aab2f6cda",
"g": "6f8fb577b11364ed",
"name": "",
"props": [],
"repeat": "",
"crontab": "00 00 * * *",
"once": false,
"onceDelay": 0.1,
"topic": "",
"x": 210,
"y": 400,
"wires": [
[
"09a9a5f729dd65b4"
]
]
},
{
"id": "0c31a507622aeb8d",
"type": "file",
"z": "a5184c5aab2f6cda",
"g": "6f8fb577b11364ed",
"name": "write empty file",
"filename": "filename",
"filenameType": "msg",
"appendNewline": false,
"createDir": true,
"overwriteFile": "false",
"encoding": "utf8",
"x": 540,
"y": 400,
"wires": [
[]
]
},
{
"id": "09a9a5f729dd65b4",
"type": "function",
"z": "a5184c5aab2f6cda",
"g": "6f8fb577b11364ed",
"name": "empty log",
"func": "const date = new Date().toISOString().replaceAll(\"-\", \"-\").replaceAll(\"T\", \"-\").replaceAll(\":\", \"-\").replaceAll(\".\", \"-\").replaceAll(\"Z\", \"-\");\nconst list = date.split(\"-\");\nconst year = list[0]; // YYYY\nconst month = list[1]; // MM\nconst day = list[2]; // DD\n\nconst logTopic = env.get(\"$parent.logTopic\").toLowerCase();\nif(!logTopic){\n return; // silent quit\n}\n\nmsg.filename = `/data/logs/${logTopic}/${logTopic}_${year}-${month}-${day}.log`;\nmsg.payload = \"\";\n\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 370,
"y": 400,
"wires": [
[
"0c31a507622aeb8d"
]
]
},
{
"id": "6f8fb577b11364ed",
"type": "group",
"z": "a5184c5aab2f6cda",
"name": "Create new log file every day",
"style": {
"stroke": "#0070c0",
"fill": "#bfdbef",
"label": true,
"color": "#000000"
},
"nodes": [
"0c31a507622aeb8d",
"09a9a5f729dd65b4",
"71082c29770ea643"
],
"x": 104,
"y": 359,
"w": 542,
"h": 82
},
{
"id": "692b7a4cf60bbc70",
"type": "subflow",
"name": "modbus retry",
"info": "A wrapper for modbus flex-getter.\r\n\r\nRetry request if it fails.\r\n\r\nmsg props take precedense over Node config.\r\n\r\nmsg prop options:\r\n- `msg.topic`: Set to some descriptive name of the value requested by modbus for logging purposes.\r\n- `msg.maxAttempts`: How many times to try.\r\n- `msg.delay`: Delay (in sec) between each attempt.\r\n- `msg.logWarning`: Log each retry attempt (as warning).\r\n- `msg.logError`: Log no more retry attempts (as error).\r\n",
"category": "",
"in": [
{
"x": 160,
"y": 140,
"wires": [
{
"id": "179048445f161308"
}
]
}
],
"out": [
{
"x": 1280,
"y": 140,
"wires": [
{
"id": "dc96b4046da1ba9b",
"port": 0
}
]
},
{
"x": 1250,
"y": 240,
"wires": [
{
"id": "9af86763351af4be",
"port": 0
},
{
"id": "6b3b0dba904650c6",
"port": 0
}
]
}
],
"env": [
{
"name": "modbusServer",
"type": "modbus-client",
"value": "",
"ui": {
"icon": "font-awesome/fa-gear",
"label": {
"en-US": "Server"
},
"type": "conf-types"
}
},
{
"name": "maxAttempts",
"type": "num",
"value": "1",
"ui": {
"icon": "font-awesome/fa-mail-reply-all",
"label": {
"en-US": "Max attempts"
},
"type": "spinner"
}
},
{
"name": "delay",
"type": "num",
"value": "10",
"ui": {
"icon": "font-awesome/fa-clock-o",
"label": {
"en-US": "Delay (seconds between attempts)"
},
"type": "spinner"
}
},
{
"name": "logWarning",
"type": "bool",
"value": "false",
"ui": {
"icon": "font-awesome/fa-warning",
"label": {
"en-US": "Log warning (retry)"
},
"type": "checkbox"
}
},
{
"name": "logError",
"type": "bool",
"value": "false",
"ui": {
"icon": "font-awesome/fa-times-circle",
"label": {
"en-US": "Log error"
},
"type": "checkbox"
}
},
{
"name": "logTopic",
"type": "env",
"value": "$parent.logTopic",
"ui": {
"label": {
"en-US": "logTopic"
},
"type": "hide"
}
}
],
"meta": {},
"color": "#E9967A",
"outputLabels": [
"Modbus output",
"Error"
],
"icon": "font-awesome/fa-mail-reply-all",
"status": {
"x": 1220,
"y": 320,
"wires": [
{
"id": "19d673b6c77fbe32",
"port": 0
}
]
}
},
{
"id": "54509e994c668256",
"type": "modbus-flex-getter",
"z": "692b7a4cf60bbc70",
"name": "Modbus",
"showStatusActivities": false,
"showErrors": false,
"showWarnings": false,
"logIOActivities": false,
"server": "${modbusServer}",
"useIOFile": false,
"ioFile": "",
"useIOForPayload": false,
"emptyMsgOnFail": true,
"keepMsgProperties": true,
"delayOnStart": false,
"startDelayTime": "",
"x": 940,
"y": 140,
"wires": [
[
"dc96b4046da1ba9b"
],
[]
],
"outputLabels": [
"Output 1",
"Output 2"
]
},
{
"id": "24823ae5174d818c",
"type": "catch",
"z": "692b7a4cf60bbc70",
"name": "catch output",
"scope": [
"dc96b4046da1ba9b"
],
"uncaught": false,
"x": 270,
"y": 220,
"wires": [
[
"cc50a72d8f2609c3"
]
]
},
{
"id": "9af86763351af4be",
"type": "subflow:a5184c5aab2f6cda",
"z": "692b7a4cf60bbc70",
"name": "Log Error",
"x": 560,
"y": 240,
"wires": [
[]
]
},
{
"id": "cc50a72d8f2609c3",
"type": "function",
"z": "692b7a4cf60bbc70",
"name": "retry?",
"func": "let tempLog = \"\";\n\nif (msg.attempt >= msg.maxAttempts) {\n if (msg.maxAttempts > 1) {\n tempLog = `ERROR: Modbus failed getting ${msg?.topic} (${msg?.error?.message}) after ${msg.maxAttempts} attempts.`;\n msg.logMessage = msg.logError ? tempLog : \"\";\n } else {\n tempLog = `ERROR: Modbus failed getting ${msg?.topic} (${msg?.error?.message}).`;\n msg.logMessage = msg.logError ? tempLog : \"\";\n }\n node.error(tempLog); // always print to debug panel\n return [null, msg];\n}\nconst attempt = msg.attempt;\n\nlet error = msg?.error?.message;\nif(error && \"error: \".toLowerCase().startsWith(\"error: \")){\n error = error.replace(/^error: /, \"\").trim();\n}\ntempLog = `WARNING: Modbus failed getting ${msg.topic} (${error}), attempt nr ${msg.attempt}. Trying again in ${msg.delay / 1000} seconds.`;\nnode.warn(tempLog); // always print to debug panel\n\nmsg = msg._backup; // revert to original message\nmsg._backup = RED.util.cloneMessage(msg); // reapply backup\n\nmsg.logMessage = msg.logWarning ? tempLog : \"\";\nmsg.attempt = attempt +1;\n\nreturn [msg, null];",
"outputs": 2,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 410,
"y": 220,
"wires": [
[
"f30de57ff0e6bc92"
],
[
"9af86763351af4be"
]
],
"outputLabels": [
"Retry",
"No retry"
]
},
{
"id": "179048445f161308",
"type": "function",
"z": "692b7a4cf60bbc70",
"name": "validate input",
"func": "const modbusServer = env.get(\"modbusServer\");\nif (!modbusServer) {\n throw new Error(\"Missing modbus server configuration.\");\n}\nmsg.attempt = 1;\nmsg.maxAttempts = msg.maxAttempts ?? env.get(\"maxAttempts\") ?? 1; // default 0 from config if not set\nmsg.delay = msg.delay ?? env.get(\"delay\") ?? 1; // default 0 from config if not set\nmsg.logWarning = msg.logWarning ?? env.get(\"logWarning\") ?? false;\nmsg.logError = msg.logError ?? env.get(\"logError\") ?? false;\n\nfunction isIntOrNull(value) {\n if (value == null) {\n return true;\n }\n value = parseInt(value, 10);\n if (Number.isInteger(value) && value > 0) {\n return true;\n }\n return false;\n}\n\nif(!isIntOrNull(msg.maxAttempts)){\n const log = `Invalid value for maxAttempts: ${msg.maxAttempts}`;\n node.error(log);\n throw new Error(log);\n}\n\nif (!isIntOrNull(msg.delay)) {\n const log = `Invalid delay value: ${msg.delay}`;\n node.error(log);\n throw new Error(log);\n}\n\nif (typeof msg.logWarning !== 'boolean'){\n msg.logWarning = false;\n}\n\nif (typeof msg.logError !== 'boolean') {\n msg.logError = false;\n}\n\n// default values\nmsg.maxAttempts = msg.maxAttempts ? msg.maxAttempts : 1; // default maxAttempt\nmsg.delay = msg.delay ? msg.delay * 1000 : 1000; // convert sec to ms, default 1000 ms\n\nmsg._backup = RED.util.cloneMessage(msg);\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 280,
"y": 140,
"wires": [
[
"fdffc0973b8b3daf"
]
]
},
{
"id": "f30de57ff0e6bc92",
"type": "subflow:a5184c5aab2f6cda",
"z": "692b7a4cf60bbc70",
"name": "Log Warning (retry)",
"x": 590,
"y": 200,
"wires": [
[
"005bc0f4b140c20f"
]
]
},
{
"id": "005bc0f4b140c20f",
"type": "delay",
"z": "692b7a4cf60bbc70",
"name": "delay",
"pauseType": "delayv",
"timeout": "1",
"timeoutUnits": "seconds",
"rate": "1",
"nbRateUnits": "1",
"rateUnits": "second",
"randomFirst": "1",
"randomLast": "5",
"randomUnits": "seconds",
"drop": false,
"allowrate": false,
"outputs": 1,
"x": 750,
"y": 200,
"wires": [
[
"54509e994c668256"
]
]
},
{
"id": "19d673b6c77fbe32",
"type": "status",
"z": "692b7a4cf60bbc70",
"name": "",
"scope": [
"54509e994c668256",
"005bc0f4b140c20f"
],
"x": 1120,
"y": 320,
"wires": [
[]
]
},
{
"id": "dc96b4046da1ba9b",
"type": "function",
"z": "692b7a4cf60bbc70",
"name": "validate output",
"func": "const errors = [];\n\nif(msg?.payload == null || msg?.payload?.length === 0 || msg?.payload === \"\"){\n // payload should be array or string\n errors.push(\"Empty payload.\");\n}\n\nif (msg?.error != null) {\n // error exists in output\n errors.push(msg.error);\n}\n\nif(errors.length > 0){\n throw new Error(errors.join(\" \"));\n}\n\nconst output = msg._backup;\n\noutput.payload = msg.payload;\n//output.messageId = msg.messageId;\n//output.modbusRequest = msg.modbusRequest;\n//output.responseBuffer = msg.responseBuffer;\n\n// clean up\ndelete output.maxAttempts;\ndelete output.delay;\ndelete output.logWarning;\ndelete output.logError;\ndelete output.logMessage;\n\nreturn output;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1100,
"y": 140,
"wires": [
[]
]
},
{
"id": "fdffc0973b8b3daf",
"type": "JsonSchemaValidatorWithDocu",
"z": "692b7a4cf60bbc70",
"name": "validate payload",
"property": "payload",
"propertyType": "msg",
"checkentireobject": false,
"func": "{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"title\": \"Modbus Payload Schema\",\n \"type\": \"object\",\n \"properties\": {\n \"value\": {\n \"type\": \"integer\"\n },\n \"fc\": {\n \"type\": \"integer\"\n },\n \"address\": {\n \"type\": \"integer\"\n },\n \"quantity\": {\n \"type\": \"integer\"\n },\n \"unitid\": {\n \"type\": \"integer\"\n }\n },\n \"required\": [\n \"value\",\n \"fc\",\n \"address\",\n \"quantity\"\n ]\n}",
"schematitle": "",
"x": 460,
"y": 140,
"wires": [
[
"54509e994c668256"
]
],
"info": "## Untitled schema Type\n\nunknown\n"
},
{
"id": "dfeb7254a39f0d0a",
"type": "catch",
"z": "692b7a4cf60bbc70",
"name": "catch input",
"scope": [
"179048445f161308",
"fdffc0973b8b3daf"
],
"uncaught": false,
"x": 260,
"y": 340,
"wires": [
[
"38ca7757e0eacb28"
]
]
},
{
"id": "38ca7757e0eacb28",
"type": "function",
"z": "692b7a4cf60bbc70",
"name": "invalid json",
"func": "msg.errorMsg = `Invalid input (${msg?.error?.message}). Info: ${JSON.stringify(msg?._error)}`;\nif (msg.logError) {\n msg.logMessage = msg.errorMsg;\n}\n\nnode.error(msg.errorMsg);\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 410,
"y": 340,
"wires": [
[
"6b3b0dba904650c6"
]
]
},
{
"id": "6b3b0dba904650c6",
"type": "subflow:a5184c5aab2f6cda",
"z": "692b7a4cf60bbc70",
"name": "",
"env": [
{
"name": "logCompleteMsgObj",
"type": "bool",
"value": "true"
}
],
"x": 550,
"y": 340,
"wires": [
[]
]
},
{
"id": "4a75d82c6ec39ab7",
"type": "catch",
"z": "692b7a4cf60bbc70",
"name": "catch modbus",
"scope": [
"54509e994c668256"
],
"uncaught": false,
"x": 270,
"y": 420,
"wires": [
[
"23fd3630c7140e81"
]
]
},
{
"id": "23fd3630c7140e81",
"type": "function",
"z": "692b7a4cf60bbc70",
"name": "invalid json",
"func": "msg.errorMsg = `Modbus threw an exception!`;\nif (msg.logError) {\n msg.logMessage = msg.errorMsg;\n}\n\nnode.error(msg.errorMsg);\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 450,
"y": 420,
"wires": [
[
"64f8edf4788d0f18"
]
]
},
{
"id": "64f8edf4788d0f18",
"type": "subflow:a5184c5aab2f6cda",
"z": "692b7a4cf60bbc70",
"name": "",
"env": [
{
"name": "logCompleteMsgObj",
"type": "bool",
"value": "true"
}
],
"x": 630,
"y": 420,
"wires": [
[]
]
}
]