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

233 lines
6.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 47 : 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
}