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
+340
View File
@@ -0,0 +1,340 @@
package pva
import (
"bytes"
"context"
"encoding/binary"
"fmt"
"log/slog"
"net"
"sync"
"sync/atomic"
"time"
"github.com/uopi/gopva/pvdata"
)
// DefaultTCPPort is the standard PVA TCP port for channel connections.
const DefaultTCPPort = 5075
// DefaultUDPPort is the PVA broadcast/search port (distinct from TCP).
const DefaultUDPPort = 5076
// Client is a PVA client that manages connections to one or more servers.
// It performs UDP search to locate PVs, then opens TCP connections on demand.
type Client struct {
mu sync.Mutex
conns map[string]*conn // server addr → connection
pending map[string][]waiter // pvName → waiters for address
searchSeq atomic.Uint32
udp *net.UDPConn
ctx context.Context
cancel context.CancelFunc
}
type waiter struct {
pvName string
cb func(addr string, err error)
}
// NewClient creates a PVA client. Call Close when done.
func NewClient() (*Client, error) {
ctx, cancel := context.WithCancel(context.Background())
cl := &Client{
conns: make(map[string]*conn),
pending: make(map[string][]waiter),
ctx: ctx,
cancel: cancel,
}
if err := cl.startUDP(); err != nil {
cancel()
return nil, err
}
return cl, nil
}
// Close shuts down the client and all connections.
func (cl *Client) Close() {
cl.cancel()
if cl.udp != nil {
cl.udp.Close()
}
cl.mu.Lock()
defer cl.mu.Unlock()
for _, c := range cl.conns {
c.close()
}
}
// Get issues a one-shot GET for pvName and returns the decoded structure.
func (cl *Client) Get(ctx context.Context, pvName string) (pvdata.StructValue, error) {
ch := make(chan struct {
v pvdata.StructValue
err error
}, 1)
cl.withConn(ctx, pvName, func(c *conn, err error) {
if err != nil {
ch <- struct {
v pvdata.StructValue
err error
}{err: err}
return
}
c.get(pvName, func(v pvdata.StructValue, e error) {
ch <- struct {
v pvdata.StructValue
err error
}{v, e}
})
})
select {
case res := <-ch:
return res.v, res.err
case <-ctx.Done():
return pvdata.StructValue{}, ctx.Err()
}
}
// Monitor subscribes to pvName and returns a channel that receives updates.
// The channel is closed when ctx is cancelled or a fatal error occurs.
func (cl *Client) Monitor(ctx context.Context, pvName string) <-chan MonitorEvent {
ch := make(chan MonitorEvent, 16)
cl.withConn(ctx, pvName, func(c *conn, err error) {
if err != nil {
ch <- MonitorEvent{Err: err}
close(ch)
return
}
c.subscribe(ctx, pvName, ch)
})
return ch
}
// ---- Internal --------------------------------------------------------
// withConn locates (or creates) a connection to the server hosting pvName
// and calls cb on it. cb may be called from a goroutine.
func (cl *Client) withConn(ctx context.Context, pvName string, cb func(*conn, error)) {
cl.search(ctx, pvName, func(addr string, err error) {
if err != nil {
cb(nil, err)
return
}
cl.mu.Lock()
c, ok := cl.conns[addr]
cl.mu.Unlock()
if ok {
cb(c, nil)
return
}
// open new TCP connection
tc, err := dial(ctx, addr)
if err != nil {
cb(nil, fmt.Errorf("pva: connect %s: %w", addr, err))
return
}
cl.mu.Lock()
cl.conns[addr] = tc
cl.mu.Unlock()
cb(tc, nil)
})
}
// ---- UDP search -------------------------------------------------------
func (cl *Client) startUDP() error {
conn, err := net.ListenUDP("udp4", &net.UDPAddr{Port: 0})
if err != nil {
return fmt.Errorf("pva: UDP listen: %w", err)
}
cl.udp = conn
go cl.udpReadLoop()
return nil
}
// search sends a PVA search request and calls cb when a response arrives.
// Currently does broadcast + localhost; production code would also check
// EPICS_PVA_ADDR_LIST environment variable.
func (cl *Client) search(ctx context.Context, pvName string, cb func(string, error)) {
seq := cl.searchSeq.Add(1)
cl.mu.Lock()
cl.pending[pvName] = append(cl.pending[pvName], waiter{pvName: pvName, cb: cb})
cl.mu.Unlock()
go func() {
for attempt := 0; attempt < 10; attempt++ {
select {
case <-ctx.Done():
cl.mu.Lock()
cl.removeWaiter(pvName, cb)
cl.mu.Unlock()
cb("", ctx.Err())
return
default:
}
cl.sendSearchRequest(seq, pvName)
// exponential backoff: 100ms, 200ms, 400ms … up to 2s
delay := time.Duration(100<<min(attempt, 4)) * time.Millisecond
select {
case <-time.After(delay):
case <-ctx.Done():
cl.mu.Lock()
cl.removeWaiter(pvName, cb)
cl.mu.Unlock()
cb("", ctx.Err())
return
}
}
// give up
cl.mu.Lock()
cl.removeWaiter(pvName, cb)
cl.mu.Unlock()
cb("", fmt.Errorf("pva: search timeout for %q", pvName))
}()
}
func (cl *Client) removeWaiter(pvName string, cb func(string, error)) {
ws := cl.pending[pvName]
for i, w := range ws {
// compare by pointer (closure identity via fmt trick)
if fmt.Sprintf("%p", w.cb) == fmt.Sprintf("%p", cb) {
cl.pending[pvName] = append(ws[:i], ws[i+1:]...)
return
}
}
}
// sendSearchRequest broadcasts a PVA SEARCH request.
func (cl *Client) sendSearchRequest(seq uint32, pvName string) {
// SEARCH payload (spec §6.3):
// searchSeqID(4) + flags(1) + reserved(3) + responseAddress(16) + responsePort(2)
// + transportCount(1) + transports[](1=TCP) + pvCount(2) + pvs[](channelID(4)+name(str))
var buf bytes.Buffer
binary.Write(&buf, binary.LittleEndian, seq) // searchSeqID
buf.WriteByte(0x81) // flags: unicast=0, reply=1, mustReply=1
buf.Write([]byte{0, 0, 0}) // reserved
// responseAddress: 16 bytes (IPv4-mapped IPv6: ::ffff:0.0.0.0 = no specific address)
buf.Write(make([]byte, 12))
buf.Write([]byte{0, 0, 0, 0}) // 0.0.0.0 → server will reply to source addr
// responsePort: our UDP listening port
laddr := cl.udp.LocalAddr().(*net.UDPAddr)
binary.Write(&buf, binary.LittleEndian, uint16(laddr.Port))
buf.WriteByte(1) // transportCount = 1
buf.WriteByte(0x01) // transport[0]: TCP
binary.Write(&buf, binary.LittleEndian, uint16(1)) // pvCount = 1
binary.Write(&buf, binary.LittleEndian, uint32(seq)) // channelID (reuse seq)
pvdata.WriteString(&buf, pvName)
msg := BuildMessage(CmdSearchRequest, flagApp, buf.Bytes())
// Send to localhost and broadcast on the PVA UDP search port (5076).
targets := []string{
fmt.Sprintf("127.0.0.1:%d", DefaultUDPPort),
fmt.Sprintf("255.255.255.255:%d", DefaultUDPPort),
}
// Also check EPICS_PVA_ADDR_LIST if set (common in labs).
// Entries without an explicit port default to DefaultTCPPort for TCP
// connections; for the search broadcast we still use the UDP port.
for _, addr := range addrsFromEnv() {
targets = append(targets, fmt.Sprintf("%s:%d", addr, DefaultUDPPort))
}
for _, addr := range targets {
dst, err := net.ResolveUDPAddr("udp4", addr)
if err != nil {
continue
}
if _, err := cl.udp.WriteTo(msg, dst); err != nil {
slog.Debug("pva search send", "dst", addr, "err", err)
}
}
}
// udpReadLoop reads SEARCH_RESPONSE (and BEACON) UDP messages.
func (cl *Client) udpReadLoop() {
buf := make([]byte, 65536)
for {
n, src, err := cl.udp.ReadFromUDP(buf)
if err != nil {
return
}
cl.handleUDP(buf[:n], src)
}
}
func (cl *Client) handleUDP(data []byte, src *net.UDPAddr) {
if len(data) < headerSize {
return
}
r := bytes.NewReader(data)
hdr, err := ReadHeader(r)
if err != nil || hdr.isControl() {
return
}
if hdr.Command != CmdSearchResponse {
return
}
payload := make([]byte, hdr.Size)
if _, err := r.Read(payload); err != nil {
return
}
pr := bytes.NewReader(payload)
// SEARCH_RESPONSE:
// guid(12) + searchSeqID(4) + serverAddress(16) + serverPort(2)
// + protocol(str) + found(1) + pvCount(2) + pvIDs[](4)
var guid [12]byte
pr.Read(guid[:])
var seq uint32
binary.Read(pr, binary.LittleEndian, &seq)
var addr [16]byte
pr.Read(addr[:])
var port uint16
binary.Read(pr, binary.LittleEndian, &port)
proto, _ := readPVAString(pr, binary.LittleEndian)
if proto != "tcp" {
return
}
found, _ := pr.ReadByte()
if found == 0 {
return
}
var pvCount uint16
binary.Read(pr, binary.LittleEndian, &pvCount)
// pvIDs — we match by searching for waiters by name (seq used as channelID)
// Determine server address: use src if serverAddress is 0.0.0.0
ip := net.IP(addr[12:16]) // last 4 bytes = IPv4
if ip.Equal(net.IPv4zero) {
ip = src.IP
}
serverAddr := fmt.Sprintf("%s:%d", ip.String(), port)
// Notify all pending waiters (simple: we got a response so all PVs are on this server)
cl.mu.Lock()
var toNotify []func(string, error)
for pvName, ws := range cl.pending {
for _, w := range ws {
toNotify = append(toNotify, w.cb)
}
delete(cl.pending, pvName)
}
cl.mu.Unlock()
for _, cb := range toNotify {
go cb(serverAddr, nil)
}
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
+576
View File
@@ -0,0 +1,576 @@
package pva
import (
"bufio"
"bytes"
"context"
"encoding/binary"
"fmt"
"io"
"log/slog"
"net"
"sync"
"sync/atomic"
"time"
"github.com/uopi/gopva/pvdata"
)
// ---- Channel/Request state machine -----------------------------------
type chanState int
const (
chanPending chanState = iota // CREATE_CHANNEL sent
chanCreated // server ack'd
chanFailed // CREATE_CHANNEL failed
)
type serverChannel struct {
pvName string
cid uint32 // client channel ID
sid uint32 // server channel ID (assigned on creation)
state chanState
// pending callbacks waiting for channel creation
onCreate []func(sid uint32, err error)
}
type monitorSub struct {
ioid uint32
sid uint32 // server channel ID — needed for pipeline ACKs and DESTROY
cid uint32
desc pvdata.FieldDesc // structure descriptor (received on INIT response)
updates chan MonitorEvent
}
// MonitorEvent carries a decoded monitor update.
type MonitorEvent struct {
// Changed is the set of top-level field indices that changed.
Changed pvdata.BitSet
// Value is the full decoded structure value.
Value pvdata.StructValue
// Err is non-nil if this is a terminal error event.
Err error
}
// conn is a single PVA TCP connection to one server.
type conn struct {
nc net.Conn
br *bufio.Reader // shared reader — used by handshake then readLoop
bw *bufio.Writer
mu sync.Mutex // protects writes + state maps
// auto-incrementing IDs
nextCID atomic.Uint32
nextIOID atomic.Uint32
// state
chans map[uint32]*serverChannel // keyed by client CID
byPV map[string]*serverChannel // keyed by PV name
monitors map[uint32]*monitorSub // keyed by IOID
done chan struct{}
}
func dial(ctx context.Context, addr string) (*conn, error) {
nc, err := (&net.Dialer{}).DialContext(ctx, "tcp", addr)
if err != nil {
return nil, err
}
c := &conn{
nc: nc,
br: bufio.NewReader(nc),
bw: bufio.NewWriter(nc),
chans: make(map[uint32]*serverChannel),
byPV: make(map[string]*serverChannel),
monitors: make(map[uint32]*monitorSub),
done: make(chan struct{}),
}
if err := c.handshake(); err != nil {
nc.Close()
return nil, err
}
go c.readLoop()
return c, nil
}
// ---- Handshake --------------------------------------------------------
// handshake completes the PVA connection setup.
//
// Protocol sequence (spec §4.2):
// 1. Client → server: SET_BYTE_ORDER control (announces little-endian)
// 2. Server → client: SET_BYTE_ORDER control (may be omitted by some servers)
// 3. Server → client: CONNECTION_VALIDATION (0x01) — server's auth options
// 4. Client → server: CONNECTION_VALIDATION (0x01) — client selects auth
//
// The client MUST NOT send step 4 before receiving step 3.
func (c *conn) handshake() error {
// Step 1: announce our byte order.
if _, err := c.nc.Write(BuildMessage(CtrlSetByteOrder, flagControl, nil)); err != nil {
return fmt.Errorf("pva handshake: send byte-order: %w", err)
}
// Step 2+3: read server messages until CONNECTION_VALIDATION arrives.
// Servers typically send SET_BYTE_ORDER first, then CONNECTION_VALIDATION.
for {
hdr, err := ReadHeader(c.br)
if err != nil {
return fmt.Errorf("pva handshake: read server message: %w", err)
}
payload := make([]byte, hdr.Size)
if _, err := io.ReadFull(c.br, payload); err != nil {
return fmt.Errorf("pva handshake: read server payload: %w", err)
}
if hdr.isControl() {
// SET_BYTE_ORDER from server — we always use LE, ignore.
continue
}
if hdr.Command == CmdConnectionValid {
break // server is ready for our reply
}
// Ignore unexpected messages (e.g. BEACON on same port).
}
// Step 4: send CONNECTION_VALIDATION response.
// payload: clientReceiveBufferSize(4) + clientIntrospectionRegistryMaxSize(4) + authNZ(str)
var buf bytes.Buffer
binary.Write(&buf, binary.LittleEndian, uint32(0x00800000)) // 8 MiB receive buffer
binary.Write(&buf, binary.LittleEndian, uint32(0x00000000)) // introspection registry size
buf.WriteByte(0) // authNZ = "" (anonymous)
if _, err := c.nc.Write(BuildMessage(CmdConnectionValid, flagApp, buf.Bytes())); err != nil {
return fmt.Errorf("pva handshake: send connection_valid: %w", err)
}
return nil
}
// ---- Write helpers ----------------------------------------------------
func (c *conn) send(msg []byte) error {
c.mu.Lock()
defer c.mu.Unlock()
_, err := c.nc.Write(msg)
return err
}
// ---- Channel creation -------------------------------------------------
// openChannel ensures a channel for pvName exists and calls cb when ready.
func (c *conn) openChannel(pvName string, cb func(sid uint32, err error)) {
c.mu.Lock()
if ch, ok := c.byPV[pvName]; ok {
switch ch.state {
case chanCreated:
sid := ch.sid
c.mu.Unlock()
cb(sid, nil)
return
case chanFailed:
c.mu.Unlock()
cb(0, fmt.Errorf("pva: channel %q creation failed", pvName))
return
default:
ch.onCreate = append(ch.onCreate, cb)
c.mu.Unlock()
return
}
}
cid := c.nextCID.Add(1)
ch := &serverChannel{pvName: pvName, cid: cid, state: chanPending, onCreate: []func(uint32, error){cb}}
c.chans[cid] = ch
c.byPV[pvName] = ch
c.mu.Unlock()
// send CREATE_CHANNEL
// payload: count(2) + cid(4) + pvName(string)
var payload bytes.Buffer
binary.Write(&payload, binary.LittleEndian, uint16(1)) // channel count = 1
binary.Write(&payload, binary.LittleEndian, cid)
pvdata.WriteString(&payload, pvName)
if err := c.send(BuildMessage(CmdCreateChannel, flagApp, payload.Bytes())); err != nil {
c.mu.Lock()
delete(c.chans, cid)
delete(c.byPV, pvName)
c.mu.Unlock()
cb(0, err)
}
}
// ---- Monitor ----------------------------------------------------------
// subscribe sends an EVENT_ADD equivalent (MONITOR INIT) for pvName.
func (c *conn) subscribe(ctx context.Context, pvName string, ch chan MonitorEvent) {
c.openChannel(pvName, func(sid uint32, err error) {
if err != nil {
ch <- MonitorEvent{Err: err}
return
}
ioid := c.nextIOID.Add(1)
// Build MONITOR INIT payload:
// sid(4) + ioid(4) + subCmd(1=INIT) + pvRequest(FieldDesc+Value)
var payload bytes.Buffer
binary.Write(&payload, binary.LittleEndian, sid)
binary.Write(&payload, binary.LittleEndian, ioid)
payload.WriteByte(SubCmdInit) // subCmd = INIT
// pvRequest: empty structure (field "field" selecting all)
// Encoded as: TypeCodeStruct + typeID("") + nfields(0)
// This selects the whole top-level structure.
payload.WriteByte(pvdata.TypeCodeStruct) // FieldDesc type
pvdata.WriteString(&payload, "") // typeID
pvdata.WriteSize(&payload, 0) // no sub-fields = select all
c.mu.Lock()
var cid uint32
for _, sch := range c.chans {
if sch.sid == sid {
cid = sch.cid
break
}
}
ms := &monitorSub{ioid: ioid, sid: sid, cid: cid, updates: ch}
c.monitors[ioid] = ms
c.mu.Unlock()
if err := c.send(BuildMessage(CmdMonitor, flagApp, payload.Bytes())); err != nil {
ch <- MonitorEvent{Err: err}
}
// watch context cancellation → send MONITOR DESTROY
go func() {
<-ctx.Done()
c.cancelMonitor(ioid, sid)
}()
})
}
func (c *conn) cancelMonitor(ioid, sid uint32) {
var payload bytes.Buffer
binary.Write(&payload, binary.LittleEndian, sid)
binary.Write(&payload, binary.LittleEndian, ioid)
payload.WriteByte(SubCmdDEstroy)
_ = c.send(BuildMessage(CmdMonitor, flagApp, payload.Bytes()))
}
// ---- GET (one-shot) ---------------------------------------------------
// get issues a GET for pvName and calls cb when the response arrives.
func (c *conn) get(pvName string, cb func(v pvdata.StructValue, err error)) {
c.openChannel(pvName, func(sid uint32, err error) {
if err != nil {
cb(pvdata.StructValue{}, err)
return
}
ioid := c.nextIOID.Add(1)
// INIT phase: GET with subCmd=INIT only (spec §7.2 — INIT and GET are separate messages)
var payload bytes.Buffer
binary.Write(&payload, binary.LittleEndian, sid)
binary.Write(&payload, binary.LittleEndian, ioid)
payload.WriteByte(SubCmdInit) // INIT only — server returns FieldDesc
// pvRequest: empty struct = select all fields
payload.WriteByte(pvdata.TypeCodeStruct)
pvdata.WriteString(&payload, "")
pvdata.WriteSize(&payload, 0)
c.mu.Lock()
ms := &monitorSub{ioid: ioid, sid: sid, updates: make(chan MonitorEvent, 1)}
c.monitors[ioid] = ms
c.mu.Unlock()
if err := c.send(BuildMessage(CmdGet, flagApp, payload.Bytes())); err != nil {
cb(pvdata.StructValue{}, err)
return
}
// wait for INIT response, then issue GET
go func() {
evt := <-ms.updates
if evt.Err != nil {
cb(pvdata.StructValue{}, evt.Err)
return
}
// INIT done — send GET sub-command
var p2 bytes.Buffer
binary.Write(&p2, binary.LittleEndian, sid)
binary.Write(&p2, binary.LittleEndian, ioid)
p2.WriteByte(SubCmdGet)
if err := c.send(BuildMessage(CmdGet, flagApp, p2.Bytes())); err != nil {
cb(pvdata.StructValue{}, err)
return
}
// wait for data response
evt = <-ms.updates
cb(evt.Value, evt.Err)
c.mu.Lock()
delete(c.monitors, ioid)
c.mu.Unlock()
}()
})
}
// ---- Read loop --------------------------------------------------------
func (c *conn) readLoop() {
defer close(c.done)
for {
hdr, err := ReadHeader(c.br)
if err != nil {
if err != io.EOF {
slog.Debug("pva readLoop", "err", err)
}
c.closeAllWithError(err)
return
}
payload := make([]byte, hdr.Size)
if _, err := io.ReadFull(c.br, payload); err != nil {
c.closeAllWithError(err)
return
}
r := bytes.NewReader(payload)
bo := hdr.byteOrder()
if hdr.isControl() {
c.handleControl(hdr, payload)
continue
}
switch hdr.Command {
case CmdCreateChannel:
c.handleCreateChannel(r, bo)
case CmdGet:
c.handleGet(r, bo)
case CmdMonitor:
c.handleMonitor(r, bo)
case CmdDestroyChannel:
// server killed the channel; clean up
case CmdMessage:
handleServerMessage(r, bo)
default:
// ignore unknown commands
}
}
}
func (c *conn) handleControl(hdr PVAHeader, payload []byte) {
switch hdr.Command {
case CtrlSetByteOrder:
// server's byte-order announcement — we always use LE so ignore
case CtrlEchoRequest:
// respond with ECHO_RESPONSE
_ = c.send(BuildMessage(CtrlEchoResponse, flagControl, payload))
}
}
// handleCreateChannel decodes a CREATE_CHANNEL response.
// Wire: count(2) × { cid(4) + status(1+…) + sid(4) }
func (c *conn) handleCreateChannel(r io.Reader, bo binary.ByteOrder) {
var count uint16
binary.Read(r, bo, &count)
for i := 0; i < int(count); i++ {
var cid uint32
binary.Read(r, bo, &cid)
st, err := ReadStatus(r, bo)
if err != nil {
return
}
var sid uint32
if st.OK() {
binary.Read(r, bo, &sid)
}
c.mu.Lock()
ch, ok := c.chans[cid]
if !ok {
c.mu.Unlock()
continue
}
cbs := ch.onCreate
ch.onCreate = nil
if st.OK() {
ch.sid = sid
ch.state = chanCreated
} else {
ch.state = chanFailed
}
c.mu.Unlock()
var cbErr error
if !st.OK() {
cbErr = fmt.Errorf("pva: %s", st.Error())
}
for _, cb := range cbs {
cb(sid, cbErr)
}
}
}
// handleGet decodes a GET response (both INIT and data phases).
func (c *conn) handleGet(r io.Reader, bo binary.ByteOrder) {
var ioid uint32
binary.Read(r, bo, &ioid)
var subCmdBuf [1]byte
io.ReadFull(r, subCmdBuf[:])
subCmd := subCmdBuf[0]
st, err := ReadStatus(r, bo)
if err != nil {
return
}
c.mu.Lock()
ms, ok := c.monitors[ioid]
c.mu.Unlock()
if !ok {
return
}
if subCmd&SubCmdInit != 0 {
// INIT response — read FieldDesc
if st.OK() {
desc, err := pvdata.ReadFieldDesc(r)
if err == nil {
ms.desc = desc
}
}
ms.updates <- MonitorEvent{Err: asError(st)}
return
}
// GET data response
if !st.OK() {
ms.updates <- MonitorEvent{Err: fmt.Errorf("pva GET: %s", st.Error())}
return
}
// read bitSet (changed mask) then value
changed, err := pvdata.ReadBitSet(r)
if err != nil {
ms.updates <- MonitorEvent{Err: err}
return
}
v, err := pvdata.ReadValue(r, ms.desc)
if err != nil {
ms.updates <- MonitorEvent{Err: err}
return
}
sv, _ := v.(pvdata.StructValue)
ms.updates <- MonitorEvent{Changed: changed, Value: sv}
}
// handleMonitor decodes a MONITOR response.
// Sub-commands: INIT (0x08) → FieldDesc, pipeline data.
func (c *conn) handleMonitor(r io.Reader, bo binary.ByteOrder) {
var ioid uint32
binary.Read(r, bo, &ioid)
var subCmdBuf2 [1]byte
io.ReadFull(r, subCmdBuf2[:])
subCmd := subCmdBuf2[0]
c.mu.Lock()
ms, ok := c.monitors[ioid]
c.mu.Unlock()
if !ok {
return
}
if subCmd&SubCmdInit != 0 {
st, err := ReadStatus(r, bo)
if err != nil || !st.OK() {
ms.updates <- MonitorEvent{Err: asError(st)}
return
}
desc, err := pvdata.ReadFieldDesc(r)
if err != nil {
ms.updates <- MonitorEvent{Err: err}
return
}
ms.desc = desc
// Acknowledge pipeline
c.sendMonitorAck(ioid)
return
}
if subCmd&SubCmdDEstroy != 0 {
close(ms.updates)
c.mu.Lock()
delete(c.monitors, ioid)
c.mu.Unlock()
return
}
// Data update: changed BitSet + overrun BitSet + value
changed, err := pvdata.ReadBitSet(r)
if err != nil {
return
}
_, err = pvdata.ReadBitSet(r) // overrun — discard for now
if err != nil {
return
}
v, err := pvdata.ReadValue(r, ms.desc)
if err != nil {
return
}
sv, _ := v.(pvdata.StructValue)
select {
case ms.updates <- MonitorEvent{Changed: changed, Value: sv}:
default:
// slow consumer — drop (overrun)
}
// Acknowledge to allow more updates (pipeline)
c.sendMonitorAck(ioid)
}
func (c *conn) sendMonitorAck(ioid uint32) {
// MONITOR pipeline ACK: sid(4) + ioid(4) + subCmd(0x80)
c.mu.Lock()
ms, ok := c.monitors[ioid]
sid := uint32(0)
if ok {
sid = ms.sid
}
c.mu.Unlock()
var p bytes.Buffer
binary.Write(&p, binary.LittleEndian, sid)
binary.Write(&p, binary.LittleEndian, ioid)
p.WriteByte(SubCmdPipeline)
_ = c.send(BuildMessage(CmdMonitor, flagApp, p.Bytes()))
}
func handleServerMessage(r io.Reader, bo binary.ByteOrder) {
// MESSAGE: type(1) + message(string) — log and ignore
var t [1]byte
r.Read(t[:])
msg, _ := readPVAString(r, bo)
slog.Debug("pva server message", "type", t[0], "msg", msg)
}
func (c *conn) closeAllWithError(err error) {
c.mu.Lock()
defer c.mu.Unlock()
for _, ms := range c.monitors {
select {
case ms.updates <- MonitorEvent{Err: err}:
default:
}
}
}
func (c *conn) close() {
c.nc.Close()
select {
case <-c.done:
case <-time.After(2 * time.Second):
}
}
func asError(s Status) error {
if s.OK() {
return nil
}
return fmt.Errorf("%s", s.Error())
}
+21
View File
@@ -0,0 +1,21 @@
package pva
import (
"os"
"strings"
)
// addrsFromEnv returns addresses from EPICS_PVA_ADDR_LIST, if set.
func addrsFromEnv() []string {
v := os.Getenv("EPICS_PVA_ADDR_LIST")
if v == "" {
return nil
}
var addrs []string
for _, a := range strings.Fields(v) {
if a != "" {
addrs = append(addrs, a)
}
}
return addrs
}
+3
View File
@@ -0,0 +1,3 @@
module github.com/uopi/gopva
go 1.22
+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
}
+73
View File
@@ -0,0 +1,73 @@
package pvdata
import "io"
// BitSet is a variable-length set of bit indices, encoded on the wire as:
// compact-size(n_bytes) + n_bytes of little-endian bits.
// Bit 0 of byte 0 is field index 0, bit 1 of byte 0 is field index 1, etc.
// Used in pvData Monitor responses for "changed" and "overrun" masks.
type BitSet struct {
bits []byte
}
// NewBitSet returns an empty BitSet.
func NewBitSet() BitSet { return BitSet{} }
// Set sets bit i in the BitSet.
func (bs *BitSet) Set(i int) {
byteIdx := i / 8
for len(bs.bits) <= byteIdx {
bs.bits = append(bs.bits, 0)
}
bs.bits[byteIdx] |= 1 << (uint(i) % 8)
}
// Has reports whether bit i is set.
func (bs *BitSet) Has(i int) bool {
byteIdx := i / 8
if byteIdx >= len(bs.bits) {
return false
}
return bs.bits[byteIdx]&(1<<(uint(i)%8)) != 0
}
// ReadBitSet reads a BitSet from r.
func ReadBitSet(r io.Reader) (BitSet, error) {
n, err := ReadSize(r)
if err != nil {
return BitSet{}, err
}
if n == 0 {
return BitSet{}, nil
}
buf := make([]byte, n)
if _, err := io.ReadFull(r, buf); err != nil {
return BitSet{}, err
}
return BitSet{bits: buf}, nil
}
// WriteBitSet writes bs to w.
func WriteBitSet(w io.Writer, bs BitSet) error {
if err := WriteSize(w, int64(len(bs.bits))); err != nil {
return err
}
if len(bs.bits) == 0 {
return nil
}
_, err := w.Write(bs.bits)
return err
}
// FieldIndices returns all set bit indices in ascending order.
func (bs *BitSet) FieldIndices() []int {
var out []int
for bi, b := range bs.bits {
for bit := 0; bit < 8; bit++ {
if b&(1<<uint(bit)) != 0 {
out = append(out, bi*8+bit)
}
}
}
return out
}
+679
View File
@@ -0,0 +1,679 @@
// Package pvdata implements the EPICS pvData serialisation layer.
//
// pvData defines a rich, self-describing type system used by PV Access (PVA).
// Every value that flows over a PVA connection is encoded according to a
// FieldDesc (field description / introspection interface), which is itself
// serialised on the wire before the payload.
//
// Reference: EPICS pvData Specification r1.1
// https://epics-pvdata.sourceforge.net/pvData.html
package pvdata
import (
"encoding/binary"
"fmt"
"io"
"math"
)
// ---- Type codes -------------------------------------------------------
//
// pvData uses a one-byte "type code" to identify scalar and array element
// types. Values from the spec §3.1.
const (
TypeCodeBoolean byte = 0x00
TypeCodeByte byte = 0x20 // int8
TypeCodeShort byte = 0x21 // int16
TypeCodeInt byte = 0x22 // int32
TypeCodeLong byte = 0x23 // int64
TypeCodeUByte byte = 0x24 // uint8
TypeCodeUShort byte = 0x25 // uint16
TypeCodeUInt byte = 0x26 // uint32
TypeCodeULong byte = 0x27 // uint64
TypeCodeFloat byte = 0x42 // float32
TypeCodeDouble byte = 0x43 // float64
TypeCodeString byte = 0x60
TypeCodeStruct byte = 0x80
TypeCodeUnion byte = 0x81
TypeCodeBoolArr byte = 0x08
TypeCodeByteArr byte = 0x28
TypeCodeShortArr byte = 0x29
TypeCodeIntArr byte = 0x2A
TypeCodeLongArr byte = 0x2B
TypeCodeUByteArr byte = 0x2C
TypeCodeUShortArr byte = 0x2D
TypeCodeUIntArr byte = 0x2E
TypeCodeULongArr byte = 0x2F
TypeCodeFloatArr byte = 0x4A
TypeCodeDoubleArr byte = 0x4B
TypeCodeStringArr byte = 0x68
TypeCodeStructArr byte = 0x88
TypeCodeUnionArr byte = 0x89
TypeCodeVariant byte = 0x82 // any
TypeCodeNull byte = 0xFF
)
// ---- Compact size encoding --------------------------------------------
//
// pvData encodes array/string lengths as a "size" using 1, 4, or 8 bytes
// (spec §3.2).
// ReadSize reads a compact-format size from r.
// 0x000xFE → 1-byte value
// 0xFF → read 4-byte int32 (negative = 8-byte int64 follows)
func ReadSize(r io.Reader) (int64, error) {
var b [1]byte
if _, err := io.ReadFull(r, b[:]); err != nil {
return 0, err
}
if b[0] != 0xFF {
return int64(b[0]), nil
}
var v int32
if err := binary.Read(r, binary.LittleEndian, &v); err != nil {
return 0, err
}
if v >= 0 {
return int64(v), nil
}
// extended 8-byte size
var v8 int64
if err := binary.Read(r, binary.LittleEndian, &v8); err != nil {
return 0, err
}
return v8, nil
}
// WriteSize writes n as a compact-format size to w.
func WriteSize(w io.Writer, n int64) error {
if n < 0xFF {
_, err := w.Write([]byte{byte(n)})
return err
}
if n <= math.MaxInt32 {
if _, err := w.Write([]byte{0xFF}); err != nil {
return err
}
return binary.Write(w, binary.LittleEndian, int32(n))
}
// rare: >2 GiB array
if _, err := w.Write([]byte{0xFF}); err != nil {
return err
}
if err := binary.Write(w, binary.LittleEndian, int32(-1)); err != nil {
return err
}
return binary.Write(w, binary.LittleEndian, n)
}
// ReadString reads a pvData string (compact size + UTF-8 bytes).
func ReadString(r io.Reader) (string, error) {
n, err := ReadSize(r)
if err != nil {
return "", err
}
if n == 0 {
return "", nil
}
buf := make([]byte, n)
if _, err := io.ReadFull(r, buf); err != nil {
return "", err
}
return string(buf), nil
}
// WriteString writes a pvData string to w.
func WriteString(w io.Writer, s string) error {
if err := WriteSize(w, int64(len(s))); err != nil {
return err
}
_, err := io.WriteString(w, s)
return err
}
// ---- Field description (introspection) --------------------------------
// Kind classifies a FieldDesc at a high level.
type Kind byte
const (
KindScalar Kind = iota // single scalar value
KindScalarArr // variable-length array of scalars
KindString // pvData string (treated specially)
KindStringArr
KindStruct // structure
KindStructArr
KindUnion // discriminated union
KindUnionArr
KindVariant // any (variant)
)
// FieldDesc describes the type of a single field in a pvData structure.
type FieldDesc struct {
Kind Kind
TypeCode byte // scalar/array type code; 0 for struct/union
TypeID string // optional structure/union type ID string
Fields []Field // child fields for struct/union
}
// Field is a named FieldDesc (one member of a struct or union).
type Field struct {
Name string
Desc FieldDesc
}
// ---- FieldDesc codec --------------------------------------------------
// ReadFieldDesc decodes a FieldDesc from r.
// This follows the pvData introspection encoding (spec §4).
func ReadFieldDesc(r io.Reader) (FieldDesc, error) {
var tc [1]byte
if _, err := io.ReadFull(r, tc[:]); err != nil {
return FieldDesc{}, err
}
return readFieldDescFromTypeCode(r, tc[0])
}
func readFieldDescFromTypeCode(r io.Reader, tc byte) (FieldDesc, error) {
switch {
case tc == TypeCodeNull:
return FieldDesc{TypeCode: TypeCodeNull}, nil
case tc == TypeCodeString:
return FieldDesc{Kind: KindString, TypeCode: tc}, nil
case tc == TypeCodeStringArr:
return FieldDesc{Kind: KindStringArr, TypeCode: tc}, nil
case tc == TypeCodeVariant:
return FieldDesc{Kind: KindVariant, TypeCode: tc}, nil
case tc == TypeCodeStruct || tc == TypeCodeUnion:
kind := KindStruct
if tc == TypeCodeUnion {
kind = KindUnion
}
typeID, err := ReadString(r)
if err != nil {
return FieldDesc{}, err
}
nfields, err := ReadSize(r)
if err != nil {
return FieldDesc{}, err
}
fields := make([]Field, nfields)
for i := range fields {
name, err := ReadString(r)
if err != nil {
return FieldDesc{}, err
}
desc, err := ReadFieldDesc(r)
if err != nil {
return FieldDesc{}, err
}
fields[i] = Field{Name: name, Desc: desc}
}
return FieldDesc{Kind: kind, TypeCode: tc, TypeID: typeID, Fields: fields}, nil
case tc == TypeCodeStructArr || tc == TypeCodeUnionArr:
kind := KindStructArr
if tc == TypeCodeUnionArr {
kind = KindUnionArr
}
// element descriptor follows
elem, err := ReadFieldDesc(r)
if err != nil {
return FieldDesc{}, err
}
return FieldDesc{Kind: kind, TypeCode: tc, TypeID: elem.TypeID, Fields: elem.Fields}, nil
// scalar array types: 0x280x2F, 0x4A0x4B, 0x08, 0x68
case isScalarArrayCode(tc):
return FieldDesc{Kind: KindScalarArr, TypeCode: tc}, nil
// plain scalars and boolean
default:
return FieldDesc{Kind: KindScalar, TypeCode: tc}, nil
}
}
func isScalarArrayCode(tc byte) bool {
switch tc {
case TypeCodeBoolArr,
TypeCodeByteArr, TypeCodeShortArr, TypeCodeIntArr, TypeCodeLongArr,
TypeCodeUByteArr, TypeCodeUShortArr, TypeCodeUIntArr, TypeCodeULongArr,
TypeCodeFloatArr, TypeCodeDoubleArr,
TypeCodeStringArr:
return true
}
return false
}
// WriteFieldDesc encodes fd into w.
func WriteFieldDesc(w io.Writer, fd FieldDesc) error {
if _, err := w.Write([]byte{fd.TypeCode}); err != nil {
return err
}
switch fd.Kind {
case KindStruct, KindUnion:
if err := WriteString(w, fd.TypeID); err != nil {
return err
}
if err := WriteSize(w, int64(len(fd.Fields))); err != nil {
return err
}
for _, f := range fd.Fields {
if err := WriteString(w, f.Name); err != nil {
return err
}
if err := WriteFieldDesc(w, f.Desc); err != nil {
return err
}
}
case KindStructArr, KindUnionArr:
// write element descriptor
elemTC := TypeCodeStruct
if fd.Kind == KindUnionArr {
elemTC = TypeCodeUnion
}
if err := WriteFieldDesc(w, FieldDesc{
Kind: KindStruct, TypeCode: byte(elemTC),
TypeID: fd.TypeID, Fields: fd.Fields,
}); err != nil {
return err
}
}
return nil
}
// ---- Value codec -------------------------------------------------------
// Value is a decoded pvData value. We use interface{} for flexibility;
// callers type-assert to concrete types listed below.
//
// Scalar types:
// bool, int8, int16, int32, int64,
// uint8, uint16, uint32, uint64,
// float32, float64, string
//
// Array types: []T for each scalar T above; []string.
// Struct: StructValue
// Union: UnionValue
// Variant: VariantValue
// Null: nil
type Value = interface{}
// StructValue holds the fields of a decoded pvData structure.
type StructValue struct {
TypeID string
Fields []FieldValue
}
// FieldValue is a named value within a StructValue.
type FieldValue struct {
Name string
Value Value
}
// UnionValue holds a selected field from a discriminated union.
type UnionValue struct {
Selected int // index into union's Fields slice
Name string // field name
Value Value
}
// VariantValue wraps an any-typed value together with its runtime descriptor.
type VariantValue struct {
Desc FieldDesc
Value Value
}
// ReadValue decodes a value conforming to desc from r.
func ReadValue(r io.Reader, desc FieldDesc) (Value, error) {
switch desc.Kind {
case KindScalar:
return readScalar(r, desc.TypeCode)
case KindScalarArr:
return readScalarArray(r, desc.TypeCode)
case KindString:
return ReadString(r)
case KindStringArr:
n, err := ReadSize(r)
if err != nil {
return nil, err
}
ss := make([]string, n)
for i := range ss {
ss[i], err = ReadString(r)
if err != nil {
return nil, err
}
}
return ss, nil
case KindStruct:
return readStruct(r, desc)
case KindStructArr:
return readStructArr(r, desc)
case KindUnion:
return readUnion(r, desc)
case KindUnionArr:
return readUnionArr(r, desc)
case KindVariant:
return readVariant(r)
default:
return nil, fmt.Errorf("pvdata: unknown kind %d", desc.Kind)
}
}
func readScalar(r io.Reader, tc byte) (Value, error) {
switch tc {
case TypeCodeBoolean:
var b [1]byte
_, err := io.ReadFull(r, b[:])
return b[0] != 0, err
case TypeCodeByte:
var v int8
return v, binary.Read(r, binary.LittleEndian, &v)
case TypeCodeShort:
var v int16
return v, binary.Read(r, binary.LittleEndian, &v)
case TypeCodeInt:
var v int32
return v, binary.Read(r, binary.LittleEndian, &v)
case TypeCodeLong:
var v int64
return v, binary.Read(r, binary.LittleEndian, &v)
case TypeCodeUByte:
var v uint8
return v, binary.Read(r, binary.LittleEndian, &v)
case TypeCodeUShort:
var v uint16
return v, binary.Read(r, binary.LittleEndian, &v)
case TypeCodeUInt:
var v uint32
return v, binary.Read(r, binary.LittleEndian, &v)
case TypeCodeULong:
var v uint64
return v, binary.Read(r, binary.LittleEndian, &v)
case TypeCodeFloat:
var v uint32
if err := binary.Read(r, binary.LittleEndian, &v); err != nil {
return nil, err
}
return math.Float32frombits(v), nil
case TypeCodeDouble:
var v uint64
if err := binary.Read(r, binary.LittleEndian, &v); err != nil {
return nil, err
}
return math.Float64frombits(v), nil
default:
return nil, fmt.Errorf("pvdata: unknown scalar type code 0x%02X", tc)
}
}
func readScalarArray(r io.Reader, tc byte) (Value, error) {
n, err := ReadSize(r)
if err != nil {
return nil, err
}
count := int(n)
switch tc {
case TypeCodeBoolArr:
v := make([]bool, count)
for i := range v {
b := [1]byte{}
if _, err := io.ReadFull(r, b[:]); err != nil {
return nil, err
}
v[i] = b[0] != 0
}
return v, nil
case TypeCodeByteArr:
v := make([]int8, count)
return v, binary.Read(r, binary.LittleEndian, v)
case TypeCodeShortArr:
v := make([]int16, count)
return v, binary.Read(r, binary.LittleEndian, v)
case TypeCodeIntArr:
v := make([]int32, count)
return v, binary.Read(r, binary.LittleEndian, v)
case TypeCodeLongArr:
v := make([]int64, count)
return v, binary.Read(r, binary.LittleEndian, v)
case TypeCodeUByteArr:
v := make([]uint8, count)
return v, binary.Read(r, binary.LittleEndian, v)
case TypeCodeUShortArr:
v := make([]uint16, count)
return v, binary.Read(r, binary.LittleEndian, v)
case TypeCodeUIntArr:
v := make([]uint32, count)
return v, binary.Read(r, binary.LittleEndian, v)
case TypeCodeULongArr:
v := make([]uint64, count)
return v, binary.Read(r, binary.LittleEndian, v)
case TypeCodeFloatArr:
bits := make([]uint32, count)
if err := binary.Read(r, binary.LittleEndian, bits); err != nil {
return nil, err
}
v := make([]float32, count)
for i, b := range bits {
v[i] = math.Float32frombits(b)
}
return v, nil
case TypeCodeDoubleArr:
bits := make([]uint64, count)
if err := binary.Read(r, binary.LittleEndian, bits); err != nil {
return nil, err
}
v := make([]float64, count)
for i, b := range bits {
v[i] = math.Float64frombits(b)
}
return v, nil
default:
return nil, fmt.Errorf("pvdata: unknown array type code 0x%02X", tc)
}
}
func readStruct(r io.Reader, desc FieldDesc) (StructValue, error) {
sv := StructValue{TypeID: desc.TypeID, Fields: make([]FieldValue, len(desc.Fields))}
for i, f := range desc.Fields {
v, err := ReadValue(r, f.Desc)
if err != nil {
return StructValue{}, fmt.Errorf("field %q: %w", f.Name, err)
}
sv.Fields[i] = FieldValue{Name: f.Name, Value: v}
}
return sv, nil
}
func readStructArr(r io.Reader, desc FieldDesc) ([]StructValue, error) {
n, err := ReadSize(r)
if err != nil {
return nil, err
}
arr := make([]StructValue, n)
for i := range arr {
sv, err := readStruct(r, desc)
if err != nil {
return nil, err
}
arr[i] = sv
}
return arr, nil
}
func readUnion(r io.Reader, desc FieldDesc) (UnionValue, error) {
idx, err := ReadSize(r)
if err != nil {
return UnionValue{}, err
}
if idx < 0 || int(idx) >= len(desc.Fields) {
return UnionValue{Selected: int(idx)}, nil // null selector
}
f := desc.Fields[idx]
v, err := ReadValue(r, f.Desc)
if err != nil {
return UnionValue{}, err
}
return UnionValue{Selected: int(idx), Name: f.Name, Value: v}, nil
}
func readUnionArr(r io.Reader, desc FieldDesc) ([]UnionValue, error) {
n, err := ReadSize(r)
if err != nil {
return nil, err
}
arr := make([]UnionValue, n)
for i := range arr {
uv, err := readUnion(r, desc)
if err != nil {
return nil, err
}
arr[i] = uv
}
return arr, nil
}
func readVariant(r io.Reader) (VariantValue, error) {
desc, err := ReadFieldDesc(r)
if err != nil {
return VariantValue{}, err
}
v, err := ReadValue(r, desc)
if err != nil {
return VariantValue{}, err
}
return VariantValue{Desc: desc, Value: v}, nil
}
// WriteValue encodes v (which must match desc) into w.
func WriteValue(w io.Writer, desc FieldDesc, v Value) error {
switch desc.Kind {
case KindScalar:
return writeScalar(w, desc.TypeCode, v)
case KindScalarArr:
return writeScalarArray(w, desc.TypeCode, v)
case KindString:
return WriteString(w, v.(string))
case KindStringArr:
ss := v.([]string)
if err := WriteSize(w, int64(len(ss))); err != nil {
return err
}
for _, s := range ss {
if err := WriteString(w, s); err != nil {
return err
}
}
return nil
case KindStruct:
return writeStruct(w, desc, v.(StructValue))
case KindUnion:
return writeUnion(w, v.(UnionValue))
case KindVariant:
vv := v.(VariantValue)
if err := WriteFieldDesc(w, vv.Desc); err != nil {
return err
}
return WriteValue(w, vv.Desc, vv.Value)
default:
return fmt.Errorf("pvdata: WriteValue unsupported kind %d", desc.Kind)
}
}
func writeScalar(w io.Writer, tc byte, v Value) error {
switch tc {
case TypeCodeBoolean:
b := byte(0)
if v.(bool) {
b = 1
}
_, err := w.Write([]byte{b})
return err
case TypeCodeFloat:
return binary.Write(w, binary.LittleEndian, math.Float32bits(v.(float32)))
case TypeCodeDouble:
return binary.Write(w, binary.LittleEndian, math.Float64bits(v.(float64)))
default:
return binary.Write(w, binary.LittleEndian, v)
}
}
func writeScalarArray(w io.Writer, tc byte, v Value) error {
switch tc {
case TypeCodeDoubleArr:
arr := v.([]float64)
if err := WriteSize(w, int64(len(arr))); err != nil {
return err
}
bits := make([]uint64, len(arr))
for i, f := range arr {
bits[i] = math.Float64bits(f)
}
return binary.Write(w, binary.LittleEndian, bits)
case TypeCodeFloatArr:
arr := v.([]float32)
if err := WriteSize(w, int64(len(arr))); err != nil {
return err
}
bits := make([]uint32, len(arr))
for i, f := range arr {
bits[i] = math.Float32bits(f)
}
return binary.Write(w, binary.LittleEndian, bits)
default:
// for integer arrays, v is already []intN or []uintN
if err := WriteSize(w, int64(arrayLen(v))); err != nil {
return err
}
return binary.Write(w, binary.LittleEndian, v)
}
}
func writeStruct(w io.Writer, desc FieldDesc, sv StructValue) error {
for i, fv := range sv.Fields {
if err := WriteValue(w, desc.Fields[i].Desc, fv.Value); err != nil {
return fmt.Errorf("field %q: %w", fv.Name, err)
}
}
return nil
}
func writeUnion(w io.Writer, uv UnionValue) error {
if err := WriteSize(w, int64(uv.Selected)); err != nil {
return err
}
// caller must supply desc to encode value; not needed if Selected is null
return nil
}
// arrayLen returns reflect-free length for the supported array types.
func arrayLen(v Value) int {
switch x := v.(type) {
case []bool:
return len(x)
case []int8:
return len(x)
case []int16:
return len(x)
case []int32:
return len(x)
case []int64:
return len(x)
case []uint8:
return len(x)
case []uint16:
return len(x)
case []uint32:
return len(x)
case []uint64:
return len(x)
default:
return 0
}
}
+220
View File
@@ -0,0 +1,220 @@
package pvdata_test
import (
"bytes"
"testing"
"github.com/uopi/gopva/pvdata"
)
// ---- Compact size encoding -------------------------------------------
func TestCompactSize(t *testing.T) {
cases := []int64{0, 1, 10, 254, 255, 256, 1000, 65535, 70000, 1<<31 - 1}
for _, n := range cases {
var buf bytes.Buffer
if err := pvdata.WriteSize(&buf, n); err != nil {
t.Fatalf("WriteSize(%d): %v", n, err)
}
got, err := pvdata.ReadSize(&buf)
if err != nil {
t.Fatalf("ReadSize for %d: %v", n, err)
}
if got != n {
t.Errorf("round-trip size %d → %d", n, got)
}
}
}
// ---- String encoding ------------------------------------------------
func TestString(t *testing.T) {
cases := []string{"", "hello", "UOPI:TICK", "a string with spaces and unicode: é"}
for _, s := range cases {
var buf bytes.Buffer
if err := pvdata.WriteString(&buf, s); err != nil {
t.Fatalf("WriteString(%q): %v", s, err)
}
got, err := pvdata.ReadString(&buf)
if err != nil {
t.Fatalf("ReadString for %q: %v", s, err)
}
if got != s {
t.Errorf("round-trip string %q → %q", s, got)
}
}
}
// ---- FieldDesc (introspection) round-trip ---------------------------
func TestFieldDescScalar(t *testing.T) {
desc := pvdata.FieldDesc{Kind: pvdata.KindScalar, TypeCode: pvdata.TypeCodeDouble}
var buf bytes.Buffer
if err := pvdata.WriteFieldDesc(&buf, desc); err != nil {
t.Fatal(err)
}
got, err := pvdata.ReadFieldDesc(&buf)
if err != nil {
t.Fatal(err)
}
if got.TypeCode != desc.TypeCode || got.Kind != desc.Kind {
t.Errorf("scalar round-trip: got %+v, want %+v", got, desc)
}
}
func TestFieldDescStruct(t *testing.T) {
// Minimal NTScalar-like struct: value(double) + timeStamp(struct{…})
desc := pvdata.FieldDesc{
Kind: pvdata.KindStruct,
TypeCode: pvdata.TypeCodeStruct,
TypeID: "epics:nt/NTScalar:1.0",
Fields: []pvdata.Field{
{Name: "value", Desc: pvdata.FieldDesc{Kind: pvdata.KindScalar, TypeCode: pvdata.TypeCodeDouble}},
{Name: "alarm", Desc: pvdata.FieldDesc{
Kind: pvdata.KindStruct, TypeCode: pvdata.TypeCodeStruct, TypeID: "alarm_t",
Fields: []pvdata.Field{
{Name: "severity", Desc: pvdata.FieldDesc{Kind: pvdata.KindScalar, TypeCode: pvdata.TypeCodeInt}},
{Name: "status", Desc: pvdata.FieldDesc{Kind: pvdata.KindScalar, TypeCode: pvdata.TypeCodeInt}},
{Name: "message", Desc: pvdata.FieldDesc{Kind: pvdata.KindString, TypeCode: pvdata.TypeCodeString}},
},
}},
{Name: "timeStamp", Desc: pvdata.FieldDesc{
Kind: pvdata.KindStruct, TypeCode: pvdata.TypeCodeStruct, TypeID: "time_t",
Fields: []pvdata.Field{
{Name: "secondsPastEpoch", Desc: pvdata.FieldDesc{Kind: pvdata.KindScalar, TypeCode: pvdata.TypeCodeLong}},
{Name: "nanoseconds", Desc: pvdata.FieldDesc{Kind: pvdata.KindScalar, TypeCode: pvdata.TypeCodeInt}},
{Name: "userTag", Desc: pvdata.FieldDesc{Kind: pvdata.KindScalar, TypeCode: pvdata.TypeCodeInt}},
},
}},
},
}
var buf bytes.Buffer
if err := pvdata.WriteFieldDesc(&buf, desc); err != nil {
t.Fatal(err)
}
got, err := pvdata.ReadFieldDesc(&buf)
if err != nil {
t.Fatal(err)
}
if got.TypeID != desc.TypeID {
t.Errorf("typeID: got %q want %q", got.TypeID, desc.TypeID)
}
if len(got.Fields) != len(desc.Fields) {
t.Fatalf("field count: got %d want %d", len(got.Fields), len(desc.Fields))
}
for i, f := range desc.Fields {
if got.Fields[i].Name != f.Name {
t.Errorf("field[%d] name: got %q want %q", i, got.Fields[i].Name, f.Name)
}
}
}
// ---- Value round-trip -----------------------------------------------
func TestValueDouble(t *testing.T) {
desc := pvdata.FieldDesc{Kind: pvdata.KindScalar, TypeCode: pvdata.TypeCodeDouble}
var v pvdata.Value = float64(3.14159)
var buf bytes.Buffer
if err := pvdata.WriteValue(&buf, desc, v); err != nil {
t.Fatal(err)
}
got, err := pvdata.ReadValue(&buf, desc)
if err != nil {
t.Fatal(err)
}
if got.(float64) != v.(float64) {
t.Errorf("double round-trip: got %v want %v", got, v)
}
}
func TestValueIntArray(t *testing.T) {
desc := pvdata.FieldDesc{Kind: pvdata.KindScalarArr, TypeCode: pvdata.TypeCodeIntArr}
var v pvdata.Value = []int32{1, 2, 3, -7, 1<<30}
var buf bytes.Buffer
if err := pvdata.WriteValue(&buf, desc, v); err != nil {
t.Fatal(err)
}
got, err := pvdata.ReadValue(&buf, desc)
if err != nil {
t.Fatal(err)
}
arr := got.([]int32)
src := v.([]int32)
if len(arr) != len(src) {
t.Fatalf("int32 array length: got %d want %d", len(arr), len(src))
}
for i := range src {
if arr[i] != src[i] {
t.Errorf("int32[%d]: got %d want %d", i, arr[i], src[i])
}
}
}
func TestValueStruct(t *testing.T) {
desc := pvdata.FieldDesc{
Kind: pvdata.KindStruct,
TypeCode: pvdata.TypeCodeStruct,
TypeID: "test_t",
Fields: []pvdata.Field{
{Name: "x", Desc: pvdata.FieldDesc{Kind: pvdata.KindScalar, TypeCode: pvdata.TypeCodeDouble}},
{Name: "n", Desc: pvdata.FieldDesc{Kind: pvdata.KindScalar, TypeCode: pvdata.TypeCodeInt}},
{Name: "s", Desc: pvdata.FieldDesc{Kind: pvdata.KindString, TypeCode: pvdata.TypeCodeString}},
},
}
sv := pvdata.StructValue{
TypeID: "test_t",
Fields: []pvdata.FieldValue{
{Name: "x", Value: float64(2.718)},
{Name: "n", Value: int32(42)},
{Name: "s", Value: "hello"},
},
}
var buf bytes.Buffer
if err := pvdata.WriteValue(&buf, desc, sv); err != nil {
t.Fatal(err)
}
got, err := pvdata.ReadValue(&buf, desc)
if err != nil {
t.Fatal(err)
}
gsv := got.(pvdata.StructValue)
if gsv.Fields[0].Value.(float64) != sv.Fields[0].Value.(float64) {
t.Errorf("x field mismatch")
}
if gsv.Fields[1].Value.(int32) != sv.Fields[1].Value.(int32) {
t.Errorf("n field mismatch")
}
if gsv.Fields[2].Value.(string) != sv.Fields[2].Value.(string) {
t.Errorf("s field mismatch")
}
}
// ---- BitSet ---------------------------------------------------------
func TestBitSet(t *testing.T) {
var bs pvdata.BitSet
bs.Set(0)
bs.Set(3)
bs.Set(15)
bs.Set(16)
var buf bytes.Buffer
if err := pvdata.WriteBitSet(&buf, bs); err != nil {
t.Fatal(err)
}
got, err := pvdata.ReadBitSet(&buf)
if err != nil {
t.Fatal(err)
}
for _, idx := range []int{0, 3, 15, 16} {
if !got.Has(idx) {
t.Errorf("BitSet missing index %d after round-trip", idx)
}
}
for _, idx := range []int{1, 2, 4, 14, 17} {
if got.Has(idx) {
t.Errorf("BitSet has unexpected index %d", idx)
}
}
}