442 lines
13 KiB
Go
442 lines
13 KiB
Go
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
|
|
}
|