Context
ros2_medkit_opcua (added in #365) requires a hand-written YAML node_map that maps OPC-UA node IDs to SOVD entities. For the tank demo this is fine (5 nodes, 1 PLC), but for a real industrial deployment a PLC can expose hundreds or thousands of tags across multiple namespaces, and writing the node map by hand is error-prone and time-consuming.
This issue tracks adding an opt-in browse mode that walks the OPC-UA address space and generates a starter node_map.yaml automatically. The integrator then edits the generated map to rename entities, group nodes under applications, set units, and mark writable nodes - far faster than writing from scratch.
Scope
Code changes
-
Add OpcuaClient::browse_address_space(BrowseConfig) -> BrowseResult that walks the OPC-UA address space using open62541pp browse services. BrowseConfig controls:
- Starting node (default:
ObjectsFolder)
- Depth limit
- Namespace filter (include / exclude list)
- Browse direction (forward / inverse / both)
- Node class filter (Variable, Object, Method, etc.)
-
Add NodeMapGenerator that takes a BrowseResult and produces a NodeMap YAML document:
- Infers
data_type from DataTypeId on each Variable node
- Groups related nodes into apps by browse hierarchy (one app per
Object node containing Variable children)
- Derives
data_name from BrowseName, sanitized to valid ROS 2 topic segments
- Sets
writable: true when the Variable has UserAccessLevel write bit
- Emits a
# TODO: comment per entry for fields the generator cannot determine (unit, min_value, max_value, alarm)
-
Expose the generator in one of two ways (discussion needed):
- Option A: new standalone CLI tool
ros2_medkit_opcua_browse (argparse-style, ros2 run ros2_medkit_opcua browse --endpoint opc.tcp://... --output tank_nodes.yaml)
- Option B: plugin parameter
plugins.opcua.auto_browse: true - when set, the plugin browses on startup and writes the generated map next to the loaded config
Option A is cleaner for a one-shot developer workflow. Option B is less typing but commingles "generate config" with "run server" which is weird. Recommend Option A.
Configuration + plugin integration
- Browse mode does not require the plugin to be running in gateway mode. It is a pure client-side OPC-UA browse operation.
- The generator should produce YAML that is a valid input for the plugin's normal
NodeMap::load so round-tripping works.
Tests
- Unit test for
NodeMapGenerator with a mocked BrowseResult fixture.
- Integration test: browse OpenPLC tank demo and verify the generated YAML contains all 5 expected nodes with correct data types.
- Integration test: compare generated YAML against the handwritten
tank_demo_nodes.yaml - most fields should match (with the expected TODO gaps for unit / alarm).
Documentation
README.md - add Quickstart: auto-generate node map section showing the CLI usage.
design/index.rst - document the browse strategy, filtering options, and the hierarchy-to-entity mapping heuristic.
Acceptance criteria
Out of scope for this issue
- Full interactive "browse wizard" with prompts
- Detection of OPC-UA Alarms & Conditions event types (use YAML
alarm: block manually)
- Automatic detection of
unit, min_value, max_value from EngineeringUnits / EURange (nice-to-have, list as follow-up)
References
Context
ros2_medkit_opcua(added in #365) requires a hand-written YAMLnode_mapthat maps OPC-UA node IDs to SOVD entities. For the tank demo this is fine (5 nodes, 1 PLC), but for a real industrial deployment a PLC can expose hundreds or thousands of tags across multiple namespaces, and writing the node map by hand is error-prone and time-consuming.This issue tracks adding an opt-in browse mode that walks the OPC-UA address space and generates a starter
node_map.yamlautomatically. The integrator then edits the generated map to rename entities, group nodes under applications, set units, and mark writable nodes - far faster than writing from scratch.Scope
Code changes
Add
OpcuaClient::browse_address_space(BrowseConfig) -> BrowseResultthat walks the OPC-UA address space usingopen62541ppbrowse services.BrowseConfigcontrols:ObjectsFolder)Add
NodeMapGeneratorthat takes aBrowseResultand produces aNodeMapYAML document:data_typefromDataTypeIdon each Variable nodeObjectnode containingVariablechildren)data_namefromBrowseName, sanitized to valid ROS 2 topic segmentswritable: truewhen the Variable hasUserAccessLevelwrite bit# TODO:comment per entry for fields the generator cannot determine (unit,min_value,max_value,alarm)Expose the generator in one of two ways (discussion needed):
ros2_medkit_opcua_browse(argparse-style,ros2 run ros2_medkit_opcua browse --endpoint opc.tcp://... --output tank_nodes.yaml)plugins.opcua.auto_browse: true- when set, the plugin browses on startup and writes the generated map next to the loaded configOption A is cleaner for a one-shot developer workflow. Option B is less typing but commingles "generate config" with "run server" which is weird. Recommend Option A.
Configuration + plugin integration
NodeMap::loadso round-tripping works.Tests
NodeMapGeneratorwith a mockedBrowseResultfixture.tank_demo_nodes.yaml- most fields should match (with the expectedTODOgaps for unit / alarm).Documentation
README.md- addQuickstart: auto-generate node mapsection showing the CLI usage.design/index.rst- document the browse strategy, filtering options, and the hierarchy-to-entity mapping heuristic.Acceptance criteria
OpcuaClient::browse_address_spacewalks the address space with depth + namespace filtersNodeMapGeneratorproduces YAML that round-trips throughNodeMap::loadwithout errorsros2 run ros2_medkit_opcua browse ...Out of scope for this issue
alarm:block manually)unit,min_value,max_valuefrom EngineeringUnits / EURange (nice-to-have, list as follow-up)References