Skip to content

Instantly share code, notes, and snippets.

@kung-foo
Last active February 20, 2020 10:02
Show Gist options
  • Save kung-foo/9db8b4f942e5fa6d8d9902acbd6ac998 to your computer and use it in GitHub Desktop.
Save kung-foo/9db8b4f942e5fa6d8d9902acbd6ac998 to your computer and use it in GitHub Desktop.
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