Repository: gopcua/opcua
Pull request #159 (2019-03-31 09:58:09)

Add support for ServerStatusDataType
While trying to read node `ns=0;i=2256` which is the server status I've found a couple of issues:

 * the zero time was not handled properly
 * all extension objects need to be registered manually

I've added this type for now but need to read up on how extension objects are registered. This should be generic. The ext object decode now prints an error message if the extension object type is not known so that you know at least what is going on.

I've also inlined the time decoding logic with `Buffer`.

The `examples/read/read.go` works but does not print anything since I still need to figure out how `Variant.String()` should behave.


Patch:
diff --git a/debug/debug.go b/debug/debug.go
index 5333340..10c6506 100644
--- a/debug/debug.go
+++ b/debug/debug.go
@@ -8,8 +8,14 @@ package debug
 import (
 	"log"
 	"os"
+	"strings"
 )
 
+// Flags contains the debug flags set by OPCUA_DEBUG.
+//
+//  * codec : print detailed debugging information when encoding/decoding
+var Flags = os.Getenv("OPCUA_DEBUG")
+
 // Enable controls whether debug logging is enabled. It is disabled by default.
 var Enable bool
 
@@ -23,3 +29,18 @@ func Printf(format string, args ...interface{}) {
 	}
 	Logger.Printf(format, args...)
 }
