firmware-base/docs/casting.md

24 KiB

Component Inheritance and Casting Strategy

1. The Problem

The Loadcell component requires functionality from both RTU_Base (for RS485 communication) and NetworkComponent (for Modbus TCP exposure). Both of these base classes inherit from Component.

This creates a "diamond inheritance" pattern:

      Component
      /       \
RTU_Base   NetworkComponent
      \       /
       Loadcell

This leads to two critical issues in C++:

  1. Ambiguous Base Class: Any code trying to access members of Component through a Loadcell pointer (e.g., loadcell->id) is ambiguous. Does it access RTU_Base::Component or NetworkComponent::Component?
  2. Casting Failures: The standard solution to ambiguity is virtual inheritance. However, if Component is a virtual base, downcasting from a Component* to a derived class pointer (e.g., OmronE5*) using static_cast becomes illegal. Since the project does not use RTTI, dynamic_cast is not available as an alternative.

This document outlines the viable and non-viable options to resolve this.

2. Option A: Virtual Inheritance (Non-Viable)

This is the classic C++ solution for the diamond problem.

  • Concept: Have RTU_Base and NetworkComponent inherit virtual public Component. This ensures that Loadcell receives only one shared instance of the Component base class, resolving the ambiguity.

  • Why it Fails: This approach makes it impossible to safely downcast. Code elsewhere in the project iterates through a list of Component* and casts them to specific types like OmronE5*.

    // This becomes illegal if Component is a virtual base
    OmronE5* omron = static_cast<OmronE5*>(component_ptr);
    

    The C++ standard forbids static_cast for downcasting from a virtual base because the memory layout is not known at compile time. The only standard-compliant tool for this is dynamic_cast, which requires RTTI.

  • Conclusion: Due to the hard requirement of downcasting Component pointers without RTTI, this option is not feasible.

This option redesigns the relationship to avoid multiple inheritance of Component.

  • Concept: Instead of Loadcell "being a" NetworkComponent, it will "have a" network helper. We will refactor NetworkComponent into a helper class that does not inherit from Component. Loadcell will continue to inherit from RTU_Base (and thus Component), but will contain the new network helper as a member variable.

  • Implementation Steps:

    1. Create a new template class, NetworkHelper<N>, from the code in NetworkComponent.
    2. NetworkHelper will not inherit from Component.
    3. The constructor for NetworkHelper will take a Component* owner to get access to id, name, slaveId, etc.
    4. Modify Loadcell to have a member variable: NetworkHelper<6> _netHelper;.
    5. The Loadcell constructor will initialize _netHelper, passing this as the owner.
    6. Loadcell will implement network-related virtual functions (like mb_tcp_blocks) by forwarding the calls to its _netHelper member.
    7. The INIT_NETWORK_VALUE macro must be updated to work with this new structure, or a new macro must be created.
  • Pros:

    • Solves the Problem: Completely avoids the diamond inheritance and eliminates all ambiguity and casting issues.
    • Clear Relationship: Arguably, a component "having" network capabilities is a clearer design than it "being" a network interface.
    • Safe: No unsafe casting or compiler-specific tricks are needed.
  • Cons:

    • Refactoring Effort: NetworkComponent must be refactored. Any other components that currently inherit from it (like NetworkValueTest) will also need to be updated to use the new NetworkHelper via composition.
    • Boilerplate Code: Components like Loadcell will need to explicitly forward calls to their helper member, adding a small amount of boilerplate code.

4. Option C: Component-centric Feature System

