14 KiB
Modbus Logic Engine (MB_SCRIPT) Design
1. Overview
The Modbus Logic Engine (ModbusLogicEngine class, enabled by #define ENABLE_MB_SCRIPT) provides a way to implement simple automation rules directly on the device, configured and monitored via Modbus. It allows users to define rules based on the state of Modbus registers or coils (conditions) and trigger actions like writing to other registers/coils or calling internal component methods.
This enables reactive logic without requiring an external controller polling and writing values constantly.
2. Modbus Register Layout
Each logic rule occupies a contiguous block of Modbus holding registers. The number of rules is defined by MAX_LOGIC_RULES (default 8) and the number of registers per rule by LOGIC_ENGINE_REGISTERS_PER_RULE (currently 13). The starting address for the first rule is defined by MODBUS_LOGIC_RULES_START.
Register Layout per Rule:
graph TD
subgraph Rule N
Reg0[Offset 0: ENABLED (0/1)]
Reg1[Offset 1: COND_SRC_TYPE (E_RegType)]
Reg2[Offset 2: COND_SRC_ADDR]
Reg3[Offset 3: COND_OPERATOR (ConditionOperator)]
Reg4[Offset 4: COND_VALUE]
Reg5[Offset 5: COMMAND_TYPE (CommandType)]
Reg6[Offset 6: COMMAND_TARGET (Addr/CompID)]
Reg7[Offset 7: COMMAND_PARAM1 (Value/MethodID)]
Reg8[Offset 8: COMMAND_PARAM2 (Arg1)]
Reg9[Offset 9: FLAGS (Debug/Receipt)]
Reg10[Offset 10: LAST_STATUS (MB_Error)]
Reg11[Offset 11: LAST_TRIGGER_TS (Lower 16bit)]
Reg12[Offset 12: TRIGGER_COUNT]
end
style Reg0 fill:#f9f,stroke:#333,stroke-width:2px
style Reg1 fill:#f9f,stroke:#333,stroke-width:2px
style Reg2 fill:#f9f,stroke:#333,stroke-width:2px
style Reg3 fill:#f9f,stroke:#333,stroke-width:2px
style Reg4 fill:#f9f,stroke:#333,stroke-width:2px
style Reg5 fill:#f9f,stroke:#333,stroke-width:2px
style Reg6 fill:#f9f,stroke:#333,stroke-width:2px
style Reg7 fill:#f9f,stroke:#333,stroke-width:2px
style Reg8 fill:#f9f,stroke:#333,stroke-width:2px
style Reg9 fill:#f9f,stroke:#333,stroke-width:2px
style Reg10 fill:#ccf,stroke:#333,stroke-width:2px
style Reg11 fill:#ccf,stroke:#333,stroke-width:2px
style Reg12 fill:#ccf,stroke:#333,stroke-width:2px
Register Descriptions (Offsets defined in ModbusLogicEngineOffsets):
-
Configuration Registers (Read/Write via Modbus):
ENABLED(0):0= Disabled,1= Enabled.COND_SRC_TYPE(1): Type of the Modbus entity to check for the condition. UsesRegisterState::E_RegTypeenum values (e.g.,REG_HOLDING = 3,REG_COIL = 2).COND_SRC_ADDR(2): Modbus address of the register/coil for the condition.COND_OPERATOR(3): Comparison operator to use. UsesConditionOperatorenum values (e.g.,EQUAL = 0,NOT_EQUAL = 1, ...).COND_VALUE(4): The value to compare the source register/coil against.COMMAND_TYPE(5): The action to perform if the condition is met. UsesCommandTypeenum values (e.g.,WRITE_COIL = 2,WRITE_HOLDING_REGISTER = 3,CALL_COMPONENT_METHOD = 100).COMMAND_TARGET(6): Target of the command.- For
WRITE_*: The Modbus address to write to. - For
CALL_COMPONENT_METHOD: TheComponent::idof the target component.
- For
COMMAND_PARAM1(7): First parameter for the command.- For
WRITE_*: The value to write. - For
CALL_COMPONENT_METHOD: ThemethodIdregistered withModbusLogicEngine::registerMethod.
- For
COMMAND_PARAM2(8): Second parameter for the command.- For
WRITE_*: Unused. - For
CALL_COMPONENT_METHOD: The first argument (arg1) passed to the registered method.
- For
FLAGS(9): Bit flags for rule behavior (See Section 7).
-
Status Registers (Read-Only via Modbus, except TRIGGER_COUNT reset):
LAST_STATUS(10): Result of the last evaluation/action attempt for this rule. UsesMB_Errorenum values (e.g.,Success = 0,IllegalDataAddress = 2,ServerDeviceFailure = 4).LAST_TRIGGER_TS(11): Timestamp (lower 16 bits ofmillis()/1000) of the last successful trigger.TRIGGER_COUNT(12): Counter of successful triggers. Can be reset by writing0.
3. Data Structures
LogicRuleStruct:- Holds the internal representation of a single rule.
config[9]: An array storing the 9 configuration registers (Offset 0 to 8) read from/written to Modbus.lastStatus(typeRuleStatus/MB_Error): Internal state variable reflecting the last status.lastTriggerTimestamp(typeuint32_t): Internal state variable for the full timestamp.triggerCount(typeuint16_t): Internal state variable for the trigger count.- Helper methods (
isEnabled(),getCondSourceType(),getCommandType(),getFlags(), etc.) provide convenient access to the values stored in theconfigarray.
std::vector<LogicRule> rules: Member variable inModbusLogicEngineholding all configured rules.std::map<uint32_t, CallableMethod> callableMethods: Member variable storing methods registered viaregisterMethod, keyed by(componentId << 16) | methodId.
4. Core Logic Flow
sequenceDiagram
participant ModbusClient
participant ModbusManager
participant ModbusLogicEngine as MLE
participant TargetComponent
Note over ModbusClient, MLE: Initialization
PHApp->>MLE: constructor(app)
PHApp->>MLE: setup()
MLE->>MLE: rules.resize(MAX_LOGIC_RULES)
Note over MLE: Methods registered via registerMethod()
PHApp->>MLE: registerMethod(compID, methodID, function)
MLE->>MLE: callableMethods.insert(...)
loop Application Loop
PHApp->>MLE: loop()
alt Not Initialized or Interval Not Met
MLE-->>PHApp: return E_OK
else Rule Evaluation
MLE->>MLE: For each rule in rules vector
alt Rule Enabled?
MLE->>MLE: evaluateCondition(rule)
Note over MLE: Reads condition source
MLE->>ModbusManager: findComponentForAddress(condAddr)
ModbusManager-->>MLE: TargetComponent*
MLE->>TargetComponent: readNetworkValue(condAddr)
TargetComponent-->>MLE: currentValue (or error)
Note over MLE: Performs comparison
alt Condition Met?
MLE->>MLE: performAction(rule)
opt CommandType == WRITE_*
Note over MLE: Performs write action
MLE->>ModbusManager: findComponentForAddress(targetAddr)
ModbusManager-->>MLE: TargetComponent*
MLE->>TargetComponent: writeNetworkValue(targetAddr, value)
TargetComponent-->>MLE: E_OK (or error)
MLE->>MLE: updateRuleStatus(rule, MB_Error::Success / ServerDeviceFailure)
opt CommandType == CALL_COMPONENT_METHOD
Note over MLE: Performs method call
MLE->>MLE: callableMethods.find(key)
alt Method Found?
MLE->>TargetComponent: Execute registered std::function(arg1)
TargetComponent-->>MLE: E_OK (or error)
MLE->>MLE: updateRuleStatus(rule, MB_Error::Success / OpExecutionFailed)
else Method Not Found
MLE->>MLE: updateRuleStatus(rule, MB_Error::IllegalDataAddress)
end
end
MLE->>MLE: update timestamp & trigger count
else Condition Not Met
MLE->>MLE: updateRuleStatus(rule, MB_Error::Success) if not error
end
else Rule Disabled
MLE->>MLE: Skip rule
end
end
end
Note over ModbusClient, MLE: Configuration/Status Access
ModbusClient->>ModbusManager: Read/Write Request (Holding Registers)
ModbusManager->>MLE: readNetworkValue(addr) / writeNetworkValue(addr, val)
MLE->>MLE: getRuleInfoFromAddress(addr)
alt Read Request
MLE->>MLE: Access rule.config[] or internal status
MLE-->>ModbusManager: value / status
else Write Request
MLE->>MLE: rule.setConfigValue(offset, val) / rule.triggerCount = 0
MLE-->>ModbusManager: E_OK / error
end
ModbusManager-->>ModbusClient: Response
setup(): Initializes therulesvector based onMAX_LOGIC_RULES.loop(): Called periodically by the main application loop (PHApp).- Checks if initialized and if the evaluation
loopIntervalhas passed. - Iterates through each
LogicRulein therulesvector. - If a rule
isEnabled(), it callsevaluateCondition(). - If
evaluateCondition()returnstrue(condition met), it callsperformAction().
- Checks if initialized and if the evaluation
evaluateCondition():- Retrieves condition parameters (source type, address, operator, value) from the rule.
- Calls
readConditionSourceValue()to get the current value of the source register/coil. - Performs the comparison based on the
condOperator. - Updates
rule.lastStatus(e.g.,MB_Error::IllegalDataAddresson read failure,MB_Error::IllegalDataValueon invalid operator). - Returns
trueif the condition is met,falseotherwise (including errors).
performAction():- Retrieves command parameters (type, target, params) from the rule.
- Based on
commandType:- Calls
performWriteAction()forWRITE_*commands. - Calls
performCallAction()forCALL_COMPONENT_METHOD.
- Calls
- Updates
rule.lastStatusbased on the success/failure of the action (e.g.,MB_Error::Success,MB_Error::ServerDeviceFailure,MB_Error::OpExecutionFailed,MB_Error::IllegalFunction). - If successful, updates
rule.lastTriggerTimestampand incrementsrule.triggerCount. - Returns
trueon success,falseon failure.
5. Interaction with ModbusManager
The ModbusLogicEngine relies on the ModbusManager (accessed via the PHApp* app pointer) to interact with other components' Modbus values.
readConditionSourceValue():- Takes the source type (
RegisterState::E_RegType) and address. - Uses
app->modbusManager->findComponentForAddress(address)to find the component responsible for that address. - Calls the target component's
readNetworkValue(address)method. - Returns the value or indicates failure.
- Takes the source type (
performWriteAction():- Takes the command type (
WRITE_COILorWRITE_HOLDING_REGISTER), target address, and value. - Uses
app->modbusManager->findComponentForAddress(address)to find the target component. - Calls the target component's
writeNetworkValue(address, value)method. - Returns
trueif the write call returnsE_OK,falseotherwise.
- Takes the command type (
6. Method Calling (CALL_COMPONENT_METHOD)
This command type allows rules to trigger C++ methods within other components.
- Registration: Components (like
PHAppor custom components) that want to expose methods to the Logic Engine must callModbusLogicEngine::registerMethod(componentId, methodId, method).componentId: The uniqueComponent::idof the component exposing the method.methodId: A unique ID (within that component) for the specific method being exposed.method: Astd::function<short(short, short)>object wrapping the actual C++ method (often created usingstd::bind). The function should accept twoshortarguments and returnE_OK(or another error code).- The engine stores this registration in the
callableMethodsmap.
- Configuration: A rule is configured with
COMMAND_TYPE = CALL_COMPONENT_METHOD.COMMAND_TARGETis set to thecomponentId.COMMAND_PARAM1is set to themethodId.COMMAND_PARAM2is set to the value to be passed as the first argument (arg1) to the registered C++ method.
- Execution (
performCallAction()):- Combines
componentId(from Target) andmethodId(from Param1) into a key. - Looks up the key in the
callableMethodsmap. - If found, executes the stored
std::function, passingparam2(asarg1) and a dummy0(asarg2) to the bound C++ method. - Updates status based on the return value of the C++ method (
MB_Error::SuccessifE_OK,MB_Error::OpExecutionFailedotherwise) orMB_Error::IllegalDataAddressif the method was not found in the map.
- Combines
Note: This std::function-based registration is internal to the ModbusLogicEngine and separate from the Bridge mechanism used for serial commands.
7. Flags (FLAGS Register - Offset 9)
The FLAGS register allows modifying rule behavior using bitmasks:
RULE_FLAG_DEBUG(Bit 0 / Value 1): If set, enables verbose logging (Log.verboseln) during the evaluation and action phases for this specific rule, showing details like addresses, values, and outcomes.RULE_FLAG_RECEIPT(Bit 1 / Value 2): If set, logs an informational message (Log.infoln) whenever the rule's action is executed successfully.
These flags can be combined (e.g., value 3 enables both).
8. Modbus Interface (read/writeNetworkValue)
- The
ModbusLogicEngineimplementsreadNetworkValueandwriteNetworkValueas required by theComponentbase class. - These methods are called by
ModbusManagerwhen a Modbus client reads/writes registers within the engine's address range (MODBUS_LOGIC_RULES_STARTonwards). - They calculate the
ruleIndexandoffsetfrom the requested Modbus address. - Read:
- If the offset corresponds to a configuration register (0-8), it returns the value from
rule.config[offset]. - If the offset is
FLAGS(9), it returns the flag value viagetFlags(). - If the offset corresponds to a status register (10-12), it returns the value from the internal state variables (
rule.lastStatus,rule.lastTriggerTimestamp & 0xFFFF,rule.triggerCount).
- If the offset corresponds to a configuration register (0-8), it returns the value from
- Write:
- If the offset corresponds to a configuration register (0-9, including FLAGS), it updates
rule.config[offset]usingrule.setConfigValue(). - If the offset is
TRIGGER_COUNT(12) and the value is0, it resetsrule.triggerCount. - Writes to read-only status registers (10, 11) are disallowed and return an error.
- If the offset corresponds to a configuration register (0-9, including FLAGS), it updates