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

RegisterExtensionObjectFromServer WIP #702

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
51 changes: 51 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -923,6 +923,57 @@ func (c *Client) closeSession(ctx context.Context, s *Session) error {
})
}

func (c *Client) RegisterExtensionObjectFromServer(ctx context.Context, ids ...*ua.NodeID) error {
nodesToRead := []*ua.ReadValueID{}
for _, id := range ids {
nodesToRead = append(nodesToRead, &ua.ReadValueID{NodeID: id, AttributeID: ua.AttributeIDDataType})
}
resp, err := c.Read(ctx, &ua.ReadRequest{
NodesToRead: nodesToRead,
})
if err != nil {
return err
}

datatypes := []*ua.NodeID{}
for i, r := range resp.Results {
if r.Value == nil {
return fmt.Errorf("failed to find node %s", ids[i].String())
}
v := r.Value.Value()
n, ok := v.(*ua.NodeID)
if !ok {
return fmt.Errorf("failed to find datatype node for node %s, found %T instead", ids[i].String(), v)
}
datatypes = append(datatypes, n)
}

types := map[string]*ua.StructureDefinition{}
nodesToRead = []*ua.ReadValueID{}
for _, dt := range datatypes {
nodesToRead = append(nodesToRead, &ua.ReadValueID{NodeID: dt})
}
resp, err = c.Read(ctx, &ua.ReadRequest{
NodesToRead: nodesToRead,
})
if err != nil {
return err
}

for i, r := range resp.Results {
if r.Value == nil {
return fmt.Errorf("failed to find structuredefinition for node %s", ids[i].String())
}
v := r.Value.Value()
sd, ok := v.(*ua.StructureDefinition)
if !ok {
return fmt.Errorf("failed to find structuredefinition for node %s, found %T instead", ids[i].String(), v)
}
types[ids[i].String()] = sd
}
return ua.RegisterExtensionObjectFromStructure(types)
}

// DetachSession removes the session from the client without closing it. The
// caller is responsible to close or re-activate the session. If the client
// does not have an active session the function returns no error.
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.20

require (
github.com/pascaldekloe/goe v0.1.1
github.com/pascaldekloe/name v1.0.1
github.com/pkg/errors v0.9.1
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
golang.org/x/term v0.8.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
github.com/pascaldekloe/goe v0.1.1 h1:Ah6WQ56rZONR3RW3qWa2NCZ6JAVvSpUcoLBaOmYFt9Q=
github.com/pascaldekloe/goe v0.1.1/go.mod h1:KSyfaxQOh0HZPjDP1FL/kFtbqYqrALJTaMafFUIccqU=
github.com/pascaldekloe/name v1.0.1 h1:9lnXOHeqeHHnWLbKfH6X98+4+ETVqFqxN09UXSjcMb0=
github.com/pascaldekloe/name v1.0.1/go.mod h1:Z//MfYJnH4jVpQ9wkclwu2I2MkHmXTlT9wR5UZScttM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
Expand Down
37 changes: 37 additions & 0 deletions ua/extension_object.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
package ua

import (
"reflect"

"github.com/gopcua/opcua/debug"
"github.com/gopcua/opcua/id"
)
Expand All @@ -20,6 +22,27 @@ func RegisterExtensionObject(typeID *NodeID, v interface{}) {
}
}

/*
func RegisterExtensionObjectFromJSON(r io.Reader) error {
var types map[string]*StructureDefinition
if err := json.NewDecoder(r).Decode(&types); err != nil {
return err
}
return RegisterExtensionObjectFromStructure(types)
}
*/

func RegisterExtensionObjectFromStructure(types map[string]*StructureDefinition) error {
for id := range types {
gotype, err := UAType(id, types)
if err != nil {
return err
}
RegisterExtensionObject(MustParseNodeID(id), reflect.New(gotype))
}
return nil
}

// These flags define the value type of an ExtensionObject.
// They cannot be combined.
const (
Expand Down Expand Up @@ -81,6 +104,14 @@ func (e *ExtensionObject) Decode(b []byte) (int, error) {
}

body.ReadStruct(e.Value)

// if e.Value is an anonymous type (maybe reflect.Type(e.Value).Name() == "") then convert to map[string]interface{}
// encode e.Value to JSON and decode back to map[string]interface{}
// quick, robust but maybe not super efficient
// if there is no type def make e.Value a []byte
// this way the read never fails but you may not be able to interpret the result
// TODO

return buf.Pos(), body.Error()
}