This is a more advanced alternative that moves the "feature" concept from NetworkValue directly into the Component base class.

  • Concept: Instead of creating helper classes or using multiple inheritance, the Component class itself would be designed to be composed of optional features. Any component could be configured to include capabilities like Modbus, ValueNotify, etc.

  • Implementation Sketch:

    1. Generalize Feature Classes: The NV_Modbus, NV_Logging, and NV_Notify classes would be refactored into generic, standalone "Feature" classes that do not inherit from Component.
    2. Adapt Component: The Component class would be modified to aggregate these features. This could be done in two ways:
      • Compile-Time (Templates): Component could become a variadic template class that inherits from the features it's given. This is powerful but creates a cascade of templates through all inheriting classes (RTU_Base, Loadcell, etc.), significantly increasing complexity.
        template<typename... Features>
        class Component : public Features... { /* ... */ };
        
        // Usage would be complex:
        class Loadcell : public RTU_Base<Feature_Modbus, ...> {};
        
      • Runtime (Composition): Component could hold pointers to optional feature objects, allocated on the heap. This avoids the template complexity but introduces dynamic memory management.
        class Component {
            std::vector<IFeature*> _features;
        public:
            template<typename F, typename... Args>
            void addFeature(Args&&... args) {
                _features.push_back(new F(std::forward<Args>(args)...));
            }
        };
        
    3. Update Loadcell: Loadcell would inherit from RTU_Base as usual. In its constructor, it would call this->addFeature<Feature_Modbus>(...) and this->addFeature<Feature_ValueNotify<uint32_t>>(...) to gain the required functionality.
  • Pros:

    • Ultimate Flexibility: Provides a unified way to add any capability to any component.
    • Clean Hierarchy: Completely avoids all inheritance problems. Loadcell has a clear, single inheritance path from Component via RTU_Base.
    • No Boilerplate Forwarding: The Component base class could manage calling loop() or setup() on all its registered features automatically.
  • Cons:

    • Highest Refactoring Effort: This is a fundamental change to the core Component class and would require updating every component in the system.
    • Architectural Complexity: Designing a robust and easy-to-use feature system is a significant undertaking. The choice between compile-time templates and runtime pointers has major implications for code complexity and memory usage.

5. Option D: Targeted Static Casting (Minimalist Fix)

This option resolves the ambiguity only at the specific lines of code where the compiler errors occur, without changing any class definitions.

  • Concept: Use static_cast to explicitly tell the compiler which inheritance path to take when converting a Loadcell* to a Component* or when accessing a member of Component. We can choose either RTU_Base or NetworkComponent as the intermediate path. Since RTU_Base is the primary functional parent, it's the more logical choice.

  • Implementation Steps:

    1. Fixing components.push_back(loadCell_0): In src/PHApp.cpp, cast the loadCell_0 pointer to an RTU_Base* before adding it to the vector. This resolves the ambiguity of which Component base to use.
      // src/PHApp.cpp
      components.push_back(static_cast<RTU_Base*>(phApp->loadCell_0));
      
    2. Fixing loadCell_0->owner = rs485: In src/RS485Devices.cpp, cast the pointer to RTU_Base* before accessing the owner member.
      // src/RS485Devices.cpp
      static_cast<RTU_Base*>(phApp->loadCell_0)->owner = rs485;
      
  • Pros:

    • Minimal Change: Requires modifying only the two lines of code that produce compiler errors. It is by far the least invasive solution.
    • No Class Refactoring: Avoids any changes to Component, RTU_Base, NetworkComponent, or Loadcell.
    • No Performance Overhead: static_cast is a compile-time operation with zero runtime cost.
  • Cons:

    • Brittle & Localized: This is a tactical patch, not a strategic architectural solution. It fixes these two errors but doesn't solve the underlying diamond inheritance problem. If Loadcell is used in any other ambiguous context in the future, that new code will also fail to compile and will require a similar explicit cast.
    • Technical Debt: This approach knowingly papers over a design flaw. It can make the code harder to reason about for future developers who will have to understand why this specific component needs special casting.

6. Option E: Virtual Inheritance + Manual RTTI (LLVM-style dyn_cast)

