feat(v2): T-308C editable settings form (camera + params) applied on start
Settings tab is now an editable, scrollable form: camera connection (host/port/channel/user/masked password/transport/timeout/low-latency) and common detection params, each with a save button that writes the untracked local config. cmd/silver-pose pre-fills from the local file, passes the config path, and the controller reloads config on each Start so saved edits apply next run. UI cross-compiles for Windows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+236
-8
@@ -6,6 +6,7 @@ import (
|
||||
"image/color"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -23,6 +24,7 @@ import (
|
||||
"gioui.org/widget/material"
|
||||
|
||||
"silverpose/v2/internal/alert"
|
||||
"silverpose/v2/internal/config"
|
||||
"silverpose/v2/internal/fall"
|
||||
"silverpose/v2/internal/monitor"
|
||||
)
|
||||
@@ -41,6 +43,7 @@ var (
|
||||
colorSubtle = color.NRGBA{R: 0x52, G: 0x65, B: 0x7C, A: 0xFF}
|
||||
colorAccent = color.NRGBA{R: 0x25, G: 0x63, B: 0xEB, A: 0xFF}
|
||||
colorAlert = color.NRGBA{R: 0xC6, G: 0x28, B: 0x28, A: 0xFF}
|
||||
colorStroke = color.NRGBA{R: 0xC8, G: 0xD4, B: 0xE3, A: 0xFF}
|
||||
colorScrim = color.NRGBA{A: 0xB0}
|
||||
)
|
||||
|
||||
@@ -49,6 +52,33 @@ type Settings struct {
|
||||
ModelSHA256 string
|
||||
RuntimeSummary string
|
||||
EventSummary string
|
||||
ConfigPath string
|
||||
Camera CameraFields
|
||||
Params ParamFields
|
||||
}
|
||||
|
||||
// CameraFields pre-fill the editable camera connection form. Credentials live
|
||||
// only in the untracked local config; the form saves them back there.
|
||||
type CameraFields struct {
|
||||
ID string
|
||||
Host string
|
||||
Port int
|
||||
Channel string
|
||||
Username string
|
||||
Password string
|
||||
Transport string
|
||||
TimeoutSeconds float64
|
||||
LowLatency bool
|
||||
}
|
||||
|
||||
// ParamFields pre-fill the editable common detection parameters.
|
||||
type ParamFields struct {
|
||||
KeypointConfidence float64
|
||||
ConfirmWindowSeconds float64
|
||||
HorizontalAngleDegrees float64
|
||||
ModelConfidence float64
|
||||
RequireRapidDrop bool
|
||||
RequireLowerBody bool
|
||||
}
|
||||
|
||||
type Commands struct {
|
||||
@@ -89,6 +119,7 @@ type Window struct {
|
||||
incoming []galleryIncoming
|
||||
seenEvents map[string]struct{}
|
||||
running bool
|
||||
configPath string
|
||||
|
||||
// Touched only by the event-loop goroutine.
|
||||
imageOp paint.ImageOp
|
||||
@@ -102,6 +133,32 @@ type Window struct {
|
||||
gallery []*galleryItem
|
||||
viewing *galleryItem
|
||||
viewerClose widget.Clickable
|
||||
|
||||
// Settings form (event-loop goroutine only).
|
||||
editHost widget.Editor
|
||||
editPort widget.Editor
|
||||
editChannel widget.Editor
|
||||
editUsername widget.Editor
|
||||
editPassword widget.Editor
|
||||
editTransport widget.Editor
|
||||
editTimeout widget.Editor
|
||||
editKeypoint widget.Editor
|
||||
editConfirm widget.Editor
|
||||
editAngle widget.Editor
|
||||
editModelConf widget.Editor
|
||||
lowLatency widget.Bool
|
||||
requireRapid widget.Bool
|
||||
requireLower widget.Bool
|
||||
saveCamera widget.Clickable
|
||||
saveParams widget.Clickable
|
||||
settingsStatus string
|
||||
settingsList widget.List
|
||||
cameraID string
|
||||
}
|
||||
|
||||
func setEditor(editor *widget.Editor, value string) {
|
||||
editor.SingleLine = true
|
||||
editor.SetText(value)
|
||||
}
|
||||
|
||||
func NewWindow(settings Settings, commands Commands) (*Window, error) {
|
||||
@@ -139,9 +196,55 @@ func NewWindow(settings Settings, commands Commands) (*Window, error) {
|
||||
tabs: []string{"监控", "设置"},
|
||||
}
|
||||
window.tabButton = make([]widget.Clickable, len(window.tabs))
|
||||
|
||||
window.configPath = settings.ConfigPath
|
||||
camera, params := settings.Camera, settings.Params
|
||||
setEditor(&window.editHost, camera.Host)
|
||||
setEditor(&window.editPort, strconv.Itoa(defaultPort(camera.Port)))
|
||||
setEditor(&window.editChannel, defaultString(camera.Channel, "102"))
|
||||
setEditor(&window.editUsername, camera.Username)
|
||||
setEditor(&window.editPassword, camera.Password)
|
||||
window.editPassword.Mask = '•'
|
||||
setEditor(&window.editTransport, defaultString(camera.Transport, "tcp"))
|
||||
setEditor(&window.editTimeout, formatFloat(defaultFloat(camera.TimeoutSeconds, 5)))
|
||||
window.lowLatency.Value = camera.LowLatency
|
||||
setEditor(&window.editKeypoint, formatFloat(defaultFloat(params.KeypointConfidence, 0.4)))
|
||||
setEditor(&window.editConfirm, formatFloat(defaultFloat(params.ConfirmWindowSeconds, 1.8)))
|
||||
setEditor(&window.editAngle, formatFloat(defaultFloat(params.HorizontalAngleDegrees, 45)))
|
||||
setEditor(&window.editModelConf, formatFloat(defaultFloat(params.ModelConfidence, 0.25)))
|
||||
window.requireRapid.Value = params.RequireRapidDrop
|
||||
window.requireLower.Value = params.RequireLowerBody
|
||||
window.settingsList.Axis = layout.Vertical
|
||||
window.cameraID = defaultString(camera.ID, "ip-camera")
|
||||
|
||||
return window, nil
|
||||
}
|
||||
|
||||
func defaultPort(port int) int {
|
||||
if port <= 0 {
|
||||
return 554
|
||||
}
|
||||
return port
|
||||
}
|
||||
|
||||
func defaultString(value, fallback string) string {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func defaultFloat(value, fallback float64) float64 {
|
||||
if value == 0 {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func formatFloat(value float64) string {
|
||||
return strconv.FormatFloat(value, 'f', -1, 64)
|
||||
}
|
||||
|
||||
func (window *Window) Run() int {
|
||||
go func() {
|
||||
err := window.loop()
|
||||
@@ -551,18 +654,143 @@ func (window *Window) layoutDialog(gtx layout.Context, record alert.Record) layo
|
||||
}
|
||||
|
||||
func (window *Window) layoutSettings(gtx layout.Context, model ViewModel) layout.Dimensions {
|
||||
if window.saveCamera.Clicked(gtx) {
|
||||
window.onSaveCamera()
|
||||
}
|
||||
if window.saveParams.Clicked(gtx) {
|
||||
window.onSaveParams()
|
||||
}
|
||||
rows := []layout.Widget{
|
||||
window.heading("摄像头连接(账号密码仅存本地配置,下次启动生效)"),
|
||||
window.editorRow("IP / 主机", &window.editHost),
|
||||
window.editorRow("端口", &window.editPort),
|
||||
window.editorRow("通道(101 主 / 102 子)", &window.editChannel),
|
||||
window.editorRow("账号", &window.editUsername),
|
||||
window.editorRow("密码", &window.editPassword),
|
||||
window.editorRow("传输 (tcp/udp)", &window.editTransport),
|
||||
window.editorRow("连接超时 (秒)", &window.editTimeout),
|
||||
window.checkRow("低延迟", &window.lowLatency),
|
||||
window.buttonRow(&window.saveCamera, "保存摄像头"),
|
||||
window.spacerRow(),
|
||||
window.heading("常用检测参数(下次启动生效)"),
|
||||
window.editorRow("关键点置信度", &window.editKeypoint),
|
||||
window.editorRow("确认窗口 (秒, 1-3)", &window.editConfirm),
|
||||
window.editorRow("水平角度阈值 (°)", &window.editAngle),
|
||||
window.editorRow("模型置信度", &window.editModelConf),
|
||||
window.checkRow("要求先快速下移", &window.requireRapid),
|
||||
window.checkRow("要求膝踝清晰", &window.requireLower),
|
||||
window.buttonRow(&window.saveParams, "保存参数"),
|
||||
window.labelRow(window.settingsStatus),
|
||||
window.spacerRow(),
|
||||
window.heading("运行依赖(只读)"),
|
||||
window.labelRow("模型 SHA-256:" + shortHash(window.settings.ModelSHA256)),
|
||||
window.labelRow("运行依赖:" + window.settings.RuntimeSummary),
|
||||
}
|
||||
return layout.UniformInset(unit.Dp(12)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(material.H6(window.theme, "来源与运行依赖(只读,不显示 RTSP 地址或密码)").Layout),
|
||||
layout.Rigid(material.Body1(window.theme, model.SettingsSummary).Layout),
|
||||
layout.Rigid(material.Body1(window.theme, "模型 SHA-256:"+shortHash(window.settings.ModelSHA256)).Layout),
|
||||
layout.Rigid(material.Body1(window.theme, "运行依赖:"+window.settings.RuntimeSummary).Layout),
|
||||
layout.Rigid(material.Body1(window.theme, "事件参数:"+window.settings.EventSummary).Layout),
|
||||
layout.Rigid(material.Body2(window.theme, "参数在配置文件中维护;修改后请停止并重新开始监控。").Layout),
|
||||
)
|
||||
return material.List(window.theme, &window.settingsList).Layout(gtx, len(rows), func(gtx layout.Context, index int) layout.Dimensions {
|
||||
return layout.UniformInset(unit.Dp(4)).Layout(gtx, rows[index])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (window *Window) heading(text string) layout.Widget {
|
||||
return func(gtx layout.Context) layout.Dimensions {
|
||||
return material.Body1(window.theme, text).Layout(gtx)
|
||||
}
|
||||
}
|
||||
|
||||
func (window *Window) labelRow(text string) layout.Widget {
|
||||
return func(gtx layout.Context) layout.Dimensions {
|
||||
return material.Body2(window.theme, text).Layout(gtx)
|
||||
}
|
||||
}
|
||||
|
||||
func (window *Window) spacerRow() layout.Widget {
|
||||
return func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Spacer{Height: unit.Dp(8)}.Layout(gtx)
|
||||
}
|
||||
}
|
||||
|
||||
func (window *Window) editorRow(label string, editor *widget.Editor) layout.Widget {
|
||||
return func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
gtx.Constraints.Min.X = gtx.Dp(150)
|
||||
return material.Body2(window.theme, label).Layout(gtx)
|
||||
}),
|
||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||
border := widget.Border{Color: colorStroke, Width: unit.Dp(1), CornerRadius: unit.Dp(4)}
|
||||
return border.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.UniformInset(unit.Dp(6)).Layout(gtx, material.Editor(window.theme, editor, "").Layout)
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (window *Window) checkRow(label string, value *widget.Bool) layout.Widget {
|
||||
return func(gtx layout.Context) layout.Dimensions {
|
||||
return material.CheckBox(window.theme, value, label).Layout(gtx)
|
||||
}
|
||||
}
|
||||
|
||||
func (window *Window) buttonRow(click *widget.Clickable, label string) layout.Widget {
|
||||
return func(gtx layout.Context) layout.Dimensions {
|
||||
return smallButton(gtx, window.theme, click, label)
|
||||
}
|
||||
}
|
||||
|
||||
func (window *Window) onSaveCamera() {
|
||||
if window.configPath == "" {
|
||||
window.settingsStatus = "未指定本地配置路径,无法保存"
|
||||
return
|
||||
}
|
||||
port, err := strconv.Atoi(strings.TrimSpace(window.editPort.Text()))
|
||||
if err != nil || port < 1 || port > 65535 {
|
||||
window.settingsStatus = "端口无效"
|
||||
return
|
||||
}
|
||||
timeout, err := strconv.ParseFloat(strings.TrimSpace(window.editTimeout.Text()), 64)
|
||||
if err != nil {
|
||||
window.settingsStatus = "连接超时无效"
|
||||
return
|
||||
}
|
||||
transport := strings.ToLower(strings.TrimSpace(window.editTransport.Text()))
|
||||
if transport != "tcp" && transport != "udp" {
|
||||
window.settingsStatus = "传输须为 tcp 或 udp"
|
||||
return
|
||||
}
|
||||
if err := config.WriteLocalCameraSource(window.configPath, window.cameraID,
|
||||
strings.TrimSpace(window.editHost.Text()), port, strings.TrimSpace(window.editChannel.Text()),
|
||||
strings.TrimSpace(window.editUsername.Text()), window.editPassword.Text(),
|
||||
transport, timeout, window.lowLatency.Value); err != nil {
|
||||
window.settingsStatus = "保存失败:" + err.Error()
|
||||
return
|
||||
}
|
||||
window.settingsStatus = "摄像头已保存,将在下次开始监控时生效"
|
||||
}
|
||||
|
||||
func (window *Window) onSaveParams() {
|
||||
if window.configPath == "" {
|
||||
window.settingsStatus = "未指定本地配置路径,无法保存"
|
||||
return
|
||||
}
|
||||
keypoint, keypointErr := strconv.ParseFloat(strings.TrimSpace(window.editKeypoint.Text()), 64)
|
||||
confirm, confirmErr := strconv.ParseFloat(strings.TrimSpace(window.editConfirm.Text()), 64)
|
||||
angle, angleErr := strconv.ParseFloat(strings.TrimSpace(window.editAngle.Text()), 64)
|
||||
modelConfidence, modelErr := strconv.ParseFloat(strings.TrimSpace(window.editModelConf.Text()), 64)
|
||||
if keypointErr != nil || confirmErr != nil || angleErr != nil || modelErr != nil {
|
||||
window.settingsStatus = "参数须为数字"
|
||||
return
|
||||
}
|
||||
if err := config.WriteLocalEventTuning(window.configPath, keypoint, confirm, angle, modelConfidence,
|
||||
window.requireRapid.Value, window.requireLower.Value); err != nil {
|
||||
window.settingsStatus = "保存失败:" + err.Error()
|
||||
return
|
||||
}
|
||||
window.settingsStatus = "参数已保存,将在下次开始监控时生效"
|
||||
}
|
||||
|
||||
func highestState(result fall.FrameResult) fall.State {
|
||||
state := fall.Normal
|
||||
for _, person := range result.People {
|
||||
|
||||
Reference in New Issue
Block a user