Initial go port of epics

This commit is contained in:
Martino Ferrari
2026-04-28 00:09:22 +02:00
parent 47e481a461
commit 6e51ffc5e1
28 changed files with 5634 additions and 178 deletions
+441
View File
@@ -0,0 +1,441 @@
package proto
import (
"encoding/binary"
"math"
"strings"
"time"
)
// epicsEpochOffset is the number of Unix seconds between the Unix epoch
// (1970-01-01 00:00:00 UTC) and the EPICS epoch (1990-01-01 00:00:00 UTC).
const epicsEpochOffset = 631152000
// maxStringSize is the fixed size of a CA string value (MAX_STRING_SIZE).
const maxStringSize = 40
// maxEnumStates is the maximum number of enum strings (MAX_ENUM_STATES).
const maxEnumStates = 16
// maxEnumStringSize is the fixed size of each enum string (MAX_ENUM_STRING_SIZE).
const maxEnumStringSize = 26
// -------------------------------------------------------------------------- //
// Alarm types //
// -------------------------------------------------------------------------- //
// AlarmSeverity mirrors epicsAlarmSeverity.
type AlarmSeverity int
const (
SeverityNone AlarmSeverity = 0
SeverityMinor AlarmSeverity = 1
SeverityMajor AlarmSeverity = 2
SeverityInvalid AlarmSeverity = 3
)
func (s AlarmSeverity) String() string {
switch s {
case SeverityNone:
return "NO_ALARM"
case SeverityMinor:
return "MINOR"
case SeverityMajor:
return "MAJOR"
case SeverityInvalid:
return "INVALID"
default:
return "UNKNOWN"
}
}
// -------------------------------------------------------------------------- //
// TimeValue — decoded DBR_TIME_* payload //
// -------------------------------------------------------------------------- //
// TimeValue is the decoded form of any DBR_TIME_* payload.
// Exactly one of the value fields is populated based on the DBR type.
type TimeValue struct {
Timestamp time.Time
Status int16
Severity AlarmSeverity
// Value fields — only one is set.
Double float64
Float float32
Long int32
Short int16
Enum uint16
Char uint8
Str string
// Waveform values (when element count > 1).
Doubles []float64
}
// decodeTimestamp converts EPICS secPastEpoch+nsec to a Go time.Time.
func decodeTimestamp(secPastEpoch, nsec uint32) time.Time {
return time.Unix(int64(secPastEpoch)+epicsEpochOffset, int64(nsec)).UTC()
}
// caString extracts a null-terminated string from a fixed-size byte slice.
func caString(b []byte) string {
idx := strings.IndexByte(string(b), 0)
if idx < 0 {
return string(b)
}
return string(b[:idx])
}
// DBR_TIME_* wire layouts (all big-endian):
//
// Common header (12 bytes):
// [0:2] int16 status
// [2:4] int16 severity
// [4:8] uint32 secPastEpoch
// [8:12] uint32 nsec
//
// DBR_TIME_DOUBLE (type 25): 4-byte RISC pad at [12:16], float64 at [16:24]. Total=24.
// DBR_TIME_FLOAT (type 20): float32 at [12:16]. Total=16.
// DBR_TIME_LONG (type 22): int32 at [12:16]. Total=16.
// DBR_TIME_SHORT (type 19): int16 at [12:14], 2-byte pad [14:16]. Total=16.
// DBR_TIME_ENUM (type 23): uint16 at [12:14], 2-byte pad [14:16]. Total=16.
// DBR_TIME_CHAR (type 24): uint8 at [12:13], 3-byte pad [13:16]. Total=16.
// DBR_TIME_STRING (type 21): [40]byte at [12:52]. Total=52.
// DecodeTimeValue decodes a DBR_TIME_* payload.
// dbrType is one of the DBRTime* constants; payload is the full message payload
// (may include multiple elements for waveforms; count is the element count).
func DecodeTimeValue(dbrType uint16, count uint32, payload []byte) (TimeValue, bool) {
if len(payload) < 12 {
return TimeValue{}, false
}
status := int16(binary.BigEndian.Uint16(payload[0:]))
severity := AlarmSeverity(binary.BigEndian.Uint16(payload[2:]))
sec := binary.BigEndian.Uint32(payload[4:])
nsec := binary.BigEndian.Uint32(payload[8:])
tv := TimeValue{
Timestamp: decodeTimestamp(sec, nsec),
Status: status,
Severity: severity,
}
switch dbrType {
case DBRTimeDouble:
// 4-byte RISC pad at [12:16], value at [16:24]
if count > 1 {
if len(payload) < 16+int(count)*8 {
return TimeValue{}, false
}
vals := make([]float64, count)
for i := range vals {
bits := binary.BigEndian.Uint64(payload[16+i*8:])
vals[i] = math.Float64frombits(bits)
}
tv.Doubles = vals
tv.Double = vals[0]
} else {
if len(payload) < 24 {
return TimeValue{}, false
}
bits := binary.BigEndian.Uint64(payload[16:])
tv.Double = math.Float64frombits(bits)
}
case DBRTimeFloat:
if len(payload) < 16 {
return TimeValue{}, false
}
bits := binary.BigEndian.Uint32(payload[12:])
tv.Float = math.Float32frombits(bits)
tv.Double = float64(tv.Float)
case DBRTimeLong:
if len(payload) < 16 {
return TimeValue{}, false
}
tv.Long = int32(binary.BigEndian.Uint32(payload[12:]))
tv.Double = float64(tv.Long)
case DBRTimeShort:
if len(payload) < 16 {
return TimeValue{}, false
}
tv.Short = int16(binary.BigEndian.Uint16(payload[12:]))
tv.Double = float64(tv.Short)
case DBRTimeEnum:
if len(payload) < 16 {
return TimeValue{}, false
}
tv.Enum = binary.BigEndian.Uint16(payload[12:])
tv.Double = float64(tv.Enum)
case DBRTimeChar:
if len(payload) < 16 {
return TimeValue{}, false
}
tv.Char = payload[12]
tv.Double = float64(tv.Char)
case DBRTimeString:
if len(payload) < 12+maxStringSize {
return TimeValue{}, false
}
tv.Str = caString(payload[12 : 12+maxStringSize])
tv.Double = 0
default:
return TimeValue{}, false
}
return tv, true
}
// -------------------------------------------------------------------------- //
// CtrlDouble — decoded DBR_CTRL_DOUBLE payload (type 34) //
// -------------------------------------------------------------------------- //
//
// Wire layout (88 bytes total, all big-endian):
//
// [0:2] int16 status
// [2:4] int16 severity
// [4:6] int16 precision
// [6:8] uint16 RISC_pad0
// [8:16] [8]byte units
// [16:24] float64 upper_disp_limit
// [24:32] float64 lower_disp_limit
// [32:40] float64 upper_alarm_limit
// [40:48] float64 upper_warning_limit
// [48:56] float64 lower_warning_limit
// [56:64] float64 lower_alarm_limit
// [64:72] float64 upper_ctrl_limit
// [72:80] float64 lower_ctrl_limit
// [80:88] float64 value
// CtrlDouble is the decoded form of a DBR_CTRL_DOUBLE payload.
type CtrlDouble struct {
Status int16
Severity AlarmSeverity
Precision int16
Units string
UpperDispLimit float64
LowerDispLimit float64
UpperAlarmLimit float64
UpperWarnLimit float64
LowerWarnLimit float64
LowerAlarmLimit float64
UpperCtrlLimit float64
LowerCtrlLimit float64
Value float64
}
// DecodeCtrlDouble decodes a DBR_CTRL_DOUBLE payload (minimum 88 bytes).
func DecodeCtrlDouble(p []byte) (CtrlDouble, bool) {
if len(p) < 88 {
return CtrlDouble{}, false
}
f64 := func(off int) float64 {
return math.Float64frombits(binary.BigEndian.Uint64(p[off:]))
}
return CtrlDouble{
Status: int16(binary.BigEndian.Uint16(p[0:])),
Severity: AlarmSeverity(binary.BigEndian.Uint16(p[2:])),
Precision: int16(binary.BigEndian.Uint16(p[4:])),
Units: caString(p[8:16]),
UpperDispLimit: f64(16),
LowerDispLimit: f64(24),
UpperAlarmLimit: f64(32),
UpperWarnLimit: f64(40),
LowerWarnLimit: f64(48),
LowerAlarmLimit: f64(56),
UpperCtrlLimit: f64(64),
LowerCtrlLimit: f64(72),
Value: f64(80),
}, true
}
// -------------------------------------------------------------------------- //
// CtrlLong — decoded DBR_CTRL_LONG payload (type 33) //
// -------------------------------------------------------------------------- //
//
// Wire layout (48 bytes total):
//
// [0:2] int16 status
// [2:4] int16 severity
// [4:12] [8]byte units
// [12:16] int32 upper_disp_limit
// [16:20] int32 lower_disp_limit
// [20:24] int32 upper_alarm_limit
// [24:28] int32 upper_warning_limit
// [28:32] int32 lower_warning_limit
// [32:36] int32 lower_alarm_limit
// [36:40] int32 upper_ctrl_limit
// [40:44] int32 lower_ctrl_limit
// [44:48] int32 value
// CtrlLong is the decoded form of a DBR_CTRL_LONG payload.
type CtrlLong struct {
Status int16
Severity AlarmSeverity
Units string
UpperDispLimit int32
LowerDispLimit int32
UpperAlarmLimit int32
UpperWarnLimit int32
LowerWarnLimit int32
LowerAlarmLimit int32
UpperCtrlLimit int32
LowerCtrlLimit int32
Value int32
}
// DecodeCtrlLong decodes a DBR_CTRL_LONG payload (minimum 48 bytes).
func DecodeCtrlLong(p []byte) (CtrlLong, bool) {
if len(p) < 48 {
return CtrlLong{}, false
}
i32 := func(off int) int32 {
return int32(binary.BigEndian.Uint32(p[off:]))
}
return CtrlLong{
Status: int16(binary.BigEndian.Uint16(p[0:])),
Severity: AlarmSeverity(binary.BigEndian.Uint16(p[2:])),
Units: caString(p[4:12]),
UpperDispLimit: i32(12),
LowerDispLimit: i32(16),
UpperAlarmLimit: i32(20),
UpperWarnLimit: i32(24),
LowerWarnLimit: i32(28),
LowerAlarmLimit: i32(32),
UpperCtrlLimit: i32(36),
LowerCtrlLimit: i32(40),
Value: i32(44),
}, true
}
// -------------------------------------------------------------------------- //
// CtrlEnum — decoded DBR_CTRL_ENUM payload (type 31) //
// -------------------------------------------------------------------------- //
//
// Wire layout (424 bytes total):
//
// [0:2] int16 status
// [2:4] int16 severity
// [4:6] int16 no_str (number of valid enum strings, max 16)
// [6:422] [16][26]byte strs (enum string table)
// [422:424] uint16 value
// CtrlEnum is the decoded form of a DBR_CTRL_ENUM payload.
type CtrlEnum struct {
Status int16
Severity AlarmSeverity
Strings []string // len == no_str
Value uint16
}
const ctrlEnumSize = 2 + 2 + 2 + maxEnumStates*maxEnumStringSize + 2 // 424
// DecodeCtrlEnum decodes a DBR_CTRL_ENUM payload (minimum 424 bytes).
func DecodeCtrlEnum(p []byte) (CtrlEnum, bool) {
if len(p) < ctrlEnumSize {
return CtrlEnum{}, false
}
noStr := int(binary.BigEndian.Uint16(p[4:]))
noStr = min(noStr, maxEnumStates)
strs := make([]string, noStr)
for i := range strs {
off := 6 + i*maxEnumStringSize
strs[i] = caString(p[off : off+maxEnumStringSize])
}
return CtrlEnum{
Status: int16(binary.BigEndian.Uint16(p[0:])),
Severity: AlarmSeverity(binary.BigEndian.Uint16(p[2:])),
Strings: strs,
Value: binary.BigEndian.Uint16(p[422:]),
}, true
}
// -------------------------------------------------------------------------- //
// CtrlString — decoded DBR_CTRL_STRING payload (type 28) //
// -------------------------------------------------------------------------- //
//
// Wire layout (44 bytes):
//
// [0:2] int16 status
// [2:4] int16 severity
// [4:44] [40]byte value
// CtrlString is the decoded form of a DBR_CTRL_STRING payload.
type CtrlString struct {
Status int16
Severity AlarmSeverity
Value string
}
// DecodeCtrlString decodes a DBR_CTRL_STRING payload (minimum 44 bytes).
func DecodeCtrlString(p []byte) (CtrlString, bool) {
if len(p) < 4+maxStringSize {
return CtrlString{}, false
}
return CtrlString{
Status: int16(binary.BigEndian.Uint16(p[0:])),
Severity: AlarmSeverity(binary.BigEndian.Uint16(p[2:])),
Value: caString(p[4 : 4+maxStringSize]),
}, true
}
// -------------------------------------------------------------------------- //
// Put payload encoders //
// -------------------------------------------------------------------------- //
// EncodeDouble encodes a float64 value as a big-endian DBR_DOUBLE payload
// padded to 8 bytes.
func EncodeDouble(v float64) []byte {
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, math.Float64bits(v))
return b
}
// EncodeLong encodes an int32 value as a big-endian DBR_LONG payload
// padded to 8 bytes.
func EncodeLong(v int32) []byte {
b := make([]byte, 8) // 4 bytes value + 4 bytes pad
binary.BigEndian.PutUint32(b, uint32(v))
return b
}
// EncodeShort encodes an int16 value as a big-endian DBR_SHORT payload
// padded to 8 bytes.
func EncodeShort(v int16) []byte {
b := make([]byte, 8)
binary.BigEndian.PutUint16(b, uint16(v))
return b
}
// EncodeString encodes a string as a null-terminated, 8-byte padded
// DBR_STRING payload (capped at maxStringSize characters).
func EncodeString(v string) []byte {
if len(v) >= maxStringSize {
v = v[:maxStringSize-1]
}
b := make([]byte, maxStringSize)
copy(b, v)
return PadBytes(b)
}
// EncodeEventMask builds the 16-byte payload for a CA_PROTO_EVENT_ADD request.
// mask is typically DBEDefault (DBEValue | DBEAlarm).
//
// Wire layout (16 bytes, all big-endian):
//
// [0:4] float32 m_lval (not used, zero)
// [4:8] float32 p_delta (delta trigger, zero = disabled)
// [8:12] float32 p_final (final trigger, zero = disabled)
// [12:14] int16 p_count (element count, zero = use PV's count)
// [14:16] int16 m_mask (DBE_VALUE | DBE_ALARM | ...)
func EncodeEventMask(mask uint16) []byte {
b := make([]byte, 16)
binary.BigEndian.PutUint16(b[14:], mask) // m_mask is at offset 14, not 12
return b
}
+108
View File
@@ -0,0 +1,108 @@
package proto
import (
"encoding/binary"
"fmt"
"io"
)
// Header is the decoded form of a 16-byte CA message header.
// All integer fields are stored in host byte order after decoding.
type Header struct {
Command uint16
PayloadSize uint32 // actual payload size (resolved from extended form if needed)
DataType uint16
DataCount uint32 // actual element count (resolved from extended form if needed)
Parameter1 uint32
Parameter2 uint32
}
// HeaderSize is the wire size of a standard CA header.
const HeaderSize = 16
// Encode writes the header to buf (must be at least HeaderSize bytes).
// If PayloadSize > 0xFFFE or DataCount > 0xFFFF the extended encoding is used
// and the caller must prepend an extra 8 bytes; use EncodeExtended instead.
func (h Header) Encode(buf []byte) {
binary.BigEndian.PutUint16(buf[0:], h.Command)
binary.BigEndian.PutUint16(buf[2:], uint16(h.PayloadSize))
binary.BigEndian.PutUint16(buf[4:], h.DataType)
binary.BigEndian.PutUint16(buf[6:], uint16(h.DataCount))
binary.BigEndian.PutUint32(buf[8:], h.Parameter1)
binary.BigEndian.PutUint32(buf[12:], h.Parameter2)
}
// Bytes returns the 16-byte wire encoding of h.
func (h Header) Bytes() []byte {
buf := make([]byte, HeaderSize)
h.Encode(buf)
return buf
}
// DecodeHeader reads exactly one CA header from r, handling the extended
// encoding (payload_size == 0xFFFF) transparently.
// Returns the decoded Header and the number of bytes read (16 or 24).
func DecodeHeader(r io.Reader) (Header, int, error) {
var raw [HeaderSize]byte
if _, err := io.ReadFull(r, raw[:]); err != nil {
return Header{}, 0, fmt.Errorf("ca: read header: %w", err)
}
h := Header{
Command: binary.BigEndian.Uint16(raw[0:]),
DataType: binary.BigEndian.Uint16(raw[4:]),
Parameter1: binary.BigEndian.Uint32(raw[8:]),
Parameter2: binary.BigEndian.Uint32(raw[12:]),
}
rawPayload := binary.BigEndian.Uint16(raw[2:])
rawCount := binary.BigEndian.Uint16(raw[6:])
// Extended message: payload_size == 0xFFFF means real sizes are in the
// next 8 bytes (parameter1 = real payload size, parameter2 = real count).
if rawPayload == 0xFFFF && rawCount == 0x0000 {
var ext [8]byte
if _, err := io.ReadFull(r, ext[:]); err != nil {
return Header{}, 16, fmt.Errorf("ca: read extended header: %w", err)
}
h.PayloadSize = binary.BigEndian.Uint32(ext[0:])
h.DataCount = binary.BigEndian.Uint32(ext[4:])
// Note: parameter1/2 in the base header are overwritten by the extended values.
// In practice, the original parameter1/2 are still valid — the extended header
// only carries sizes, not the original parameters. We already decoded parameter1/2
// above from the base header.
return h, 24, nil
}
h.PayloadSize = uint32(rawPayload)
h.DataCount = uint32(rawCount)
return h, 16, nil
}
// PadTo8 returns n rounded up to the nearest multiple of 8.
// CA requires all message payloads to be padded to 8-byte boundaries.
func PadTo8(n int) int {
return (n + 7) &^ 7
}
// PadBytes appends zero bytes to b until len(b) is a multiple of 8.
func PadBytes(b []byte) []byte {
need := PadTo8(len(b)) - len(b)
return append(b, make([]byte, need)...)
}
// BuildMessage assembles a complete CA message (header + payload).
// payload may be nil for zero-length messages.
func BuildMessage(h Header, payload []byte) []byte {
h.PayloadSize = uint32(len(payload))
msg := make([]byte, HeaderSize+len(payload))
h.Encode(msg)
copy(msg[HeaderSize:], payload)
return msg
}
// BuildStringPayload encodes a string as a null-terminated, 8-byte padded payload.
func BuildStringPayload(s string) []byte {
b := append([]byte(s), 0) // null terminator
return PadBytes(b)
}
+139
View File
@@ -0,0 +1,139 @@
// Package proto contains the low-level Channel Access wire-protocol constants,
// header codec, and DBR type decode functions. All types are big-endian as
// required by the CA specification.
package proto
// CA command opcodes (uint16, first field of every message header).
const (
CmdVersion = 0 // version negotiation (first message on every TCP connection)
CmdEventAdd = 1 // subscribe to value updates / server event push
CmdEventCancel = 2 // cancel subscription
CmdWrite = 4 // put value (fire-and-forget, no server reply)
CmdSearch = 6 // UDP: locate IOC hosting a PV
CmdNotFound = 14 // UDP: IOC does not host the requested PV
CmdReadNotify = 15 // get value with callback (async GET)
CmdCreateChan = 18 // create channel on TCP circuit
CmdWriteNotify = 19 // put value with acknowledgement
CmdClientName = 20 // announce client username (sent after VERSION)
CmdHostName = 21 // announce client hostname (sent after VERSION)
CmdAccessRights = 22 // server → client: access rights bitmask
CmdEcho = 23 // heartbeat ping/pong
CmdCreateFail = 26 // server → client: CREATE_CHAN failed
CmdServerDisc = 27 // server → client: server shutting down
)
// CA minor protocol revision announced during the VERSION handshake.
// Corresponds to CA 4.13, supported by EPICS 3.14+ and all EPICS 7 servers.
const MinorVersion = 13
// Default CA port (TCP and UDP).
const DefaultPort = 5064
// Search reply data_type values.
const (
SearchReply = 10 // DO_REPLY: ask server to respond
SearchNoReply = 5 // DONT_REPLY: suppress response (used by repeater)
)
// AccessRights bitmask values (parameter2 of CmdAccessRights message).
const (
AccessRead = 1 << 0
AccessWrite = 1 << 1
)
// DBE event mask bits used in the 16-byte EVENT_ADD payload.
const (
DBEValue = 0x01 // trigger on any value change
DBEAlarm = 0x04 // trigger on alarm state change
DBEDefault = DBEValue | DBEAlarm
)
// ---- DBF field types (native IOC field type, reported in CREATE_CHAN reply) ----
const (
DBFString = 0
DBFShort = 1 // also DBFInt
DBFFloat = 2
DBFEnum = 3
DBFChar = 4
DBFLong = 5
DBFDouble = 6
)
// ---- DBR request types ----
// Plain value types (used in WRITE).
const (
DBRString = 0
DBRShort = 1
DBRFloat = 2
DBREnum = 3
DBRChar = 4
DBRLong = 5
DBRDouble = 6
)
// DBR_TIME_* types (value + timestamp + alarm status; used in EVENT_ADD).
const (
DBRTimeString = 21
DBRTimeShort = 19
DBRTimeFloat = 20
DBRTimeEnum = 23
DBRTimeChar = 24
DBRTimeLong = 22
DBRTimeDouble = 25
)
// DBR_CTRL_* types (full control info: units, limits, enum strings; used in READ_NOTIFY).
const (
DBRCtrlString = 28
DBRCtrlShort = 29
DBRCtrlFloat = 30
DBRCtrlEnum = 31
DBRCtrlChar = 32
DBRCtrlLong = 33
DBRCtrlDouble = 34
)
// NativeTimeType maps a DBF field type to the corresponding DBR_TIME_* type
// that should be used for monitor subscriptions.
func NativeTimeType(dbfType int, elementCount int) uint16 {
switch dbfType {
case DBFDouble:
return DBRTimeDouble
case DBFFloat:
return DBRTimeFloat
case DBFLong:
return DBRTimeLong
case DBFShort:
return DBRTimeShort
case DBFEnum:
return DBRTimeEnum
case DBFString:
return DBRTimeString
case DBFChar:
if elementCount > 1 {
return DBRTimeString // waveform of chars treated as string
}
return DBRTimeChar
default:
return DBRTimeDouble
}
}
// NativeCtrlType maps a DBF field type to the corresponding DBR_CTRL_* type
// that should be used for metadata gets.
func NativeCtrlType(dbfType int) uint16 {
switch dbfType {
case DBFDouble, DBFFloat:
return DBRCtrlDouble
case DBFLong, DBFShort, DBFChar:
return DBRCtrlLong
case DBFEnum:
return DBRCtrlEnum
case DBFString:
return DBRCtrlString
default:
return DBRCtrlDouble
}
}
+387
View File
@@ -0,0 +1,387 @@
package proto_test
import (
"bytes"
"encoding/binary"
"math"
"testing"
"time"
"github.com/uopi/goca/proto"
)
// -------------------------------------------------------------------------- //
// Header round-trip //
// -------------------------------------------------------------------------- //
func TestHeaderRoundTrip(t *testing.T) {
h := proto.Header{
Command: proto.CmdCreateChan,
PayloadSize: 16,
DataType: proto.DBFDouble,
DataCount: 1,
Parameter1: 0xDEAD,
Parameter2: 0xBEEF,
}
wire := h.Bytes()
if len(wire) != proto.HeaderSize {
t.Fatalf("Bytes() len = %d, want %d", len(wire), proto.HeaderSize)
}
got, n, err := proto.DecodeHeader(bytes.NewReader(wire))
if err != nil {
t.Fatalf("DecodeHeader: %v", err)
}
if n != proto.HeaderSize {
t.Errorf("bytes consumed = %d, want %d", n, proto.HeaderSize)
}
if got.Command != h.Command {
t.Errorf("Command = %d, want %d", got.Command, h.Command)
}
if got.DataType != h.DataType {
t.Errorf("DataType = %d, want %d", got.DataType, h.DataType)
}
if got.Parameter1 != h.Parameter1 {
t.Errorf("Parameter1 = %d, want %d", got.Parameter1, h.Parameter1)
}
if got.Parameter2 != h.Parameter2 {
t.Errorf("Parameter2 = %d, want %d", got.Parameter2, h.Parameter2)
}
}
func TestBuildMessage(t *testing.T) {
payload := []byte("hello\x00\x00\x00") // 8 bytes (padded)
h := proto.Header{Command: proto.CmdHostName, PayloadSize: 0}
msg := proto.BuildMessage(h, payload)
if len(msg) != proto.HeaderSize+len(payload) {
t.Fatalf("len(msg) = %d, want %d", len(msg), proto.HeaderSize+len(payload))
}
// PayloadSize in wire should equal len(payload).
wireSize := binary.BigEndian.Uint16(msg[2:4])
if int(wireSize) != len(payload) {
t.Errorf("wire PayloadSize = %d, want %d", wireSize, len(payload))
}
}
func TestPadTo8(t *testing.T) {
cases := [][2]int{{0, 0}, {1, 8}, {7, 8}, {8, 8}, {9, 16}, {16, 16}, {17, 24}}
for _, c := range cases {
if got := proto.PadTo8(c[0]); got != c[1] {
t.Errorf("PadTo8(%d) = %d, want %d", c[0], got, c[1])
}
}
}
func TestBuildStringPayload(t *testing.T) {
p := proto.BuildStringPayload("TEST")
if len(p)%8 != 0 {
t.Errorf("payload length %d not padded to 8", len(p))
}
if p[4] != 0 {
t.Errorf("expected null terminator at index 4, got %d", p[4])
}
}
// -------------------------------------------------------------------------- //
// DBR_TIME_* decoding //
// -------------------------------------------------------------------------- //
// buildTimeHeader builds the 12-byte common DBR_TIME header.
func buildTimeHeader(status, severity int16, sec, nsec uint32) []byte {
b := make([]byte, 12)
binary.BigEndian.PutUint16(b[0:], uint16(status))
binary.BigEndian.PutUint16(b[2:], uint16(severity))
binary.BigEndian.PutUint32(b[4:], sec)
binary.BigEndian.PutUint32(b[8:], nsec)
return b
}
func TestDecodeTimeDouble(t *testing.T) {
const sec = 1_000_000
const nsec = 500_000_000
const want = 3.14159
hdr := buildTimeHeader(0, 0, sec, nsec)
pad := make([]byte, 4) // RISC pad
val := make([]byte, 8)
binary.BigEndian.PutUint64(val, math.Float64bits(want))
payload := append(append(hdr, pad...), val...)
tv, ok := proto.DecodeTimeValue(proto.DBRTimeDouble, 1, payload)
if !ok {
t.Fatal("DecodeTimeValue returned false")
}
if math.Abs(tv.Double-want) > 1e-10 {
t.Errorf("Double = %g, want %g", tv.Double, want)
}
// EPICS epoch offset: 1990-01-01 00:00:00 UTC = Unix 631152000.
wantUnix := int64(sec) + 631152000
if tv.Timestamp.Unix() != wantUnix {
t.Errorf("Timestamp.Unix() = %d, want %d", tv.Timestamp.Unix(), wantUnix)
}
if tv.Timestamp.Nanosecond() != int(nsec) {
t.Errorf("Timestamp.Nanosecond() = %d, want %d", tv.Timestamp.Nanosecond(), nsec)
}
}
func TestDecodeTimeLong(t *testing.T) {
hdr := buildTimeHeader(0, 1, 0, 0)
val := make([]byte, 4)
binary.BigEndian.PutUint32(val, 0xFFFFFFD6) // -42 as two's complement
payload := append(hdr, val...)
tv, ok := proto.DecodeTimeValue(proto.DBRTimeLong, 1, payload)
if !ok {
t.Fatal("DecodeTimeValue returned false")
}
if tv.Long != -42 {
t.Errorf("Long = %d, want -42", tv.Long)
}
if tv.Severity != proto.SeverityMinor {
t.Errorf("Severity = %v, want Minor", tv.Severity)
}
}
func TestDecodeTimeString(t *testing.T) {
hdr := buildTimeHeader(0, 0, 0, 0)
str := make([]byte, 40)
copy(str, "hello")
payload := append(hdr, str...)
tv, ok := proto.DecodeTimeValue(proto.DBRTimeString, 1, payload)
if !ok {
t.Fatal("DecodeTimeValue returned false")
}
if tv.Str != "hello" {
t.Errorf("Str = %q, want %q", tv.Str, "hello")
}
}
func TestDecodeTimeEnum(t *testing.T) {
hdr := buildTimeHeader(0, 0, 0, 0)
val := []byte{0x00, 0x03, 0x00, 0x00} // enum=3 + 2-byte pad
payload := append(hdr, val...)
tv, ok := proto.DecodeTimeValue(proto.DBRTimeEnum, 1, payload)
if !ok {
t.Fatal("DecodeTimeValue returned false")
}
if tv.Enum != 3 {
t.Errorf("Enum = %d, want 3", tv.Enum)
}
}
func TestDecodeTimeWaveform(t *testing.T) {
hdr := buildTimeHeader(0, 0, 0, 0)
pad := make([]byte, 4)
vals := []float64{1.0, 2.5, -0.5}
valbytes := make([]byte, len(vals)*8)
for i, v := range vals {
binary.BigEndian.PutUint64(valbytes[i*8:], math.Float64bits(v))
}
payload := append(append(hdr, pad...), valbytes...)
tv, ok := proto.DecodeTimeValue(proto.DBRTimeDouble, 3, payload)
if !ok {
t.Fatal("DecodeTimeValue returned false")
}
if len(tv.Doubles) != 3 {
t.Fatalf("len(Doubles) = %d, want 3", len(tv.Doubles))
}
for i, want := range vals {
if math.Abs(tv.Doubles[i]-want) > 1e-12 {
t.Errorf("Doubles[%d] = %g, want %g", i, tv.Doubles[i], want)
}
}
}
func TestDecodeTimeTooShort(t *testing.T) {
_, ok := proto.DecodeTimeValue(proto.DBRTimeDouble, 1, []byte{0, 1, 2})
if ok {
t.Error("expected false for short payload")
}
}
// -------------------------------------------------------------------------- //
// DBR_CTRL_DOUBLE decoding //
// -------------------------------------------------------------------------- //
func TestDecodeCtrlDouble(t *testing.T) {
p := make([]byte, 88)
// status=0, severity=0, precision=3, pad=0, units="mA\0..."
binary.BigEndian.PutUint16(p[4:], 3) // precision
copy(p[8:], "mA")
f64 := func(off int, v float64) {
binary.BigEndian.PutUint64(p[off:], math.Float64bits(v))
}
f64(16, 100.0) // upper_disp
f64(24, 0.0) // lower_disp
f64(32, 90.0) // upper_alarm
f64(40, 80.0) // upper_warn
f64(48, 20.0) // lower_warn
f64(56, 10.0) // lower_alarm
f64(64, 95.0) // upper_ctrl
f64(72, 5.0) // lower_ctrl
f64(80, 55.5) // value
cd, ok := proto.DecodeCtrlDouble(p)
if !ok {
t.Fatal("DecodeCtrlDouble returned false")
}
if cd.Units != "mA" {
t.Errorf("Units = %q, want %q", cd.Units, "mA")
}
if cd.Precision != 3 {
t.Errorf("Precision = %d, want 3", cd.Precision)
}
if math.Abs(cd.Value-55.5) > 1e-10 {
t.Errorf("Value = %g, want 55.5", cd.Value)
}
if math.Abs(cd.UpperDispLimit-100.0) > 1e-10 {
t.Errorf("UpperDispLimit = %g, want 100.0", cd.UpperDispLimit)
}
}
func TestDecodeCtrlEnum(t *testing.T) {
p := make([]byte, 424)
binary.BigEndian.PutUint16(p[4:], 3) // no_str = 3
strs := []string{"OFF", "ON", "FAULT"}
for i, s := range strs {
copy(p[6+i*26:], s)
}
binary.BigEndian.PutUint16(p[422:], 1) // value = 1
ce, ok := proto.DecodeCtrlEnum(p)
if !ok {
t.Fatal("DecodeCtrlEnum returned false")
}
if len(ce.Strings) != 3 {
t.Fatalf("len(Strings) = %d, want 3", len(ce.Strings))
}
if ce.Strings[2] != "FAULT" {
t.Errorf("Strings[2] = %q, want FAULT", ce.Strings[2])
}
if ce.Value != 1 {
t.Errorf("Value = %d, want 1", ce.Value)
}
}
// -------------------------------------------------------------------------- //
// Put payload encoders //
// -------------------------------------------------------------------------- //
func TestEncodeDouble(t *testing.T) {
b := proto.EncodeDouble(3.14)
if len(b) != 8 {
t.Fatalf("len = %d, want 8", len(b))
}
got := math.Float64frombits(binary.BigEndian.Uint64(b))
if math.Abs(got-3.14) > 1e-15 {
t.Errorf("decoded = %g, want 3.14", got)
}
}
func TestEncodeLong(t *testing.T) {
b := proto.EncodeLong(-1)
if len(b) != 8 {
t.Fatalf("len = %d, want 8", len(b))
}
got := int32(binary.BigEndian.Uint32(b))
if got != -1 {
t.Errorf("decoded = %d, want -1", got)
}
}
func TestEncodeString(t *testing.T) {
b := proto.EncodeString("hello")
if len(b)%8 != 0 {
t.Errorf("len %d not padded to 8", len(b))
}
if string(b[:5]) != "hello" {
t.Errorf("content = %q, want %q", b[:5], "hello")
}
}
func TestEncodeEventMask(t *testing.T) {
b := proto.EncodeEventMask(proto.DBEDefault)
if len(b) != 16 {
t.Fatalf("len = %d, want 16", len(b))
}
// m_mask is at offset 14 (after m_lval[0:4], p_delta[4:8], p_final[8:12], p_count[12:14]).
mask := binary.BigEndian.Uint16(b[14:])
if mask != proto.DBEDefault {
t.Errorf("mask = 0x%02X, want 0x%02X", mask, proto.DBEDefault)
}
// p_count at [12:14] must be zero.
if pc := binary.BigEndian.Uint16(b[12:]); pc != 0 {
t.Errorf("p_count = %d, want 0", pc)
}
}
// -------------------------------------------------------------------------- //
// NativeTimeType / NativeCtrlType //
// -------------------------------------------------------------------------- //
func TestNativeTimeType(t *testing.T) {
cases := []struct {
dbf int
n int
want uint16
}{
{proto.DBFDouble, 1, proto.DBRTimeDouble},
{proto.DBFFloat, 1, proto.DBRTimeFloat},
{proto.DBFLong, 1, proto.DBRTimeLong},
{proto.DBFShort, 1, proto.DBRTimeShort},
{proto.DBFEnum, 1, proto.DBRTimeEnum},
{proto.DBFString, 1, proto.DBRTimeString},
{proto.DBFChar, 1, proto.DBRTimeChar},
{proto.DBFChar, 10, proto.DBRTimeString}, // char waveform → string
}
for _, c := range cases {
got := proto.NativeTimeType(c.dbf, c.n)
if got != c.want {
t.Errorf("NativeTimeType(%d,%d) = %d, want %d", c.dbf, c.n, got, c.want)
}
}
}
// -------------------------------------------------------------------------- //
// AlarmSeverity.String //
// -------------------------------------------------------------------------- //
func TestAlarmSeverityString(t *testing.T) {
cases := []struct {
s proto.AlarmSeverity
want string
}{
{proto.SeverityNone, "NO_ALARM"},
{proto.SeverityMinor, "MINOR"},
{proto.SeverityMajor, "MAJOR"},
{proto.SeverityInvalid, "INVALID"},
}
for _, c := range cases {
if got := c.s.String(); got != c.want {
t.Errorf("Severity(%d).String() = %q, want %q", c.s, got, c.want)
}
}
}
// -------------------------------------------------------------------------- //
// Timestamp sanity check //
// -------------------------------------------------------------------------- //
func TestEPICSEpoch(t *testing.T) {
// secPastEpoch=0 should decode to 1990-01-01 00:00:00 UTC.
hdr := buildTimeHeader(0, 0, 0, 0)
val := make([]byte, 8)
payload := append(append(hdr, make([]byte, 4)...), val...)
tv, ok := proto.DecodeTimeValue(proto.DBRTimeDouble, 1, payload)
if !ok {
t.Fatal("DecodeTimeValue returned false")
}
want := time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC)
if !tv.Timestamp.Equal(want) {
t.Errorf("timestamp = %v, want %v", tv.Timestamp, want)
}
}