# `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 `NetworkValue`s, without any knowledge of specific network protocols. **`lib/polymech-base/src/modbus/NetworkComponent.h` (Proposed Changes)** ```cpp #ifndef NETWORK_COMPONENT_H #define NETWORK_COMPONENT_H #include "Component.h" #include #include "NetworkValue.h" // The template parameter N (number of NetworkValues) is kept for consistency. template class NetworkComponent : public Component { protected: std::vector _networkValues; NetworkValue m_enabled; public: // The constructor no longer takes a Modbus-specific 'baseAddress'. template NetworkComponent(Args &&... args) : Component(std::forward(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)** ```cpp #ifndef MIXIN_WITH_MODBUS_H #define MIXIN_WITH_MODBUS_H #include "modbus/NetworkComponent.h" #include "modbus/ModbusTCP.h" template class WithModbus : public TBase { protected: uint16_t _baseAddress; MB_Registers* _modbusBlocks; mutable ModbusBlockView _modbusBlockView; size_t _nextIndex; ModbusTCP* modbusTCP; public: template WithModbus(uint16_t baseAddress, Args&&... args) : TBase(std::forward(args)...), _baseAddress(baseAddress), _modbusBlocks(new MB_Registers[N]()), _modbusBlockView{_modbusBlocks, static_cast(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); } 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(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)** ```cpp #include "modbus/NetworkComponent.h" //... class Relay : public NetworkComponent { /*...*/ }; ``` **`Relay.h` (After)** ```cpp #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 using ModbusNetworkComponent = WithModbus, 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 { 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>` or even compose features: `WithModbus>>`. This modular approach ensures that `NetworkComponent` remains a lightweight and stable base, while features can be developed and composed independently.