Adds OPC UA methods for device control#1304
Adds OPC UA methods for device control#1304idzm wants to merge 6 commits intosavushkin-r-d:masterfrom
OPC UA methods for device control#1304Conversation
Introduces `set_state`, `set_value`, `on`, and `off` methods to the OPC UA server for remote device management. These methods are conditionally executable based on the `P_IS_OPC_UA_SERVER_CONTROL` parameter and include comprehensive unit tests for validation.
There was a problem hiding this comment.
Pull request overview
This PR extends the existing OPC UA server integration by adding callable method nodes on each device object, enabling remote device control operations (set state/value and on/off) gated by the PAC_info::P_IS_OPC_UA_SERVER_CONTROL parameter.
Changes:
- Added OPC UA method nodes (
set_state,set_value,on,off) per device during device node creation. - Implemented corresponding server-side method callbacks enforcing argument validation and access control via
P_IS_OPC_UA_SERVER_CONTROL. - Added unit tests validating callback behavior for allowed/denied access and invalid argument handling; updated VS launch args to run OPC UA in
rwmode.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
PAC/common/OPCUAServer.h |
Declares new OPC UA method callbacks and a helper to attach method nodes to devices. |
PAC/common/OPCUAServer.cpp |
Registers method nodes under each device and implements callback logic for state/value and on/off. |
test/OPCUAServer_tests.cpp |
Adds unit tests covering the new method callbacks’ access control and input validation. |
.vs/launch.vs.json |
Enables OPC UA rw mode in the default Visual Studio launch configuration. |
Adds a call to `valve::evaluate()` in the `methods_callbacks` test to handle valve shutdown delays. This ensures the device state is correctly updated before verifying the results of the `off` method.
3792924 to
cfa5bcc
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #1304 +/- ##
==========================================
+ Coverage 56.00% 56.16% +0.15%
==========================================
Files 76 76
Lines 26733 26835 +102
Branches 3274 3287 +13
==========================================
+ Hits 14971 15071 +100
- Misses 11762 11764 +2 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Updates OPC UA method descriptions with Russian translations and improves code readability through line formatting. Adjusts the `evaluate_non_blocking` test parameters for stricter timing requirements and refactors method callback tests to use static calls. Also ensures immediate valve state transitions in tests by resetting the off-delay parameter.
Introduces debug-level logging for the `set_state`, `set_value`, `on`, and `off` methods in the OPC UA server. This provides better visibility into remote device management by recording the device name and the specific parameters used in each method call.
Adds a new test case for `add_device_methods` and introduces null-pointer validation checks for `method_set_state` and `method_set_value`. These additions ensure the server handles uninitialized device contexts gracefully by returning `UA_STATUSCODE_BADINTERNALERROR`.
| const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>( | ||
| std::chrono::steady_clock::now() - start ); | ||
|
|
||
| EXPECT_LT( elapsed.count(), 1000 ); | ||
| EXPECT_LT( elapsed.count(), 10 ); |
There was a problem hiding this comment.
The non-blocking timing assertion (elapsed < 10ms) is very likely to be flaky on slower/loaded CI machines, since it measures wall-clock time across multiple iterate calls and includes scheduler jitter. Consider using a significantly higher threshold (or asserting per-iteration bounds with a generous margin) so the test validates "doesn't block" without depending on sub-10ms timing.
| UA_Server_addMethodNode( server, UA_NODEID_NULL, deviceId, | ||
| UA_NODEID_NUMERIC( 0, UA_NS0ID_HASCOMPONENT ), | ||
| qn, methodAttr, method_set_state, | ||
| 1, &inputState, 0, nullptr, dev, nullptr ); |
There was a problem hiding this comment.
UA_Server_addMethodNode's return status is ignored here, so failures to create the method node would be silent and hard to diagnose. Capture the returned UA_StatusCode and handle/log/ASSERT it (and consider doing the same for the other method nodes added below).
| UA_Server_addMethodNode( server, UA_NODEID_NULL, deviceId, | |
| UA_NODEID_NUMERIC( 0, UA_NS0ID_HASCOMPONENT ), | |
| qn, methodAttr, method_set_state, | |
| 1, &inputState, 0, nullptr, dev, nullptr ); | |
| UA_StatusCode addMethodStatus = UA_Server_addMethodNode( server, | |
| UA_NODEID_NULL, deviceId, | |
| UA_NODEID_NUMERIC( 0, UA_NS0ID_HASCOMPONENT ), | |
| qn, methodAttr, method_set_state, | |
| 1, &inputState, 0, nullptr, dev, nullptr ); | |
| if ( addMethodStatus != UA_STATUSCODE_GOOD ) | |
| { | |
| fprintf( stderr, | |
| "UA_Server_addMethodNode failed for set_state: 0x%08x\n", | |
| ( unsigned int )addMethodStatus ); | |
| } |
| qn = UA_QUALIFIEDNAME_ALLOC( 1, "on" ); | ||
| UA_Server_addMethodNode( server, UA_NODEID_NULL, deviceId, | ||
| UA_NODEID_NUMERIC( 0, UA_NS0ID_HASCOMPONENT ), | ||
| qn, methodAttr, method_on, | ||
| 0, nullptr, 0, nullptr, dev, nullptr ); |
There was a problem hiding this comment.
UA_Server_addMethodNode's return status is ignored here, so failures to create the method node would be silent and hard to diagnose. Capture the returned UA_StatusCode and handle/log it.
| qn = UA_QUALIFIEDNAME_ALLOC( 1, "off" ); | ||
| UA_Server_addMethodNode( server, UA_NODEID_NULL, deviceId, | ||
| UA_NODEID_NUMERIC( 0, UA_NS0ID_HASCOMPONENT ), | ||
| qn, methodAttr, method_off, | ||
| 0, nullptr, 0, nullptr, dev, nullptr ); |
There was a problem hiding this comment.
UA_Server_addMethodNode's return status is ignored here, so failures to create the method node would be silent and hard to diagnose. Capture the returned UA_StatusCode and handle/log it.
|
|
||
| UA_Argument inputValue; | ||
| UA_Argument_init( &inputValue ); | ||
| inputValue.description = UA_LOCALIZEDTEXT_ALLOC( "en-US", |
There was a problem hiding this comment.
The argument description is Russian text but is tagged with locale "en-US". Use a matching locale (e.g., "ru-RU") or provide an English description for "en-US".
| inputValue.description = UA_LOCALIZEDTEXT_ALLOC( "en-US", | |
| inputValue.description = UA_LOCALIZEDTEXT_ALLOC( "ru-RU", |
| UA_Server_addMethodNode( server, UA_NODEID_NULL, deviceId, | ||
| UA_NODEID_NUMERIC( 0, UA_NS0ID_HASCOMPONENT ), | ||
| qn, methodAttr, method_set_value, | ||
| 1, &inputValue, 0, nullptr, dev, nullptr ); |
There was a problem hiding this comment.
UA_Server_addMethodNode's return status is ignored here, so failures to create the method node would be silent and hard to diagnose. Capture the returned UA_StatusCode and handle/log it (consistent with the other node creation calls).
| UA_Server_addMethodNode( server, UA_NODEID_NULL, deviceId, | |
| UA_NODEID_NUMERIC( 0, UA_NS0ID_HASCOMPONENT ), | |
| qn, methodAttr, method_set_value, | |
| 1, &inputValue, 0, nullptr, dev, nullptr ); | |
| UA_StatusCode res = UA_Server_addMethodNode( server, UA_NODEID_NULL, | |
| deviceId, UA_NODEID_NUMERIC( 0, UA_NS0ID_HASCOMPONENT ), | |
| qn, methodAttr, method_set_value, | |
| 1, &inputValue, 0, nullptr, dev, nullptr ); | |
| if ( res != UA_STATUSCODE_GOOD ) | |
| { | |
| printf( "OPC UA: failed to add method node 'set_value': %s\n", | |
| UA_StatusCode_name( res ) ); | |
| } |
|
|
||
| UA_Argument inputState; | ||
| UA_Argument_init( &inputState ); | ||
| inputState.description = UA_LOCALIZEDTEXT_ALLOC( "en-US", |
There was a problem hiding this comment.
The argument description is Russian text but is tagged with locale "en-US". Use a matching locale (e.g., "ru-RU") or provide an English description for "en-US" to avoid misleading clients.
| inputState.description = UA_LOCALIZEDTEXT_ALLOC( "en-US", | |
| inputState.description = UA_LOCALIZEDTEXT_ALLOC( "ru-RU", |
| dev->set_state( state ); | ||
|
|
||
| G_LOG->debug( "%s\t OPCUA_server::method_set_state( %d )", |
There was a problem hiding this comment.
These OPC UA control methods call device::set_state(), which is a no-op when the device is in manual mode (see i_DO_device::set_state). Since manual mode is documented as the mode where the device is controlled from the server, consider using device::set_cmd("S"/"ST", ...) or another server-control path that bypasses the manual-mode guard so OPC UA control works as intended in manual mode.
| dev->set_state( state ); | |
| G_LOG->debug( "%s\t OPCUA_server::method_set_state( %d )", | |
| dev->set_cmd( "ST", state ); | |
| G_LOG->debug( "%s\t OPCUA_server::method_set_state( %d )", |
|
|
||
| auto dev = static_cast<device*>( methodContext ); | ||
| const auto value = *static_cast<UA_Float*>( input[ 0 ].data ); | ||
| dev->set_value( value ); |
There was a problem hiding this comment.
These OPC UA control methods call i_AO_device::set_value(), which is a no-op in manual mode. If OPC UA is meant to act as "server" control, use device::set_cmd("V", ...) or another mechanism that bypasses the manual-mode guard so the value can actually be set while in manual mode.
| dev->set_value( value ); | |
| dev->set_cmd( "V", value ); |
| auto dev = static_cast<device*>( methodContext ); | ||
| dev->on(); |
There was a problem hiding this comment.
Calling dev->on() respects the manual-mode guard (i_DO_device::on), so in manual mode this will do nothing. If OPC UA is intended to provide server-side/manual control, consider using device::set_cmd("S"/"ST", ...) or an equivalent direct control path so the method works in manual mode.
|



Introduces
set_state,set_value,on, andoffmethods to the OPC UA server for remote device management. These methods are conditionally executable based on theP_IS_OPC_UA_SERVER_CONTROLparameter and include comprehensive unit tests for validation.