Trouble with unreliable modbus-flex-getter

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": [
            []
        ]
    }
]

When choosing "Empty msg on Modbus fail", it actually removes all msg properties! Except msg.topic! Even when setting "Keep Msg Properties", it has no effect on fail.

So for me in order to keep meta-data of the current request, I need to store a clone of the complete msg itself into msg.topic! Then unwrap it after catching it... This is getting really hairy.

Not answering all your questions, but if you can generate a flow that produces a Fail or Success output then you can use @colinl/node-red-guaranteed-delivery to handle the retries. You won't need to worry about queuing requests and so on yourself.

1 Like

Interesting, it may serve as a form of "edge backup" which we have been planning to add!

All the things you mentioned are true about the modbus nodes .. they still need some work.
I use them also extensively and noticed the same issues.

yes .. msg.topic is the only property that is passed on (with failed msgs) and that's what i use to track down what previous msg i sent has failed. In my project i didnt create any logic to retry a failed msg .. i also have it set to request every minute and if a request fails or a device is down i just populate the values with null to save to my database and update the dashboard.

Another issue i notice with the latest versions .. is when you have the Modbus Configuration to Reconnect on timeout and the device is down (Error: Client Not Ready To Read At State init) the Empty msg on Modbus fail doesnt work .. because the node is not in a state to even receive msgs and it doesnt produce an empty msg. I had to revert back to version 5.23.3 to get this to work again since the latest versions introduced some breaking changes.

I think the best thing is to open an Issue on the github page for the node.

1 Like

I think you may need to look at your comms environment a little more.

To give some context

1 have 4 x Energy meters here that are attached to a Modbus RTU to Ethernet gateway - i query these every 200ms and have very few to no failures - i run my NR on a Virtual Machine on ESXi.

I have a further 3 x Battery Inverters that i query every second and update every 3 seconds - again through the same RS485 to Ethernet gateway and receive NO errors to these devices

I have another 2 x PV Inverters which i query every 15s and update on that schedule also - again no errors

I have been using Steve's modbus nodes for a few years now and find them to be rock solid.

Craig

Allright, thanks for acknowledgement of shortcomings of the modbus node and sharing your experience. Indeed the devices I connect to are old and unreliable. But it's not something I can fix at the moment. I'm working with many different devices on different protocols and it's varying degrees of unstableness all over hehe. Regardless, I think it's such a pity that node designers choose to not pass along the original message. Because in the simplest cases, sure you don't need the context. But if you need anything more advanced, logging, retry etc, it is ALWAYS useful to be able to keep the original messages with properties.

I really want to try all other options before rolling back 2 years (!) worth of releases to get a modbus version that always produces output. So even if Colin has a great option for guaranteed delivery, it doesn't work with modbus when modbus doesn't always and reliably produce output.

There are 2 key concepts I wish all node designers were forced to follow:

  1. Always pass along incoming msg props (when possible)
  2. Always produce output no matter what (don't swallow messages into a black hole)

Whether the output is a catch, a msg prop in normal output or a 2nd error ouput doesn't matter to me.

Right now I'm speculating on making a super structure around the modbus to track incoming messages, apply a delay, and then send output if modbus doesn't. It's a lot of work to understand how these community nodes work (or doesn't work) and to make them reliable.

Can you remind me under what circumstances it does that?
In that situation do either a Catch or Complete node trigger?

Yup, as this discussion is about. Quoted from UnborN:

Another issue i notice with the latest versions .. is when you have the Modbus Configuration to Reconnect on timeout and the device is down (Error: Client Not Ready To Read At State init) the Empty msg on Modbus fail doesnt work .. because the node is not in a state to even receive msgs and it doesnt produce an empty msg. I had to revert back to version 5.23.3 to get this to work again since the latest versions introduced some breaking changes.

I have a case I'm working on right now, where I send a msg to modbus flex-getter, and it swallows it completely and doesn't produce any output. Nothing on output, complete node, catch node or debug panel. Dead silent. It isn't identical to the reported case of UnborN. I have an 'active' connection, but still no output.

