firmware-base/docs/mb-script-testing.md

197 lines
10 KiB
Markdown

# Modbus Logic Engine (MB_SCRIPT) Functional Testing Strategy
## 1. Objective
To verify the functional correctness of the `ModbusLogicEngine` component (enabled by the `ENABLE_MB_SCRIPT` define). This includes:
* Correct configuration of logic rules via Modbus.
* Accurate evaluation of rule conditions based on Modbus register/coil values.
* Proper execution of defined actions (writing registers/coils, calling component methods).
* Correct reporting of rule status, trigger timestamps, and trigger counts via Modbus.
* Handling of various error conditions.
## 2. Scope
This strategy focuses on black-box functional testing from the perspective of a Modbus client interacting with the device. It does not cover unit testing of individual `ModbusLogicEngine` methods or performance/stress testing (which may have separate test plans).
## 3. Approach
Testing will primarily utilize the existing `npm` scripts which wrap Python helper scripts to interact with the device over Modbus TCP.
* **Configuration:** Rules will be configured by writing to the specific Modbus holding registers allocated to the Logic Engine (`MODBUS_LOGIC_RULES_START` and subsequent offsets defined in `src/ModbusLogicEngine.h`).
* **Triggering:** Rule conditions will be triggered by writing appropriate values to the source Modbus holding registers or coils specified in the rule's condition.
* **Verification:**
* Action outcomes will be verified by reading the target Modbus holding registers or coils.
* Rule status, timestamps, and trigger counts will be verified by reading the corresponding status registers for the rule.
* Internal behavior and potential errors can be monitored using device logs (`npm run build:monitor` or `npm run debug:serial`).
* **No New Scripts:** This strategy aims to use only the pre-existing `npm` scripts for Modbus interaction.
## 4. Tools
* **Modbus Read/Write:**
* `npm run modbus:read:holding -- --address <addr>`
* `npm run modbus:write:holding -- --address <addr> --value <val>`
* `npm run modbus:read:coil -- --address <addr>`
* `npm run modbus:write:coil -- --address <addr> --value <0|1>`
* **Logging:**
* `npm run build:monitor` (Live serial monitoring)
* `npm run debug:serial` (Fetch buffered logs via REST API)
* **Reference:**
* `src/ModbusLogicEngine.h` (for register offsets, enums)
* `src/config-modbus.h` (for `MODBUS_LOGIC_RULES_START` address)
## 5. Prerequisites
* Firmware compiled with `#define ENABLE_MB_SCRIPT` in `config.h` and flashed to the ESP32.
* Device connected to the network and accessible via its IP address or mDNS name (`modbus-esp32.local` by default).
* Python environment configured correctly to run the scripts in the `scripts/` directory.
* Knowledge of the Modbus register mapping for the Logic Engine (starting address + offsets).
## 6. Test Cases
Let `RULE_START = MODBUS_LOGIC_RULES_START`.
Let `RULE_0_BASE = RULE_START`.
Let `RULE_1_BASE = RULE_START + LOGIC_ENGINE_REGISTERS_PER_RULE`.
Offsets are defined in `ModbusLogicEngineOffsets`.
*(Note: Choose suitable, otherwise unused Modbus addresses for source/target registers/coils for testing)*
**TC 1: Basic Rule - Write Holding Register**
1. **Configure:**
* Write `1` to `RULE_0_BASE + ENABLED`.
* Write `3` (REG_HOLDING) to `RULE_0_BASE + COND_SRC_TYPE`.
* Write `2000` to `RULE_0_BASE + COND_SRC_ADDR`.
* Write `0` (EQUAL) to `RULE_0_BASE + COND_OPERATOR`.
* Write `123` to `RULE_0_BASE + COND_VALUE`.
* Write `3` (WRITE_HOLDING_REGISTER) to `RULE_0_BASE + COMMAND_TYPE`.
* Write `2001` to `RULE_0_BASE + COMMAND_TARGET`.
* Write `456` to `RULE_0_BASE + COMMAND_PARAM1` (Value to write).
* Write `0` to `RULE_0_BASE + COMMAND_PARAM2` (Unused).
* Write `0` to `RULE_0_BASE + FLAGS`.
2. **Trigger:** Write `123` to Modbus address `2000`.
3. **Verify:**
* Read address `2001`. Expected: `456`.
* Read `RULE_0_BASE + LAST_STATUS`. Expected: `0` (MB_Error::Success).
* Read `RULE_0_BASE + TRIGGER_COUNT`. Expected: `1`.
**TC 2: Condition Operator - Not Equal**
1. **Configure:** Similar to TC 1, but:
* Write `1` (NOT_EQUAL) to `RULE_0_BASE + COND_OPERATOR`.
* Write `123` to `RULE_0_BASE + COND_VALUE`.
2. **Trigger 1:** Write `123` to address `2000`.
3. **Verify 1:** Read address `2001`. Expected: *Unchanged* from previous state (action should *not* run).
4. **Trigger 2:** Write `124` to address `2000`.
5. **Verify 2:**
* Read address `2001`. Expected: `456`.
* Read `RULE_0_BASE + LAST_STATUS`. Expected: `0` (MB_Error::Success).
* Read `RULE_0_BASE + TRIGGER_COUNT`. Expected: Incremented.
* *(Repeat for other operators: `<`, `<=`, `>`, `>=`)*
**TC 3: Source Type - Coil**
1. **Configure:** Similar to TC 1, but:
* Write `2` (REG_COIL) to `RULE_0_BASE + COND_SRC_TYPE`.
* Write `100` (Test Coil Addr) to `RULE_0_BASE + COND_SRC_ADDR`.
* Write `1` to `RULE_0_BASE + COND_VALUE` (Condition is Coil ON).
2. **Trigger:** Write `1` to Coil address `100`.
3. **Verify:**
* Read address `2001`. Expected: `456`.
* Read `RULE_0_BASE + LAST_STATUS`. Expected: `0` (MB_Error::Success).
**TC 4: Action Type - Write Coil**
1. **Configure:** Similar to TC 1, but:
* Write `2` (WRITE_COIL) to `RULE_0_BASE + COMMAND_TYPE`.
* Write `101` to `RULE_0_BASE + COMMAND_TARGET`.
* Write `1` to `RULE_0_BASE + COMMAND_PARAM1` (Value: ON).
* Write `0` to `RULE_0_BASE + COMMAND_PARAM2` (Unused).
2. **Trigger:** Write `123` to address `2000`.
3. **Verify:**
* Read Coil address `101`. Expected: `1`.
* Read `RULE_0_BASE + LAST_STATUS`. Expected: `0` (MB_Error::Success).
**TC 5: Action Type - Call Component Method (Requires Setup)**
* **Prerequisite:** A method must be registered with the `ModbusLogicEngine` that has a verifiable side-effect readable via Modbus. Example: A simple method in `PHApp` that increments a counter stored in a Modbus register (`e.g., address 3000`).
```cpp
// In PHApp.h (or a test component)
short testMethod(short p1, short p2) {
testMethodCounter += p1; // Use p1 (arg1 from rule)
Log.infoln("Test Method Called! Arg1=%d, Counter: %d", p1, testMethodCounter);
return E_OK;
}
uint16_t testMethodCounter = 0;
// In PHApp::setup() or where ModbusLogicEngine is initialized
logicEngine->registerMethod(this->id, 1, // Use app ID and method ID 1
std::bind(&PHApp::testMethod, this, std::placeholders::_1, std::placeholders::_2));
// In PHApp::readNetworkValue()
if (address == 3000) return testMethodCounter;
```
1. **Configure:** Similar to TC 1, but:
* Write `100` (CALL_COMPONENT_METHOD) to `RULE_0_BASE + COMMAND_TYPE`.
* Write `app->id` to `RULE_0_BASE + COMMAND_TARGET` (Component ID).
* Write `1` to `RULE_0_BASE + COMMAND_PARAM1` (Method ID).
* Write `5` to `RULE_0_BASE + COMMAND_PARAM2` (Argument 1).
2. **Initial Read:** Read address `3000`. Note the value (e.g., `X`).
3. **Trigger:** Write `123` to address `2000`.
4. **Verify:**
* Read address `3000`. Expected: `X + 5`.
* Read `RULE_0_BASE + LAST_STATUS`. Expected: `0` (MB_Error::Success).
* Check logs (`build:monitor`) for "Test Method Called! Arg1=5".
**TC 6: Rule Enable/Disable**
1. **Configure:** Configure rule as in TC 1.
2. **Disable:** Write `0` to `RULE_0_BASE + ENABLED`.
3. **Trigger:** Write `123` to address `2000`.
4. **Verify (Disabled):** Read address `2001`. Expected: *Unchanged*. Trigger count should *not* increment.
5. **Enable:** Write `1` to `RULE_0_BASE + ENABLED`.
6. **Trigger:** Write `123` to address `2000`.
7. **Verify (Enabled):** Read address `2001`. Expected: `456`. Trigger count *should* increment.
**TC 7: Error - Invalid Condition Source Address**
1. **Configure:** Similar to TC 1, but:
* Write `9999` (Invalid/Unregistered address) to `RULE_0_BASE + COND_SRC_ADDR`.
2. **Trigger:** Let the engine loop run.
3. **Verify:** Read `RULE_0_BASE + LAST_STATUS`. Expected: `2` (MB_Error::IllegalDataAddress).
**TC 8: Error - Invalid Action Target Address (Write)**
1. **Configure:** Similar to TC 1, but:
* Write `9998` (Invalid/Unregistered address) to `RULE_0_BASE + COMMAND_TARGET`.
2. **Trigger:** Write `123` to address `2000`.
3. **Verify:** Read `RULE_0_BASE + LAST_STATUS`. Expected: `4` (MB_Error::ServerDeviceFailure).
**TC 9: Error - Invalid Action Target Method (Call)**
1. **Configure:** Similar to TC 5, but:
* Write `99` (Non-existent Method ID) to `RULE_0_BASE + COMMAND_PARAM1`.
2. **Trigger:** Write `123` to address `2000`.
3. **Verify:** Read `RULE_0_BASE + LAST_STATUS`. Expected: `2` (MB_Error::IllegalDataAddress).
**TC 10: Status/Counter Reset**
1. **Configure & Trigger:** Perform TC 1.
2. **Verify Count:** Read `RULE_0_BASE + TRIGGER_COUNT`. Expected: `1` (or current count).
3. **Reset Counter:** Write `0` to `RULE_0_BASE + TRIGGER_COUNT`.
4. **Verify Reset:** Read `RULE_0_BASE + TRIGGER_COUNT`. Expected: `0`.
5. **Trigger Again:** Write `123` to address `2000`.
6. **Verify Increment:** Read `RULE_0_BASE + TRIGGER_COUNT`. Expected: `1`.
**TC 11: Debug Flag**
1. **Configure:** Configure as in TC 1, but:
* Write `RULE_FLAG_DEBUG` (value 1) to `RULE_0_BASE + FLAGS`.
2. **Trigger:** Write `123` to address `2000`.
3. **Verify:**
* Check logs (`build:monitor`). Expected: Verbose logs like "MLE Eval [0]: ...", "MLE Action [0]: ...".
* Read address `2001`. Expected: `456`.
**TC 12: Receipt Flag**
1. **Configure:** Configure as in TC 1, but:
* Write `RULE_FLAG_RECEIPT` (value 2) to `RULE_0_BASE + FLAGS`.
2. **Trigger:** Write `123` to address `2000`.
3. **Verify:**
* Check logs (`build:monitor`). Expected: Info log "MLE: Rule 0 action successful.".
* Read address `2001`. Expected: `456`.
**TC 13: Debug + Receipt Flags**
1. **Configure:** Configure as in TC 1, but:
* Write `RULE_FLAG_DEBUG | RULE_FLAG_RECEIPT` (value 3) to `RULE_0_BASE + FLAGS`.
2. **Trigger:** Write `123` to address `2000`.
3. **Verify:**
* Check logs (`build:monitor`). Expected: Both verbose debug logs and the receipt log.
* Read address `2001`. Expected: `456`.