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..77b18a80 100644 --- a/examples/browse/browse.go +++ b/examples/browse/browse.go @@ -6,9 +6,12 @@ package main import ( "context" + "encoding/csv" "flag" "fmt" "log" + "os" + "strconv" "github.com/gopcua/opcua" "github.com/gopcua/opcua/debug" @@ -16,6 +19,25 @@ import ( "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 @@ -23,32 +45,125 @@ 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 = "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) @@ -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() } 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 {