222 lines
6.8 KiB
Go
222 lines
6.8 KiB
Go
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]) + "…"
|
||
}
|