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
+61 -57
View File
@@ -53,19 +53,17 @@ type monState struct {
// - sid, dbfType, count, access: valid only while readyC is closed.
// - readyC: closed when CREATE_CHAN reply received; replaced on reconnect.
// - monitors: append-only (except removeMonitor); never cleared on reconnect.
// - pending: ioid → GET reply channel; drained (channels closed) on reconnect.
type chanState struct {
cid uint32
pvName string
mu sync.RWMutex
sid uint32 // server-assigned channel ID (0 until CREATE_CHAN reply)
dbfType int // native DBF field type
count uint32 // element count
access uint32 // AccessRead | AccessWrite bitmask
readyC chan struct{} // closed once CREATE_CHAN reply is received
monitors []*monState // active subscriptions
pending map[uint32]chan reply // ioid → one-shot GET callback
sid uint32 // server-assigned channel ID (0 until CREATE_CHAN reply)
dbfType int // native DBF field type
count uint32 // element count
access uint32 // AccessRead | AccessWrite bitmask
readyC chan struct{} // closed once CREATE_CHAN reply is received
monitors []*monState // active subscriptions
}
func newChanState(cid uint32, pvName string) *chanState {
@@ -73,7 +71,6 @@ func newChanState(cid uint32, pvName string) *chanState {
cid: cid,
pvName: pvName,
readyC: make(chan struct{}),
pending: make(map[uint32]chan reply),
}
}
@@ -81,16 +78,12 @@ func newChanState(cid uint32, pvName string) *chanState {
// It closes the current readyC (waking any waitReady callers so they can
// re-wait on the fresh channel) and then replaces it.
// Must be called before the circuit sends CREATE_CHAN on the new conn.
// In-flight GET requests are tracked in circuit.byIOID and cleared there.
func (cs *chanState) resetForReconnect() {
cs.mu.Lock()
cs.sid = 0
old := cs.readyC
cs.readyC = make(chan struct{})
// Drain pending GETs — they will receive zero reply (ok=false).
for id, ch := range cs.pending {
close(ch)
delete(cs.pending, id)
}
cs.mu.Unlock()
// Close the old readyC outside the lock to avoid deadlock with waitReady.
select {
@@ -150,7 +143,9 @@ type circuit struct {
mu sync.RWMutex
channels []*chanState // append-only; survive reconnect
byCID map[uint32]*chanState // cid → chanState (fast lookup)
bySID map[uint32]*chanState // sid → chanState (fast lookup)
bySubID map[uint32]*monState // subID → monState (fast event dispatch)
byIOID map[uint32]chan reply // ioid → GET/PUT callback (circuit-wide)
writeQ chan []byte // serialised outbound message queue
seq atomic.Uint32 // shared ID sequence (cid, ioid, subID)
@@ -165,7 +160,9 @@ func newCircuit(ctx context.Context, addr, clientName, hostName string) *circuit
ctx: cctx,
cancel: cancel,
byCID: make(map[uint32]*chanState),
bySID: make(map[uint32]*chanState),
bySubID: make(map[uint32]*monState),
byIOID: make(map[uint32]chan reply),
writeQ: make(chan []byte, writeQueueDepth),
}
go c.run()
@@ -204,6 +201,12 @@ func (c *circuit) run() {
// Snapshot channels and reset their per-connection state.
c.mu.Lock()
c.bySID = make(map[uint32]*chanState)
// Close all in-flight GET reply channels so waiters unblock with ok=false.
for _, ch := range c.byIOID {
close(ch)
}
c.byIOID = make(map[uint32]chan reply)
for _, cs := range c.channels {
cs.resetForReconnect()
}
@@ -361,9 +364,13 @@ func (c *circuit) dispatch(conn net.Conn, hdr proto.Header, payload []byte) {
case proto.CmdCreateChan:
// Parameter1 = cid (echoed), Parameter2 = SID assigned by server.
c.mu.RLock()
c.mu.Lock()
cs, ok := c.byCID[hdr.Parameter1]
c.mu.RUnlock()
if ok {
c.bySID[hdr.Parameter2] = cs
}
c.mu.Unlock()
if !ok {
return
}
@@ -425,21 +432,23 @@ func (c *circuit) dispatch(conn net.Conn, hdr proto.Header, payload []byte) {
}
case proto.CmdEventAdd:
// Monitor update: Parameter1 = subscriptionID.
// Monitor update: server puts subscription ID in Parameter2 (m_available).
// Parameter1 carries ECA_NORMAL (1) as a status code.
subID := hdr.Parameter2
c.mu.RLock()
ms, ok := c.bySubID[hdr.Parameter1]
ms, ok := c.bySubID[subID]
c.mu.RUnlock()
if !ok {
dbg("CA EVENT_ADD for unknown subID", "subID", hdr.Parameter1)
dbg("CA EVENT_ADD for unknown subID", "subID", subID)
return
}
tv, ok := proto.DecodeTimeValue(hdr.DataType, hdr.DataCount, payload)
if !ok {
dbg("CA EVENT_ADD decode failed", "subID", hdr.Parameter1,
dbg("CA EVENT_ADD decode failed", "subID", subID,
"dbrType", hdr.DataType, "count", hdr.DataCount, "payloadLen", len(payload))
return
}
dbg("CA EVENT_ADD value", "subID", hdr.Parameter1, "dbrType", hdr.DataType,
dbg("CA EVENT_ADD value", "subID", subID, "dbrType", hdr.DataType,
"double", tv.Double, "severity", tv.Severity)
select {
case ms.ch <- tv:
@@ -447,25 +456,20 @@ func (c *circuit) dispatch(conn net.Conn, hdr proto.Header, payload []byte) {
}
case proto.CmdReadNotify:
// GET reply: Parameter1 = ioid.
ioid := hdr.Parameter1
c.mu.RLock()
var replyCh chan reply
var found bool
for _, cs := range c.channels {
cs.mu.Lock()
if ch, exists := cs.pending[ioid]; exists {
delete(cs.pending, ioid)
replyCh = ch
found = true
}
cs.mu.Unlock()
if found {
break
}
// Parameter2 = ioid (our request ID echoed back by server).
ioid := hdr.Parameter2
c.mu.Lock()
replyCh, ok := c.byIOID[ioid]
if ok {
delete(c.byIOID, ioid)
}
c.mu.RUnlock()
if found {
c.mu.Unlock()
dbg("CA CmdReadNotify", "ioid", ioid, "found", ok, "dbrType", hdr.DataType,
"count", hdr.DataCount, "payloadSize", hdr.PayloadSize)
if ok {
select {
case replyCh <- reply{hdr: hdr, payload: payload, ok: true}:
default:
@@ -615,9 +619,9 @@ func (c *circuit) getRaw(ctx context.Context, cs *chanState, dbrType uint16, cou
ioid := c.nextID()
replyCh := make(chan reply, 1)
cs.mu.Lock()
cs.pending[ioid] = replyCh
cs.mu.Unlock()
c.mu.Lock()
c.byIOID[ioid] = replyCh
c.mu.Unlock()
msg := proto.BuildMessage(proto.Header{
Command: proto.CmdReadNotify,
@@ -630,9 +634,9 @@ func (c *circuit) getRaw(ctx context.Context, cs *chanState, dbrType uint16, cou
select {
case c.writeQ <- msg:
case <-ctx.Done():
cs.mu.Lock()
delete(cs.pending, ioid)
cs.mu.Unlock()
c.mu.Lock()
delete(c.byIOID, ioid)
c.mu.Unlock()
return proto.Header{}, nil, ctx.Err()
}
@@ -643,9 +647,9 @@ func (c *circuit) getRaw(ctx context.Context, cs *chanState, dbrType uint16, cou
}
return r.hdr, r.payload, nil
case <-ctx.Done():
cs.mu.Lock()
delete(cs.pending, ioid)
cs.mu.Unlock()
c.mu.Lock()
delete(c.byIOID, ioid)
c.mu.Unlock()
return proto.Header{}, nil, ctx.Err()
}
}
@@ -667,9 +671,9 @@ func (c *circuit) get(ctx context.Context, cs *chanState, dbrType uint16, count
ioid := c.nextID()
replyCh := make(chan reply, 1)
cs.mu.Lock()
cs.pending[ioid] = replyCh
cs.mu.Unlock()
c.mu.Lock()
c.byIOID[ioid] = replyCh
c.mu.Unlock()
msg := proto.BuildMessage(proto.Header{
Command: proto.CmdReadNotify,
@@ -682,9 +686,9 @@ func (c *circuit) get(ctx context.Context, cs *chanState, dbrType uint16, count
select {
case c.writeQ <- msg:
case <-ctx.Done():
cs.mu.Lock()
delete(cs.pending, ioid)
cs.mu.Unlock()
c.mu.Lock()
delete(c.byIOID, ioid)
c.mu.Unlock()
return proto.TimeValue{}, ctx.Err()
}
@@ -699,9 +703,9 @@ func (c *circuit) get(ctx context.Context, cs *chanState, dbrType uint16, count
}
return tv, nil
case <-ctx.Done():
cs.mu.Lock()
delete(cs.pending, ioid)
cs.mu.Unlock()
c.mu.Lock()
delete(c.byIOID, ioid)
c.mu.Unlock()
return proto.TimeValue{}, ctx.Err()
}
}
+21 -18
View File
@@ -85,7 +85,7 @@ func caString(b []byte) string {
return string(b[:idx])
}
// DBR_TIME_* wire layouts (all big-endian):
// DBR_TIME_* wire layouts (all big-endian, matching EPICS db_access.h structs):
//
// Common header (12 bytes):
// [0:2] int16 status
@@ -93,13 +93,13 @@ func caString(b []byte) string {
// [4:8] uint32 secPastEpoch
// [8:12] uint32 nsec
//
// DBR_TIME_DOUBLE (type 25): 4-byte RISC pad at [12:16], float64 at [16:24]. Total=24.
// DBR_TIME_FLOAT (type 20): float32 at [12:16]. Total=16.
// DBR_TIME_LONG (type 22): int32 at [12:16]. Total=16.
// DBR_TIME_SHORT (type 19): int16 at [12:14], 2-byte pad [14:16]. Total=16.
// DBR_TIME_ENUM (type 23): uint16 at [12:14], 2-byte pad [14:16]. Total=16.
// DBR_TIME_CHAR (type 24): uint8 at [12:13], 3-byte pad [13:16]. Total=16.
// DBR_TIME_STRING (type 21): [40]byte at [12:52]. Total=52.
// DBR_TIME_STRING (type 14): [40]byte at [12:52]. Total=52.
// DBR_TIME_SHORT (type 15): 2-byte RISC pad at [12:14], int16 at [14:16]. Total=16.
// DBR_TIME_FLOAT (type 16): float32 at [12:16]. Total=16.
// DBR_TIME_ENUM (type 17): 2-byte RISC pad at [12:14], uint16 at [14:16]. Total=16.
// DBR_TIME_CHAR (type 18): RISC_pad0[12:14], RISC_pad1[14], uint8 at [15]. Total=16.
// DBR_TIME_LONG (type 19): int32 at [12:16]. Total=16.
// DBR_TIME_DOUBLE (type 20): 4-byte RISC pad at [12:16], float64 at [16:24]. Total=24.
// DecodeTimeValue decodes a DBR_TIME_* payload.
// dbrType is one of the DBRTime* constants; payload is the full message payload
@@ -158,24 +158,27 @@ func DecodeTimeValue(dbrType uint16, count uint32, payload []byte) (TimeValue, b
tv.Double = float64(tv.Long)
case DBRTimeShort:
// 2-byte RISC pad at [12:14], value at [14:16]
if len(payload) < 16 {
return TimeValue{}, false
}
tv.Short = int16(binary.BigEndian.Uint16(payload[12:]))
tv.Short = int16(binary.BigEndian.Uint16(payload[14:]))
tv.Double = float64(tv.Short)
case DBRTimeEnum:
// 2-byte RISC pad at [12:14], value at [14:16]
if len(payload) < 16 {
return TimeValue{}, false
}
tv.Enum = binary.BigEndian.Uint16(payload[12:])
tv.Enum = binary.BigEndian.Uint16(payload[14:])
tv.Double = float64(tv.Enum)
case DBRTimeChar:
// RISC_pad0[12:14], RISC_pad1[14], value[15] per db_access.h dbr_time_char
if len(payload) < 16 {
return TimeValue{}, false
}
tv.Char = payload[12]
tv.Char = payload[15]
tv.Double = float64(tv.Char)
case DBRTimeString:
@@ -427,15 +430,15 @@ func EncodeString(v string) []byte {
// EncodeEventMask builds the 16-byte payload for a CA_PROTO_EVENT_ADD request.
// mask is typically DBEDefault (DBEValue | DBEAlarm).
//
// Wire layout (16 bytes, all big-endian):
// Wire layout matches caProto.h struct mon_info (all big-endian):
//
// [0:4] float32 m_lval (not used, zero)
// [4:8] float32 p_delta (delta trigger, zero = disabled)
// [8:12] float32 p_final (final trigger, zero = disabled)
// [12:14] int16 p_count (element count, zero = use PV's count)
// [14:16] int16 m_mask (DBE_VALUE | DBE_ALARM | ...)
// [0:4] float32 m_lval (low delta, zero = disabled)
// [4:8] float32 m_hval (high delta, zero = disabled)
// [8:12] float32 m_toval (period between samples, zero = disabled)
// [12:14] uint16 m_mask (DBE_VALUE | DBE_ALARM | ...)
// [14:16] uint16 m_pad (alignment padding)
func EncodeEventMask(mask uint16) []byte {
b := make([]byte, 16)
binary.BigEndian.PutUint16(b[14:], mask) // m_mask is at offset 14, not 12
binary.BigEndian.PutUint16(b[12:], mask) // m_mask is at offset 12 per caProto.h
return b
}
+8 -7
View File
@@ -74,14 +74,15 @@ const (
)
// DBR_TIME_* types (value + timestamp + alarm status; used in EVENT_ADD).
// Numbers from db_access.h: STRING=14, SHORT/INT=15, FLOAT=16, ENUM=17, CHAR=18, LONG=19, DOUBLE=20.
const (
DBRTimeString = 21
DBRTimeShort = 19
DBRTimeFloat = 20
DBRTimeEnum = 23
DBRTimeChar = 24
DBRTimeLong = 22
DBRTimeDouble = 25
DBRTimeString = 14
DBRTimeShort = 15
DBRTimeFloat = 16
DBRTimeEnum = 17
DBRTimeChar = 18
DBRTimeLong = 19
DBRTimeDouble = 20
)
// DBR_CTRL_* types (full control info: units, limits, enum strings; used in READ_NOTIFY).
+6 -6
View File
@@ -159,7 +159,7 @@ func TestDecodeTimeString(t *testing.T) {
func TestDecodeTimeEnum(t *testing.T) {
hdr := buildTimeHeader(0, 0, 0, 0)
val := []byte{0x00, 0x03, 0x00, 0x00} // enum=3 + 2-byte pad
val := []byte{0x00, 0x00, 0x00, 0x03} // 2-byte RISC pad + enum=3
payload := append(hdr, val...)
tv, ok := proto.DecodeTimeValue(proto.DBRTimeEnum, 1, payload)
@@ -307,14 +307,14 @@ func TestEncodeEventMask(t *testing.T) {
if len(b) != 16 {
t.Fatalf("len = %d, want 16", len(b))
}
// m_mask is at offset 14 (after m_lval[0:4], p_delta[4:8], p_final[8:12], p_count[12:14]).
mask := binary.BigEndian.Uint16(b[14:])
// m_mask is at offset 12 per caProto.h struct mon_info.
mask := binary.BigEndian.Uint16(b[12:])
if mask != proto.DBEDefault {
t.Errorf("mask = 0x%02X, want 0x%02X", mask, proto.DBEDefault)
}
// p_count at [12:14] must be zero.
if pc := binary.BigEndian.Uint16(b[12:]); pc != 0 {
t.Errorf("p_count = %d, want 0", pc)
// m_pad at [14:16] must be zero.
if pad := binary.BigEndian.Uint16(b[14:]); pad != 0 {
t.Errorf("m_pad = %d, want 0", pad)
}
}
+11 -7
View File
@@ -338,9 +338,11 @@ func (s *Server) handleConn(conn net.Conn) {
ioid := hdr.Parameter2
var pvName string
for _, info := range channels {
var cid uint32
for c, info := range channels {
if info.sid == sid {
pvName = info.pvName
cid = c
break
}
}
@@ -368,7 +370,8 @@ func (s *Server) handleConn(conn net.Conn) {
Command: proto.CmdReadNotify,
DataType: hdr.DataType,
DataCount: hdr.DataCount,
Parameter1: ioid,
Parameter1: cid, // echo client channel ID (matches real EPICS IOC)
Parameter2: ioid, // echo ioid so client can match the reply
}, replyPayload)
conn.Write(reply)
@@ -430,7 +433,8 @@ func (s *Server) buildEventMsg(sub *serverSub, pv *serverPV) []byte {
Command: proto.CmdEventAdd,
DataType: sub.dbrType,
DataCount: sub.count,
Parameter1: sub.subID,
Parameter1: 1, // ECA_NORMAL
Parameter2: sub.subID,
}, payload)
}
@@ -461,12 +465,12 @@ func (s *Server) encodeTimeValue(dbrType uint16, _ uint32, val any) []byte {
binary.BigEndian.PutUint32(body, uint32(int32(toF64(val))))
case proto.DBRTimeShort, proto.DBRTimeEnum:
body = make([]byte, 4) // 2 value + 2 pad
binary.BigEndian.PutUint16(body, uint16(int16(toF64(val))))
body = make([]byte, 4) // 2-byte RISC pad + 2-byte value
binary.BigEndian.PutUint16(body[2:], uint16(int16(toF64(val))))
case proto.DBRTimeChar:
body = make([]byte, 4) // 1 value + 3 pad
body[0] = byte(uint8(toF64(val)))
body = make([]byte, 4) // RISC_pad0[0:2] + RISC_pad1[2] + value[3]
body[3] = byte(uint8(toF64(val)))
case proto.DBRTimeString:
body = make([]byte, 40)