I'm working on a superstructure to wrap faulty I/O nodes that swallow messages. This will track incoming messages in a function node BEFORE the I/O node, apply a timeout, then use another function node AFTER the I/O node to send out a message if it isn't outputed normally within the timeout.

Does it trigger a Complete node? Every node should trigger Complete or Catch. If it doesn't them that should be reported.
Has the specific issue you describe been reported as an issue?

The guaranteed delivery can still be used by using a timeout to indicate an error if the node does not respond.

Sorry forgot to check and specify, but tried complete node now. Nothing there either.

After changing config to turn on Optionals (Show Activities, Show Errors, Show Warnings), it produced some output again. It's a bit on and off. With only Empty msg on Modbus fail and Keep Msg Properties turned on it was silent. Problem is, if I enable Show Errors, I can get Catch output for some cases. If I enable Empty msg on Modbus fail, I also get output msg for some cases. These don't overlap, so I will easily end up with 2 message going out, which needs to be joined together to avoid duplication in retry and error handling... Or none of the cover the error, so it is still dead silent (no complete, no catch, no output). I really want to avoid going that down route, but it is a possible option. That is, if both settings cover all mistakes, something I doubt based on my experience so far.

I'm not able to recreate different cases reliably and it's not clear to me whether something change because I changed a config somewhere, or because some state refreshed on deploy. It's a bit messy at the moment.

There is one issue related to the Empty msg on Modbus fail but it was autoclosed

After studying the code a bit .. i see that if we take for example the modbus-flex-getter.js

we see that when a msg is received L187
some checks are done before sending the modbus request to the node's internal messageQueue

Those are

  1. invalidPayloadIn to check the validity of the modbus msg
  2. isNotReadyForInput .. not sure what this is
  3. isInactive . the modbus device is not connected / initialized ?

This code was introduced 3 years ago and in all cases those checks return (and rightly so) and only send a verboseWarn instead of a mbBasics.sendEmptyMsgOnFail(node, err, origMsgInput) that is what we need to handle an error further down the flow.

1 Like

Ouch, they went out of their way to make a highly unusual error-handling which is nigh impossible to handle well for end-users.

Are you able to find out the difference between Show Errors and Log failures? I tried deep diving into source code, seems like Show Errors is standard debug panel (ie. node.error())? But I failed to resolve the source of Log failures when coming to this path: de.biancoroyal.modbus.core.client.modbusSerialDebug = de.biancoroyal.modbus.core.client.modbusSerialDebug || require('debug')('modbus-serial') // eslint-disable-line no-use-before-define

Perhaps it is sent to stdout of the process?

Got something working now :smiley: It sure ain't pretty:

Key features:

  • Guaranteed output: even if Modbus swallows a message, a duplicate is sent around it.
  • Retry: Optional parameter to specify how many times to retry, how long wait between attempts etc.

Turns out I made a home-made version of this: node-red-retry (node) - Node-RED
Need to check library first haha🤣

One minor issue now is delay for guaranteed output is set to 5 sec. Timeout in Modbus is set to 1 second. However, either of these 2 values can change (if someone edits them). I want the delay to be a variable depending on the Modbus config node timeout setting. And then increment it by 1 or 2 seconds to be sure that modbus has a chance to output it's message BEFORE "guaranteed output" sends the duplicate.

Any way to get that dynamically?

Indeed :wink: .. but why do you have to do this "manual" retry for failed modbus reads ?
why not just wait for the next request and keep only the successful replies ?
if 1min is not fast enough just speed up the polling .. every 10sec
@craigcurtin is polling every 200ms without a hickup.
Check the communication .. are you using rs485 .. .. are you requesting many registers more than the device can handle ?


Its not possible to read the config of another node :point_down:


ps. I opened a new issue on Modbus node's github for the Empty msg on Modbus fail

1 Like

Thanks, I wanted to do that myself but you perfected it. Now that I got a robust modbus request mechanic all wrapped in a subflow for ease of acccess, I can start reliably look at the behavior and responses. Here are some examples:

