Skip to content

feat(opcua): read-only PLC network discovery -> auto_browse#509

Merged
mfaferek93 merged 3 commits into
mainfrom
feat/opcua-network-discovery
Jul 11, 2026
Merged

feat(opcua): read-only PLC network discovery -> auto_browse#509
mfaferek93 merged 3 commits into
mainfrom
feat/opcua-network-discovery

Conversation

@mfaferek93

@mfaferek93 mfaferek93 commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

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 configured endpoint_url and 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 + GetEndpoints identify 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 (default enabled: 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 overrides OPCUA_DISCOVERY_*.
  • Discovery core (network_discovery.{hpp,cpp}): CIDR expand (rejects >/16), bounded concurrent TCP sweep, per-:4840 GetEndpoints, 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 in network_discovery_io.cpp.
  • Integration: when discovery is on and no endpoint_url is 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 + GetEndpoints only; no writes, no subscriptions). Explicit endpoint_url always 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). Full ros2_medkit_opcua suite green (219/0). clang-format-18 clean.

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 .9 and used its scanned ip:port, auto-selected .10, connected and read the DeviceSet nameplate.

Deferred

Recursive auto_browse tree 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.

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.
Copilot AI review requested due to automatic review settings July 9, 2026 15:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/ros2_medkit_plugins/ros2_medkit_opcua/src/network_discovery.cpp
Comment thread src/ros2_medkit_plugins/ros2_medkit_opcua/src/network_discovery.cpp Outdated
Comment thread src/ros2_medkit_plugins/ros2_medkit_opcua/src/network_discovery.cpp Outdated
Comment thread src/ros2_medkit_plugins/ros2_medkit_opcua/src/opcua_plugin.cpp
Comment thread src/ros2_medkit_plugins/ros2_medkit_opcua/src/opcua_plugin.cpp Outdated
…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).
@mfaferek93 mfaferek93 marked this pull request as ready for review July 10, 2026 17:06
@mfaferek93 mfaferek93 requested a review from bburda July 10, 2026 17:21
Comment thread src/ros2_medkit_plugins/ros2_medkit_opcua/src/network_discovery.cpp Outdated
Comment thread src/ros2_medkit_plugins/ros2_medkit_opcua/src/network_discovery_io.cpp Outdated
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.
@mfaferek93 mfaferek93 merged commit 72b0aa3 into main Jul 11, 2026
21 of 22 checks passed
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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ros2_medkit_opcua: read-only PLC network discovery (auto-find OPC UA endpoint)

3 participants