From c122369ca7cf67b37acb5ab259ce4290ffe469f9 Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Tue, 19 May 2026 13:14:46 +0200 Subject: [PATCH] multi source --- Client/WebUI/sources.go | 128 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 Client/WebUI/sources.go diff --git a/Client/WebUI/sources.go b/Client/WebUI/sources.go new file mode 100644 index 0000000..cf53f78 --- /dev/null +++ b/Client/WebUI/sources.go @@ -0,0 +1,128 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "strings" + "sync" + "sync/atomic" +) + +// SourceConfig is the serialisable description of one data source (for file save/load). +type SourceConfig struct { + Label string `json:"label"` + Addr string `json:"addr"` +} + +// managedSource is the SourceManager's view of one running source. +type managedSource struct { + id string + label string + addr string + client *UDPClient +} + +// SourceManager owns the lifecycle of all active data sources. +// It is the only place where UDPClient instances are created and stopped. +type SourceManager struct { + mu sync.RWMutex + sources map[string]*managedSource + hub *Hub + filePath string // empty = save not configured + nextID atomic.Int32 +} + +// NewSourceManager creates a SourceManager bound to the given hub. +// filePath is the default path for Save(); may be empty. +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, registers it in the hub, and starts connecting. +// Returns the source ID. +func (sm *SourceManager) Add(label, addr string) string { + if label == "" { + label = addr + } + id := sm.genID() + c := NewUDPClient(addr, id, sm.hub) + ms := &managedSource{id: id, label: label, addr: addr, client: c} + + sm.mu.Lock() + sm.sources[id] = ms + sm.mu.Unlock() + + // Register in hub (creates hub-side state, broadcasts sources list). + sm.hub.AddSource(id, label, addr) + go c.Run() + return id +} + +// Remove stops the source's client 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 (start with --sources-file path)") + } + 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}) + } + 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. Sets filePath for future saves. +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) + } + return nil +} + +// ParseSourceArg parses "label@host:port" or "host:port". +func ParseSourceArg(s string) (label, addr string) { + s = strings.TrimSpace(s) + if idx := strings.Index(s, "@"); idx >= 0 { + return strings.TrimSpace(s[:idx]), strings.TrimSpace(s[idx+1:]) + } + return "", s +}