Files
2026-04-30 23:01:01 +02:00

445 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, matching EPICS db_access.h structs):
//
// Common header (12 bytes):
// [0:2] int16 status
// [2:4] int16 severity
// [4:8] uint32 secPastEpoch
// [8:12] uint32 nsec
//
// DBR_TIME_STRING (type 14): [40]byte at [12:52]. Total=52.
// DBR_TIME_SHORT (type 15): 2-byte RISC pad at [12:14], int16 at [14:16]. Total=16.
// DBR_TIME_FLOAT (type 16): float32 at [12:16]. Total=16.
// DBR_TIME_ENUM (type 17): 2-byte RISC pad at [12:14], uint16 at [14:16]. Total=16.
// DBR_TIME_CHAR (type 18): RISC_pad0[12:14], RISC_pad1[14], uint8 at [15]. Total=16.
// DBR_TIME_LONG (type 19): int32 at [12:16]. Total=16.
// DBR_TIME_DOUBLE (type 20): 4-byte RISC pad at [12:16], float64 at [16:24]. Total=24.
// 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:
// 2-byte RISC pad at [12:14], value at [14:16]
if len(payload) < 16 {
return TimeValue{}, false
}
tv.Short = int16(binary.BigEndian.Uint16(payload[14:]))
tv.Double = float64(tv.Short)
case DBRTimeEnum:
// 2-byte RISC pad at [12:14], value at [14:16]
if len(payload) < 16 {
return TimeValue{}, false
}
tv.Enum = binary.BigEndian.Uint16(payload[14:])
tv.Double = float64(tv.Enum)
case DBRTimeChar:
// RISC_pad0[12:14], RISC_pad1[14], value[15] per db_access.h dbr_time_char
if len(payload) < 16 {
return TimeValue{}, false
}
tv.Char = payload[15]
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 matches caProto.h struct mon_info (all big-endian):
//
// [0:4] float32 m_lval (low delta, zero = disabled)
// [4:8] float32 m_hval (high delta, zero = disabled)
// [8:12] float32 m_toval (period between samples, zero = disabled)
// [12:14] uint16 m_mask (DBE_VALUE | DBE_ALARM | ...)
// [14:16] uint16 m_pad (alignment padding)
func EncodeEventMask(mask uint16) []byte {
b := make([]byte, 16)
binary.BigEndian.PutUint16(b[12:], mask) // m_mask is at offset 12 per caProto.h
return b
}