package wshub import ( "encoding/json" "fmt" "io" "log" "net" "os" "strconv" "strings" "sync" "sync/atomic" "time" "marte2/common/udpsprotocol" ) // ─── Source configuration ───────────────────────────────────────────────────── // SourceConfig is the serialisable description of one data source (for file save/load). type SourceConfig struct { Label string `json:"label"` Addr string `json:"addr"` MulticastGroup string `json:"multicastGroup,omitempty"` DataPort int `json:"dataPort,omitempty"` } // managedSource is the SourceManager's view of one running source. type managedSource struct { id string label string addr string multicastGroup string dataPort int client *UDPClient } // SourceManager owns the lifecycle of all active data sources. type SourceManager struct { mu sync.RWMutex sources map[string]*managedSource hub *Hub filePath string nextID atomic.Int32 } // NewSourceManager creates a SourceManager bound to the given hub. func NewSourceManager(hub *Hub, filePath string) *SourceManager { return &SourceManager{ sources: make(map[string]*managedSource), hub: hub, filePath: filePath, } } func (sm *SourceManager) genID() string { return fmt.Sprintf("s%d", sm.nextID.Add(1)) } // Add creates a new source and starts connecting. func (sm *SourceManager) Add(label, addr, multicastGroup string, dataPort int) string { if label == "" { label = addr } id := sm.genID() c := NewUDPClient(addr, id, sm.hub, multicastGroup, dataPort) ms := &managedSource{ id: id, label: label, addr: addr, multicastGroup: multicastGroup, dataPort: dataPort, client: c, } sm.mu.Lock() sm.sources[id] = ms sm.mu.Unlock() sm.hub.AddSource(id, label, addr) go c.Run() return id } // Remove stops the source and removes it from the hub. func (sm *SourceManager) Remove(id string) { sm.mu.Lock() ms, ok := sm.sources[id] if ok { delete(sm.sources, id) } sm.mu.Unlock() if ok { ms.client.Stop() sm.hub.RemoveSource(id) } } // Save writes the current source list to filePath. func (sm *SourceManager) Save() error { if sm.filePath == "" { return fmt.Errorf("no sources-file configured") } sm.mu.RLock() cfgs := make([]SourceConfig, 0, len(sm.sources)) for _, ms := range sm.sources { cfgs = append(cfgs, SourceConfig{ Label: ms.label, Addr: ms.addr, MulticastGroup: ms.multicastGroup, DataPort: ms.dataPort, }) } sm.mu.RUnlock() data, err := json.MarshalIndent(cfgs, "", " ") if err != nil { return err } return os.WriteFile(sm.filePath, data, 0644) } // Load reads sources from path and adds them. func (sm *SourceManager) Load(path string) error { data, err := os.ReadFile(path) if err != nil { return err } var cfgs []SourceConfig if err := json.Unmarshal(data, &cfgs); err != nil { return err } sm.filePath = path for _, cfg := range cfgs { sm.Add(cfg.Label, cfg.Addr, cfg.MulticastGroup, cfg.DataPort) } return nil } // ParseSourceArg parses "label@host:port" or "host:port". func ParseSourceArg(s string) (label, addr string) { label, addr, _, _ = ParseSourceArgFull(s) return } // ParseSourceArgFull parses "[label@]host:port[/multicastGroup:dataPort]". func ParseSourceArgFull(s string) (label, addr, multicastGroup string, dataPort int) { s = strings.TrimSpace(s) rest := s if idx := strings.Index(s, "@"); idx >= 0 { label = strings.TrimSpace(s[:idx]) rest = strings.TrimSpace(s[idx+1:]) } if idx := strings.Index(rest, "/"); idx >= 0 { addr = strings.TrimSpace(rest[:idx]) mcastPart := strings.TrimSpace(rest[idx+1:]) if lastColon := strings.LastIndex(mcastPart, ":"); lastColon >= 0 { multicastGroup = strings.TrimSpace(mcastPart[:lastColon]) dataPort, _ = strconv.Atoi(strings.TrimSpace(mcastPart[lastColon+1:])) } else { multicastGroup = mcastPart } } else { addr = rest } return } // ─── UDPClient ──────────────────────────────────────────────────────────────── const ( silenceTimeout = 5 * time.Second reconnectDelay = 2 * time.Second readBufSize = 65536 udpRcvBufSize = 8 * 1024 * 1024 ) // 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{} } // 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{}), } } // 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] connecting to %s", u.sourceID, u.serverAddr) var err error if u.multicastGroup != "" { err = u.runMulticastSession() } else { err = u.runSession() } if err != nil { log.Printf("[%s] 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 silent or error. func (u *UDPClient) runSession() error { conn, err := net.ListenUDP("udp4", &net.UDPAddr{}) if err != nil { return err } defer conn.Close() if err := conn.SetReadBuffer(udpRcvBufSize); err != nil { log.Printf("[%s] udp: SetReadBuffer: %v", u.sourceID, err) } serverAddr, err := net.ResolveUDPAddr("udp4", u.serverAddr) if err != nil { return err } if _, err := conn.WriteToUDP(udpsprotocol.BuildConnectPacket(), serverAddr); err != nil { return err } log.Printf("[%s] udp: sent CONNECT", u.sourceID) reassembler := udpsprotocol.NewReassembler(2 * time.Second) buf := make([]byte, readBufSize) var currentSigs []udpsprotocol.SignalInfo var currentPublishMode uint8 for { conn.SetReadDeadline(time.Now().Add(silenceTimeout)) n, _, err := conn.ReadFromUDP(buf) arrivalTime := time.Now() if err != nil { conn.WriteToUDP(udpsprotocol.BuildDisconnectPacket(), serverAddr) return err } if n < udpsprotocol.HeaderSize { log.Printf("[%s] udp: short datagram (%d bytes), skipping", u.sourceID, n) continue } hdr, err := udpsprotocol.ParseHeader(buf[:n]) if err != nil { log.Printf("[%s] udp: parse header: %v", u.sourceID, err) continue } payload := make([]byte, n-udpsprotocol.HeaderSize) copy(payload, buf[udpsprotocol.HeaderSize:n]) complete, ok := reassembler.AddFragment(hdr, payload) if hdr.Type == udpsprotocol.PktData { u.hub.RecordDataFragment(u.sourceID, hdr.Counter, n, arrivalTime.UnixNano(), ok) } if !ok { continue } switch hdr.Type { case udpsprotocol.PktConfig: sigs, pm, err := udpsprotocol.ParseConfig(complete) if err != nil { log.Printf("[%s] udp: parse config: %v", u.sourceID, err) continue } currentSigs = sigs currentPublishMode = pm log.Printf("[%s] udp: received CONFIG (%d signals, publishMode=%d)", u.sourceID, len(sigs), pm) u.hub.SetSourceState(u.sourceID, "connected") u.hub.UpdateConfigForSource(u.sourceID, sigs) case udpsprotocol.PktData: if len(currentSigs) == 0 { continue } samples, err := udpsprotocol.ParseData(complete, currentSigs, currentPublishMode, arrivalTime) if err != nil { log.Printf("[%s] udp: parse data: %v", u.sourceID, err) continue } for _, s := range samples { u.hub.PushDataForSource(u.sourceID, s) } case udpsprotocol.PktACK: log.Printf("[%s] udp: received ACK (counter=%d)", u.sourceID, hdr.Counter) case udpsprotocol.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(udpsprotocol.BuildDisconnectPacket(), serverAddr) return nil default: } } } // runMulticastSession handles the multicast mode session. func (u *UDPClient) runMulticastSession() error { tcpAddr, err := net.ResolveTCPAddr("tcp4", u.serverAddr) if err != nil { return err } tcpConn, err := net.DialTCP("tcp4", nil, tcpAddr) if err != nil { return err } defer tcpConn.Close() if _, err := tcpConn.Write(udpsprotocol.BuildConnectPacket()); err != nil { return err } log.Printf("[%s] tcp: sent CONNECT to %s", u.sourceID, u.serverAddr) hdrBuf := make([]byte, udpsprotocol.HeaderSize) if _, err := io.ReadFull(tcpConn, hdrBuf); err != nil { return err } cfgHdr, err := udpsprotocol.ParseHeader(hdrBuf) if err != nil { return err } if cfgHdr.Type != udpsprotocol.PktConfig { return net.ErrClosed } cfgPayload := make([]byte, cfgHdr.PayloadBytes) if cfgHdr.PayloadBytes > 0 { if _, err := io.ReadFull(tcpConn, cfgPayload); err != nil { return err } } currentSigs, currentPublishMode, err := udpsprotocol.ParseConfig(cfgPayload) if err != nil { return err } log.Printf("[%s] tcp: received CONFIG (%d signals, publishMode=%d)", u.sourceID, len(currentSigs), currentPublishMode) u.hub.SetSourceState(u.sourceID, "connected") u.hub.UpdateConfigForSource(u.sourceID, currentSigs) mcastPort := u.dataPort if mcastPort == 0 { mcastPort = tcpAddr.Port + 1 } mcastIP := net.ParseIP(u.multicastGroup) if mcastIP == nil { return &net.AddrError{Err: "invalid multicast group IP", Addr: u.multicastGroup} } mcastAddr := &net.UDPAddr{IP: mcastIP, Port: mcastPort} mcastConn, err := net.ListenMulticastUDP("udp4", nil, mcastAddr) if err != nil { return err } defer mcastConn.Close() if err := mcastConn.SetReadBuffer(udpRcvBufSize); err != nil { log.Printf("[%s] multicast SetReadBuffer: %v", u.sourceID, err) } log.Printf("[%s] joined multicast %s:%s", u.sourceID, u.multicastGroup, strconv.Itoa(mcastPort)) tcpDone := make(chan error, 1) go func() { buf := make([]byte, udpsprotocol.HeaderSize+64) for { n, readErr := tcpConn.Read(buf) if readErr != nil { tcpDone <- readErr return } if n >= udpsprotocol.HeaderSize { hdr, parseErr := udpsprotocol.ParseHeader(buf[:n]) if parseErr == nil && hdr.Type == udpsprotocol.PktDisconnect { tcpDone <- nil return } } } }() reassembler := udpsprotocol.NewReassembler(2 * time.Second) buf := make([]byte, readBufSize) for { mcastConn.SetReadDeadline(time.Now().Add(silenceTimeout)) n, _, readErr := mcastConn.ReadFromUDP(buf) arrivalTime := time.Now() if readErr != nil { select { case <-tcpDone: return nil default: } tcpConn.Write(udpsprotocol.BuildDisconnectPacket()) return readErr } if n < udpsprotocol.HeaderSize { continue } hdr, parseErr := udpsprotocol.ParseHeader(buf[:n]) if parseErr != nil { log.Printf("[%s] multicast: parse header: %v", u.sourceID, parseErr) continue } payload := make([]byte, n-udpsprotocol.HeaderSize) copy(payload, buf[udpsprotocol.HeaderSize:n]) complete, ok := reassembler.AddFragment(hdr, payload) if hdr.Type == udpsprotocol.PktData { u.hub.RecordDataFragment(u.sourceID, hdr.Counter, n, arrivalTime.UnixNano(), ok) } if !ok { continue } if hdr.Type == udpsprotocol.PktData { if len(currentSigs) == 0 { continue } samples, parseErr := udpsprotocol.ParseData(complete, currentSigs, currentPublishMode, arrivalTime) if parseErr != nil { log.Printf("[%s] multicast: parse data: %v", u.sourceID, parseErr) continue } for _, s := range samples { u.hub.PushDataForSource(u.sourceID, s) } } select { case <-u.stopCh: tcpConn.Write(udpsprotocol.BuildDisconnectPacket()) return nil case tcpErr := <-tcpDone: log.Printf("[%s] tcp control closed: %v", u.sourceID, tcpErr) return nil default: } } }