feat(opcua): read-only PLC network discovery -> auto_browse#509
Merged
Conversation
Opt-in `discovery:` block on the opcua plugin. A bounded, read-only active TCP scan finds OPC-UA servers with no pre-configured endpoint, identifies each via GetEndpoints, classifies data server vs LDS vs non-OPC-UA lead, and (single-endpoint auto mode) hands the best None/Anonymous data server to the existing connect + introspect path. Default disabled.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR adds an opt-in, read-only OPC-UA/PLC “network discovery” path to ros2_medkit_opcua, allowing the plugin to actively scan configured subnets/ports, identify OPC-UA servers via GetEndpoints, and auto-select a connectable endpoint when no explicit endpoint_url is configured.
Changes:
- Introduces discovery core (
NetworkDiscovery) with CIDR expansion, bounded concurrent port probing, OPC-UA identification, dedup/classification, and deterministic endpoint selection. - Integrates startup discovery into the OPC-UA plugin with YAML/env configuration and logging.
- Adds a focused unit test suite for discovery logic and wires it into CMake; updates plugin README docs.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/ros2_medkit_plugins/ros2_medkit_opcua/test/test_network_discovery.cpp | Adds unit tests for CIDR expansion, config parsing, discovery classification/dedup, and endpoint selection. |
| src/ros2_medkit_plugins/ros2_medkit_opcua/src/opcua_plugin.cpp | Tracks explicit endpoint configuration and runs startup discovery when enabled and not explicitly configured. |
| src/ros2_medkit_plugins/ros2_medkit_opcua/src/network_discovery.cpp | Implements discovery orchestration: CIDR expansion, task fan-out, identification, dedup, selection. |
| src/ros2_medkit_plugins/ros2_medkit_opcua/src/network_discovery_io.cpp | Implements real I/O primitives: non-blocking TCP connect probe + open62541pp GetEndpoints identify. |
| src/ros2_medkit_plugins/ros2_medkit_opcua/README.md | Documents the discovery configuration block and operational behavior. |
| src/ros2_medkit_plugins/ros2_medkit_opcua/include/ros2_medkit_opcua/opcua_plugin.hpp | Declares discovery config/state and injected I/O hooks; adds startup discovery helper. |
| src/ros2_medkit_plugins/ros2_medkit_opcua/include/ros2_medkit_opcua/network_discovery.hpp | Declares discovery types/config API and orchestrator interface. |
| src/ros2_medkit_plugins/ros2_medkit_opcua/CMakeLists.txt | Adds new sources to the plugin/test targets and introduces the new discovery unit test target. |
This was referenced Jul 9, 2026
…ssive mode - Sort/select discovered endpoints by numeric IPv4 value instead of lexicographic string compare (192.168.1.2 vs .10 sorted wrong before). - Treat an empty endpoint_url config value / OPCUA_ENDPOINT_URL env var as not configured, so discovery is no longer wrongly suppressed and the default endpoint is no longer silently cleared. - Check fcntl(F_GETFL/F_SETFL) return values in tcp_connect_probe(); bail out instead of leaving the probe socket blocking on failure. - discovery.mode == passive is now an actual no-op (matches the existing doc comment) instead of silently still running the active scan. - clang-format-18 pass on touched files (CI format-lint).
bburda
reviewed
Jul 10, 2026
Gate anonymous_none_available on an actual Anonymous UserIdentityToken, not just None/None transport, so username/cert-only servers are no longer mis-marked auto-connectable. Run the GetEndpoints identify sweep on the same bounded pool as the connect sweep (results written by index keep the deterministic ip:port order and lowest-ip dedup). Switch the TCP probe from select() to poll() to drop the FD_SETSIZE UB, and clamp scan_concurrency to 512.
bburda
approved these changes
Jul 11, 2026
mfaferek93
added a commit
that referenced
this pull request
Jul 11, 2026
Add auto_alarms to the node map: subscribe to a server EventNotifier (default Server object) and auto-derive a fault per condition with no per-alarm event_alarms mapping. fault_code from ConditionName/SourceName, falling back to a hash of SourceNode+EventType+Message so Siemens alarms sharing one SourceNode don't collapse. Filters non-condition system messages (null ConditionId). Explicit event_alarms still take precedence. Composes with #509 (discovery) and #510 (auto_browse).
mfaferek93
added a commit
that referenced
this pull request
Jul 11, 2026
Add auto_alarms to the node map: subscribe to a server EventNotifier (default Server object) and auto-derive a fault per condition with no per-alarm event_alarms mapping. fault_code from ConditionName/SourceName, falling back to a hash of SourceNode+EventType+Message so Siemens alarms sharing one SourceNode don't collapse. Filters non-condition system messages (null ConditionId). Explicit event_alarms still take precedence. Composes with #509 (discovery) and #510 (auto_browse).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #512
Summary
Opt-in, read-only OPC-UA network discovery for
ros2_medkit_opcua: a box on a plant LAN auto-finds a PLC with no configuredendpoint_urland hands the selected endpoint to the existing connect + nameplate introspect path. Disabled by default (no behaviour change).Why active scan
Validated on a real Siemens S7-1500: a stock S7-1500 neither multicast-announces (mDNS) nor registers with an LDS, so passive discovery finds nothing. A bounded read-only TCP scan of
:4840+GetEndpointsidentify is the only mechanism that discovers it.getEndpoints()works in the current open62541pp build;FindServersOnNetwork/mDNS are compiled out and deferred.What's included
discovery:config (defaultenabled: false):mode,subnets/cidr(empty => local /24),ports([4840]),connect_timeout_ms,scan_concurrency,identify_timeout_ms,interval_s(0=one-shot),anonymous_none_only; env overridesOPCUA_DISCOVERY_*.network_discovery.{hpp,cpp}): CIDR expand (rejects >/16), bounded concurrent TCP sweep, per-:4840GetEndpoints, dedup by ApplicationUri, classify (data server / LDS / non-OPC-UA lead), deterministic endpoint selection. Network I/O injected -> logic unit-tested offline. Real I/O innetwork_discovery_io.cpp.endpoint_urlis set, scan once at startup and connect to the selected None/Anonymous server's scanned ip:port (servers may advertise non-resolvable hostnames).Safety (OT)
Read-only (TCP connect +
GetEndpointsonly; no writes, no subscriptions). Explicitendpoint_urlalways wins (never a second session on a polled PLC). Secured-only servers surfaced as leads, never auto-connected. Bounded scan; CIDRs >/16 rejected.Tests
test_network_discovery(21 cases: CIDR, config validate, live scenario with fakes, dedup, URL preference, selection). Fullros2_medkit_opcuasuite green (219/0).clang-format-18clean.Hardware-tested
On the real S7-1500 LAN: discovered
.10:4840, identified the Siemens (urn:SIMATIC.S7-1500...Software PLC_1, None/Anonymous), classified the LDS at.9and used its scanned ip:port, auto-selected.10, connected and read the DeviceSet nameplate.Deferred
Recursive
auto_browsetree building (flag parsed but mapping not yet implemented; today the endpoint feeds the nameplate introspect path), multi-endpoint registration, passive sources (mDNS/LDS/FindServersOnNetwork), periodic re-scan + persistence.