From f3f73e3ecefb976960ce9425a8f999d8beed35a0 Mon Sep 17 00:00:00 2001 From: Frank Schroeder Date: Wed, 27 Mar 2019 11:04:27 +0100 Subject: [PATCH 1/4] more complex browsing example --- client.go | 14 ++++ examples/browse/browse.go | 148 +++++++++++++++++++++++++++++++++----- go.mod | 1 + go.sum | 2 + node.go | 87 +++++++++++++++++++--- ua/expanded_node_id.go | 4 ++ ua/variant.go | 9 +++ uatest/read_test.go | 4 +- 8 files changed, 243 insertions(+), 26 deletions(-) diff --git a/client.go b/client.go index 462f3480..a67568b3 100644 --- a/client.go +++ b/client.go @@ -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 diff --git a/examples/browse/browse.go b/examples/browse/browse.go index b35c3c80..13e15aba 100644 --- a/examples/browse/browse.go +++ b/examples/browse/browse.go @@ -9,13 +9,31 @@ import ( "flag" "fmt" "log" + "os" + "text/tabwriter" "github.com/gopcua/opcua" "github.com/gopcua/opcua/debug" "github.com/gopcua/opcua/id" "github.com/gopcua/opcua/ua" + "github.com/pkg/errors" ) +type NodeDef struct { + NodeID *ua.NodeID + NodeClass ua.NodeClass + BrowseName string + Description string + AccessLevel ua.AccessLevelType + Path string + DataType string + Writable bool +} + +func (n NodeDef) String() string { + return fmt.Sprintf("%s\t%s\t%s\t%v\t%s", n.NodeID, n.Path, n.DataType, n.Writable, n.Description) +} + func join(a, b string) string { if a == "" { return b @@ -23,32 +41,123 @@ func join(a, b string) string { 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 = "DateTime" + case id.Boolean: + def.DataType = "Boolean" + case id.SByte: + def.DataType = "SByte" + 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 = "UtcTime" + case id.String: + def.DataType = "String" + case id.Float: + def.DataType = "Float" + 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 errors.Wrapf(err, "References: %d", refType) + } + // 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 errors.Wrapf(err, "browse children") + } + 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) @@ -61,13 +170,20 @@ 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 := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + fmt.Fprintln(w, "node_id\tpath\ttype\twritable\tdescription") for _, s := range nodeList { - fmt.Println(s) + fmt.Fprintln(w, s) } + w.Flush() } diff --git a/go.mod b/go.mod index 527fb9ff..b516d5f4 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.12 require ( github.com/pascaldekloe/goe v0.1.0 + github.com/pkg/errors v0.8.1 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed // indirect ) diff --git a/go.sum b/go.sum index 36d97ebb..25326c72 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= diff --git a/node.go b/node.go index e8993718..83898c84 100644 --- a/node.go +++ b/node.go @@ -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) @@ -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)) + } + 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), } @@ -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. diff --git a/ua/expanded_node_id.go b/ua/expanded_node_id.go index 07e5def3..92016fa6 100644 --- a/ua/expanded_node_id.go +++ b/ua/expanded_node_id.go @@ -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{ diff --git a/ua/variant.go b/ua/variant.go index d9f5636d..e5ad89b5 100644 --- a/ua/variant.go +++ b/ua/variant.go @@ -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 { diff --git a/uatest/read_test.go b/uatest/read_test.go index 64f74470..2020ec1c 100644 --- a/uatest/read_test.go +++ b/uatest/read_test.go @@ -19,9 +19,9 @@ func TestRead(t *testing.T) { v interface{} }{ {ua.NewStringNodeID(2, "ro_bool"), true}, - {ua.NewStringNodeID(2, "rw_bool"), true}, + //{ua.NewStringNodeID(2, "rw_bool"), true}, {ua.NewStringNodeID(2, "ro_int32"), int32(5)}, - {ua.NewStringNodeID(2, "rw_int32"), int32(5)}, + //{ua.NewStringNodeID(2, "rw_int32"), int32(5)}, } srv := NewServer("rw_server.py") From a213905ebab07ce64e320badbb5ec20ec0d29abb Mon Sep 17 00:00:00 2001 From: Frank Schroeder Date: Sun, 1 Sep 2019 13:56:13 +0200 Subject: [PATCH 2/4] update browsing tool --- examples/browse/browse.go | 44 +++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/examples/browse/browse.go b/examples/browse/browse.go index 13e15aba..17510121 100644 --- a/examples/browse/browse.go +++ b/examples/browse/browse.go @@ -6,11 +6,11 @@ package main import ( "context" + "encoding/csv" "flag" - "fmt" "log" "os" - "text/tabwriter" + "strconv" "github.com/gopcua/opcua" "github.com/gopcua/opcua/debug" @@ -28,10 +28,14 @@ type NodeDef struct { Path string DataType string Writable bool + Unit string + Scale string + Min string + Max string } -func (n NodeDef) String() string { - return fmt.Sprintf("%s\t%s\t%s\t%v\t%s", n.NodeID, n.Path, n.DataType, n.Writable, n.Description) +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 { @@ -93,27 +97,29 @@ func browse(n *opcua.Node, path string, level int) ([]NodeDef, error) { case ua.StatusOK: switch v := attrs[4].Value.NodeID().IntID(); v { case id.DateTime: - def.DataType = "DateTime" + def.DataType = "time.Time" case id.Boolean: - def.DataType = "Boolean" + def.DataType = "bool" case id.SByte: - def.DataType = "SByte" + def.DataType = "int8" case id.Int16: - def.DataType = "Int16" + def.DataType = "int16" case id.Int32: - def.DataType = "Int32" + def.DataType = "int32" case id.Byte: - def.DataType = "Byte" + def.DataType = "byte" case id.UInt16: - def.DataType = "UInt16" + def.DataType = "uint16" case id.UInt32: - def.DataType = "UInt32" + def.DataType = "uint32" case id.UtcTime: - def.DataType = "UtcTime" + def.DataType = "time.Time" case id.String: - def.DataType = "String" + def.DataType = "string" case id.Float: - def.DataType = "Float" + def.DataType = "float32" + case id.Double: + def.DataType = "float64" default: def.DataType = attrs[4].Value.NodeID().String() } @@ -180,10 +186,12 @@ func main() { log.Fatal(err) } - w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) - fmt.Fprintln(w, "node_id\tpath\ttype\twritable\tdescription") + 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.Fprintln(w, s) + w.Write(s.Records()) } w.Flush() } From 4b95fb649b9a753fb3975f8c71a990017aacc5c6 Mon Sep 17 00:00:00 2001 From: Frank Schroeder Date: Tue, 24 Sep 2019 19:35:49 +0200 Subject: [PATCH 3/4] drop dependency on github.com/pkg/errors --- examples/browse/browse.go | 6 +++--- go.mod | 1 - go.sum | 2 -- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/examples/browse/browse.go b/examples/browse/browse.go index 17510121..77b18a80 100644 --- a/examples/browse/browse.go +++ b/examples/browse/browse.go @@ -8,6 +8,7 @@ import ( "context" "encoding/csv" "flag" + "fmt" "log" "os" "strconv" @@ -16,7 +17,6 @@ import ( "github.com/gopcua/opcua/debug" "github.com/gopcua/opcua/id" "github.com/gopcua/opcua/ua" - "github.com/pkg/errors" ) type NodeDef struct { @@ -139,13 +139,13 @@ func browse(n *opcua.Node, path string, level int) ([]NodeDef, error) { browseChildren := func(refType uint32) error { refs, err := n.ReferencedNodes(refType, ua.BrowseDirectionForward, ua.NodeClassAll, true) if err != nil { - return errors.Wrapf(err, "References: %d", refType) + 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 errors.Wrapf(err, "browse children") + return fmt.Errorf("browse children: %s", err) } nodes = append(nodes, children...) } diff --git a/go.mod b/go.mod index b516d5f4..527fb9ff 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.12 require ( github.com/pascaldekloe/goe v0.1.0 - github.com/pkg/errors v0.8.1 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed // indirect ) diff --git a/go.sum b/go.sum index 25326c72..36d97ebb 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= From 98869451a7e35f61993f61687be1bc4da479888f Mon Sep 17 00:00:00 2001 From: Frank Schroeder Date: Tue, 24 Sep 2019 19:37:50 +0200 Subject: [PATCH 4/4] re-enable read-test --- uatest/read_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uatest/read_test.go b/uatest/read_test.go index 2020ec1c..64f74470 100644 --- a/uatest/read_test.go +++ b/uatest/read_test.go @@ -19,9 +19,9 @@ func TestRead(t *testing.T) { v interface{} }{ {ua.NewStringNodeID(2, "ro_bool"), true}, - //{ua.NewStringNodeID(2, "rw_bool"), true}, + {ua.NewStringNodeID(2, "rw_bool"), true}, {ua.NewStringNodeID(2, "ro_int32"), int32(5)}, - //{ua.NewStringNodeID(2, "rw_int32"), int32(5)}, + {ua.NewStringNodeID(2, "rw_int32"), int32(5)}, } srv := NewServer("rw_server.py")