firmware-base/docs/nc-mb.md

10 KiB

ModbusTCP as a Feature for NetworkComponent

This document outlines a design to refactor NetworkComponent to make network protocols like Modbus TCP modular, opt-in features. This approach increases flexibility, reduces the memory footprint for components that don't need network capabilities, and provides a clear path for adding other protocols (e.g., Serial, CAN) in the future.

1. Motivation

Currently, NetworkComponent is tightly coupled with Modbus TCP. Its constructor requires a Modbus baseAddress, and it contains member variables and methods specific to Modbus. This design has a few drawbacks:

  • Lack of Modularity: Every component inheriting from NetworkComponent carries the overhead of Modbus, even if unused.
  • Limited Extensibility: Adding new network protocols is difficult without modifying the base NetworkComponent and Component classes.
  • Inconsistency: NetworkValue already uses a flexible feature system (for logging, notifications, etc.), which has proven effective. Aligning NetworkComponent with a similar pattern would create a more consistent and robust architecture.

The goal is to evolve NetworkComponent into a lean, protocol-agnostic base class and provide network capabilities via composable mixins, starting with Modbus TCP.

2. Proposed Design

The proposed solution is inspired by the Persistent mixin and involves two main steps:

  1. Refactor NetworkComponent to remove hard-coded Modbus TCP logic.
  2. Create a WithModbus mixin class that encapsulates the extracted Modbus TCP functionality.

This approach ensures that existing component implementations remain source-compatible.

2.1. Refactored NetworkComponent (Core)

The core NetworkComponent will be simplified to a generic manager for NetworkValues, without any knowledge of specific network protocols.

lib/polymech-base/src/modbus/NetworkComponent.h (Proposed Changes)

#ifndef NETWORK_COMPONENT_H
#define NETWORK_COMPONENT_H

#include "Component.h"
#include <vector>
#include "NetworkValue.h"

// The template parameter N (number of NetworkValues) is kept for consistency.
template <size_t N = 20>
class NetworkComponent : public Component {
protected:
    std::vector<NetworkValueBase *> _networkValues;
    NetworkValue<bool> m_enabled;

public:
    // The constructor no longer takes a Modbus-specific 'baseAddress'.
    template <typename... Args>
    NetworkComponent(Args &&... args)
        : Component(std::forward<Args>(args)...),
          m_enabled(this, this->id, "Enabled", /*...initial values...*/),
          // No modbus blocks are initialized here.
    {
        _networkValues.reserve(N);
        // Default capability is removed. It will be added by the feature mixin.
        // setNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS); 
        addNetworkValue(&m_enabled);
    }

    virtual ~NetworkComponent() = default; // Destructor simplifies.

    // Generic setup remains, but Modbus-specific calls are gone.
    short setup() override {
        Component::setup();
        // The m_enabled.initModbus() call is removed.
        return E_OK;
    }

    // Generic loop logic.
    short loop() override {
        Component::loop();
        if (!this->enabled()) {
            return E_OK;
        }
        return loopNetwork();
    }

    virtual short loopNetwork() { return E_OK; }

    void addNetworkValue(NetworkValueBase *nv) {
        if (nv) {
            _networkValues.push_back(nv);
        }
    }

    // onMessage can be simplified if it only contained protocol-specific logic.
    short onMessage(int id, E_CALLS verb, E_MessageFlags flags, void* user, Component *src) override {
        // If the Protobuf logic is also moved to a feature, this can be cleaner.
        if (verb == E_CALLS::EC_PROTOBUF_UPDATE && user != nullptr) {
            return this->owner->onMessage(id, verb, flags, user, src);
        }
        return Component::onMessage(id, verb, flags, user, src);
    }
};

// Convenience macros for NetworkValues can be moved to the Modbus mixin
// if they are only used in that context.
// #define INIT_NETWORK_VALUE(...)
// #define SETUP_NETWORK_VALUE(...)

#endif // NETWORK_COMPONENT_H

The virtual functions for Modbus (mb_tcp_write, mb_tcp_read, etc.) remain in the base Component class with default implementations. The refactored NetworkComponent simply inherits them without providing an implementation.

2.2. The WithModbus Mixin

A new header will define the WithModbus mixin. This class inherits from its template parameter TBase (which will be a NetworkComponent instantiation) and re-introduces all the Modbus TCP logic.

lib/polymech-base/src/mixins/WithModbus.h (New File)

#ifndef MIXIN_WITH_MODBUS_H
#define MIXIN_WITH_MODBUS_H

#include "modbus/NetworkComponent.h"
#include "modbus/ModbusTCP.h"

