Skip to content

Commit

Permalink
custom encode func
Browse files Browse the repository at this point in the history
  • Loading branch information
deosjr committed Dec 7, 2023
1 parent 2c74fde commit 76fab1f
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 96 deletions.
57 changes: 39 additions & 18 deletions ua/extension_object.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,7 @@ var eotypes = NewFuncRegistry()
// RegisterExtensionObject registers a new extension object type.
// It panics if the type or the id is already registered.
func RegisterExtensionObject(typeID *NodeID, v interface{}) {
ef := func(vv interface{}) ([]byte, error) {
// TODO check/ensure vv is of type v ?
// TODO
return Encode(vv)
}
df := func(b []byte, vv interface{}) error {
rv := reflect.ValueOf(vv)
if rv.Kind() != reflect.Pointer || rv.IsNil() {
return fmt.Errorf("incorrect type to decode into")
}
r := reflect.New(reflect.TypeOf(v).Elem()).Interface()
buf := NewBuffer(b)
buf.ReadStruct(r)
reflect.Indirect(rv).Set(reflect.ValueOf(r))
return nil
}
RegisterExtensionObjectFunc(typeID, ef, df)
RegisterExtensionObjectFunc(typeID, DefaultEncodeExtensionObject, DefaultDecodeExtensionObject(v))
}

// RegisterExtensionObjectFunc registers a new extension object type using encode and decode functions
Expand All @@ -44,6 +28,10 @@ func RegisterExtensionObjectFunc(typeID *NodeID, ef encodefunc, df decodefunc) {
}
}

func Deregister(typeID *NodeID) {
eotypes.Deregister(typeID)
}

// These flags define the value type of an ExtensionObject.
// They cannot be combined.
const (
Expand Down Expand Up @@ -74,6 +62,7 @@ func NewExtensionObject(value interface{}, typeID *ExpandedNodeID) *ExtensionObj
return e
}

