package discovery import ( "context" "encoding/json" "errors" "fmt" "net" "os" "path/filepath" "strings" "time" "safesight-agent/internal/sysinfo" ) const Magic = "RK3588SYS_DISCOVERY_V1" type Responder struct { Port int DeviceID string DeviceName string Hostname string AgentPort int MediaPort int Version string BuildID string BuildType string GitSHA string ConfigPath string } type discoverReq struct { Type string `json:"type"` ReqID string `json:"req_id"` ReplyPort int `json:"reply_port"` } type discoverReply 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"` BuildType string `json:"build_type"` GitSHA string `json:"git_sha"` ConfigID string `json:"config_id,omitempty"` ConfigVersion string `json:"config_version,omitempty"` BusinessName string `json:"business_name,omitempty"` Template string `json:"template,omitempty"` Profile string `json:"profile,omitempty"` Overlays []string `json:"overlays,omitempty"` InstanceName string `json:"instance_name,omitempty"` InstanceDisplayName string `json:"instance_display_name,omitempty"` UptimeSec int64 `json:"uptime_sec"` } func (r *Responder) Run(ctx context.Context) error { addr := &net.UDPAddr{IP: net.IPv4zero, Port: r.Port} conn, err := net.ListenUDP("udp4", addr) if err != nil { return fmt.Errorf("listen udp :%d: %w", r.Port, err) } defer conn.Close() buf := make([]byte, 2048) for { select { case <-ctx.Done(): return nil default: } _ = conn.SetReadDeadline(time.Now().Add(500 * time.Millisecond)) n, remote, err := conn.ReadFromUDP(buf) if err != nil { var ne net.Error if errors.As(err, &ne) && ne.Timeout() { continue } return err } if remote == nil || remote.IP == nil { continue } msg := string(buf[:n]) line1, line2, ok := split2lines(msg) if !ok || strings.TrimSpace(line1) != Magic { continue } var req discoverReq if err := json.Unmarshal([]byte(line2), &req); err != nil { continue } if req.Type != "discover" || strings.TrimSpace(req.ReqID) == "" { continue } remoteIP := remote.IP.String() ip := sysinfo.PrimaryIPv4() if v, err := sysinfo.LocalIPTo(remoteIP); err == nil && v != "" { ip = v } reply := discoverReply{ Type: "discover_reply", ReqID: req.ReqID, DeviceID: r.DeviceID, DeviceName: r.DeviceName, Hostname: r.Hostname, IP: ip, AgentPort: r.AgentPort, MediaPort: r.MediaPort, Version: r.Version, BuildID: r.BuildID, BuildType: r.BuildType, GitSHA: r.GitSHA, UptimeSec: sysinfo.UptimeSec(), } if summary := readConfigMetadataSummary(r.ConfigPath); summary != nil { reply.ConfigID = summary.ConfigID reply.ConfigVersion = summary.ConfigVersion reply.BusinessName = summary.BusinessName reply.Template = summary.Template reply.Profile = summary.Profile reply.Overlays = copyStringSlice(summary.Overlays) reply.InstanceName = summary.InstanceName reply.InstanceDisplayName = summary.InstanceDisplayName } b, _ := json.Marshal(reply) out := Magic + "\n" + string(b) + "\n" _, _ = conn.WriteToUDP([]byte(out), remote) } } func split2lines(s string) (string, string, bool) { idx := strings.IndexByte(s, '\n') if idx < 0 { return "", "", false } line1 := strings.TrimRight(s[:idx], "\r") line2 := strings.TrimSpace(strings.TrimRight(s[idx+1:], "\r")) if line2 == "" { return "", "", false } return line1, line2, true } type configMetadataSummary struct { ConfigID string ConfigVersion string BusinessName string Template string Profile string Overlays []string InstanceName string InstanceDisplayName string } func readConfigMetadataSummary(path string) *configMetadataSummary { if strings.TrimSpace(path) == "" { return nil } b, err := os.ReadFile(filepath.Clean(path)) if err != nil { return nil } var root struct { Metadata map[string]any `json:"metadata"` } if err := json.Unmarshal(b, &root); err != nil { return nil } return metadataSummaryFromMap(root.Metadata) } func metadataSummaryFromMap(metadata map[string]any) *configMetadataSummary { if len(metadata) == 0 { return nil } summary := &configMetadataSummary{ ConfigID: stringValue(metadata["config_id"]), ConfigVersion: stringValue(metadata["config_version"]), BusinessName: stringValue(metadata["business_name"]), Template: stringValue(metadata["template"]), Profile: stringValue(metadata["profile"]), Overlays: stringSliceValue(metadata["overlays"]), } if names := stringSliceValue(metadata["instance_names"]); len(names) > 0 { summary.InstanceName = names[0] } if names := stringSliceValue(metadata["instance_display_names"]); len(names) > 0 { summary.InstanceDisplayName = names[0] } return summary } func stringValue(v any) string { s, _ := v.(string) return strings.TrimSpace(s) } func stringSliceValue(v any) []string { switch vv := v.(type) { case []string: return copyStringSlice(vv) case []any: out := make([]string, 0, len(vv)) for _, item := range vv { if s, ok := item.(string); ok { s = strings.TrimSpace(s) if s != "" { out = append(out, s) } } } return out default: return nil } } func copyStringSlice(v []string) []string { if len(v) == 0 { return nil } out := make([]string, len(v)) copy(out, v) return out }