Skip to content

Config-less discovery: device-derived identity, A&C faults, AccessLevel writable, runtime fault triggers#544

Open
mfaferek93 wants to merge 28 commits into
mainfrom
feat/zeroconfig-discovery
Open

Config-less discovery: device-derived identity, A&C faults, AccessLevel writable, runtime fault triggers#544
mfaferek93 wants to merge 28 commits into
mainfrom
feat/zeroconfig-discovery

Conversation

@mfaferek93

Copy link
Copy Markdown
Collaborator

Makes config-less PLC discovery production-usable and adds runtime threshold alarms.

opcua plugin

  • Component identity read from the device (DI nameplate, then BuildInfo, then endpoint fallback) - no hardcoded names.
  • Native A&C auto-subscribe when no node_map: conditions surface on /faults, auto-clear on inactive.
  • writable inferred from server AccessLevel (CurrentWrite); width-typed integer writes.
  • Config-less subnet scan auto-selects the endpoint; ODR fix (OpcuaDiscoveryConfig) + -fvisibility=hidden so multiple plugins co-load in one gateway safely.

gateway core

  • FaultTriggerEngine: runtime threshold rules on discovered data points - CRUD at /api/v1/apps/{app}/fault-triggers, JSON persistence, fires through FaultManager (freeze-frame captured), auto-clears on un-cross. Rejects rules on nonexistent points. Sibling of the existing SOVD notification /triggers (documented).
  • Components count as fault reporting sources in scoped queries; entity-scoped fault lists include occurrence data; sqlite store records confirmed_at_ns (with migration); plugin entities advertise only capabilities they serve.

Tests: full suite green (4797 gateway/fault_manager/opcua), new suites for the trigger engine, identity derivation, AccessLevel writable, confirmed_at. Verified live against an S7-1500: subnet scan -> auto-browse (57 nodes), write round-trip, trigger fault with freeze-frame.

Copilot AI review requested due to automatic review settings July 20, 2026 07:11

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 makes “config-less” PLC/OPC-UA discovery more production-ready and adds a new gateway-side runtime threshold (fault-trigger) engine. It also adjusts fault scoping and serialization so entity-scoped fault queries can include component sources and preserve occurrence metadata.

Changes:

  • OPC-UA plugin: derive component identity from device fields, enable native A&C auto-alarms in config-less mode, infer writability from AccessLevel, and harden multi-plugin co-loading with hidden symbol visibility.
  • Gateway: add FaultTriggerEngine with REST CRUD under /api/v1/apps/{app}/fault-triggers, persistence, and a dedicated evaluation loop.
  • Fault pipeline improvements: component IDs included as valid reporting sources in scoped queries; occurrence/confirmed timestamps added to storage and passed through where needed; plugin entity capabilities pruned when unsupported.

Reviewed changes