+
+// FlagSet returns true if the OPCUA_DEBUG environment variable contains the
+// given flag.
+func FlagSet(name string) bool {
+	return stringSliceContains(name, strings.Fields(Flags))
+}
+
+func stringSliceContains(s string, vals []string) bool {
+	for _, v := range vals {
+		if s == v {
+			return true
+		}
+	}
+	return false
+}
diff --git a/ua/buffer.go b/ua/buffer.go
index 34a0adb..26f6ff4 100644
--- a/ua/buffer.go
+++ b/ua/buffer.go
@@ -6,8 +6,6 @@ import (
 	"io"
 	"math"
 	"time"
-
-	"github.com/gopcua/opcua/utils"
 )
 
 const (
@@ -180,7 +178,12 @@ func (b *Buffer) ReadTime() time.Time {
 	if b.err != nil {
 		return time.Time{}
 	}
-	return utils.DecodeTimestamp(d)
+	ts := binary.LittleEndian.Uint64(d)
+	if ts == 0 {
+		return time.Time{}
+	}
+	// decode time in "100 nanosecond intervals since January 1, 1601" manner.
+	return time.Unix(0, int64((ts-116444736000000000)*100)).UTC()
 }
 
 func (b *Buffer) ReadN(n int) []byte {
@@ -302,7 +305,11 @@ func (b *Buffer) WriteStruct(w interface{}) {
 
 func (b *Buffer) WriteTime(v time.Time) {
 	d := make([]byte, 8)
-	utils.EncodeTimestamp(d, v)
+	if !v.IsZero() {
+		// encode time in "100 nanosecond intervals since January 1, 1601"
+		ts := uint64(v.UTC().UnixNano()/100 + 116444736000000000)
+		binary.LittleEndian.PutUint64(d, ts)
+	}
 	b.Write(d)
 }
 
diff --git a/ua/encode.go b/ua/encode.go
index 09f3e9a..7e3e28c 100644
--- a/ua/encode.go
+++ b/ua/encode.go
@@ -9,10 +9,12 @@ import (
 	"math"
 	"reflect"
 	"time"
+
+	"github.com/gopcua/opcua/debug"
 )
 
 // debugCodec enables printing of debug messages in the opcua codec.
-const debugCodec = false
+var debugCodec = debug.FlagSet("codec")
 
 // BinaryEncoder is the interface implemented by an object that can
 // marshal itself into a binary OPC/UA representation.
diff --git a/ua/extension_object.go b/ua/extension_object.go
index 50663a9..b1e69da 100644
--- a/ua/extension_object.go
+++ b/ua/extension_object.go
@@ -5,6 +5,8 @@
 package ua
 
 import (
+	"fmt"
+
 	"github.com/gopcua/opcua/id"
 )
 
@@ -64,19 +66,18 @@ func (e *ExtensionObject) Decode(b []byte) (int, error) {
 	switch e.TypeID.NodeID.IntID() {
 	case id.AnonymousIdentityToken_Encoding_DefaultBinary:
 		e.Value = new(AnonymousIdentityToken)
-		body.ReadStruct(e.Value)
 	case id.UserNameIdentityToken_Encoding_DefaultBinary:
 		e.Value = new(UserNameIdentityToken)
-		body.ReadStruct(e.Value)
 	case id.X509IdentityToken_Encoding_DefaultBinary:
 		e.Value = new(X509IdentityToken)
-		body.ReadStruct(e.Value)
 	case id.IssuedIdentityToken_Encoding_DefaultBinary:
 		e.Value = new(IssuedIdentityToken)
-		body.ReadStruct(e.Value)
+	case id.ServerStatusDataType_Encoding_DefaultBinary:
+		e.Value = new(ServerStatusDataType)
 	default:
-		e.Value = body.ReadBytes()
+		return buf.Pos(), fmt.Errorf("invalid extension object 0x%x", e.TypeID.NodeID.IntID())
 	}
+	body.ReadStruct(e.Value)
 	return buf.Pos(), body.Error()
 }
 
@@ -118,6 +119,8 @@ func ExtensionObjectTypeID(v interface{}) *ExpandedNodeID {
 		return NewFourByteExpandedNodeID(0, id.X509IdentityToken_Encoding_DefaultBinary)
 	case *IssuedIdentityToken:
 		return NewFourByteExpandedNodeID(0, id.IssuedIdentityToken_Encoding_DefaultBinary)
+	case *ServerStatusDataType:
+		return NewFourByteExpandedNodeID(0, id.ServerStatusDataType_Encoding_DefaultBinary)
 	default:
 		return NewTwoByteExpandedNodeID(0)
 	}
diff --git a/utils/time.go b/utils/time.go
deleted file mode 100644
index 60c260f..0000000
--- a/utils/time.go
+++ /dev/null
@@ -1,23 +0,0 @@
-// 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 utils
-
-import (
-	"encoding/binary"
-	"time"
-)
-
-// EncodeTimestamp serializes time.Time into given bytes buffer
-// in "100 nanosecond intervals since January 1, 1601" manner.
-func EncodeTimestamp(b []byte, t time.Time) {
-	binary.LittleEndian.PutUint64(b, uint64(t.UTC().UnixNano()/100+116444736000000000))
-}
-
-// DecodeTimestamp decodes given bytes into time.Time
-// in "100 nanosecond intervals since January 1, 1601" manner.
-func DecodeTimestamp(b []byte) time.Time {
-	t := binary.LittleEndian.Uint64(b[:8])
-	return time.Unix(0, int64((t-116444736000000000)*100)).UTC()
-}


Test patch:
diff --git a/ua/decode_test.go b/ua/decode_test.go
index 3a13000..c63b02e 100644
--- a/ua/decode_test.go
+++ b/ua/decode_test.go
@@ -147,6 +147,11 @@ func TestCodec(t *testing.T) {
 			v:    &struct{ V time.Time }{time.Date(2018, time.August, 10, 23, 0, 0, 0, time.UTC)},
 			b:    []byte{0x00, 0x98, 0x67, 0xdd, 0xfd, 0x30, 0xd4, 0x01},
 		},
+		{
+			name: "DateTimeZero",
+			v:    &struct{ V time.Time }{time.Time{}},
+			b:    []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
+		},
 		{
 			name: "[]uint32==nil",
 			v:    &struct{ V []uint32 }{},
diff --git a/ua/variant_test.go b/ua/variant_test.go
index 5feb71a..fdae48b 100644
--- a/ua/variant_test.go
+++ b/ua/variant_test.go
@@ -265,6 +265,76 @@ func TestVariant(t *testing.T) {
 				0x09, 0x00, 0x00, 0x00, 0x61, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x6f, 0x75, 0x73,
 			},
 		},
+		{
+			Name: "ExtensionObjeject - ServerStatusDataType",
+			Struct: MustVariant(NewExtensionObject(
+				&ServerStatusDataType{
+					StartTime:   time.Date(2019, 3, 29, 19, 45, 3, 816525000, time.UTC), // Mar 29, 2019 20:45:03.816525000 CET
+					CurrentTime: time.Date(2019, 3, 31, 8, 37, 14, 876798000, time.UTC), // Mar 31, 2019 10:37:14.876798000 CEST
+					State:       ServerStateRunning,
+					BuildInfo: &BuildInfo{
+						ProductURI:       "http://open62541.org",
+						ManufacturerName: "open62541",
+						ProductName:      "open62541 OPC UA Server",
+						SoftwareVersion:  "0.4.0-dev",
+						BuildNumber:      "Mar  4 2019 15:22:43",
+						BuildDate:        time.Time{},
+					},
+					SecondsTillShutdown: 0,
+					ShutdownReason:      &LocalizedText{},
+				},
+			)),
+			Bytes: []byte{
+				// variant encoding mask
+				0x16,
+				// TypeID
+				0x01, 0x00, 0x60, 0x03,
+				// EncodingMask
+				0x01,
+				// Length
+				0x86, 0x00, 0x00, 0x00,
+
+				// ServerStatusDataType
+				// StartTime
+				0x02, 0xe1, 0x5b, 0xe7, 0x67, 0xe6, 0xd4, 0x01,
+				// CurrentTime
+				0xec, 0x62, 0x3c, 0xf1, 0x9c, 0xe7, 0xd4, 0x01,
+				// State
+				0x00, 0x00, 0x00, 0x00,
+
+				// BuildInfo
+				// ProductURI
+				0x14, 0x00, 0x00, 0x00, // length
+				0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x6f,
+				0x70, 0x65, 0x6e, 0x36, 0x32, 0x35, 0x34, 0x31,
+				0x2e, 0x6f, 0x72, 0x67,
+				// ManufacturerName
+				0x09, 0x00, 0x00, 0x00, // length
+				0x6f, 0x70, 0x65, 0x6e, 0x36, 0x32, 0x35, 0x34,
+				0x31,
+				// ProductName
+				0x17, 0x00, 0x00, 0x00, //length
+				0x6f, 0x70, 0x65, 0x6e, 0x36, 0x32, 0x35, 0x34,
+				0x31, 0x20, 0x4f, 0x50, 0x43, 0x20, 0x55, 0x41,
+				0x20, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72,
+				// SoftwareVersion
+				0x09, 0x00, 0x00, 0x00, // length
+				0x30, 0x2e, 0x34, 0x2e, 0x30, 0x2d, 0x64, 0x65,
+				0x76,
+				// BuildNumber
+				0x14, 0x00, 0x00, 0x00, // length
+				0x4d, 0x61, 0x72, 0x20, 0x20, 0x34, 0x20, 0x32,
+				0x30, 0x31, 0x39, 0x20, 0x31, 0x35, 0x3a, 0x32,
+				0x32, 0x3a, 0x34, 0x33,
+				// BuildDate
+				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+
+				// SecondsTillShutdown
+				0x00, 0x00, 0x00, 0x00,
+				// ShutdownReason
+				0x00,
+			},
+		},
 		{
 			Name: "DataValue",
 			Struct: MustVariant(&DataValue{
diff --git a/utils/time_test.go b/utils/time_test.go
deleted file mode 100644
index 47f22b3..0000000
--- a/utils/time_test.go
+++ /dev/null
@@ -1,39 +0,0 @@
-// 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 utils
-
-import (
-	"testing"
-	"time"
-)
-
-var testTimestampBytes = [][]byte{
-	{ // 2018-08-10 23:00:00 +0000 UTC, 1533942000000000000
-		0x00, 0x98, 0x67, 0xdd, 0xfd, 0x30, 0xd4, 0x01,
-	},
-}
-
-func TestDecodeTime(t *testing.T) {
-	ts := DecodeTimestamp(testTimestampBytes[0])
-
-	if ts.UnixNano() != 1533942000000000000 {
-		t.Errorf("Timestamp doesn't match. Want: %d, Got: %d", 1533942000000000000, ts.UnixNano())
-	}
-}
-
-func TestEncodeTime(t *testing.T) {
-	ts := time.Date(2018, time.August, 10, 23, 0, 0, 0, time.UTC)
-
-	serialized := make([]byte, 8)
-	EncodeTimestamp(serialized, ts)
-
-	for i, s := range serialized {
-		x := testTimestampBytes[0][i]
-		if s != x {
-			t.Errorf("Bytes doesn't match. Want: %#x, Got: %#x at %dth", x, s, i)
-		}
-	}
-	t.Logf("%x", serialized)
-}