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:
+10
@@ -612,3 +612,13 @@
|
|||||||
- T-308 落 DOING:分三段——A 界面四项(80%窗口/2×3画廊/Esc关弹窗/按钮灰显)、B V2 配置支持结构化摄像头(账号密码,百分号编码,仅未跟踪本地配置)、C Gio 可编辑设置表单(摄像头+常用参数,保存下次启动生效)。用户明确要求内网演示要账号密码框;合规做法=凭证仅入未跟踪 config.local.json,公开示例禁凭证、凭证不进 config_version/日志/事件(V2 已脱敏 ffmpeg 日志、版本已排除 URL)。顺带解决手动 %40 编码痛点。
|
- T-308 落 DOING:分三段——A 界面四项(80%窗口/2×3画廊/Esc关弹窗/按钮灰显)、B V2 配置支持结构化摄像头(账号密码,百分号编码,仅未跟踪本地配置)、C Gio 可编辑设置表单(摄像头+常用参数,保存下次启动生效)。用户明确要求内网演示要账号密码框;合规做法=凭证仅入未跟踪 config.local.json,公开示例禁凭证、凭证不进 config_version/日志/事件(V2 已脱敏 ffmpeg 日志、版本已排除 URL)。顺带解决手动 %40 编码痛点。
|
||||||
- 验证:每段 `GOOS=windows CGO_ENABLED=0 go build -mod=vendor` 交叉编译;配置层本地 go test;窗口目视需 Windows。
|
- 验证:每段 `GOOS=windows CGO_ENABLED=0 go build -mod=vendor` 交叉编译;配置层本地 go test;窗口目视需 Windows。
|
||||||
- 下一步:先做 A。
|
- 下一步:先做 A。
|
||||||
|
|
||||||
|
## 【2026-07-23】T-308 A/B/C 代码完成(交叉编译验证)
|
||||||
|
|
||||||
|
- 状态:DOING(三段代码完成 + Windows 交叉编译/本地 go test 通过;窗口目视需 Windows)
|
||||||
|
- A(界面四项):80% 屏幕开窗(GetSystemMetrics)、画廊 2×3 且右栏缩小、Esc 关全图弹窗(key focus+filter)、开始/停止按运行状态灰显禁用(gtx.Disabled;硬失败 ReportIssue 复位 running)。`GOOS=windows` 交叉编译通过。
|
||||||
|
- B(V2 结构化摄像头):`config` 接受 host/port/channel/username/password(仅本地)或 rtsp_url_env,`net/url` 自动百分号编码(修掉手动 %40);新增 `WriteLocalCameraSource`/`WriteLocalEventTuning`;护栏保留(拒绝整段 url/rtsp_url、凭证不进 config_version、公开示例无凭证测试)。本地 `go test ./internal/config` 通过。
|
||||||
|
- C(Gio 可编辑设置表单):设置页改为可编辑——摄像头(host/端口/通道/账号/密码掩码/传输/超时/低延迟)与常用参数(关键点/确认窗/水平角/模型置信度/快速下移/膝踝),两个保存按钮写回本地配置;`cmd/silver-pose` 预填(读本地文件)、传 ConfigPath,控制器在每次开始监控时 `config.Load` 重载使保存生效。UI 交叉编译通过、gofmt 干净。
|
||||||
|
- 阻塞:`cmd/silver-pose` 全量构建需 Windows+CGo(onnxruntime)+Gio;窗口目视/表单交互需 Windows。
|
||||||
|
- 决策:账号密码按用户要求进设置(内网演示),但仅存未跟踪 config.local.json、UI 掩码、不入日志/版本/事件;source.id 用固定/原值避免把内网 IP 写进 JSONL。设置改动"下次开始监控生效"经控制器重载 config 实现(对齐 V1 草稿→运行快照语义)。
|
||||||
|
- 下一步:用户 Windows `go run ./cmd/silver-pose` 目视——80% 窗口、2×3 画廊、Esc 关图、按钮灰显、设置页可编辑并保存后重启监控生效。
|
||||||
|
|||||||
+75
-23
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -32,8 +33,8 @@ func main() {
|
|||||||
if err := validateStartup(cfg); err != nil {
|
if err := validateStartup(cfg); err != nil {
|
||||||
log.Fatal("启动预检失败:", err)
|
log.Fatal("启动预检失败:", err)
|
||||||
}
|
}
|
||||||
controller := &controller{config: cfg}
|
controller := &controller{config: cfg, configPath: *configPath}
|
||||||
window, err := ui.NewWindow(settingsFor(cfg), ui.Commands{Start: controller.Start, Stop: controller.Stop})
|
window, err := ui.NewWindow(settingsFor(cfg, *configPath), ui.Commands{Start: controller.Start, Stop: controller.Stop})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("创建操作窗口失败:", err)
|
log.Fatal("创建操作窗口失败:", err)
|
||||||
}
|
}
|
||||||
@@ -43,11 +44,12 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type controller struct {
|
type controller struct {
|
||||||
config config.Config
|
config config.Config
|
||||||
window *ui.Window
|
configPath string
|
||||||
mu sync.Mutex
|
window *ui.Window
|
||||||
cancel func()
|
mu sync.Mutex
|
||||||
runID uint64
|
cancel func()
|
||||||
|
runID uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (controller *controller) Start() {
|
func (controller *controller) Start() {
|
||||||
@@ -83,22 +85,27 @@ func (controller *controller) run(ctx context.Context, cancel func(), runID uint
|
|||||||
controller.mu.Unlock()
|
controller.mu.Unlock()
|
||||||
cancel()
|
cancel()
|
||||||
}()
|
}()
|
||||||
runtime, err := spike.OpenRuntime(controller.config.Model.ONNXPath, controller.config.Model.ONNXRuntimeDLLPath)
|
cfg, err := config.Load(controller.configPath)
|
||||||
|
if err != nil {
|
||||||
|
controller.window.ReportIssue("配置无效,无法开始监控")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
runtime, err := spike.OpenRuntime(cfg.Model.ONNXPath, cfg.Model.ONNXRuntimeDLLPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("初始化 ONNX Runtime 失败:%v", err)
|
log.Printf("初始化 ONNX Runtime 失败:%v", err)
|
||||||
controller.window.ReportIssue("无法初始化 ONNX Runtime")
|
controller.window.ReportIssue("无法初始化 ONNX Runtime")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
engine, err := fall.NewEngine(fall.EngineConfig{
|
engine, err := fall.NewEngine(fall.EngineConfig{
|
||||||
KeypointConfidenceThreshold: controller.config.Event.KeypointConfidenceThreshold,
|
KeypointConfidenceThreshold: cfg.Event.KeypointConfidenceThreshold,
|
||||||
SuspectWindowSeconds: controller.config.Event.SuspectWindowSeconds,
|
SuspectWindowSeconds: cfg.Event.SuspectWindowSeconds,
|
||||||
ConfirmWindowSeconds: controller.config.Event.ConfirmWindowSeconds,
|
ConfirmWindowSeconds: cfg.Event.ConfirmWindowSeconds,
|
||||||
RecoveryWindowSeconds: controller.config.Event.RecoveryWindowSeconds,
|
RecoveryWindowSeconds: cfg.Event.RecoveryWindowSeconds,
|
||||||
CooldownSeconds: controller.config.Event.CooldownSeconds,
|
CooldownSeconds: cfg.Event.CooldownSeconds,
|
||||||
RequireRapidDrop: controller.config.Event.RequireRapidDrop,
|
RequireRapidDrop: cfg.Event.RequireRapidDrop,
|
||||||
RequireLowerBody: controller.config.Event.RequireLowerBody,
|
RequireLowerBody: cfg.Event.RequireLowerBody,
|
||||||
HorizontalAngleThresholdDegrees: controller.config.Event.HorizontalAngleThresholdDegrees,
|
HorizontalAngleThresholdDegrees: cfg.Event.HorizontalAngleThresholdDegrees,
|
||||||
ConfigVersion: controller.config.RuntimeConfigVersion,
|
ConfigVersion: cfg.RuntimeConfigVersion,
|
||||||
SessionID: time.Now().UTC().Format("20060102-150405"),
|
SessionID: time.Now().UTC().Format("20060102-150405"),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -108,13 +115,13 @@ func (controller *controller) run(ctx context.Context, cancel func(), runID uint
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
stream := source.Start(ctx, source.Config{
|
stream := source.Start(ctx, source.Config{
|
||||||
SourceURL: controller.config.Source.URL, FFmpegPath: controller.config.Tools.FFmpegPath,
|
SourceURL: cfg.Source.URL, FFmpegPath: cfg.Tools.FFmpegPath,
|
||||||
FFprobePath: controller.config.Tools.FFprobePath, Transport: controller.config.Source.Transport,
|
FFprobePath: cfg.Tools.FFprobePath, Transport: cfg.Source.Transport,
|
||||||
Timeout: controller.config.Source.Timeout, LowLatency: controller.config.Source.LowLatency,
|
Timeout: cfg.Source.Timeout, LowLatency: cfg.Source.LowLatency,
|
||||||
}, source.Dependencies{})
|
}, source.Dependencies{})
|
||||||
monitored, err := monitor.New(
|
monitored, err := monitor.New(
|
||||||
stream, runtime, engine, controller.config.Model.ConfidenceThreshold,
|
stream, runtime, engine, cfg.Model.ConfidenceThreshold,
|
||||||
alert.NewDispatcher(controller.config.Artifacts.EventDirectory, controller.config.Source.ID), time.Now,
|
alert.NewDispatcher(cfg.Artifacts.EventDirectory, cfg.Source.ID), time.Now,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("启动监控管线失败:%v", err)
|
log.Printf("启动监控管线失败:%v", err)
|
||||||
@@ -172,7 +179,7 @@ func sha256File(path string) (string, error) {
|
|||||||
return fmt.Sprintf("%x", digest.Sum(nil)), nil
|
return fmt.Sprintf("%x", digest.Sum(nil)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func settingsFor(cfg config.Config) ui.Settings {
|
func settingsFor(cfg config.Config, configPath string) ui.Settings {
|
||||||
return ui.Settings{
|
return ui.Settings{
|
||||||
SourceEnvironment: cfg.Source.RTSPURLEnv,
|
SourceEnvironment: cfg.Source.RTSPURLEnv,
|
||||||
ModelSHA256: cfg.Model.SHA256,
|
ModelSHA256: cfg.Model.SHA256,
|
||||||
@@ -180,5 +187,50 @@ func settingsFor(cfg config.Config) ui.Settings {
|
|||||||
EventSummary: fmt.Sprintf("确认 %.1f 秒;水平角 %.0f°;快速下移=%t;下肢=%t",
|
EventSummary: fmt.Sprintf("确认 %.1f 秒;水平角 %.0f°;快速下移=%t;下肢=%t",
|
||||||
cfg.Event.ConfirmWindowSeconds, cfg.Event.HorizontalAngleThresholdDegrees,
|
cfg.Event.ConfirmWindowSeconds, cfg.Event.HorizontalAngleThresholdDegrees,
|
||||||
cfg.Event.RequireRapidDrop, cfg.Event.RequireLowerBody),
|
cfg.Event.RequireRapidDrop, cfg.Event.RequireLowerBody),
|
||||||
|
ConfigPath: configPath,
|
||||||
|
Camera: cameraFieldsFrom(configPath),
|
||||||
|
Params: ui.ParamFields{
|
||||||
|
KeypointConfidence: float64(cfg.Event.KeypointConfidenceThreshold),
|
||||||
|
ConfirmWindowSeconds: cfg.Event.ConfirmWindowSeconds,
|
||||||
|
HorizontalAngleDegrees: float64(cfg.Event.HorizontalAngleThresholdDegrees),
|
||||||
|
ModelConfidence: float64(cfg.Model.ConfidenceThreshold),
|
||||||
|
RequireRapidDrop: cfg.Event.RequireRapidDrop,
|
||||||
|
RequireLowerBody: cfg.Event.RequireLowerBody,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cameraFieldsFrom pre-fills the settings form from the untracked local config's
|
||||||
|
// source block. It reads credentials only to display them in the (masked) form;
|
||||||
|
// they are never logged.
|
||||||
|
func cameraFieldsFrom(configPath string) ui.CameraFields {
|
||||||
|
data, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return ui.CameraFields{}
|
||||||
|
}
|
||||||
|
var document struct {
|
||||||
|
Source struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
Transport string `json:"transport"`
|
||||||
|
TimeoutSeconds float64 `json:"timeout_seconds"`
|
||||||
|
LowLatency *bool `json:"low_latency"`
|
||||||
|
} `json:"source"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &document); err != nil {
|
||||||
|
return ui.CameraFields{}
|
||||||
|
}
|
||||||
|
lowLatency := true
|
||||||
|
if document.Source.LowLatency != nil {
|
||||||
|
lowLatency = *document.Source.LowLatency
|
||||||
|
}
|
||||||
|
return ui.CameraFields{
|
||||||
|
ID: document.Source.ID, Host: document.Source.Host, Port: document.Source.Port,
|
||||||
|
Channel: document.Source.Channel, Username: document.Source.Username, Password: document.Source.Password,
|
||||||
|
Transport: document.Source.Transport, TimeoutSeconds: document.Source.TimeoutSeconds, LowLatency: lowLatency,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+236
-8
@@ -6,6 +6,7 @@ import (
|
|||||||
"image/color"
|
"image/color"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -23,6 +24,7 @@ import (
|
|||||||
"gioui.org/widget/material"
|
"gioui.org/widget/material"
|
||||||
|
|
||||||
"silverpose/v2/internal/alert"
|
"silverpose/v2/internal/alert"
|
||||||
|
"silverpose/v2/internal/config"
|
||||||
"silverpose/v2/internal/fall"
|
"silverpose/v2/internal/fall"
|
||||||
"silverpose/v2/internal/monitor"
|
"silverpose/v2/internal/monitor"
|
||||||
)
|
)
|
||||||
@@ -41,6 +43,7 @@ var (
|
|||||||
colorSubtle = color.NRGBA{R: 0x52, G: 0x65, B: 0x7C, A: 0xFF}
|
colorSubtle = color.NRGBA{R: 0x52, G: 0x65, B: 0x7C, A: 0xFF}
|
||||||
colorAccent = color.NRGBA{R: 0x25, G: 0x63, B: 0xEB, A: 0xFF}
|
colorAccent = color.NRGBA{R: 0x25, G: 0x63, B: 0xEB, A: 0xFF}
|
||||||
colorAlert = color.NRGBA{R: 0xC6, G: 0x28, B: 0x28, 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}
|
colorScrim = color.NRGBA{A: 0xB0}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -49,6 +52,33 @@ type Settings struct {
|
|||||||
ModelSHA256 string
|
ModelSHA256 string
|
||||||
RuntimeSummary string
|
RuntimeSummary string
|
||||||
EventSummary 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 {
|
type Commands struct {
|
||||||
@@ -89,6 +119,7 @@ type Window struct {
|
|||||||
incoming []galleryIncoming
|
incoming []galleryIncoming
|
||||||
seenEvents map[string]struct{}
|
seenEvents map[string]struct{}
|
||||||
running bool
|
running bool
|
||||||
|
configPath string
|
||||||
|
|
||||||
// Touched only by the event-loop goroutine.
|
// Touched only by the event-loop goroutine.
|
||||||
imageOp paint.ImageOp
|
imageOp paint.ImageOp
|
||||||
@@ -102,6 +133,32 @@ type Window struct {
|
|||||||
gallery []*galleryItem
|
gallery []*galleryItem
|
||||||
viewing *galleryItem
|
viewing *galleryItem
|
||||||
viewerClose widget.Clickable
|
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) {
|
func NewWindow(settings Settings, commands Commands) (*Window, error) {
|
||||||
@@ -139,9 +196,55 @@ func NewWindow(settings Settings, commands Commands) (*Window, error) {
|
|||||||
tabs: []string{"监控", "设置"},
|
tabs: []string{"监控", "设置"},
|
||||||
}
|
}
|
||||||
window.tabButton = make([]widget.Clickable, len(window.tabs))
|
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
|
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 {
|
func (window *Window) Run() int {
|
||||||
go func() {
|
go func() {
|
||||||
err := window.loop()
|
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 {
|
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.UniformInset(unit.Dp(12)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
return material.List(window.theme, &window.settingsList).Layout(gtx, len(rows), func(gtx layout.Context, index int) layout.Dimensions {
|
||||||
layout.Rigid(material.H6(window.theme, "来源与运行依赖(只读,不显示 RTSP 地址或密码)").Layout),
|
return layout.UniformInset(unit.Dp(4)).Layout(gtx, rows[index])
|
||||||
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),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
func highestState(result fall.FrameResult) fall.State {
|
||||||
state := fall.Normal
|
state := fall.Normal
|
||||||
for _, person := range result.People {
|
for _, person := range result.People {
|
||||||
|
|||||||
Reference in New Issue
Block a user