Copilot reviewed 31 out of 31 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
src/ros2_medkit_plugins/ros2_medkit_opcua/test/test_opcua_plugin.cpp Extends plugin fault scoping tests to include reporting_sources and occurrence passthrough.
src/ros2_medkit_plugins/ros2_medkit_opcua/test/test_node_map.cpp Updates default alarm-entity ID and tests identity override flowing into derived alarm entity IDs.
src/ros2_medkit_plugins/ros2_medkit_opcua/test/test_network_discovery.cpp Updates tests to the renamed OpcuaDiscoveryConfig.
src/ros2_medkit_plugins/ros2_medkit_opcua/test/test_device_identity.cpp Adds unit tests for config-less device-derived component identity derivation.
src/ros2_medkit_plugins/ros2_medkit_opcua/test/test_address_space_browser.cpp Adds tests for infer_writable behavior and AccessLevel read failures.
src/ros2_medkit_plugins/ros2_medkit_opcua/src/opcua_plugin.cpp Implements config-less identity override, auto-alarms enablement, writable logging, and fault list passthrough improvements.
src/ros2_medkit_plugins/ros2_medkit_opcua/src/opcua_client.cpp Adds AccessLevel decoding and width-typed integer writes for PLC compatibility.
src/ros2_medkit_plugins/ros2_medkit_opcua/src/network_discovery.cpp Renames discovery config type and updates parsing/constructor signatures.
src/ros2_medkit_plugins/ros2_medkit_opcua/src/device_identity.cpp Adds slugification + endpoint host fallback and component identity derivation.
src/ros2_medkit_plugins/ros2_medkit_opcua/src/address_space_browser.cpp Infers writability from server AccessLevel/UserAccessLevel during auto-browse.
src/ros2_medkit_plugins/ros2_medkit_opcua/include/ros2_medkit_opcua/opcua_plugin.hpp Switches to OpcuaDiscoveryConfig and tracks whether auto-alarms were explicitly configured.
src/ros2_medkit_plugins/ros2_medkit_opcua/include/ros2_medkit_opcua/opcua_client.hpp Adds AccessLevelInfo and read_access_level() API.
src/ros2_medkit_plugins/ros2_medkit_opcua/include/ros2_medkit_opcua/node_map.hpp Adds infer_writable, runtime set_component_identity(), and updates neutral component defaults.
src/ros2_medkit_plugins/ros2_medkit_opcua/include/ros2_medkit_opcua/network_discovery.hpp Renames DiscoveryConfig -> OpcuaDiscoveryConfig to avoid ODR collisions.
src/ros2_medkit_plugins/ros2_medkit_opcua/include/ros2_medkit_opcua/device_identity.hpp Declares ComponentIdentity + derive_component_identity().
src/ros2_medkit_plugins/ros2_medkit_opcua/include/ros2_medkit_opcua/address_space_browser.hpp Extends browse source interface with read_writable().
src/ros2_medkit_plugins/ros2_medkit_opcua/CMakeLists.txt Enables hidden visibility to reduce plugin symbol interposition/ODR collisions.
src/ros2_medkit_gateway/test/test_handler_context.cpp Updates fault-source resolution expectations to include component IDs as reporting sources.
src/ros2_medkit_gateway/test/test_fault_trigger_engine.cpp Adds unit tests for fault-trigger CRUD, validation, edge behavior, and persistence.
src/ros2_medkit_gateway/src/main.cpp Starts the fault-trigger engine after plugins are loaded.
src/ros2_medkit_gateway/src/http/rest_server.cpp Adds REST CRUD routes for fault-triggers (direct httplib registration).
src/ros2_medkit_gateway/src/http/handlers/discovery_handlers.cpp Prunes plugin entity capabilities that would dead-link to 404 when providers are absent.
src/ros2_medkit_gateway/src/gateway_node.cpp Wires the fault-trigger engine: fetch via plugin route, report/clear via FaultManager, thread loop lifecycle.
src/ros2_medkit_gateway/src/core/faults/fault_scope.cpp Treats components as valid reporting sources in scoped fault queries.
src/ros2_medkit_gateway/src/core/fault_trigger_engine.cpp Implements the engine: JSON persistence, rule evaluation, report/clear behavior.
src/ros2_medkit_gateway/README.md Documents the new /fault-triggers endpoint family and its relationship to /triggers.
src/ros2_medkit_gateway/include/ros2_medkit_gateway/gateway_node.hpp Exposes init_fault_trigger_engine() and accessor for REST layer.
src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/fault_trigger_engine.hpp Declares engine API and rule schema/validation helpers.
src/ros2_medkit_gateway/CMakeLists.txt Adds the new unit test target for the engine (links gateway_core).
src/ros2_medkit_fault_manager/test/test_sqlite_storage.cpp Adds tests for confirmed_at_ns recording and migration behavior.
src/ros2_medkit_fault_manager/src/sqlite_fault_storage.cpp Adds confirmed_at_ns column, migration, and logic to record confirmation time.

Comment thread src/ros2_medkit_gateway/src/http/rest_server.cpp Outdated
Comment thread src/ros2_medkit_gateway/src/http/rest_server.cpp
Comment thread src/ros2_medkit_gateway/src/http/rest_server.cpp Outdated
Comment thread src/ros2_medkit_gateway/README.md
Comment thread src/ros2_medkit_gateway/src/gateway_node.cpp
Comment thread src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/fault_trigger_engine.hpp Outdated
Comment thread src/ros2_medkit_plugins/ros2_medkit_opcua/src/opcua_plugin.cpp Outdated
Comment thread src/ros2_medkit_plugins/ros2_medkit_opcua/src/device_identity.cpp
Comment thread src/ros2_medkit_gateway/test/test_fault_trigger_engine.cpp
Comment thread src/ros2_medkit_gateway/test/test_fault_trigger_engine.cpp Outdated
@mfaferek93 mfaferek93 self-assigned this Jul 20, 2026
…lity hardening

Zero-config discovery now names the SOVD component from the device's own
read identity (DI nameplate manufacturer+model, else BuildInfo
ProductName, else opcua-<host>) instead of a hardcoded "OpenPLC Runtime"
default, and auto-enables native Alarms & Conditions (Server EventNotifier
i=2253) when no node map and no explicit auto_alarms block are given, so
discovered conditions surface on /api/v1/faults with a derived fault_code.