This option revisits virtual inheritance (Option A) and solves its downcasting problem by implementing a manual, type-safe casting function.

  • Concept: The Component base class already has a type member that serves as a manual run-time type identifier. We can use this to create a dyn_cast function that checks this type before performing a safe static_cast. This avoids the need for the compiler's RTTI and dynamic_cast.

  • Implementation Steps:

    1. Enable Virtual Inheritance: First, implement Option A. RTU_Base and NetworkComponent must inherit virtual public Component. This solves the diamond inheritance ambiguity.

    2. Ensure Type ID is Set: Every class that inherits from Component must set its type member correctly in its constructor (e.g., type = COMPONENT_TYPE::COMPONENT_TYPE_LOADCELL;).

    3. Implement dyn_cast: Create a dyn_cast template function that checks the component's type before casting.

      // in a new component_cast.h
      template<typename To, typename From>
      To* dyn_cast(From* src) {
          using T = std::remove_pointer_t<To>;
          if (!src) return nullptr;
          if (src->type == T::COMPONENT_TYPE_ENUM) {
              return static_cast<To*>(src);
          }
          return nullptr;
      }
      
      // Note: This requires each castable class (e.g., OmronE5)
      // to expose its type enum, for example:
      // class OmronE5 : public RTU_Base {
      // public:
      //   static const COMPONENT_TYPE COMPONENT_TYPE_ENUM = COMPONENT_TYPE::COMPONENT_TYPE_PID;
      //   OmronE5() { type = COMPONENT_TYPE_ENUM; }
      //   ...
      // };
      
    4. Replace Casts: Replace the static_cast calls that were failing in src/PHApp.cpp with the new dyn_cast.

      // src/PHApp.cpp
      if (auto* omron = dyn_cast<OmronE5>(comp)) {
          omron->setConsumption(...);
      }
      
  • Pros:

    • "Correct" Inheritance: Allows for a true "is-a" relationship using standard C++ virtual inheritance, which correctly models the single Component base.
    • Type Safe: The dyn_cast check prevents incorrect casts at runtime.
    • Centralized Logic: The casting logic is contained within the dyn_cast function, not scattered across the application.
  • Cons:

    • Requires Discipline: Developers must remember to set the type and COMPONENT_TYPE_ENUM for every new component.
    • Two-Step Refactor: Requires implementing virtual inheritance first and then fixing the resulting casting issues. This is more involved than the targeted patch of Option D.
    • slaveId Ambiguity: The slaveId member exists in both Component and RTU_Base. Virtual inheritance will not solve this ambiguity; it must be resolved manually (e.g., by removing it from one of the classes or renaming it).

7. Recommendation

Option B (Composition over Inheritance) remains the most robust long-term solution. It correctly models the relationships between the classes ("has-a" vs "is-a") and eliminates the root cause of all ambiguity problems without manual workarounds.

However, if maintaining the "is-a" relationship with multiple inheritance is a design goal, then Option E (Virtual Inheritance + dyn_cast) is the most architecturally sound way to achieve it. It is superior to the simple static_cast patches of Option D because it correctly solves the underlying single-base-instance problem first, and then provides a safe way to handle the consequences.

Updated Recommendation:

  • For a quick, low-impact fix that accepts some technical debt: Choose Option D.
  • For the most robust, unambiguous, and maintainable architecture: Choose Option B.
  • For a "correct" multiple inheritance architecture: Choose Option E. It is a valid but more involved alternative to Option B.

8. Option G: Refactoring RTU_Base into a Feature Mixin

This section outlines the strategy for refactoring RTU_Base into a NC_Rtu feature mixin for NetworkComponent. This will create a unified base class for all Modbus components, solving the diamond inheritance problem. The documentation will cover the concept, implementation details, a refactoring example using OmronE5, and a thorough analysis of the pros and cons of this advanced architectural approach.

8.1. Concept: A Single, Powerful Base Class

The core idea is to treat "being a Modbus RTU client" not as a base class to inherit from, but as a capability or feature that a NetworkComponent can possess. This elevates NetworkComponent to be the single, unified base for any component that needs to communicate over Modbus, whether it's acting as a TCP server endpoint, an RTU client, or both.

8.2. How It Would Work: The NC_Rtu Feature

We would introduce a new feature class, analogous to NV_Logging or NV_Modbus from NetworkValue, but for NetworkComponent itself.

  1. Create NC_Rtu: A new feature class, NC_Rtu, would be created. This class would not inherit from Component. It would contain the logic currently found in RTU_Base, such as:

    • The addMandatoryReadBlock() method.
    • The logic to queue write operations with the global ModbusRTU manager.
    • State tracking for RTU communication (timeouts, errors, etc.).
  2. Enhance NetworkComponent: NetworkComponent would be modified to optionally inherit from NC_Rtu based on a template parameter or feature flag.

    • It would gain the onRegisterUpdate(uint16_t address, uint16_t newValue) virtual function, which would be called by the NC_Rtu feature when new data arrives from the bus.
    • The NetworkComponent would manage a pointer to the global ModbusRTU instance, passing it to the NC_Rtu feature upon initialization.

