// Package pva implements an EPICS PV Access (PVA) client in pure Go. // // PVA is the modern EPICS network protocol introduced in EPICS 7, replacing // Channel Access (CA) for structured/typed data. This package implements // the client side of the PVA TCP protocol sufficient for GET and MONITOR // operations on NTScalar and NTScalarArray normative types. // // Reference: EPICS PVAccess Protocol Specification r1.1 // https://epics-pvdata.sourceforge.net/pvAccess.html package pva import ( "bytes" "encoding/binary" "fmt" "io" ) // ---- Message flags ---------------------------------------------------- const ( flagApp byte = 0x00 // application message (vs control) flagControl byte = 0x01 // control message flagSegFirst byte = 0x08 // first segment flagSegLast byte = 0x10 // last segment flagSegMid byte = 0x18 // middle segment flagFromServer byte = 0x40 // direction: server→client flagBigEndian byte = 0x80 // byte order: big-endian (we use little-endian) ) // ---- Application message command codes -------------------------------- const ( CmdBeacon byte = 0x00 CmdConnectionValid byte = 0x01 CmdEcho byte = 0x02 CmdSearchRequest byte = 0x03 CmdSearchResponse byte = 0x04 CmdAuthNZ byte = 0x05 // authentication/authorisation CmdAclChange byte = 0x06 CmdCreateChannel byte = 0x07 CmdDestroyChannel byte = 0x08 CmdConnectionReq byte = 0x09 CmdGet byte = 0x0A CmdPut byte = 0x0B CmdPutGet byte = 0x0C CmdMonitor byte = 0x0D CmdArray byte = 0x0E CmdDestroyReq byte = 0x0F CmdProcess byte = 0x10 CmdGetField byte = 0x11 CmdMessage byte = 0x12 // server status/warning CmdMultipleData byte = 0x13 CmdRpcCall byte = 0x14 CmdCancelRequest byte = 0x15 CmdOriginTag byte = 0x16 ) // ---- Control message sub-commands ------------------------------------- const ( CtrlSetMarker byte = 0x00 CtrlAckMarker byte = 0x01 CtrlSetByteOrder byte = 0x02 CtrlEchoRequest byte = 0x03 CtrlEchoResponse byte = 0x04 ) // ---- Status codes ----------------------------------------------------- const ( StatusOK byte = 0xFF // special "OK" short encoding StatusOKFull byte = 0x00 // full OK message (type=OK, msg="") StatusWarn byte = 0x01 StatusError byte = 0x02 StatusFatal byte = 0x03 ) // ---- Request sub-command bits ----------------------------------------- const ( SubCmdInit byte = 0x08 // initialise request (send FieldDesc) SubCmdGet byte = 0x40 // GET sub-command SubCmdPipeline byte = 0x80 // pipeline (for monitor) SubCmdDEstroy byte = 0x10 // destroy request SubCmdProcess byte = 0x04 ) // ---- Header ----------------------------------------------------------- // PVAHeader is the 8-byte fixed header on every PVA message. // // byte 0 : magic 0xCA // byte 1 : protocol version (0x01) // byte 2 : flags (see flag* constants) // byte 3 : command code // bytes 4–7 : payload size (uint32, matches byte-order flag) type PVAHeader struct { Version byte Flags byte Command byte Size uint32 } const magic byte = 0xCA const headerSize = 8 func (h PVAHeader) isLittleEndian() bool { return h.Flags&flagBigEndian == 0 } func (h PVAHeader) isFromServer() bool { return h.Flags&flagFromServer != 0 } func (h PVAHeader) isControl() bool { return h.Flags&flagControl != 0 } // byteOrder returns the binary.ByteOrder matching the header flags. func (h PVAHeader) byteOrder() binary.ByteOrder { if h.isLittleEndian() { return binary.LittleEndian } return binary.BigEndian } // ReadHeader reads an 8-byte PVA message header from r. func ReadHeader(r io.Reader) (PVAHeader, error) { var raw [headerSize]byte if _, err := io.ReadFull(r, raw[:]); err != nil { return PVAHeader{}, err } if raw[0] != magic { return PVAHeader{}, fmt.Errorf("pva: bad magic 0x%02X (expected 0xCA)", raw[0]) } h := PVAHeader{ Version: raw[1], Flags: raw[2], Command: raw[3], } bo := h.byteOrder() h.Size = bo.Uint32(raw[4:8]) return h, nil } // WriteHeader encodes h and writes the 8-byte header to w. // Always writes in little-endian (client sends LE after SetByteOrder). func WriteHeader(w io.Writer, h PVAHeader) error { raw := [headerSize]byte{ magic, h.Version, h.Flags, h.Command, } binary.LittleEndian.PutUint32(raw[4:8], h.Size) _, err := w.Write(raw[:]) return err } // BuildMessage assembles a complete PVA message (header + payload). func BuildMessage(cmd byte, flags byte, payload []byte) []byte { var buf bytes.Buffer _ = WriteHeader(&buf, PVAHeader{ Version: 1, Flags: flags & ^flagFromServer, // clear server bit — this is client Command: cmd, Size: uint32(len(payload)), }) buf.Write(payload) return buf.Bytes() } // ---- Status decoding --------------------------------------------------- // Status is a decoded PVA status message embedded in many response types. type Status struct { Type byte Message string Stack string } // OK reports whether the status is success. func (s Status) OK() bool { return s.Type == StatusOKFull || s.Type == StatusOK } // Error implements the error interface so Status can be returned as error. func (s Status) Error() string { if s.OK() { return "" } return fmt.Sprintf("pva status %d: %s", s.Type, s.Message) } // ReadStatus reads a PVA status from r. // The short form (0xFF = OK) is handled transparently. func ReadStatus(r io.Reader, bo binary.ByteOrder) (Status, error) { var b [1]byte if _, err := io.ReadFull(r, b[:]); err != nil { return Status{}, err } if b[0] == StatusOK { return Status{Type: StatusOKFull}, nil } typ := b[0] msg, err := readPVAString(r, bo) if err != nil { return Status{}, err } stack, err := readPVAString(r, bo) if err != nil { return Status{}, err } return Status{Type: typ, Message: msg, Stack: stack}, nil } // readPVAString reads a PVA length-prefixed string. // PVA uses compact size encoding (same as pvdata package). func readPVAString(r io.Reader, _ binary.ByteOrder) (string, error) { // delegate to pvdata compact-size reader var b [1]byte if _, err := io.ReadFull(r, b[:]); err != nil { return "", err } n := int(b[0]) if n == 0xFF { var v int32 if err := binary.Read(r, binary.LittleEndian, &v); err != nil { return "", err } n = int(v) } if n <= 0 { return "", nil } buf := make([]byte, n) if _, err := io.ReadFull(r, buf); err != nil { return "", err } return string(buf), nil }