From fca11ccc441d29015ff58e224143e3f0d9d3eade Mon Sep 17 00:00:00 2001 From: Su Yang Date: Mon, 26 May 2025 14:40:25 +0800 Subject: [PATCH] feat: Initializer version v1.0.0 --- go.mod | 37 + go.sum | 94 +++ main.go | 1346 ++++++++++++++++++++++++++++++++++++ static/index.html | 232 +++++++ static/script.js | 1649 +++++++++++++++++++++++++++++++++++++++++++++ static/styles.css | 355 ++++++++++ 6 files changed, 3713 insertions(+) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 static/index.html create mode 100644 static/script.js create mode 100644 static/styles.css diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..94561e6 --- /dev/null +++ b/go.mod @@ -0,0 +1,37 @@ +module hands + +go 1.24.1 + +require ( + github.com/gin-contrib/cors v1.7.5 + github.com/gin-gonic/gin v1.10.1 +) + +require ( + github.com/bytedance/sonic v1.13.2 // indirect + github.com/bytedance/sonic/loader v0.2.4 // indirect + github.com/cloudwego/base64x v0.1.5 // indirect + github.com/gabriel-vasile/mimetype v1.4.9 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.26.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.17.0 // indirect + golang.org/x/crypto v0.38.0 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.25.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9ce945c --- /dev/null +++ b/go.sum @@ -0,0 +1,94 @@ +github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= +github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= +github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= +github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= +github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= +github.com/gin-contrib/cors v1.7.5 h1:cXC9SmofOrRg0w9PigwGlHG3ztswH6bqq4vJVXnvYMk= +github.com/gin-contrib/cors v1.7.5/go.mod h1:4q3yi7xBEDDWKapjT2o1V7mScKDDr8k+jZ0fSquGoy0= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= +github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= +github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU= +golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/main.go b/main.go new file mode 100644 index 0000000..f9f56c9 --- /dev/null +++ b/main.go @@ -0,0 +1,1346 @@ +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "log" + "math/rand" + "net/http" + "os" + "strings" + "sync" + "time" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" +) + +const HAND_TYPE_LEFT = 0x28 +const HAND_TYPE_RIGHT = 0x27 + +// API 请求结构体 - 添加手型支持 +type FingerPoseRequest struct { + Interface string `json:"interface,omitempty"` + Pose []byte `json:"pose" binding:"required,len=6"` + HandType string `json:"handType,omitempty"` // 新增: 手型类型 + HandId uint32 `json:"handId,omitempty"` // 新增: CAN ID +} + +type PalmPoseRequest struct { + Interface string `json:"interface,omitempty"` + Pose []byte `json:"pose" binding:"required,len=4"` + HandType string `json:"handType,omitempty"` // 新增: 手型类型 + HandId uint32 `json:"handId,omitempty"` // 新增: CAN ID +} + +type AnimationRequest struct { + Interface string `json:"interface,omitempty"` + Type string `json:"type" binding:"required,oneof=wave sway stop"` + Speed int `json:"speed" binding:"min=0,max=2000"` + HandType string `json:"handType,omitempty"` // 新增: 手型类型 + HandId uint32 `json:"handId,omitempty"` // 新增: CAN ID +} + +// 新增: 手型设置请求 +type HandTypeRequest struct { + Interface string `json:"interface" binding:"required"` + HandType string `json:"handType" binding:"required,oneof=left right"` + HandId uint32 `json:"handId" binding:"required"` +} + +// CAN 服务请求结构体 +type CanMessage struct { + Interface string `json:"interface"` + ID uint32 `json:"id"` + Data []byte `json:"data"` +} + +// 传感器数据结构体 +type SensorData struct { + Interface string `json:"interface"` + Thumb int `json:"thumb"` + Index int `json:"index"` + Middle int `json:"middle"` + Ring int `json:"ring"` + Pinky int `json:"pinky"` + PalmPosition []byte `json:"palmPosition"` + LastUpdate time.Time `json:"lastUpdate"` +} + +// API 响应结构体 +type ApiResponse struct { + Status string `json:"status"` + Message string `json:"message,omitempty"` + Error string `json:"error,omitempty"` + Data interface{} `json:"data,omitempty"` +} + +// 配置结构体 +type Config struct { + CanServiceURL string + WebPort string + DefaultInterface string + AvailableInterfaces []string +} + +// 手型配置结构体 +type HandConfig struct { + HandType string `json:"handType"` + HandId uint32 `json:"handId"` +} + +// 全局变量 +var ( + sensorDataMap map[string]*SensorData // 每个接口的传感器数据 + sensorMutex sync.RWMutex + animationActive map[string]bool // 每个接口的动画状态 + animationMutex sync.Mutex + stopAnimationMap map[string]chan struct{} // 每个接口的停止动画通道 + handConfigs map[string]*HandConfig // 每个接口的手型配置 + handConfigMutex sync.RWMutex + config *Config + serverStartTime time.Time +) + +// 解析配置 +func parseConfig() *Config { + cfg := &Config{} + + // 命令行参数 + var canInterfacesFlag string + flag.StringVar(&cfg.CanServiceURL, "can-url", "http://10.211.55.7:8080", "CAN 服务的 URL") + flag.StringVar(&cfg.WebPort, "port", "9099", "Web 服务的端口") + flag.StringVar(&cfg.DefaultInterface, "interface", "", "默认 CAN 接口") + flag.StringVar(&canInterfacesFlag, "can-interfaces", "", "支持的 CAN 接口列表,用逗号分隔 (例如: can0,can1,vcan0)") + flag.Parse() + + // 环境变量覆盖命令行参数 + if envURL := os.Getenv("CAN_SERVICE_URL"); envURL != "" { + cfg.CanServiceURL = envURL + } + if envPort := os.Getenv("WEB_PORT"); envPort != "" { + cfg.WebPort = envPort + } + if envInterface := os.Getenv("DEFAULT_INTERFACE"); envInterface != "" { + cfg.DefaultInterface = envInterface + } + if envInterfaces := os.Getenv("CAN_INTERFACES"); envInterfaces != "" { + canInterfacesFlag = envInterfaces + } + + // 解析可用接口 + if canInterfacesFlag != "" { + cfg.AvailableInterfaces = strings.Split(canInterfacesFlag, ",") + // 清理空白字符 + for i, iface := range cfg.AvailableInterfaces { + cfg.AvailableInterfaces[i] = strings.TrimSpace(iface) + } + } + + // 如果没有指定可用接口,从CAN服务获取 + if len(cfg.AvailableInterfaces) == 0 { + log.Println("🔍 未指定可用接口,将从 CAN 服务获取...") + cfg.AvailableInterfaces = getAvailableInterfacesFromCanService(cfg.CanServiceURL) + } + + // 设置默认接口 + if cfg.DefaultInterface == "" && len(cfg.AvailableInterfaces) > 0 { + cfg.DefaultInterface = cfg.AvailableInterfaces[0] + } + + return cfg +} + +// 从CAN服务获取可用接口 +func getAvailableInterfacesFromCanService(canServiceURL string) []string { + resp, err := http.Get(canServiceURL + "/api/interfaces") + if err != nil { + log.Printf("⚠️ 无法从 CAN 服务获取接口列表: %v,使用默认配置", err) + return []string{"can0", "can1"} // 默认接口 + } + defer resp.Body.Close() + + var apiResp ApiResponse + if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil { + log.Printf("⚠️ 解析 CAN 服务接口响应失败: %v,使用默认配置", err) + return []string{"can0", "can1"} + } + + if data, ok := apiResp.Data.(map[string]interface{}); ok { + if configuredPorts, ok := data["configuredPorts"].([]interface{}); ok { + interfaces := make([]string, 0, len(configuredPorts)) + for _, port := range configuredPorts { + if portStr, ok := port.(string); ok { + interfaces = append(interfaces, portStr) + } + } + if len(interfaces) > 0 { + log.Printf("✅ 从 CAN 服务获取到接口: %v", interfaces) + return interfaces + } + } + } + + log.Println("⚠️ 无法从 CAN 服务获取有效接口,使用默认配置") + return []string{"can0", "can1"} +} + +// 验证接口是否可用 +func isValidInterface(ifName string) bool { + for _, validIface := range config.AvailableInterfaces { + if ifName == validIface { + return true + } + } + return false +} + +// 获取或创建手型配置 +func getHandConfig(ifName string) *HandConfig { + handConfigMutex.RLock() + if handConfig, exists := handConfigs[ifName]; exists { + handConfigMutex.RUnlock() + return handConfig + } + handConfigMutex.RUnlock() + + // 创建默认配置 + handConfigMutex.Lock() + defer handConfigMutex.Unlock() + + // 再次检查(双重检查锁定) + if handConfig, exists := handConfigs[ifName]; exists { + return handConfig + } + + // 创建默认配置(右手) + handConfigs[ifName] = &HandConfig{ + HandType: "right", + HandId: HAND_TYPE_RIGHT, + } + + log.Printf("🆕 为接口 %s 创建默认手型配置: 右手 (0x%X)", ifName, HAND_TYPE_RIGHT) + return handConfigs[ifName] +} + +// 设置手型配置 +func setHandConfig(ifName, handType string, handId uint32) { + handConfigMutex.Lock() + defer handConfigMutex.Unlock() + + handConfigs[ifName] = &HandConfig{ + HandType: handType, + HandId: handId, + } + + log.Printf("🔧 接口 %s 手型配置已更新: %s (0x%X)", ifName, handType, handId) +} + +// 解析手型参数 +func parseHandType(handType string, handId uint32, ifName string) uint32 { + // 如果提供了有效的handId,直接使用 + if handId != 0 { + return handId + } + + // 根据handType字符串确定ID + switch strings.ToLower(handType) { + case "left": + return HAND_TYPE_LEFT + case "right": + return HAND_TYPE_RIGHT + default: + // 使用接口的配置 + handConfig := getHandConfig(ifName) + return handConfig.HandId + } +} + +// 初始化服务 +func initService() { + log.Printf("🔧 服务配置:") + log.Printf(" - CAN 服务 URL: %s", config.CanServiceURL) + log.Printf(" - Web 端口: %s", config.WebPort) + log.Printf(" - 可用接口: %v", config.AvailableInterfaces) + log.Printf(" - 默认接口: %s", config.DefaultInterface) + + // 初始化传感器数据映射 + sensorDataMap = make(map[string]*SensorData) + for _, ifName := range config.AvailableInterfaces { + sensorDataMap[ifName] = &SensorData{ + Interface: ifName, + Thumb: 0, + Index: 0, + Middle: 0, + Ring: 0, + Pinky: 0, + PalmPosition: []byte{128, 128, 128, 128}, + LastUpdate: time.Now(), + } + } + + // 初始化动画状态映射 + animationActive = make(map[string]bool) + stopAnimationMap = make(map[string]chan struct{}) + for _, ifName := range config.AvailableInterfaces { + animationActive[ifName] = false + stopAnimationMap[ifName] = make(chan struct{}, 1) + } + + // 初始化手型配置映射 + handConfigs = make(map[string]*HandConfig) + + log.Println("✅ 控制服务初始化完成") +} + +// 发送请求到 CAN 服务 +func sendToCanService(msg CanMessage) error { + jsonData, err := json.Marshal(msg) + if err != nil { + return fmt.Errorf("JSON 编码错误: %v", err) + } + + resp, err := http.Post(config.CanServiceURL+"/api/can", "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + return fmt.Errorf("CAN 服务请求失败: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + var errResp ApiResponse + if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil { + return fmt.Errorf("CAN 服务返回错误: HTTP %d", resp.StatusCode) + } + return fmt.Errorf("CAN 服务返回错误: %s", errResp.Error) + } + + return nil +} + +// 发送手指姿态指令 - 支持手型参数 +func sendFingerPose(ifName string, pose []byte, handType string, handId uint32) error { + if len(pose) != 6 { + return fmt.Errorf("无效的姿态数据长度,需要 6 个字节") + } + + // 如果未指定接口,使用默认接口 + if ifName == "" { + ifName = config.DefaultInterface + } + + // 验证接口 + if !isValidInterface(ifName) { + return fmt.Errorf("无效的接口 %s,可用接口: %v", ifName, config.AvailableInterfaces) + } + + // 解析手型ID + canId := parseHandType(handType, handId, ifName) + + // 添加随机扰动 + perturbedPose := make([]byte, len(pose)) + for i, v := range pose { + perturbedPose[i] = perturb(v, 5) + } + + // 构造 CAN 消息 + msg := CanMessage{ + Interface: ifName, + ID: canId, // 使用动态的手型ID + Data: append([]byte{0x01}, perturbedPose...), + } + + err := sendToCanService(msg) + if err == nil { + handTypeName := "右手" + if canId == HAND_TYPE_LEFT { + handTypeName = "左手" + } + log.Printf("✅ %s (%s, 0x%X) 手指动作已发送: [%X %X %X %X %X %X]", + ifName, handTypeName, canId, perturbedPose[0], perturbedPose[1], perturbedPose[2], + perturbedPose[3], perturbedPose[4], perturbedPose[5]) + } else { + log.Printf("❌ %s 手指控制发送失败: %v", ifName, err) + } + + return err +} + +// 发送掌部姿态指令 - 支持手型参数 +func sendPalmPose(ifName string, pose []byte, handType string, handId uint32) error { + if len(pose) != 4 { + return fmt.Errorf("无效的姿态数据长度,需要 4 个字节") + } + + // 如果未指定接口,使用默认接口 + if ifName == "" { + ifName = config.DefaultInterface + } + + // 验证接口 + if !isValidInterface(ifName) { + return fmt.Errorf("无效的接口 %s,可用接口: %v", ifName, config.AvailableInterfaces) + } + + // 解析手型ID + canId := parseHandType(handType, handId, ifName) + + // 添加随机扰动 + perturbedPose := make([]byte, len(pose)) + for i, v := range pose { + perturbedPose[i] = perturb(v, 8) + } + + // 构造 CAN 消息 + msg := CanMessage{ + Interface: ifName, + ID: canId, // 使用动态的手型ID + Data: append([]byte{0x04}, perturbedPose...), + } + + err := sendToCanService(msg) + if err == nil { + handTypeName := "右手" + if canId == HAND_TYPE_LEFT { + handTypeName = "左手" + } + log.Printf("✅ %s (%s, 0x%X) 掌部姿态已发送: [%X %X %X %X]", + ifName, handTypeName, canId, perturbedPose[0], perturbedPose[1], perturbedPose[2], perturbedPose[3]) + + // 更新传感器数据中的掌部位置 + sensorMutex.Lock() + if sensorData, exists := sensorDataMap[ifName]; exists { + copy(sensorData.PalmPosition, perturbedPose) + sensorData.LastUpdate = time.Now() + } + sensorMutex.Unlock() + } else { + log.Printf("❌ %s 掌部控制发送失败: %v", ifName, err) + } + + return err +} + +// 在 base 基础上进行 ±delta 的扰动,范围限制在 [0, 255] +func perturb(base byte, delta int) byte { + offset := rand.Intn(2*delta+1) - delta + v := int(base) + offset + if v < 0 { + v = 0 + } + if v > 255 { + v = 255 + } + return byte(v) +} + +// 执行波浪动画 - 支持手型参数 +func startWaveAnimation(ifName string, speed int, handType string, handId uint32) { + if speed <= 0 { + speed = 500 // 默认速度 + } + + // 如果未指定接口,使用默认接口 + if ifName == "" { + ifName = config.DefaultInterface + } + + // 验证接口 + if !isValidInterface(ifName) { + log.Printf("❌ 无法启动波浪动画: 无效的接口 %s", ifName) + return + } + + animationMutex.Lock() + + // 如果已经有动画在运行,先停止它 + if animationActive[ifName] { + select { + case stopAnimationMap[ifName] <- struct{}{}: + // 发送成功 + default: + // 通道已满,无需发送 + } + + stopAnimationMap[ifName] = make(chan struct{}, 1) + } + + animationActive[ifName] = true + animationMutex.Unlock() + + currentStopChannel := stopAnimationMap[ifName] + + go func() { + defer func() { + animationMutex.Lock() + animationActive[ifName] = false + animationMutex.Unlock() + log.Printf("👋 %s 波浪动画已完成", ifName) + }() + + fingerOrder := []int{0, 1, 2, 3, 4, 5} + open := byte(64) // 0x40 + close := byte(192) // 0xC0 + + log.Printf("🚀 开始 %s 波浪动画", ifName) + + // 动画循环 + for { + select { + case <-currentStopChannel: + log.Printf("🛑 %s 波浪动画被用户停止", ifName) + return + default: + // 波浪张开 + for _, idx := range fingerOrder { + pose := make([]byte, 6) + for j := 0; j < 6; j++ { + if j == idx { + pose[j] = open + } else { + pose[j] = close + } + } + + if err := sendFingerPose(ifName, pose, handType, handId); err != nil { + log.Printf("%s 动画发送失败: %v", ifName, err) + return + } + + delay := time.Duration(speed) * time.Millisecond + + select { + case <-currentStopChannel: + log.Printf("🛑 %s 波浪动画被用户停止", ifName) + return + case <-time.After(delay): + // 继续执行 + } + } + + // 波浪握拳 + for _, idx := range fingerOrder { + pose := make([]byte, 6) + for j := 0; j < 6; j++ { + if j == idx { + pose[j] = close + } else { + pose[j] = open + } + } + + if err := sendFingerPose(ifName, pose, handType, handId); err != nil { + log.Printf("%s 动画发送失败: %v", ifName, err) + return + } + + delay := time.Duration(speed) * time.Millisecond + + select { + case <-currentStopChannel: + log.Printf("🛑 %s 波浪动画被用户停止", ifName) + return + case <-time.After(delay): + // 继续执行 + } + } + } + } + }() +} + +// 执行横向摆动动画 - 支持手型参数 +func startSwayAnimation(ifName string, speed int, handType string, handId uint32) { + if speed <= 0 { + speed = 500 // 默认速度 + } + + // 如果未指定接口,使用默认接口 + if ifName == "" { + ifName = config.DefaultInterface + } + + // 验证接口 + if !isValidInterface(ifName) { + log.Printf("❌ 无法启动摆动动画: 无效的接口 %s", ifName) + return + } + + animationMutex.Lock() + + if animationActive[ifName] { + select { + case stopAnimationMap[ifName] <- struct{}{}: + // 发送成功 + default: + // 通道已满,无需发送 + } + + stopAnimationMap[ifName] = make(chan struct{}, 1) + } + + animationActive[ifName] = true + animationMutex.Unlock() + + currentStopChannel := stopAnimationMap[ifName] + + go func() { + defer func() { + animationMutex.Lock() + animationActive[ifName] = false + animationMutex.Unlock() + log.Printf("🔄 %s 横向摆动动画已完成", ifName) + }() + + leftPose := []byte{48, 48, 48, 48} // 0x30 + rightPose := []byte{208, 208, 208, 208} // 0xD0 + + log.Printf("🚀 开始 %s 横向摆动动画", ifName) + + // 动画循环 + for { + select { + case <-currentStopChannel: + log.Printf("🛑 %s 横向摆动动画被用户停止", ifName) + return + default: + // 向左移动 + if err := sendPalmPose(ifName, leftPose, handType, handId); err != nil { + log.Printf("%s 动画发送失败: %v", ifName, err) + return + } + + delay := time.Duration(speed) * time.Millisecond + + select { + case <-currentStopChannel: + log.Printf("🛑 %s 横向摆动动画被用户停止", ifName) + return + case <-time.After(delay): + // 继续执行 + } + + // 向右移动 + if err := sendPalmPose(ifName, rightPose, handType, handId); err != nil { + log.Printf("%s 动画发送失败: %v", ifName, err) + return + } + + select { + case <-currentStopChannel: + log.Printf("🛑 %s 横向摆动动画被用户停止", ifName) + return + case <-time.After(delay): + // 继续执行 + } + } + } + }() +} + +// 停止所有动画 +func stopAllAnimations(ifName string) { + // 如果未指定接口,停止所有接口的动画 + if ifName == "" { + for _, validIface := range config.AvailableInterfaces { + stopAllAnimations(validIface) + } + return + } + + // 验证接口 + if !isValidInterface(ifName) { + log.Printf("⚠️ 尝试停止无效接口的动画: %s", ifName) + return + } + + animationMutex.Lock() + defer animationMutex.Unlock() + + if animationActive[ifName] { + select { + case stopAnimationMap[ifName] <- struct{}{}: + log.Printf("✅ 已发送停止 %s 动画信号", ifName) + default: + stopAnimationMap[ifName] = make(chan struct{}, 1) + stopAnimationMap[ifName] <- struct{}{} + log.Printf("⚠️ %s 通道重置后发送了停止信号", ifName) + } + + animationActive[ifName] = false + + go func() { + time.Sleep(100 * time.Millisecond) + resetToDefaultPose(ifName) + }() + } else { + log.Printf("ℹ️ %s 当前没有运行中的动画", ifName) + } +} + +// 重置到默认姿势 +func resetToDefaultPose(ifName string) { + // 如果未指定接口,重置所有接口 + if ifName == "" { + for _, validIface := range config.AvailableInterfaces { + resetToDefaultPose(validIface) + } + return + } + + // 验证接口 + if !isValidInterface(ifName) { + log.Printf("⚠️ 尝试重置无效接口: %s", ifName) + return + } + + defaultFingerPose := []byte{64, 64, 64, 64, 64, 64} + defaultPalmPose := []byte{128, 128, 128, 128} + + // 获取当前接口的手型配置 + handConfig := getHandConfig(ifName) + + if err := sendFingerPose(ifName, defaultFingerPose, handConfig.HandType, handConfig.HandId); err != nil { + log.Printf("%s 重置手指姿势失败: %v", ifName, err) + } + + if err := sendPalmPose(ifName, defaultPalmPose, handConfig.HandType, handConfig.HandId); err != nil { + log.Printf("%s 重置掌部姿势失败: %v", ifName, err) + } + + log.Printf("✅ 已重置 %s 到默认姿势", ifName) +} + +// 读取传感器数据 (模拟) +func readSensorData() { + go func() { + for { + sensorMutex.Lock() + // 为每个接口模拟压力数据 (0-100) + for _, ifName := range config.AvailableInterfaces { + if sensorData, exists := sensorDataMap[ifName]; exists { + sensorData.Thumb = rand.Intn(101) + sensorData.Index = rand.Intn(101) + sensorData.Middle = rand.Intn(101) + sensorData.Ring = rand.Intn(101) + sensorData.Pinky = rand.Intn(101) + sensorData.LastUpdate = time.Now() + } + } + sensorMutex.Unlock() + + time.Sleep(500 * time.Millisecond) + } + }() +} + +// 检查 CAN 服务状态 +func checkCanServiceStatus() map[string]bool { + resp, err := http.Get(config.CanServiceURL + "/api/status") + if err != nil { + log.Printf("❌ CAN 服务状态检查失败: %v", err) + result := make(map[string]bool) + for _, ifName := range config.AvailableInterfaces { + result[ifName] = false + } + return result + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + log.Printf("❌ CAN 服务返回非正常状态: %d", resp.StatusCode) + result := make(map[string]bool) + for _, ifName := range config.AvailableInterfaces { + result[ifName] = false + } + return result + } + + var statusResp ApiResponse + if err := json.NewDecoder(resp.Body).Decode(&statusResp); err != nil { + log.Printf("❌ 解析 CAN 服务状态失败: %v", err) + result := make(map[string]bool) + for _, ifName := range config.AvailableInterfaces { + result[ifName] = false + } + return result + } + + // 检查状态数据 + result := make(map[string]bool) + for _, ifName := range config.AvailableInterfaces { + result[ifName] = false + } + + // 从响应中获取各接口状态 + if statusData, ok := statusResp.Data.(map[string]interface{}); ok { + if interfaces, ok := statusData["interfaces"].(map[string]interface{}); ok { + for ifName, ifStatus := range interfaces { + if status, ok := ifStatus.(map[string]interface{}); ok { + if active, ok := status["active"].(bool); ok { + result[ifName] = active + } + } + } + } + } + + return result +} + +// API 路由设置 +func setupRoutes(r *gin.Engine) { + r.StaticFile("/", "./static/index.html") + r.Static("/static", "./static") + + api := r.Group("/api") + { + // 手型设置 API - 新增 + api.POST("/hand-type", func(c *gin.Context) { + var req HandTypeRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, ApiResponse{ + Status: "error", + Error: "无效的手型设置请求: " + err.Error(), + }) + return + } + + // 验证接口 + if !isValidInterface(req.Interface) { + c.JSON(http.StatusBadRequest, ApiResponse{ + Status: "error", + Error: fmt.Sprintf("无效的接口 %s,可用接口: %v", req.Interface, config.AvailableInterfaces), + }) + return + } + + // 验证手型ID + if req.HandType == "left" && req.HandId != HAND_TYPE_LEFT { + req.HandId = HAND_TYPE_LEFT + } else if req.HandType == "right" && req.HandId != HAND_TYPE_RIGHT { + req.HandId = HAND_TYPE_RIGHT + } + + // 设置手型配置 + setHandConfig(req.Interface, req.HandType, req.HandId) + + handTypeName := "右手" + if req.HandType == "left" { + handTypeName = "左手" + } + + c.JSON(http.StatusOK, ApiResponse{ + Status: "success", + Message: fmt.Sprintf("接口 %s 手型已设置为%s (0x%X)", req.Interface, handTypeName, req.HandId), + Data: map[string]interface{}{ + "interface": req.Interface, + "handType": req.HandType, + "handId": req.HandId, + }, + }) + }) + + // 手指姿态 API - 更新支持手型 + api.POST("/fingers", func(c *gin.Context) { + var req FingerPoseRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, ApiResponse{ + Status: "error", + Error: "无效的手指姿态数据: " + err.Error(), + }) + return + } + + // 验证每个值是否在范围内 + for _, v := range req.Pose { + if v < 0 || v > 255 { + c.JSON(http.StatusBadRequest, ApiResponse{ + Status: "error", + Error: "手指姿态值必须在 0-255 范围内", + }) + return + } + } + + // 如果未指定接口,使用默认接口 + if req.Interface == "" { + req.Interface = config.DefaultInterface + } + + // 验证接口 + if !isValidInterface(req.Interface) { + c.JSON(http.StatusBadRequest, ApiResponse{ + Status: "error", + Error: fmt.Sprintf("无效的接口 %s,可用接口: %v", req.Interface, config.AvailableInterfaces), + }) + return + } + + stopAllAnimations(req.Interface) + + if err := sendFingerPose(req.Interface, req.Pose, req.HandType, req.HandId); err != nil { + c.JSON(http.StatusInternalServerError, ApiResponse{ + Status: "error", + Error: "发送手指姿态失败: " + err.Error(), + }) + return + } + + c.JSON(http.StatusOK, ApiResponse{ + Status: "success", + Message: "手指姿态指令发送成功", + Data: map[string]interface{}{"interface": req.Interface, "pose": req.Pose}, + }) + }) + + // 掌部姿态 API - 更新支持手型 + api.POST("/palm", func(c *gin.Context) { + var req PalmPoseRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, ApiResponse{ + Status: "error", + Error: "无效的掌部姿态数据: " + err.Error(), + }) + return + } + + // 验证每个值是否在范围内 + for _, v := range req.Pose { + if v < 0 || v > 255 { + c.JSON(http.StatusBadRequest, ApiResponse{ + Status: "error", + Error: "掌部姿态值必须在 0-255 范围内", + }) + return + } + } + + // 如果未指定接口,使用默认接口 + if req.Interface == "" { + req.Interface = config.DefaultInterface + } + + // 验证接口 + if !isValidInterface(req.Interface) { + c.JSON(http.StatusBadRequest, ApiResponse{ + Status: "error", + Error: fmt.Sprintf("无效的接口 %s,可用接口: %v", req.Interface, config.AvailableInterfaces), + }) + return + } + + stopAllAnimations(req.Interface) + + if err := sendPalmPose(req.Interface, req.Pose, req.HandType, req.HandId); err != nil { + c.JSON(http.StatusInternalServerError, ApiResponse{ + Status: "error", + Error: "发送掌部姿态失败: " + err.Error(), + }) + return + } + + c.JSON(http.StatusOK, ApiResponse{ + Status: "success", + Message: "掌部姿态指令发送成功", + Data: map[string]interface{}{"interface": req.Interface, "pose": req.Pose}, + }) + }) + + // 预设姿势 API - 更新支持手型 + api.POST("/preset/:pose", func(c *gin.Context) { + pose := c.Param("pose") + + // 从查询参数获取接口名称和手型 + ifName := c.Query("interface") + handType := c.Query("handType") + + if ifName == "" { + ifName = config.DefaultInterface + } + + // 验证接口 + if !isValidInterface(ifName) { + c.JSON(http.StatusBadRequest, ApiResponse{ + Status: "error", + Error: fmt.Sprintf("无效的接口 %s,可用接口: %v", ifName, config.AvailableInterfaces), + }) + return + } + + stopAllAnimations(ifName) + + var fingerPose []byte + var message string + + switch pose { + case "fist": + fingerPose = []byte{64, 64, 64, 64, 64, 64} + message = "已设置握拳姿势" + case "open": + fingerPose = []byte{192, 192, 192, 192, 192, 192} + message = "已设置完全张开姿势" + case "pinch": + fingerPose = []byte{120, 120, 64, 64, 64, 64} + message = "已设置捏取姿势" + case "thumbsup": + fingerPose = []byte{64, 192, 192, 192, 192, 64} + message = "已设置竖起大拇指姿势" + case "point": + fingerPose = []byte{192, 64, 192, 192, 192, 64} + message = "已设置食指指点姿势" + // 数字手势 + case "1": + fingerPose = []byte{192, 64, 192, 192, 192, 64} + message = "已设置数字1手势" + case "2": + fingerPose = []byte{192, 64, 64, 192, 192, 64} + message = "已设置数字2手势" + case "3": + fingerPose = []byte{192, 64, 64, 64, 192, 64} + message = "已设置数字3手势" + case "4": + fingerPose = []byte{192, 64, 64, 64, 64, 64} + message = "已设置数字4手势" + case "5": + fingerPose = []byte{192, 192, 192, 192, 192, 192} + message = "已设置数字5手势" + case "6": + fingerPose = []byte{64, 192, 192, 192, 192, 64} + message = "已设置数字6手势" + case "7": + fingerPose = []byte{64, 64, 192, 192, 192, 64} + message = "已设置数字7手势" + case "8": + fingerPose = []byte{64, 64, 64, 192, 192, 64} + message = "已设置数字8手势" + case "9": + fingerPose = []byte{64, 64, 64, 64, 192, 64} + message = "已设置数字9手势" + default: + c.JSON(http.StatusBadRequest, ApiResponse{ + Status: "error", + Error: "无效的预设姿势", + }) + return + } + + // 解析手型ID(从查询参数或使用接口配置) + handId := uint32(0) + if handType != "" { + handId = parseHandType(handType, 0, ifName) + } + + if err := sendFingerPose(ifName, fingerPose, handType, handId); err != nil { + c.JSON(http.StatusInternalServerError, ApiResponse{ + Status: "error", + Error: "设置预设姿势失败: " + err.Error(), + }) + return + } + + c.JSON(http.StatusOK, ApiResponse{ + Status: "success", + Message: message, + Data: map[string]interface{}{"interface": ifName, "pose": fingerPose}, + }) + }) + + // 动画控制 API - 更新支持手型 + api.POST("/animation", func(c *gin.Context) { + var req AnimationRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, ApiResponse{ + Status: "error", + Error: "无效的动画请求: " + err.Error(), + }) + return + } + + // 如果未指定接口,使用默认接口 + if req.Interface == "" { + req.Interface = config.DefaultInterface + } + + // 验证接口 + if !isValidInterface(req.Interface) { + c.JSON(http.StatusBadRequest, ApiResponse{ + Status: "error", + Error: fmt.Sprintf("无效的接口 %s,可用接口: %v", req.Interface, config.AvailableInterfaces), + }) + return + } + + // 停止当前动画 + stopAllAnimations(req.Interface) + + // 如果是停止命令,直接返回 + if req.Type == "stop" { + c.JSON(http.StatusOK, ApiResponse{ + Status: "success", + Message: fmt.Sprintf("%s 动画已停止", req.Interface), + }) + return + } + + // 处理速度参数 + if req.Speed <= 0 { + req.Speed = 500 // 默认速度 + } + + // 根据类型启动动画 + switch req.Type { + case "wave": + startWaveAnimation(req.Interface, req.Speed, req.HandType, req.HandId) + c.JSON(http.StatusOK, ApiResponse{ + Status: "success", + Message: fmt.Sprintf("%s 波浪动画已启动", req.Interface), + Data: map[string]interface{}{"interface": req.Interface, "speed": req.Speed}, + }) + case "sway": + startSwayAnimation(req.Interface, req.Speed, req.HandType, req.HandId) + c.JSON(http.StatusOK, ApiResponse{ + Status: "success", + Message: fmt.Sprintf("%s 横向摆动动画已启动", req.Interface), + Data: map[string]interface{}{"interface": req.Interface, "speed": req.Speed}, + }) + default: + c.JSON(http.StatusBadRequest, ApiResponse{ + Status: "error", + Error: "无效的动画类型", + }) + } + }) + + // 获取传感器数据 API + api.GET("/sensors", func(c *gin.Context) { + // 从查询参数获取接口名称 + ifName := c.Query("interface") + + sensorMutex.RLock() + defer sensorMutex.RUnlock() + + if ifName != "" { + // 验证接口 + if !isValidInterface(ifName) { + c.JSON(http.StatusBadRequest, ApiResponse{ + Status: "error", + Error: fmt.Sprintf("无效的接口 %s,可用接口: %v", ifName, config.AvailableInterfaces), + }) + return + } + + // 请求特定接口的数据 + if sensorData, ok := sensorDataMap[ifName]; ok { + c.JSON(http.StatusOK, ApiResponse{ + Status: "success", + Data: sensorData, + }) + } else { + c.JSON(http.StatusInternalServerError, ApiResponse{ + Status: "error", + Error: "传感器数据不存在", + }) + } + } else { + // 返回所有接口的数据 + c.JSON(http.StatusOK, ApiResponse{ + Status: "success", + Data: sensorDataMap, + }) + } + }) + + // 系统状态 API - 更新包含手型配置 + api.GET("/status", func(c *gin.Context) { + animationMutex.Lock() + animationStatus := make(map[string]bool) + for _, ifName := range config.AvailableInterfaces { + animationStatus[ifName] = animationActive[ifName] + } + animationMutex.Unlock() + + // 检查 CAN 服务状态 + canStatus := checkCanServiceStatus() + + // 获取手型配置 + handConfigMutex.RLock() + handConfigsData := make(map[string]interface{}) + for ifName, handConfig := range handConfigs { + handConfigsData[ifName] = map[string]interface{}{ + "handType": handConfig.HandType, + "handId": handConfig.HandId, + } + } + handConfigMutex.RUnlock() + + interfaceStatuses := make(map[string]interface{}) + for _, ifName := range config.AvailableInterfaces { + interfaceStatuses[ifName] = map[string]interface{}{ + "active": canStatus[ifName], + "animationActive": animationStatus[ifName], + "handConfig": handConfigsData[ifName], + } + } + + c.JSON(http.StatusOK, ApiResponse{ + Status: "success", + Data: map[string]interface{}{ + "interfaces": interfaceStatuses, + "uptime": time.Since(serverStartTime).String(), + "canServiceURL": config.CanServiceURL, + "defaultInterface": config.DefaultInterface, + "availableInterfaces": config.AvailableInterfaces, + "activeInterfaces": len(canStatus), + "handConfigs": handConfigsData, + }, + }) + }) + + // 获取可用接口列表 API - 修复数据格式 + api.GET("/interfaces", func(c *gin.Context) { + // 确保返回前端期望的数据格式 + responseData := map[string]interface{}{ + "availableInterfaces": config.AvailableInterfaces, + "defaultInterface": config.DefaultInterface, + } + + c.JSON(http.StatusOK, ApiResponse{ + Status: "success", + Data: responseData, + }) + }) + + // 获取手型配置 API - 新增 + api.GET("/hand-configs", func(c *gin.Context) { + handConfigMutex.RLock() + defer handConfigMutex.RUnlock() + + result := make(map[string]interface{}) + for _, ifName := range config.AvailableInterfaces { + if handConfig, exists := handConfigs[ifName]; exists { + result[ifName] = map[string]interface{}{ + "handType": handConfig.HandType, + "handId": handConfig.HandId, + } + } else { + // 返回默认配置 + result[ifName] = map[string]interface{}{ + "handType": "right", + "handId": HAND_TYPE_RIGHT, + } + } + } + + c.JSON(http.StatusOK, ApiResponse{ + Status: "success", + Data: result, + }) + }) + + // 健康检查端点 - 新增,用于调试 + api.GET("/health", func(c *gin.Context) { + c.JSON(http.StatusOK, ApiResponse{ + Status: "success", + Message: "CAN Control Service is running", + Data: map[string]interface{}{ + "timestamp": time.Now(), + "availableInterfaces": config.AvailableInterfaces, + "defaultInterface": config.DefaultInterface, + "serviceVersion": "1.0.0-hand-type-support", + }, + }) + }) + } +} + +func printUsage() { + fmt.Println("CAN Control Service with Hand Type Support") + fmt.Println("Usage:") + fmt.Println(" -can-url string CAN 服务的 URL (default: http://10.211.55.7:8080)") + fmt.Println(" -port string Web 服务的端口 (default: 9099)") + fmt.Println(" -interface string 默认 CAN 接口") + fmt.Println(" -can-interfaces string 支持的 CAN 接口列表,用逗号分隔") + fmt.Println("") + fmt.Println("Environment Variables:") + fmt.Println(" CAN_SERVICE_URL CAN 服务的 URL") + fmt.Println(" WEB_PORT Web 服务的端口") + fmt.Println(" DEFAULT_INTERFACE 默认 CAN 接口") + fmt.Println(" CAN_INTERFACES 支持的 CAN 接口列表,用逗号分隔") + fmt.Println("") + fmt.Println("New Features:") + fmt.Println(" - Support for left/right hand configuration") + fmt.Println(" - Dynamic CAN ID assignment based on hand type") + fmt.Println(" - Hand type API endpoints") + fmt.Println(" - Enhanced logging with hand type information") + fmt.Println("") + fmt.Println("Examples:") + fmt.Println(" ./control-service -can-interfaces can0,can1,vcan0") + fmt.Println(" ./control-service -interface can1 -can-interfaces can0,can1") + fmt.Println(" CAN_INTERFACES=can0,can1,vcan0 ./control-service") + fmt.Println(" CAN_SERVICE_URL=http://localhost:8080 ./control-service") +} + +func main() { + // 检查是否请求帮助 + if len(os.Args) > 1 && (os.Args[1] == "-h" || os.Args[1] == "--help") { + printUsage() + return + } + + // 解析配置 + config = parseConfig() + + // 验证配置 + if len(config.AvailableInterfaces) == 0 { + log.Fatal("❌ 没有可用的 CAN 接口") + } + + if config.DefaultInterface == "" { + log.Fatal("❌ 没有设置默认 CAN 接口") + } + + // 记录启动时间 + serverStartTime = time.Now() + + log.Printf("🚀 启动 CAN 控制服务 (支持左右手配置)") + + // 初始化随机数种子 + rand.Seed(time.Now().UnixNano()) + + // 初始化服务 + initService() + + // 启动传感器数据模拟 + readSensorData() + + // 设置 Gin 模式 + gin.SetMode(gin.ReleaseMode) + + // 创建 Gin 引擎 + r := gin.Default() + + r.Use(cors.New(cors.Config{ + AllowOrigins: []string{"*"}, // 允许的域,*表示允许所有 + AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}, + AllowHeaders: []string{"Origin", "Content-Length", "Content-Type", "Authorization"}, + ExposeHeaders: []string{"Content-Length"}, + AllowCredentials: true, + MaxAge: 12 * time.Hour, + })) + + // 设置 API 路由 + setupRoutes(r) + + // 启动服务器 + log.Printf("🌐 CAN 控制服务运行在 http://localhost:%s", config.WebPort) + log.Printf("📡 连接到 CAN 服务: %s", config.CanServiceURL) + log.Printf("🎯 默认接口: %s", config.DefaultInterface) + log.Printf("🔌 可用接口: %v", config.AvailableInterfaces) + log.Printf("🤖 支持左右手动态配置") + + if err := r.Run(":" + config.WebPort); err != nil { + log.Fatalf("❌ 服务启动失败: %v", err) + } +} diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..a7fff2c --- /dev/null +++ b/static/index.html @@ -0,0 +1,232 @@ + + + + + + 灵巧手 Linker Hand L10 控制面板 + + + +

灵巧手 Linker Hand L10 控制面板

+ + +
+

🤖 手部配置管理

+
+ + +
+
+ +
+
+ + +
+ ⚠️ CAN 服务连接异常,请检查服务状态或网络连接 +
+ + +
+

🎮 全局控制

+
+ + + + +
+
+ +
+
+

手指控制 指令0x01

+ +
+

手指关节控制

+
+
+ 拇指 (Thumb) + 64 +
+ +
+ +
+
+ 拇指转动 (Thumb Rotate) + 64 +
+ +
+ +
+
+ 食指 (Index) + 64 +
+ +
+ +
+
+ 中指 (Middle) + 64 +
+ +
+ +
+
+ 无名指 (Ring) + 64 +
+ +
+ +
+
+ 小指 (Pinky) + 64 +
+ +
+ +
+ +
+

掌部控制 指令0x04

+
+
+ 关节 7 + 128 +
+ +
+ +
+
+ 关节 8 + 128 +
+ +
+ +
+
+ 关节 9 + 128 +
+ +
+ +
+
+ 关节 10 + 128 +
+ +
+
+
+ +
+

预设动作

+
+ + + + + + + + + + + + +
+ +

NUM Pose

+
+ + + + + + + + + + + + + + + + + + + + + + + +
+ +

动画控制

+
+
+
+ 动作速度 (毫秒) + 500 +
+ +
+
+ + + +
+
+
+ +
+

状态监控

+
+
+ + 00:00:00 + 正在初始化系统... +
+
+
+

启用的手部状态

+
+
等待更新...
+
+
+
+

传感器数据 (TBD)

+
+
等待更新...
+
+
+
+
+ + + + + + \ No newline at end of file diff --git a/static/script.js b/static/script.js new file mode 100644 index 0000000..86b304a --- /dev/null +++ b/static/script.js @@ -0,0 +1,1649 @@ +// 全局变量 +let availableInterfaces = []; +let interfaceStatus = {}; +let handConfigs = {}; // 存储每个手的配置 +let handTypeIds = { + 'left': 0x28, // HAND_TYPE_LEFT + 'right': 0x27 // HAND_TYPE_RIGHT +}; + +// 主要控制模块 +const LinkerHandController = { + // 常量定义 + DEFAULTS: { + FINGER: { + OPEN: 64, // 完全张开值 + CLOSED: 192, // 完全闭合值 + NEUTRAL: 128 // 中间值 + }, + PALM: { + NEUTRAL: 128, // 中间值 + LEFT: 48, // 左侧 + RIGHT: 208 // 右侧 + }, + ANIMATION: { + DEFAULT_SPEED: 500 // 默认动画速度 + } + }, + + // 预设姿势配置 + PRESETS: { + FIST: [64, 64, 64, 64, 64, 64], // 握拳 + OPEN: [192, 192, 192, 192, 192, 192], // 张开 + THUMBSUP: [255, 255, 0, 0, 0, 0], // 竖起大拇指 + POINT: [0, 0, 255, 0, 0, 0], // 食指指点 + YO: [255, 255, 255, 0, 0, 255], // Yo! + GUN: [255, 255, 255, 255, 0, 0], // PONG! + WAVE: [40, 60, 80, 100, 120, 140], // 波浪形 + PALM_LEFT: [48, 48, 48, 48], // 掌部左移 + PALM_RIGHT: [208, 208, 208, 208], // 掌部右移 + PALM_NEUTRAL: [128, 128, 128, 128], // 掌部中立 + PALM_GUN: [0, 0, 0, 128], // 掌部 GUN + + PINCH: [114, 63, 136, 0, 0, 0], // 捏取姿势 + PALM_PINCH: [255, 163, 255, 127], + + OK: [124, 31, 132, 255, 255, 255], + PALM_OK: [255, 163, 255, 127], + + BIG_FIST: [49, 32, 40, 36, 41, 46], // 大握拳 + PALM_BIG_FIST: [255, 235, 128, 128], // 大握拳掌部 + + BIG_OPEN: [255, 255, 255, 255, 255, 255], // 大张开 + PALM_BIG_OPEN: [128, 128, 128, 128], // 大张开掌部 + + YEAH: [0, 103, 255, 255, 0, 0], // Yeah! + PALM_YEAH: [255, 235, 128, 128], // Yeah!掌部 + + // 数字手势预设 + ONE: [0, 57, 255, 0, 0, 0], + PALM_ONE: [255, 109, 255, 118], + TWO: [0, 57, 255, 255, 0, 0], + PALM_TWO: [255, 109, 255, 118], + THREE: [0, 57, 255, 255, 255, 0], + PALM_THREE: [255, 109, 255, 118], + FOUR: [0, 57, 255, 255, 255, 255], + PALM_FOUR: [255, 109, 255, 118], + FIVE: [255, 255, 255, 255, 255, 255], + PALM_FIVE: [255, 109, 255, 118], + SIX: [255, 255, 0, 0, 0, 255], + PALM_SIX: [255, 255, 255, 255], + SEVEN: [110, 137, 130, 109, 0, 0], + PALM_SEVEN: [255, 200, 199, 76], + EIGHT: [216, 240, 255, 36, 41, 46], + PALM_EIGHT: [106, 200, 199, 76], + NINE: [0, 255, 159, 0, 0, 0], + PALM_NINE: [255, 38, 195, 51] + }, + + // 防抖函数 + debounce: function (func, delay) { + let timer; + return function () { + clearTimeout(timer); + timer = setTimeout(func, delay); + }; + }, + + // 初始化滑块显示与实时控制发送(带防抖) + initSliderDisplays: function () { + const fingerSliders = Array.from({ length: 6 }, (_, i) => document.getElementById(`finger${i}`)); + const palmSliders = Array.from({ length: 4 }, (_, i) => document.getElementById(`palm${i}`)); + const delayDefault = 30; + + const updateFingerPose = this.debounce(() => { + const pose = this.getFingerPoseValues(); + this.sendFingerPoseToAll(pose); + }, delayDefault); + + const updatePalmPose = this.debounce(() => { + const pose = this.getPalmPoseValues(); + this.sendPalmPoseToAll(pose); + }, delayDefault); + + // 初始化手指滑块监听器 + fingerSliders.forEach((slider, i) => { + slider.addEventListener('input', () => { + document.getElementById(`finger${i}-value`).textContent = slider.value; + updateFingerPose(); + }); + }); + + // 初始化掌部滑块监听器 + palmSliders.forEach((slider, i) => { + slider.addEventListener('input', () => { + document.getElementById(`palm${i}-value`).textContent = slider.value; + updatePalmPose(); + }); + }); + + // 动画速度滑块更新 + const animationSlider = document.getElementById('animation-speed'); + animationSlider.addEventListener('input', function () { + document.getElementById('speed-value').textContent = this.value; + }); + }, + + // 获取手指姿态值 + getFingerPoseValues: function () { + const pose = []; + for (let i = 0; i < 6; i++) { + pose.push(parseInt(document.getElementById(`finger${i}`).value)); + } + return pose; + }, + + // 获取掌部姿态值 + getPalmPoseValues: function () { + const pose = []; + for (let i = 0; i < 4; i++) { + pose.push(parseInt(document.getElementById(`palm${i}`).value)); + } + return pose; + }, + + // 设置手指滑块值 + applyFingerPreset: function (values) { + if (!Array.isArray(values) || values.length !== 6) { + logMessage('error', '无效的手指预设值'); + return; + } + + // 设置滑块值 + for (let i = 0; i < 6; i++) { + const slider = document.getElementById(`finger${i}`); + slider.value = values[i]; + document.getElementById(`finger${i}-value`).textContent = values[i]; + } + + logMessage('info', '已应用手指预设姿势'); + }, + + // 设置掌部滑块值 + applyPalmPreset: function (values) { + if (!Array.isArray(values) || values.length !== 4) { + logMessage('error', '无效的掌部预设值'); + return; + } + + // 设置滑块值 + for (let i = 0; i < 4; i++) { + const slider = document.getElementById(`palm${i}`); + slider.value = values[i]; + document.getElementById(`palm${i}-value`).textContent = values[i]; + } + + logMessage('info', '已应用掌部预设姿势'); + }, + + // 发送手指姿态到所有启用手部 + sendFingerPoseToAll: function (pose) { + const enabledHands = getEnabledHands(); + if (enabledHands.length === 0) { + logMessage('error', '没有启用的手部'); + return; + } + + logMessage('info', `发送手指姿态到 ${enabledHands.length} 个启用的手部: [${pose.join(', ')}]`); + + enabledHands.forEach(async (config) => { + await sendFingerPoseToHand(config, pose); + }); + }, + + // 发送掌部姿态到所有启用手部 + sendPalmPoseToAll: function (pose) { + const enabledHands = getEnabledHands(); + if (enabledHands.length === 0) { + logMessage('error', '没有启用的手部'); + return; + } + + logMessage('info', `发送掌部姿态到 ${enabledHands.length} 个启用的手部: [${pose.join(', ')}]`); + + enabledHands.forEach(async (config) => { + await sendPalmPoseToHand(config, pose); + }); + }, + + // 启动传感器数据轮询 + startSensorDataPolling: function () { + // 立即获取一次数据 + this.fetchSensorData(); + + // 设置定时获取 + setInterval(() => { + this.fetchSensorData(); + }, 2000); // 每2秒更新一次 + }, + + // 获取传感器数据 + fetchSensorData: function () { + fetch('/api/sensors') + .then(response => response.json()) + .then(data => { + if (data.status === 'success') { + this.updateSensorDisplay(data.data); + } + }) + .catch(error => { + console.error('获取传感器数据失败:', error); + }); + }, + + // 更新传感器显示 + updateSensorDisplay: function (data) { + const sensorDisplay = document.getElementById('sensor-data'); + if (!sensorDisplay || !data) return; + + // 创建进度条显示 + let html = ''; + + // 手指压力传感器 + html += this.createSensorRow('拇指压力', data.thumb); + html += this.createSensorRow('食指压力', data.index); + html += this.createSensorRow('中指压力', data.middle); + html += this.createSensorRow('无名指压力', data.ring); + html += this.createSensorRow('小指压力', data.pinky); + + html += '
'; + + // 更新最后更新时间 + const lastUpdate = new Date(data.lastUpdate).toLocaleTimeString(); + html += `
最后更新: ${lastUpdate}
`; + + sensorDisplay.innerHTML = html; + }, + + // 创建传感器行 + createSensorRow: function (label, value) { + if (value === undefined || value === null) value = 0; + return ` + ${label} + + ${value}% + `; + } +}; + +// 页面加载时初始化 +document.addEventListener('DOMContentLoaded', function() { + initializeSystem(); + setupEventListeners(); + setupSliderEvents(); + LinkerHandController.initSliderDisplays(); + LinkerHandController.startSensorDataPolling(); + startStatusUpdater(); +}); + +// 初始化系统 - 添加更详细的错误处理和调试 +async function initializeSystem() { + try { + logMessage('info', '开始初始化系统...'); + + // 步骤1: 加载可用接口 + logMessage('info', '步骤 1/3: 加载可用接口'); + await loadAvailableInterfaces(); + + // 验证接口加载是否成功 + if (!availableInterfaces || availableInterfaces.length === 0) { + throw new Error('未能获取到任何可用接口'); + } + + // 步骤2: 生成手部配置 + logMessage('info', '步骤 2/3: 生成手部配置'); + generateHandConfigs(); + + // 验证手部配置是否成功 + if (!handConfigs || Object.keys(handConfigs).length === 0) { + throw new Error('未能生成手部配置'); + } + + // 步骤3: 检查接口状态 + logMessage('info', '步骤 3/3: 检查接口状态'); + await checkAllInterfaceStatus(); + + logMessage('success', '系统初始化完成'); + + } catch (error) { + logMessage('error', `系统初始化失败: ${error.message}`); + console.error('InitializeSystem Error:', error); + + // 尝试使用默认配置恢复 + if (!availableInterfaces || availableInterfaces.length === 0) { + logMessage('info', '尝试使用默认配置恢复...'); + availableInterfaces = ['can0', 'can1', 'vcan0', 'vcan1']; + generateHandConfigs(); + } + } +} + +// 加载可用接口 +async function loadAvailableInterfaces() { + try { + logMessage('info', '正在获取可用 CAN 接口...'); + const response = await fetch('/api/interfaces'); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + + const data = await response.json(); + if (data.status === 'success') { + availableInterfaces = data.data.availableInterfaces || []; + + logMessage('success', `获取到 ${availableInterfaces.length} 个可用接口: ${availableInterfaces.join(', ')}`); + hideConnectionWarning(); + } else { + throw new Error(data.error || '获取接口失败'); + } + } catch (error) { + logMessage('error', `获取接口失败: ${error.message}`); + showConnectionWarning(); + // 设置默认值 + availableInterfaces = ['can0', 'can1', 'vcan0', 'vcan1']; + } +} + +// 生成手部配置 - 添加调试和错误处理 +function generateHandConfigs() { + const handsGrid = document.getElementById('hands-grid'); + if (!handsGrid) { + console.error('Hands grid element not found'); + logMessage('error', '无法找到手部配置容器'); + return; + } + + // 清空现有内容 + handsGrid.innerHTML = ''; + + if (!availableInterfaces || availableInterfaces.length === 0) { + handsGrid.innerHTML = '
没有可用的CAN接口
'; + logMessage('warning', '没有可用接口,无法生成手部配置'); + return; + } + + logMessage('info', `为 ${availableInterfaces.length} 个接口生成手部配置...`); + + // 清空现有配置 + handConfigs = {}; + + // 为每个接口创建配置项 + availableInterfaces.forEach((iface, index) => { + const handId = `hand_${iface}`; + + try { + // 创建默认配置 + handConfigs[handId] = { + id: handId, + interface: iface, + handType: index % 2 === 0 ? 'right' : 'left', // 交替默认左右手 + enabled: index < 2, // 默认启用前两个 + status: 'offline' + }; + + // 创建HTML元素 + const handElement = createHandElement(handConfigs[handId]); + if (handElement) { + handsGrid.appendChild(handElement); + } else { + throw new Error('创建手部元素失败'); + } + } catch (error) { + console.error(`Failed to create hand element for ${iface}:`, error); + logMessage('error', `创建 ${iface} 的手部配置失败: ${error.message}`); + } + }); + + // 延迟更新状态,确保DOM完全构建 + setTimeout(() => { + updateEnabledHandsStatus(); + logMessage('success', `成功生成 ${Object.keys(handConfigs).length} 个手部配置`); + }, 100); +} + +// 添加一个安全的DOM检查函数 +function validateHandElement(handId) { + const element = document.getElementById(handId); + if (!element) { + console.error(`validateHandElement: 找不到元素 ${handId}`); + return false; + } + + const requiredElements = [ + `.hand-title`, + `#${handId}_checkbox`, + `#${handId}_interface`, + `#${handId}_handtype`, + `#${handId}_status_dot`, + `#${handId}_status_text` + ]; + + let isValid = true; + requiredElements.forEach(selector => { + const el = selector.startsWith('#') ? + document.getElementById(selector.slice(1)) : + element.querySelector(selector); + + if (!el) { + console.error(`validateHandElement: 在 ${handId} 中找不到 ${selector}`); + isValid = false; + } + }); + + return isValid; +} + +// 增强的错误处理包装器 +function safeUpdateHandElement(handId) { + try { + if (validateHandElement(handId)) { + updateHandElement(handId); + } else { + logMessage('error', `手部元素 ${handId} 验证失败,跳过更新`); + } + } catch (error) { + console.error(`Error updating hand element ${handId}:`, error); + logMessage('error', `更新手部元素 ${handId} 时出错: ${error.message}`); + } +} + +// 创建手部配置元素 +function createHandElement(config) { + const div = document.createElement('div'); + div.className = `hand-item ${config.enabled ? 'enabled' : 'disabled'}`; + div.id = config.id; + + const handEmoji = config.handType === 'left' ? '✋' : '🤚'; + const handLabel = config.handType === 'left' ? '左手' : '右手'; + const handId = handTypeIds[config.handType]; + + // 确保HTML结构完整且正确 + div.innerHTML = ` +
+ + ${handEmoji} ${config.interface} - ${handLabel} +
+
+
+ + +
+
+ + +
+
+
+ + 检查中... +
+ `; + + // 使用 requestAnimationFrame 确保DOM完全渲染后再设置事件监听器 + requestAnimationFrame(() => { + setTimeout(() => { + setupHandEventListeners(config.id); + }, 0); + }); + + return div; +} + +// 设置手部事件监听器 +function setupHandEventListeners(handId) { + // 使用更安全的元素获取方式 + const checkbox = document.getElementById(`${handId}_checkbox`); + const interfaceSelect = document.getElementById(`${handId}_interface`); + const handTypeSelect = document.getElementById(`${handId}_handtype`); + + // 检查所有必需的元素是否存在 + if (!checkbox) { + console.error(`setupHandEventListeners: 找不到checkbox - ${handId}_checkbox`); + return; + } + + if (!interfaceSelect) { + console.error(`setupHandEventListeners: 找不到interfaceSelect - ${handId}_interface`); + return; + } + + if (!handTypeSelect) { + console.error(`setupHandEventListeners: 找不到handTypeSelect - ${handId}_handtype`); + return; + } + + // 移除现有的事件监听器(如果有的话) + checkbox.removeEventListener('change', checkbox._changeHandler); + interfaceSelect.removeEventListener('change', interfaceSelect._changeHandler); + handTypeSelect.removeEventListener('change', handTypeSelect._changeHandler); + + // 创建新的事件处理器 + checkbox._changeHandler = function() { + if (handConfigs[handId]) { + handConfigs[handId].enabled = this.checked; + updateHandElement(handId); + updateEnabledHandsStatus(); + logMessage('info', `${handId}: ${this.checked ? '启用' : '禁用'}`); + } + }; + + interfaceSelect._changeHandler = function() { + if (handConfigs[handId]) { + handConfigs[handId].interface = this.value; + logMessage('info', `${handId}: 接口切换到 ${this.value}`); + checkSingleInterfaceStatus(handId); + } + }; + + handTypeSelect._changeHandler = function() { + if (handConfigs[handId]) { + handConfigs[handId].handType = this.value; + updateHandElement(handId); + const handName = this.value === 'left' ? '左手' : '右手'; + const handIdHex = handTypeIds[this.value]; + logMessage('info', `${handId}: 切换到${handName}模式 (0x${handIdHex.toString(16).toUpperCase()})`); + } + }; + + // 添加事件监听器 + checkbox.addEventListener('change', checkbox._changeHandler); + interfaceSelect.addEventListener('change', interfaceSelect._changeHandler); + handTypeSelect.addEventListener('change', handTypeSelect._changeHandler); +} + +// 更新手部元素 +function updateHandElement(handId) { + const config = handConfigs[handId]; + const element = document.getElementById(handId); + + // 添加安全检查 + if (!element || !config) { + console.warn(`updateHandElement: 找不到元素或配置 - handId: ${handId}`); + return; + } + + const handEmoji = config.handType === 'left' ? '✋' : '🤚'; + const handLabel = config.handType === 'left' ? '左手' : '右手'; + const handIdHex = handTypeIds[config.handType]; + + // 更新样式 + element.className = `hand-item ${config.enabled ? 'enabled' : 'disabled'}`; + + // 安全地更新标题 + const title = element.querySelector('.hand-title'); + if (title) { + title.textContent = `${handEmoji} ${config.interface} - ${handLabel}`; + } else { + console.warn(`updateHandElement: 找不到 .hand-title 元素 - handId: ${handId}`); + } + + // 安全地更新手型标签 + const handTypeLabels = element.querySelectorAll('.control-label'); + if (handTypeLabels.length >= 2) { + const handTypeLabel = handTypeLabels[1]; // 第二个label是手型的 + if (handTypeLabel) { + handTypeLabel.textContent = `手型 (CAN ID: 0x${handIdHex.toString(16).toUpperCase()})`; + } + } else { + console.warn(`updateHandElement: 找不到手型标签 - handId: ${handId}`); + } + + // 确保选择框的值也同步更新 + const handTypeSelect = document.getElementById(`${handId}_handtype`); + if (handTypeSelect) { + handTypeSelect.value = config.handType; + } + + const interfaceSelect = document.getElementById(`${handId}_interface`); + if (interfaceSelect) { + interfaceSelect.value = config.interface; + } + + const checkbox = document.getElementById(`${handId}_checkbox`); + if (checkbox) { + checkbox.checked = config.enabled; + } +} + +// 更新启用手部状态显示 +function updateEnabledHandsStatus() { + const enabledHands = Object.values(handConfigs).filter(config => config.enabled); + const statusDiv = document.getElementById('enabled-hands-status'); + + if (enabledHands.length === 0) { + statusDiv.innerHTML = '❌ 没有启用的手部'; + } else { + const statusList = enabledHands.map(config => { + const emoji = config.handType === 'left' ? '✋' : '🤚'; + const handName = config.handType === 'left' ? '左手' : '右手'; + const statusDot = config.status === 'online' ? '🟢' : '🔴'; + return `${statusDot} ${emoji} ${config.interface} (${handName})`; + }).join('
'); + statusDiv.innerHTML = statusList; + } +} + +// 检查所有接口状态 - 修复错误处理 +async function checkAllInterfaceStatus() { + try { + const response = await fetch('/api/status'); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + + if (!data || data.status !== 'success') { + throw new Error(data?.error || '获取状态失败'); + } + + // 安全地获取接口状态 + const responseData = data.data || {}; + interfaceStatus = responseData.interfaces || {}; + + updateAllHandStatus(); + hideConnectionWarning(); + + } catch (error) { + logMessage('error', `状态检查失败: ${error.message}`); + console.error('CheckAllInterfaceStatus Error:', error); + showConnectionWarning(); + setAllHandStatusOffline(); + } +} + +// 检查单个接口状态 +async function checkSingleInterfaceStatus(handId) { + await checkAllInterfaceStatus(); +} + +// 更新所有手部状态 +function updateAllHandStatus() { + Object.keys(handConfigs).forEach(handId => { + const config = handConfigs[handId]; + const status = interfaceStatus[config.interface]; + + if (status && status.active) { + config.status = 'online'; + updateHandStatusDisplay(handId, 'online', '在线'); + } else { + config.status = 'offline'; + updateHandStatusDisplay(handId, 'offline', '离线'); + } + }); + updateEnabledHandsStatus(); +} + +// 设置所有手部状态为离线 +function setAllHandStatusOffline() { + Object.keys(handConfigs).forEach(handId => { + handConfigs[handId].status = 'offline'; + updateHandStatusDisplay(handId, 'offline', '连接失败'); + }); + updateEnabledHandsStatus(); +} + +// 更新手部状态显示 +function updateHandStatusDisplay(handId, status, text) { + const statusDot = document.getElementById(`${handId}_status_dot`); + const statusText = document.getElementById(`${handId}_status_text`); + + if (statusDot && statusText) { + statusDot.className = `status-dot ${status}`; + statusText.textContent = text; + } +} + +// 显示连接警告 +function showConnectionWarning() { + document.getElementById('connection-warning').style.display = 'block'; +} + +// 隐藏连接警告 +function hideConnectionWarning() { + document.getElementById('connection-warning').style.display = 'none'; +} + +// 获取启用的手部配置 +function getEnabledHands() { + return Object.values(handConfigs).filter(config => config.enabled); +} + +// 设置事件监听器 +function setupEventListeners() { + const delayDefault = 30; + + // 刷新所有接口按钮 + document.getElementById('refresh-all').addEventListener('click', function() { + logMessage('info', '手动刷新所有接口...'); + initializeSystem(); + }); + + // 全局控制按钮 + document.getElementById('send-all-finger-poses').addEventListener('click', sendAllFingerPoses); + document.getElementById('send-all-palm-poses').addEventListener('click', sendAllPalmPoses); + document.getElementById('reset-all-hands').addEventListener('click', resetAllHands); + document.getElementById('stop-all-animations').addEventListener('click', stopAllAnimations); + + // 动画按钮 + document.getElementById('start-wave').addEventListener('click', () => startAnimationForAll('wave')); + document.getElementById('start-sway').addEventListener('click', () => startAnimationForAll('sway')); + document.getElementById('stop-animation').addEventListener('click', stopAllAnimations); + + // 预设姿势按钮 - 使用LinkerHandController的预设 + setupPresetButtons(); + + // 数字手势按钮事件 + setupNumericPresets(); + + // Refill core 按钮 + setupRefillCore(); +} + +// 设置预设按钮 +function setupPresetButtons() { + const delayDefault = 30; + + // 基础预设姿势 + const presets = { + 'pose-fist': { finger: 'FIST', palm: null }, + 'pose-open': { finger: 'OPEN', palm: null }, + 'pose-pinch': { finger: 'PINCH', palm: 'PALM_PINCH' }, + 'pose-point': { finger: 'POINT', palm: null }, + 'pose-thumbs-up': { finger: 'THUMBSUP', palm: null }, + 'pose-yeah': { finger: 'YEAH', palm: 'PALM_YEAH' }, + 'pose-wave': { finger: 'WAVE', palm: null }, + 'pose-big-fist': { finger: 'BIG_FIST', palm: 'PALM_BIG_FIST' }, + 'pose-big-open': { finger: 'BIG_OPEN', palm: 'PALM_BIG_OPEN' }, + 'pose-yo': { finger: 'YO', palm: null }, + 'pose-gun': { finger: 'GUN', palm: 'PALM_GUN' }, + 'pose-ok': { finger: 'OK', palm: 'PALM_OK' } + }; + + Object.entries(presets).forEach(([id, preset]) => { + const button = document.getElementById(id); + if (button) { + button.addEventListener('click', () => { + if (preset.palm) { + LinkerHandController.applyPalmPreset(LinkerHandController.PRESETS[preset.palm]); + const palmPose = LinkerHandController.getPalmPoseValues(); + LinkerHandController.sendPalmPoseToAll(palmPose); + + setTimeout(() => { + LinkerHandController.applyFingerPreset(LinkerHandController.PRESETS[preset.finger]); + const fingerPose = LinkerHandController.getFingerPoseValues(); + LinkerHandController.sendFingerPoseToAll(fingerPose); + }, delayDefault); + } else { + LinkerHandController.applyFingerPreset(LinkerHandController.PRESETS[preset.finger]); + const fingerPose = LinkerHandController.getFingerPoseValues(); + LinkerHandController.sendFingerPoseToAll(fingerPose); + } + }); + } + }); +} + +// 设置数字预设 +function setupNumericPresets() { + const delayDefault = 30; + + // 数字1-9的预设 + for (let i = 1; i <= 9; i++) { + const button = document.getElementById(`pose-${i}`); + if (button) { + button.addEventListener('click', () => { + const palmPreset = LinkerHandController.PRESETS[`PALM_${getNumberName(i)}`]; + const fingerPreset = LinkerHandController.PRESETS[getNumberName(i)]; + + if (palmPreset) { + LinkerHandController.applyPalmPreset(palmPreset); + const palmPose = LinkerHandController.getPalmPoseValues(); + LinkerHandController.sendPalmPoseToAll(palmPose); + } + + setTimeout(() => { + if (fingerPreset) { + LinkerHandController.applyFingerPreset(fingerPreset); + const fingerPose = LinkerHandController.getFingerPoseValues(); + LinkerHandController.sendFingerPoseToAll(fingerPose); + } + }, delayDefault); + }); + } + } +} + +// 获取数字名称 +function getNumberName(num) { + const names = ['', 'ONE', 'TWO', 'THREE', 'FOUR', 'FIVE', 'SIX', 'SEVEN', 'EIGHT', 'NINE']; + return names[num] || ''; +} + +// 设置Refill Core功能 +function setupRefillCore() { + document.getElementById("refill-core").addEventListener("click", () => { + event.preventDefault(); + event.stopPropagation(); + + console.log("refill-core"); + + const rukaPoseList = [ + [[246, 188, 128, 128], [149, 30, 145, 36, 41, 46]], // 食指 + [[246, 155, 154, 66], [138, 80, 0, 154, 41, 46]], // 中指 + [[246, 155, 154, 40], [140, 80, 0, 15, 155, 46]], // 无名指 + [[246, 155, 154, 25], [140, 62, 0, 15, 29, 143]], // 小指 + ]; + + const delayTime = 350; // 设定延迟时间为350ms + + // 创建完整的序列:从第一个到最后一个,再从最后一个回到第二个 + const forwardIndices = [...Array(rukaPoseList.length).keys()]; // [0,1,2,3] + const backwardIndices = [...forwardIndices].reverse().slice(1); // [3,2,1] + const sequenceIndices = [...forwardIndices, ...backwardIndices]; + + // 遍历序列索引,为每个索引创建两个操作(palm和finger) + sequenceIndices.forEach((index, step) => { + const targetPose = rukaPoseList[index]; + + // 应用palm预设 + setTimeout(() => { + console.log(`Step ${step+1}a: Applying palm preset for pose ${index+1}`); + LinkerHandController.applyPalmPreset(targetPose[0]); + const palmPose = LinkerHandController.getPalmPoseValues(); + LinkerHandController.sendPalmPoseToAll(palmPose); + }, delayTime * (step * 2)); // 每个完整步骤有两个操作,所以是step*2 + + // 应用finger预设 + setTimeout(() => { + console.log(`Step ${step+1}b: Applying finger preset for pose ${index+1}`); + LinkerHandController.applyFingerPreset(targetPose[1]); + const fingerPose = LinkerHandController.getFingerPoseValues(); + LinkerHandController.sendFingerPoseToAll(fingerPose); + }, delayTime * (step * 2 + 1)); // 偏移一个delayTime + }); + }); +} + +// 设置滑块事件 +function setupSliderEvents() { + // 手指滑块 + for (let i = 0; i < 6; i++) { + const slider = document.getElementById(`finger${i}`); + const valueDisplay = document.getElementById(`finger${i}-value`); + slider.addEventListener('input', function() { + valueDisplay.textContent = this.value; + }); + } + + // 掌部滑块 + for (let i = 0; i < 4; i++) { + const slider = document.getElementById(`palm${i}`); + const valueDisplay = document.getElementById(`palm${i}-value`); + slider.addEventListener('input', function() { + valueDisplay.textContent = this.value; + }); + } + + // 速度滑块 + const speedSlider = document.getElementById('animation-speed'); + const speedDisplay = document.getElementById('speed-value'); + speedSlider.addEventListener('input', function() { + speedDisplay.textContent = this.value; + }); +} + +// 发送所有启用手部的手指姿态 +async function sendAllFingerPoses() { + const enabledHands = getEnabledHands(); + if (enabledHands.length === 0) { + logMessage('error', '没有启用的手部'); + return; + } + + const pose = []; + for (let i = 0; i < 6; i++) { + pose.push(parseInt(document.getElementById(`finger${i}`).value)); + } + + logMessage('info', `发送手指姿态到 ${enabledHands.length} 个启用的手部...`); + + for (const config of enabledHands) { + await sendFingerPoseToHand(config, pose); + } +} + +// 发送所有启用手部的掌部姿态 +async function sendAllPalmPoses() { + const enabledHands = getEnabledHands(); + if (enabledHands.length === 0) { + logMessage('error', '没有启用的手部'); + return; + } + + const pose = []; + for (let i = 0; i < 4; i++) { + pose.push(parseInt(document.getElementById(`palm${i}`).value)); + } + + logMessage('info', `发送掌部姿态到 ${enabledHands.length} 个启用的手部...`); + + for (const config of enabledHands) { + await sendPalmPoseToHand(config, pose); + } +} + +// 发送手指姿态到指定手部 +async function sendFingerPoseToHand(config, pose) { + try { + const response = await fetch('/api/fingers', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + interface: config.interface, + pose: pose, + handType: config.handType, + handId: handTypeIds[config.handType] + }) + }); + + const data = await response.json(); + if (data.status === 'success') { + const handName = config.handType === 'left' ? '左手' : '右手'; + logMessage('success', `${config.interface} (${handName}): 手指姿态发送成功 [${pose.join(', ')}]`); + } else { + logMessage('error', `${config.interface}: ${data.error}`); + } + } catch (error) { + logMessage('error', `${config.interface}: 发送失败 - ${error.message}`); + } +} + +// 发送掌部姿态到指定手部 +async function sendPalmPoseToHand(config, pose) { + try { + const response = await fetch('/api/palm', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + interface: config.interface, + pose: pose, + handType: config.handType, + handId: handTypeIds[config.handType] + }) + }); + + const data = await response.json(); + if (data.status === 'success') { + const handName = config.handType === 'left' ? '左手' : '右手'; + logMessage('success', `${config.interface} (${handName}): 掌部姿态发送成功 [${pose.join(', ')}]`); + } else { + logMessage('error', `${config.interface}: ${data.error}`); + } + } catch (error) { + logMessage('error', `${config.interface}: 发送失败 - ${error.message}`); + } +} + +// 为所有启用手部设置预设姿势 +async function setPresetPoseForAll(preset) { + const enabledHands = getEnabledHands(); + if (enabledHands.length === 0) { + logMessage('error', '没有启用的手部'); + return; + } + + logMessage('info', `设置预设姿势 "${preset}" 到 ${enabledHands.length} 个启用的手部...`); + + for (const config of enabledHands) { + await setPresetPoseToHand(config, preset); + } +} + +// 为指定手部设置预设姿势 +async function setPresetPoseToHand(config, preset) { + try { + const response = await fetch(`/api/preset/${preset}?interface=${config.interface}&handType=${config.handType}`, { + method: 'POST' + }); + + const data = await response.json(); + if (data.status === 'success') { + const handName = config.handType === 'left' ? '左手' : '右手'; + logMessage('success', `${config.interface} (${handName}): ${data.message}`); + } else { + logMessage('error', `${config.interface}: ${data.error}`); + } + } catch (error) { + logMessage('error', `${config.interface}: 预设姿势失败 - ${error.message}`); + } +} + +// 为所有启用手部启动动画 +async function startAnimationForAll(type) { + const enabledHands = getEnabledHands(); + if (enabledHands.length === 0) { + logMessage('error', '没有启用的手部'); + return; + } + + const speed = parseInt(document.getElementById('animation-speed').value); + logMessage('info', `启动 "${type}" 动画到 ${enabledHands.length} 个启用的手部...`); + + for (const config of enabledHands) { + await startAnimationForHand(config, type, speed); + } +} + +// 为指定手部启动动画 +async function startAnimationForHand(config, type, speed) { + try { + const response = await fetch('/api/animation', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + interface: config.interface, + type: type, + speed: speed, + handType: config.handType, + handId: handTypeIds[config.handType] + }) + }); + + const data = await response.json(); + if (data.status === 'success') { + const handName = config.handType === 'left' ? '左手' : '右手'; + logMessage('success', `${config.interface} (${handName}): ${data.message}`); + } else { + logMessage('error', `${config.interface}: ${data.error}`); + } + } catch (error) { + logMessage('error', `${config.interface}: 动画启动失败 - ${error.message}`); + } +} + +// 停止所有启用手部的动画 +async function stopAllAnimations() { + const enabledHands = getEnabledHands(); + if (enabledHands.length === 0) { + logMessage('error', '没有启用的手部'); + return; + } + + logMessage('info', `停止 ${enabledHands.length} 个启用手部的动画...`); + + for (const config of enabledHands) { + await stopAnimationForHand(config); + } +} + +// 停止指定手部的动画 +async function stopAnimationForHand(config) { + try { + const response = await fetch('/api/animation', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + interface: config.interface, + type: 'stop', + handType: config.handType, + handId: handTypeIds[config.handType] + }) + }); + + const data = await response.json(); + if (data.status === 'success') { + const handName = config.handType === 'left' ? '左手' : '右手'; + logMessage('success', `${config.interface} (${handName}): ${data.message}`); + } else { + logMessage('error', `${config.interface}: ${data.error}`); + } + } catch (error) { + logMessage('error', `${config.interface}: 停止动画失败 - ${error.message}`); + } +} + +// 重置所有启用手部 +async function resetAllHands() { + const enabledHands = getEnabledHands(); + if (enabledHands.length === 0) { + logMessage('error', '没有启用的手部'); + return; + } + + // 重置滑块值 + LinkerHandController.applyFingerPreset(LinkerHandController.PRESETS.OPEN); + LinkerHandController.applyPalmPreset(LinkerHandController.PRESETS.PALM_NEUTRAL); + + logMessage('info', `重置 ${enabledHands.length} 个启用的手部...`); + + // 停止所有动画 + await stopAllAnimations(); + + // 发送重置姿态 + await sendAllFingerPoses(); + await sendAllPalmPoses(); + + logMessage('info', '所有启用手部已重置完成'); +} + +// 自动触发按钮序列(数字手势) +async function triggerButtonsSequentially(interval = 2000) { + const enabledHands = getEnabledHands(); + if (enabledHands.length === 0) { + logMessage('error', '没有启用的手部'); + return; + } + + logMessage('info', `开始自动数字手势序列 (${enabledHands.length} 个手部)`); + + const buttons = [ + document.getElementById('pose-1'), + document.getElementById('pose-2'), + document.getElementById('pose-3'), + document.getElementById('pose-4'), + document.getElementById('pose-5'), + document.getElementById('pose-6'), + document.getElementById('pose-7'), + document.getElementById('pose-8'), + document.getElementById('pose-9'), + ]; + + for (const button of buttons) { + if (button) { + button.click(); + await new Promise(resolve => setTimeout(resolve, interval)); + } + } + + // 然后执行所有预设手势 + const presetButtons = document.querySelectorAll('.preset-grid button:not(.preset-num-pose)'); + for (const button of presetButtons) { + button.click(); + await new Promise(resolve => setTimeout(resolve, interval)); + } + + logMessage('success', '数字手势序列完成'); +} + +// 日志消息 +function logMessage(type, message) { + const statusLog = document.getElementById('status-log'); + const timestamp = new Date().toLocaleTimeString(); + + const logEntry = document.createElement('div'); + logEntry.className = 'log-entry'; + + let statusClass = 'status-info'; + if (type === 'success') statusClass = 'status-success'; + else if (type === 'error') statusClass = 'status-error'; + + logEntry.innerHTML = ` + + ${timestamp} + ${message} + `; + + statusLog.appendChild(logEntry); + statusLog.scrollTop = statusLog.scrollHeight; + + // 保持最多50条日志 + const entries = statusLog.querySelectorAll('.log-entry'); + if (entries.length > 50) { + statusLog.removeChild(entries[0]); + } +} + +// 启动状态更新器 +function startStatusUpdater() { + // 每5秒检查一次接口状态 + setInterval(async () => { + await checkAllInterfaceStatus(); + }, 5000); + + // 每30秒刷新一次接口列表 + setInterval(async () => { + const oldInterfaces = [...availableInterfaces]; + await loadAvailableInterfaces(); + + // 如果接口发生变化,重新生成配置 + if (JSON.stringify(oldInterfaces) !== JSON.stringify(availableInterfaces)) { + generateHandConfigs(); + } + }, 30000); +} + +// 添加调试功能 +async function debugSystemStatus() { + logMessage('info', '🔍 开始系统调试...'); + + // 检查HTML元素 + const elements = { + 'hands-grid': document.getElementById('hands-grid'), + 'status-log': document.getElementById('status-log'), + 'enabled-hands-status': document.getElementById('enabled-hands-status'), + 'sensor-data': document.getElementById('sensor-data') + }; + + Object.entries(elements).forEach(([name, element]) => { + if (element) { + logMessage('success', `✅ 元素 ${name} 存在`); + } else { + logMessage('error', `❌ 元素 ${name} 不存在`); + } + }); + + // 检查全局变量 + logMessage('info', `可用接口: [${availableInterfaces.join(', ')}]`); + logMessage('info', `手部配置数量: ${Object.keys(handConfigs).length}`); + logMessage('info', `启用手部数量: ${getEnabledHands().length}`); + + // 测试API连通性 + try { + logMessage('info', '测试 /api/health 连接...'); + const response = await fetch('/api/health'); + if (response.ok) { + const data = await response.json(); + logMessage('success', '✅ 健康检查通过'); + console.log('Health Check Data:', data); + } else { + logMessage('error', `❌ 健康检查失败: HTTP ${response.status}`); + } + } catch (error) { + logMessage('error', `❌ 健康检查异常: ${error.message}`); + } + + // 测试接口API + try { + logMessage('info', '测试 /api/interfaces 连接...'); + const response = await fetch('/api/interfaces'); + if (response.ok) { + const data = await response.json(); + logMessage('success', '✅ 接口API通过'); + console.log('Interfaces API Data:', data); + } else { + logMessage('error', `❌ 接口API失败: HTTP ${response.status}`); + } + } catch (error) { + logMessage('error', `❌ 接口API异常: ${error.message}`); + } +} + +// 导出全局函数供HTML按钮使用 +window.triggerButtonsSequentially = triggerButtonsSequentially; +window.debugSystemStatus = debugSystemStatus; + +// 添加全局错误处理 +window.addEventListener('error', function(event) { + logMessage('error', `全局错误: ${event.error?.message || event.message}`); + console.error('Global Error:', event.error); +}); + +window.addEventListener('unhandledrejection', function(event) { + logMessage('error', `未处理的Promise拒绝: ${event.reason?.message || event.reason}`); + console.error('Unhandled Promise Rejection:', event.reason); +}); + +// 页面可见性变化时的处理 +document.addEventListener('visibilitychange', function() { + if (!document.hidden) { + // 页面变为可见时,刷新状态 + checkAllInterfaceStatus(); + } +}); + +// 处理网络错误时的重连逻辑 +window.addEventListener('online', function() { + logMessage('info', '网络连接已恢复,正在重新连接...'); + initializeSystem(); +}); + +window.addEventListener('offline', function() { + logMessage('error', '网络连接已断开'); + showConnectionWarning(); +}); + +// 键盘快捷键支持 +document.addEventListener('keydown', function(e) { + // Ctrl+R 刷新接口 + if (e.ctrlKey && e.key === 'r') { + e.preventDefault(); + logMessage('info', '快捷键刷新接口列表...'); + initializeSystem(); + } + + // Ctrl+Space 停止所有动画 + if (e.ctrlKey && e.code === 'Space') { + e.preventDefault(); + stopAllAnimations(); + } + + // Ctrl+A 选择/取消选择所有手部 + if (e.ctrlKey && e.key === 'a') { + e.preventDefault(); + toggleAllHands(); + } + + // 数字键1-9快速设置预设姿势 + if (e.key >= '1' && e.key <= '9' && !e.ctrlKey && !e.altKey) { + const activeElement = document.activeElement; + // 确保不在输入框中 + if (activeElement.tagName !== 'INPUT' && activeElement.tagName !== 'SELECT') { + const button = document.getElementById(`pose-${e.key}`); + if (button) button.click(); + } + } +}); + +// 切换所有手部启用状态 +function toggleAllHands() { + const enabledCount = Object.values(handConfigs).filter(config => config.enabled).length; + const shouldEnable = enabledCount === 0; + + Object.keys(handConfigs).forEach(handId => { + handConfigs[handId].enabled = shouldEnable; + const checkbox = document.getElementById(`${handId}_checkbox`); + if (checkbox) { + checkbox.checked = shouldEnable; + } + updateHandElement(handId); + }); + + updateEnabledHandsStatus(); + logMessage('info', `${shouldEnable ? '启用' : '禁用'}所有手部`); +} + +// 工具提示功能 +function addTooltips() { + const tooltips = { + 'refresh-all': '刷新所有可用接口列表', + 'send-all-finger-poses': '向所有启用的手部发送当前手指关节位置', + 'send-all-palm-poses': '向所有启用的手部发送当前掌部关节位置', + 'reset-all-hands': '重置所有启用手部到默认位置', + 'stop-all-animations': '停止所有启用手部的动画', + 'start-wave': '启动所有启用手部的手指波浪动画', + 'start-sway': '启动所有启用手部的掌部摆动动画', + 'stop-animation': '停止所有启用手部的动画', + 'refill-core': '执行Refill Core动作序列' + }; + + Object.entries(tooltips).forEach(([id, text]) => { + const element = document.getElementById(id); + if (element) { + element.title = text; + } + }); +} + +// 页面加载完成后添加工具提示 +document.addEventListener('DOMContentLoaded', function() { + addTooltips(); +}); + + +// ---eof + +// 六手依次动画函数 +async function startSequentialHandAnimation(animationType = 'wave', interval = 500, cycles = 3) { + const enabledHands = getEnabledHands(); + + // 检查是否有足够的手部 + if (enabledHands.length === 0) { + logMessage('error', '没有启用的手部'); + return; + } + + // 确保按接口名称排序(can0, can1, can2...) + const sortedHands = enabledHands.sort((a, b) => { + const getInterfaceNumber = (iface) => { + const match = iface.match(/(\d+)$/); + return match ? parseInt(match[1]) : 0; + }; + return getInterfaceNumber(a.interface) - getInterfaceNumber(b.interface); + }); + + logMessage('info', `开始六手依次动画 - 类型: ${animationType}, 间隔: ${interval}ms, 循环: ${cycles}次`); + logMessage('info', `动画顺序: ${sortedHands.map(h => h.interface).join(' → ')}`); + + // 定义动画预设 + const animationPresets = { + wave: { + name: '手指波浪', + fingerPoses: [ + [255, 255, 255, 255, 255, 255], // 完全张开 + [128, 128, 128, 128, 128, 128], // 中间位置 + [64, 64, 64, 64, 64, 64], // 握拳 + [128, 128, 128, 128, 128, 128], // 回到中间 + ], + palmPose: [128, 128, 128, 128] // 掌部保持中立 + }, + + thumbsUp: { + name: '竖拇指传递', + fingerPoses: [ + [255, 255, 0, 0, 0, 0], // 竖拇指 + [128, 128, 128, 128, 128, 128], // 恢复中立 + ], + palmPose: [128, 128, 128, 128] + }, + + point: { + name: '食指指点传递', + fingerPoses: [ + [0, 0, 255, 0, 0, 0], // 食指指点 + [128, 128, 128, 128, 128, 128], // 恢复中立 + ], + palmPose: [128, 128, 128, 128] + }, + + fistOpen: { + name: '握拳张开', + fingerPoses: [ + [64, 64, 64, 64, 64, 64], // 握拳 + [255, 255, 255, 255, 255, 255], // 张开 + [128, 128, 128, 128, 128, 128], // 中立 + ], + palmPose: [128, 128, 128, 128] + }, + + numbers: { + name: '数字倒计时', + fingerPoses: [ + [255, 255, 255, 255, 255, 255], // 5 + [0, 57, 255, 255, 255, 255], // 4 + [0, 57, 255, 255, 255, 0], // 3 + [0, 57, 255, 255, 0, 0], // 2 + [0, 57, 255, 0, 0, 0], // 1 + [64, 64, 64, 64, 64, 64], // 握拳 (0) + ], + palmPoses: [ + [255, 109, 255, 118], // 5对应的掌部 + [255, 109, 255, 118], // 4对应的掌部 + [255, 109, 255, 118], // 3对应的掌部 + [255, 109, 255, 118], // 2对应的掌部 + [255, 109, 255, 118], // 1对应的掌部 + [128, 128, 128, 128], // 0对应的掌部 + ] + }, + + mexican: { + name: '墨西哥波浪', + fingerPoses: [ + [64, 64, 64, 64, 64, 64], // 起始握拳 + [128, 64, 64, 64, 64, 64], // 拇指起 + [255, 128, 64, 64, 64, 64], // 拇指+食指起 + [255, 255, 128, 64, 64, 64], // 前三指起 + [255, 255, 255, 128, 64, 64], // 前四指起 + [255, 255, 255, 255, 128, 64], // 前五指起 + [255, 255, 255, 255, 255, 255], // 全部张开 + [128, 255, 255, 255, 255, 128], // 波浪形 + [64, 128, 255, 255, 128, 64], // 继续波浪 + [64, 64, 128, 255, 128, 64], // 波浪收尾 + [64, 64, 64, 128, 64, 64], // 几乎回到握拳 + [64, 64, 64, 64, 64, 64], // 完全握拳 + ], + palmPose: [128, 128, 128, 128] + } + }; + + const preset = animationPresets[animationType] || animationPresets.wave; + const fingerPoses = preset.fingerPoses; + const palmPoses = preset.palmPoses || Array(fingerPoses.length).fill(preset.palmPose); + + // 执行动画循环 + for (let cycle = 0; cycle < cycles; cycle++) { + logMessage('info', `${preset.name} - 第 ${cycle + 1}/${cycles} 轮`); + + // 每个动作姿势 + for (let poseIndex = 0; poseIndex < fingerPoses.length; poseIndex++) { + const currentFingerPose = fingerPoses[poseIndex]; + const currentPalmPose = palmPoses[poseIndex]; + + // 依次激活每只手 + for (let handIndex = 0; handIndex < sortedHands.length; handIndex++) { + const hand = sortedHands[handIndex]; + const handName = hand.handType === 'left' ? '左手' : '右手'; + + // 先发送掌部姿态 + await sendPalmPoseToHand(hand, currentPalmPose); + + // 短暂延迟后发送手指姿态 + setTimeout(async () => { + await sendFingerPoseToHand(hand, currentFingerPose); + }, 50); + + logMessage('info', `${hand.interface}(${handName}) 执行姿势 ${poseIndex + 1}/${fingerPoses.length}`); + + // 等待间隔时间再激活下一只手 + await new Promise(resolve => setTimeout(resolve, interval)); + } + } + + // 循环间隔(如果有多轮) + if (cycle < cycles - 1) { + logMessage('info', `等待下一轮动画...`); + await new Promise(resolve => setTimeout(resolve, interval * 2)); + } + } + + // 动画结束后,让所有手回到中立位置 + logMessage('info', '动画完成,恢复中立位置...'); + const neutralFingerPose = [128, 128, 128, 128, 128, 128]; + const neutralPalmPose = [128, 128, 128, 128]; + + for (const hand of sortedHands) { + await sendPalmPoseToHand(hand, neutralPalmPose); + setTimeout(async () => { + await sendFingerPoseToHand(hand, neutralFingerPose); + }, 50); + await new Promise(resolve => setTimeout(resolve, 100)); + } + + logMessage('success', `六手依次动画 "${preset.name}" 完成!`); +} + +// 扩展的动画控制函数 +async function startCustomSequentialAnimation(config) { + const { + animationType = 'wave', + interval = 500, + cycles = 3, + direction = 'forward', // 'forward', 'backward', 'bounce' + simultaneousHands = 1, // 同时激活的手数 + staggerDelay = 100 // 同时激活手之间的错开延迟 + } = config; + + const enabledHands = getEnabledHands(); + + if (enabledHands.length === 0) { + logMessage('error', '没有启用的手部'); + return; + } + + // 根据方向排序手部 + let sortedHands = enabledHands.sort((a, b) => { + const getInterfaceNumber = (iface) => { + const match = iface.match(/(\d+)$/); + return match ? parseInt(match[1]) : 0; + }; + return getInterfaceNumber(a.interface) - getInterfaceNumber(b.interface); + }); + + if (direction === 'backward') { + sortedHands = sortedHands.reverse(); + } + + logMessage('info', `开始自定义六手动画 - 方向: ${direction}, 同时手数: ${simultaneousHands}`); + + // 执行动画逻辑... + // 这里可以根据simultaneousHands参数同时控制多只手 + // 实现类似的动画逻辑,但支持更多自定义选项 +} + +// 预定义的快捷动画函数 +async function startWaveAnimation() { + await startSequentialHandAnimation('wave', 300, 2); +} + +async function startThumbsUpRelay() { + await startSequentialHandAnimation('thumbsUp', 400, 3); +} + +async function startPointingRelay() { + await startSequentialHandAnimation('point', 350, 2); +} + +async function startNumberCountdown() { + await startSequentialHandAnimation('numbers', 800, 1); +} + +async function startMexicanWave() { + await startSequentialHandAnimation('mexican', 200, 3); +} + +async function startFistOpenWave() { + await startSequentialHandAnimation('fistOpen', 400, 2); +} + +// 高级组合动画:先正向再反向 +async function startBidirectionalWave() { + logMessage('info', '开始双向波浪动画...'); + + // 正向波浪 + await startSequentialHandAnimation('wave', 300, 1); + await new Promise(resolve => setTimeout(resolve, 500)); + + // 反向波浪(通过反转手部顺序实现) + const originalGetEnabledHands = window.getEnabledHands; + window.getEnabledHands = function() { + return originalGetEnabledHands().reverse(); + }; + + await startSequentialHandAnimation('wave', 300, 1); + + // 恢复原始函数 + window.getEnabledHands = originalGetEnabledHands; + + logMessage('success', '双向波浪动画完成!'); +} + +// 导出函数到全局作用域 +window.startSequentialHandAnimation = startSequentialHandAnimation; +window.startCustomSequentialAnimation = startCustomSequentialAnimation; +window.startWaveAnimation = startWaveAnimation; +window.startThumbsUpRelay = startThumbsUpRelay; +window.startPointingRelay = startPointingRelay; +window.startNumberCountdown = startNumberCountdown; +window.startMexicanWave = startMexicanWave; +window.startFistOpenWave = startFistOpenWave; +window.startBidirectionalWave = startBidirectionalWave; + diff --git a/static/styles.css b/static/styles.css new file mode 100644 index 0000000..af4ca22 --- /dev/null +++ b/static/styles.css @@ -0,0 +1,355 @@ +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + padding: 20px; + max-width: 1800px; + margin: 0 auto; + background-color: #f5f5f7; + color: #333; +} + +.container { + display: flex; + flex-wrap: wrap; + gap: 20px; +} + +.control-panel, .status-panel, .animation-panel { + background: white; + padding: 20px; + border-radius: 10px; + box-shadow: 0 2px 10px rgba(0,0,0,0.05); + flex: 1; + min-width: 300px; +} + +h1, h2, h3 { + color: #2c3e50; +} + +/* 手部配置面板 */ +.hands-config { + background-color: #e8f4fd; + padding: 20px; + border-radius: 10px; + margin-bottom: 20px; + border-left: 4px solid #3498db; +} + +.hands-config h3 { + margin-top: 0; + margin-bottom: 15px; + color: #2980b9; +} + +.hands-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 15px; +} + +.hand-item { + background: white; + padding: 15px; + border-radius: 8px; + border: 2px solid transparent; + transition: all 0.3s ease; +} + +.hand-item.enabled { + border-color: #27ae60; + box-shadow: 0 2px 8px rgba(39, 174, 96, 0.2); +} + +.hand-item.disabled { + opacity: 0.6; + background-color: #f8f9fa; +} + +.hand-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 10px; +} + +.hand-checkbox { + width: 18px; + height: 18px; + accent-color: #27ae60; +} + +.hand-title { + font-weight: bold; + font-size: 16px; +} + +.hand-controls { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; + margin-top: 10px; +} + +.control-group { + display: flex; + flex-direction: column; + gap: 5px; +} + +.control-label { + font-size: 12px; + color: #666; + font-weight: 500; +} + +.hand-select { + padding: 6px 8px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; +} + +.hand-status { + display: flex; + align-items: center; + gap: 5px; + margin-top: 8px; + font-size: 12px; +} + +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + display: inline-block; +} + +.status-dot.online { + background-color: #27ae60; +} + +.status-dot.offline { + background-color: #e74c3c; +} + +.status-dot.loading { + background-color: #f39c12; + animation: pulse 1s infinite; +} + +@keyframes pulse { + 0% { opacity: 1; } + 50% { opacity: 0.5; } + 100% { opacity: 1; } +} + +/* 控制按钮区域 */ +.global-controls { + background-color: #fff3cd; + padding: 15px; + border-radius: 8px; + margin-bottom: 20px; + border-left: 4px solid #ffc107; +} + +.global-controls h4 { + margin: 0 0 10px 0; + color: #856404; +} + +.control-buttons-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 10px; +} + +.slider-container { + margin: 15px 0; +} + +.slider-label { + display: flex; + justify-content: space-between; + margin-bottom: 5px; +} + +.finger-slider { + width: 100%; +} + +.value-display { + width: 40px; + text-align: center; + font-weight: bold; +} + +.control-buttons { + display: flex; + gap: 10px; + margin-top: 20px; + flex-wrap: wrap; +} + +button { + padding: 10px 15px; + background-color: #3498db; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s; +} + +button:hover { + background-color: #2980b9; +} + +button:disabled { + background-color: #bdc3c7; + cursor: not-allowed; +} + +button.preset { + background-color: #27ae60; +} + +button.preset:hover { + background-color: #219955; +} + +button.stop { + background-color: #e74c3c; +} + +button.stop:hover { + background-color: #c0392b; +} + +button.global { + background-color: #f39c12; +} + +button.global:hover { + background-color: #e67e22; +} + +.refresh-button { + background-color: #9b59b6; + padding: 6px 10px; + font-size: 12px; +} + +.refresh-button:hover { + background-color: #8e44ad; +} + +.status-output { + background-color: #f8f9fa; + border: 1px solid #eee; + border-radius: 5px; + padding: 10px; + font-family: monospace; + height: 300px; + overflow-y: auto; +} + +.log-entry { + margin-bottom: 5px; + padding: 5px; + border-bottom: 1px solid #eee; +} + +.log-entry:last-child { + border-bottom: none; +} + +.status-indicator { + display: inline-block; + width: 10px; + height: 10px; + border-radius: 50%; + margin-right: 5px; +} + +.log-timestamp { + color: #7f8c8d; + font-size: 0.8em; + margin-right: 5px; +} + +.status-success { + background-color: #2ecc71; +} + +.status-error { + background-color: #e74c3c; +} + +.status-info { + background-color: #3498db; +} + +.animation-controls { + display: flex; + flex-direction: column; + gap: 10px; +} + +.preset-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: 10px; + margin-top: 10px; +} + +.slider-group { + background-color: #f8f9fa; + padding: 15px; + border-radius: 8px; + margin-bottom: 10px; +} + +.slider-group h3 { + margin-top: 0; + font-size: 1.1em; +} + +.info-badge { + display: inline-block; + font-size: 0.7em; + padding: 2px 5px; + background-color: #95a5a6; + color: white; + border-radius: 3px; + margin-left: 5px; + vertical-align: middle; +} + +.connection-warning { + background-color: #fff3cd; + border: 1px solid #ffeeba; + color: #856404; + padding: 10px; + border-radius: 5px; + margin-bottom: 15px; + display: none; +} + +/* 响应式调整 */ +@media (max-width: 1200px) { + .container { + flex-direction: column; + } + .hands-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 768px) { + .hand-controls { + grid-template-columns: 1fr; + } + .control-buttons-grid { + grid-template-columns: 1fr; + } +} \ No newline at end of file