Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More browsing #200

Merged
merged 4 commits into from
Sep 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,20 @@ func (c *Client) Call(req *ua.CallMethodRequest) (*ua.CallMethodResult, error) {
return res.Results[0], nil
}

// BrowseNext executes a synchronous browse request.
func (c *Client) BrowseNext(req *ua.BrowseNextRequest) (*ua.BrowseNextResponse, error) {
var res *ua.BrowseNextResponse
err := c.Send(req, func(v interface{}) error {
r, ok := v.(*ua.BrowseNextResponse)
if !ok {
return fmt.Errorf("invalid response: %T", v)
}
res = r
return nil
})
return res, err
}

// Subscribe creates a Subscription with given parameters. Parameters that have not been set
// (have zero values) are overwritten with default values.
// See opcua.DefaultSubscription* constants
Expand Down
156 changes: 140 additions & 16 deletions examples/browse/browse.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,49 +6,164 @@ package main

import (
"context"
"encoding/csv"
"flag"
"fmt"
"log"
"os"
"strconv"

"github.com/gopcua/opcua"
"github.com/gopcua/opcua/debug"
"github.com/gopcua/opcua/id"
"github.com/gopcua/opcua/ua"
)

type NodeDef struct {
NodeID *ua.NodeID
NodeClass ua.NodeClass
BrowseName string
Description string
AccessLevel ua.AccessLevelType
Path string
DataType string
Writable bool
Unit string
Scale string
Min string
Max string
}

func (n NodeDef) Records() []string {
return []string{n.BrowseName, n.DataType, n.NodeID.String(), n.Unit, n.Scale, n.Min, n.Max, strconv.FormatBool(n.Writable), n.Description}
}

func join(a, b string) string {
if a == "" {
return b
}
return a + "." + b
}

func browse(n *opcua.Node, path string, level int) ([]string, error) {
func browse(n *opcua.Node, path string, level int) ([]NodeDef, error) {
// fmt.Printf("node:%s path:%q level:%d\n", n, path, level)
if level > 10 {
return nil, nil
}
// nodeClass, err := n.NodeClass()
// if err != nil {
// return nil, err
// }
browseName, err := n.BrowseName()

attrs, err := n.Attributes(ua.AttributeIDNodeClass, ua.AttributeIDBrowseName, ua.AttributeIDDescription, ua.AttributeIDAccessLevel, ua.AttributeIDDataType)
if err != nil {
return nil, err
}
path = join(path, browseName.Name)

typeDefs := ua.NewTwoByteNodeID(id.HasTypeDefinition)
refs, err := n.References(typeDefs)
if err != nil {
var def = NodeDef{
NodeID: n.ID,
}

switch err := attrs[0].Status; err {
case ua.StatusOK:
def.NodeClass = ua.NodeClass(attrs[0].Value.Int())
default:
return nil, err
}

switch err := attrs[1].Status; err {
case ua.StatusOK:
def.BrowseName = attrs[1].Value.String()
default:
return nil, err
}

switch err := attrs[2].Status; err {
case ua.StatusOK:
def.Description = attrs[2].Value.String()
case ua.StatusBadAttributeIDInvalid:
// ignore
default:
return nil, err
}

switch err := attrs[3].Status; err {
case ua.StatusOK:
def.AccessLevel = ua.AccessLevelType(attrs[3].Value.Int())
def.Writable = def.AccessLevel&ua.AccessLevelTypeCurrentWrite == ua.AccessLevelTypeCurrentWrite
case ua.StatusBadAttributeIDInvalid:
// ignore
default:
return nil, err
}
// todo(fs): example still incomplete
log.Printf("refs: %#v err: %v", refs, err)
return nil, nil

switch err := attrs[4].Status; err {
case ua.StatusOK:
switch v := attrs[4].Value.NodeID().IntID(); v {
case id.DateTime:
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.UtcTime:
def.DataType = "time.Time"
case id.String:
def.DataType = "string"
case id.Float:
def.DataType = "float32"
case id.Double:
def.DataType = "float64"
default:
def.DataType = attrs[4].Value.NodeID().String()
}
case ua.StatusBadAttributeIDInvalid:
// ignore
default:
return nil, err
}

def.Path = join(path, def.BrowseName)

var nodes []NodeDef
if def.NodeClass == ua.NodeClassVariable {
nodes = append(nodes, def)
}

browseChildren := func(refType uint32) error {
refs, err := n.ReferencedNodes(refType, ua.BrowseDirectionForward, ua.NodeClassAll, true)
if err != nil {
return fmt.Errorf("References: %d: %s", refType, err)
}
// fmt.Printf("found %d child refs\n", len(refs))
for _, rn := range refs {
children, err := browse(rn, def.Path, level+1)
if err != nil {
return fmt.Errorf("browse children: %s", err)
}
nodes = append(nodes, children...)
}
return nil
}

if err := browseChildren(id.HasComponent); err != nil {
return nil, err
}
if err := browseChildren(id.Organizes); err != nil {
return nil, err
}
return nodes, nil
}

func main() {
endpoint := flag.String("endpoint", "opc.tcp://localhost:4840", "OPC UA Endpoint URL")
nodeID := flag.String("node", "", "node id for the root node")
flag.BoolVar(&debug.Enable, "debug", false, "enable debug logging")
flag.Parse()
log.SetFlags(0)
Expand All @@ -61,13 +176,22 @@ func main() {
}
defer c.Close()

root := c.Node(ua.NewStringNodeID(1, "Root"))
id, err := ua.ParseNodeID(*nodeID)
if err != nil {
log.Fatalf("invalid node id: %s", err)
}

nodeList, err := browse(root, "", 0)
nodeList, err := browse(c.Node(id), "", 0)
if err != nil {
log.Fatal(err)
}

w := csv.NewWriter(os.Stdout)
w.Comma = ';'
hdr := []string{"Name", "Type", "Addr", "Unit (SI)", "Scale", "Min", "Max", "Writable", "Description"}
w.Write(hdr)
for _, s := range nodeList {
fmt.Println(s)
w.Write(s.Records())
}
w.Flush()
}
87 changes: 79 additions & 8 deletions node.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ func (n *Node) BrowseName() (*ua.QualifiedName, error) {
return v.Value().(*ua.QualifiedName), nil
}

// Description returns the description of the node.
func (n *Node) Description() (*ua.LocalizedText, error) {
v, err := n.Attribute(ua.AttributeIDDescription)
if err != nil {
return nil, err
}
return v.Value().(*ua.LocalizedText), nil
}

// DisplayName returns the display name of the node.
func (n *Node) DisplayName() (*ua.LocalizedText, error) {
v, err := n.Attribute(ua.AttributeIDDisplayName)
Expand Down Expand Up @@ -118,16 +127,58 @@ func (n *Node) Attribute(attrID ua.AttributeID) (*ua.Variant, error) {
return value, nil
}

func (n *Node) Attributes(attrID ...ua.AttributeID) ([]*ua.DataValue, error) {
req := &ua.ReadRequest{}
for _, id := range attrID {
rv := &ua.ReadValueID{NodeID: n.ID, AttributeID: id}
req.NodesToRead = append(req.NodesToRead, rv)
}
res, err := n.c.Read(req)
if err != nil {
return nil, err
}
return res.Results, nil
}

func (n *Node) Children(refs uint32, mask ua.NodeClass) ([]*Node, error) {
if refs == 0 {
refs = id.HierarchicalReferences
}
return n.ReferencedNodes(refs, ua.BrowseDirectionForward, mask, true)
}

func (n *Node) ReferencedNodes(refs uint32, dir ua.BrowseDirection, mask ua.NodeClass, includeSubtypes bool) ([]*Node, error) {
if refs == 0 {
refs = id.References
}
var nodes []*Node
res, err := n.References(refs, dir, mask, includeSubtypes)
if err != nil {
return nil, err
}
for _, r := range res {
nodes = append(nodes, n.c.Node(r.NodeID.NodeID))
Copy link
Collaborator

@alexbrdn alexbrdn May 27, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wrote a browse example along the same lines to explore a little and have identified a problem with using this pattern: client.Node(reference.NodeID.NodeID).
The ExpandedNodeId in reference.NodeID may have been actually expanded and contains a namespace uri and 0 as namespace index (as per the spec). Now, assigning reference.NodeID.NodeID to a Node's id copies the mask which contains (in my case) NamespaceUri flag and ServerIndex flag. Encoding this NodeID in the BrowseRequest writes 0xC0 as DataEncoding byte into the stream. And the Server responds with StatusBadDecodingError, because the stream doesn't contain the promised namespace URI and ServerIndex and actually the ExpandedNodeId is not allowed in BrowseRequest anyway.

You can test this by browsing the Prosys OPC UA Simulation Server in default configuration. Funny thing is, the namespace that gets expanded is the OPC UA core namespace and AFAICT it's only this namespace that gets expanded. Nevertheless, I see this as a problem with the API as the Reference.NodeID.NodeID allows one to get hold of a NodeID, which is has a mask a NodeID is not supposed to have.

A possible solution would be to make ExpandedNodeID.nodeID private and add a function ExpandedNodeID.NodeID() to safely convert to NodeID or return an error. One problem I see is one would need a lookup to resolve a namespace URI to an index.

Maybe this needs to be extracted into an issue of its own.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ran into this exact issue yesterday

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a super quick'n'dirty work around I used:

diff --git a/node.go b/node.go
index 5adf925..00fc9fd 100644
--- a/node.go
+++ b/node.go
@@ -156,6 +156,9 @@ func (n *Node) ReferencedNodes(refs uint32, dir ua.BrowseDirection, mask ua.Node
                return nil, err
        }
        for _, r := range res {
+               r.NodeID.NodeID.ClearIndexFlag()
+               r.NodeID.NodeID.ClearURIFlag()
+
                nodes = append(nodes, n.c.Node(r.NodeID.NodeID))
        }
        return nodes, nil
diff --git a/ua/node_id.go b/ua/node_id.go
index 8533e8c..d178f47 100644
--- a/ua/node_id.go
+++ b/ua/node_id.go
@@ -179,6 +179,10 @@ func (n *NodeID) SetURIFlag() {
        n.mask |= 0x80
 }
 
+func (n *NodeID) ClearURIFlag() {
+       n.mask &^= 0x80
+}
+
 // IndexFlag returns whether the Index flag is set in EncodingMask.
 func (n *NodeID) IndexFlag() bool {
        return n.mask&0x40 == 0x40
@@ -189,6 +193,10 @@ func (n *NodeID) SetIndexFlag() {
        n.mask |= 0x40
 }
 
+func (n *NodeID) ClearIndexFlag() {
+       n.mask &^= 0x40
+}
+
 // Namespace returns the namespace id. For two byte node ids
 // this will always be zero.
 func (n *NodeID) Namespace() uint16 {

}
return nodes, nil
}

// References returns all references for the node.
// todo(fs): this is not complete since it only returns the
// todo(fs): top-level reference at this point.
func (n *Node) References(refs *ua.NodeID) (*ua.BrowseResponse, error) {
func (n *Node) References(refType uint32, dir ua.BrowseDirection, mask ua.NodeClass, includeSubtypes bool) ([]*ua.ReferenceDescription, error) {
if refType == 0 {
refType = id.References
}
if mask == 0 {
mask = ua.NodeClassAll
}

desc := &ua.BrowseDescription{
NodeID: n.ID,
BrowseDirection: ua.BrowseDirectionBoth,
ReferenceTypeID: refs,
IncludeSubtypes: true,
NodeClassMask: uint32(ua.NodeClassAll),
BrowseDirection: dir,
ReferenceTypeID: ua.NewNumericNodeID(0, refType),
IncludeSubtypes: includeSubtypes,
NodeClassMask: uint32(mask),
ResultMask: uint32(ua.BrowseResultMaskAll),
}

Expand All @@ -136,12 +187,32 @@ func (n *Node) References(refs *ua.NodeID) (*ua.BrowseResponse, error) {
ViewID: ua.NewTwoByteNodeID(0),
Timestamp: time.Now(),
},
RequestedMaxReferencesPerNode: 1000,
RequestedMaxReferencesPerNode: 0,
NodesToBrowse: []*ua.BrowseDescription{desc},
}

return n.c.Browse(req)
// implement browse_next
resp, err := n.c.Browse(req)
if err != nil {
return nil, err
}
return n.browseNext(resp.Results)
}

func (n *Node) browseNext(results []*ua.BrowseResult) ([]*ua.ReferenceDescription, error) {
refs := results[0].References
for len(results[0].ContinuationPoint) > 0 {
req := &ua.BrowseNextRequest{
ContinuationPoints: [][]byte{results[0].ContinuationPoint},
ReleaseContinuationPoints: false,
}
resp, err := n.c.BrowseNext(req)
if err != nil {
return nil, err
}
results = resp.Results
refs = append(refs, results[0].References...)
}
return refs, nil
}

// TranslateBrowsePathsToNodeIDs translates an array of browseName segments to NodeIDs.
Expand Down
4 changes: 4 additions & 0 deletions ua/expanded_node_id.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ type ExpandedNodeID struct {
ServerIndex uint32
}

func (a ExpandedNodeID) String() string {
return a.NodeID.String()
}

// NewExpandedNodeID creates a new ExpandedNodeID.
func NewExpandedNodeID(hasURI, hasIndex bool, nodeID *NodeID, uri string, idx uint32) *ExpandedNodeID {
e := &ExpandedNodeID{
Expand Down
9 changes: 9 additions & 0 deletions ua/variant.go
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,15 @@ func (m *Variant) set(v interface{}) error {
return nil
}

func (m *Variant) NodeID() *NodeID {
switch m.Type() {
case TypeIDNodeID:
return m.value.(*NodeID)
default:
return nil
}
}

// todo(fs): this should probably be StringValue or we need to handle all types
// todo(fs): and recursion
func (m *Variant) String() string {
Expand Down