8.3. Blueprint: Refactoring OmronE5

The OmronE5 component provides a clear "after" picture for this architecture.

Current (Problematic) Structure:

// Inherits from RTU_Base, which creates a diamond with NetworkComponent
class OmronE5 : public RTU_Base { /* ... */ };

Proposed Unified Structure:

// A template parameter could enable the RTU feature
template <size_t N, bool HasRtu = false>
class NetworkComponent : public Component, public maybe<HasRtu, NC_Rtu> 
{ /* ... */ };

// OmronE5 inherits from ONE base class and declares its needs
class OmronE5 : public NetworkComponent<OMRON_TCP_BLOCK_COUNT, /* HasRtu = */ true> 
{
public:
    OmronE5(Component* owner, uint8_t slaveId, ...) 
        : NetworkComponent(owner, ...)
    {
        // Initialize the RTU feature with its slaveId
        this->initRtuFeature(slaveId); 
    }

    short setup() override {
        // TCP setup is handled by NetworkComponent's base setup
        NetworkComponent::setup(); 

        // Add RTU read blocks. This method now comes from the NC_Rtu mixin.
        this->addMandatoryReadBlock(0x0000, 6, FN_READ_HOLD_REGISTER);
        
        // ... other setup ...
        return E_OK;
    }

    // This gets called by the NC_Rtu feature when data arrives from the bus
    void onRegisterUpdate(uint16_t address, uint16_t newValue) override {
        // Handle RTU register updates here...
    }

    // TCP methods remain unchanged
    short mb_tcp_read(MB_Registers* reg) override { /* ... */ }
    short mb_tcp_write(MB_Registers* reg, short value) override { /* ... */ }
    uint16_t mb_tcp_base_address() const override { /* ... */ }
};

8.4. Analysis of the Unified Approach

This model represents a highly integrated and powerful design.

Pros:

  • Solves Diamond Inheritance: It completely eliminates the diamond inheritance problem by establishing a single, clear inheritance path for all networked components.
  • Reduces Boilerplate: Components like OmronE5 and Loadcell become much simpler. They no longer need to manually implement forwarding to a helper class; they simply inherit the required functionality from their single NetworkComponent base.
  • True "Is-A" Relationship: A component truly "is a" NetworkComponent, with all the capabilities that entails, rather than having to delegate to helper objects.

Cons:

  • "God Class" Risk: This approach concentrates a significant amount of responsibility into NetworkComponent, making it a large and complex class that handles both TCP server logic and RTU client logic. This can make the base class harder to understand and maintain.
  • High Refactoring Cost: This is the most invasive refactoring option. It requires fundamentally redesigning NetworkComponent and RTU_Base, and would necessitate updating every single component that currently uses either of them.
  • Blurred Responsibilities: It merges the very different concerns of being a passive TCP endpoint and an active RTU client into a single class, which can be seen as a violation of the Single Responsibility Principle.

8.5. Conclusion on Option F

Option F is an architecturally elegant but pragmatically challenging solution. It offers the cleanest public API for components like OmronE5 and Loadcell, but at the cost of creating a highly complex base class and requiring a system-wide refactoring effort. It should be considered a long-term architectural goal, to be approached with caution after the more immediate problems are solved using the less invasive compositional approach (Option B).

9. Final Architecture: A Unified NetworkComponent with Composable Features

This section details the definitive architectural solution that resolves the diamond inheritance problem by refactoring NetworkComponent into a versatile, feature-based class. This approach draws inspiration from the design of NetworkValue, using template-based composition to mix in required capabilities like Modbus TCP and RTU, rather than relying on multiple inheritance or helper objects.

9.1. Core Concept: From Inheritance to Feature Composition

