Implemented hearthbit client side + tests

This commit is contained in:
Martino Ferrari
2026-08-09 18:51:51 +02:00
parent 915a192b16
commit 1ddb4fe356
12 changed files with 994 additions and 285 deletions
+58 -13
View File
@@ -172,27 +172,34 @@ const (
reconnectDelay = 2 * time.Second
readBufSize = 65536
udpRcvBufSize = 8 * 1024 * 1024
// keepAliveInterval is the unicast keepalive period. The UDPStreamer
// server evicts silent unicast clients after its ClientTimeout (default
// 30 s); an ACK from the same socket refreshes its last-seen without
// triggering a CONFIG resend (a CONNECT would).
keepAliveInterval = 15 * time.Second
)
// UDPClient manages the connection to one MARTe2 streamer source.
type UDPClient struct {
serverAddr string
sourceID string
hub *Hub
multicastGroup string
dataPort int
stopCh chan struct{}
serverAddr string
sourceID string
hub *Hub
multicastGroup string
dataPort int
keepAliveInterval time.Duration
stopCh chan struct{}
}
// NewUDPClient creates a UDPClient bound to a specific source ID.
func NewUDPClient(serverAddr, sourceID string, hub *Hub, multicastGroup string, dataPort int) *UDPClient {
return &UDPClient{
serverAddr: serverAddr,
sourceID: sourceID,
hub: hub,
multicastGroup: multicastGroup,
dataPort: dataPort,
stopCh: make(chan struct{}),
serverAddr: serverAddr,
sourceID: sourceID,
hub: hub,
multicastGroup: multicastGroup,
dataPort: dataPort,
keepAliveInterval: keepAliveInterval,
stopCh: make(chan struct{}),
}
}
@@ -253,6 +260,20 @@ func (u *UDPClient) runSession() error {
return err
}
log.Printf("[%s] udp: sent CONNECT", u.sourceID)
lastData := time.Now()
lastKeepAlive := time.Now()
// sendKeepAliveIfDue sends an ACK if the keepalive interval has elapsed.
// ACK refreshes the server's last-seen without re-sending CONFIG (which a
// repeated CONNECT would trigger).
sendKeepAliveIfDue := func() error {
if u.keepAliveInterval > 0 && time.Since(lastKeepAlive) >= u.keepAliveInterval {
if _, err := conn.WriteToUDP(udpsprotocol.BuildAckPacket(), serverAddr); err != nil {
return err
}
lastKeepAlive = time.Now()
}
return nil
}
reassembler := udpsprotocol.NewReassembler(2 * time.Second)
buf := make([]byte, readBufSize)
@@ -260,14 +281,34 @@ func (u *UDPClient) runSession() error {
var currentPublishMode uint8
for {
conn.SetReadDeadline(time.Now().Add(silenceTimeout))
// Wake up at least every keepalive interval so ACKs are sent even
// when the server is idle; the read deadline also doubles as the
// silence detector (no data for silenceTimeout = server gone).
wakeup := silenceTimeout
if u.keepAliveInterval > 0 && u.keepAliveInterval < wakeup {
wakeup = u.keepAliveInterval
}
conn.SetReadDeadline(time.Now().Add(wakeup))
n, _, err := conn.ReadFromUDP(buf)
arrivalTime := time.Now()
if err != nil {
if ne, ok := err.(net.Error); ok && ne.Timeout() {
if time.Since(lastData) >= silenceTimeout {
// True silence: stream is dead — Run() reconnects.
conn.WriteToUDP(udpsprotocol.BuildDisconnectPacket(), serverAddr)
return err
}
// Short wakeup: keepalive if due, then keep waiting.
if kaErr := sendKeepAliveIfDue(); kaErr != nil {
return kaErr
}
continue
}
conn.WriteToUDP(udpsprotocol.BuildDisconnectPacket(), serverAddr)
return err
}
lastData = arrivalTime
if n < udpsprotocol.HeaderSize {
log.Printf("[%s] udp: short datagram (%d bytes), skipping", u.sourceID, n)
@@ -334,6 +375,10 @@ func (u *UDPClient) runSession() error {
return nil
default:
}
if kaErr := sendKeepAliveIfDue(); kaErr != nil {
return kaErr
}
}
}