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
+
+
+
+
+
+
+
+
+
+
+
+
预设动作
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
NUM Pose
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
动画控制
+
+
+
+ 动作速度 (毫秒)
+ 500
+
+
+
+
+
+
+
+
+
+
+
+
+
状态监控
+
+
+
+ 00:00:00
+ 正在初始化系统...
+
+
+
+
+
+
+
+
+
+
+
+
\ 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