node-red-contrib-modbus

repository·master·Indexed 43 days ago

https://github.com/biancoroyal/node-red-contrib-modbus

An all-in-one Modbus TCP, UDP, and Serial contribution package for Node-RED providing client and server nodes for industrial automation. Version 5.60.2 (OSS LTS) includes a comprehensive suite of nodes such as Modbus-Read, Modbus-Getter, Modbus-Write, and Modbus-Server, along with a structured learning path of 18 examples covering register concepts, FC1–16, and serial RTU clients. Requires Node.js >= 22 and Node-RED >= 4.

Tokens
21.6K
Snippets
27
Records
98
Agent score
56%

What's inside node-red-contrib-modbus

  1. Overview of Modbus nodes in v5

    master

    The node-red-contrib-modbus package (v5) provides a comprehensive suite of nodes for Modbus TCP and Serial communication.

    Client Nodes:

    • Modbus-Client: Core configuration for TCP or Serial, featuring an XState FSM for queueing and reconnection.
    • Modbus-Read: Performs periodic polling using Function Codes (FC) 1–4.
    • Modbus-Getter: Performs on-demand reads using FC 1–4.
    • Modbus-Flex-Getter: Performs FC 1–4 reads driven by the incoming payload.
    • Modbus-Write: Performs writes using FC 5, 6, 15, or 16.
    • Modbus-Flex-Write: Performs writes driven by the incoming payload.
    • Modbus-Flex-Sequencer: Handles ordered multi-range reads.
    • Modbus-Flex-FC: Allows custom FC maps using extras/argumentMaps.
    • Modbus-Flex-Connector: Enables runtime endpoint switching and reconnection.
    • Modbus-Queue-Info: Provides client queue depth information and reset capabilities.

    Utility & Server Nodes:

    • Modbus-Response: Displays status for responses.
    • Modbus-IO-Config: Provides named IO JSON mapping.
    • Modbus-Response-Filter: Filters IO payloads by name.
    • Modbus-Server: A buffer-backed TCP slave (using jsmodbus) intended for demos and tests.
  2. Understand the Repo Quality & FSM Hardening Specification

    master

    The repo-quality-and-fsm-hardening specification defines the engineering goals for the node-red-contrib-modbus v5 OSS LTS line. The primary focus is on improving Finite State Machine (FSM) correctness, ensuring Modbus-spec compliance, and hardening the internal queue and memory management to prevent common production issues like connection instability, silent stalls, and reconnect loops.

    Key improvements include:

    • FSM Robustness: Fixing race conditions related to reconnectTimeoutId and ensuring closingModbus guards are respected during state transitions.
    • Modbus Compliance: Strict validation of msg.payload.address, msg.payload.quantity, and msg.payload.unitid against the Modbus Application Protocol Specification V1.1b3.
    • Memory & Queue Safety: Implementing queue-depth caps and preventing unbounded growth of the unitSendingAllowed array.
    • Code Modernization: Replacing archaic global namespace guards with standard CommonJS module.exports to improve tree-shaking and static analysis.
  3. Understand the live Modbus testing layers

    master

    The project distinguishes between three testing layers to ensure stability and prevent regressions (such as queue overflows or shared-client stops):

    1. Core Tests: Low-level logic testing.
    2. Helper-Unit Tests: Testing using node-red-node-test-helper with stubs or mocked handlers.
    3. Live Modbus Tests: The highest level of assurance. These tests boot a real Node-RED runtime, connect real Modbus clients to an in-process Modbus-Server on ephemeral ports, and assert actual msg.payload values and events rather than stubbing internal onModbus*Done methods.
  4. Understand Sequential Queue Drain Re-arm behavior

    master

    In node-red-contrib-modbus (v5.x), the Sequential Queue Drain Re-arm capability manages how commands are processed when multiple requests are buffered for the same device (UnitId) under sequential constraints.

    Key Concepts

    • Sequential Drain: When parallelUnitIdsAllowed is false (or when using clienttype: 'serial'), the client processes commands one by one.
    • Re-arming: To prevent the queue from stalling, the system must 're-arm' the unitSendingAllowed status. If a command for UnitId N completes and there are still commands for UnitId N waiting in the bufferCommandList, the UnitId must be added back to the allowed sending list so the next dequeue operation can proceed.
    • Push-time Deduplication: To prevent the allowed list from growing unboundedly, the system uses an includes guard. A UnitId is only added to the unitSendingAllowed list if it is not already present, ensuring that only one 'pending drain slot' exists per UnitId at any time.

    Requirements for Correct Operation

    1. Command Completion: Upon completion of a command, if bufferCommands is true and work remains for that UnitId, the UnitId must be re-pushed to the allowed list.
    2. Validation Failures: If a command is rejected due to address or quantity validation after being dequeued, the system must trigger an 'unlock' (via activateSendingOnFailure) to ensure the queue does not become stranded.
  5. Handle unitId and unitid consistency

    master

    When resolving the Modbus Unit ID from a message payload, the following priority and logic rules apply:

    1. Priority: If both msg.payload.unitId and msg.payload.unitid are present, unitId takes precedence.
    2. Type Handling: If unitId is provided as an integer (including 0), it must be treated as a valid ID and not fall back to other values due to truthiness checks.
    3. Naming: The system aims for consistency between the camelCase unitId and lowercase unitid keys.
  6. How shared Modbus clients handle multiple consumers

    master

    In node-red-contrib-modbus v5.x, multiple consumer nodes (such as Flex-Getter, Read, or Write nodes) can share a single modbus-client configuration node.

    Previously, disabling or deleting one consumer node would inadvertently close the connection for all other nodes sharing that same client. The Shared Client Deregister Isolation capability fixes this by ensuring that consumers are isolated from one another.

    Key behaviors of the shared client model:

    • Partial Deregistration: If you disable or delete one consumer node, other nodes sharing the same client remain connected and active. The connection only closes when the last registered consumer is removed.
    • First Registration: The first consumer to register with a client triggers the connection initialization (NEW/INIT state).
    • Last Deregistration: When the final consumer is deregistered, the client performs its standard shutdown (closing the socket and setting the FSM to STOP).
  7. Understand the Modbus Dynamic Server / Gateway concept

    master

    The Modbus Dynamic Server / Gateway is a proposed capability (currently a draft spec for a separate package, not implemented in node-red-contrib-modbus v5) designed for building smart Modbus gateways.

    Unlike the standard modbus-server which is a buffer-backed slave (using register maps), a Dynamic Server allows a Node-RED flow to act as a protocol mediator. The workflow is:

    1. An inbound Modbus TCP request is emitted into the Node-RED flow as a structured object.
    2. The flow processes the request (relays, caches, filters, or mediates).
    3. The flow sends a response message back to the gateway, which then crafts the protocol response to the original client.

    This pattern is intended for users who need to bridge Modbus requests to other protocols or complex logic rather than just reading/writing to a static register map.

  8. Understand Modbus-Client reconnection behavior

    master

    The Modbus-Client handles reconnection based on the reconnectOnTimeout setting. It is important to distinguish between expected behavior and bugs:

    Expected Behavior (Legitimate Retry)

    If reconnectOnTimeout is set to true and the peer is down, the client will perform a sequential retry loop: RECONNECT → reconnecting → INIT → connect. This repeats every reconnectTimeout ms. This is the intended behavior for maintaining connectivity.

    Bug Symptoms (Pathological Loop)

    If you experience the following, it may be a bug (addressed in recent hardening updates):

    • Multiple overlapping timers after a single disconnect.
    • CPU spikes during reconnection attempts.
    • Reconnection attempts continuing even after the Node-RED node has been closed.
    • Parallel INIT chains interfering with each other, preventing the FSM from reaching the activated state.
  9. Understand the in-package Modbus-Server

    master

    The modbus-server included in the package is a buffer-backed Modbus TCP slave.

    Use Cases:

    • Demonstrations and testing.
    • Simple edge caching.
    • Allowing users to inject data into holding/coils/input/discrete buffers and mirror activity on five outputs.

    Limitations:

    • It is not a dynamic gateway. You cannot craft a custom protocol response per inbound request using the flow logic. For dynamic gateway capabilities, a separate package is recommended (see Issue #567).
  10. Understand Modbus Reconnect Semantics and Cycles

    master

    The Modbus client uses an Intentional reconnect retry cycle to handle device or network outages. This is the desired production behavior where the client attempts to recover automatically.

    A single retry cycle consists of:

    1. Entering a reconnecting state.
    2. Waiting for the configured reconnectTimeout (default: 2000 ms).
    3. Moving to INIT state.
    4. Waiting for serialConnectionDelayTimeMS or reconnectTimeout.
    5. Executing connectClient().
    6. On Success: Transition to connected $\rightarrow$ activated.
    7. On Failure: Transition to failed or broken and repeat the cycle if reconnectOnTimeout is true and the node is not closing.

    Important Terminology:

    • Intentional reconnect retry cycle: The sequential, single-timer process described above.
    • Pathological reconnect loop: A defect where multiple uncancelled timers fire in parallel, causing CPU spikes or re-entrancy issues. The library is designed to eliminate this by ensuring at most one active reconnect timer exists at any time.
  11. Understand Client Timeout and Reconnect Recovery behavior

    master

    In node-red-contrib-modbus (v5.60.2+), the Modbus client follows a strict 'honesty' model for connection states. This means the client will not report a 'Fake-Ready' status during outages. Instead, it follows a visible state machine cycle: failed → broken → reconnecting → init → connectClient.

    Key behaviors to note:

    • Single Reconnect Delay: When reconnectOnTimeout is enabled, the client waits for the duration of reconnectTimeout exactly once during the reconnecting phase before attempting to initialize (init) and connect. It does not stack multiple delays.
    • Automatic Activation: Upon a successful connection (reaching the connected state), the client automatically sends an ACTIVATE signal. This ensures the client reaches the activated state (ready to process queues/reads) without requiring a prior command to complete.
    • Half-Open Sockets: If a socket becomes half-open (not readable), the client will fail into the standard FSM recovery cycle rather than attempting an in-band connection nudge.
  12. How Function Code (FC) dispatching works in Modbus Client

    master

    The Modbus client uses a mapping strategy to dispatch Function Codes (FC) to their respective methods. This supports both standard Modbus messages and 'deformed' messages (used when enableDeformedMessages is true in the payload).

    Supported Function Code Mappings:

    • Standard Read Methods:
      • 1: readCoils
      • 2: readDiscreteInputs
      • 3: readHoldingRegisters
      • 4: readInputRegisters
    • Deformed Read Methods:
      • 1: readCoils_deformedReadEnabled
      • 2: readDiscreteInputs_deformedReadEnabled
      • 3: readHoldingRegisters_deformedReadEnabled
      • 4: readInputRegisters_deformedReadEnabled

    If an unknown Function Code is provided, the client will trigger activateSendingOnFailure with a Function Code Unknown error.

    const READ_FC_METHOD = {
      1: 'readCoils',
      2: 'readDiscreteInputs',
      3: 'readHoldingRegisters',
      4: 'readInputRegisters'
    }
    const READ_FC_METHOD_DEFORMED = {
      1: 'readCoils_deformedReadEnabled',
      2: 'readDiscreteInputs_deformedReadEnabled',
      3: 'readHoldingRegisters_deformedReadEnabled',
      4: 'readInputRegisters_deformedReadEnabled'
    }
    
    function readModbusByFunctionCode (node, msg, cb, cberr) {
      const fc = parseInt(msg.payload.fc)
      const method = msg.payload.enableDeformedMessages ? READ_FC_METHOD_DEFORMED[fc] : READ_FC_METHOD[fc]
      if (!method) {
        activateSendingOnFailure(node, cberr, new Error('Function Code Unknown'), msg)
        return
      }
      node.client[method](parseInt(msg.payload.address), parseInt(msg.payload.quantity))
        .then(resp => activateSendingOnSuccess(node, cb, cberr, resp, msg))
        .catch(err => { activateSendingOnFailure(node, cberr, new Error(err.message), msg); node.modbusErrorHandling(err) })
    }