commit
7a1e6f8eb3
6
.github/dependabot.yml
vendored
Normal file
6
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
BIN
.github/logo.png
vendored
Normal file
BIN
.github/logo.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 306 KiB |
39
.github/workflows/CodeQL.yaml
vendored
Normal file
39
.github/workflows/CodeQL.yaml
vendored
Normal file
@ -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}}"
|
55
.github/workflows/release.yaml
vendored
Normal file
55
.github/workflows/release.yaml
vendored
Normal file
@ -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 }}
|
362
.gitignore
vendored
Normal file
362
.gitignore
vendored
Normal file
@ -0,0 +1,362 @@
|
||||
# Custom
|
||||
/temp.md
|
||||
/hands
|
||||
|
||||
#################### Go.gitignore ####################
|
||||
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
#################### Archives.gitignore ####################
|
||||
|
||||
# It's better to unpack these files and commit the raw source because
|
||||
# git has its own built in compression methods.
|
||||
*.7z
|
||||
*.jar
|
||||
*.rar
|
||||
*.zip
|
||||
*.gz
|
||||
*.gzip
|
||||
*.tgz
|
||||
*.bzip
|
||||
*.bzip2
|
||||
*.bz2
|
||||
*.xz
|
||||
*.lzma
|
||||
*.cab
|
||||
*.xar
|
||||
|
||||
# Packing-only formats
|
||||
*.iso
|
||||
*.tar
|
||||
|
||||
# Package management formats
|
||||
*.dmg
|
||||
*.xpi
|
||||
*.gem
|
||||
*.egg
|
||||
*.deb
|
||||
*.rpm
|
||||
*.msi
|
||||
*.msm
|
||||
*.msp
|
||||
*.txz
|
||||
|
||||
#################### Backup.gitignore ####################
|
||||
|
||||
*.bak
|
||||
*.gho
|
||||
*.ori
|
||||
*.orig
|
||||
*.tmp
|
||||
|
||||
#################### Emacs.gitignore ####################
|
||||
|
||||
# -*- mode: gitignore; -*-
|
||||
*~
|
||||
\#*\#
|
||||
/.emacs.desktop
|
||||
/.emacs.desktop.lock
|
||||
*.elc
|
||||
auto-save-list
|
||||
tramp
|
||||
.\#*
|
||||
|
||||
# Org-mode
|
||||
.org-id-locations
|
||||
*_archive
|
||||
|
||||
# flymake-mode
|
||||
*_flymake.*
|
||||
|
||||
# eshell files
|
||||
/eshell/history
|
||||
/eshell/lastdir
|
||||
|
||||
# elpa packages
|
||||
/elpa/
|
||||
|
||||
# reftex files
|
||||
*.rel
|
||||
|
||||
# AUCTeX auto folder
|
||||
/auto/
|
||||
|
||||
# cask packages
|
||||
.cask/
|
||||
dist/
|
||||
|
||||
# Flycheck
|
||||
flycheck_*.el
|
||||
|
||||
# server auth directory
|
||||
/server/
|
||||
|
||||
# projectiles files
|
||||
.projectile
|
||||
|
||||
# directory configuration
|
||||
.dir-locals.el
|
||||
|
||||
# network security
|
||||
/network-security.data
|
||||
|
||||
|
||||
#################### JetBrains.gitignore ####################
|
||||
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# AWS User-specific
|
||||
.idea/**/aws.xml
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# SonarLint plugin
|
||||
.idea/sonarlint/
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
#################### Linux.gitignore ####################
|
||||
|
||||
*~
|
||||
|
||||
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||
.fuse_hidden*
|
||||
|
||||
# KDE directory preferences
|
||||
.directory
|
||||
|
||||
# Linux trash folder which might appear on any partition or disk
|
||||
.Trash-*
|
||||
|
||||
# .nfs files are created when an open file is removed but is still being accessed
|
||||
.nfs*
|
||||
|
||||
#################### NotepadPP.gitignore ####################
|
||||
|
||||
# Notepad++ backups #
|
||||
*.bak
|
||||
|
||||
#################### PuTTY.gitignore ####################
|
||||
|
||||
# Private key
|
||||
*.ppk
|
||||
|
||||
#################### SublimeText.gitignore ####################
|
||||
|
||||
# Cache files for Sublime Text
|
||||
*.tmlanguage.cache
|
||||
*.tmPreferences.cache
|
||||
*.stTheme.cache
|
||||
|
||||
# Workspace files are user-specific
|
||||
*.sublime-workspace
|
||||
|
||||
# Project files should be checked into the repository, unless a significant
|
||||
# proportion of contributors will probably not be using Sublime Text
|
||||
# *.sublime-project
|
||||
|
||||
# SFTP configuration file
|
||||
sftp-config.json
|
||||
sftp-config-alt*.json
|
||||
|
||||
# Package control specific files
|
||||
Package Control.last-run
|
||||
Package Control.ca-list
|
||||
Package Control.ca-bundle
|
||||
Package Control.system-ca-bundle
|
||||
Package Control.cache/
|
||||
Package Control.ca-certs/
|
||||
Package Control.merged-ca-bundle
|
||||
Package Control.user-ca-bundle
|
||||
oscrypto-ca-bundle.crt
|
||||
bh_unicode_properties.cache
|
||||
|
||||
# Sublime-github package stores a github token in this file
|
||||
# https://packagecontrol.io/packages/sublime-github
|
||||
GitHub.sublime-settings
|
||||
|
||||
#################### Vim.gitignore ####################
|
||||
|
||||
# Swap
|
||||
[._]*.s[a-v][a-z]
|
||||
!*.svg # comment out if you don't need vector files
|
||||
[._]*.sw[a-p]
|
||||
[._]s[a-rt-v][a-z]
|
||||
[._]ss[a-gi-z]
|
||||
[._]sw[a-p]
|
||||
|
||||
# Session
|
||||
Session.vim
|
||||
Sessionx.vim
|
||||
|
||||
# Temporary
|
||||
.netrwhist
|
||||
*~
|
||||
# Auto-generated tag files
|
||||
tags
|
||||
# Persistent undo
|
||||
[._]*.un~
|
||||
|
||||
#################### VisualStudioCode.gitignore ####################
|
||||
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/*.code-snippets
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
#################### Windows.gitignore ####################
|
||||
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
Thumbs.db:encryptable
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
#################### macOS.gitignore ####################
|
||||
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
#################### Custom.gitignore ####################
|
||||
|
||||
# add your custom gitignore here:
|
||||
!.gitignore
|
||||
!.gitsubmodules
|
117
.goreleaser.yaml
Normal file
117
.goreleaser.yaml
Normal file
@ -0,0 +1,117 @@
|
||||
version: 2
|
||||
|
||||
project_name: dashboard-server
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
|
||||
builds:
|
||||
- id: dashboard-server
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
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"
|
26
Dockerfile
Normal file
26
Dockerfile
Normal file
@ -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 /app/dashboard-server
|
||||
|
||||
EXPOSE 9099
|
||||
|
||||
ENV SERVER_PORT="9099"
|
||||
|
||||
CMD ["/app/dashboard-server"]
|
10
Dockerfile.goreleaser
Normal file
10
Dockerfile.goreleaser
Normal file
@ -0,0 +1,10 @@
|
||||
FROM alpine:3.21
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --link . .
|
||||
|
||||
EXPOSE 9099
|
||||
ENV SERVER_PORT="9099"
|
||||
|
||||
CMD ["/app/dashboard-server"]
|
@ -2,6 +2,8 @@
|
||||
|
||||
Dexterous hand-operated dashboard for LinkerHand 👋!
|
||||
|
||||
[中文文档](./docs/README_CN.md) [中文贡献指南](./docs/contribute_CN.md)
|
||||
|
||||
## Project Overview
|
||||
|
||||
**Dexterous Hand Dashboard** is a control dashboard service specifically developed for the LinkerHand dexterous hand device. Built with Golang, it provides a flexible RESTful API interface, enabling finger and palm pose control, execution of preset gestures, real-time sensor data monitoring, and dynamic configuration of hand type (left or right) and CAN interfaces.
|
||||
|
197
api/animation_handlers.go
Normal file
197
api/animation_handlers.go
Normal file
@ -0,0 +1,197 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// handleGetAnimations 获取可用动画列表
|
||||
func (s *Server) handleGetAnimations(c *gin.Context) {
|
||||
deviceId := c.Param("deviceId")
|
||||
|
||||
// 获取设备
|
||||
dev, err := s.deviceManager.GetDevice(deviceId)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, ApiResponse{
|
||||
Status: "error",
|
||||
Error: fmt.Sprintf("设备 %s 不存在", deviceId),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取动画引擎
|
||||
animEngine := dev.GetAnimationEngine()
|
||||
|
||||
// 获取已注册的动画列表
|
||||
availableAnimations := animEngine.GetRegisteredAnimations()
|
||||
|
||||
// 获取当前动画状态
|
||||
isRunning := animEngine.IsRunning()
|
||||
currentName := animEngine.GetCurrentAnimation()
|
||||
|
||||
response := AnimationStatusResponse{
|
||||
IsRunning: isRunning,
|
||||
CurrentName: currentName,
|
||||
AvailableList: availableAnimations,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, ApiResponse{
|
||||
Status: "success",
|
||||
Data: response,
|
||||
})
|
||||
}
|
||||
|
||||
// handleStartAnimation 启动动画
|
||||
func (s *Server) handleStartAnimation(c *gin.Context) {
|
||||
deviceId := c.Param("deviceId")
|
||||
|
||||
var req AnimationStartRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, ApiResponse{
|
||||
Status: "error",
|
||||
Error: "无效的动画请求:" + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取设备
|
||||
dev, err := s.deviceManager.GetDevice(deviceId)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, ApiResponse{
|
||||
Status: "error",
|
||||
Error: fmt.Sprintf("设备 %s 不存在", deviceId),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取动画引擎
|
||||
animEngine := dev.GetAnimationEngine()
|
||||
|
||||
// 验证动画名称是否已注册
|
||||
availableAnimations := animEngine.GetRegisteredAnimations()
|
||||
validAnimation := false
|
||||
for _, name := range availableAnimations {
|
||||
if name == req.Name {
|
||||
validAnimation = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !validAnimation {
|
||||
c.JSON(http.StatusBadRequest, ApiResponse{
|
||||
Status: "error",
|
||||
Error: fmt.Sprintf("无效的动画类型:%s,可用动画:%v", req.Name, availableAnimations),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 处理速度参数
|
||||
speedMs := req.SpeedMs
|
||||
if speedMs <= 0 {
|
||||
speedMs = 500 // 默认速度
|
||||
}
|
||||
|
||||
// 启动动画
|
||||
if err := animEngine.Start(req.Name, speedMs); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ApiResponse{
|
||||
Status: "error",
|
||||
Error: fmt.Sprintf("启动动画失败:%v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, ApiResponse{
|
||||
Status: "success",
|
||||
Message: fmt.Sprintf("设备 %s 的 %s 动画已启动", deviceId, req.Name),
|
||||
Data: map[string]any{
|
||||
"deviceId": deviceId,
|
||||
"name": req.Name,
|
||||
"speedMs": speedMs,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// handleStopAnimation 停止动画
|
||||
func (s *Server) handleStopAnimation(c *gin.Context) {
|
||||
deviceId := c.Param("deviceId")
|
||||
|
||||
// 获取设备
|
||||
dev, err := s.deviceManager.GetDevice(deviceId)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, ApiResponse{
|
||||
Status: "error",
|
||||
Error: fmt.Sprintf("设备 %s 不存在", deviceId),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取动画引擎
|
||||
animEngine := dev.GetAnimationEngine()
|
||||
|
||||
// 检查是否有动画在运行
|
||||
if !animEngine.IsRunning() {
|
||||
c.JSON(http.StatusOK, ApiResponse{
|
||||
Status: "success",
|
||||
Message: fmt.Sprintf("设备 %s 当前没有动画在运行", deviceId),
|
||||
Data: map[string]any{
|
||||
"deviceId": deviceId,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 停止动画
|
||||
if err := animEngine.Stop(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ApiResponse{
|
||||
Status: "error",
|
||||
Error: fmt.Sprintf("停止动画失败:%v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, ApiResponse{
|
||||
Status: "success",
|
||||
Message: fmt.Sprintf("设备 %s 的动画已停止", deviceId),
|
||||
Data: map[string]any{
|
||||
"deviceId": deviceId,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// handleAnimationStatus 获取动画状态
|
||||
func (s *Server) handleAnimationStatus(c *gin.Context) {
|
||||
deviceId := c.Param("deviceId")
|
||||
|
||||
// 获取设备
|
||||
dev, err := s.deviceManager.GetDevice(deviceId)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, ApiResponse{
|
||||
Status: "error",
|
||||
Error: fmt.Sprintf("设备 %s 不存在", deviceId),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取动画引擎
|
||||
animEngine := dev.GetAnimationEngine()
|
||||
|
||||
// 获取已注册的动画列表
|
||||
availableAnimations := animEngine.GetRegisteredAnimations()
|
||||
|
||||
// 获取当前状态
|
||||
isRunning := animEngine.IsRunning()
|
||||
currentName := animEngine.GetCurrentAnimation()
|
||||
|
||||
response := AnimationStatusResponse{
|
||||
IsRunning: isRunning,
|
||||
CurrentName: currentName,
|
||||
AvailableList: availableAnimations,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, ApiResponse{
|
||||
Status: "success",
|
||||
Data: response,
|
||||
})
|
||||
}
|
251
api/device_handlers.go
Normal file
251
api/device_handlers.go
Normal file
@ -0,0 +1,251 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"hands/define"
|
||||
"hands/device"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// handleGetDevices 获取所有设备列表
|
||||
func (s *Server) handleGetDevices(c *gin.Context) {
|
||||
devices := s.deviceManager.GetAllDevices()
|
||||
|
||||
deviceInfos := make([]DeviceInfo, 0, len(devices))
|
||||
for _, dev := range devices {
|
||||
status, err := dev.GetStatus()
|
||||
if err != nil {
|
||||
// 如果获取状态失败,使用默认状态
|
||||
status = device.DeviceStatus{
|
||||
IsConnected: false,
|
||||
IsActive: false,
|
||||
ErrorCount: 1,
|
||||
LastError: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
deviceInfo := DeviceInfo{
|
||||
ID: dev.GetID(),
|
||||
Model: dev.GetModel(),
|
||||
HandType: dev.GetHandType().String(),
|
||||
Status: status,
|
||||
}
|
||||
deviceInfos = append(deviceInfos, deviceInfo)
|
||||
}
|
||||
|
||||
response := DeviceListResponse{
|
||||
Devices: deviceInfos,
|
||||
Total: len(deviceInfos),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, ApiResponse{
|
||||
Status: "success",
|
||||
Data: response,
|
||||
})
|
||||
}
|
||||
|
||||
// handleCreateDevice 创建新设备
|
||||
func (s *Server) handleCreateDevice(c *gin.Context) {
|
||||
var req DeviceCreateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, ApiResponse{
|
||||
Status: "error",
|
||||
Error: "无效的设备创建请求:" + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查设备是否已存在
|
||||
if _, err := s.deviceManager.GetDevice(req.ID); err == nil {
|
||||
c.JSON(http.StatusConflict, ApiResponse{
|
||||
Status: "error",
|
||||
Error: fmt.Sprintf("设备 %s 已存在", req.ID),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 准备设备配置
|
||||
config := req.Config
|
||||
if config == nil {
|
||||
config = make(map[string]any)
|
||||
}
|
||||
config["id"] = req.ID
|
||||
|
||||
// 设置手型
|
||||
if req.HandType != "" {
|
||||
config["hand_type"] = req.HandType
|
||||
}
|
||||
|
||||
// 创建设备实例
|
||||
dev, err := device.CreateDevice(req.Model, config)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, ApiResponse{
|
||||
Status: "error",
|
||||
Error: fmt.Sprintf("创建设备失败:%v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 注册设备到管理器
|
||||
if err := s.deviceManager.RegisterDevice(dev); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ApiResponse{
|
||||
Status: "error",
|
||||
Error: fmt.Sprintf("注册设备失败:%v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取设备状态
|
||||
status, err := dev.GetStatus()
|
||||
if err != nil {
|
||||
status = device.DeviceStatus{
|
||||
IsConnected: false,
|
||||
IsActive: false,
|
||||
ErrorCount: 1,
|
||||
LastError: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
deviceInfo := DeviceInfo{
|
||||
ID: dev.GetID(),
|
||||
Model: dev.GetModel(),
|
||||
HandType: dev.GetHandType().String(),
|
||||
Status: status,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, ApiResponse{
|
||||
Status: "success",
|
||||
Message: fmt.Sprintf("设备 %s 创建成功", req.ID),
|
||||
Data: deviceInfo,
|
||||
})
|
||||
}
|
||||
|
||||
// handleGetDevice 获取设备详情
|
||||
func (s *Server) handleGetDevice(c *gin.Context) {
|
||||
deviceId := c.Param("deviceId")
|
||||
|
||||
dev, err := s.deviceManager.GetDevice(deviceId)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, ApiResponse{
|
||||
Status: "error",
|
||||
Error: fmt.Sprintf("设备 %s 不存在", deviceId),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
status, err := dev.GetStatus()
|
||||
if err != nil {
|
||||
status = device.DeviceStatus{
|
||||
IsConnected: false,
|
||||
IsActive: false,
|
||||
ErrorCount: 1,
|
||||
LastError: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
deviceInfo := DeviceInfo{
|
||||
ID: dev.GetID(),
|
||||
Model: dev.GetModel(),
|
||||
HandType: dev.GetHandType().String(),
|
||||
Status: status,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, ApiResponse{
|
||||
Status: "success",
|
||||
Data: deviceInfo,
|
||||
})
|
||||
}
|
||||
|
||||
// handleDeleteDevice 删除设备
|
||||
func (s *Server) handleDeleteDevice(c *gin.Context) {
|
||||
deviceId := c.Param("deviceId")
|
||||
|
||||
// 检查设备是否存在
|
||||
dev, err := s.deviceManager.GetDevice(deviceId)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, ApiResponse{
|
||||
Status: "error",
|
||||
Error: fmt.Sprintf("设备 %s 不存在", deviceId),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 停止设备的动画(如果正在运行)
|
||||
animEngine := dev.GetAnimationEngine()
|
||||
if animEngine.IsRunning() {
|
||||
if err := animEngine.Stop(); err != nil {
|
||||
// 记录错误但不阻止删除
|
||||
fmt.Printf("警告:停止设备 %s 动画时出错:%v\n", deviceId, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 从管理器中移除设备
|
||||
if err := s.deviceManager.RemoveDevice(deviceId); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ApiResponse{
|
||||
Status: "error",
|
||||
Error: fmt.Sprintf("删除设备失败:%v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, ApiResponse{
|
||||
Status: "success",
|
||||
Message: fmt.Sprintf("设备 %s 已删除", deviceId),
|
||||
})
|
||||
}
|
||||
|
||||
// handleSetHandType 设置设备手型
|
||||
func (s *Server) handleSetHandType(c *gin.Context) {
|
||||
deviceId := c.Param("deviceId")
|
||||
|
||||
var req HandTypeRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, ApiResponse{
|
||||
Status: "error",
|
||||
Error: "无效的手型设置请求:" + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取设备
|
||||
dev, err := s.deviceManager.GetDevice(deviceId)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, ApiResponse{
|
||||
Status: "error",
|
||||
Error: fmt.Sprintf("设备 %s 不存在", deviceId),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 转换手型字符串为枚举
|
||||
var handType define.HandType
|
||||
handType = define.HandTypeFromString(req.HandType)
|
||||
if handType == define.HAND_TYPE_UNKNOWN {
|
||||
c.JSON(http.StatusBadRequest, ApiResponse{
|
||||
Status: "error",
|
||||
Error: "无效的手型,必须是 'left' 或 'right'",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 设置手型
|
||||
if err := dev.SetHandType(handType); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ApiResponse{
|
||||
Status: "error",
|
||||
Error: fmt.Sprintf("设置手型失败:%v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, ApiResponse{
|
||||
Status: "success",
|
||||
Message: fmt.Sprintf("设备 %s 手型已设置为 %s", deviceId, req.HandType),
|
||||
Data: map[string]any{
|
||||
"deviceId": deviceId,
|
||||
"handType": req.HandType,
|
||||
},
|
||||
})
|
||||
}
|
111
api/models.go
Normal file
111
api/models.go
Normal file
@ -0,0 +1,111 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"hands/device"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ===== 通用响应模型 =====
|
||||
|
||||
// ApiResponse 统一 API 响应格式(保持与原 API 兼容)
|
||||
type ApiResponse struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Data any `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// ===== 设备管理相关模型 =====
|
||||
|
||||
// DeviceCreateRequest 创建设备请求
|
||||
type DeviceCreateRequest struct {
|
||||
ID string `json:"id" binding:"required"`
|
||||
Model string `json:"model" binding:"required"`
|
||||
Config map[string]any `json:"config"`
|
||||
HandType string `json:"handType,omitempty"` // "left" 或 "right"
|
||||
}
|
||||
|
||||
// DeviceInfo 设备信息响应
|
||||
type DeviceInfo struct {
|
||||
ID string `json:"id"`
|
||||
Model string `json:"model"`
|
||||
HandType string `json:"handType"`
|
||||
Status device.DeviceStatus `json:"status"`
|
||||
}
|
||||
|
||||
// DeviceListResponse 设备列表响应
|
||||
type DeviceListResponse struct {
|
||||
Devices []DeviceInfo `json:"devices"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// HandTypeRequest 手型设置请求
|
||||
type HandTypeRequest struct {
|
||||
HandType string `json:"handType" binding:"required,oneof=left right"`
|
||||
}
|
||||
|
||||
// ===== 姿态控制相关模型 =====
|
||||
|
||||
// FingerPoseRequest 手指姿态设置请求
|
||||
type FingerPoseRequest struct {
|
||||
Pose []byte `json:"pose" binding:"required,len=6"`
|
||||
}
|
||||
|
||||
// PalmPoseRequest 手掌姿态设置请求
|
||||
type PalmPoseRequest struct {
|
||||
Pose []byte `json:"pose" binding:"required,len=4"`
|
||||
}
|
||||
|
||||
// ===== 动画控制相关模型 =====
|
||||
|
||||
// AnimationStartRequest 动画启动请求
|
||||
type AnimationStartRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
SpeedMs int `json:"speedMs,omitempty"`
|
||||
}
|
||||
|
||||
// AnimationStatusResponse 动画状态响应
|
||||
type AnimationStatusResponse struct {
|
||||
IsRunning bool `json:"isRunning"`
|
||||
CurrentName string `json:"currentName,omitempty"`
|
||||
AvailableList []string `json:"availableList"`
|
||||
}
|
||||
|
||||
// ===== 传感器相关模型 =====
|
||||
|
||||
// SensorDataResponse 传感器数据响应
|
||||
type SensorDataResponse struct {
|
||||
SensorID string `json:"sensorId"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Values map[string]any `json:"values"`
|
||||
}
|
||||
|
||||
// SensorListResponse 传感器列表响应
|
||||
type SensorListResponse struct {
|
||||
Sensors []SensorDataResponse `json:"sensors"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// ===== 系统管理相关模型 =====
|
||||
|
||||
// SystemStatusResponse 系统状态响应
|
||||
type SystemStatusResponse struct {
|
||||
TotalDevices int `json:"totalDevices"`
|
||||
ActiveDevices int `json:"activeDevices"`
|
||||
SupportedModels []string `json:"supportedModels"`
|
||||
Devices map[string]DeviceInfo `json:"devices"`
|
||||
Uptime time.Duration `json:"uptime"`
|
||||
}
|
||||
|
||||
// SupportedModelsResponse 支持的设备型号响应
|
||||
type SupportedModelsResponse struct {
|
||||
Models []string `json:"models"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// HealthResponse 健康检查响应
|
||||
type HealthResponse struct {
|
||||
Status string `json:"status"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Version string `json:"version,omitempty"`
|
||||
}
|
273
api/pose_handlers.go
Normal file
273
api/pose_handlers.go
Normal file
@ -0,0 +1,273 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// handleSetFingerPose 设置手指姿态
|
||||
func (s *Server) handleSetFingerPose(c *gin.Context) {
|
||||
deviceId := c.Param("deviceId")
|
||||
|
||||
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 > 255 {
|
||||
c.JSON(http.StatusBadRequest, ApiResponse{
|
||||
Status: "error",
|
||||
Error: "手指姿态值必须在 0-255 范围内",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 获取设备
|
||||
dev, err := s.deviceManager.GetDevice(deviceId)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, ApiResponse{
|
||||
Status: "error",
|
||||
Error: fmt.Sprintf("设备 %s 不存在", deviceId),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 停止当前动画(如果正在运行)
|
||||
animEngine := dev.GetAnimationEngine()
|
||||
if animEngine.IsRunning() {
|
||||
if err := animEngine.Stop(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ApiResponse{
|
||||
Status: "error",
|
||||
Error: fmt.Sprintf("停止动画失败:%v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 设置手指姿态
|
||||
if err := dev.SetFingerPose(req.Pose); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ApiResponse{
|
||||
Status: "error",
|
||||
Error: "发送手指姿态失败:" + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, ApiResponse{
|
||||
Status: "success",
|
||||
Message: "手指姿态指令发送成功",
|
||||
Data: map[string]any{
|
||||
"deviceId": deviceId,
|
||||
"pose": req.Pose,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// handleSetPalmPose 设置手掌姿态
|
||||
func (s *Server) handleSetPalmPose(c *gin.Context) {
|
||||
deviceId := c.Param("deviceId")
|
||||
|
||||
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 > 255 {
|
||||
c.JSON(http.StatusBadRequest, ApiResponse{
|
||||
Status: "error",
|
||||
Error: "掌部姿态值必须在 0-255 范围内",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 获取设备
|
||||
dev, err := s.deviceManager.GetDevice(deviceId)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, ApiResponse{
|
||||
Status: "error",
|
||||
Error: fmt.Sprintf("设备 %s 不存在", deviceId),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 停止当前动画(如果正在运行)
|
||||
animEngine := dev.GetAnimationEngine()
|
||||
if animEngine.IsRunning() {
|
||||
if err := animEngine.Stop(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ApiResponse{
|
||||
Status: "error",
|
||||
Error: fmt.Sprintf("停止动画失败:%v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 设置手掌姿态
|
||||
if err := dev.SetPalmPose(req.Pose); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ApiResponse{
|
||||
Status: "error",
|
||||
Error: "发送掌部姿态失败:" + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, ApiResponse{
|
||||
Status: "success",
|
||||
Message: "掌部姿态指令发送成功",
|
||||
Data: map[string]any{
|
||||
"deviceId": deviceId,
|
||||
"pose": req.Pose,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// handleSetPresetPose 设置预设姿势
|
||||
func (s *Server) handleSetPresetPose(c *gin.Context) {
|
||||
deviceId := c.Param("deviceId")
|
||||
pose := c.Param("pose")
|
||||
|
||||
// 获取设备
|
||||
dev, err := s.deviceManager.GetDevice(deviceId)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, ApiResponse{
|
||||
Status: "error",
|
||||
Error: fmt.Sprintf("设备 %s 不存在", deviceId),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 停止当前动画(如果正在运行)
|
||||
animEngine := dev.GetAnimationEngine()
|
||||
if animEngine.IsRunning() {
|
||||
if err := animEngine.Stop(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ApiResponse{
|
||||
Status: "error",
|
||||
Error: fmt.Sprintf("停止动画失败:%v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 使用设备的预设姿势方法
|
||||
if err := dev.ExecutePreset(pose); err != nil {
|
||||
c.JSON(http.StatusBadRequest, ApiResponse{
|
||||
Status: "error",
|
||||
Error: fmt.Sprintf("执行预设姿势失败: %v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取预设姿势的描述
|
||||
description := dev.GetPresetDescription(pose)
|
||||
message := fmt.Sprintf("已设置预设姿势: %s", pose)
|
||||
if description != "" {
|
||||
message = fmt.Sprintf("已设置%s", description)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, ApiResponse{
|
||||
Status: "success",
|
||||
Message: message,
|
||||
Data: map[string]any{
|
||||
"deviceId": deviceId,
|
||||
"pose": pose,
|
||||
"description": description,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// handleResetPose 重置姿态
|
||||
func (s *Server) handleResetPose(c *gin.Context) {
|
||||
deviceId := c.Param("deviceId")
|
||||
|
||||
// 获取设备
|
||||
dev, err := s.deviceManager.GetDevice(deviceId)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, ApiResponse{
|
||||
Status: "error",
|
||||
Error: fmt.Sprintf("设备 %s 不存在", deviceId),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 停止当前动画(如果正在运行)
|
||||
animEngine := dev.GetAnimationEngine()
|
||||
if animEngine.IsRunning() {
|
||||
if err := animEngine.Stop(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ApiResponse{
|
||||
Status: "error",
|
||||
Error: fmt.Sprintf("停止动画失败:%v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 重置姿态
|
||||
if err := dev.ResetPose(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ApiResponse{
|
||||
Status: "error",
|
||||
Error: "重置姿态失败:" + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, ApiResponse{
|
||||
Status: "success",
|
||||
Message: fmt.Sprintf("设备 %s 已重置到默认姿态", deviceId),
|
||||
Data: map[string]any{
|
||||
"deviceId": deviceId,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// handleGetPresetPose 获取设备支持的预设姿势列表
|
||||
func (s *Server) handleGetPresetPose(c *gin.Context) {
|
||||
deviceID := c.Param("deviceId")
|
||||
|
||||
device, err := s.deviceManager.GetDevice(deviceID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, ApiResponse{
|
||||
Status: "error",
|
||||
Error: fmt.Sprintf("设备 %s 不存在", deviceID),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 使用设备的预设姿势方法
|
||||
presets := device.GetSupportedPresets()
|
||||
|
||||
// 构建详细的预设信息
|
||||
presetDetails := make([]map[string]string, 0, len(presets))
|
||||
for _, presetName := range presets {
|
||||
description := device.GetPresetDescription(presetName)
|
||||
presetDetails = append(presetDetails, map[string]string{
|
||||
"name": presetName,
|
||||
"description": description,
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, ApiResponse{
|
||||
Status: "success",
|
||||
Message: "获取设备支持的预设姿势列表成功",
|
||||
Data: map[string]any{
|
||||
"deviceId": deviceID,
|
||||
"presets": presetDetails,
|
||||
"count": len(presets),
|
||||
},
|
||||
})
|
||||
}
|
87
api/router.go
Normal file
87
api/router.go
Normal file
@ -0,0 +1,87 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"hands/device"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Server API v2 服务器结构体
|
||||
type Server struct {
|
||||
deviceManager *device.DeviceManager
|
||||
startTime time.Time
|
||||
version string
|
||||
}
|
||||
|
||||
// NewServer 创建新的 API v1 服务器实例
|
||||
func NewServer(deviceManager *device.DeviceManager) *Server {
|
||||
return &Server{
|
||||
deviceManager: deviceManager,
|
||||
startTime: time.Now(),
|
||||
version: "1.0.0",
|
||||
}
|
||||
}
|
||||
|
||||
// SetupRoutes 设置 API v1 路由
|
||||
func (s *Server) SetupRoutes(r *gin.Engine) {
|
||||
r.StaticFile("/", "./static/index.html")
|
||||
r.Static("/static", "./static")
|
||||
|
||||
// API v1 路由组
|
||||
v2 := r.Group("/api/v1")
|
||||
{
|
||||
// 设备管理路由
|
||||
devices := v2.Group("/devices")
|
||||
{
|
||||
devices.GET("", s.handleGetDevices) // 获取所有设备列表
|
||||
devices.POST("", s.handleCreateDevice) // 创建新设备
|
||||
devices.GET("/:deviceId", s.handleGetDevice) // 获取设备详情
|
||||
devices.DELETE("/:deviceId", s.handleDeleteDevice) // 删除设备
|
||||
devices.PUT("/:deviceId/hand-type", s.handleSetHandType) // 设置手型
|
||||
|
||||
// 设备级别的功能路由
|
||||
deviceRoutes := devices.Group("/:deviceId")
|
||||
{
|
||||
// 姿态控制路由
|
||||
poses := deviceRoutes.Group("/poses")
|
||||
{
|
||||
poses.POST("/fingers", s.handleSetFingerPose) // 设置手指姿态
|
||||
poses.POST("/palm", s.handleSetPalmPose) // 设置手掌姿态
|
||||
poses.POST("/reset", s.handleResetPose) // 重置姿态
|
||||
|
||||
// 新的预设姿势 API
|
||||
poses.GET("/presets", s.handleGetPresetPose) // 获取支持的预设姿势列表
|
||||
poses.POST("/presets/:presetName", s.handleSetPresetPose) // 执行预设姿势
|
||||
}
|
||||
|
||||
// 动画控制路由
|
||||
animations := deviceRoutes.Group("/animations")
|
||||
{
|
||||
animations.GET("", s.handleGetAnimations) // 获取可用动画列表
|
||||
animations.POST("/start", s.handleStartAnimation) // 启动动画
|
||||
animations.POST("/stop", s.handleStopAnimation) // 停止动画
|
||||
animations.GET("/status", s.handleAnimationStatus) // 获取动画状态
|
||||
}
|
||||
|
||||
// 传感器数据路由
|
||||
sensors := deviceRoutes.Group("/sensors")
|
||||
{
|
||||
sensors.GET("", s.handleGetSensors) // 获取所有传感器数据
|
||||
sensors.GET("/:sensorId", s.handleGetSensorData) // 获取特定传感器数据
|
||||
}
|
||||
|
||||
// 设备状态路由
|
||||
deviceRoutes.GET("/status", s.handleGetDeviceStatus) // 获取设备状态
|
||||
}
|
||||
}
|
||||
|
||||
// 系统管理路由
|
||||
system := v2.Group("/system")
|
||||
{
|
||||
system.GET("/models", s.handleGetSupportedModels) // 获取支持的设备型号
|
||||
system.GET("/status", s.handleGetSystemStatus) // 获取系统状态
|
||||
system.GET("/health", s.handleHealthCheck) // 健康检查
|
||||
}
|
||||
}
|
||||
}
|
180
api/sensor_handlers.go
Normal file
180
api/sensor_handlers.go
Normal file
@ -0,0 +1,180 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"hands/device"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// handleGetSensors 获取所有传感器数据
|
||||
func (s *Server) handleGetSensors(c *gin.Context) {
|
||||
deviceId := c.Param("deviceId")
|
||||
|
||||
// 获取设备
|
||||
dev, err := s.deviceManager.GetDevice(deviceId)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, ApiResponse{
|
||||
Status: "error",
|
||||
Error: fmt.Sprintf("设备 %s 不存在", deviceId),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取设备的传感器组件
|
||||
sensorComponents := dev.GetComponents(device.SensorComponent)
|
||||
|
||||
sensors := make([]SensorDataResponse, 0, len(sensorComponents))
|
||||
|
||||
// 遍历所有传感器组件,读取数据
|
||||
for _, component := range sensorComponents {
|
||||
sensorId := component.GetID()
|
||||
|
||||
// 读取传感器数据
|
||||
sensorData, err := dev.ReadSensorData(sensorId)
|
||||
if err != nil {
|
||||
// 如果读取失败,创建一个错误状态的传感器数据
|
||||
sensors = append(sensors, SensorDataResponse{
|
||||
SensorID: sensorId,
|
||||
Timestamp: time.Now(),
|
||||
Values: map[string]any{
|
||||
"error": err.Error(),
|
||||
"status": "error",
|
||||
},
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// 转换为响应格式
|
||||
sensorResponse := SensorDataResponse{
|
||||
SensorID: sensorData.SensorID(),
|
||||
Timestamp: sensorData.Timestamp(),
|
||||
Values: sensorData.Values(),
|
||||
}
|
||||
sensors = append(sensors, sensorResponse)
|
||||
}
|
||||
|
||||
response := SensorListResponse{
|
||||
Sensors: sensors,
|
||||
Total: len(sensors),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, ApiResponse{
|
||||
Status: "success",
|
||||
Data: response,
|
||||
})
|
||||
}
|
||||
|
||||
// handleGetSensorData 获取特定传感器数据
|
||||
func (s *Server) handleGetSensorData(c *gin.Context) {
|
||||
deviceId := c.Param("deviceId")
|
||||
sensorId := c.Param("sensorId")
|
||||
|
||||
// 获取设备
|
||||
dev, err := s.deviceManager.GetDevice(deviceId)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, ApiResponse{
|
||||
Status: "error",
|
||||
Error: fmt.Sprintf("设备 %s 不存在", deviceId),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证传感器是否存在
|
||||
sensorComponents := dev.GetComponents(device.SensorComponent)
|
||||
sensorExists := false
|
||||
for _, component := range sensorComponents {
|
||||
if component.GetID() == sensorId {
|
||||
sensorExists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !sensorExists {
|
||||
c.JSON(http.StatusNotFound, ApiResponse{
|
||||
Status: "error",
|
||||
Error: fmt.Sprintf("设备 %s 上不存在传感器 %s", deviceId, sensorId),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 读取传感器数据
|
||||
sensorData, err := dev.ReadSensorData(sensorId)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ApiResponse{
|
||||
Status: "error",
|
||||
Error: fmt.Sprintf("读取传感器 %s 数据失败:%v", sensorId, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为响应格式
|
||||
response := SensorDataResponse{
|
||||
SensorID: sensorData.SensorID(),
|
||||
Timestamp: sensorData.Timestamp(),
|
||||
Values: sensorData.Values(),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, ApiResponse{
|
||||
Status: "success",
|
||||
Data: response,
|
||||
})
|
||||
}
|
||||
|
||||
// handleGetDeviceStatus 获取设备状态
|
||||
func (s *Server) handleGetDeviceStatus(c *gin.Context) {
|
||||
deviceId := c.Param("deviceId")
|
||||
|
||||
// 获取设备
|
||||
dev, err := s.deviceManager.GetDevice(deviceId)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, ApiResponse{
|
||||
Status: "error",
|
||||
Error: fmt.Sprintf("设备 %s 不存在", deviceId),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取设备状态
|
||||
status, err := dev.GetStatus()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ApiResponse{
|
||||
Status: "error",
|
||||
Error: fmt.Sprintf("获取设备状态失败:%v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取动画引擎状态
|
||||
animEngine := dev.GetAnimationEngine()
|
||||
animationStatus := map[string]any{
|
||||
"isRunning": animEngine.IsRunning(),
|
||||
}
|
||||
|
||||
// 获取传感器组件数量
|
||||
sensorComponents := dev.GetComponents(device.SensorComponent)
|
||||
|
||||
// 构建详细的设备状态响应
|
||||
deviceInfo := DeviceInfo{
|
||||
ID: dev.GetID(),
|
||||
Model: dev.GetModel(),
|
||||
HandType: dev.GetHandType().String(),
|
||||
Status: status,
|
||||
}
|
||||
|
||||
// 扩展状态信息
|
||||
extendedStatus := map[string]any{
|
||||
"device": deviceInfo,
|
||||
"animation": animationStatus,
|
||||
"sensorCount": len(sensorComponents),
|
||||
"lastUpdate": status.LastUpdate,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, ApiResponse{
|
||||
Status: "success",
|
||||
Data: extendedStatus,
|
||||
})
|
||||
}
|
114
api/system_handlers.go
Normal file
114
api/system_handlers.go
Normal file
@ -0,0 +1,114 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"hands/device"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// handleGetSupportedModels 获取支持的设备型号
|
||||
func (s *Server) handleGetSupportedModels(c *gin.Context) {
|
||||
// 获取支持的设备型号列表
|
||||
models := device.GetSupportedModels()
|
||||
|
||||
response := SupportedModelsResponse{
|
||||
Models: models,
|
||||
Total: len(models),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, ApiResponse{
|
||||
Status: "success",
|
||||
Data: response,
|
||||
})
|
||||
}
|
||||
|
||||
// handleGetSystemStatus 获取系统状态
|
||||
func (s *Server) handleGetSystemStatus(c *gin.Context) {
|
||||
// 获取所有设备
|
||||
devices := s.deviceManager.GetAllDevices()
|
||||
|
||||
// 统计设备信息
|
||||
totalDevices := len(devices)
|
||||
activeDevices := 0
|
||||
deviceInfos := make(map[string]DeviceInfo)
|
||||
|
||||
for _, dev := range devices {
|
||||
status, err := dev.GetStatus()
|
||||
if err != nil {
|
||||
// 如果获取状态失败,使用默认状态
|
||||
status = device.DeviceStatus{
|
||||
IsConnected: false,
|
||||
IsActive: false,
|
||||
ErrorCount: 1,
|
||||
LastError: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
if status.IsActive {
|
||||
activeDevices++
|
||||
}
|
||||
|
||||
deviceInfo := DeviceInfo{
|
||||
ID: dev.GetID(),
|
||||
Model: dev.GetModel(),
|
||||
HandType: dev.GetHandType().String(),
|
||||
Status: status,
|
||||
}
|
||||
deviceInfos[dev.GetID()] = deviceInfo
|
||||
}
|
||||
|
||||
// 获取支持的设备型号
|
||||
supportedModels := device.GetSupportedModels()
|
||||
|
||||
// 计算系统运行时间
|
||||
uptime := time.Since(s.startTime)
|
||||
|
||||
response := SystemStatusResponse{
|
||||
TotalDevices: totalDevices,
|
||||
ActiveDevices: activeDevices,
|
||||
SupportedModels: supportedModels,
|
||||
Devices: deviceInfos,
|
||||
Uptime: uptime,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, ApiResponse{
|
||||
Status: "success",
|
||||
Data: response,
|
||||
})
|
||||
}
|
||||
|
||||
// handleHealthCheck 健康检查
|
||||
func (s *Server) handleHealthCheck(c *gin.Context) {
|
||||
// 执行基本的健康检查
|
||||
status := "healthy"
|
||||
|
||||
// 检查设备管理器是否正常
|
||||
if s.deviceManager == nil {
|
||||
status = "unhealthy"
|
||||
}
|
||||
|
||||
// 可以添加更多健康检查逻辑,比如:
|
||||
// - 检查关键服务是否可用
|
||||
// - 检查数据库连接
|
||||
// - 检查外部依赖
|
||||
|
||||
response := HealthResponse{
|
||||
Status: status,
|
||||
Timestamp: time.Now(),
|
||||
Version: s.version,
|
||||
}
|
||||
|
||||
// 根据健康状态返回相应的 HTTP 状态码
|
||||
httpStatus := http.StatusOK
|
||||
if status != "healthy" {
|
||||
httpStatus = http.StatusServiceUnavailable
|
||||
}
|
||||
|
||||
c.JSON(httpStatus, ApiResponse{
|
||||
Status: "success",
|
||||
Data: response,
|
||||
})
|
||||
}
|
@ -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 {
|
||||
|
131
communication/communicator.go
Normal file
131
communication/communicator.go
Normal file
@ -0,0 +1,131 @@
|
||||
package communication
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"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(ctx context.Context, 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(ctx context.Context, msg RawMessage) error {
|
||||
jsonData, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化消息失败:%w", err)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/api/can", c.serviceURL)
|
||||
|
||||
// 创建带有 context 的请求
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建 HTTP 请求失败:%w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
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
|
||||
}
|
78
component/pressure_sensor.go
Normal file
78
component/pressure_sensor.go
Normal file
@ -0,0 +1,78 @@
|
||||
package component
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"hands/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
|
||||
}
|
42
component/sensor.go
Normal file
42
component/sensor.go
Normal file
@ -0,0 +1,42 @@
|
||||
package component
|
||||
|
||||
import (
|
||||
"hands/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
|
||||
}
|
12
config/config.go
Normal file
12
config/config.go
Normal file
@ -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)
|
||||
}
|
@ -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"`
|
||||
}
|
||||
|
27
define/hands.go
Normal file
27
define/hands.go
Normal file
@ -0,0 +1,27 @@
|
||||
package define
|
||||
|
||||
type HandType int
|
||||
|
||||
const (
|
||||
HAND_TYPE_LEFT HandType = 0x28
|
||||
HAND_TYPE_RIGHT HandType = 0x27
|
||||
HAND_TYPE_UNKNOWN HandType = 0x00
|
||||
)
|
||||
|
||||
func (ht HandType) String() string {
|
||||
if ht == HAND_TYPE_LEFT {
|
||||
return "左手"
|
||||
}
|
||||
return "右手"
|
||||
}
|
||||
|
||||
func HandTypeFromString(s string) HandType {
|
||||
switch s {
|
||||
case "left":
|
||||
return HAND_TYPE_LEFT
|
||||
case "right":
|
||||
return HAND_TYPE_RIGHT
|
||||
default:
|
||||
return HAND_TYPE_UNKNOWN
|
||||
}
|
||||
}
|
12
device/animation.go
Normal file
12
device/animation.go
Normal file
@ -0,0 +1,12 @@
|
||||
package device
|
||||
|
||||
// Animation 定义了一个动画序列的行为
|
||||
type Animation interface {
|
||||
// Run 执行动画的一个周期或直到被停止
|
||||
// executor: 用于执行姿态指令
|
||||
// stop: 接收停止信号的通道
|
||||
// speedMs: 动画执行的速度(毫秒)
|
||||
Run(executor PoseExecutor, stop <-chan struct{}, speedMs int) error
|
||||
// Name 返回动画的名称
|
||||
Name() string
|
||||
}
|
80
device/commands.go
Normal file
80
device/commands.go
Normal file
@ -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
|
||||
}
|
69
device/device.go
Normal file
69
device/device.go
Normal file
@ -0,0 +1,69 @@
|
||||
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 // 断开设备连接
|
||||
|
||||
// --- 新增 ---
|
||||
PoseExecutor // 嵌入 PoseExecutor 接口,Device 需实现它
|
||||
GetAnimationEngine() *AnimationEngine // 获取设备的动画引擎
|
||||
|
||||
// --- 预设姿势相关方法 ---
|
||||
GetSupportedPresets() []string // 获取支持的预设姿势列表
|
||||
ExecutePreset(presetName string) error // 执行预设姿势
|
||||
GetPresetDescription(presetName string) string // 获取预设姿势描述
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
210
device/engine.go
Normal file
210
device/engine.go
Normal file
@ -0,0 +1,210 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// defaultAnimationSpeedMs 定义默认动画速度(毫秒)
|
||||
const defaultAnimationSpeedMs = 500
|
||||
|
||||
// AnimationEngine 管理和执行动画
|
||||
type AnimationEngine struct {
|
||||
executor PoseExecutor // 关联的姿态执行器
|
||||
animations map[string]Animation // 注册的动画
|
||||
stopChan chan struct{} // 当前动画的停止通道
|
||||
current string // 当前运行的动画名称
|
||||
isRunning bool // 是否有动画在运行
|
||||
engineMutex sync.Mutex // 保护引擎状态 (isRunning, current, stopChan)
|
||||
registerMutex sync.RWMutex // 保护动画注册表 (animations)
|
||||
}
|
||||
|
||||
// NewAnimationEngine 创建一个新的动画引擎
|
||||
func NewAnimationEngine(executor PoseExecutor) *AnimationEngine {
|
||||
return &AnimationEngine{
|
||||
executor: executor,
|
||||
animations: make(map[string]Animation),
|
||||
}
|
||||
}
|
||||
|
||||
// Register 注册一个动画
|
||||
func (e *AnimationEngine) Register(anim Animation) {
|
||||
e.registerMutex.Lock()
|
||||
defer e.registerMutex.Unlock()
|
||||
|
||||
if anim == nil {
|
||||
log.Printf("⚠️ 尝试注册一个空动画")
|
||||
return
|
||||
}
|
||||
|
||||
name := anim.Name()
|
||||
if _, exists := e.animations[name]; exists {
|
||||
log.Printf("⚠️ 动画 %s 已注册,将被覆盖", name)
|
||||
}
|
||||
e.animations[name] = anim
|
||||
log.Printf("✅ 动画 %s 已注册", name)
|
||||
}
|
||||
|
||||
// getAnimation 安全地获取一个已注册的动画
|
||||
func (e *AnimationEngine) getAnimation(name string) (Animation, bool) {
|
||||
e.registerMutex.RLock()
|
||||
defer e.registerMutex.RUnlock()
|
||||
anim, exists := e.animations[name]
|
||||
return anim, exists
|
||||
}
|
||||
|
||||
// getDeviceName 尝试获取设备 ID 用于日志记录
|
||||
func (e *AnimationEngine) getDeviceName() string {
|
||||
// 尝试通过接口断言获取 ID
|
||||
if idProvider, ok := e.executor.(interface{ GetID() string }); ok {
|
||||
return idProvider.GetID()
|
||||
}
|
||||
return "设备" // 默认名称
|
||||
}
|
||||
|
||||
// Start 启动一个动画
|
||||
func (e *AnimationEngine) Start(name string, speedMs int) error {
|
||||
e.engineMutex.Lock()
|
||||
defer e.engineMutex.Unlock() // 确保在任何情况下都释放锁
|
||||
|
||||
anim, exists := e.getAnimation(name)
|
||||
if !exists {
|
||||
return fmt.Errorf("❌ 动画 %s 未注册", name)
|
||||
}
|
||||
|
||||
// 如果有动画在运行,先发送停止信号
|
||||
if e.isRunning {
|
||||
log.Printf("ℹ️ 正在停止当前动画 %s 以启动 %s...", e.current, name)
|
||||
close(e.stopChan)
|
||||
// 注意:我们不在此处等待旧动画结束。
|
||||
// 新动画将立即启动,旧动画的 goroutine 在收到信号后会退出。
|
||||
// 其 defer 中的 `stopChan` 比较会确保它不会干扰新动画的状态。
|
||||
}
|
||||
|
||||
// 设置新动画状态
|
||||
e.stopChan = make(chan struct{}) // 创建新的停止通道
|
||||
e.isRunning = true
|
||||
e.current = name
|
||||
|
||||
// 验证并设置速度
|
||||
actualSpeedMs := speedMs
|
||||
if actualSpeedMs <= 0 {
|
||||
actualSpeedMs = defaultAnimationSpeedMs
|
||||
}
|
||||
|
||||
log.Printf("🚀 准备启动动画 %s (设备: %s, 速度: %dms)", name, e.getDeviceName(), actualSpeedMs)
|
||||
|
||||
// 启动动画 goroutine
|
||||
go e.runAnimationLoop(anim, e.stopChan, actualSpeedMs)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop 停止当前正在运行的动画
|
||||
func (e *AnimationEngine) Stop() error {
|
||||
e.engineMutex.Lock()
|
||||
defer e.engineMutex.Unlock()
|
||||
|
||||
if !e.isRunning {
|
||||
log.Printf("ℹ️ 当前没有动画在运行 (设备: %s)", e.getDeviceName())
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf("⏳ 正在发送停止信号给动画 %s (设备: %s)...", e.current, e.getDeviceName())
|
||||
close(e.stopChan) // 发送停止信号
|
||||
e.isRunning = false // 立即标记为未运行,防止重复停止
|
||||
e.current = ""
|
||||
// 动画的 goroutine 将在下一次检查通道时退出,
|
||||
// 并在其 defer 块中执行最终的清理(包括 ResetPose)。
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsRunning 检查是否有动画在运行
|
||||
func (e *AnimationEngine) IsRunning() bool {
|
||||
e.engineMutex.Lock()
|
||||
defer e.engineMutex.Unlock()
|
||||
return e.isRunning
|
||||
}
|
||||
|
||||
// GetRegisteredAnimations 获取已注册的动画名称列表
|
||||
func (e *AnimationEngine) GetRegisteredAnimations() []string {
|
||||
e.registerMutex.RLock()
|
||||
defer e.registerMutex.RUnlock()
|
||||
|
||||
animations := make([]string, 0, len(e.animations))
|
||||
for name := range e.animations {
|
||||
animations = append(animations, name)
|
||||
}
|
||||
return animations
|
||||
}
|
||||
|
||||
// GetCurrentAnimation 获取当前运行的动画名称
|
||||
func (e *AnimationEngine) GetCurrentAnimation() string {
|
||||
e.engineMutex.Lock()
|
||||
defer e.engineMutex.Unlock()
|
||||
return e.current
|
||||
}
|
||||
|
||||
// runAnimationLoop 是动画执行的核心循环,在单独的 Goroutine 中运行。
|
||||
func (e *AnimationEngine) runAnimationLoop(anim Animation, stopChan <-chan struct{}, speedMs int) {
|
||||
deviceName := e.getDeviceName()
|
||||
animName := anim.Name()
|
||||
|
||||
// 使用 defer 确保无论如何都能执行清理逻辑
|
||||
defer e.handleLoopExit(stopChan, deviceName, animName)
|
||||
|
||||
log.Printf("▶️ %s 动画 %s 已启动", deviceName, animName)
|
||||
|
||||
// 动画主循环
|
||||
for {
|
||||
select {
|
||||
case <-stopChan:
|
||||
log.Printf("🛑 %s 动画 %s 被显式停止", deviceName, animName)
|
||||
return // 接收到停止信号,退出循环
|
||||
default:
|
||||
// 执行一轮动画
|
||||
err := anim.Run(e.executor, stopChan, speedMs)
|
||||
if err != nil {
|
||||
log.Printf("❌ %s 动画 %s 执行出错: %v", deviceName, animName, err)
|
||||
return // 出错则退出
|
||||
}
|
||||
|
||||
// 再次检查停止信号,防止 Run 结束后才收到信号
|
||||
select {
|
||||
case <-stopChan:
|
||||
log.Printf("🛑 %s 动画 %s 在周期结束时被停止", deviceName, animName)
|
||||
return
|
||||
default:
|
||||
// 继续下一个循环
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleLoopExit 是动画 Goroutine 退出时执行的清理函数。
|
||||
func (e *AnimationEngine) handleLoopExit(stopChan <-chan struct{}, deviceName, animName string) {
|
||||
e.engineMutex.Lock()
|
||||
defer e.engineMutex.Unlock()
|
||||
|
||||
// --- 关键并发控制 ---
|
||||
// 检查当前引擎的 stopChan 是否与此 Goroutine 启动时的 stopChan 相同。
|
||||
// 如果不相同,说明一个新的动画已经启动,并且接管了引擎状态。
|
||||
// 这种情况下,旧的 Goroutine 不应该修改引擎状态或重置姿态,
|
||||
// 以避免干扰新动画。
|
||||
if stopChan == e.stopChan {
|
||||
// 只有当自己仍然是"活跃"的动画时,才更新状态并重置姿态
|
||||
e.isRunning = false
|
||||
e.current = ""
|
||||
log.Printf("👋 %s 动画 %s 已完成或停止,正在重置姿态...", deviceName, animName)
|
||||
if err := e.executor.ResetPose(); err != nil {
|
||||
log.Printf("⚠️ %s 动画结束后重置姿态失败: %v", deviceName, err)
|
||||
} else {
|
||||
log.Printf("✅ %s 姿态已重置", deviceName)
|
||||
}
|
||||
} else {
|
||||
// 如果 stopChan 不同,说明自己是旧的 Goroutine,只需安静退出
|
||||
log.Printf("ℹ️ 旧的 %s 动画 %s goroutine 退出,但新动画已启动,无需重置。", deviceName, animName)
|
||||
}
|
||||
}
|
35
device/factory.go
Normal file
35
device/factory.go
Normal file
@ -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
|
||||
}
|
63
device/manager.go
Normal file
63
device/manager.go
Normal file
@ -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
|
||||
}
|
8
device/models/init.go
Normal file
8
device/models/init.go
Normal file
@ -0,0 +1,8 @@
|
||||
package models
|
||||
|
||||
import "hands/device"
|
||||
|
||||
func RegisterDeviceTypes() {
|
||||
// 注册 L10 设备类型
|
||||
device.RegisterDeviceType("L10", NewL10Hand)
|
||||
}
|
380
device/models/l10.go
Normal file
380
device/models/l10.go
Normal file
@ -0,0 +1,380 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand/v2"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"hands/communication"
|
||||
"hands/component"
|
||||
"hands/define"
|
||||
"hands/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"
|
||||
animationEngine *device.AnimationEngine // 动画引擎
|
||||
presetManager *device.PresetManager // 预设姿势管理器
|
||||
}
|
||||
|
||||
// 在 base 基础上进行 ±delta 的扰动,范围限制在 [0, 255]
|
||||
func perturb(base byte, delta int) byte {
|
||||
offset := rand.IntN(2*delta+1) - delta
|
||||
v := min(max(int(base)+offset, 0), 255)
|
||||
return byte(v)
|
||||
}
|
||||
|
||||
// NewL10Hand 创建 L10 手部设备实例
|
||||
// 参数 config 是设备配置,包含以下字段:
|
||||
// - id: 设备 ID
|
||||
// - can_service_url: CAN 服务 URL
|
||||
// - can_interface: CAN 接口名称,如 "can0"
|
||||
// - hand_type: 手型,可选值为 "left" 或 "right",默认值为 "right"
|
||||
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" // 默认接口
|
||||
}
|
||||
|
||||
handTypeStr, ok := config["hand_type"].(string)
|
||||
handType := define.HAND_TYPE_RIGHT // 默认右手
|
||||
if ok && handTypeStr == "left" {
|
||||
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{
|
||||
// TODO: 这里需要修改,根据实际连接情况设置,因为当前还没有实现连接和断开路由,先设置为 true
|
||||
IsConnected: true,
|
||||
IsActive: true,
|
||||
LastUpdate: time.Now(),
|
||||
},
|
||||
}
|
||||
|
||||
// 初始化动画引擎,将 hand 自身作为 PoseExecutor
|
||||
hand.animationEngine = device.NewAnimationEngine(hand)
|
||||
|
||||
// 注册默认动画
|
||||
hand.animationEngine.Register(NewL10WaveAnimation())
|
||||
hand.animationEngine.Register(NewL10SwayAnimation())
|
||||
|
||||
// 初始化预设姿势管理器
|
||||
hand.presetManager = device.NewPresetManager()
|
||||
|
||||
// 注册 L10 的预设姿势
|
||||
for _, preset := range GetL10Presets() {
|
||||
hand.presetManager.RegisterPreset(preset)
|
||||
}
|
||||
|
||||
// 初始化组件
|
||||
if err := hand.initializeComponents(config); err != nil {
|
||||
return nil, fmt.Errorf("初始化组件失败:%w", err)
|
||||
}
|
||||
|
||||
log.Printf("✅ 设备 L10 (%s, %s) 创建成功", id, handType.String())
|
||||
return hand, nil
|
||||
}
|
||||
|
||||
// GetHandType 获取设备手型
|
||||
func (h *L10Hand) GetHandType() define.HandType {
|
||||
h.mutex.RLock()
|
||||
defer h.mutex.RUnlock()
|
||||
return h.handType
|
||||
}
|
||||
|
||||
// SetHandType 设置设备手型
|
||||
func (h *L10Hand) SetHandType(handType define.HandType) error {
|
||||
h.mutex.Lock()
|
||||
defer h.mutex.Unlock()
|
||||
if handType != define.HAND_TYPE_LEFT && handType != define.HAND_TYPE_RIGHT {
|
||||
return fmt.Errorf("无效的手型:%d", handType)
|
||||
}
|
||||
h.handType = handType
|
||||
log.Printf("🔧 设备 %s 手型已更新: %s", h.id, handType.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAnimationEngine 获取动画引擎
|
||||
func (h *L10Hand) GetAnimationEngine() *device.AnimationEngine {
|
||||
return h.animationEngine
|
||||
}
|
||||
|
||||
// SetFingerPose 设置手指姿态 (实现 PoseExecutor)
|
||||
func (h *L10Hand) SetFingerPose(pose []byte) error {
|
||||
if len(pose) != 6 {
|
||||
return fmt.Errorf("无效的手指姿态数据长度,需要 6 个字节")
|
||||
}
|
||||
|
||||
// 添加随机扰动
|
||||
perturbedPose := make([]byte, len(pose))
|
||||
for i, v := range pose {
|
||||
perturbedPose[i] = perturb(v, 5)
|
||||
}
|
||||
|
||||
// 创建指令
|
||||
cmd := device.NewFingerPoseCommand("all", perturbedPose)
|
||||
|
||||
// 执行指令
|
||||
err := h.ExecuteCommand(cmd)
|
||||
if err == nil {
|
||||
log.Printf("✅ %s (%s) 手指动作已发送: [%X %X %X %X %X %X]",
|
||||
h.id, h.GetHandType().String(), perturbedPose[0], perturbedPose[1], perturbedPose[2],
|
||||
perturbedPose[3], perturbedPose[4], perturbedPose[5])
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// SetPalmPose 设置手掌姿态 (实现 PoseExecutor)
|
||||
func (h *L10Hand) SetPalmPose(pose []byte) error {
|
||||
if len(pose) != 4 {
|
||||
return fmt.Errorf("无效的手掌姿态数据长度,需要 4 个字节")
|
||||
}
|
||||
|
||||
// 添加随机扰动
|
||||
perturbedPose := make([]byte, len(pose))
|
||||
for i, v := range pose {
|
||||
perturbedPose[i] = perturb(v, 8)
|
||||
}
|
||||
|
||||
// 创建指令
|
||||
cmd := device.NewPalmPoseCommand(perturbedPose)
|
||||
|
||||
// 执行指令
|
||||
err := h.ExecuteCommand(cmd)
|
||||
if err == nil {
|
||||
log.Printf("✅ %s (%s) 掌部姿态已发送: [%X %X %X %X]",
|
||||
h.id, h.GetHandType().String(), perturbedPose[0], perturbedPose[1], perturbedPose[2], perturbedPose[3])
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// ResetPose 重置到默认姿态 (实现 PoseExecutor)
|
||||
func (h *L10Hand) ResetPose() error {
|
||||
log.Printf("🔄 正在重置设备 %s (%s) 到默认姿态...", h.id, h.GetHandType().String())
|
||||
defaultFingerPose := []byte{64, 64, 64, 64, 64, 64} // 0x40 - 半开
|
||||
defaultPalmPose := []byte{128, 128, 128, 128} // 0x80 - 居中
|
||||
|
||||
if err := h.SetFingerPose(defaultFingerPose); err != nil {
|
||||
log.Printf("❌ %s 重置手指姿势失败: %v", h.id, err)
|
||||
return err
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond) // 短暂延时
|
||||
if err := h.SetPalmPose(defaultPalmPose); err != nil {
|
||||
log.Printf("❌ %s 重置掌部姿势失败: %v", h.id, err)
|
||||
return err
|
||||
}
|
||||
log.Printf("✅ 设备 %s 已重置到默认姿态", h.id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// commandToRawMessage 将通用指令转换为 L10 特定的 CAN 消息
|
||||
func (h *L10Hand) commandToRawMessage(cmd device.Command) (communication.RawMessage, error) {
|
||||
h.mutex.RLock()
|
||||
defer h.mutex.RUnlock()
|
||||
|
||||
var data []byte
|
||||
canID := uint32(h.handType)
|
||||
|
||||
switch cmd.Type() {
|
||||
case "SetFingerPose":
|
||||
// 添加 0x01 前缀
|
||||
data = append([]byte{0x01}, cmd.Payload()...)
|
||||
if len(data) > 8 { // CAN 消息数据长度限制
|
||||
return communication.RawMessage{}, fmt.Errorf("手指姿态数据过长")
|
||||
}
|
||||
case "SetPalmPose":
|
||||
// 添加 0x04 前缀
|
||||
data = append([]byte{0x04}, cmd.Payload()...)
|
||||
if len(data) > 8 { // CAN 消息数据长度限制
|
||||
return communication.RawMessage{}, fmt.Errorf("手掌姿态数据过长")
|
||||
}
|
||||
default:
|
||||
return communication.RawMessage{}, fmt.Errorf("L10 不支持的指令类型: %s", cmd.Type())
|
||||
}
|
||||
|
||||
return communication.RawMessage{
|
||||
Interface: h.canInterface,
|
||||
ID: canID,
|
||||
Data: data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ExecuteCommand 执行一个通用指令
|
||||
func (h *L10Hand) ExecuteCommand(cmd device.Command) error {
|
||||
h.mutex.Lock() // 使用写锁,因为会更新状态
|
||||
defer h.mutex.Unlock()
|
||||
|
||||
if !h.status.IsConnected || !h.status.IsActive {
|
||||
return fmt.Errorf("设备 %s 未连接或未激活", h.id)
|
||||
}
|
||||
|
||||
// 转换指令为 CAN 消息
|
||||
rawMsg, err := h.commandToRawMessage(cmd)
|
||||
if err != nil {
|
||||
h.status.ErrorCount++
|
||||
h.status.LastError = err.Error()
|
||||
return fmt.Errorf("转换指令失败:%w", err)
|
||||
}
|
||||
|
||||
// 创建带有超时的 context,设置 3 秒超时
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 发送到 can-bridge 服务
|
||||
if err := h.communicator.SendMessage(ctx, rawMsg); err != nil {
|
||||
h.status.ErrorCount++
|
||||
h.status.LastError = err.Error()
|
||||
log.Printf("❌ %s (%s) 发送指令失败: %v (ID: 0x%X, Data: %X)", h.id, h.handType.String(), err, rawMsg.ID, rawMsg.Data)
|
||||
return fmt.Errorf("发送指令失败:%w", err)
|
||||
}
|
||||
|
||||
h.status.LastUpdate = time.Now()
|
||||
// 成功的日志记录移到 SetFingerPose 和 SetPalmPose 中,因为那里有更详细的信息
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- 其他 L10Hand 方法 (initializeComponents, GetID, GetModel, ReadSensorData, etc.) 保持不变 ---
|
||||
// --- 确保它们存在且与您上传的版本一致 ---
|
||||
|
||||
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) 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()
|
||||
|
||||
// TODO: 假设连接总是成功,除非有显式错误
|
||||
h.status.IsConnected = true
|
||||
h.status.IsActive = true
|
||||
h.status.LastUpdate = time.Now()
|
||||
log.Printf("🔗 设备 %s 已连接", h.id)
|
||||
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()
|
||||
log.Printf("🔌 设备 %s 已断开", h.id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- 预设姿势相关方法 ---
|
||||
|
||||
// GetSupportedPresets 获取支持的预设姿势列表
|
||||
func (h *L10Hand) GetSupportedPresets() []string { return h.presetManager.GetSupportedPresets() }
|
||||
|
||||
// ExecutePreset 执行预设姿势
|
||||
func (h *L10Hand) ExecutePreset(presetName string) error {
|
||||
preset, exists := h.presetManager.GetPreset(presetName)
|
||||
if !exists {
|
||||
return fmt.Errorf("预设姿势 '%s' 不存在", presetName)
|
||||
}
|
||||
|
||||
log.Printf("🎯 设备 %s (%s) 执行预设姿势: %s", h.id, h.GetHandType().String(), presetName)
|
||||
|
||||
// 执行手指姿态
|
||||
if err := h.SetFingerPose(preset.FingerPose); err != nil {
|
||||
return fmt.Errorf("执行预设姿势 '%s' 的手指姿态失败: %w", presetName, err)
|
||||
}
|
||||
|
||||
// 如果有手掌姿态数据,也执行
|
||||
if len(preset.PalmPose) > 0 {
|
||||
time.Sleep(20 * time.Millisecond) // 短暂延时
|
||||
if err := h.SetPalmPose(preset.PalmPose); err != nil {
|
||||
return fmt.Errorf("执行预设姿势 '%s' 的手掌姿态失败: %w", presetName, err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("✅ 设备 %s 预设姿势 '%s' 执行完成", h.id, presetName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPresetDescription 获取预设姿势描述
|
||||
func (h *L10Hand) GetPresetDescription(presetName string) string {
|
||||
return h.presetManager.GetPresetDescription(presetName)
|
||||
}
|
125
device/models/l10_animation.go
Normal file
125
device/models/l10_animation.go
Normal file
@ -0,0 +1,125 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"hands/device"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
// --- L10WaveAnimation ---
|
||||
|
||||
// L10WaveAnimation 实现 L10 的波浪动画
|
||||
type L10WaveAnimation struct{}
|
||||
|
||||
// NewL10WaveAnimation 创建 L10 波浪动画实例
|
||||
func NewL10WaveAnimation() *L10WaveAnimation { return &L10WaveAnimation{} }
|
||||
|
||||
func (w *L10WaveAnimation) Name() string { return "wave" }
|
||||
|
||||
func (w *L10WaveAnimation) Run(executor device.PoseExecutor, stop <-chan struct{}, speedMs int) error {
|
||||
fingerOrder := []int{0, 1, 2, 3, 4, 5}
|
||||
open := byte(64) // 0x40
|
||||
close := byte(192) // 0xC0
|
||||
delay := time.Duration(speedMs) * time.Millisecond
|
||||
|
||||
deviceName := "L10"
|
||||
|
||||
// 波浪张开
|
||||
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 := executor.SetFingerPose(pose); err != nil {
|
||||
log.Printf("❌ %s 动画 %s 发送失败: %v", deviceName, w.Name(), err)
|
||||
return err
|
||||
}
|
||||
|
||||
select {
|
||||
case <-stop:
|
||||
return nil // 动画被停止
|
||||
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 := executor.SetFingerPose(pose); err != nil {
|
||||
log.Printf("❌ %s 动画 %s 发送失败: %v", deviceName, w.Name(), err)
|
||||
return err
|
||||
}
|
||||
|
||||
select {
|
||||
case <-stop:
|
||||
return nil // 动画被停止
|
||||
case <-time.After(delay):
|
||||
// 继续
|
||||
}
|
||||
}
|
||||
|
||||
return nil // 完成一个周期
|
||||
}
|
||||
|
||||
// --- L10SwayAnimation ---
|
||||
|
||||
// L10SwayAnimation 实现 L10 的横向摆动动画
|
||||
type L10SwayAnimation struct{}
|
||||
|
||||
// NewL10SwayAnimation 创建 L10 摆动动画实例
|
||||
func NewL10SwayAnimation() *L10SwayAnimation { return &L10SwayAnimation{} }
|
||||
|
||||
func (s *L10SwayAnimation) Name() string { return "sway" }
|
||||
|
||||
func (s *L10SwayAnimation) Run(executor device.PoseExecutor, stop <-chan struct{}, speedMs int) error {
|
||||
leftPose := []byte{48, 48, 48, 48} // 0x30
|
||||
rightPose := []byte{208, 208, 208, 208} // 0xD0
|
||||
delay := time.Duration(speedMs) * time.Millisecond
|
||||
|
||||
deviceName := "L10"
|
||||
if idProvider, ok := executor.(interface{ GetID() string }); ok {
|
||||
deviceName = idProvider.GetID()
|
||||
}
|
||||
|
||||
// 向左移动
|
||||
if err := executor.SetPalmPose(leftPose); err != nil {
|
||||
log.Printf("❌ %s 动画 %s 发送失败: %v", deviceName, s.Name(), err)
|
||||
return err
|
||||
}
|
||||
|
||||
select {
|
||||
case <-stop:
|
||||
return nil // 动画被停止
|
||||
case <-time.After(delay):
|
||||
// 继续
|
||||
}
|
||||
|
||||
// 向右移动
|
||||
if err := executor.SetPalmPose(rightPose); err != nil {
|
||||
log.Printf("❌ %s 动画 %s 发送失败: %v", deviceName, s.Name(), err)
|
||||
return err
|
||||
}
|
||||
|
||||
select {
|
||||
case <-stop:
|
||||
return nil // 动画被停止
|
||||
case <-time.After(delay):
|
||||
// 继续
|
||||
}
|
||||
|
||||
return nil // 完成一个周期
|
||||
}
|
82
device/models/l10_presets.go
Normal file
82
device/models/l10_presets.go
Normal file
@ -0,0 +1,82 @@
|
||||
package models
|
||||
|
||||
import "hands/device"
|
||||
|
||||
// GetL10Presets 获取 L10 设备的所有预设姿势
|
||||
func GetL10Presets() []device.PresetPose {
|
||||
return []device.PresetPose{
|
||||
// 基础姿势
|
||||
{
|
||||
Name: "fist",
|
||||
Description: "握拳姿势",
|
||||
FingerPose: []byte{64, 64, 64, 64, 64, 64},
|
||||
},
|
||||
{
|
||||
Name: "open",
|
||||
Description: "完全张开姿势",
|
||||
FingerPose: []byte{192, 192, 192, 192, 192, 192},
|
||||
},
|
||||
{
|
||||
Name: "pinch",
|
||||
Description: "捏取姿势",
|
||||
FingerPose: []byte{120, 120, 64, 64, 64, 64},
|
||||
},
|
||||
{
|
||||
Name: "thumbsup",
|
||||
Description: "竖起大拇指姿势",
|
||||
FingerPose: []byte{64, 192, 192, 192, 192, 64},
|
||||
},
|
||||
{
|
||||
Name: "point",
|
||||
Description: "食指指点姿势",
|
||||
FingerPose: []byte{192, 64, 192, 192, 192, 64},
|
||||
},
|
||||
|
||||
// 数字手势
|
||||
{
|
||||
Name: "1",
|
||||
Description: "数字 1 手势",
|
||||
FingerPose: []byte{192, 64, 192, 192, 192, 64},
|
||||
},
|
||||
{
|
||||
Name: "2",
|
||||
Description: "数字 2 手势",
|
||||
FingerPose: []byte{192, 64, 64, 192, 192, 64},
|
||||
},
|
||||
{
|
||||
Name: "3",
|
||||
Description: "数字 3 手势",
|
||||
FingerPose: []byte{192, 64, 64, 64, 192, 64},
|
||||
},
|
||||
{
|
||||
Name: "4",
|
||||
Description: "数字 4 手势",
|
||||
FingerPose: []byte{192, 64, 64, 64, 64, 64},
|
||||
},
|
||||
{
|
||||
Name: "5",
|
||||
Description: "数字 5 手势",
|
||||
FingerPose: []byte{192, 192, 192, 192, 192, 192},
|
||||
},
|
||||
{
|
||||
Name: "6",
|
||||
Description: "数字 6 手势",
|
||||
FingerPose: []byte{64, 192, 192, 192, 192, 64},
|
||||
},
|
||||
{
|
||||
Name: "7",
|
||||
Description: "数字 7 手势",
|
||||
FingerPose: []byte{64, 64, 192, 192, 192, 64},
|
||||
},
|
||||
{
|
||||
Name: "8",
|
||||
Description: "数字 8 手势",
|
||||
FingerPose: []byte{64, 64, 64, 192, 192, 64},
|
||||
},
|
||||
{
|
||||
Name: "9",
|
||||
Description: "数字 9 手势",
|
||||
FingerPose: []byte{64, 64, 64, 64, 192, 64},
|
||||
},
|
||||
}
|
||||
}
|
20
device/pose_executor.go
Normal file
20
device/pose_executor.go
Normal file
@ -0,0 +1,20 @@
|
||||
package device
|
||||
|
||||
import "hands/define"
|
||||
|
||||
// PoseExecutor 定义了执行基本姿态指令的能力
|
||||
type PoseExecutor interface {
|
||||
// SetFingerPose 设置手指姿态
|
||||
// pose: 6 字节数据,代表 6 个手指的位置
|
||||
SetFingerPose(pose []byte) error
|
||||
|
||||
// SetPalmPose 设置手掌姿态
|
||||
// pose: 4 字节数据,代表手掌的 4 个自由度
|
||||
SetPalmPose(pose []byte) error
|
||||
|
||||
// ResetPose 重置到默认姿态
|
||||
ResetPose() error
|
||||
|
||||
// GetHandType 获取当前手型
|
||||
GetHandType() define.HandType
|
||||
}
|
45
device/preset.go
Normal file
45
device/preset.go
Normal file
@ -0,0 +1,45 @@
|
||||
package device
|
||||
|
||||
// PresetPose 定义预设姿势的结构
|
||||
type PresetPose struct {
|
||||
Name string // 姿势名称
|
||||
Description string // 姿势描述
|
||||
FingerPose []byte // 手指姿态数据
|
||||
PalmPose []byte // 手掌姿态数据(可选)
|
||||
}
|
||||
|
||||
// PresetManager 预设姿势管理器
|
||||
type PresetManager struct{ presets map[string]PresetPose }
|
||||
|
||||
// NewPresetManager 创建新的预设姿势管理器
|
||||
func NewPresetManager() *PresetManager {
|
||||
return &PresetManager{
|
||||
presets: make(map[string]PresetPose),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterPreset 注册一个预设姿势
|
||||
func (pm *PresetManager) RegisterPreset(preset PresetPose) { pm.presets[preset.Name] = preset }
|
||||
|
||||
// GetPreset 获取指定名称的预设姿势
|
||||
func (pm *PresetManager) GetPreset(name string) (PresetPose, bool) {
|
||||
preset, exists := pm.presets[name]
|
||||
return preset, exists
|
||||
}
|
||||
|
||||
// GetSupportedPresets 获取所有支持的预设姿势名称列表
|
||||
func (pm *PresetManager) GetSupportedPresets() []string {
|
||||
presets := make([]string, 0, len(pm.presets))
|
||||
for name := range pm.presets {
|
||||
presets = append(presets, name)
|
||||
}
|
||||
return presets
|
||||
}
|
||||
|
||||
// GetPresetDescription 获取预设姿势的描述
|
||||
func (pm *PresetManager) GetPresetDescription(name string) string {
|
||||
if preset, exists := pm.presets[name]; exists {
|
||||
return preset.Description
|
||||
}
|
||||
return ""
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
# Dexterous Hand Dashboard 项目文档
|
||||
|
||||
[贡献指南](./contribute_CN.md)
|
||||
|
||||
## 项目概述
|
||||
|
||||
**Dexterous Hand Dashboard** 是专为 LinkerHand 灵巧手设备开发的控制仪表盘服务。该服务基于 Golang 开发,提供灵活的 RESTful API 接口,可实现手指与掌部姿态控制、预设动作执行及实时传感器数据监控,并支持动态配置手型(左手或右手)及 CAN 接口。
|
||||
@ -8,7 +10,7 @@
|
||||
|
||||
* **动态手型配置**:支持左手和右手手型的动态切换。
|
||||
* **灵活接口配置**:支持多种 CAN 接口(如 `can0`, `can1`),可通过命令行参数或环境变量动态设置。
|
||||
* **手指与掌部姿态控制**:提供手指(6字节)和掌部(4字节)姿态数据发送功能。
|
||||
* **手指与掌部姿态控制**:提供手指(6 字节)和掌部(4 字节)姿态数据发送功能。
|
||||
* **预设动作执行**:内置丰富的手势动作,如握拳、张开、捏取、点赞、数字手势等。
|
||||
* **实时动画控制**:支持波浪、横向摆动等动画效果,用户可动态启动和停止。
|
||||
* **传感器数据实时监控**:提供接口压力数据的实时模拟和更新。
|
623
docs/contribute_CN.md
Normal file
623
docs/contribute_CN.md
Normal file
@ -0,0 +1,623 @@
|
||||
# 当前架构详解
|
||||
|
||||
## 设备抽象层 (device 包)
|
||||
|
||||
目标:统一不同型号设备的操作接口,屏蔽底层硬件差异(主要体现在指令到 RawMessage 的转换和设备特定功能的实现上)。
|
||||
|
||||
核心接口与结构体:
|
||||
|
||||
**Device 接口 (device/device.go): 代表一个可控制的设备单元。**
|
||||
|
||||
```go
|
||||
type Device interface {
|
||||
GetID() string
|
||||
GetModel() string
|
||||
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
|
||||
|
||||
PoseExecutor // 嵌入 PoseExecutor 接口
|
||||
GetAnimationEngine() *AnimationEngine
|
||||
|
||||
GetSupportedPresets() []string
|
||||
ExecutePreset(presetName string) error
|
||||
GetPresetDescription(presetName string) string
|
||||
}
|
||||
```
|
||||
|
||||
**PoseExecutor 接口 (device/pose_executor.go): 定义了执行基本姿态指令的能力。**
|
||||
|
||||
```go
|
||||
type PoseExecutor interface {
|
||||
SetFingerPose(pose []byte) error
|
||||
SetPalmPose(pose []byte) error
|
||||
ResetPose() error
|
||||
GetHandType() define.HandType
|
||||
}
|
||||
```
|
||||
|
||||
**Command 接口 (device/device.go): 代表一个发送给设备的指令。**
|
||||
|
||||
```go
|
||||
type Command interface {
|
||||
Type() string
|
||||
Payload() []byte
|
||||
TargetComponent() string // 目标组件 ID
|
||||
}
|
||||
```
|
||||
|
||||
具体指令实现位于 device/commands.go,如 FingerPoseCommand, PalmPoseCommand, GenericCommand。
|
||||
|
||||
**SensorData 接口 (device/device.go): 代表从传感器读取的数据。**
|
||||
|
||||
```go
|
||||
type SensorData interface {
|
||||
Timestamp() time.Time
|
||||
Values() map[string]any
|
||||
SensorID() string
|
||||
}
|
||||
```
|
||||
|
||||
**ComponentType (device/device.go): 定义组件类型。**
|
||||
|
||||
```go
|
||||
const (
|
||||
SensorComponent ComponentType = "sensor"
|
||||
SkinComponent ComponentType = "skin" // 示例,可扩展
|
||||
ActuatorComponent ComponentType = "actuator" // 示例,可扩展
|
||||
)
|
||||
```
|
||||
|
||||
**Component 接口 (device/device.go): 代表设备的一个可插拔组件。**
|
||||
|
||||
```go
|
||||
type Component interface {
|
||||
GetID() string
|
||||
GetType() ComponentType
|
||||
GetConfiguration() map[string]interface{}
|
||||
IsActive() bool
|
||||
}
|
||||
```
|
||||
|
||||
**具体设备型号实现 (如 device/models/l10.go 中的 L10Hand):**
|
||||
|
||||
1. 实现 Device 和 PoseExecutor 接口。
|
||||
2. 管理内部的 AnimationEngine 和 PresetManager。
|
||||
3. 包含将通用 Command 转换为发送给 can-bridge 的 RawMessage 的逻辑 (如 commandToRawMessage 方法)。
|
||||
4. 管理其配备的传感器等组件 (initializeComponents 方法)。
|
||||
|
||||
**DeviceManager (device/manager.go): 用于注册、发现和管理可用的设备实例。**
|
||||
|
||||
```go
|
||||
type DeviceManager struct { /* ... */ }
|
||||
func NewDeviceManager() *DeviceManager { /* ... */ }
|
||||
func (m *DeviceManager) RegisterDevice(dev Device) error { /* ... */ }
|
||||
func (m *DeviceManager) GetDevice(id string) (Device, error) { /* ... */ }
|
||||
```
|
||||
|
||||
## 组件化设计 (component 包)
|
||||
|
||||
目标:将“皮肤”、“传感器”等视为可配置、可替换的组件。
|
||||
|
||||
核心接口与结构体:
|
||||
|
||||
**传感器组件 (Sensor):**
|
||||
|
||||
component/sensor.go 中定义了通用的 Sensor 接口 (嵌入了 device.Component)。
|
||||
|
||||
```go
|
||||
type Sensor interface {
|
||||
device.Component
|
||||
ReadData() (device.SensorData, error)
|
||||
GetDataType() string
|
||||
GetSamplingRate() int
|
||||
SetSamplingRate(rate int) error
|
||||
}
|
||||
```
|
||||
|
||||
具体的传感器实现,如 component/component.go 中的 PressureSensor,实现了 Sensor 接口。
|
||||
|
||||
传感器数据的实际获取方式(模拟、通过 can-bridge 的特定端点,或完全独立的数据源)在具体的 Sensor 组件实现中处理。
|
||||
|
||||
SensorDataImpl (component/sensor.go) 是 device.SensorData 的一个具体实现。
|
||||
|
||||
皮肤组件 (Skin) 及其他组件:
|
||||
|
||||
如果“皮肤”影响设备的物理特性或参数范围,可以将其抽象为一个 Skin 组件,实现 device.Component 接口。
|
||||
|
||||
设备可以关联多个不同类型的组件,并在其 initializeComponents 方法中进行初始化。
|
||||
|
||||
## 动画与姿态控制
|
||||
|
||||
目标:提供灵活的动画播放和直接的姿态控制能力,与具体设备和通信方式解耦。
|
||||
|
||||
**AnimationEngine (device/engine.go):**
|
||||
|
||||
每个设备实例拥有一个 AnimationEngine。
|
||||
|
||||
负责注册、启动、停止和管理动画的生命周期。
|
||||
|
||||
**使用 PoseExecutor 来执行动画中的姿态变化。**
|
||||
|
||||
```go
|
||||
type AnimationEngine struct { /* ... */ }
|
||||
func NewAnimationEngine(executor PoseExecutor) *AnimationEngine { /* ... */ }
|
||||
func (e *AnimationEngine) Register(anim Animation) { /* ... */ }
|
||||
func (e *AnimationEngine) Start(name string, speedMs int) error { /* ... */ }
|
||||
func (e *AnimationEngine) Stop() error { /* ... */ }
|
||||
```
|
||||
|
||||
Animation 接口 (device/animation.go): 定义了动画的行为。
|
||||
|
||||
```go
|
||||
type Animation interface {
|
||||
Run(executor PoseExecutor, stop <-chan struct{}, speedMs int) error
|
||||
Name() string
|
||||
}
|
||||
```
|
||||
|
||||
具体的动画实现与设备型号绑定,例如 device/models/l10_animation.go 中的 L10WaveAnimation。
|
||||
|
||||
直接姿态控制:
|
||||
|
||||
通过设备实例直接调用其实现的 PoseExecutor 接口方法 (SetFingerPose, SetPalmPose, ResetPose)。
|
||||
|
||||
或者通过构造 FingerPoseCommand 或 PalmPoseCommand,然后调用 device.ExecuteCommand()。
|
||||
|
||||
预设姿势 (PresetManager - device/preset.go):
|
||||
|
||||
每个设备实例拥有一个 PresetManager。
|
||||
|
||||
负责注册和管理预设姿势 (PresetPose 结构体)。
|
||||
|
||||
Device 接口提供了 GetSupportedPresets, ExecutePreset, GetPresetDescription 方法与预设姿势交互。
|
||||
|
||||
## 通信层抽象 (communication 包)
|
||||
|
||||
目标:将与 can-bridge Web 服务的 HTTP 通信细节封装起来,对上层透明。
|
||||
|
||||
RawMessage 结构体 (communication/communicator.go): 匹配 can-bridge 服务期望的 JSON 格式。
|
||||
|
||||
```go
|
||||
type RawMessage struct {
|
||||
Interface string `json:"interface"`
|
||||
ID uint32 `json:"id"`
|
||||
Data []byte `json:"data"`
|
||||
}
|
||||
```
|
||||
|
||||
Communicator 接口 (communication/communicator.go): 定义了与 can-bridge Web 服务进行通信的接口。
|
||||
|
||||
```go
|
||||
type Communicator interface {
|
||||
SendMessage(ctx context.Context, msg RawMessage) error
|
||||
GetInterfaceStatus(ifName string) (isActive bool, err error)
|
||||
GetAllInterfaceStatuses() (statuses map[string]bool, err error)
|
||||
SetServiceURL(url string)
|
||||
IsConnected() bool
|
||||
}
|
||||
```
|
||||
|
||||
**CanBridgeClient (communication/communicator.go): Communicator 接口的实现。**
|
||||
|
||||
1. 内部使用标准的 net/http 包与 can-bridge 服务交互。
|
||||
2. 负责构造 HTTP 请求 (POST 到 /api/can 用于发送,GET 到 /api/status/* 用于状态检查)。
|
||||
3. 处理 JSON 序列化/反序列化以及 HTTP 错误。
|
||||
4. 需要配置 can-bridge 服务的 URL。
|
||||
|
||||
具体设备实现 (如 L10Hand) 依赖此 Communicator 接口来发送指令。
|
||||
|
||||
## 指令生成与解析
|
||||
|
||||
指令生成:上层逻辑(如动画、直接控制)创建 device.Command 类型的对象 (如 NewFingerPoseCommand(...))。
|
||||
|
||||
设备的 ExecuteCommand 方法接收此 Command。
|
||||
|
||||
设备内部的 commandToRawMessage (或类似) 方法将通用的 Command 转换为特定于该型号的 RawMessage(包含正确的 Interface, ID, Data)。
|
||||
|
||||
传感器数据解析:
|
||||
|
||||
L10Hand 的 ReadSensorData 方法委托给相应的 Sensor 组件。
|
||||
|
||||
Sensor 组件的 ReadData 方法负责获取原始数据(如果通过 CAN,则可能需要 Communicator 支持读取功能,目前 can-bridge 主要用于发送)并将其解析为高层可理解的 SensorData。当前实现中,PressureSensor 是模拟数据。
|
||||
|
||||
## 配置与注册
|
||||
|
||||
设备工厂 (device/factory.go):
|
||||
|
||||
使用 DeviceFactory (defaultFactory) 来创建不同型号的 Device 实例。
|
||||
|
||||
RegisterDeviceType(modelName string, constructor func(config map[string]any) (Device, error)): 注册新的设备型号及其构造函数。
|
||||
|
||||
CreateDevice(modelName string, config map[string]any) (Device, error): 根据型号和配置创建设备实例。
|
||||
|
||||
设备构造函数 (如 NewL10Hand) 接收一个 map[string]any 类型的配置参数。
|
||||
|
||||
动画和预设姿势注册:
|
||||
|
||||
动画通过 AnimationEngine.Register() 在设备实例化时注册。
|
||||
|
||||
预设姿势通过 PresetManager.RegisterPreset() 在设备实例化时注册。
|
||||
|
||||
## 如何添加新的设备实现
|
||||
|
||||
要添加对新型号设备(例如 "L20")的支持,请遵循以下步骤:
|
||||
|
||||
### 创建设备模型文件:
|
||||
|
||||
在 device/models/ 目录下为新设备创建一个 Go 文件,例如 l20.go。
|
||||
|
||||
如果需要设备特定的动画,创建 l20_animation.go。
|
||||
|
||||
如果需要设备特定的预设姿势,创建 l20_presets.go。
|
||||
|
||||
定义设备结构体 (l20.go):
|
||||
|
||||
```go
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
// ... 其他必要的 import
|
||||
"hands/communication"
|
||||
"hands/component" // 如果需要自定义组件或使用现有组件
|
||||
"hands/define"
|
||||
"hands/device"
|
||||
)
|
||||
|
||||
type L20Hand 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
|
||||
animationEngine *device.AnimationEngine
|
||||
presetManager *device.PresetManager
|
||||
// ... L20 特有的字段
|
||||
}
|
||||
```
|
||||
|
||||
实现构造函数 (NewL20Hand):
|
||||
|
||||
```go
|
||||
func NewL20Hand(config map[string]any) (device.Device, error) {
|
||||
// 1. 解析配置 (id, can_service_url, can_interface, hand_type 等)
|
||||
// ...
|
||||
|
||||
// 2. 创建 communicator
|
||||
comm := communication.NewCanBridgeClient(serviceURL) // serviceURL from config
|
||||
|
||||
hand := &L20Hand{
|
||||
id: id, // from config
|
||||
model: "L20",
|
||||
handType: handType, // from config or default
|
||||
communicator: comm,
|
||||
components: make(map[device.ComponentType][]device.Component),
|
||||
canInterface: canInterface, // from config or default
|
||||
status: device.DeviceStatus{ /* initial status */ },
|
||||
// ... 初始化 L20 特有字段
|
||||
}
|
||||
|
||||
// 3. 初始化 AnimationEngine
|
||||
hand.animationEngine = device.NewAnimationEngine(hand) // hand 实现了 PoseExecutor
|
||||
// 注册 L20 特定的动画 (见步骤 6)
|
||||
// hand.animationEngine.Register(NewL20WaveAnimation()) // 示例
|
||||
|
||||
// 4. 初始化 PresetManager
|
||||
hand.presetManager = device.NewPresetManager()
|
||||
// 注册 L20 特定的预设姿势 (见步骤 7)
|
||||
// for _, preset := range GetL20Presets() { hand.presetManager.RegisterPreset(preset) } // 示例
|
||||
|
||||
// 5. 初始化组件
|
||||
if err := hand.initializeComponents(config); err != nil {
|
||||
return nil, fmt.Errorf("L20 初始化组件失败:%w", err)
|
||||
}
|
||||
|
||||
log.Printf("✅ 设备 L20 (%s, %s) 创建成功", hand.id, hand.handType.String())
|
||||
return hand, nil
|
||||
}
|
||||
```
|
||||
|
||||
**实现 device.Device 和 device.PoseExecutor 接口:**
|
||||
|
||||
基本方法:GetID(), GetModel(), GetHandType(), SetHandType(), GetStatus(), Connect(), Disconnect()。这些通常比较直接。
|
||||
|
||||
**PoseExecutor 方法:**
|
||||
|
||||
1. SetFingerPose(pose []byte) error
|
||||
2. SetPalmPose(pose []byte) error
|
||||
3. ResetPose() error
|
||||
|
||||
这些方法内部会调用 ExecuteCommand,或者直接构造 RawMessage 发送(如果 L20 的姿态设置非常特殊)。通常建议通过 ExecuteCommand。
|
||||
|
||||
```go
|
||||
ExecuteCommand(cmd device.Command) error:
|
||||
|
||||
func (h *L20Hand) ExecuteCommand(cmd device.Command) error {
|
||||
h.mutex.Lock()
|
||||
defer h.mutex.Unlock()
|
||||
// 1. 检查设备状态
|
||||
// 2. 调用 h.commandToRawMessage(cmd) 将通用指令转换为 L20 特定的 RawMessage
|
||||
// 3. 使用 h.communicator.SendMessage(ctx, rawMsg) 发送
|
||||
// 4. 更新设备状态和日志
|
||||
return nil // or error
|
||||
}
|
||||
```
|
||||
|
||||
commandToRawMessage(cmd device.Command) (communication.RawMessage, error): 这个辅助方法是设备差异化的关键。它需要根据 L20 的 CAN 协议,将 cmd.Type() 和 cmd.Payload() 转换为正确的 RawMessage.ID 和 RawMessage.Data。
|
||||
|
||||
组件和传感器方法:ReadSensorData(), GetComponents()。
|
||||
|
||||
动画和预设方法:GetAnimationEngine(), GetSupportedPresets(), ExecutePreset(), GetPresetDescription()。这些通常直接委托给内部的 animationEngine 和 presetManager。
|
||||
|
||||
实现设备特定逻辑:initializeComponents(config map[string]any) error: 根据 L20 的硬件配置,创建并注册其传感器、执行器等组件到 h.components。
|
||||
|
||||
```go
|
||||
func (h *L20Hand) initializeComponents(config map[string]any) error {
|
||||
// 示例:添加一个 L20 特有的传感器
|
||||
// l20Sensor := component.NewL20SpecificSensor("l20_sensor_1", nil)
|
||||
// h.components[device.SensorComponent] = append(h.components[device.SensorComponent], l20Sensor)
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
添加设备特定动画 (l20_animation.go):
|
||||
|
||||
定义实现 device.Animation 接口的动画结构体,如 L20WaveAnimation。
|
||||
|
||||
在 NewL20Hand 中,使用 hand.animationEngine.Register(NewL20WaveAnimation()) 注册它们。
|
||||
|
||||
添加设备特定预设姿势 (l20_presets.go):
|
||||
|
||||
定义一个函数如 GetL20Presets() []device.PresetPose,返回 L20 的预设姿势列表。
|
||||
|
||||
在 NewL20Hand 中,遍历这些预设并使用 hand.presetManager.RegisterPreset(preset) 注册它们。
|
||||
|
||||
注册设备类型:
|
||||
|
||||
在 device/models/init.go 的 RegisterDeviceTypes() 函数中添加一行:
|
||||
|
||||
device.RegisterDeviceType("L20", NewL20Hand)
|
||||
|
||||
## 如何添加新的动画/预设姿势
|
||||
|
||||
这里主要指实现项目已定义的 Go 接口,如 device.Animation 或 component.Sensor。
|
||||
|
||||
### 添加新的动画 (实现 device.Animation)
|
||||
|
||||
定义动画结构体:在设备模型相关的动画文件内 (例如,若为 L10 添加新动画,则在 device/models/l10_animation.go 中),或为通用动画创建新文件。
|
||||
|
||||
示例:
|
||||
|
||||
```go
|
||||
// device/models/l10_animation.go
|
||||
type L10GreetingAnimation struct{}
|
||||
|
||||
func NewL10GreetingAnimation() *L10GreetingAnimation { return &L10GreetingAnimation{} }
|
||||
```
|
||||
|
||||
实现 device.Animation 接口:
|
||||
|
||||
```go
|
||||
func (a *L10GreetingAnimation) Name() string { return "greeting" }
|
||||
|
||||
func (a *L10GreetingAnimation) Run(executor device.PoseExecutor, stop <-chan struct{}, speedMs int) error {
|
||||
log.Printf("Running %s animation on %s", a.Name(), executor.GetHandType())
|
||||
delay := time.Duration(speedMs) * time.Millisecond
|
||||
|
||||
// 示例:挥手动作
|
||||
poses := [][]byte{
|
||||
{192, 192, 192, 192, 192, 192}, // 张开
|
||||
{160, 160, 160, 160, 160, 160}, // 稍弯曲
|
||||
}
|
||||
palmPoses := [][]byte{
|
||||
{100, 128, 128, 128}, // 手掌姿态 1
|
||||
{150, 128, 128, 128}, // 手掌姿态 2
|
||||
}
|
||||
|
||||
for i := 0; i < 3; i++ { // 重复几次
|
||||
for j, pose := range poses {
|
||||
if err := executor.SetFingerPose(pose); err != nil { return err }
|
||||
if err := executor.SetPalmPose(palmPoses[j%len(palmPoses)]); err != nil { return err } // 循环使用手掌姿态
|
||||
|
||||
select {
|
||||
case <-stop:
|
||||
log.Printf("%s animation stopped.", a.Name())
|
||||
return nil
|
||||
case <-time.After(delay):
|
||||
// continue
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
注册动画:在对应设备的构造函数中 (例如 NewL10Hand),获取 AnimationEngine 实例并注册新动画:
|
||||
|
||||
```go
|
||||
// 在 NewL10Hand 中:
|
||||
hand.animationEngine.Register(NewL10GreetingAnimation())
|
||||
```
|
||||
|
||||
### 添加新的传感器类型 (实现 component.Sensor 和 device.Component)
|
||||
|
||||
定义传感器结构体:
|
||||
|
||||
在 component/ 目录下创建新文件,例如 temperature_sensor.go。
|
||||
|
||||
定义结构体:
|
||||
|
||||
```go
|
||||
// component/temperature_sensor.go
|
||||
package component
|
||||
|
||||
import (
|
||||
"hands/device"
|
||||
"math/rand/v2"
|
||||
"time"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type TemperatureSensor struct {
|
||||
id string
|
||||
config map[string]any
|
||||
isActive bool
|
||||
samplingRate int // Hz
|
||||
}
|
||||
|
||||
func NewTemperatureSensor(id string, config map[string]any) Sensor { // 返回 Sensor 接口
|
||||
return &TemperatureSensor{
|
||||
id: id,
|
||||
config: config,
|
||||
isActive: true,
|
||||
samplingRate: 1, // 默认 1Hz
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
实现 device.Component 接口:
|
||||
|
||||
```go
|
||||
func (ts *TemperatureSensor) GetID() string { return ts.id }
|
||||
func (ts *TemperatureSensor) GetType() device.ComponentType { return device.SensorComponent }
|
||||
func (ts *TemperatureSensor) GetConfiguration() map[string]any { return ts.config }
|
||||
func (ts *TemperatureSensor) IsActive() bool { return ts.isActive }
|
||||
```
|
||||
|
||||
实现 component.Sensor 接口:
|
||||
|
||||
```go
|
||||
func (ts *TemperatureSensor) ReadData() (device.SensorData, error) {
|
||||
if !ts.isActive {
|
||||
return nil, fmt.Errorf("sensor %s is not active", ts.id)
|
||||
}
|
||||
// 模拟读取温度数据
|
||||
tempValue := 20.0 + rand.Float64()*15.0 // 20-35 度
|
||||
values := map[string]any{
|
||||
"temperature": tempValue,
|
||||
"unit": "Celsius",
|
||||
}
|
||||
return NewSensorData(ts.id, values), nil // 使用 component.NewSensorData
|
||||
}
|
||||
|
||||
func (ts *TemperatureSensor) GetDataType() string { return "temperature" }
|
||||
|
||||
func (ts *TemperatureSensor) GetSamplingRate() int { return ts.samplingRate }
|
||||
|
||||
func (ts *TemperatureSensor) SetSamplingRate(rate int) error {
|
||||
if rate <= 0 {
|
||||
return fmt.Errorf("sampling rate must be positive")
|
||||
}
|
||||
ts.samplingRate = rate
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
集成到设备:在具体设备模型 (如 L10Hand 或 L20Hand) 的 initializeComponents 方法中,创建并添加此传感器的实例:
|
||||
|
||||
```go
|
||||
// 在 L10Hand.initializeComponents 中:
|
||||
tempSensor1 := component.NewTemperatureSensor("temp_palm", map[string]any{"location": "palm"})
|
||||
h.components[device.SensorComponent] = append(h.components[device.SensorComponent], tempSensor1)
|
||||
```
|
||||
|
||||
### 如何添加新的 Component
|
||||
|
||||
添加一个新的通用组件(非特指传感器)与添加传感器类似,主要区别在于它可能不会实现 component.Sensor 接口,而是直接实现 device.Component 以及任何该组件特有的接口。
|
||||
|
||||
定义组件类型 (如果需要新的 ComponentType): 在 device/device.go 中为新的组件类型添加一个常量:
|
||||
|
||||
```go
|
||||
const (
|
||||
// ...
|
||||
MyCustomComponentType ComponentType = "my_custom_type"
|
||||
)
|
||||
```
|
||||
|
||||
定义组件特定接口:如果该组件有特定行为,可以在 component/ 目录下或与组件实现同文件中定义一个接口:
|
||||
|
||||
```go
|
||||
// component/my_custom_component.go
|
||||
package component
|
||||
|
||||
import "hands/device"
|
||||
|
||||
type MyCustomFunctionality interface {
|
||||
PerformAction(param string) (string, error)
|
||||
}
|
||||
```
|
||||
|
||||
定义组件结构体:在 component/ 目录下创建新文件,例如 my_custom_component.go。
|
||||
|
||||
定义结构体:
|
||||
|
||||
```go
|
||||
type MyCustomComponent struct {
|
||||
id string
|
||||
config map[string]any
|
||||
isActive bool
|
||||
// ... 其他字段
|
||||
}
|
||||
|
||||
func NewMyCustomComponent(id string, config map[string]any) device.Component { // 返回 device.Component
|
||||
return &MyCustomComponent{
|
||||
id: id,
|
||||
config: config,
|
||||
isActive: true,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
实现 device.Component 接口:
|
||||
|
||||
```go
|
||||
func (mcc *MyCustomComponent) GetID() string { return mcc.id }
|
||||
func (mcc *MyCustomComponent) GetType() device.ComponentType { return MyCustomComponentType } // 使用新定义的类型
|
||||
func (mcc *MyCustomComponent) GetConfiguration() map[string]any { return mcc.config }
|
||||
func (mcc *MyCustomComponent) IsActive() bool { return mcc.isActive }
|
||||
```
|
||||
|
||||
实现组件特定接口:
|
||||
|
||||
```go
|
||||
// 确保 MyCustomComponent 也实现了 MyCustomFunctionality
|
||||
func (mcc *MyCustomComponent) PerformAction(param string) (string, error) {
|
||||
// 实现特定功能
|
||||
return "Action performed with " + param, nil
|
||||
}
|
||||
```
|
||||
|
||||
在这种情况下,NewMyCustomComponent 的返回类型可能需要同时满足 device.Component 和 MyCustomFunctionality,或者在使用时进行类型断言。一个常见的做法是返回具体类型指针 *MyCustomComponent,它自然实现了所有嵌入或直接定义的方法。或者,如果希望返回接口,可以返回 device.Component,然后在需要特定功能时进行类型断言。
|
||||
|
||||
集成到设备:在具体设备模型的 initializeComponents 方法中,创建并添加此组件的实例:
|
||||
|
||||
```go
|
||||
// 在 L10Hand.initializeComponents 中:
|
||||
customComp := component.NewMyCustomComponent("custom_1", map[string]any{"setting": "value"})
|
||||
h.components[component.MyCustomComponentType] = append(h.components[component.MyCustomComponentType], customComp)
|
||||
```
|
||||
|
||||
设备代码可能需要通过 GetComponents(component.MyCustomComponentType) 获取这些组件,并进行类型断言以调用其特定方法:
|
||||
|
||||
```go
|
||||
comps := h.GetComponents(component.MyCustomComponentType)
|
||||
for _, comp := range comps {
|
||||
if customComp, ok := comp.(component.MyCustomFunctionality); ok { // 或 *component.MyCustomComponent
|
||||
result, err := customComp.PerformAction("test")
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
@ -39,7 +39,7 @@
|
||||
|
||||
<div class="container">
|
||||
<div class="control-panel">
|
||||
<h2>手指控制 <span class="info-badge">指令0x01</span></h2>
|
||||
<h2>手指控制 <span class="info-badge">指令 0x01</span></h2>
|
||||
|
||||
<div class="slider-group">
|
||||
<h3>手指关节控制</h3>
|
||||
@ -94,7 +94,7 @@
|
||||
</div>
|
||||
|
||||
<div class="slider-group">
|
||||
<h3>掌部控制 <span class="info-badge">指令0x04</span></h3>
|
||||
<h3>掌部控制 <span class="info-badge">指令 0x04</span></h3>
|
||||
<div class="slider-container">
|
||||
<div class="slider-label">
|
||||
<span>关节 7</span>
|
||||
|
120
static/script.js
120
static/script.js
@ -53,7 +53,7 @@ const LinkerHandController = {
|
||||
PALM_BIG_OPEN: [128, 128, 128, 128], // 大张开掌部
|
||||
|
||||
YEAH: [0, 103, 255, 255, 0, 0], // Yeah!
|
||||
PALM_YEAH: [255, 235, 128, 128], // Yeah!掌部
|
||||
PALM_YEAH: [255, 235, 128, 128], // Yeah! 掌部
|
||||
|
||||
// 数字手势预设
|
||||
ONE: [0, 57, 255, 0, 0, 0],
|
||||
@ -199,7 +199,7 @@ const LinkerHandController = {
|
||||
return;
|
||||
}
|
||||
|
||||
logMessage('info', `发送掌部姿态到 ${enabledHands.length} 个启用的手部: [${pose.join(', ')}]`);
|
||||
logMessage('info', `发送掌部姿态到 ${enabledHands.length} 个启用的手部:[${pose.join(', ')}]`);
|
||||
|
||||
enabledHands.forEach(async (config) => {
|
||||
await sendPalmPoseToHand(config, pose);
|
||||
@ -214,7 +214,7 @@ const LinkerHandController = {
|
||||
// 设置定时获取
|
||||
setInterval(() => {
|
||||
this.fetchSensorData();
|
||||
}, 2000); // 每2秒更新一次
|
||||
}, 2000); // 每 2 秒更新一次
|
||||
},
|
||||
|
||||
// 获取传感器数据
|
||||
@ -227,7 +227,7 @@ const LinkerHandController = {
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('获取传感器数据失败:', error);
|
||||
console.error('获取传感器数据失败:', error);
|
||||
});
|
||||
},
|
||||
|
||||
@ -250,7 +250,7 @@ const LinkerHandController = {
|
||||
|
||||
// 更新最后更新时间
|
||||
const lastUpdate = new Date(data.lastUpdate).toLocaleTimeString();
|
||||
html += `<div style="text-align:right;font-size:0.8em;margin-top:5px;">最后更新: ${lastUpdate}</div>`;
|
||||
html += `<div style="text-align:right;font-size:0.8em;margin-top:5px;">最后更新:${lastUpdate}</div>`;
|
||||
|
||||
sensorDisplay.innerHTML = html;
|
||||
},
|
||||
@ -281,7 +281,7 @@ async function initializeSystem() {
|
||||
try {
|
||||
logMessage('info', '开始初始化系统...');
|
||||
|
||||
// 步骤1: 加载可用接口
|
||||
// 步骤 1: 加载可用接口
|
||||
logMessage('info', '步骤 1/3: 加载可用接口');
|
||||
await loadAvailableInterfaces();
|
||||
|
||||
@ -290,7 +290,7 @@ async function initializeSystem() {
|
||||
throw new Error('未能获取到任何可用接口');
|
||||
}
|
||||
|
||||
// 步骤2: 生成手部配置
|
||||
// 步骤 2: 生成手部配置
|
||||
logMessage('info', '步骤 2/3: 生成手部配置');
|
||||
generateHandConfigs();
|
||||
|
||||
@ -299,14 +299,14 @@ async function initializeSystem() {
|
||||
throw new Error('未能生成手部配置');
|
||||
}
|
||||
|
||||
// 步骤3: 检查接口状态
|
||||
// 步骤 3: 检查接口状态
|
||||
logMessage('info', '步骤 3/3: 检查接口状态');
|
||||
await checkAllInterfaceStatus();
|
||||
|
||||
logMessage('success', '系统初始化完成');
|
||||
|
||||
} catch (error) {
|
||||
logMessage('error', `系统初始化失败: ${error.message}`);
|
||||
logMessage('error', `系统初始化失败:${error.message}`);
|
||||
console.error('InitializeSystem Error:', error);
|
||||
|
||||
// 尝试使用默认配置恢复
|
||||
@ -332,13 +332,13 @@ async function loadAvailableInterfaces() {
|
||||
if (data.status === 'success') {
|
||||
availableInterfaces = data.data.availableInterfaces || [];
|
||||
|
||||
logMessage('success', `获取到 ${availableInterfaces.length} 个可用接口: ${availableInterfaces.join(', ')}`);
|
||||
logMessage('success', `获取到 ${availableInterfaces.length} 个可用接口:${availableInterfaces.join(', ')}`);
|
||||
hideConnectionWarning();
|
||||
} else {
|
||||
throw new Error(data.error || '获取接口失败');
|
||||
}
|
||||
} catch (error) {
|
||||
logMessage('error', `获取接口失败: ${error.message}`);
|
||||
logMessage('error', `获取接口失败:${error.message}`);
|
||||
showConnectionWarning();
|
||||
// 设置默认值
|
||||
availableInterfaces = ['can0', 'can1', 'vcan0', 'vcan1'];
|
||||
@ -358,7 +358,7 @@ function generateHandConfigs() {
|
||||
handsGrid.innerHTML = '';
|
||||
|
||||
if (!availableInterfaces || availableInterfaces.length === 0) {
|
||||
handsGrid.innerHTML = '<div style="text-align: center; color: #666; padding: 20px;">没有可用的CAN接口</div>';
|
||||
handsGrid.innerHTML = '<div style="text-align: center; color: #666; padding: 20px;">没有可用的 CAN 接口</div>';
|
||||
logMessage('warning', '没有可用接口,无法生成手部配置');
|
||||
return;
|
||||
}
|
||||
@ -402,7 +402,7 @@ function generateHandConfigs() {
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// 添加一个安全的DOM检查函数
|
||||
// 添加一个安全的 DOM 检查函数
|
||||
function validateHandElement(handId) {
|
||||
const element = document.getElementById(handId);
|
||||
if (!element) {
|
||||
@ -444,7 +444,7 @@ function safeUpdateHandElement(handId) {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error updating hand element ${handId}:`, error);
|
||||
logMessage('error', `更新手部元素 ${handId} 时出错: ${error.message}`);
|
||||
logMessage('error', `更新手部元素 ${handId} 时出错:${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -458,7 +458,7 @@ function createHandElement(config) {
|
||||
const handLabel = config.handType === 'left' ? '左手' : '右手';
|
||||
const handId = handTypeIds[config.handType];
|
||||
|
||||
// 确保HTML结构完整且正确
|
||||
// 确保 HTML 结构完整且正确
|
||||
div.innerHTML = `
|
||||
<div class="hand-header">
|
||||
<input type="checkbox" class="hand-checkbox" id="${config.id}_checkbox" ${config.enabled ? 'checked' : ''}>
|
||||
@ -487,7 +487,7 @@ function createHandElement(config) {
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 使用 requestAnimationFrame 确保DOM完全渲染后再设置事件监听器
|
||||
// 使用 requestAnimationFrame 确保 DOM 完全渲染后再设置事件监听器
|
||||
requestAnimationFrame(() => {
|
||||
setTimeout(() => {
|
||||
setupHandEventListeners(config.id);
|
||||
@ -506,17 +506,17 @@ function setupHandEventListeners(handId) {
|
||||
|
||||
// 检查所有必需的元素是否存在
|
||||
if (!checkbox) {
|
||||
console.error(`setupHandEventListeners: 找不到checkbox - ${handId}_checkbox`);
|
||||
console.error(`setupHandEventListeners: 找不到 checkbox - ${handId}_checkbox`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!interfaceSelect) {
|
||||
console.error(`setupHandEventListeners: 找不到interfaceSelect - ${handId}_interface`);
|
||||
console.error(`setupHandEventListeners: 找不到 interfaceSelect - ${handId}_interface`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!handTypeSelect) {
|
||||
console.error(`setupHandEventListeners: 找不到handTypeSelect - ${handId}_handtype`);
|
||||
console.error(`setupHandEventListeners: 找不到 handTypeSelect - ${handId}_handtype`);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -588,7 +588,7 @@ function updateHandElement(handId) {
|
||||
// 安全地更新手型标签
|
||||
const handTypeLabels = element.querySelectorAll('.control-label');
|
||||
if (handTypeLabels.length >= 2) {
|
||||
const handTypeLabel = handTypeLabels[1]; // 第二个label是手型的
|
||||
const handTypeLabel = handTypeLabels[1]; // 第二个 label 是手型的
|
||||
if (handTypeLabel) {
|
||||
handTypeLabel.textContent = `手型 (CAN ID: 0x${handIdHex.toString(16).toUpperCase()})`;
|
||||
}
|
||||
@ -654,7 +654,7 @@ async function checkAllInterfaceStatus() {
|
||||
hideConnectionWarning();
|
||||
|
||||
} catch (error) {
|
||||
logMessage('error', `状态检查失败: ${error.message}`);
|
||||
logMessage('error', `状态检查失败:${error.message}`);
|
||||
console.error('CheckAllInterfaceStatus Error:', error);
|
||||
showConnectionWarning();
|
||||
setAllHandStatusOffline();
|
||||
@ -739,7 +739,7 @@ function setupEventListeners() {
|
||||
document.getElementById('start-sway').addEventListener('click', () => startAnimationForAll('sway'));
|
||||
document.getElementById('stop-animation').addEventListener('click', stopAllAnimations);
|
||||
|
||||
// 预设姿势按钮 - 使用LinkerHandController的预设
|
||||
// 预设姿势按钮 - 使用 LinkerHandController 的预设
|
||||
setupPresetButtons();
|
||||
|
||||
// 数字手势按钮事件
|
||||
@ -797,7 +797,7 @@ function setupPresetButtons() {
|
||||
function setupNumericPresets() {
|
||||
const delayDefault = 30;
|
||||
|
||||
// 数字1-9的预设
|
||||
// 数字 1-9 的预设
|
||||
for (let i = 1; i <= 9; i++) {
|
||||
const button = document.getElementById(`pose-${i}`);
|
||||
if (button) {
|
||||
@ -829,7 +829,7 @@ function getNumberName(num) {
|
||||
return names[num] || '';
|
||||
}
|
||||
|
||||
// 设置Refill Core功能
|
||||
// 设置 Refill Core 功能
|
||||
function setupRefillCore() {
|
||||
document.getElementById("refill-core").addEventListener("click", () => {
|
||||
event.preventDefault();
|
||||
@ -844,32 +844,32 @@ function setupRefillCore() {
|
||||
[[246, 155, 154, 25], [140, 62, 0, 15, 29, 143]], // 小指
|
||||
];
|
||||
|
||||
const delayTime = 350; // 设定延迟时间为350ms
|
||||
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)
|
||||
// 遍历序列索引,为每个索引创建两个操作(palm 和 finger)
|
||||
sequenceIndices.forEach((index, step) => {
|
||||
const targetPose = rukaPoseList[index];
|
||||
|
||||
// 应用palm预设
|
||||
// 应用 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
|
||||
}, delayTime * (step * 2)); // 每个完整步骤有两个操作,所以是 step*2
|
||||
|
||||
// 应用finger预设
|
||||
// 应用 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
|
||||
}, delayTime * (step * 2 + 1)); // 偏移一个 delayTime
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -1196,7 +1196,7 @@ function logMessage(type, message) {
|
||||
statusLog.appendChild(logEntry);
|
||||
statusLog.scrollTop = statusLog.scrollHeight;
|
||||
|
||||
// 保持最多50条日志
|
||||
// 保持最多 50 条日志
|
||||
const entries = statusLog.querySelectorAll('.log-entry');
|
||||
if (entries.length > 50) {
|
||||
statusLog.removeChild(entries[0]);
|
||||
@ -1205,12 +1205,12 @@ function logMessage(type, message) {
|
||||
|
||||
// 启动状态更新器
|
||||
function startStatusUpdater() {
|
||||
// 每5秒检查一次接口状态
|
||||
// 每 5 秒检查一次接口状态
|
||||
setInterval(async () => {
|
||||
await checkAllInterfaceStatus();
|
||||
}, 5000);
|
||||
|
||||
// 每30秒刷新一次接口列表
|
||||
// 每 30 秒刷新一次接口列表
|
||||
setInterval(async () => {
|
||||
const oldInterfaces = [...availableInterfaces];
|
||||
await loadAvailableInterfaces();
|
||||
@ -1226,7 +1226,7 @@ function startStatusUpdater() {
|
||||
async function debugSystemStatus() {
|
||||
logMessage('info', '🔍 开始系统调试...');
|
||||
|
||||
// 检查HTML元素
|
||||
// 检查 HTML 元素
|
||||
const elements = {
|
||||
'hands-grid': document.getElementById('hands-grid'),
|
||||
'status-log': document.getElementById('status-log'),
|
||||
@ -1243,11 +1243,11 @@ async function debugSystemStatus() {
|
||||
});
|
||||
|
||||
// 检查全局变量
|
||||
logMessage('info', `可用接口: [${availableInterfaces.join(', ')}]`);
|
||||
logMessage('info', `手部配置数量: ${Object.keys(handConfigs).length}`);
|
||||
logMessage('info', `启用手部数量: ${getEnabledHands().length}`);
|
||||
logMessage('info', `可用接口:[${availableInterfaces.join(', ')}]`);
|
||||
logMessage('info', `手部配置数量:${Object.keys(handConfigs).length}`);
|
||||
logMessage('info', `启用手部数量:${getEnabledHands().length}`);
|
||||
|
||||
// 测试API连通性
|
||||
// 测试 API 连通性
|
||||
try {
|
||||
logMessage('info', '测试 /api/health 连接...');
|
||||
const response = await fetch('/api/health');
|
||||
@ -1256,40 +1256,40 @@ async function debugSystemStatus() {
|
||||
logMessage('success', '✅ 健康检查通过');
|
||||
console.log('Health Check Data:', data);
|
||||
} else {
|
||||
logMessage('error', `❌ 健康检查失败: HTTP ${response.status}`);
|
||||
logMessage('error', `❌ 健康检查失败:HTTP ${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logMessage('error', `❌ 健康检查异常: ${error.message}`);
|
||||
logMessage('error', `❌ 健康检查异常:${error.message}`);
|
||||
}
|
||||
|
||||
// 测试接口API
|
||||
// 测试接口 API
|
||||
try {
|
||||
logMessage('info', '测试 /api/interfaces 连接...');
|
||||
const response = await fetch('/api/interfaces');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
logMessage('success', '✅ 接口API通过');
|
||||
logMessage('success', '✅ 接口 API 通过');
|
||||
console.log('Interfaces API Data:', data);
|
||||
} else {
|
||||
logMessage('error', `❌ 接口API失败: HTTP ${response.status}`);
|
||||
logMessage('error', `❌ 接口 API 失败:HTTP ${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logMessage('error', `❌ 接口API异常: ${error.message}`);
|
||||
logMessage('error', `❌ 接口 API 异常:${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 导出全局函数供HTML按钮使用
|
||||
// 导出全局函数供 HTML 按钮使用
|
||||
window.triggerButtonsSequentially = triggerButtonsSequentially;
|
||||
window.debugSystemStatus = debugSystemStatus;
|
||||
|
||||
// 添加全局错误处理
|
||||
window.addEventListener('error', function(event) {
|
||||
logMessage('error', `全局错误: ${event.error?.message || event.message}`);
|
||||
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}`);
|
||||
logMessage('error', `未处理的 Promise 拒绝:${event.reason?.message || event.reason}`);
|
||||
console.error('Unhandled Promise Rejection:', event.reason);
|
||||
});
|
||||
|
||||
@ -1333,7 +1333,7 @@ document.addEventListener('keydown', function(e) {
|
||||
toggleAllHands();
|
||||
}
|
||||
|
||||
// 数字键1-9快速设置预设姿势
|
||||
// 数字键 1-9 快速设置预设姿势
|
||||
if (e.key >= '1' && e.key <= '9' && !e.ctrlKey && !e.altKey) {
|
||||
const activeElement = document.activeElement;
|
||||
// 确保不在输入框中
|
||||
@ -1373,7 +1373,7 @@ function addTooltips() {
|
||||
'start-wave': '启动所有启用手部的手指波浪动画',
|
||||
'start-sway': '启动所有启用手部的掌部摆动动画',
|
||||
'stop-animation': '停止所有启用手部的动画',
|
||||
'refill-core': '执行Refill Core动作序列'
|
||||
'refill-core': '执行 Refill Core 动作序列'
|
||||
};
|
||||
|
||||
Object.entries(tooltips).forEach(([id, text]) => {
|
||||
@ -1411,8 +1411,8 @@ async function startSequentialHandAnimation(animationType = 'wave', interval = 5
|
||||
return getInterfaceNumber(a.interface) - getInterfaceNumber(b.interface);
|
||||
});
|
||||
|
||||
logMessage('info', `开始六手依次动画 - 类型: ${animationType}, 间隔: ${interval}ms, 循环: ${cycles}次`);
|
||||
logMessage('info', `动画顺序: ${sortedHands.map(h => h.interface).join(' → ')}`);
|
||||
logMessage('info', `开始六手依次动画 - 类型:${animationType}, 间隔:${interval}ms, 循环:${cycles}次`);
|
||||
logMessage('info', `动画顺序:${sortedHands.map(h => h.interface).join(' → ')}`);
|
||||
|
||||
// 定义动画预设
|
||||
const animationPresets = {
|
||||
@ -1466,12 +1466,12 @@ async function startSequentialHandAnimation(animationType = 'wave', interval = 5
|
||||
[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对应的掌部
|
||||
[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 对应的掌部
|
||||
]
|
||||
},
|
||||
|
||||
@ -1480,7 +1480,7 @@ async function startSequentialHandAnimation(animationType = 'wave', interval = 5
|
||||
fingerPoses: [
|
||||
[64, 64, 64, 64, 64, 64], // 起始握拳
|
||||
[128, 64, 64, 64, 64, 64], // 拇指起
|
||||
[255, 128, 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], // 前五指起
|
||||
@ -1582,10 +1582,10 @@ async function startCustomSequentialAnimation(config) {
|
||||
sortedHands = sortedHands.reverse();
|
||||
}
|
||||
|
||||
logMessage('info', `开始自定义六手动画 - 方向: ${direction}, 同时手数: ${simultaneousHands}`);
|
||||
logMessage('info', `开始自定义六手动画 - 方向:${direction}, 同时手数:${simultaneousHands}`);
|
||||
|
||||
// 执行动画逻辑...
|
||||
// 这里可以根据simultaneousHands参数同时控制多只手
|
||||
// 这里可以根据 simultaneousHands 参数同时控制多只手
|
||||
// 实现类似的动画逻辑,但支持更多自定义选项
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user