firmware-base/docs/mb-sync.md

8.4 KiB

ValueWrapper and Modbus Block Synchronization

This document outlines a streamlined mechanism for synchronizing component state with the Modbus TCP interface using the ValueWrapper class and a unifying macro, MB_REG_EX. This pattern reduces boilerplate code, improves readability, and centralizes the logic for value-change detection and notification.

Core Concepts

The system is built on three key components:

  1. ValueWrapper<T>: A template class that wraps a value of type T. It monitors the value for significant changes (based on a configurable threshold) and triggers a notification to its owner component when such a change occurs.
  2. MB_Registers: A structure that defines a Modbus register block, including its address, function code, and access permissions.
  3. MB_REG_EX / MB_REG: Macros that initialize both a ValueWrapper instance and its corresponding MB_Registers entry in a single, atomic operation within a component's constructor.

Class Relationships

The following diagram illustrates how these components relate to a custom component that uses them.

classDiagram
    class Component {
        +owner: Component*
        +id: ushort
        +name: String
        +mb_tcp_base_address() uint16_t
        +onMessage(source, call, flags, data, sender)
    }

    class ValueWrapper {
        -m_owner: Component*
        -m_value: T
        -m_threshold: T
        +update(T newValue)
        +get() T
    }

    class MB_Registers {
        +startAddress: ushort
        +count: ushort
        +type: E_FN_CODE
        +access: E_ModbusAccess
    }

    Component <|-- YourCustomComponent
    YourCustomComponent "1" *-- "N" ValueWrapper : has-a
    YourCustomComponent "1" *-- "N" MB_Registers : defines
    ValueWrapper ..> Component : notifies via onMessage

A custom component derives from Component and owns its ValueWrapper instances. When a ValueWrapper detects a change, it uses its owner pointer to send an onMessage notification, carrying the updated Modbus data.

Initialization and Runtime Flow

The entire process, from initialization to a runtime update, is designed to be efficient and straightforward.

Initialization Sequence

The MB_REG_EX or MB_REG macro is the cornerstone of the setup process. It must be called from within a component's constructor body (not the initializer list).

sequenceDiagram
    participant User as User/System
    participant C as YourCustomComponent
    participant VW as "ValueWrapper"
    participant MB as "MB_Registers[]"

    User->>C: new YourCustomComponent()
    activate C
    Note over C: Constructor calls MB_REG_EX or MB_REG
    C->>VW: new ValueWrapper(...)
    Note right of VW: Initializes with owner, threshold, and optional notification callback.
    C->>MB: MB_Registers(...)
    Note right of MB: Defines Modbus register properties (address, access, etc.).
    deactivate C

Runtime Update Sequence

During normal operation, the component's loop() method is responsible for feeding new values to the ValueWrapper. The wrapper handles the rest.

sequenceDiagram
    participant C as YourCustomComponent
    participant VW as "ValueWrapper"
    participant Owner as "Owner Component (e.g., ModbusTCP)"

    loop Component's loop()
        C->>C: Read or calculate new value
        C->>VW: update(newValue)
        activate VW
        VW->>VW: Check if |newValue - oldValue| >= threshold
        alt threshold is met AND notifications enabled
            VW->>Owner: onMessage(MB_UpdateData)
            activate Owner
            Owner->>Owner: Process update (e.g., queue Modbus message)
            deactivate Owner
            opt Post-notification callback exists
                VW->>C: post_notify_cb(newValue, oldValue)
            end
        end
        deactivate VW
    end

How to Use MB_REG and MB_REG_EX

To implement this pattern, follow these steps:

  1. Declare the ValueWrapper<T> members in your component's header file. They will be default-initialized.
  2. In the component's constructor, call the appropriate macro (MB_REG or MB_REG_EX) for each wrapped value.

Macro Signatures

MB_REG_EX (Extended)

The extended macro allows for fine-grained control over all parameters, including the slave ID and whether notifications are enabled.

