package service import ( "encoding/json" "net" "strings" "time" "3588AdminBackend/internal/config" "3588AdminBackend/internal/models" "github.com/google/uuid" ) const discoveryMagicV1 = "RK3588SYS_DISCOVERY_V1" type DiscoveryService struct { cfg *config.Config registry *RegistryService } func NewDiscoveryService(cfg *config.Config, registry *RegistryService) *DiscoveryService { return &DiscoveryService{ cfg: cfg, registry: registry, } } func (s *DiscoveryService) SearchDefault() ([]*models.Device, error) { if s == nil || s.cfg == nil { return nil, nil } timeoutMs := s.cfg.DiscoveryTimeoutMs if timeoutMs <= 0 { timeoutMs = 1200 } return s.Search(timeoutMs) } func (s *DiscoveryService) Search(timeoutMs int) ([]*models.Device, error) { reqID := uuid.NewString() payload, _ := json.Marshal(map[string]interface{}{ "type": "discover", "req_id": reqID, "reply_port": 0, }) msg := []byte(discoveryMagicV1 + "\n" + string(payload)) // Listen for replies addr, err := net.ResolveUDPAddr("udp", ":0") // Random port for receiving if err != nil { return nil, err } conn, err := net.ListenUDP("udp", addr) if err != nil { return nil, err } defer conn.Close() // Send Broadcast to all interfaces interfaces, err := net.Interfaces() if err == nil { for _, iface := range interfaces { if iface.Flags&net.FlagBroadcast != 0 && iface.Flags&net.FlagUp != 0 { addrs, err := iface.Addrs() if err != nil { continue } for _, addr := range addrs { if ipnet, ok := addr.(*net.IPNet); ok && ipnet.IP.To4() != nil { // Calculate broadcast address ip := ipnet.IP.To4() mask := ipnet.Mask broadcast := make(net.IP, len(ip)) for i := 0; i < len(ip); i++ { broadcast[i] = ip[i] | ^mask[i] } broadcastAddr := &net.UDPAddr{ IP: broadcast, Port: s.cfg.DiscoveryPort, } _, _ = conn.WriteToUDP(msg, broadcastAddr) } } } } } stop := time.After(time.Duration(timeoutMs) * time.Millisecond) found := make(map[string]*models.Device) for { select { case <-stop: // Convert map to slice list := make([]*models.Device, 0, len(found)) for _, d := range found { list = append(list, d) } return list, nil default: conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) buf := make([]byte, 2048) n, raddr, err := conn.ReadFromUDP(buf) if err != nil { continue } text := strings.TrimSpace(string(buf[:n])) lines := strings.SplitN(text, "\n", 3) if len(lines) < 2 { continue } if strings.TrimSpace(lines[0]) != discoveryMagicV1 { continue } var reply struct { Type string `json:"type"` ReqID string `json:"req_id"` DeviceID string `json:"device_id"` DeviceName string `json:"device_name"` Hostname string `json:"hostname"` IP string `json:"ip"` AgentPort int `json:"agent_port"` MediaPort int `json:"media_port"` Version string `json:"version"` BuildID string `json:"build_id"` GitSha string `json:"git_sha"` UptimeSec int64 `json:"uptime_sec"` InstanceName string `json:"instance_name"` InstanceDisplayName string `json:"instance_display_name"` ConfigID string `json:"config_id"` ConfigVersion string `json:"config_version"` Template string `json:"template"` Profile string `json:"profile"` Overlays []string `json:"overlays"` } if err := json.Unmarshal([]byte(strings.TrimSpace(lines[1])), &reply); err != nil { continue } if reply.Type != "discover_reply" || reply.ReqID != reqID || reply.DeviceID == "" { continue } dev := &models.Device{ DeviceID: reply.DeviceID, DeviceName: reply.DeviceName, Hostname: reply.Hostname, IP: reply.IP, AgentPort: reply.AgentPort, MediaPort: reply.MediaPort, Version: reply.Version, BuildID: reply.BuildID, GitSha: reply.GitSha, UptimeSec: reply.UptimeSec, InstanceName: reply.InstanceName, InstanceDisplayName: reply.InstanceDisplayName, } if dev.IP == "" { dev.IP = raddr.IP.String() } s.registry.UpdateDevice(dev) found[dev.DeviceID] = dev } } }