package main import ( "log" "os" "os/signal" "path/filepath" "strings" "syscall" "time" ) // dockSpec 描述机巢及其直接模拟上报的绑定无人机。 type dockSpec struct { DockID string Name string Location string DroneSN string DroneName string Latitude float64 Longitude float64 DroneLat float64 DroneLon float64 Flying bool Battery int Alarm bool } func main() { broker := envOr("MOCK_MQTT_BROKER", "tcp://roll.jiagutech.com:1883") username := envOr("MOCK_MQTT_USERNAME", "laic") password := envOr("MOCK_MQTT_PASSWORD", "") clientIDPrefix := envOr("MOCK_MQTT_CLIENT_ID_PREFIX", "mock-dock-") videoFile := resolveVideoFile(envOr("MOCK_VIDEO_FILE", "mock/mock.mp4")) if info, err := os.Stat(videoFile); err != nil { log.Printf("视频素材不可用(仅影响真实推流和原始视频上传): %s: %v", videoFile, err) } else { log.Printf("视频素材: %s (%d bytes)", videoFile, info.Size()) } specs := []dockSpec{ { DockID: "dock-1", Name: "青山湖 01 号机巢", Location: "青山湖科技城 A 区", DroneSN: "JG-UAV-001", DroneName: "巡检无人机 001", Latitude: 30.262438, Longitude: 119.802631, DroneLat: 30.262438, DroneLon: 119.802631, Battery: 86, }, { DockID: "dock-2", Name: "青山湖 02 号机巢", Location: "青山湖科技城 B 区", DroneSN: "JG-UAV-002", DroneName: "巡检无人机 002", Latitude: 30.255917, Longitude: 119.818264, DroneLat: 30.256500, DroneLon: 119.818900, Flying: true, Battery: 72, }, { DockID: "dock-3", Name: "青山湖 03 号机巢", Location: "青山湖科技城 C 区", DroneSN: "JG-UAV-003", DroneName: "巡检无人机 003", Latitude: 30.248610, Longitude: 119.791450, DroneLat: 30.248610, DroneLon: 119.791450, Battery: 41, Alarm: true, }, } docks := make([]*MockDock, 0, len(specs)) for _, spec := range selectDockSpecs(specs, os.Getenv("MOCK_DOCKS")) { dock := newMockDock(spec, clientIDPrefix, videoFile) if err := dock.start(broker, username, password); err != nil { log.Fatalf("启动机巢 %s 失败: %v", spec.DockID, err) } docks = append(docks, dock) } log.Println("模拟设备已启动(机巢 MQTT 直接上报机巢与无人机数据),按 Ctrl+C 退出") quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit log.Println("正在退出...") for _, dock := range docks { dock.stop() } time.Sleep(200 * time.Millisecond) } // resolveVideoFile first checks the current working directory, then the executable directory. // This keeps the default path usable both with `go run` and with a copied binary. func resolveVideoFile(path string) string { if filepath.IsAbs(path) { return path } if _, err := os.Stat(path); err == nil { return path } if exe, err := os.Executable(); err == nil { candidate := filepath.Join(filepath.Dir(exe), path) if _, err := os.Stat(candidate); err == nil { return candidate } } return path } func selectDockSpecs(specs []dockSpec, selected string) []dockSpec { if strings.TrimSpace(selected) == "" { return specs } allowed := make(map[string]struct{}) for _, dockID := range strings.Split(selected, ",") { if dockID = strings.TrimSpace(dockID); dockID != "" { allowed[dockID] = struct{}{} } } selectedSpecs := make([]dockSpec, 0, len(allowed)) for _, spec := range specs { if _, ok := allowed[spec.DockID]; ok { selectedSpecs = append(selectedSpecs, spec) } } if len(selectedSpecs) == 0 { log.Fatalf("MOCK_DOCKS 未匹配任何设备: %s", selected) } return selectedSpecs } func envOr(key, def string) string { if v := os.Getenv(key); v != "" { return v } return def }