Initial working fully go release

This commit is contained in:
Martino Ferrari
2026-04-30 23:01:01 +02:00
parent 6e51ffc5e1
commit 90669c5fd6
22 changed files with 2950 additions and 196 deletions
+232
View File
@@ -0,0 +1,232 @@
// 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
}