Files
MARTe_IO_Components/Client/WebUI/udpclient.go
T
2026-05-21 15:48:08 +02:00

168 lines
3.9 KiB
Go

package main
import (
"log"
"net"
"time"
)
const (
silenceTimeout = 5 * time.Second
reconnectDelay = 2 * time.Second
readBufSize = 65536
udpRcvBufSize = 8 * 1024 * 1024 // 8 MB OS receive buffer — absorbs bursts at high data rates
)
// UDPClient manages the UDP connection to one MARTe2 streamer source.
type UDPClient struct {
serverAddr string
sourceID string
hub *Hub
stopCh chan struct{}
}
// NewUDPClient creates a UDPClient bound to a specific source ID. Call Run() in a goroutine.
func NewUDPClient(serverAddr, sourceID string, hub *Hub) *UDPClient {
return &UDPClient{
serverAddr: serverAddr,
sourceID: sourceID,
hub: hub,
stopCh: make(chan struct{}),
}
}
// Stop asks the client to disconnect and exit.
func (u *UDPClient) Stop() {
close(u.stopCh)
}
// Run is the main loop; it reconnects automatically if the server goes silent.
func (u *UDPClient) Run() {
for {
select {
case <-u.stopCh:
return
default:
}
u.hub.SetSourceState(u.sourceID, "connecting")
log.Printf("[%s] udp: connecting to %s", u.sourceID, u.serverAddr)
err := u.runSession()
if err != nil {
log.Printf("[%s] udp: session ended: %v", u.sourceID, err)
}
u.hub.SetSourceState(u.sourceID, "disconnected")
select {
case <-u.stopCh:
return
case <-time.After(reconnectDelay):
}
}
}
// runSession opens a UDP socket, sends CONNECT, reads data until the server
// goes silent or an error occurs.
func (u *UDPClient) runSession() error {
// Port 0 lets the OS pick a free local port.
conn, err := net.ListenUDP("udp4", &net.UDPAddr{})
if err != nil {
return err
}
defer conn.Close()
// Increase OS receive buffer to reduce kernel-level packet drops at high data rates.
if err := conn.SetReadBuffer(udpRcvBufSize); err != nil {
log.Printf("[%s] udp: SetReadBuffer: %v (proceeding with OS default)", u.sourceID, err)
}
serverAddr, err := net.ResolveUDPAddr("udp4", u.serverAddr)
if err != nil {
return err
}
if _, err := conn.WriteToUDP(BuildConnectPacket(), serverAddr); err != nil {
return err
}
log.Printf("[%s] udp: sent CONNECT", u.sourceID)
reassembler := NewReassembler(2 * time.Second)
buf := make([]byte, readBufSize)
var currentSigs []SignalInfo
for {
conn.SetReadDeadline(time.Now().Add(silenceTimeout))
n, _, err := conn.ReadFromUDP(buf)
arrivalTime := time.Now()
if err != nil {
conn.WriteToUDP(BuildDisconnectPacket(), serverAddr)
return err
}
if n < HeaderSize {
log.Printf("[%s] udp: short datagram (%d bytes), skipping", u.sourceID, n)
continue
}
hdr, err := ParseHeader(buf[:n])
if err != nil {
log.Printf("[%s] udp: parse header: %v", u.sourceID, err)
continue
}
payload := make([]byte, n-HeaderSize)
copy(payload, buf[HeaderSize:n])
complete, ok := reassembler.AddFragment(hdr, payload)
if hdr.Type == PktData {
u.hub.RecordDataFragment(u.sourceID, hdr.Counter, n, arrivalTime.UnixNano(), ok)
}
if !ok {
continue
}
switch hdr.Type {
case PktConfig:
sigs, err := ParseConfig(complete)
if err != nil {
log.Printf("[%s] udp: parse config: %v", u.sourceID, err)
continue
}
currentSigs = sigs
log.Printf("[%s] udp: received CONFIG (%d signals)", u.sourceID, len(sigs))
u.hub.SetSourceState(u.sourceID, "connected")
u.hub.UpdateConfigForSource(u.sourceID, sigs)
case PktData:
if len(currentSigs) == 0 {
continue
}
sample, err := ParseData(complete, currentSigs, arrivalTime)
if err != nil {
log.Printf("[%s] udp: parse data: %v", u.sourceID, err)
continue
}
u.hub.PushDataForSource(u.sourceID, sample)
case PktACK:
log.Printf("[%s] udp: received ACK (counter=%d)", u.sourceID, hdr.Counter)
case PktDisconnect:
log.Printf("[%s] udp: server sent DISCONNECT", u.sourceID)
return nil
default:
log.Printf("[%s] udp: unknown packet type %d", u.sourceID, hdr.Type)
}
select {
case <-u.stopCh:
conn.WriteToUDP(BuildDisconnectPacket(), serverAddr)
return nil
default:
}
}
}