diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b2dfe19 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "weekly" \ No newline at end of file diff --git a/.github/logo.png b/.github/logo.png new file mode 100644 index 0000000..136cb4c Binary files /dev/null and b/.github/logo.png differ diff --git a/.github/workflows/CodeQL.yaml b/.github/workflows/CodeQL.yaml new file mode 100644 index 0000000..3bd3ea4 --- /dev/null +++ b/.github/workflows/CodeQL.yaml @@ -0,0 +1,39 @@ +name: "CodeQL" + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + schedule: + - cron: "0 0 * * *" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: ["go"] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..f4f4f0e --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,55 @@ +name: Release Dashboard Server + +on: + workflow_dispatch: + push: + branches: + - "main" + tags: + - "*.*.*" + +env: + GO_VERSION: "1.24" + GO111MODULE: on + +permissions: + contents: write + id-token: write + packages: write + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6 + if: success() && startsWith(github.ref, 'refs/tags/') + with: + version: "~> v2" + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_USERNAME: ${{ github.repository_owner }} diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..46acb6e --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,115 @@ +version: 2 + +project_name: dashboard-server + +before: + hooks: + - go mod tidy + +builds: + - id: dashboard-server + goos: + - linux + goarch: + - amd64 + - arm64 + - arm + goarm: + - "6" + - "7" + ldflags: + - -s -w + +archives: + - formats: ["tar.gz"] + files: + - static + - README.md + - README_CN.md + - LICENSE + +release: + draft: false + +dockers: + - image_templates: + - "ghcr.io/{{ .Env.GITHUB_USERNAME }}/dashboard-server:{{ .Tag }}-amd64" + dockerfile: Dockerfile.goreleaser + use: buildx + build_flag_templates: + - "--platform=linux/amd64" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + goos: linux + goarch: amd64 + extra_files: + - static + + - image_templates: + - "ghcr.io/{{ .Env.GITHUB_USERNAME }}/dashboard-server:{{ .Tag }}-arm64" + dockerfile: Dockerfile.goreleaser + use: buildx + build_flag_templates: + - "--platform=linux/arm64" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + goos: linux + goarch: arm64 + extra_files: + - static + + - image_templates: + - "ghcr.io/{{ .Env.GITHUB_USERNAME }}/dashboard-server:{{ .Tag }}-armv6" + dockerfile: Dockerfile.goreleaser + use: buildx + build_flag_templates: + - "--platform=linux/arm/v6" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + goos: linux + goarch: arm + goarm: "6" + extra_files: + - static + + - image_templates: + - "ghcr.io/{{ .Env.GITHUB_USERNAME }}/dashboard-server:{{ .Tag }}-armv7" + dockerfile: Dockerfile.goreleaser + use: buildx + build_flag_templates: + - "--platform=linux/arm/v7" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + goos: linux + goarch: arm + goarm: "7" + extra_files: + - static + +docker_manifests: + - name_template: "ghcr.io/{{ .Env.GITHUB_USERNAME }}/dashboard-server:{{ .Tag }}" + image_templates: + - "ghcr.io/{{ .Env.GITHUB_USERNAME }}/dashboard-server:{{ .Tag }}-amd64" + - "ghcr.io/{{ .Env.GITHUB_USERNAME }}/dashboard-server:{{ .Tag }}-arm64" + - "ghcr.io/{{ .Env.GITHUB_USERNAME }}/dashboard-server:{{ .Tag }}-armv6" + - "ghcr.io/{{ .Env.GITHUB_USERNAME }}/dashboard-server:{{ .Tag }}-armv7" + - name_template: "ghcr.io/{{ .Env.GITHUB_USERNAME }}/dashboard-server:latest" + image_templates: + - "ghcr.io/{{ .Env.GITHUB_USERNAME }}/dashboard-server:{{ .Tag }}-amd64" + - "ghcr.io/{{ .Env.GITHUB_USERNAME }}/dashboard-server:{{ .Tag }}-arm64" + - "ghcr.io/{{ .Env.GITHUB_USERNAME }}/dashboard-server:{{ .Tag }}-armv6" + - "ghcr.io/{{ .Env.GITHUB_USERNAME }}/dashboard-server:{{ .Tag }}-armv7" + - name_template: "ghcr.io/{{ .Env.GITHUB_USERNAME }}/dashboard-server:v{{ .Major }}" + image_templates: + - "ghcr.io/{{ .Env.GITHUB_USERNAME }}/dashboard-server:{{ .Tag }}-amd64" + - "ghcr.io/{{ .Env.GITHUB_USERNAME }}/dashboard-server:{{ .Tag }}-arm64" + - "ghcr.io/{{ .Env.GITHUB_USERNAME }}/dashboard-server:{{ .Tag }}-armv6" + - "ghcr.io/{{ .Env.GITHUB_USERNAME }}/dashboard-server:{{ .Tag }}-armv7" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9537283 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +# ---- Build Stage ---- +FROM golang:1.24-alpine AS builder + +WORKDIR /app + +COPY --link go.mod go.sum ./ +RUN go mod download + +COPY --link . . + +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o dashboard-server . + +# ---- Runtime Stage ---- +FROM alpine:3.21 + +WORKDIR /app + +COPY --link static/ ./static/ + +COPY --link --from=builder /app/dashboard-server /usr/local/bin/dashboard-server + +EXPOSE 9099 + +ENV SERVER_PORT="9099" + +CMD ["dashboard-server"] \ No newline at end of file diff --git a/Dockerfile.goreleaser b/Dockerfile.goreleaser new file mode 100644 index 0000000..fcf31a5 --- /dev/null +++ b/Dockerfile.goreleaser @@ -0,0 +1,10 @@ +FROM alpine:3.21 + +COPY --link dashboard-server /usr/local/bin/dashboard-server +COPY --link static/ ./static/ + +EXPOSE 9099 + +ENV SERVER_PORT="9099" + +ENTRYPOINT ["dashboard-server"] diff --git a/README_CN.md b/README_CN.md index 53235a1..ecf30d5 100644 --- a/README_CN.md +++ b/README_CN.md @@ -8,7 +8,7 @@ * **动态手型配置**:支持左手和右手手型的动态切换。 * **灵活接口配置**:支持多种 CAN 接口(如 `can0`, `can1`),可通过命令行参数或环境变量动态设置。 -* **手指与掌部姿态控制**:提供手指(6字节)和掌部(4字节)姿态数据发送功能。 +* **手指与掌部姿态控制**:提供手指(6 字节)和掌部(4 字节)姿态数据发送功能。 * **预设动作执行**:内置丰富的手势动作,如握拳、张开、捏取、点赞、数字手势等。 * **实时动画控制**:支持波浪、横向摆动等动画效果,用户可动态启动和停止。 * **传感器数据实时监控**:提供接口压力数据的实时模拟和更新。 diff --git a/api/handler.go b/api/handler.go new file mode 100644 index 0000000..3ea1d2e --- /dev/null +++ b/api/handler.go @@ -0,0 +1,468 @@ +package api + +import ( + "fmt" + "hands/config" + "hands/define" + "hands/hands" + "net/http" + "time" + + "github.com/gin-gonic/gin" +) + +// 手型设置处理函数 +func HandleHandType(c *gin.Context) { + var req HandTypeRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, define.ApiResponse{ + Status: "error", + Error: "无效的手型设置请求:" + err.Error(), + }) + return + } + + // 验证接口 + if !config.IsValidInterface(req.Interface) { + c.JSON(http.StatusBadRequest, define.ApiResponse{ + Status: "error", + Error: fmt.Sprintf("无效的接口 %s,可用接口: %v", req.Interface, config.Config.AvailableInterfaces), + }) + return + } + + // 验证手型 ID + if req.HandType == "left" && req.HandId != define.HAND_TYPE_LEFT { + req.HandId = define.HAND_TYPE_LEFT + } else if req.HandType == "right" && req.HandId != define.HAND_TYPE_RIGHT { + req.HandId = define.HAND_TYPE_RIGHT + } + + // 设置手型配置 + hands.SetHandConfig(req.Interface, req.HandType, req.HandId) + + handTypeName := "右手" + if req.HandType == "left" { + handTypeName = "左手" + } + + c.JSON(http.StatusOK, define.ApiResponse{ + Status: "success", + Message: fmt.Sprintf("接口 %s 手型已设置为%s (0x%X)", req.Interface, handTypeName, req.HandId), + Data: map[string]any{ + "interface": req.Interface, + "handType": req.HandType, + "handId": req.HandId, + }, + }) +} + +// 手指姿态处理函数 +func HandleFingers(c *gin.Context) { + var req FingerPoseRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, define.ApiResponse{ + Status: "error", + Error: "无效的手指姿态数据:" + err.Error(), + }) + return + } + + // 验证每个值是否在范围内 + for _, v := range req.Pose { + if v < 0 || v > 255 { + c.JSON(http.StatusBadRequest, define.ApiResponse{ + Status: "error", + Error: "手指姿态值必须在 0-255 范围内", + }) + return + } + } + + // 如果未指定接口,使用默认接口 + if req.Interface == "" { + req.Interface = config.Config.DefaultInterface + } + + // 验证接口 + if !config.IsValidInterface(req.Interface) { + c.JSON(http.StatusBadRequest, define.ApiResponse{ + Status: "error", + Error: fmt.Sprintf("无效的接口 %s,可用接口: %v", req.Interface, config.Config.AvailableInterfaces), + }) + return + } + + hands.StopAllAnimations(req.Interface) + + if err := hands.SendFingerPose(req.Interface, req.Pose, req.HandType, req.HandId); err != nil { + c.JSON(http.StatusInternalServerError, define.ApiResponse{ + Status: "error", + Error: "发送手指姿态失败:" + err.Error(), + }) + return + } + + c.JSON(http.StatusOK, define.ApiResponse{ + Status: "success", + Message: "手指姿态指令发送成功", + Data: map[string]any{"interface": req.Interface, "pose": req.Pose}, + }) +} + +// 掌部姿态处理函数 +func HandlePalm(c *gin.Context) { + var req PalmPoseRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, define.ApiResponse{ + Status: "error", + Error: "无效的掌部姿态数据:" + err.Error(), + }) + return + } + + // 验证每个值是否在范围内 + for _, v := range req.Pose { + if v < 0 || v > 255 { + c.JSON(http.StatusBadRequest, define.ApiResponse{ + Status: "error", + Error: "掌部姿态值必须在 0-255 范围内", + }) + return + } + } + + // 如果未指定接口,使用默认接口 + if req.Interface == "" { + req.Interface = config.Config.DefaultInterface + } + + // 验证接口 + if !config.IsValidInterface(req.Interface) { + c.JSON(http.StatusBadRequest, define.ApiResponse{ + Status: "error", + Error: fmt.Sprintf("无效的接口 %s,可用接口: %v", req.Interface, config.Config.AvailableInterfaces), + }) + return + } + + hands.StopAllAnimations(req.Interface) + + if err := hands.SendPalmPose(req.Interface, req.Pose, req.HandType, req.HandId); err != nil { + c.JSON(http.StatusInternalServerError, define.ApiResponse{ + Status: "error", + Error: "发送掌部姿态失败:" + err.Error(), + }) + return + } + + c.JSON(http.StatusOK, define.ApiResponse{ + Status: "success", + Message: "掌部姿态指令发送成功", + Data: map[string]any{"interface": req.Interface, "pose": req.Pose}, + }) +} + +// 预设姿势处理函数 +func HandlePreset(c *gin.Context) { + pose := c.Param("pose") + + // 从查询参数获取接口名称和手型 + ifName := c.Query("interface") + handType := c.Query("handType") + + if ifName == "" { + ifName = config.Config.DefaultInterface + } + + // 验证接口 + if !config.IsValidInterface(ifName) { + c.JSON(http.StatusBadRequest, define.ApiResponse{ + Status: "error", + Error: fmt.Sprintf("无效的接口 %s,可用接口: %v", ifName, config.Config.AvailableInterfaces), + }) + return + } + + hands.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, define.ApiResponse{ + Status: "error", + Error: "无效的预设姿势", + }) + return + } + + // 解析手型 ID(从查询参数或使用接口配置) + handId := uint32(0) + if handType != "" { + handId = hands.ParseHandType(handType, 0, ifName) + } + + if err := hands.SendFingerPose(ifName, fingerPose, handType, handId); err != nil { + c.JSON(http.StatusInternalServerError, define.ApiResponse{ + Status: "error", + Error: "设置预设姿势失败:" + err.Error(), + }) + return + } + + c.JSON(http.StatusOK, define.ApiResponse{ + Status: "success", + Message: message, + Data: map[string]any{"interface": ifName, "pose": fingerPose}, + }) +} + +// 动画控制处理函数 +func HandleAnimation(c *gin.Context) { + var req AnimationRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, define.ApiResponse{ + Status: "error", + Error: "无效的动画请求:" + err.Error(), + }) + return + } + + // 如果未指定接口,使用默认接口 + if req.Interface == "" { + req.Interface = config.Config.DefaultInterface + } + + // 验证接口 + if !config.IsValidInterface(req.Interface) { + c.JSON(http.StatusBadRequest, define.ApiResponse{ + Status: "error", + Error: fmt.Sprintf("无效的接口 %s,可用接口: %v", req.Interface, config.Config.AvailableInterfaces), + }) + return + } + + // 停止当前动画 + hands.StopAllAnimations(req.Interface) + + // 如果是停止命令,直接返回 + if req.Type == "stop" { + c.JSON(http.StatusOK, define.ApiResponse{ + Status: "success", + Message: fmt.Sprintf("%s 动画已停止", req.Interface), + }) + return + } + + // 处理速度参数 + if req.Speed <= 0 { + req.Speed = 500 // 默认速度 + } + + // 根据类型启动动画 + switch req.Type { + case "wave": + hands.StartWaveAnimation(req.Interface, req.Speed, req.HandType, req.HandId) + c.JSON(http.StatusOK, define.ApiResponse{ + Status: "success", + Message: fmt.Sprintf("%s 波浪动画已启动", req.Interface), + Data: map[string]any{"interface": req.Interface, "speed": req.Speed}, + }) + case "sway": + hands.StartSwayAnimation(req.Interface, req.Speed, req.HandType, req.HandId) + c.JSON(http.StatusOK, define.ApiResponse{ + Status: "success", + Message: fmt.Sprintf("%s 横向摆动动画已启动", req.Interface), + Data: map[string]any{"interface": req.Interface, "speed": req.Speed}, + }) + default: + c.JSON(http.StatusBadRequest, define.ApiResponse{ + Status: "error", + Error: "无效的动画类型", + }) + } +} + +// 获取传感器数据处理函数 +func HandleSensors(c *gin.Context) { + // 从查询参数获取接口名称 + ifName := c.Query("interface") + + hands.SensorMutex.RLock() + defer hands.SensorMutex.RUnlock() + + if ifName != "" { + // 验证接口 + if !config.IsValidInterface(ifName) { + c.JSON(http.StatusBadRequest, define.ApiResponse{ + Status: "error", + Error: fmt.Sprintf("无效的接口 %s,可用接口: %v", ifName, config.Config.AvailableInterfaces), + }) + return + } + + // 请求特定接口的数据 + if sensorData, ok := hands.SensorDataMap[ifName]; ok { + c.JSON(http.StatusOK, define.ApiResponse{ + Status: "success", + Data: sensorData, + }) + } else { + c.JSON(http.StatusInternalServerError, define.ApiResponse{ + Status: "error", + Error: "传感器数据不存在", + }) + } + } else { + // 返回所有接口的数据 + c.JSON(http.StatusOK, define.ApiResponse{ + Status: "success", + Data: hands.SensorDataMap, + }) + } +} + +// 系统状态处理函数 +func HandleStatus(c *gin.Context) { + hands.AnimationMutex.Lock() + animationStatus := make(map[string]bool) + for _, ifName := range config.Config.AvailableInterfaces { + animationStatus[ifName] = hands.AnimationActive[ifName] + } + hands.AnimationMutex.Unlock() + + // 检查 CAN 服务状态 + canStatus := hands.CheckCanServiceStatus() + + // 获取手型配置 + hands.HandConfigMutex.RLock() + handConfigsData := make(map[string]any) + for ifName, handConfig := range hands.HandConfigs { + handConfigsData[ifName] = map[string]any{ + "handType": handConfig.HandType, + "handId": handConfig.HandId, + } + } + hands.HandConfigMutex.RUnlock() + + interfaceStatuses := make(map[string]any) + for _, ifName := range config.Config.AvailableInterfaces { + interfaceStatuses[ifName] = map[string]any{ + "active": canStatus[ifName], + "animationActive": animationStatus[ifName], + "handConfig": handConfigsData[ifName], + } + } + + c.JSON(http.StatusOK, define.ApiResponse{ + Status: "success", + Data: map[string]any{ + "interfaces": interfaceStatuses, + "uptime": time.Since(ServerStartTime).String(), + "canServiceURL": config.Config.CanServiceURL, + "defaultInterface": config.Config.DefaultInterface, + "availableInterfaces": config.Config.AvailableInterfaces, + "activeInterfaces": len(canStatus), + "handConfigs": handConfigsData, + }, + }) +} + +// 获取可用接口列表处理函数 +func HandleInterfaces(c *gin.Context) { + responseData := map[string]any{ + "availableInterfaces": config.Config.AvailableInterfaces, + "defaultInterface": config.Config.DefaultInterface, + } + + c.JSON(http.StatusOK, define.ApiResponse{ + Status: "success", + Data: responseData, + }) +} + +// 获取手型配置处理函数 +func HandleHandConfigs(c *gin.Context) { + hands.HandConfigMutex.RLock() + defer hands.HandConfigMutex.RUnlock() + + result := make(map[string]any) + for _, ifName := range config.Config.AvailableInterfaces { + if handConfig, exists := hands.HandConfigs[ifName]; exists { + result[ifName] = map[string]any{ + "handType": handConfig.HandType, + "handId": handConfig.HandId, + } + } else { + // 返回默认配置 + result[ifName] = map[string]any{ + "handType": "right", + "handId": define.HAND_TYPE_RIGHT, + } + } + } + + c.JSON(http.StatusOK, define.ApiResponse{ + Status: "success", + Data: result, + }) +} + +// 健康检查处理函数 +func HandleHealth(c *gin.Context) { + c.JSON(http.StatusOK, define.ApiResponse{ + Status: "success", + Message: "CAN Control Service is running", + Data: map[string]any{ + "timestamp": time.Now(), + "availableInterfaces": config.Config.AvailableInterfaces, + "defaultInterface": config.Config.DefaultInterface, + "serviceVersion": "1.0.0-hand-type-support", + }, + }) +} diff --git a/api/models.go b/api/models.go new file mode 100644 index 0000000..9f09550 --- /dev/null +++ b/api/models.go @@ -0,0 +1,29 @@ +package 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"` +} diff --git a/api/router.go b/api/router.go new file mode 100644 index 0000000..b7987c2 --- /dev/null +++ b/api/router.go @@ -0,0 +1,50 @@ +package api + +import ( + "time" + + "github.com/gin-gonic/gin" +) + +// 全局变量 +var ( + ServerStartTime time.Time +) + +func SetupRoutes(r *gin.Engine) { + r.StaticFile("/", "./static/index.html") + r.Static("/static", "./static") + + api := r.Group("/api") + { + // 手型设置 API + api.POST("/hand-type", HandleHandType) + + // 手指姿态 API + api.POST("/fingers", HandleFingers) + + // 掌部姿态 API + api.POST("/palm", HandlePalm) + + // 预设姿势 API + api.POST("/preset/:pose", HandlePreset) + + // 动画控制 API + api.POST("/animation", HandleAnimation) + + // 获取传感器数据 API + api.GET("/sensors", HandleSensors) + + // 系统状态 API + api.GET("/status", HandleStatus) + + // 获取可用接口列表 API + api.GET("/interfaces", HandleInterfaces) + + // 获取手型配置 API + api.GET("/hand-configs", HandleHandConfigs) + + // 健康检查端点 + api.GET("/health", HandleHealth) + } +} diff --git a/cli/cli.go b/cli/cli.go index 8bbc25c..9c6f2c3 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -19,7 +19,7 @@ func ParseConfig() *define.Config { flag.StringVar(&cfg.CanServiceURL, "can-url", "http://127.0.0.1:5260", "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.StringVar(&canInterfacesFlag, "can-interfaces", "", "支持的 CAN 接口列表,用逗号分隔 (例如:can0,can1,vcan0)") flag.Parse() // 环境变量覆盖命令行参数 @@ -45,7 +45,7 @@ func ParseConfig() *define.Config { } } - // 如果没有指定可用接口,从CAN服务获取 + // 如果没有指定可用接口,从 CAN 服务获取 if len(cfg.AvailableInterfaces) == 0 { log.Println("🔍 未指定可用接口,将从 CAN 服务获取...") cfg.AvailableInterfaces = getAvailableInterfacesFromCanService(cfg.CanServiceURL) @@ -59,7 +59,7 @@ func ParseConfig() *define.Config { return cfg } -// 从CAN服务获取可用接口 +// 从 CAN 服务获取可用接口 func getAvailableInterfacesFromCanService(canServiceURL string) []string { resp, err := http.Get(canServiceURL + "/api/interfaces") if err != nil { diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..426ee2f --- /dev/null +++ b/config/config.go @@ -0,0 +1,12 @@ +package config + +import ( + "hands/define" + "slices" +) + +var Config *define.Config + +func IsValidInterface(ifName string) bool { + return slices.Contains(Config.AvailableInterfaces, ifName) +} diff --git a/define/define.go b/define/define.go index 0a81583..5f0cdf9 100644 --- a/define/define.go +++ b/define/define.go @@ -10,8 +10,8 @@ type Config struct { // API 响应结构体 type ApiResponse struct { - Status string `json:"status"` - Message string `json:"message,omitempty"` - Error string `json:"error,omitempty"` - Data interface{} `json:"data,omitempty"` + Status string `json:"status"` + Message string `json:"message,omitempty"` + Error string `json:"error,omitempty"` + Data any `json:"data,omitempty"` } diff --git a/define/hands.go b/define/hands.go new file mode 100644 index 0000000..54cd472 --- /dev/null +++ b/define/hands.go @@ -0,0 +1,8 @@ +package define + +type HandType int + +const ( + HAND_TYPE_LEFT HandType = 0x28 + HAND_TYPE_RIGHT HandType = 0x27 +) diff --git a/hands/animation.go b/hands/animation.go new file mode 100644 index 0000000..27500a4 --- /dev/null +++ b/hands/animation.go @@ -0,0 +1,268 @@ +package hands + +import ( + "hands/config" + "log" + "sync" + "time" +) + +var ( + AnimationActive map[string]bool // 每个接口的动画状态 + AnimationMutex sync.Mutex + StopAnimationMap map[string]chan struct{} // 每个接口的停止动画通道 +) + +func initAnimation() { + // 初始化动画状态映射 + AnimationActive = make(map[string]bool) + StopAnimationMap = make(map[string]chan struct{}) + for _, ifName := range config.Config.AvailableInterfaces { + AnimationActive[ifName] = false + StopAnimationMap[ifName] = make(chan struct{}, 1) + } +} + +// 执行波浪动画 - 支持手型参数 +func StartWaveAnimation(ifName string, speed int, handType string, handId uint32) { + if speed <= 0 { + speed = 500 // 默认速度 + } + + // 如果未指定接口,使用默认接口 + if ifName == "" { + ifName = config.Config.DefaultInterface + } + + // 验证接口 + if !config.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.Config.DefaultInterface + } + + // 验证接口 + if !config.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.Config.AvailableInterfaces { + StopAllAnimations(validIface) + } + return + } + + // 验证接口 + if !config.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) + } +} diff --git a/hands/can.go b/hands/can.go new file mode 100644 index 0000000..15e3baf --- /dev/null +++ b/hands/can.go @@ -0,0 +1,95 @@ +package hands + +import ( + "bytes" + "encoding/json" + "fmt" + "hands/config" + "hands/define" + "log" + "net/http" +) + +type CanMessage struct { + Interface string `json:"interface"` + ID uint32 `json:"id"` + Data []byte `json:"data"` +} + +// 检查 CAN 服务状态 +func CheckCanServiceStatus() map[string]bool { + resp, err := http.Get(config.Config.CanServiceURL + "/api/status") + if err != nil { + log.Printf("❌ CAN 服务状态检查失败: %v", err) + result := make(map[string]bool) + for _, ifName := range config.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.Config.AvailableInterfaces { + result[ifName] = false + } + return result + } + + var statusResp define.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.Config.AvailableInterfaces { + result[ifName] = false + } + return result + } + + // 检查状态数据 + result := make(map[string]bool) + for _, ifName := range config.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 +} + +// 发送请求到 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.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 define.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 +} diff --git a/hands/hands.go b/hands/hands.go new file mode 100644 index 0000000..6bccf2f --- /dev/null +++ b/hands/hands.go @@ -0,0 +1,241 @@ +package hands + +import ( + "fmt" + "hands/config" + "hands/define" + "log" + "math/rand/v2" + "strings" + "sync" + "time" +) + +// 手型配置结构体 +type HandConfig struct { + HandType string `json:"handType"` + HandId uint32 `json:"handId"` +} + +var ( + HandConfigMutex sync.RWMutex + HandConfigs map[string]*HandConfig // 每个接口的手型配置 +) + +func Init() { + initSensorData() + initAnimation() + initHands() +} + +func initHands() { + HandConfigs = make(map[string]*HandConfig) +} + +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 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: define.HAND_TYPE_RIGHT, + } + + log.Printf("🆕 为接口 %s 创建默认手型配置: 右手 (0x%X)", ifName, define.HAND_TYPE_RIGHT) + return HandConfigs[ifName] +} + +// 解析手型参数 +func ParseHandType(handType string, handId uint32, ifName string) uint32 { + // 如果提供了有效的 handId,直接使用 + if handId != 0 { + return handId + } + + // 根据 handType 字符串确定 ID + switch strings.ToLower(handType) { + case "left": + return define.HAND_TYPE_LEFT + case "right": + return define.HAND_TYPE_RIGHT + default: + // 使用接口的配置 + handConfig := GetHandConfig(ifName) + return handConfig.HandId + } +} + +// 发送手指姿态指令 - 支持手型参数 +func SendFingerPose(ifName string, pose []byte, handType string, handId uint32) error { + if len(pose) != 6 { + return fmt.Errorf("无效的姿态数据长度,需要 6 个字节") + } + + // 如果未指定接口,使用默认接口 + if ifName == "" { + ifName = config.Config.DefaultInterface + } + + // 验证接口 + if !config.IsValidInterface(ifName) { + return fmt.Errorf("无效的接口 %s,可用接口: %v", ifName, config.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 == define.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 +} + +// 在 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 SendPalmPose(ifName string, pose []byte, handType string, handId uint32) error { + if len(pose) != 4 { + return fmt.Errorf("无效的姿态数据长度,需要 4 个字节") + } + + // 如果未指定接口,使用默认接口 + if ifName == "" { + ifName = config.Config.DefaultInterface + } + + // 验证接口 + if !config.IsValidInterface(ifName) { + return fmt.Errorf("无效的接口 %s,可用接口: %v", ifName, config.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 == define.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 +} + +// 重置到默认姿势 +func resetToDefaultPose(ifName string) { + // 如果未指定接口,重置所有接口 + if ifName == "" { + for _, validIface := range config.Config.AvailableInterfaces { + resetToDefaultPose(validIface) + } + return + } + + // 验证接口 + if !config.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) +} diff --git a/hands/sensor.go b/hands/sensor.go new file mode 100644 index 0000000..a8185d7 --- /dev/null +++ b/hands/sensor.go @@ -0,0 +1,65 @@ +package hands + +import ( + "hands/config" + "math/rand/v2" + "sync" + "time" +) + +// 传感器数据结构体 +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"` +} + +var ( + SensorDataMap map[string]*SensorData // 每个接口的传感器数据 + SensorMutex sync.RWMutex +) + +func initSensorData() { + // 初始化传感器数据映射 + SensorDataMap = make(map[string]*SensorData) + for _, ifName := range config.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(), + } + } +} + +// 读取传感器数据 (模拟) +func ReadSensorData() { + go func() { + for { + SensorMutex.Lock() + // 为每个接口模拟压力数据 (0-100) + for _, ifName := range config.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) + } + }() +} diff --git a/main.go b/main.go index f8daab4..1f6c93e 100644 --- a/main.go +++ b/main.go @@ -1,1162 +1,33 @@ package main import ( - "bytes" - "encoding/json" "fmt" + "hands/api" "hands/cli" - "hands/define" + "hands/config" + "hands/hands" "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"` -} - -// 手型配置结构体 -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 *define.Config - serverStartTime time.Time -) - -// 验证接口是否可用 -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) - } + log.Printf("🔧 服务配置:") + log.Printf(" - CAN 服务 URL: %s", config.Config.CanServiceURL) + log.Printf(" - Web 端口: %s", config.Config.WebPort) + log.Printf(" - 可用接口: %v", config.Config.AvailableInterfaces) + log.Printf(" - 默认接口: %s", config.Config.DefaultInterface) // 初始化手型配置映射 - handConfigs = make(map[string]*HandConfig) + hands.Init() 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 define.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 define.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, define.ApiResponse{ - Status: "error", - Error: "无效的手型设置请求: " + err.Error(), - }) - return - } - - // 验证接口 - if !isValidInterface(req.Interface) { - c.JSON(http.StatusBadRequest, define.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, define.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, define.ApiResponse{ - Status: "error", - Error: "无效的手指姿态数据: " + err.Error(), - }) - return - } - - // 验证每个值是否在范围内 - for _, v := range req.Pose { - if v < 0 || v > 255 { - c.JSON(http.StatusBadRequest, define.ApiResponse{ - Status: "error", - Error: "手指姿态值必须在 0-255 范围内", - }) - return - } - } - - // 如果未指定接口,使用默认接口 - if req.Interface == "" { - req.Interface = config.DefaultInterface - } - - // 验证接口 - if !isValidInterface(req.Interface) { - c.JSON(http.StatusBadRequest, define.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, define.ApiResponse{ - Status: "error", - Error: "发送手指姿态失败: " + err.Error(), - }) - return - } - - c.JSON(http.StatusOK, define.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, define.ApiResponse{ - Status: "error", - Error: "无效的掌部姿态数据: " + err.Error(), - }) - return - } - - // 验证每个值是否在范围内 - for _, v := range req.Pose { - if v < 0 || v > 255 { - c.JSON(http.StatusBadRequest, define.ApiResponse{ - Status: "error", - Error: "掌部姿态值必须在 0-255 范围内", - }) - return - } - } - - // 如果未指定接口,使用默认接口 - if req.Interface == "" { - req.Interface = config.DefaultInterface - } - - // 验证接口 - if !isValidInterface(req.Interface) { - c.JSON(http.StatusBadRequest, define.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, define.ApiResponse{ - Status: "error", - Error: "发送掌部姿态失败: " + err.Error(), - }) - return - } - - c.JSON(http.StatusOK, define.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, define.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, define.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, define.ApiResponse{ - Status: "error", - Error: "设置预设姿势失败: " + err.Error(), - }) - return - } - - c.JSON(http.StatusOK, define.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, define.ApiResponse{ - Status: "error", - Error: "无效的动画请求: " + err.Error(), - }) - return - } - - // 如果未指定接口,使用默认接口 - if req.Interface == "" { - req.Interface = config.DefaultInterface - } - - // 验证接口 - if !isValidInterface(req.Interface) { - c.JSON(http.StatusBadRequest, define.ApiResponse{ - Status: "error", - Error: fmt.Sprintf("无效的接口 %s,可用接口: %v", req.Interface, config.AvailableInterfaces), - }) - return - } - - // 停止当前动画 - stopAllAnimations(req.Interface) - - // 如果是停止命令,直接返回 - if req.Type == "stop" { - c.JSON(http.StatusOK, define.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, define.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, define.ApiResponse{ - Status: "success", - Message: fmt.Sprintf("%s 横向摆动动画已启动", req.Interface), - Data: map[string]interface{}{"interface": req.Interface, "speed": req.Speed}, - }) - default: - c.JSON(http.StatusBadRequest, define.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, define.ApiResponse{ - Status: "error", - Error: fmt.Sprintf("无效的接口 %s,可用接口: %v", ifName, config.AvailableInterfaces), - }) - return - } - - // 请求特定接口的数据 - if sensorData, ok := sensorDataMap[ifName]; ok { - c.JSON(http.StatusOK, define.ApiResponse{ - Status: "success", - Data: sensorData, - }) - } else { - c.JSON(http.StatusInternalServerError, define.ApiResponse{ - Status: "error", - Error: "传感器数据不存在", - }) - } - } else { - // 返回所有接口的数据 - c.JSON(http.StatusOK, define.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, define.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, define.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, define.ApiResponse{ - Status: "success", - Data: result, - }) - }) - - // 健康检查端点 - 新增,用于调试 - api.GET("/health", func(c *gin.Context) { - c.JSON(http.StatusOK, define.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:") @@ -1192,30 +63,27 @@ func main() { } // 解析配置 - config = cli.ParseConfig() + config.Config = cli.ParseConfig() // 验证配置 - if len(config.AvailableInterfaces) == 0 { + if len(config.Config.AvailableInterfaces) == 0 { log.Fatal("❌ 没有可用的 CAN 接口") } - if config.DefaultInterface == "" { + if config.Config.DefaultInterface == "" { log.Fatal("❌ 没有设置默认 CAN 接口") } // 记录启动时间 - serverStartTime = time.Now() + api.ServerStartTime = time.Now() log.Printf("🚀 启动 CAN 控制服务 (支持左右手配置)") - // 初始化随机数种子 - rand.Seed(time.Now().UnixNano()) - // 初始化服务 initService() // 启动传感器数据模拟 - readSensorData() + hands.ReadSensorData() // 设置 Gin 模式 gin.SetMode(gin.ReleaseMode) @@ -1233,16 +101,16 @@ func main() { })) // 设置 API 路由 - setupRoutes(r) + 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("🌐 CAN 控制服务运行在 http://localhost:%s", config.Config.WebPort) + log.Printf("📡 连接到 CAN 服务: %s", config.Config.CanServiceURL) + log.Printf("🎯 默认接口: %s", config.Config.DefaultInterface) + log.Printf("🔌 可用接口: %v", config.Config.AvailableInterfaces) log.Printf("🤖 支持左右手动态配置") - if err := r.Run(":" + config.WebPort); err != nil { + if err := r.Run(":" + config.Config.WebPort); err != nil { log.Fatalf("❌ 服务启动失败: %v", err) } } diff --git a/pkg/communication/communicator.go b/pkg/communication/communicator.go new file mode 100644 index 0000000..41d2323 --- /dev/null +++ b/pkg/communication/communicator.go @@ -0,0 +1,122 @@ +package communication + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +// TODO: ID 的作用是什么 +// RawMessage 代表发送给 can-bridge 服务或从其接收的原始消息结构 +type RawMessage struct { + Interface string `json:"interface"` // 目标 CAN 接口名,例如 "can0", "vcan1" + ID uint32 `json:"id"` // CAN 帧的 ID + Data []byte `json:"data"` // CAN 帧的数据负载 +} + +// Communicator 定义了与 can-bridge Web 服务进行通信的接口 +type Communicator interface { + // SendMessage 将 RawMessage 通过 HTTP POST 请求发送到 can-bridge 服务 + SendMessage(msg RawMessage) error + + // GetInterfaceStatus 获取指定 CAN 接口的状态 + GetInterfaceStatus(ifName string) (isActive bool, err error) + + // GetAllInterfaceStatuses 获取所有已知 CAN 接口的状态 + GetAllInterfaceStatuses() (statuses map[string]bool, err error) + + // SetServiceURL 设置 can-bridge 服务的 URL + SetServiceURL(url string) + + // IsConnected 检查与 can-bridge 服务的连接状态 + IsConnected() bool +} + +// CanBridgeClient 实现与 can-bridge 服务的 HTTP 通信 +type CanBridgeClient struct { + serviceURL string + client *http.Client +} + +func NewCanBridgeClient(serviceURL string) Communicator { + return &CanBridgeClient{ + serviceURL: serviceURL, + client: &http.Client{ + Timeout: 5 * time.Second, + }, + } +} + +func (c *CanBridgeClient) SendMessage(msg RawMessage) error { + jsonData, err := json.Marshal(msg) + if err != nil { + return fmt.Errorf("序列化消息失败:%w", err) + } + + url := fmt.Sprintf("%s/api/can", c.serviceURL) + resp, err := c.client.Post(url, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + return fmt.Errorf("发送 HTTP 请求失败:%w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("can-bridge服务返回错误: %d, %s", resp.StatusCode, string(body)) + } + + return nil +} + +func (c *CanBridgeClient) GetInterfaceStatus(ifName string) (bool, error) { + url := fmt.Sprintf("%s/api/status/%s", c.serviceURL, ifName) + resp, err := c.client.Get(url) + if err != nil { + return false, fmt.Errorf("获取接口状态失败:%w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return false, fmt.Errorf("can-bridge 服务返回错误:%d", resp.StatusCode) + } + + var status struct { + Active bool `json:"active"` + } + + if err := json.NewDecoder(resp.Body).Decode(&status); err != nil { + return false, fmt.Errorf("解析状态响应失败:%w", err) + } + + return status.Active, nil +} + +func (c *CanBridgeClient) GetAllInterfaceStatuses() (map[string]bool, error) { + url := fmt.Sprintf("%s/api/status", c.serviceURL) + resp, err := c.client.Get(url) + if err != nil { + return nil, fmt.Errorf("获取所有接口状态失败:%w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("can-bridge 服务返回错误:%d", resp.StatusCode) + } + + var statuses map[string]bool + if err := json.NewDecoder(resp.Body).Decode(&statuses); err != nil { + return nil, fmt.Errorf("解析状态响应失败:%w", err) + } + + return statuses, nil +} + +func (c *CanBridgeClient) SetServiceURL(url string) { c.serviceURL = url } + +func (c *CanBridgeClient) IsConnected() bool { + _, err := c.GetAllInterfaceStatuses() + return err == nil +} diff --git a/pkg/component/pressure_sensor.go b/pkg/component/pressure_sensor.go new file mode 100644 index 0000000..1c1d454 --- /dev/null +++ b/pkg/component/pressure_sensor.go @@ -0,0 +1,78 @@ +package component + +import ( + "fmt" + "hands/pkg/device" + "math/rand/v2" + "time" +) + +// PressureSensor 压力传感器实现 +type PressureSensor struct { + id string + config map[string]any + isActive bool + samplingRate int + lastReading time.Time +} + +func NewPressureSensor(id string, config map[string]any) *PressureSensor { + return &PressureSensor{ + id: id, + config: config, + isActive: true, + samplingRate: 100, + lastReading: time.Now(), + } +} + +func (p *PressureSensor) GetID() string { + return p.id +} + +func (p *PressureSensor) GetType() device.ComponentType { + return device.SensorComponent +} + +func (p *PressureSensor) GetConfiguration() map[string]any { + return p.config +} + +func (p *PressureSensor) IsActive() bool { + return p.isActive +} + +func (p *PressureSensor) ReadData() (device.SensorData, error) { + if !p.isActive { + return nil, fmt.Errorf("传感器 %s 未激活", p.id) + } + + // 模拟压力数据读取 + // 在实际实现中,这里应该从 can-bridge 或其他数据源读取真实数据 + pressure := rand.Float64() * 100 // 0-100 的随机压力值 + + values := map[string]any{ + "pressure": pressure, + "unit": "kPa", + "location": p.config["location"], + } + + p.lastReading = time.Now() + return NewSensorData(p.id, values), nil +} + +func (p *PressureSensor) GetDataType() string { + return "pressure" +} + +func (p *PressureSensor) GetSamplingRate() int { + return p.samplingRate +} + +func (p *PressureSensor) SetSamplingRate(rate int) error { + if rate <= 0 || rate > 1000 { + return fmt.Errorf("采样率必须在 1-1000Hz 之间") + } + p.samplingRate = rate + return nil +} diff --git a/pkg/component/sensor.go b/pkg/component/sensor.go new file mode 100644 index 0000000..c684e19 --- /dev/null +++ b/pkg/component/sensor.go @@ -0,0 +1,42 @@ +package component + +import ( + "hands/pkg/device" + "time" +) + +// Sensor 传感器组件接口 +type Sensor interface { + device.Component + ReadData() (device.SensorData, error) + GetDataType() string + GetSamplingRate() int + SetSamplingRate(rate int) error +} + +// SensorDataImpl 传感器数据的具体实现 +type SensorDataImpl struct { + timestamp time.Time + values map[string]any + sensorID string +} + +func NewSensorData(sensorID string, values map[string]any) *SensorDataImpl { + return &SensorDataImpl{ + timestamp: time.Now(), + values: values, + sensorID: sensorID, + } +} + +func (s *SensorDataImpl) Timestamp() time.Time { + return s.timestamp +} + +func (s *SensorDataImpl) Values() map[string]any { + return s.values +} + +func (s *SensorDataImpl) SensorID() string { + return s.sensorID +} diff --git a/pkg/device/commands.go b/pkg/device/commands.go new file mode 100644 index 0000000..fb14a71 --- /dev/null +++ b/pkg/device/commands.go @@ -0,0 +1,80 @@ +package device + +// FingerPoseCommand 手指姿态指令 +type FingerPoseCommand struct { + fingerID string + poseData []byte + targetComp string +} + +func NewFingerPoseCommand(fingerID string, poseData []byte) *FingerPoseCommand { + return &FingerPoseCommand{ + fingerID: fingerID, + poseData: poseData, + targetComp: "finger_" + fingerID, + } +} + +func (c *FingerPoseCommand) Type() string { + return "SetFingerPose" +} + +func (c *FingerPoseCommand) Payload() []byte { + return c.poseData +} + +func (c *FingerPoseCommand) TargetComponent() string { + return c.targetComp +} + +// PalmPoseCommand 手掌姿态指令 +type PalmPoseCommand struct { + poseData []byte + targetComp string +} + +func NewPalmPoseCommand(poseData []byte) *PalmPoseCommand { + return &PalmPoseCommand{ + poseData: poseData, + targetComp: "palm", + } +} + +func (c *PalmPoseCommand) Type() string { + return "SetPalmPose" +} + +func (c *PalmPoseCommand) Payload() []byte { + return c.poseData +} + +func (c *PalmPoseCommand) TargetComponent() string { + return c.targetComp +} + +// GenericCommand 通用指令 +type GenericCommand struct { + cmdType string + payload []byte + targetComp string +} + +func NewGenericCommand(cmdType string, payload []byte, targetComp string) *GenericCommand { + return &GenericCommand{ + cmdType: cmdType, + payload: payload, + targetComp: targetComp, + } +} + +func (c *GenericCommand) Type() string { + return c.cmdType +} + +func (c *GenericCommand) Payload() []byte { + return c.payload +} + +func (c *GenericCommand) TargetComponent() string { + return c.targetComp +} diff --git a/pkg/device/device.go b/pkg/device/device.go new file mode 100644 index 0000000..0a4d31c --- /dev/null +++ b/pkg/device/device.go @@ -0,0 +1,60 @@ +package device + +import ( + "hands/define" + "time" +) + +// Device 代表一个可控制的设备单元 +type Device interface { + GetID() string // 获取设备唯一标识 + GetModel() string // 获取设备型号 (例如 "L10", "L20") + GetHandType() define.HandType // 获取设备手型 + SetHandType(handType define.HandType) error // 设置设备手型 + ExecuteCommand(cmd Command) error // 执行一个通用指令 + ReadSensorData(sensorID string) (SensorData, error) // 读取特定传感器数据 + GetComponents(componentType ComponentType) []Component // 获取指定类型的组件 + GetStatus() (DeviceStatus, error) // 获取设备状态 + Connect() error // 连接设备 + Disconnect() error // 断开设备连接 +} + +// Command 代表一个发送给设备的指令 +type Command interface { + Type() string // 指令类型,例如 "SetFingerPose", "SetPalmAngle" + Payload() []byte // 指令的实际数据 + TargetComponent() string // 目标组件 ID +} + +// SensorData 代表从传感器读取的数据 +type SensorData interface { + Timestamp() time.Time + Values() map[string]any // 例如 {"pressure": 100, "angle": 30.5} + SensorID() string +} + +// ComponentType 定义组件类型 +type ComponentType string + +const ( + SensorComponent ComponentType = "sensor" + SkinComponent ComponentType = "skin" + ActuatorComponent ComponentType = "actuator" +) + +// Component 代表设备的一个可插拔组件 +type Component interface { + GetID() string + GetType() ComponentType + GetConfiguration() map[string]interface{} // 组件的特定配置 + IsActive() bool +} + +// DeviceStatus 代表设备状态 +type DeviceStatus struct { + IsConnected bool + IsActive bool + LastUpdate time.Time + ErrorCount int + LastError string +} diff --git a/pkg/device/factory.go b/pkg/device/factory.go new file mode 100644 index 0000000..b147563 --- /dev/null +++ b/pkg/device/factory.go @@ -0,0 +1,35 @@ +package device + +import "fmt" + +// DeviceFactory 设备工厂 +type DeviceFactory struct { + constructors map[string]func(config map[string]any) (Device, error) +} + +var defaultFactory = &DeviceFactory{ + constructors: make(map[string]func(config map[string]any) (Device, error)), +} + +// RegisterDeviceType 注册设备类型 +func RegisterDeviceType(modelName string, constructor func(config map[string]any) (Device, error)) { + defaultFactory.constructors[modelName] = constructor +} + +// CreateDevice 创建设备实例 +func CreateDevice(modelName string, config map[string]any) (Device, error) { + constructor, ok := defaultFactory.constructors[modelName] + if !ok { + return nil, fmt.Errorf("未知的设备型号: %s", modelName) + } + return constructor(config) +} + +// GetSupportedModels 获取支持的设备型号列表 +func GetSupportedModels() []string { + models := make([]string, 0, len(defaultFactory.constructors)) + for model := range defaultFactory.constructors { + models = append(models, model) + } + return models +} diff --git a/pkg/device/manager.go b/pkg/device/manager.go new file mode 100644 index 0000000..d7d6e8c --- /dev/null +++ b/pkg/device/manager.go @@ -0,0 +1,63 @@ +package device + +import ( + "fmt" + "sync" +) + +// DeviceManager 管理设备实例 +type DeviceManager struct { + devices map[string]Device + mutex sync.RWMutex +} + +func NewDeviceManager() *DeviceManager { return &DeviceManager{devices: make(map[string]Device)} } + +func (m *DeviceManager) RegisterDevice(dev Device) error { + m.mutex.Lock() + defer m.mutex.Unlock() + + id := dev.GetID() + if _, exists := m.devices[id]; exists { + return fmt.Errorf("设备 %s 已存在", id) + } + + m.devices[id] = dev + return nil +} + +func (m *DeviceManager) GetDevice(id string) (Device, error) { + m.mutex.RLock() + defer m.mutex.RUnlock() + + dev, exists := m.devices[id] + if !exists { + return nil, fmt.Errorf("设备 %s 不存在", id) + } + + return dev, nil +} + +func (m *DeviceManager) GetAllDevices() []Device { + m.mutex.RLock() + defer m.mutex.RUnlock() + + devices := make([]Device, 0, len(m.devices)) + for _, dev := range m.devices { + devices = append(devices, dev) + } + + return devices +} + +func (m *DeviceManager) RemoveDevice(id string) error { + m.mutex.Lock() + defer m.mutex.Unlock() + + if _, exists := m.devices[id]; !exists { + return fmt.Errorf("设备 %s 不存在", id) + } + + delete(m.devices, id) + return nil +} diff --git a/pkg/device/models/init.go b/pkg/device/models/init.go new file mode 100644 index 0000000..c3910c7 --- /dev/null +++ b/pkg/device/models/init.go @@ -0,0 +1,8 @@ +package models + +import "hands/pkg/device" + +func init() { + // 注册 L10 设备类型 + device.RegisterDeviceType("L10", NewL10Hand) +} diff --git a/pkg/device/models/l10.go b/pkg/device/models/l10.go new file mode 100644 index 0000000..1711d58 --- /dev/null +++ b/pkg/device/models/l10.go @@ -0,0 +1,240 @@ +package models + +import ( + "fmt" + "sync" + "time" + + "hands/define" + "hands/pkg/communication" + "hands/pkg/component" + "hands/pkg/device" +) + +// L10Hand L10 型号手部设备实现 +type L10Hand struct { + id string + model string + handType define.HandType + communicator communication.Communicator + components map[device.ComponentType][]device.Component + status device.DeviceStatus + mutex sync.RWMutex + canInterface string // CAN 接口名称,如 "can0" +} + +// NewL10Hand 创建 L10 手部设备实例 +func NewL10Hand(config map[string]any) (device.Device, error) { + id, ok := config["id"].(string) + if !ok { + return nil, fmt.Errorf("缺少设备 ID 配置") + } + + serviceURL, ok := config["can_service_url"].(string) + if !ok { + return nil, fmt.Errorf("缺少 can 服务 URL 配置") + } + + canInterface, ok := config["can_interface"].(string) + if !ok { + canInterface = "can0" // 默认接口 + } + + handType, ok := config["hand_type"].(define.HandType) + if !ok { + handType = define.HAND_TYPE_LEFT + } + + // 创建通信客户端 + comm := communication.NewCanBridgeClient(serviceURL) + + hand := &L10Hand{ + id: id, + model: "L10", + handType: handType, + communicator: comm, + components: make(map[device.ComponentType][]device.Component), + canInterface: canInterface, + status: device.DeviceStatus{ + IsConnected: false, + IsActive: false, + LastUpdate: time.Now(), + }, + } + + // 初始化组件 + if err := hand.initializeComponents(config); err != nil { + return nil, fmt.Errorf("初始化组件失败:%w", err) + } + + return hand, nil +} + +func (h *L10Hand) GetHandType() define.HandType { + return h.handType +} + +func (h *L10Hand) SetHandType(handType define.HandType) error { + h.handType = handType + return nil +} + +func (h *L10Hand) initializeComponents(_ map[string]any) error { + // 初始化传感器组件 + sensors := []device.Component{ + component.NewPressureSensor("pressure_thumb", map[string]any{"location": "thumb"}), + component.NewPressureSensor("pressure_index", map[string]any{"location": "index"}), + component.NewPressureSensor("pressure_middle", map[string]any{"location": "middle"}), + component.NewPressureSensor("pressure_ring", map[string]any{"location": "ring"}), + component.NewPressureSensor("pressure_pinky", map[string]any{"location": "pinky"}), + } + h.components[device.SensorComponent] = sensors + + return nil +} + +func (h *L10Hand) GetID() string { + return h.id +} + +func (h *L10Hand) GetModel() string { + return h.model +} + +func (h *L10Hand) ExecuteCommand(cmd device.Command) error { + h.mutex.Lock() + defer h.mutex.Unlock() + + // 将通用指令转换为 L10 特定的 CAN 消息 + rawMsg, err := h.commandToRawMessage(cmd) + if err != nil { + return fmt.Errorf("转换指令失败:%w", err) + } + + // 发送到 can-bridge 服务 + if err := h.communicator.SendMessage(rawMsg); err != nil { + h.status.ErrorCount++ + h.status.LastError = err.Error() + return fmt.Errorf("发送指令失败:%w", err) + } + + h.status.LastUpdate = time.Now() + return nil +} + +func (h *L10Hand) commandToRawMessage(cmd device.Command) (communication.RawMessage, error) { + var canID uint32 + var data []byte + + switch cmd.Type() { + case "SetFingerPose": + // 根据目标组件确定 CAN ID + canID = h.getFingerCanID(cmd.TargetComponent()) + data = cmd.Payload() + case "SetPalmPose": + canID = h.getPalmCanID() + data = cmd.Payload() + default: + return communication.RawMessage{}, fmt.Errorf("不支持的指令类型: %s", cmd.Type()) + } + + return communication.RawMessage{ + Interface: h.canInterface, + ID: canID, + Data: data, + }, nil +} + +func (h *L10Hand) getFingerCanID(targetComponent string) uint32 { + // L10 设备的手指 CAN ID 映射 + fingerIDs := map[string]uint32{ + "finger_thumb": 0x100, + "finger_index": 0x101, + "finger_middle": 0x102, + "finger_ring": 0x103, + "finger_pinky": 0x104, + } + + if id, exists := fingerIDs[targetComponent]; exists { + return id + } + return 0x100 // 默认拇指 +} + +func (h *L10Hand) getPalmCanID() uint32 { + return 0x200 // L10 设备的手掌 CAN ID +} + +func (h *L10Hand) ReadSensorData(sensorID string) (device.SensorData, error) { + h.mutex.RLock() + defer h.mutex.RUnlock() + + // 查找传感器组件 + sensors := h.components[device.SensorComponent] + for _, comp := range sensors { + if comp.GetID() == sensorID { + if sensor, ok := comp.(component.Sensor); ok { + return sensor.ReadData() + } + } + } + + return nil, fmt.Errorf("传感器 %s 不存在", sensorID) +} + +func (h *L10Hand) GetComponents(componentType device.ComponentType) []device.Component { + h.mutex.RLock() + defer h.mutex.RUnlock() + + if components, exists := h.components[componentType]; exists { + result := make([]device.Component, len(components)) + copy(result, components) + return result + } + + return []device.Component{} +} + +func (h *L10Hand) GetStatus() (device.DeviceStatus, error) { + h.mutex.RLock() + defer h.mutex.RUnlock() + + return h.status, nil +} + +func (h *L10Hand) Connect() error { + h.mutex.Lock() + defer h.mutex.Unlock() + + // 检查与 can-bridge 服务的连接 + if !h.communicator.IsConnected() { + return fmt.Errorf("无法连接到 can-bridge 服务") + } + + // 检查 CAN 接口状态 + isActive, err := h.communicator.GetInterfaceStatus(h.canInterface) + if err != nil { + return fmt.Errorf("检查 CAN 接口状态失败:%w", err) + } + + if !isActive { + return fmt.Errorf("CAN接口 %s 未激活", h.canInterface) + } + + h.status.IsConnected = true + h.status.IsActive = true + h.status.LastUpdate = time.Now() + + return nil +} + +func (h *L10Hand) Disconnect() error { + h.mutex.Lock() + defer h.mutex.Unlock() + + h.status.IsConnected = false + h.status.IsActive = false + h.status.LastUpdate = time.Now() + + return nil +} diff --git a/static/index.html b/static/index.html index a7fff2c..f565dd9 100644 --- a/static/index.html +++ b/static/index.html @@ -39,7 +39,7 @@