Found while commissioning a field box: triggers created on a plugin-provided entity pass validation and return 201, but never fire. Verified end to end against a gateway running a protocol plugin (Beckhoff ADS) that auto-registers entities whose data points are backed by gateway-published /plc/... topics.
1. Trigger on a plugin entity is accepted but never fires (bug)
Repro (any plugin that registers entities with topic-backed data points):
- Plugin registers entity
twincat_runtime_device with data points; the gateway publishes their values as topics, e.g. /plc/main/counter (std_msgs/Float64, updated every poll cycle).
POST /api/v1/apps/twincat_runtime_device/triggers with {"resource": "/api/v1/apps/twincat_runtime_device/data/counter", "trigger_condition": {"condition_type": "OnChange"}, "multishot": true}.
- Response is 201, status active. The SSE event stream stays silent forever while the underlying topic visibly changes every second.
Log at create time:
[WARN] [rest_server]: Could not resolve resource_path '/counter' to a ROS 2 topic for entity 'twincat_runtime_device'. Topic subscription will be attempted when topic becomes available.
The late binding never happens (the topic already exists, so nothing ever re-attempts).
Root cause: the trigger create path resolves the data point via cache.get_entity_data(entity_id).topics (trigger_handlers.cpp, "resolve the resource_path URI segment to a full ROS 2 topic name"). For plugin entities that cache has no topics - the /plc/* topics are attributed to the gateway's own node entity (ros2_medkit_gateway), not to the plugin entity that owns the data points. The data-point-exists validation and the topic resolution consult two different registries that disagree about entity ownership.
Proof of the mismatch: the identical trigger created on the gateway node entity works immediately:
POST /api/v1/apps/ros2_medkit_gateway/triggers
{"resource": "/api/v1/apps/ros2_medkit_gateway/data/counter", "trigger_condition": {"condition_type": "OnChange"}, "multishot": true}
-> TriggerTopicSubscriber: subscribed handle 'trig_handle_1' to '/plc/main/counter' and SSE events arrive on every value change.
A trigger that validates, returns active, and silently does nothing is the worst failure mode for an operator - they only find out when the alarm they relied on never comes.
Related symptom, likely same wiring gap: GET /api/v1/apps/<plugin-entity>/data returns 404 {"message": "No data provider for plugin entity '<id>'"} for the same entities whose data points the trigger validation happily enumerates.
2. Data point naming error is unhelpful for namespaced symbols (enhancement)
Plugin symbols are registered under the sanitized leaf name only: MAIN.counter -> data point counter. An operator naturally types the source notation and gets:
data point 'MAIN.counter' does not exist on app 'twincat_runtime_device' (available: _task_oid_plc_task, ..., adsigrp_sym_uploadinfo, ...)
Two problems:
- The mapping is deterministic, so the error could simply suggest it: "did you mean 'counter'?" (strip namespace, sanitize, check).
- The available-list truncation is alphabetical and cut the list exactly before the entry the operator needed (
counter sorts right after the ~25 adsigrp_* system symbols that dominate the prefix of the list).
Environment
- Gateway image
ghcr.io/selfpatch/ros2_medkit-jazzy@sha256:230895ea... (main @ bbd9944 era), ROS 2 Jazzy, plugin API version 7
- Entity tree: component
twincat_runtime with plugin apps (main, gvl, global_variables, twincat_runtime_device, ...); ~220 auto-discovered data points; topics under /plc/<namespace>/<leaf>
- Condition types verified:
OnChange works, on_change / change rejected (PascalCase only, matching docs)
Found while commissioning a field box: triggers created on a plugin-provided entity pass validation and return 201, but never fire. Verified end to end against a gateway running a protocol plugin (Beckhoff ADS) that auto-registers entities whose data points are backed by gateway-published
/plc/...topics.1. Trigger on a plugin entity is accepted but never fires (bug)
Repro (any plugin that registers entities with topic-backed data points):
twincat_runtime_devicewith data points; the gateway publishes their values as topics, e.g./plc/main/counter(std_msgs/Float64, updated every poll cycle).POST /api/v1/apps/twincat_runtime_device/triggerswith{"resource": "/api/v1/apps/twincat_runtime_device/data/counter", "trigger_condition": {"condition_type": "OnChange"}, "multishot": true}.Log at create time:
The late binding never happens (the topic already exists, so nothing ever re-attempts).
Root cause: the trigger create path resolves the data point via
cache.get_entity_data(entity_id).topics(trigger_handlers.cpp, "resolve the resource_path URI segment to a full ROS 2 topic name"). For plugin entities that cache has no topics - the/plc/*topics are attributed to the gateway's own node entity (ros2_medkit_gateway), not to the plugin entity that owns the data points. The data-point-exists validation and the topic resolution consult two different registries that disagree about entity ownership.Proof of the mismatch: the identical trigger created on the gateway node entity works immediately:
->
TriggerTopicSubscriber: subscribed handle 'trig_handle_1' to '/plc/main/counter'and SSE events arrive on every value change.A trigger that validates, returns active, and silently does nothing is the worst failure mode for an operator - they only find out when the alarm they relied on never comes.
Related symptom, likely same wiring gap:
GET /api/v1/apps/<plugin-entity>/datareturns404 {"message": "No data provider for plugin entity '<id>'"}for the same entities whose data points the trigger validation happily enumerates.2. Data point naming error is unhelpful for namespaced symbols (enhancement)
Plugin symbols are registered under the sanitized leaf name only:
MAIN.counter-> data pointcounter. An operator naturally types the source notation and gets:Two problems:
countersorts right after the ~25adsigrp_*system symbols that dominate the prefix of the list).Environment
ghcr.io/selfpatch/ros2_medkit-jazzy@sha256:230895ea...(main @ bbd9944 era), ROS 2 Jazzy, plugin API version 7twincat_runtimewith plugin apps (main,gvl,global_variables,twincat_runtime_device, ...); ~220 auto-discovered data points; topics under/plc/<namespace>/<leaf>OnChangeworks,on_change/changerejected (PascalCase only, matching docs)