package main import ( "context" "crypto/sha256" "flag" "fmt" "io" "log" "os" "path/filepath" "strings" "sync" "time" "silverpose/v2/internal/alert" "silverpose/v2/internal/config" "silverpose/v2/internal/fall" "silverpose/v2/internal/monitor" "silverpose/v2/internal/source" "silverpose/v2/internal/spike" "silverpose/v2/internal/ui" ) func main() { configPath := flag.String("config", "config.local.json", "local V2 JSON configuration without RTSP credentials") flag.Parse() cfg, err := config.Load(*configPath) if err != nil { log.Fatal("配置无效:", err) } if err := validateStartup(cfg); err != nil { log.Fatal("启动预检失败:", err) } controller := &controller{config: cfg} window, err := ui.NewWindow(settingsFor(cfg), ui.Commands{Start: controller.Start, Stop: controller.Stop}) if err != nil { log.Fatal("创建操作窗口失败:", err) } controller.window = window window.Run() controller.Stop() } type controller struct { config config.Config window *ui.Window mu sync.Mutex cancel func() runID uint64 } func (controller *controller) Start() { controller.mu.Lock() if controller.cancel != nil { controller.mu.Unlock() return } ctx, cancel := context.WithCancel(context.Background()) controller.cancel = cancel controller.runID++ runID := controller.runID controller.mu.Unlock() go controller.run(ctx, cancel, runID) } func (controller *controller) Stop() { controller.mu.Lock() cancel := controller.cancel controller.cancel = nil controller.mu.Unlock() if cancel != nil { cancel() } } func (controller *controller) run(ctx context.Context, cancel func(), runID uint64) { defer func() { controller.mu.Lock() if controller.runID == runID { controller.cancel = nil } controller.mu.Unlock() cancel() }() runtime, err := spike.OpenRuntime(controller.config.Model.ONNXPath, controller.config.Model.ONNXRuntimeDLLPath) if err != nil { log.Printf("初始化 ONNX Runtime 失败:%v", err) controller.window.ReportIssue("无法初始化 ONNX Runtime") return } engine, err := fall.NewEngine(fall.EngineConfig{ KeypointConfidenceThreshold: controller.config.Event.KeypointConfidenceThreshold, SuspectWindowSeconds: controller.config.Event.SuspectWindowSeconds, ConfirmWindowSeconds: controller.config.Event.ConfirmWindowSeconds, RecoveryWindowSeconds: controller.config.Event.RecoveryWindowSeconds, CooldownSeconds: controller.config.Event.CooldownSeconds, RequireRapidDrop: controller.config.Event.RequireRapidDrop, RequireLowerBody: controller.config.Event.RequireLowerBody, HorizontalAngleThresholdDegrees: controller.config.Event.HorizontalAngleThresholdDegrees, ConfigVersion: controller.config.RuntimeConfigVersion, SessionID: time.Now().UTC().Format("20060102-150405"), }) if err != nil { log.Printf("初始化摔倒事件引擎失败:%v", err) runtime.Close() controller.window.ReportIssue("无法初始化摔倒事件引擎") return } stream := source.Start(ctx, source.Config{ SourceURL: controller.config.Source.URL, FFmpegPath: controller.config.Tools.FFmpegPath, FFprobePath: controller.config.Tools.FFprobePath, Transport: controller.config.Source.Transport, Timeout: controller.config.Source.Timeout, LowLatency: controller.config.Source.LowLatency, }, source.Dependencies{}) monitored, err := monitor.New( stream, runtime, engine, controller.config.Model.ConfidenceThreshold, alert.NewDispatcher(controller.config.Artifacts.EventDirectory, controller.config.Source.ID), time.Now, ) if err != nil { log.Printf("启动监控管线失败:%v", err) runtime.Close() controller.window.ReportIssue("无法启动监控管线") return } go monitored.Run(ctx) updates, alerts := monitored.Updates(), monitored.Alerts() for updates != nil || alerts != nil { select { case update, open := <-updates: if !open { updates = nil continue } controller.window.Present(update) case record, open := <-alerts: if !open { alerts = nil continue } controller.window.PresentAlert(record) } } } func validateStartup(cfg config.Config) error { for _, path := range []string{cfg.Model.ONNXPath, cfg.Model.ONNXRuntimeDLLPath, cfg.Tools.FFmpegPath, cfg.Tools.FFprobePath} { info, err := os.Stat(path) if err != nil || info.IsDir() { return fmt.Errorf("必需运行文件缺失或不可读") } } actual, err := sha256File(cfg.Model.ONNXPath) if err != nil { return fmt.Errorf("无法校验 ONNX 模型") } if !strings.EqualFold(actual, cfg.Model.SHA256) { return fmt.Errorf("ONNX 模型哈希与配置不匹配") } return nil } func sha256File(path string) (string, error) { file, err := os.Open(filepath.Clean(path)) if err != nil { return "", err } defer file.Close() digest := sha256.New() if _, err := io.Copy(digest, file); err != nil { return "", err } return fmt.Sprintf("%x", digest.Sum(nil)), nil } func settingsFor(cfg config.Config) ui.Settings { return ui.Settings{ SourceEnvironment: cfg.Source.RTSPURLEnv, ModelSHA256: cfg.Model.SHA256, RuntimeSummary: "ONNX Runtime、FFmpeg 和 FFprobe 已通过启动预检", EventSummary: fmt.Sprintf("确认 %.1f 秒;水平角 %.0f°;快速下移=%t;下肢=%t", cfg.Event.ConfirmWindowSeconds, cfg.Event.HorizontalAngleThresholdDegrees, cfg.Event.RequireRapidDrop, cfg.Event.RequireLowerBody), } }