Initial working fully go release
This commit is contained in:
@@ -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 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
|
||||
}
|
||||
Reference in New Issue
Block a user