Rename the plugin's DiscoveryConfig to OpcuaDiscoveryConfig and build the
.so with -fvisibility=hidden -fvisibility-inlines-hidden so its internals
cannot interpose another plugin's symbols when the gateway dlopens all
plugins into one process (the extern "C" entry points stay exported).
Two lines in network_discovery.{hpp,cpp} failed clang-format-18
against the repo config. Formatting only, no behaviour change.
…ites

auto_browse now reads each Variable's AccessLevel/UserAccessLevel and marks
the point writable when the session's CurrentWrite bit is set (new
auto_browse.infer_writable flag, default on; explicit node_map still wins).
Integer writes probe the node's real DataType and write the matching scalar
width, so narrow types like the S7 Int (Int16) no longer get BadTypeMismatch.
Runtime threshold rules on discovered data points: cross fires a SOVD fault,
un-cross auto-clears; rules persist to JSON and are edited over REST at
/api/v1/apps/{app_id}/fault-triggers. Kept as a sibling of /triggers, which
stays the SOVD notification-trigger CRUD - the two share no schema or store.
POST /fault-triggers accepted any data_name and listed the rule as
active even when the app has no such point - a silently dead alarm.
Validate against the app's discovered points (400 lists the valid
names); unenumerable points (PLC offline) still skip the check.
Component-scoped fault queries collected only child-app FQNs, so
faults reported under the component's own id (PLC_COMMS_LOST from
bridge plugins) returned empty on /components/<id>/faults while the
instance route listed them. Always include the component id.
list_faults dropped first/last_occurred, occurrence_count and
severity_label and always served an empty source_id, so clients
rendered 'time unknown'. Pass the fields through and derive
source_id from reporting_sources.
Stamp the transition into CONFIRMED (report, reactivation and
time-based paths) into a new confirmed_at_ns column; older DBs are
migrated with 0 = unknown. Consumed by the compliance timeline,
whose records carried confirmed_at_ns null.
Plugin-owned entities advertised data/operations hrefs that 404 by
contract when the plugin registers no Data/OperationProvider - dead
links every SOVD client follows. Prune them from the detail
capability lists.
occurrence_count no longer increments on every FAILED re-report of an
active fault - only on first raise and reactivation after CLEARED.
Reactivation also resets first_occurred to the new cycle.
DataProvider/OperationProvider gain has_data/has_operations (default
true); PluginManager stops routing entities the provider cannot serve,
so capabilities stay honest. opcua: grouping component and the alarms
fallback app no longer advertise data/operations/x-plc-data they 404.
Restore the #516 guardrail broken by the PLC_COMMS_LOST scope fix: an
internal component must not claim faults reported under its bare id.
The real gap was the opcua plugin never marking its component external;
fix that instead of widening the scope grant to every component.
Re-reports of a still-active fault no longer bump occurrence_count;
the reactivation-after-CLEARED case already asserted the edge semantics.
…ssed-latch docs

fault_code is the fault store's primary key - two rules sharing it fight
over one stored fault. The latch comment claimed edge-triggered reporting;
evaluation is level-triggered with an edge-gated clear.
raw() escape hatch on the registry (documented JSON routes without a
typed DTO) so the CRUD appears in the OpenAPI spec, Swagger UI and the
endpoint list instead of being invisible to /api/v1/docs.
… registry

The docs completeness gate pins both: tags must be globally defined and
4xx responses must be response-level GenericError refs - satisfied by
not declaring 400/404 manually (the registry auto-adds them).
Publish responses are only processed inside runIterate; the poll
thread's single iterate per cycle loses the client to sync API traffic
long enough for every outstanding publish to expire in one burst, after
which AlarmCondition events silently stop. A 20 ms iterate every 100 ms
from its own thread keeps the channel serviced regardless of poll
cadence or API contention. Seen live on the DiagBox bench vs a 1505SP.
Siemens A&C mints a fresh ConditionId per activation of the same
Program_Alarm; the previous instance's heal / reconcile-clear then races
the new instance's raise and - because fault_manager keys by fault_code -
wipes the fault it just asserted (CONFIRMED then HEALED 170 us apart on
a real 1505SP, alarm invisible). Heal/clear from an instance is now
suppressed while another instance of the same fault_code is Confirmed.
…ctive

Siemens sends secondary notifications of the same condition (ack-state
transitions) without a readable ActiveState/Id; defaulting the missing
field to false healed the fault the primary notification confirmed
microseconds earlier. A notification that cannot state activity now
inherits the tracked instance's previous activity; an explicit false
still heals.
@mfaferek93
mfaferek93 force-pushed the feat/zeroconfig-discovery branch from 338e5a5 to 59945a2 Compare July 23, 2026 09:50
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.

2 participants