template <class TBase, size_t N>
class WithModbus : public TBase {
protected:
    uint16_t _baseAddress;
    MB_Registers* _modbusBlocks;
    mutable ModbusBlockView _modbusBlockView;
    size_t _nextIndex;
    ModbusTCP* modbusTCP;

public:
    template <typename... Args>
    WithModbus(uint16_t baseAddress, Args&&... args)
        : TBase(std::forward<Args>(args)...),
          _baseAddress(baseAddress),
          _modbusBlocks(new MB_Registers[N]()),
          _modbusBlockView{_modbusBlocks, static_cast<int>(N)},
          _nextIndex(0),
          modbusTCP(nullptr)
    {
        // Add the Modbus capability flag.
        this->setNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS);
    }

    virtual ~WithModbus() {
        delete[] _modbusBlocks;
    }

    // The Modbus-specific parts of setup() are now here.
    short setup() override {
        TBase::setup();
        this->m_enabled.initModbus(_baseAddress + E_NVC_ENABLED, 1, this->id, this->slaveId, FN_WRITE_COIL, "Enabled", this->name.c_str());
        this->m_enabled.initNotify(true, true, NetworkValue_ThresholdMode::DIFFERENCE);
        registerBlock(this->m_enabled.getRegisterInfo());
        return E_OK;
    }

    // All Modbus-related methods are implemented here.
    MB_Registers* registerBlock(const MB_Registers& reg) {
        if (_nextIndex >= N) { /* ... error handling ... */ return nullptr; }
        _modbusBlocks[_nextIndex] = reg;
        return &_modbusBlocks[_nextIndex++];
    }

    // --- Component virtual overrides ---
    ModbusBlockView* mb_tcp_blocks() const override {
        _modbusBlockView.count = _nextIndex;
        return const_cast<ModbusBlockView*>(&_modbusBlockView);
    }

    void mb_tcp_register(ModbusTCP* manager) override {
        if (!this->hasNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS)) return;
        this->modbusTCP = manager;
        // ... (rest of the original implementation)
    }

    uint16_t mb_tcp_base_address() const override { return _baseAddress; }

    short mb_tcp_read(MB_Registers *reg) override {
        if (reg->startAddress == (_baseAddress + E_NVC_ENABLED)) {
            return this->enabled() ? 1 : 0;
        }
        // Chain up to allow base class to handle other reads if needed.
        return TBase::mb_tcp_read(reg); 
    }

    short mb_tcp_write(MB_Registers *reg, short value) override {
        if (reg->startAddress == (_baseAddress + E_NVC_ENABLED)) {
            this->enable(value != 0);
            this->m_enabled.update(value != 0);
            return E_OK;
        }
        return TBase::mb_tcp_write(reg, value);
    }
};

// Helper macros can be defined here as they are specific to the Modbus context
#define SETUP_NETWORK_VALUE(nv_member, reg_offset_enum, fn_code, desc, ...) \
    do { \
        (nv_member).initNotify(__VA_ARGS__); \
        (nv_member).initModbus( \
            _baseAddress + static_cast<uint16_t>(reg_offset_enum), \
            1, \
            this->id, \
            this->slaveId, \
            fn_code, \
            desc, \
            this->name.c_str() \
        ); \
        registerBlock((nv_member).getRegisterInfo()); \
    } while(0)

#endif // MIXIN_WITH_MODBUS_H

3. Usage and Compatibility

To maintain source compatibility, a component that previously inherited from NetworkComponent will now inherit from the WithModbus mixin, which wraps the core NetworkComponent.

Example: Relay Component

Relay.h (Before)

#include "modbus/NetworkComponent.h"
//...
class Relay : public NetworkComponent<RELAY_MB_COUNT> { /*...*/ };

Relay.h (After)

#include "modbus/NetworkComponent.h" // The new lean version
#include "mixins/WithModbus.h"       // The new mixin
//...

// Define a type that represents the classic NetworkComponent behavior.
template<size_t N>
using ModbusNetworkComponent = WithModbus<NetworkComponent<N>, N>;

// Relay's inheritance changes, but the rest of its interface and implementation
// remains IDENTICAL. The constructor signature doesn't change.
class Relay : public ModbusNetworkComponent<RELAY_MB_COUNT> {
public:
    // ... same as before
    Relay(
        Component *owner,
        short _pin,
        short _id,
        short _modbusAddress); // Signature is compatible!
    // ...
};

The constructor in Relay.cpp will call the WithModbus constructor, which has the exact same signature as the old NetworkComponent constructor, ensuring full compatibility.

4. Future Extensibility

This design provides a clear template for adding other network protocols. For instance, to add a "Serial" feature:

  1. Create a WithSerial mixin class similar to WithModbus.
  2. Implement the serial communication logic and necessary Component virtual overrides within it.
  3. A component could then use WithSerial<NetworkComponent<...>> or even compose features: WithModbus<WithSerial<NetworkComponent<...>>>.

This modular approach ensures that NetworkComponent remains a lightweight and stable base, while features can be developed and composed independently.