MB_REG_EX(
    vw_member,             // The ValueWrapper member variable
    mb_blocks_array,       // The MB_Registers array
    vw_type,               // The data type (e.g., PlotStatus, int16_t)
    mb_reg_offset_enum,    // The register offset enum value
    mb_fn_code,            // The Modbus function code
    mb_access,             // Read/write access (MB_ACCESS_...)
    mb_slave_id,           // The slave ID
    mb_desc,               // A description string
    mb_group,              // A group name string
    vw_initial_val,        // The wrapper's initial value
    vw_threshold_val,      // The threshold for notification
    vw_threshold_mode,     // DIFFERENCE or INTERVAL_STEP
    vw_enable_notification,// true to enable notifications, false to disable
    vw_post_notify_cb      // An optional post-notification callback (or nullptr)
);

MB_REG (Simple)

The simple macro is a convenience for the most common use case. It assumes notifications are enabled and the slave ID is 1.

MB_REG(
    vw_member,             // The ValueWrapper member variable
    mb_blocks_array,       // The MB_Registers array
    vw_type,               // The data type (e.g., PlotStatus, int16_t)
    mb_reg_offset_enum,    // The register offset enum value
    mb_fn_code,            // The Modbus function code
    mb_access,             // Read/write access (MB_ACCESS_...)
    mb_desc,               // A description string
    mb_group,              // A group name string
    vw_initial_val,        // The wrapper's initial value
    vw_threshold_val,      // The threshold for notification
    vw_threshold_mode,     // DIFFERENCE or INTERVAL_STEP
    vw_post_notify_cb      // An optional post-notification callback (or nullptr)
);

Example Implementation

Here is a conceptual example based on TemperatureProfile. Since it needs to configure notification enablement, it uses MB_REG_EX.

// TemperatureProfile.h
class TemperatureProfile : public PlotBase {
    // ...
private:
    ValueWrapper<PlotStatus> _statusWrapper;
    ValueWrapper<int16_t> _currentTemperatureWrapper;
    // ...
    MB_Registers _modbusBlocks[TEMP_PROFILE_REGISTER_COUNT];
};

// TemperatureProfile.cpp
TemperatureProfile::TemperatureProfile(Component *owner, short slot, ushort componentId)
    : PlotBase(owner, componentId),
      // _statusWrapper is default-initialized here
      // ...
{
    name = "TempProfile_" + String(this->id) + "_Slot_" + String(slot);
    const char* group = name.c_str();

    // Now, initialize the wrapper and the Modbus block together
    MB_REG_EX(
        _statusWrapper,
        _modbusBlocks,
        PlotStatus,
        TemperatureProfileRegisterOffset::STATUS,
        E_FN_CODE::FN_READ_HOLD_REGISTER,
        MB_ACCESS_READ_ONLY,
        1, // slaveId
        "TProf Status",
        group,
        PlotStatus::IDLE,
        PlotStatus::RUNNING, // Threshold: notify when value becomes RUNNING
        ValueWrapper<PlotStatus>::ThresholdMode::DIFFERENCE,
        true,           // Enable notifications
        nullptr         // No post-notification callback
    );

    MB_REG_EX(
        _currentTemperatureWrapper,
        _modbusBlocks,
        int16_t,
        TemperatureProfileRegisterOffset::CURRENT_TEMP,
        E_FN_CODE::FN_WRITE_HOLD_REGISTER,
        MB_ACCESS_READ_ONLY, // This block is RO from Modbus master perspective
        1, // slaveId
        "TProf Curr Temp",
        group,
        INT16_MIN,
        1, // Threshold: notify if value changes by at least 1
        ValueWrapper<int16_t>::ThresholdMode::DIFFERENCE,
        false,          // Disable notifications for this wrapper
        nullptr
    );

    // ... initialize other blocks
}

// In the loop method
void TemperatureProfile::loop() {
    // ...
    _statusWrapper.update(getCurrentStatus());
    _currentTemperatureWrapper.update(getTemperature(getElapsedMs()));
    // ...
}

By setting vw_enable_notification to false in MB_REG_EX, the ValueWrapper still tracks the value and holds state, but its update method will not trigger an onMessage call, effectively decoupling it from the Modbus notification system while still allowing its use as a stateful wrapper.