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 }