Skip to content

Commit

Permalink
Merge pull request #115 from united-manufacturing-hub/eng-2319
Browse files Browse the repository at this point in the history
[ENG-2319] Remove browseHierarchicalReferences flags and parameters
  • Loading branch information
kanapuli authored Jan 29, 2025
2 parents 0531a7a + b0a282d commit 86150a2
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 161 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,6 @@ input:
securityPolicy: None | Basic256Sha256 # optional (default: unset)
subscribeEnabled: false | true # optional (default: false)
useHeartbeat: false | true # optional (default: false)
browseHierarchicalReferences: false | true # optional (default: false)
pollRate: 1000 # optional (default: 1000) The rate in milliseconds at which to poll the OPC UA server when not using subscriptions
autoReconnect: false | true # optional (default: false)
reconnectIntervalInSeconds: 5 # optional (default: 5) The rate in seconds at which to reconnect to the OPC UA server when the connection is lost
Expand Down Expand Up @@ -219,7 +218,9 @@ input:
useHeartbeat: true
```

##### Browse Hierarchical References
##### Browse Hierarchical References (Option until version 0.5.2)

**NOTE**: This property is removed in version 0.6.0 and made as a standard way to browse OPCUA nodes. From version 0.6.0 onwards, opcua_plugin will browse all nodes with Hierarchical References.

The plugin offers an option to browse OPCUA nodes by following Hierarchical References. By default, this feature is disabled (`false`), which means the plugin will only browse a limited subset of reference types, including:
- `HasComponent`
Expand Down Expand Up @@ -1607,7 +1608,7 @@ Output:
],
"notes": "Replace worn nozzle to prevent defects."
}

},
"timestamp_ms": 1733903611000
}
Expand Down
123 changes: 17 additions & 106 deletions opcua_plugin/browse.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ type Logger interface {

// Browse is a public wrapper function for the browse function
// Avoid using this function directly, use it only for testing
func Browse(ctx context.Context, n NodeBrowser, path string, level int, logger Logger, parentNodeId string, nodeChan chan NodeDef, errChan chan error, wg *TrackedWaitGroup, browseHierarchicalReferences bool, opcuaBrowserChan chan NodeDef) {
browse(ctx, n, path, level, logger, parentNodeId, nodeChan, errChan, wg, browseHierarchicalReferences, opcuaBrowserChan)
func Browse(ctx context.Context, n NodeBrowser, path string, level int, logger Logger, parentNodeId string, nodeChan chan NodeDef, errChan chan error, wg *TrackedWaitGroup, opcuaBrowserChan chan NodeDef) {
browse(ctx, n, path, level, logger, parentNodeId, nodeChan, errChan, wg, opcuaBrowserChan)
}

// browse recursively explores OPC UA nodes to build a comprehensive list of NodeDefs.
Expand All @@ -90,10 +90,9 @@ func Browse(ctx context.Context, n NodeBrowser, path string, level int, logger L
// - `errChan` (`chan error`): Channel to send encountered errors for centralized handling.
// - `pathIDMapChan` (`chan map[string]string`): Channel to send the mapping of node names to NodeIDs.
// - `wg` (`*sync.WaitGroup`): WaitGroup to synchronize the completion of goroutines.
// - `browseHierarchicalReferences` (`bool`): Indicates whether to browse hierarchical references.
// **Returns:**
// - `void`: Errors are sent through `errChan`, and discovered nodes are sent through `nodeChan`.
func browse(ctx context.Context, n NodeBrowser, path string, level int, logger Logger, parentNodeId string, nodeChan chan NodeDef, errChan chan error, wg *TrackedWaitGroup, browseHierarchicalReferences bool, opcuaBrowserChan chan NodeDef) {
func browse(ctx context.Context, n NodeBrowser, path string, level int, logger Logger, parentNodeId string, nodeChan chan NodeDef, errChan chan error, wg *TrackedWaitGroup, opcuaBrowserChan chan NodeDef) {
defer wg.Done()

// Limits browsing depth to a maximum of 25 levels in the node hierarchy.
Expand Down Expand Up @@ -280,14 +279,6 @@ func browse(ctx context.Context, n NodeBrowser, path string, level int, logger L
logger.Debugf("%d: def.Path:%s def.NodeClass:%s\n", level, def.Path, def.NodeClass)
def.ParentNodeID = parentNodeId

hasNodeReferencedComponents := func() bool {
refs, err := n.ReferencedNodes(ctx, id.HasComponent, ua.BrowseDirectionForward, ua.NodeClassAll, true)
if err != nil || len(refs) == 0 {
return false
}
return true
}

browseChildrenV2 := func(refType uint32) error {
children, err := n.Children(ctx, refType, ua.NodeClassVariable|ua.NodeClassObject)
if err != nil {
Expand All @@ -296,21 +287,7 @@ func browse(ctx context.Context, n NodeBrowser, path string, level int, logger L
}
for _, child := range children {
wg.Add(1)
go browse(ctx, child, def.Path, level+1, logger, def.NodeID.String(), nodeChan, errChan, wg, browseHierarchicalReferences, opcuaBrowserChan)
}
return nil
}

browseChildren := func(refType uint32) error {
refs, err := n.ReferencedNodes(ctx, refType, ua.BrowseDirectionForward, ua.NodeClassAll, true)
if err != nil {
sendError(ctx, errors.Errorf("References: %d: %s", refType, err), errChan, logger)
return err
}
logger.Debugf("found %d child refs\n", len(refs))
for _, rn := range refs {
wg.Add(1)
go browse(ctx, rn, def.Path, level+1, logger, def.NodeID.String(), nodeChan, errChan, wg, browseHierarchicalReferences, opcuaBrowserChan)
go browse(ctx, child, def.Path, level+1, logger, def.NodeID.String(), nodeChan, errChan, wg, opcuaBrowserChan)
}
return nil
}
Expand All @@ -326,100 +303,34 @@ func browse(ctx context.Context, n NodeBrowser, path string, level int, logger L
}
// In OPC Unified Architecture (UA), hierarchical references are a type of reference that establishes a containment relationship between nodes in the address space. They are used to model a hierarchical structure, such as a system composed of components, where each component may contain sub-components.
// Refer link: https://qiyuqi.gitbooks.io/opc-ua/content/Part3/Chapter7.html
// With browseHierarchicalReferences set to true, we can browse the hierarchical references of a node which will check for references of type
// HasEventSource, HasChild, HasComponent, Organizes, FolderType, HasNotifier and all their sub-hierarchical references
// Setting browseHierarchicalReferences to true will be the new way to browse for tags and folders properly without any duplicate browsing
if browseHierarchicalReferences {
switch def.NodeClass {

// If its a variable, we add it to the node list and browse all its children
case ua.NodeClassVariable:
if err := browseChildrenV2(id.HierarchicalReferences); err != nil {
sendError(ctx, err, errChan, logger)
return
}

// adding it to the node list
def.Path = join(path, def.BrowseName)
select {
case nodeChan <- def:
case <-ctx.Done():
logger.Warnf("Failed to send node due to context cancellation")
return
}
return

// If its an object, we browse all its children but DO NOT add it to the node list and therefore not subscribe
case ua.NodeClassObject:
if err := browseChildrenV2(id.HierarchicalReferences); err != nil {
sendError(ctx, err, errChan, logger)
return
}
return
}
}

// This old way of browsing is deprecated and will be removed in the future
// This method is called only when browseHierarchicalReferences is false
browseReferencesDeprecated(ctx, def, nodeChan, errChan, path, logger, hasNodeReferencedComponents, browseChildren)
}

// browseReferencesDeprecated is the old way to browse for tags and folders without any duplicate browsing
// It browses the following references: HasComponent, HasProperty, HasEventSource, HasOrder, HasNotifier, Organizes, FolderType
func browseReferencesDeprecated(ctx context.Context, def NodeDef, nodeChan chan<- NodeDef, errChan chan<- error, path string, logger Logger, hasNodeReferencedComponents func() bool, browseChildren func(refType uint32) error) {

// If a node has a Variable class, it probably means that it is a tag
// Normally, there is no need to browse further. However, structs will be a variable on the top level,
// but it then will have HasComponent references to its children
if def.NodeClass == ua.NodeClassVariable {
switch def.NodeClass {

if hasNodeReferencedComponents() {
if err := browseChildren(id.HasComponent); err != nil {
sendError(ctx, err, errChan, logger)
return
}
// If its a variable, we add it to the node list and browse all its children
case ua.NodeClassVariable:
if err := browseChildrenV2(id.HierarchicalReferences); err != nil {
sendError(ctx, err, errChan, logger)
return
}

// adding it to the node list
def.Path = join(path, def.BrowseName)
select {
case nodeChan <- def:
case <-ctx.Done():
logger.Warnf("Failed to send node due to context cancellation")
default:
logger.Warnf("Channel is blocked, skipping node send")
return
}
return
}

// If a node has an Object class, it probably means that it is a folder
// Therefore, browse its children
if def.NodeClass == ua.NodeClassObject {
// To determine if an Object is a folder, we need to check different references
// Add here all references that should be checked

if err := browseChildren(id.HasComponent); err != nil {
// If its an object, we browse all its children but DO NOT add it to the node list and therefore not subscribe
case ua.NodeClassObject:
if err := browseChildrenV2(id.HierarchicalReferences); err != nil {
sendError(ctx, err, errChan, logger)
return
}
if err := browseChildren(id.Organizes); err != nil {
sendError(ctx, err, errChan, logger)
return
}
if err := browseChildren(id.FolderType); err != nil {
sendError(ctx, err, errChan, logger)
return
}
if err := browseChildren(id.HasNotifier); err != nil {
sendError(ctx, err, errChan, logger)
return
}
// For hasProperty it makes sense to show it very close to the tag itself, e.g., use the tagName as tagGroup and then the properties as subparts of it
/*
if err := browseChildren(id.HasProperty); err != nil {
return nil, err
}
*/
return
}
}

Expand Down Expand Up @@ -492,7 +403,7 @@ func (g *OPCUAInput) discoverNodes(ctx context.Context) ([]NodeDef, map[string]s
g.Log.Debugf("Browsing nodeID: %s", nodeID.String())
wg.Add(1)
wrapperNodeID := NewOpcuaNodeWrapper(g.Client.Node(nodeID))
go browse(timeoutCtx, wrapperNodeID, "", 0, g.Log, nodeID.String(), nodeChan, errChan, &wg, g.BrowseHierarchicalReferences, opcuaBrowserChan)
go browse(timeoutCtx, wrapperNodeID, "", 0, g.Log, nodeID.String(), nodeChan, errChan, &wg, opcuaBrowserChan)
}

go func() {
Expand Down Expand Up @@ -568,7 +479,7 @@ func (g *OPCUAInput) BrowseAndSubscribeIfNeeded(ctx context.Context) error {

wgHeartbeat.Add(1)
wrapperNodeID := NewOpcuaNodeWrapper(g.Client.Node(heartbeatNodeID))
go browse(ctx, wrapperNodeID, "", 1, g.Log, heartbeatNodeID.String(), nodeHeartbeatChan, errChanHeartbeat, &wgHeartbeat, g.BrowseHierarchicalReferences, opcuaBrowserChanHeartbeat)
go browse(ctx, wrapperNodeID, "", 1, g.Log, heartbeatNodeID.String(), nodeHeartbeatChan, errChanHeartbeat, &wgHeartbeat, opcuaBrowserChanHeartbeat)

wgHeartbeat.Wait()
close(nodeHeartbeatChan)
Expand Down
2 changes: 1 addition & 1 deletion opcua_plugin/browse_frontend.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func (g *OPCUAInput) GetNodeTree(ctx context.Context, msgChan chan<- string, roo

var wg TrackedWaitGroup
wg.Add(1)
browse(ctx, NewOpcuaNodeWrapper(g.Client.Node(rootNode.NodeId)), "", 0, g.Log, rootNode.NodeId.String(), nodeChan, errChan, &wg, g.BrowseHierarchicalReferences, opcuaBrowserChan)
browse(ctx, NewOpcuaNodeWrapper(g.Client.Node(rootNode.NodeId)), "", 0, g.Log, rootNode.NodeId.String(), nodeChan, errChan, &wg, opcuaBrowserChan)
go logBrowseStatus(ctx, nodeChan, msgChan, &wg)
go logErrors(ctx, errChan, g.Log)
go collectNodes(ctx, opcuaBrowserChan, nodeIDMap, &nodes)
Expand Down
8 changes: 0 additions & 8 deletions opcua_plugin/opcua.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ var OPCUAConfigSpec = service.NewConfigSpec().
Field(service.NewBoolField("subscribeEnabled").Description("Set to true to subscribe to OPC UA nodes instead of fetching them every seconds. Default is pulling messages every second (false).").Default(false)).
Field(service.NewBoolField("directConnect").Description("Set this to true to directly connect to an OPC UA endpoint. This can be necessary in cases where the OPC UA server does not allow 'endpoint discovery'. This requires having the full endpoint name in endpoint, and securityMode and securityPolicy set. Defaults to 'false'").Default(false)).
Field(service.NewBoolField("useHeartbeat").Description("Set to true to provide an extra message with the servers timestamp as a heartbeat").Default(false)).
Field(service.NewBoolField("browseHierarchicalReferences").Description("Set to true to browse hierarchical references. This is the new way to browse for tags and folders references properly without any duplicates. Defaults to 'false'").Default(false)).
Field(service.NewIntField("pollRate").Description("The rate in milliseconds at which to poll the OPC UA server when not using subscriptions. Defaults to 1000ms (1 second).").Default(DefaultPollRate)).
Field(service.NewBoolField("autoReconnect").Description("Set to true to automatically reconnect to the OPC UA server when the connection is lost. Defaults to 'false'").Default(false)).
Field(service.NewIntField("reconnectIntervalInSeconds").Description("The interval in seconds at which to reconnect to the OPC UA server when the connection is lost. This is only used if `autoReconnect` is set to true. Defaults to 5 seconds.").Default(5))
Expand Down Expand Up @@ -123,11 +122,6 @@ func newOPCUAInput(conf *service.ParsedConfig, mgr *service.Resources) (service.
return nil, err
}

browseHierarchicalReferences, err := conf.FieldBool("browseHierarchicalReferences")
if err != nil {
return nil, err
}

pollRate, err := conf.FieldInt("pollRate")
if err != nil {
return nil, err
Expand Down Expand Up @@ -163,7 +157,6 @@ func newOPCUAInput(conf *service.ParsedConfig, mgr *service.Resources) (service.
SessionTimeout: sessionTimeout,
DirectConnect: directConnect,
UseHeartbeat: useHeartbeat,
BrowseHierarchicalReferences: browseHierarchicalReferences,
LastHeartbeatMessageReceived: atomic.Uint32{},
LastMessageReceived: atomic.Uint32{},
HeartbeatManualSubscribed: false,
Expand Down Expand Up @@ -214,7 +207,6 @@ type OPCUAInput struct {
HeartbeatNodeId *ua.NodeID
Subscription *opcua.Subscription
ServerInfo ServerInfo
BrowseHierarchicalReferences bool
PollRate int
browseCancel context.CancelFunc
browseWaitGroup sync.WaitGroup
Expand Down
2 changes: 1 addition & 1 deletion opcua_plugin/opcua_opc-plc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1429,7 +1429,7 @@ opcua:
// Browse the node
wg.Add(1)
wrapperNodeID := NewOpcuaNodeWrapper(input.Client.Node(parsedNodeIDs[0]))
go Browse(ctx, wrapperNodeID, "", 0, input.Log, parsedNodeIDs[0].String(), nodeChan, errChan, &wg, true, opcuaBrowserChan)
go Browse(ctx, wrapperNodeID, "", 0, input.Log, parsedNodeIDs[0].String(), nodeChan, errChan, &wg, opcuaBrowserChan)

wg.Wait()
close(nodeChan)
Expand Down
24 changes: 11 additions & 13 deletions opcua_plugin/opcua_plc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,13 +316,12 @@ var _ = Describe("Test Against WAGO PLC", Serial, func() {
parsedNodeIDs := ParseNodeIDs(nodeIDStrings)

input = &OPCUAInput{
Endpoint: endpoint,
Username: "",
Password: "",
NodeIDs: parsedNodeIDs,
BrowseHierarchicalReferences: true,
AutoReconnect: true,
ReconnectIntervalInSeconds: 5,
Endpoint: endpoint,
Username: "",
Password: "",
NodeIDs: parsedNodeIDs,
AutoReconnect: true,
ReconnectIntervalInSeconds: 5,
}
// Attempt to connect
err = input.Connect(ctx)
Expand Down Expand Up @@ -355,12 +354,11 @@ var _ = Describe("Test Against WAGO PLC", Serial, func() {
parsedNodeIDs := ParseNodeIDs(nodeIDStrings)

input = &OPCUAInput{
Endpoint: endpoint,
Username: "",
Password: "",
NodeIDs: parsedNodeIDs,
SubscribeEnabled: true,
BrowseHierarchicalReferences: true,
Endpoint: endpoint,
Username: "",
Password: "",
NodeIDs: parsedNodeIDs,
SubscribeEnabled: true,
}
ctx := context.Background()
err = input.Connect(ctx)
Expand Down
Loading

0 comments on commit 86150a2

Please sign in to comment.