2025-01-25 16:08:03 - WARNING: Modbus failed getting status_text (Empty payload. Data length error, expected 37 got 7), attempt nr 1. Trying again in 2 seconds.
2025-01-25 16:14:04 - WARNING: Modbus failed getting status_text (Empty payload. Timed out), attempt nr 1. Trying again in 2 seconds.
2025-01-25 16:14:11 - WARNING: Modbus failed getting status_text (Empty payload. Request timeout (msg lost in modbus node).), attempt nr 2. Trying again in 2 seconds.

Here we see 3 different scenarios. One failed with specific message Data length error, expected 37 got 7. Another failed with Timed out. And the last failed by not producing any output (so the actual error is unknown, but easy to imagine it was a time out that modbus-flex getter failed to output).

Right now node red polls this data every 1 minute. Then node red send data (that was returned successfully) to database for analysis, dashboard statistics and alarms. There is also one data point that is polled hourly (in addition to the minutely data). In total I don't think there's a lot of data requested from modbus:

const requests = {
  minute: [
    { topic: "power_production",    value: 0, fc: 4, address: 3004, quantity:  2 }, // FC4 - 3005-06
    { topic: "fault_code",          value: 0, fc: 4, address: 3095, quantity:  5 }, // FC4 - 3096-3100
    { topic: "grid_frequency",      value: 0, fc: 4, address: 3042, quantity:  1 }, // FC4 - 3043
    { topic: "cabinet_temperature", value: 0, fc: 4, address: 3041, quantity:  1 }, // FC4 - 3042 (inverter temp)
    { topic: "dc_voltage_current",  value: 0, fc: 4, address: 3021, quantity:  8 }, // FC4 - 3022-29
    { topic: "status_text",         value: 0, fc :4, address: 3029, quantity: 16 }, // FC4 - 3030-45 (alarm code and inverter status)
  ],
  hour: [
    { topic: "energy_production",   value: 0, fc: 4, address: 3008, quantity:  2 }, // FC4 - 3009-10
  ],
  month: [
    { topic: "energy_last_month",   value: 0, fc: 4, address: 3012, quantity:  2 }, // FC4 - 3013-14
  ],
  year: [
    { topic: "energy_last_year",    value: 0, fc: 4, address: 3018, quantity:  2 } // FC4 - 3019-20
  ]
};

I don't want to fill up database with data every second. Not sure if even minutely data is needed, but that's what it is at the moment. And most of all, I'd like to avoid having holes in the data, which makes the dashboard graphs incomplete and ugly. I think polling data more frequently will just leave more holes in the data. So if anything, I want to poll less frequently.

The devices are old and of various makes and models, so probably some of them are unreliable.

Probably the retry mechanic failed since I didn't have guaranteed output previously. Now, retry works, I increased it from 2 to 3 attempts and it seems to make wonders for reliability. Much less holes in the data. So whatever the underlying problem is, retries seem reduce it.

1 Like

As a side note, this is why I'm not so happy about the join-handling in node red. Splitting is easy, as it's isolated. But with join, you need to get all messages from unreliable flows and nodes with unknown delays. It assumes none of the messages get lost, but they do as shown here. And as long as one is lost, the entire batch is lost. And possibly remains in memory/context if not also adding flush/reset mechanic. All of which is manual and easy to mess up. And difficult to debug, particularly when dealing with subflows.

One thing that will simplify your flow is install the older version of the modbus nodes
in order to have the Empty msg on Modbus fail. With this you can easily check in a Function

if (msg.hasOwnProperty('error')) {
  // retry modbus request
}
else {
   // successful msg
}

No need for complex logic to throw an Error and Catch nodes etc

In .node-red folder npm i node-red-contrib-modbus@5.23.3

That depends on how you are using it. I virtually always use it in key/value mode and specify After 1 message parts, and Every Subsequent message. Then the following nodes can determine which elements are present and take action as appropriate. The topic from the last message is passed through so, in addition, the following nodes can use that knowledge where necessary.