// Decode fails if there is no decode func registered for e
func (e *ExtensionObject) Decode(b []byte) (int, error) {
buf := NewBuffer(b)
e.TypeID = new(ExpandedNodeID)
Expand Down Expand Up @@ -115,11 +104,43 @@ func (e *ExtensionObject) Decode(b []byte) (int, error) {
return buf.Pos(), body.Error()
}

// Encode falls back to defaultencode if there is no encode func registered for e
func (e *ExtensionObject) Encode() ([]byte, error) {
buf := NewBuffer(nil)
if e == nil {
e = &ExtensionObject{TypeID: NewTwoByteExpandedNodeID(0), EncodingMask: ExtensionObjectEmpty}
}

typeID := e.TypeID.NodeID
encode := eotypes.EncodeFunc(typeID)
if encode == nil {
debug.Printf("ua: unknown extension object %s", typeID)
return DefaultEncodeExtensionObject(e)
}
return encode(e)
}

// DefaultDecode creates a new instance of v and decodes into it
func DefaultDecodeExtensionObject(v interface{}) func([]byte, interface{}) error {
return func(b []byte, vv interface{}) error {
rv := reflect.ValueOf(vv)
if rv.Kind() != reflect.Pointer || rv.IsNil() {
return fmt.Errorf("incorrect type to decode into")
}
r := reflect.New(reflect.TypeOf(v).Elem()).Interface()
buf := NewBuffer(b)
buf.ReadStruct(r)
reflect.Indirect(rv).Set(reflect.ValueOf(r))
return nil
}
}

// DefaultEncode encodes into bytes based on the go struct
func DefaultEncodeExtensionObject(v interface{}) ([]byte, error) {
e, ok := v.(*ExtensionObject)
if !ok {
return nil, fmt.Errorf("expected ExtensionObject")
}
buf := NewBuffer(nil)
buf.WriteStruct(e.TypeID)
buf.WriteByte(e.EncodingMask)
if e.EncodingMask == ExtensionObjectEmpty {
Expand Down
14 changes: 14 additions & 0 deletions ua/typereg.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,17 @@ func (r *FuncRegistry) Register(id *NodeID, ef encodefunc, df decodefunc) error
}
return nil
}

// Deregister removes a node from the registry
func (r *FuncRegistry) Deregister(id *NodeID) {
if id == nil {
panic("opcua: missing id in call to FuncRegistry.Register")
}

r.mu.Lock()
defer r.mu.Unlock()

ids := id.String()
delete(r.encodeFuncs, ids)
delete(r.decodeFuncs, ids)
}
141 changes: 141 additions & 0 deletions uatest/custom_codec_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
//go:build integration
// +build integration

package uatest

import (
"context"
"encoding/json"
"fmt"
"reflect"
"testing"

"github.com/gopcua/opcua"
"github.com/gopcua/opcua/ua"
"github.com/pascaldekloe/goe/verify"
)

func TestReadNodeIDWithDecodeFunc(t *testing.T) {
ctx := context.Background()

srv := NewServer("read_unknow_node_id_server.py")
defer srv.Close()

c, err := opcua.NewClient(srv.Endpoint, srv.Opts...)
if err != nil {
t.Fatal(err)
}
if err := c.Connect(ctx); err != nil {
t.Fatal(err)
}
defer c.Close(ctx)

nodeID := ua.NewStringNodeID(2, "IntValZero")

decodeFunc := func(b []byte, v interface{}) error {
// decode into map[string]interface, which means
// decode into dynamically generated go type
// then json marshal/unmarshal :)
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Pointer || rv.IsNil() {
return fmt.Errorf("incorrect type to decode into")
}
r := &struct {
I int64 `json:"i"`
}{} // TODO generate dynamically
buf := ua.NewBuffer(b)
buf.ReadStruct(r)
out := map[string]interface{}{}
b, err := json.Marshal(r)
if err != nil {
return err
}
if err := json.Unmarshal(b, &out); err != nil {
return err
}
reflect.Indirect(rv).Set(reflect.ValueOf(out))
return nil
}

ua.RegisterExtensionObjectFunc(ua.NewStringNodeID(2, "IntValType"), nil, decodeFunc)
defer ua.Deregister(ua.NewStringNodeID(2, "IntValType"))

resp, err := c.Read(ctx, &ua.ReadRequest{
NodesToRead: []*ua.ReadValueID{
{NodeID: nodeID},
},
})
if err != nil {
t.Fatal(err)
}

want := map[string]interface{}{"i": float64(0)} // TODO: float64? yay json!
if got := resp.Results[0].Value.Value().(*ua.ExtensionObject).Value; !reflect.DeepEqual(got, want) {
t.Errorf("got %#v want %#v for a node with an unknown type", got, want)
}
}

type ExtraComplex struct {
ignore, i, j int64
}

// TestCallMethod, but instead of passing Complex{3,8} as an input argument, we pass ExtraComplex{42,3,8}
// We expect the same result only because we register the nodeID for Complex objects with a custom encode func
// Imagine ExtraComplex as a newer version of the API, and encodefunc allows for backwards compatibility
func TestCallMethodWithEncodeFunc(t *testing.T) {
complexNodeID := ua.NewStringNodeID(2, "ComplexType")

encode := func(v interface{}) ([]byte, error) {
// map ExtraComplex -> Complex, dropping 'ignore' field
e, ok := v.(*ua.ExtensionObject)
if !ok {
return nil, fmt.Errorf("expected extensionobject")
}
if ec, ok := e.Value.(*ExtraComplex); ok {
e.Value = &Complex{ec.i, ec.j}
return e.Encode()
}
return ua.DefaultEncodeExtensionObject(e)
}

ua.RegisterExtensionObjectFunc(complexNodeID, encode, nil)
defer ua.Deregister(complexNodeID)

req := &ua.CallMethodRequest{
ObjectID: ua.NewStringNodeID(2, "main"),
MethodID: ua.NewStringNodeID(2, "sumOfSquare"),
InputArguments: []*ua.Variant{
ua.MustVariant(ua.NewExtensionObject(&ExtraComplex{42, 3, 8}, &ua.ExpandedNodeID{NodeID: complexNodeID})),
},
}
out := []*ua.Variant{ua.MustVariant(int64(9 + 64))}

ctx := context.Background()

srv := NewServer("method_server.py")
defer srv.Close()

c, err := opcua.NewClient(srv.Endpoint, srv.Opts...)
if err != nil {
t.Fatal(err)
}
if err := c.Connect(ctx); err != nil {
t.Fatal(err)
}
defer c.Close(ctx)

resp, err := c.Call(ctx, 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, out; !verify.Values(t, "", got, want) {
t.Fail()
}
}

func TestReadUnregisteredExtensionObject(t *testing.T) {
// TODO ask server for description, then decode anyways?
}
5 changes: 3 additions & 2 deletions uatest/method_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ type Complex struct {

func TestCallMethod(t *testing.T) {
complexNodeID := ua.NewStringNodeID(2, "ComplexType")
ua.RegisterExtensionObject(complexNodeID, new(Complex))
//ua.RegisterExtensionObject(complexNodeID, new(Complex))
//defer ua.Deregister(complexNodeID)

tests := []struct {
req *ua.CallMethodRequest
Expand Down Expand Up @@ -50,7 +51,7 @@ func TestCallMethod(t *testing.T) {
ObjectID: ua.NewStringNodeID(2, "main"),
MethodID: ua.NewStringNodeID(2, "sumOfSquare"),
InputArguments: []*ua.Variant{
ua.MustVariant(ua.NewExtensionObject(&Complex{3, 8}, &ua.ExpandedNodeID{NodeID:complexNodeID})),
ua.MustVariant(ua.NewExtensionObject(&Complex{3, 8}, &ua.ExpandedNodeID{NodeID: complexNodeID})),
},
},
out: []*ua.Variant{ua.MustVariant(int64(9 + 64))},
Expand Down
76 changes: 0 additions & 76 deletions uatest/read_unknow_node_id_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ package uatest

import (
"context"
"encoding/json"
"fmt"
"reflect"
"testing"

"github.com/gopcua/opcua"
Expand Down Expand Up @@ -61,76 +58,3 @@ func TestReadUnknowNodeID(t *testing.T) {
t.Error(err)
}
}

func TestReadUnknowNodeIDWithDecodeFunc(t *testing.T) {
ctx := context.Background()

srv := NewServer("read_unknow_node_id_server.py")
defer srv.Close()

c, err := opcua.NewClient(srv.Endpoint, srv.Opts...)
if err != nil {
t.Fatal(err)
}
if err := c.Connect(ctx); err != nil {
t.Fatal(err)
}
defer c.Close(ctx)

// read node with unknown extension object
// This should be OK
nodeWithUnknownType := ua.NewStringNodeID(2, "IntValZero")

decodeFunc := func(b []byte, v interface{}) error {
// decode into map[string]interface, which means
// decode into dynamically generated go type
// then json marshal/unmarshal :)
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Pointer || rv.IsNil() {
return fmt.Errorf("incorrect type to decode into")
}
r := &struct {
I int64 `json:"i"`
}{} // TODO generate dynamically
buf := ua.NewBuffer(b)
buf.ReadStruct(r)
out := map[string]interface{}{}
b, err := json.Marshal(r)
if err != nil {
return err
}
if err := json.Unmarshal(b, &out); err != nil {
return err
}
reflect.Indirect(rv).Set(reflect.ValueOf(out))
return nil
}
// note: encodefunc is nil
ua.RegisterExtensionObjectFunc(ua.NewStringNodeID(2, "IntValType"), nil, decodeFunc)

resp, err := c.Read(ctx, &ua.ReadRequest{
NodesToRead: []*ua.ReadValueID{
{NodeID: nodeWithUnknownType},
},
})
if err != nil {
t.Fatal(err)
}

want := map[string]interface{}{"i": float64(0)} // TODO: float64? yay json!
if got := resp.Results[0].Value.Value().(*ua.ExtensionObject).Value; !reflect.DeepEqual(got, want) {
t.Errorf("got %#v want %#v for a node with an unknown type", got, want)
}

// check that the connection is still usable by reading another node.
_, err = c.Read(ctx, &ua.ReadRequest{
NodesToRead: []*ua.ReadValueID{
{
NodeID: ua.NewNumericNodeID(0, id.Server_ServerStatus_State),
},
},
})
if err != nil {
t.Error(err)
}
}

0 comments on commit 76fab1f

Please sign in to comment.