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()) }