package ui import ( "fmt" "strings" "github.com/lxn/walk" d "github.com/lxn/walk/declarative" "github.com/lxn/win" "silverpose/v2/internal/alert" "silverpose/v2/internal/fall" "silverpose/v2/internal/monitor" ) type Settings struct { SourceEnvironment string ModelSHA256 string RuntimeSummary string EventSummary string } type Commands struct { Start func() Stop func() } // Window is an observer of monitor updates. It does not own a stream, runtime, // fall engine, or event artifact writer. type Window struct { main *walk.MainWindow imageView *walk.ImageView connectionLabel *walk.TextLabel stateLabel *walk.TextLabel eventLabel *walk.TextLabel startButton *walk.PushButton stopButton *walk.PushButton model ViewModel bitmap *walk.Bitmap seenAlerts map[string]struct{} commands Commands } func NewWindow(settings Settings, commands Commands) (*Window, error) { spec := MonitorWindowSpec() window := &Window{model: NewViewModel(), seenAlerts: make(map[string]struct{}), commands: commands} window.model.SetSourceReady(settings.SourceEnvironment, settings.SourceEnvironment != "") background := d.SolidColorBrush{Color: walk.RGB(234, 241, 248)} surface := d.SolidColorBrush{Color: walk.RGB(255, 255, 255)} if err := (d.MainWindow{ AssignTo: &window.main, Title: spec.Title, Size: d.Size{Width: spec.Width, Height: spec.Height}, MinSize: d.Size{Width: 960, Height: 640}, Background: background, Layout: d.VBox{}, Children: []d.Widget{ d.TabWidget{ Pages: []d.TabPage{ { Title: "监控", Layout: d.HBox{}, Children: []d.Widget{ d.Composite{ Background: surface, Layout: d.VBox{}, StretchFactor: 3, Children: []d.Widget{ d.TextLabel{Text: "实时画面", ToolTipText: "显示完成推理与姿态叠加后的最新画面"}, d.ImageView{AssignTo: &window.imageView, Mode: d.ImageViewModeZoom, StretchFactor: 8, ToolTipText: "实时姿态画面"}, }, }, d.Composite{ Background: surface, Layout: d.VBox{}, StretchFactor: 2, Children: []d.Widget{ d.TextLabel{Text: "运行状态"}, d.TextLabel{AssignTo: &window.connectionLabel, Text: window.model.ConnectionText}, d.TextLabel{AssignTo: &window.stateLabel, Text: window.model.StateText}, d.TextLabel{AssignTo: &window.eventLabel, Text: window.model.EventText}, d.PushButton{AssignTo: &window.startButton, Text: "开始监控", ToolTipText: "使用当前配置启动单路监控", OnClicked: window.start}, d.PushButton{AssignTo: &window.stopButton, Text: "停止监控", Enabled: false, ToolTipText: "停止监控并释放本地运行资源", OnClicked: window.stop}, d.TextLabel{Text: "红色仅表示已确认摔倒。连接异常不会触发报警。"}, }, }, }, }, { Title: "设置", Layout: d.VBox{}, Children: []d.Widget{ d.TextLabel{Text: "来源与运行依赖(只读,不显示 RTSP 地址或密码)"}, d.TextLabel{Text: window.model.SettingsSummary}, d.TextLabel{Text: "模型 SHA-256:" + shortHash(settings.ModelSHA256)}, d.TextLabel{Text: "运行依赖:" + settings.RuntimeSummary}, d.TextLabel{Text: "事件参数:" + settings.EventSummary}, d.TextLabel{Text: "参数在配置文件中维护;修改后请停止并重新开始监控。"}, }, }, }, }, }, }).Create(); err != nil { return nil, err } window.main.Closing().Attach(func(canceled *bool, reason walk.CloseReason) { window.stop() if window.bitmap != nil { window.bitmap.Dispose() window.bitmap = nil } }) return window, nil } func (window *Window) Run() int { return window.main.Run() } func (window *Window) start() { if window.commands.Start != nil { window.commands.Start() } window.startButton.SetEnabled(false) window.stopButton.SetEnabled(true) } func (window *Window) stop() { if window.commands.Stop != nil { window.commands.Stop() } if window.startButton != nil { window.startButton.SetEnabled(true) } if window.stopButton != nil { window.stopButton.SetEnabled(false) } } // Present runs only the final state assignment on the UI thread. func (window *Window) Present(update monitor.Update) { window.main.Synchronize(func() { state := highestState(update.Result) window.model.Apply(state, update.SourceStatus) if update.SourceMessage != "" { window.model.ConnectionText = "视频流:" + update.SourceMessage } _ = window.connectionLabel.SetText(window.model.ConnectionText) _ = window.stateLabel.SetText(window.model.StateText) _ = window.eventLabel.SetText(window.model.EventText) if update.Image == nil { return } bitmap, err := walk.NewBitmapFromImage(update.Image) if err != nil { return } if err := window.imageView.SetImage(bitmap); err != nil { bitmap.Dispose() return } previous := window.bitmap window.bitmap = bitmap if previous != nil { previous.Dispose() } }) } // PresentAlert is called by a consumer of Monitor.Alerts. Event IDs are checked // again at the UI boundary so a reconnect/repaint cannot replay sound or popup. func (window *Window) PresentAlert(record alert.Record) { if !record.Written { return } window.main.Synchronize(func() { if _, seen := window.seenAlerts[record.Event.EventID]; seen { return } window.seenAlerts[record.Event.EventID] = struct{}{} win.MessageBeep(win.MB_ICONHAND) message := fmt.Sprintf("确认摔倒\n人员:%s\n延迟:%.2f 秒\n截图:%s", record.Event.TrackID, record.Event.LatencySeconds, record.ScreenshotPath) walk.MsgBox(window.main, "Silver Pose V2 · 确认摔倒", message, walk.MsgBoxOK|walk.MsgBoxIconError) }) } // ReportIssue communicates a recoverable startup/worker issue without using // critical-red fall language or exposing configuration values. func (window *Window) ReportIssue(message string) { window.main.Synchronize(func() { window.model.ConnectionText = "视频流:" + message window.model.Critical = false _ = window.connectionLabel.SetText(window.model.ConnectionText) _ = window.stateLabel.SetText("检测状态:正常") }) } func highestState(result fall.FrameResult) fall.State { state := fall.Normal for _, person := range result.People { switch person.State { case fall.Confirmed: return fall.Confirmed case fall.Suspect: if state == fall.Normal || state == fall.Recovering { state = fall.Suspect } case fall.Recovering: if state == fall.Normal { state = fall.Recovering } } } return state } func shortHash(value string) string { if len(value) <= 12 { return value } return strings.ToLower(value[:12]) + "…" }