Add TCP control + UDP multicast mode to UDPStreamer

DataSource (C++):
- New optional config: MulticastGroup (e.g. "239.0.0.1") and DataPort
  (default Port+1); when set, enables multicast mode; absent = unicast
- Control plane: BasicTCPSocket listener on Port; CONNECT received over
  TCP triggers HandleTCPConnect() which sends CONFIG via the same TCP
  connection
- Data plane: BasicUDPSocket aimed at MulticastGroup:DataPort; all DATA
  fragments sent via dataSocket.Write() so any joined client receives them
- useMulticast is the single branch point; every unicast code path is
  unchanged when MulticastGroup is absent
- New methods: HandleTCPConnect(), IsMulticast()
- 5 new unit tests (ports 44710-44729); all 38 tests passing

WebUI hub (Go):
- SourceConfig gains MulticastGroup and DataPort fields (JSON, optional)
- UDPClient gains multicastGroup/dataPort; Run() routes to new
  runMulticastSession() when multicastGroup is non-empty
- runMulticastSession(): TCP dial for CONNECT/CONFIG, net.ListenMulticastUDP
  for DATA, background goroutine watches TCP for DISCONNECT/close
- All existing sm.Add() call sites updated (unicast callers pass "", 0)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino Ferrari
2026-05-26 22:56:27 +02:00
parent f85ab8652c
commit b15e637f14
9 changed files with 864 additions and 58 deletions
+26 -11
View File
@@ -11,16 +11,20 @@ import (
// SourceConfig is the serialisable description of one data source (for file save/load).
type SourceConfig struct {
Label string `json:"label"`
Addr string `json:"addr"`
Label string `json:"label"`
Addr string `json:"addr"`
MulticastGroup string `json:"multicastGroup,omitempty"` // "" = unicast mode
DataPort int `json:"dataPort,omitempty"` // 0 = Addr port+1
}
// managedSource is the SourceManager's view of one running source.
type managedSource struct {
id string
label string
addr string
client *UDPClient
id string
label string
addr string
multicastGroup string
dataPort int
client *UDPClient
}
// SourceManager owns the lifecycle of all active data sources.
@@ -48,14 +52,20 @@ func (sm *SourceManager) genID() string {
}
// Add creates a new source, registers it in the hub, and starts connecting.
// multicastGroup: multicast IP (e.g. "239.0.0.1") or "" for unicast mode.
// dataPort: UDP data port in multicast mode; 0 = Addr port+1.
// Returns the source ID.
func (sm *SourceManager) Add(label, addr string) string {
func (sm *SourceManager) Add(label, addr, multicastGroup string, dataPort int) string {
if label == "" {
label = addr
}
id := sm.genID()
c := NewUDPClient(addr, id, sm.hub)
ms := &managedSource{id: id, label: label, addr: addr, client: c}
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
@@ -90,7 +100,12 @@ func (sm *SourceManager) Save() error {
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})
cfgs = append(cfgs, SourceConfig{
Label: ms.label,
Addr: ms.addr,
MulticastGroup: ms.multicastGroup,
DataPort: ms.dataPort,
})
}
sm.mu.RUnlock()
@@ -113,7 +128,7 @@ func (sm *SourceManager) Load(path string) error {
}
sm.filePath = path
for _, cfg := range cfgs {
sm.Add(cfg.Label, cfg.Addr)
sm.Add(cfg.Label, cfg.Addr, cfg.MulticastGroup, cfg.DataPort)
}
return nil
}