233 lines
6.4 KiB
Go
233 lines
6.4 KiB
Go
// 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
|
||
}
|