Last active
February 20, 2020 10:02
-
-
Save kung-foo/9db8b4f942e5fa6d8d9902acbd6ac998 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package supersecret | |
import ( | |
"github.com/pkg/errors" | |
"github.com/gopcua/opcua" | |
"github.com/gopcua/opcua/id" | |
"github.com/gopcua/opcua/ua" | |
) | |
// SkipNode should be returned from a `WalkFunc` when the caller wants to stop recursing. | |
// It is not returned by `Walk`. | |
var SkipNode = errors.New("skip this node") | |
// NodeDef represents a node in the OPC-UA tree | |
type NodeDef struct { | |
ID string | |
Parent string | |
Class string | |
BrowseName string | |
Description string | |
AccessLevel uint32 | |
Path string | |
DataType string | |
Writable bool | |
/* | |
Unit string | |
Scale string | |
Min string | |
Max string | |
*/ | |
} | |
// WalkFunc is called for every node visited by Walk | |
type WalkFunc func(node *NodeDef, err error) error | |
// NodeWalker walks the OPC-UA nodes | |
type NodeWalker struct { | |
client *opcua.Client | |
maxDepth uint | |
} | |
func NewNodeWalker(client *opcua.Client, maxDepth uint) *NodeWalker { | |
return &NodeWalker{ | |
client: client, | |
maxDepth: maxDepth, | |
} | |
} | |
// Walk walks the OPC-UA tree starting at root. fn is called for each node | |
func (w *NodeWalker) Walk(root *ua.NodeID, fn WalkFunc) error { | |
if root == nil { | |
root = ua.NewNumericNodeID(0, id.ObjectsFolder) | |
} | |
return w.walk(w.client.Node(root), nil, "", 1, fn) | |
} | |
func (w *NodeWalker) walk(node *opcua.Node, parent *opcua.Node, parentPath string, depth uint, fn WalkFunc) error { | |
def := &NodeDef{ | |
ID: node.ID.String(), | |
} | |
if parent != nil { | |
def.Parent = parent.ID.String() | |
} | |
callOnError := func(err error) error { | |
err = errors.WithStack(err) | |
e2 := fn(def, err) | |
if e2 == SkipNode { | |
return nil | |
} | |
return e2 | |
} | |
attrs, err := node.Attributes(ua.AttributeIDNodeClass, ua.AttributeIDBrowseName, ua.AttributeIDDescription, ua.AttributeIDDataType, ua.AttributeIDAccessLevel) | |
if err != nil { | |
return callOnError(err) | |
} | |
// ua.AttributeIDNodeClass | |
switch err := attrs[0].Status; err { | |
case ua.StatusOK: | |
def.Class = ua.NodeClass(attrs[0].Value.Int()).String() | |
default: | |
return callOnError(err) | |
} | |
// ua.AttributeIDBrowseName | |
switch err := attrs[1].Status; err { | |
case ua.StatusOK: | |
def.BrowseName = attrs[1].Value.String() | |
def.Path = join(parentPath, def.BrowseName) | |
default: | |
return callOnError(err) | |
} | |
// ua.AttributeIDDescription | |
switch err := attrs[2].Status; err { | |
case ua.StatusOK: | |
if attrs[2].Value != nil { | |
def.Description = attrs[2].Value.String() | |
} | |
case ua.StatusBadAttributeIDInvalid: | |
// ignore | |
default: | |
return callOnError(err) | |
} | |
// ua.AttributeIDDataType | |
switch err := attrs[3].Status; err { | |
case ua.StatusOK: | |
switch v := attrs[3].Value.NodeID().IntID(); v { | |
case id.DateTime, id.UtcTime: | |
def.DataType = "time.Time" | |
case id.Boolean: | |
def.DataType = "bool" | |
case id.SByte: | |
def.DataType = "int8" | |
case id.Int16: | |
def.DataType = "int16" | |
case id.Int32: | |
def.DataType = "int32" | |
case id.Byte: | |
def.DataType = "byte" | |
case id.UInt16: | |
def.DataType = "uint16" | |
case id.UInt32: | |
def.DataType = "uint32" | |
case id.String: | |
def.DataType = "string" | |
case id.Float: | |
def.DataType = "float32" | |
case id.Double: | |
def.DataType = "float64" | |
default: | |
def.DataType = attrs[3].Value.NodeID().String() | |
} | |
case ua.StatusBadAttributeIDInvalid: | |
// ignore | |
default: | |
return callOnError(err) | |
} | |
// ua.AttributeIDAccessLevel | |
switch err := attrs[4].Status; err { | |
case ua.StatusOK: | |
def.AccessLevel = uint32(ua.AccessLevelType(attrs[4].Value.Uint())) | |
def.Writable = def.AccessLevel&uint32(ua.AccessLevelTypeCurrentWrite) == uint32(ua.AccessLevelTypeCurrentWrite) | |
case ua.StatusBadAttributeIDInvalid: | |
// ignore | |
default: | |
return callOnError(err) | |
} | |
// invoke callback | |
if err := fn(def, nil); err != nil { | |
if err == SkipNode { | |
return nil | |
} | |
return err | |
} | |
if w.maxDepth > 0 && depth+1 > w.maxDepth { | |
return nil | |
} | |
refs, err := findReferences(node, id.Organizes, id.HasComponent) | |
if err != nil { | |
return callOnError(err) | |
} | |
for _, ref := range refs { | |
// fix up node IDs | |
// TODO(jca): remove this once `ReferencedNodes` addresses: | |
// https://github.com/gopcua/opcua/pull/200#discussion_r287770460 | |
// https://github.com/gopcua/opcua/issues/281 | |
nid, err := ua.ParseNodeID(ref.ID.String()) | |
if err != nil { | |
return err | |
} | |
ref = w.client.Node(nid) | |
// end fix | |
// recurse | |
if err := w.walk(ref, node, def.Path, depth+1, fn); err != nil { | |
return err | |
} | |
} | |
return nil | |
} | |
func findReferences(node *opcua.Node, refType ...uint32) ([]*opcua.Node, error) { | |
refs := make([]*opcua.Node, 0) | |
for _, rt := range refType { | |
r, err := node.ReferencedNodes(rt, ua.BrowseDirectionForward, ua.NodeClassAll, true) | |
if err != nil { | |
return nil, err | |
} | |
refs = append(refs, r...) | |
} | |
return refs, nil | |
} | |
func join(a, b string) string { | |
if a == "" { | |
return b | |
} | |
return a + "." + b | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment