From e53badd314958b3ff4061b7fdaccbfa579c102f4 Mon Sep 17 00:00:00 2001 From: Frank Schroeder Date: Tue, 13 Aug 2019 16:45:51 +0200 Subject: [PATCH] Add support for methods This patch adds support for the Method Service Set to call methods on the server. Fixes #251 --- .gitignore | 1 + README.md | 2 +- client.go | 18 +++++++++++++ examples/method/method.go | 49 +++++++++++++++++++++++++++++++++++ uatest/method_server.py | 25 ++++++++++++++++++ uatest/method_test.go | 54 +++++++++++++++++++++++++++++++++++++++ uatest/rw_server.py | 10 ++------ 7 files changed, 150 insertions(+), 9 deletions(-) create mode 100644 examples/method/method.go create mode 100755 uatest/method_server.py create mode 100644 uatest/method_test.go diff --git a/.gitignore b/.gitignore index 224f6c1e..a8b16b2e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ vendor/ dist/ +__pycache__/ diff --git a/README.md b/README.md index 77859bfc..277058fc 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ The current set of supported services is only for the high-level client. | | Write | Yes | | | | HistoryRead | Yes | | | | HistoryUpdate | | | -| Method Service Set | Call | | | +| Method Service Set | Call | Yes | | | MonitoredItems Service Set | CreateMonitoredItems | Yes | | | | DeleteMonitoredItems | Yes | | | | ModifyMonitoredItems | | | diff --git a/client.go b/client.go index d4971fae..9becfcaa 100644 --- a/client.go +++ b/client.go @@ -516,6 +516,24 @@ func (c *Client) Browse(req *ua.BrowseRequest) (*ua.BrowseResponse, error) { return res, err } +// Call executes a synchronous call request for a single method. +func (c *Client) Call(req *ua.CallMethodRequest) (*ua.CallMethodResult, error) { + creq := &ua.CallRequest{ + MethodsToCall: []*ua.CallMethodRequest{req}, + } + var res *ua.CallResponse + err := c.Send(creq, func(v interface{}) error { + return safeAssign(v, &res) + }) + if err != nil { + return nil, err + } + if len(res.Results) != 1 { + return nil, ua.StatusBadUnknownResponse + } + return res.Results[0], nil +} + // 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/method/method.go b/examples/method/method.go new file mode 100644 index 00000000..79134905 --- /dev/null +++ b/examples/method/method.go @@ -0,0 +1,49 @@ +// Copyright 2018-2019 opcua authors. All rights reserved. +// Use of this source code is governed by a MIT-style license that can be +// found in the LICENSE file. + +package main + +import ( + "context" + "flag" + "log" + + "github.com/gopcua/opcua" + "github.com/gopcua/opcua/debug" + "github.com/gopcua/opcua/ua" +) + +func main() { + var ( + endpoint = flag.String("endpoint", "opc.tcp://localhost:4840", "OPC UA Endpoint URL") + ) + flag.BoolVar(&debug.Enable, "debug", false, "enable debug logging") + flag.Parse() + log.SetFlags(0) + + ctx := context.Background() + + c := opcua.NewClient(*endpoint, opcua.SecurityMode(ua.MessageSecurityModeNone)) + if err := c.Connect(ctx); err != nil { + log.Fatal(err) + } + defer c.Close() + + in := int64(12) + req := &ua.CallMethodRequest{ + ObjectID: ua.NewStringNodeID(2, "main"), + MethodID: ua.NewStringNodeID(2, "even"), + InputArguments: []*ua.Variant{ua.MustVariant(in)}, + } + + resp, err := c.Call(req) + if err != nil { + log.Fatal(err) + } + if got, want := resp.StatusCode, ua.StatusOK; got != want { + log.Fatalf("got status %v want %v", got, want) + } + out := resp.OutputArguments[0].Value() + log.Printf("%d is even: %v", in, out) +} diff --git a/uatest/method_server.py b/uatest/method_server.py new file mode 100755 index 00000000..2c3989c8 --- /dev/null +++ b/uatest/method_server.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 + +from opcua import ua, Server + +def even(parent, variant): + print("even method call with parameters: ", variant.Value) + ret = (variant.Value % 2 == 0) + print("even", type(ret)) + return [ua.Variant(ret, ua.VariantType.Boolean)] + +def square(parent, variant): + print("square method call with parameters: ", variant.Value) + variant.Value *= variant.Value + return [variant] + +if __name__ == "__main__": + server = Server() + server.set_endpoint("opc.tcp://0.0.0.0:4840/") + + ns = server.register_namespace("http://gopcua.com/") + main = server.nodes.objects.add_object(ua.NodeId("main", ns), "main") + fnEven = main.add_method(ua.NodeId("even", ns), "even", even, [ua.VariantType.Int64], [ua.VariantType.Boolean]) + fnSquare = main.add_method(ua.NodeId("square", ns), "square", square, [ua.VariantType.Int64], [ua.VariantType.Int64]) + + server.start() diff --git a/uatest/method_test.go b/uatest/method_test.go new file mode 100644 index 00000000..b601d811 --- /dev/null +++ b/uatest/method_test.go @@ -0,0 +1,54 @@ +// +build integration + +package uatest + +import ( + "context" + "testing" + + "github.com/gopcua/opcua" + "github.com/gopcua/opcua/ua" + "github.com/pascaldekloe/goe/verify" +) + +func TestCallMethod(t *testing.T) { + tests := []struct { + req *ua.CallMethodRequest + out []*ua.Variant + }{ + { + req: &ua.CallMethodRequest{ + ObjectID: ua.NewStringNodeID(2, "main"), + MethodID: ua.NewStringNodeID(2, "even"), + InputArguments: []*ua.Variant{ + ua.MustVariant(int64(12)), + }, + }, + out: []*ua.Variant{ua.MustVariant(true)}, + }, + } + + srv := NewServer("method_server.py") + defer srv.Close() + + c := opcua.NewClient(srv.Endpoint, srv.Opts...) + if err := c.Connect(context.Background()); err != nil { + t.Fatal(err) + } + defer c.Close() + + for _, tt := range tests { + t.Run(tt.req.ObjectID.String(), func(t *testing.T) { + resp, err := c.Call(tt.req) + if err != nil { + t.Fatal(err) + } + if got, want := resp.StatusCode, ua.StatusOK; got != want { + t.Fatalf("got status %v want %v", got, want) + } + if got, want := resp.OutputArguments, tt.out; !verify.Values(t, "", got, want) { + t.Fail() + } + }) + } +} diff --git a/uatest/rw_server.py b/uatest/rw_server.py index 65017ace..43cd03ae 100755 --- a/uatest/rw_server.py +++ b/uatest/rw_server.py @@ -1,18 +1,12 @@ #!/usr/bin/env python3 -import logging from opcua import ua, Server if __name__ == "__main__": - logging.basicConfig(level=logging.WARN) - server = Server() - server.set_endpoint("opc.tcp://0.0.0.0:4840/gopcua/server/") - server.set_server_name("OPC/UA server Read/Write tests") - - uri = "http://gopcua.com/" - ns = server.register_namespace(uri) + server.set_endpoint("opc.tcp://0.0.0.0:4840/") + ns = server.register_namespace("http://gopcua.com/") main = server.nodes.objects.add_object(ua.NodeId("main", ns), "main") roBool = main.add_variable(ua.NodeId("ro_bool", ns), "ro_bool", True, ua.VariantType.Boolean) rwBool = main.add_variable(ua.NodeId("rw_bool", ns), "rw_bool", True, ua.VariantType.Boolean)