The fundamental shift is to stop treating network protocols as base classes to inherit from. Instead, they become features that are composed at compile time into a single, lean NetworkComponent base.

  1. A Lean NetworkComponent Base: The NetworkComponent class will be refactored into a variadic template class. It will inherit from Component and also from any feature classes passed to it as template arguments.
  2. Standalone Feature Classes (NC_ModbusTcp, NC_ModbusRtu):
    • The logic from the current NetworkComponent will be extracted into a feature class named NC_ModbusTcp.
    • The logic from RTU_Base will be extracted into a feature class named NC_ModbusRtu.
    • Crucially, these feature classes will not inherit from Component. They are pure feature mixins. They will be given an owner pointer to the NetworkComponent instance to access shared data (id, name, etc.).

9.2. Architecture Diagram

This diagram illustrates the final proposed architecture. NetworkComponent acts as an aggregator, composing the TCP and RTU features. Loadcell then inherits this aggregated functionality.

classDiagram
    direction LR
    Component <|-- NetworkComponent
    NetworkComponent <|-- Loadcell
    
    class Component {
        +id
        +name
        +owner
        +enabled()
    }

    class NetworkComponent {
        <<template (T...)>>
        +mb_tcp_register()
        +onRegisterUpdate()
    }

    NetworkComponent --o NC_ModbusTcp : composes
    NetworkComponent --o NC_ModbusRtu : composes

    class NC_ModbusTcp {
        <<feature>>
        #registerBlock()
        #mb_tcp_read()
        #mb_tcp_write()
    }
    class NC_ModbusRtu {
        <<feature>>
        #addMandatoryReadBlock()
        #onRegisterUpdate()
    }
    class Loadcell {
        +getWeight()
    }

9.3. Blueprint: Refactoring Loadcell

This code demonstrates how Loadcell would be implemented under the new architecture.

  1. Refactored NetworkComponent Header (Conceptual):

    // In a new file, e.g., features/NC_ModbusTcp.h
    class NC_ModbusTcp { /* ... TCP logic ... */ };
    
    // In a new file, e.g., features/NC_ModbusRtu.h
    class NC_ModbusRtu { /* ... RTU logic ... */ };
    
    // The new NetworkComponent.h
    #include "features/NC_ModbusTcp.h"
    #include "features/NC_ModbusRtu.h"
    
    template <typename... Features>
    class NetworkComponent : public Component, public Features... {
    public:
        NetworkComponent(Component* owner, ...) : Component(owner, ...) {
            // Pass 'this' pointer to each feature
            (Features::initFeature(this), ...);
        }
        // ... common network component logic ...
    };
    
  2. Loadcell Implementation: The Loadcell class becomes significantly cleaner. It declares its needs through the template arguments of its base class.

    // In Loadcell.h
    #include "modbus/NetworkComponent.h"
    #include "features/NC_ModbusTcp.h"
    #include "features/NC_ModbusRtu.h"
    
    class Loadcell : public NetworkComponent<NC_ModbusTcp, NC_ModbusRtu>
    {
    public:
        Loadcell(Component* owner, uint8_t slaveId, ...);
    
        short setup() override;
        short loop() override;
    
        // This is an RTU-specific callback, now part of the class interface
        void onRegisterUpdate(uint16_t address, uint16_t newValue) override;
    
        // These are TCP-specific methods, also part of the class interface
        short mb_tcp_read(MB_Registers* reg) override;
        short mb_tcp_write(MB_Registers* reg, short value) override;
        uint16_t mb_tcp_base_address() const override;
    
        // ... other Loadcell methods
    };
    
    // In Loadcell.cpp
    Loadcell::Loadcell(Component* owner, uint8_t slaveId, ...)
        : NetworkComponent(owner, ...) // Base class constructor
    {
        // Feature constructors (if any) are handled by NetworkComponent
        this->setRtuSlaveId(slaveId); // Method inherited from NC_ModbusRtu feature
    }
    
    short Loadcell::setup() {
        NetworkComponent::setup(); // Calls setup on all features
    
        // RTU setup
        addMandatoryReadBlock(...); 
    
        // TCP setup
        registerBlock(...); 
    
        return E_OK;
    }
    

9.4. Final Recommendation

This unified, feature-based composition model is the recommended architectural direction. It is the most robust, flexible, and scalable solution, completely eliminating the diamond inheritance problem while providing a clean and expressive API for component development. While it represents a significant refactoring effort, the long-term benefits to code clarity, maintainability, and extensibility are substantial.