Expand All @@ -89,6 +120,12 @@ func (e *ExtensionObject) Encode() ([]byte, error) {
if e == nil {
e = &ExtensionObject{TypeID: NewTwoByteExpandedNodeID(0), EncodingMask: ExtensionObjectEmpty}
}

// lookup type id, if anonymous type do the json dance
// v := eotypes.New(typeID)
// encode value to JSON and decode JSON into v
// TODO

buf.WriteStruct(e.TypeID)
buf.WriteByte(e.EncodingMask)
if e.EncodingMask == ExtensionObjectEmpty {
Expand Down
137 changes: 137 additions & 0 deletions ua/schema.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package ua

import (
"fmt"
"reflect"
"time"

"github.com/pascaldekloe/name"
)

// Serializable checks if the Go struct t can be serialized into the UA type name.
deosjr marked this conversation as resolved.
Show resolved Hide resolved
func Serializable(t reflect.Type, id string, types map[string]*StructureDefinition) error {
uatyp, err := UAType(id, types)
if err != nil {
return err
}
uadef, godef := GoString(uatyp), GoString(anonType(t))
if uadef != godef {
return fmt.Errorf(`schema: Go type "%s" is not serializable into UA type "%s" (%s)`, godef, uadef, id)
}
return nil
}

// GoString returns a Go-syntax representation of the type of the value.
func GoString(t reflect.Type) string {
return fmt.Sprintf("%T", reflect.New(t).Interface())
}

// anonType returns the struct without the name.
func anonType(t reflect.Type) reflect.Type {
switch {
// do not unpack time.Time
case isTimeType(t):
return t
case t.Kind() == reflect.Slice:
return reflect.SliceOf(anonType(t.Elem()))
case t.Kind() == reflect.Struct:
return reflect.StructOf(structFields(t))
default:
return t
}
}

// structFields returns the fields of a struct as an array.
// it panics if t is not a struct.
func structFields(t reflect.Type) []reflect.StructField {
if t.Kind() != reflect.Struct {
panic(fmt.Sprintf("%s is not a struct", t.Name()))
}
var fields []reflect.StructField
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
f.Name = name.CamelCase(f.Name, true)
f.Type = anonType(f.Type)
fields = append(fields, f)
}
return fields
}

// uaType returns an anonymous Go representation of the OPC/UA struct definition with the NodeID 'id'.
func UAType(id string, types map[string]*StructureDefinition) (reflect.Type, error) {
t := types[id]
if t == nil {
return nil, fmt.Errorf("schema: ua type %q not defined", id)
}

// we don't support structs with optional fields or unions (yet or maybe never)
if t.StructureType != StructureTypeStructure {
return nil, fmt.Errorf("schema: ua type %q has unsupported structure type %s", id, t.StructureType)
}

var fields []reflect.StructField
for _, f := range t.Fields {
if len(f.ArrayDimensions) > 1 {
return nil, fmt.Errorf("schema: ua type %s.%s is a multi-dimensional array: %v", id, f.Name, f.ArrayDimensions)
Copy link
Contributor

Choose a reason for hiding this comment

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

This error is unclear. It just states the fact without saying what is wrong imo :)
I am not sure i understand why is it an error. This is a valid use case for PackML for example.

}

var elem reflect.Type
switch f.DataType.String() {
case "i=1":
elem = reflect.TypeOf(bool(false))
case "i=2":
elem = reflect.TypeOf(int8(0))
case "i=3":
elem = reflect.TypeOf(uint8(0))
case "i=4":
elem = reflect.TypeOf(int16(0))
case "i=5":
elem = reflect.TypeOf(uint16(0))
case "i=6":
elem = reflect.TypeOf(int32(0))
case "i=7":
elem = reflect.TypeOf(uint32(0))
case "i=8":
elem = reflect.TypeOf(int64(0))
case "i=9":
elem = reflect.TypeOf(uint64(0))
case "i=10":
elem = reflect.TypeOf(float32(0))
case "i=11":
elem = reflect.TypeOf(float64(0))
case "i=12":
// todo(fs): maybe encode length constrained strings as []rune
// if f.MaxStringLength > 0 {
// elem = reflect.TypeOf(make([]rune, f.MaxStringLength))
// }
elem = reflect.TypeOf(string(""))
case "i=13":
elem = reflect.TypeOf(time.Time{})
default:
fid := f.DataType.String()
ft := types[fid]
switch {
case ft.BaseDataType.String() == "i=22":
var err error
elem, err = UAType(fid, types)
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("schema: invalid data type %s", fid)
}
}

typ := elem
for rank := f.ValueRank; rank > 0; rank-- {
typ = reflect.SliceOf(typ)
}

fname := name.CamelCase(f.Name, true)
fields = append(fields, reflect.StructField{Name: fname, Type: typ})
}

return reflect.StructOf(fields), nil
}

func isTimeType(t reflect.Type) bool { return t == reflect.TypeOf(time.Time{}) }
7 changes: 7 additions & 0 deletions uatest/read_unknow_node_id_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ func TestReadUnknowNodeID(t *testing.T) {
// read node with unknown extension object
// This should be OK
nodeWithUnknownType := ua.NewStringNodeID(2, "IntValZero")

// TEMP: expect test to pass now
err = c.RegisterExtensionObjectFromServer(ctx, nodeWithUnknownType)
if err != nil {
t.Fatal(err)
}

resp, err := c.Read(ctx, &ua.ReadRequest{
NodesToRead: []*ua.ReadValueID{
{NodeID: nodeWithUnknownType},
Expand Down
Loading