Merge pull request #7 from eli-yip/refactor

chore: refactor(wip)
This commit is contained in:
Su Yang 2025-05-29 01:49:26 +08:00 committed by GitHub
commit debd5bc0a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 2529 additions and 927 deletions

361
.gitignore vendored Normal file
View File

@ -0,0 +1,361 @@
# Custom
/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

View File

@ -8,6 +8,8 @@ before:
builds:
- id: dashboard-server
env:
- CGO_ENABLED=0
goos:
- linux
goarch:

View File

@ -17,10 +17,10 @@ WORKDIR /app
COPY --link static/ ./static/
COPY --link --from=builder /app/dashboard-server /usr/local/bin/dashboard-server
COPY --link --from=builder /app/dashboard-server /app/dashboard-server
EXPOSE 9099
ENV SERVER_PORT="9099"
CMD ["dashboard-server"]
CMD ["/app/dashboard-server"]

View File

@ -1,10 +1,10 @@
FROM alpine:3.21
COPY --link dashboard-server /usr/local/bin/dashboard-server
COPY --link static/ ./static/
WORKDIR /app
COPY --link . .
EXPOSE 9099
ENV SERVER_PORT="9099"
ENTRYPOINT ["dashboard-server"]
CMD ["/app/dashboard-server"]

197
api2/animation_handlers.go Normal file
View File

@ -0,0 +1,197 @@
package api2
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
api2/device_handlers.go Normal file
View File

@ -0,0 +1,251 @@
package api2
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
api2/models.go Normal file
View File

@ -0,0 +1,111 @@
package api2
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"`
}

312
api2/pose_handlers.go Normal file
View File

@ -0,0 +1,312 @@
package api2
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,
},
})
}
// ExecutePresetPose 执行预设姿势
func (s *Server) ExecutePresetPose(c *gin.Context) {
deviceID := c.Param("deviceId")
presetName := c.Param("presetName")
device, err := s.deviceManager.GetDevice(deviceID)
if err != nil {
c.JSON(http.StatusNotFound, ApiResponse{
Status: "error",
Error: fmt.Sprintf("设备 %s 不存在", deviceID),
})
return
}
// 使用设备的预设姿势方法
if err := device.ExecutePreset(presetName); err != nil {
c.JSON(http.StatusBadRequest, ApiResponse{
Status: "error",
Error: fmt.Sprintf("执行预设姿势失败: %v", err),
})
return
}
// 停止当前动画(如果有)
engine := device.GetAnimationEngine()
if engine.IsRunning() {
engine.Stop()
}
c.JSON(http.StatusOK, ApiResponse{
Status: "success",
Message: fmt.Sprintf("预设姿势 '%s' 执行成功", presetName),
Data: map[string]any{
"deviceId": deviceID,
"presetName": presetName,
},
})
}
// GetSupportedPresets 获取设备支持的预设姿势列表
func (s *Server) GetSupportedPresets(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),
},
})
}

88
api2/router.go Normal file
View File

@ -0,0 +1,88 @@
package api2
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 v2 服务器实例
func NewServer(deviceManager *device.DeviceManager) *Server {
return &Server{
deviceManager: deviceManager,
startTime: time.Now(),
version: "2.0.0",
}
}
// SetupRoutes 设置 API v2 路由
func (s *Server) SetupRoutes(r *gin.Engine) {
r.StaticFile("/", "./static/index.html")
r.Static("/static", "./static")
// API v2 路由组
v2 := r.Group("/api/v2")
{
// 设备管理路由
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("/preset/:pose", s.handleSetPresetPose) // 设置预设姿势
poses.POST("/reset", s.handleResetPose) // 重置姿态
// 新的预设姿势 API
poses.GET("/presets", s.GetSupportedPresets) // 获取支持的预设姿势列表
poses.POST("/presets/:presetName", s.ExecutePresetPose) // 执行预设姿势
}
// 动画控制路由
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
api2/sensor_handlers.go Normal file
View File

@ -0,0 +1,180 @@
package api2
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
api2/system_handlers.go Normal file
View File

@ -0,0 +1,114 @@
package api2
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,
})
}

View File

@ -2,7 +2,7 @@ package component
import (
"fmt"
"hands/pkg/device"
"hands/device"
"math/rand/v2"
"time"
)

View File

@ -1,7 +1,7 @@
package component
import (
"hands/pkg/device"
"hands/device"
"time"
)

View File

@ -3,6 +3,25 @@ package define
type HandType int
const (
HAND_TYPE_LEFT HandType = 0x28
HAND_TYPE_RIGHT HandType = 0x27
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
View 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
}

View File

@ -17,6 +17,15 @@ type Device interface {
GetStatus() (DeviceStatus, error) // 获取设备状态
Connect() error // 连接设备
Disconnect() error // 断开设备连接
// --- 新增 ---
PoseExecutor // 嵌入 PoseExecutor 接口Device 需实现它
GetAnimationEngine() *AnimationEngine // 获取设备的动画引擎
// --- 预设姿势相关方法 ---
GetSupportedPresets() []string // 获取支持的预设姿势列表
ExecutePreset(presetName string) error // 执行预设姿势
GetPresetDescription(presetName string) string // 获取预设姿势描述
}
// Command 代表一个发送给设备的指令

210
device/engine.go Normal file
View 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)
}
}

View File

@ -1,6 +1,6 @@
package models
import "hands/pkg/device"
import "hands/device"
func init() {
// 注册 L10 设备类型

380
device/models/l10.go Normal file
View File

@ -0,0 +1,380 @@
package models
import (
"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 := int(base) + offset
if v < 0 {
v = 0
}
if v > 255 {
v = 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{
IsConnected: false,
IsActive: false,
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)
}
// 发送到 can-bridge 服务
if err := h.communicator.SendMessage(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)
}

View 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 // 完成一个周期
}

View 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
View 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
View 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 ""
}

View File

@ -1,268 +0,0 @@
package hands
import (
"hands/config"
"log"
"sync"
"time"
)
var (
AnimationActive map[string]bool // 每个接口的动画状态
AnimationMutex sync.Mutex
StopAnimationMap map[string]chan struct{} // 每个接口的停止动画通道
)
func initAnimation() {
// 初始化动画状态映射
AnimationActive = make(map[string]bool)
StopAnimationMap = make(map[string]chan struct{})
for _, ifName := range config.Config.AvailableInterfaces {
AnimationActive[ifName] = false
StopAnimationMap[ifName] = make(chan struct{}, 1)
}
}
// 执行波浪动画 - 支持手型参数
func StartWaveAnimation(ifName string, speed int, handType string, handId uint32) {
if speed <= 0 {
speed = 500 // 默认速度
}
// 如果未指定接口,使用默认接口
if ifName == "" {
ifName = config.Config.DefaultInterface
}
// 验证接口
if !config.IsValidInterface(ifName) {
log.Printf("❌ 无法启动波浪动画: 无效的接口 %s", ifName)
return
}
AnimationMutex.Lock()
// 如果已经有动画在运行,先停止它
if AnimationActive[ifName] {
select {
case StopAnimationMap[ifName] <- struct{}{}:
// 发送成功
default:
// 通道已满,无需发送
}
StopAnimationMap[ifName] = make(chan struct{}, 1)
}
AnimationActive[ifName] = true
AnimationMutex.Unlock()
currentStopChannel := StopAnimationMap[ifName]
go func() {
defer func() {
AnimationMutex.Lock()
AnimationActive[ifName] = false
AnimationMutex.Unlock()
log.Printf("👋 %s 波浪动画已完成", ifName)
}()
fingerOrder := []int{0, 1, 2, 3, 4, 5}
open := byte(64) // 0x40
close := byte(192) // 0xC0
log.Printf("🚀 开始 %s 波浪动画", ifName)
// 动画循环
for {
select {
case <-currentStopChannel:
log.Printf("🛑 %s 波浪动画被用户停止", ifName)
return
default:
// 波浪张开
for _, idx := range fingerOrder {
pose := make([]byte, 6)
for j := 0; j < 6; j++ {
if j == idx {
pose[j] = open
} else {
pose[j] = close
}
}
if err := SendFingerPose(ifName, pose, handType, handId); err != nil {
log.Printf("%s 动画发送失败: %v", ifName, err)
return
}
delay := time.Duration(speed) * time.Millisecond
select {
case <-currentStopChannel:
log.Printf("🛑 %s 波浪动画被用户停止", ifName)
return
case <-time.After(delay):
// 继续执行
}
}
// 波浪握拳
for _, idx := range fingerOrder {
pose := make([]byte, 6)
for j := 0; j < 6; j++ {
if j == idx {
pose[j] = close
} else {
pose[j] = open
}
}
if err := SendFingerPose(ifName, pose, handType, handId); err != nil {
log.Printf("%s 动画发送失败: %v", ifName, err)
return
}
delay := time.Duration(speed) * time.Millisecond
select {
case <-currentStopChannel:
log.Printf("🛑 %s 波浪动画被用户停止", ifName)
return
case <-time.After(delay):
// 继续执行
}
}
}
}
}()
}
// 执行横向摆动动画 - 支持手型参数
func StartSwayAnimation(ifName string, speed int, handType string, handId uint32) {
if speed <= 0 {
speed = 500 // 默认速度
}
// 如果未指定接口,使用默认接口
if ifName == "" {
ifName = config.Config.DefaultInterface
}
// 验证接口
if !config.IsValidInterface(ifName) {
log.Printf("❌ 无法启动摆动动画: 无效的接口 %s", ifName)
return
}
AnimationMutex.Lock()
if AnimationActive[ifName] {
select {
case StopAnimationMap[ifName] <- struct{}{}:
// 发送成功
default:
// 通道已满,无需发送
}
StopAnimationMap[ifName] = make(chan struct{}, 1)
}
AnimationActive[ifName] = true
AnimationMutex.Unlock()
currentStopChannel := StopAnimationMap[ifName]
go func() {
defer func() {
AnimationMutex.Lock()
AnimationActive[ifName] = false
AnimationMutex.Unlock()
log.Printf("🔄 %s 横向摆动动画已完成", ifName)
}()
leftPose := []byte{48, 48, 48, 48} // 0x30
rightPose := []byte{208, 208, 208, 208} // 0xD0
log.Printf("🚀 开始 %s 横向摆动动画", ifName)
// 动画循环
for {
select {
case <-currentStopChannel:
log.Printf("🛑 %s 横向摆动动画被用户停止", ifName)
return
default:
// 向左移动
if err := SendPalmPose(ifName, leftPose, handType, handId); err != nil {
log.Printf("%s 动画发送失败: %v", ifName, err)
return
}
delay := time.Duration(speed) * time.Millisecond
select {
case <-currentStopChannel:
log.Printf("🛑 %s 横向摆动动画被用户停止", ifName)
return
case <-time.After(delay):
// 继续执行
}
// 向右移动
if err := SendPalmPose(ifName, rightPose, handType, handId); err != nil {
log.Printf("%s 动画发送失败: %v", ifName, err)
return
}
select {
case <-currentStopChannel:
log.Printf("🛑 %s 横向摆动动画被用户停止", ifName)
return
case <-time.After(delay):
// 继续执行
}
}
}
}()
}
// 停止所有动画
func StopAllAnimations(ifName string) {
// 如果未指定接口,停止所有接口的动画
if ifName == "" {
for _, validIface := range config.Config.AvailableInterfaces {
StopAllAnimations(validIface)
}
return
}
// 验证接口
if !config.IsValidInterface(ifName) {
log.Printf("⚠️ 尝试停止无效接口的动画: %s", ifName)
return
}
AnimationMutex.Lock()
defer AnimationMutex.Unlock()
if AnimationActive[ifName] {
select {
case StopAnimationMap[ifName] <- struct{}{}:
log.Printf("✅ 已发送停止 %s 动画信号", ifName)
default:
StopAnimationMap[ifName] = make(chan struct{}, 1)
StopAnimationMap[ifName] <- struct{}{}
log.Printf("⚠️ %s 通道重置后发送了停止信号", ifName)
}
AnimationActive[ifName] = false
go func() {
time.Sleep(100 * time.Millisecond)
resetToDefaultPose(ifName)
}()
} else {
log.Printf(" %s 当前没有运行中的动画", ifName)
}
}

View File

@ -1,95 +0,0 @@
package hands
import (
"bytes"
"encoding/json"
"fmt"
"hands/config"
"hands/define"
"log"
"net/http"
)
type CanMessage struct {
Interface string `json:"interface"`
ID uint32 `json:"id"`
Data []byte `json:"data"`
}
// 检查 CAN 服务状态
func CheckCanServiceStatus() map[string]bool {
resp, err := http.Get(config.Config.CanServiceURL + "/api/status")
if err != nil {
log.Printf("❌ CAN 服务状态检查失败: %v", err)
result := make(map[string]bool)
for _, ifName := range config.Config.AvailableInterfaces {
result[ifName] = false
}
return result
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Printf("❌ CAN 服务返回非正常状态:%d", resp.StatusCode)
result := make(map[string]bool)
for _, ifName := range config.Config.AvailableInterfaces {
result[ifName] = false
}
return result
}
var statusResp define.ApiResponse
if err := json.NewDecoder(resp.Body).Decode(&statusResp); err != nil {
log.Printf("❌ 解析 CAN 服务状态失败: %v", err)
result := make(map[string]bool)
for _, ifName := range config.Config.AvailableInterfaces {
result[ifName] = false
}
return result
}
// 检查状态数据
result := make(map[string]bool)
for _, ifName := range config.Config.AvailableInterfaces {
result[ifName] = false
}
// 从响应中获取各接口状态
if statusData, ok := statusResp.Data.(map[string]interface{}); ok {
if interfaces, ok := statusData["interfaces"].(map[string]interface{}); ok {
for ifName, ifStatus := range interfaces {
if status, ok := ifStatus.(map[string]interface{}); ok {
if active, ok := status["active"].(bool); ok {
result[ifName] = active
}
}
}
}
}
return result
}
// 发送请求到 CAN 服务
func sendToCanService(msg CanMessage) error {
jsonData, err := json.Marshal(msg)
if err != nil {
return fmt.Errorf("JSON 编码错误: %v", err)
}
resp, err := http.Post(config.Config.CanServiceURL+"/api/can", "application/json", bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("CAN 服务请求失败: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
var errResp define.ApiResponse
if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil {
return fmt.Errorf("CAN 服务返回错误HTTP %d", resp.StatusCode)
}
return fmt.Errorf("CAN 服务返回错误: %s", errResp.Error)
}
return nil
}

View File

@ -1,241 +0,0 @@
package hands
import (
"fmt"
"hands/config"
"hands/define"
"log"
"math/rand/v2"
"strings"
"sync"
"time"
)
// 手型配置结构体
type HandConfig struct {
HandType string `json:"handType"`
HandId uint32 `json:"handId"`
}
var (
HandConfigMutex sync.RWMutex
HandConfigs map[string]*HandConfig // 每个接口的手型配置
)
func Init() {
initSensorData()
initAnimation()
initHands()
}
func initHands() {
HandConfigs = make(map[string]*HandConfig)
}
func SetHandConfig(ifName, handType string, handId uint32) {
HandConfigMutex.Lock()
defer HandConfigMutex.Unlock()
HandConfigs[ifName] = &HandConfig{
HandType: handType,
HandId: handId,
}
log.Printf("🔧 接口 %s 手型配置已更新: %s (0x%X)", ifName, handType, handId)
}
func GetHandConfig(ifName string) *HandConfig {
HandConfigMutex.RLock()
if handConfig, exists := HandConfigs[ifName]; exists {
HandConfigMutex.RUnlock()
return handConfig
}
HandConfigMutex.RUnlock()
// 创建默认配置
HandConfigMutex.Lock()
defer HandConfigMutex.Unlock()
// 再次检查(双重检查锁定)
if handConfig, exists := HandConfigs[ifName]; exists {
return handConfig
}
// 创建默认配置(右手)
HandConfigs[ifName] = &HandConfig{
HandType: "right",
HandId: uint32(define.HAND_TYPE_RIGHT),
}
log.Printf("🆕 为接口 %s 创建默认手型配置: 右手 (0x%X)", ifName, define.HAND_TYPE_RIGHT)
return HandConfigs[ifName]
}
// 解析手型参数
func ParseHandType(handType string, handId uint32, ifName string) uint32 {
// 如果提供了有效的 handId直接使用
if handId != 0 {
return handId
}
// 根据 handType 字符串确定 ID
switch strings.ToLower(handType) {
case "left":
return uint32(define.HAND_TYPE_LEFT)
case "right":
return uint32(define.HAND_TYPE_RIGHT)
default:
// 使用接口的配置
handConfig := GetHandConfig(ifName)
return handConfig.HandId
}
}
// 发送手指姿态指令 - 支持手型参数
func SendFingerPose(ifName string, pose []byte, handType string, handId uint32) error {
if len(pose) != 6 {
return fmt.Errorf("无效的姿态数据长度,需要 6 个字节")
}
// 如果未指定接口,使用默认接口
if ifName == "" {
ifName = config.Config.DefaultInterface
}
// 验证接口
if !config.IsValidInterface(ifName) {
return fmt.Errorf("无效的接口 %s可用接口: %v", ifName, config.Config.AvailableInterfaces)
}
// 解析手型 ID
canId := ParseHandType(handType, handId, ifName)
// 添加随机扰动
perturbedPose := make([]byte, len(pose))
for i, v := range pose {
perturbedPose[i] = perturb(v, 5)
}
// 构造 CAN 消息
msg := CanMessage{
Interface: ifName,
ID: canId, // 使用动态的手型 ID
Data: append([]byte{0x01}, perturbedPose...),
}
err := sendToCanService(msg)
if err == nil {
handTypeName := "右手"
if canId == uint32(define.HAND_TYPE_LEFT) {
handTypeName = "左手"
}
log.Printf("✅ %s (%s, 0x%X) 手指动作已发送: [%X %X %X %X %X %X]",
ifName, handTypeName, canId, perturbedPose[0], perturbedPose[1], perturbedPose[2],
perturbedPose[3], perturbedPose[4], perturbedPose[5])
} else {
log.Printf("❌ %s 手指控制发送失败: %v", ifName, err)
}
return err
}
// 在 base 基础上进行 ±delta 的扰动,范围限制在 [0, 255]
func perturb(base byte, delta int) byte {
offset := rand.IntN(2*delta+1) - delta
v := int(base) + offset
if v < 0 {
v = 0
}
if v > 255 {
v = 255
}
return byte(v)
}
// 发送掌部姿态指令 - 支持手型参数
func SendPalmPose(ifName string, pose []byte, handType string, handId uint32) error {
if len(pose) != 4 {
return fmt.Errorf("无效的姿态数据长度,需要 4 个字节")
}
// 如果未指定接口,使用默认接口
if ifName == "" {
ifName = config.Config.DefaultInterface
}
// 验证接口
if !config.IsValidInterface(ifName) {
return fmt.Errorf("无效的接口 %s可用接口: %v", ifName, config.Config.AvailableInterfaces)
}
// 解析手型 ID
canId := ParseHandType(handType, handId, ifName)
// 添加随机扰动
perturbedPose := make([]byte, len(pose))
for i, v := range pose {
perturbedPose[i] = perturb(v, 8)
}
// 构造 CAN 消息
msg := CanMessage{
Interface: ifName,
ID: canId, // 使用动态的手型 ID
Data: append([]byte{0x04}, perturbedPose...),
}
err := sendToCanService(msg)
if err == nil {
handTypeName := "右手"
if canId == uint32(define.HAND_TYPE_LEFT) {
handTypeName = "左手"
}
log.Printf("✅ %s (%s, 0x%X) 掌部姿态已发送: [%X %X %X %X]",
ifName, handTypeName, canId, perturbedPose[0], perturbedPose[1], perturbedPose[2], perturbedPose[3])
// 更新传感器数据中的掌部位置
SensorMutex.Lock()
if sensorData, exists := SensorDataMap[ifName]; exists {
copy(sensorData.PalmPosition, perturbedPose)
sensorData.LastUpdate = time.Now()
}
SensorMutex.Unlock()
} else {
log.Printf("❌ %s 掌部控制发送失败: %v", ifName, err)
}
return err
}
// 重置到默认姿势
func resetToDefaultPose(ifName string) {
// 如果未指定接口,重置所有接口
if ifName == "" {
for _, validIface := range config.Config.AvailableInterfaces {
resetToDefaultPose(validIface)
}
return
}
// 验证接口
if !config.IsValidInterface(ifName) {
log.Printf("⚠️ 尝试重置无效接口: %s", ifName)
return
}
defaultFingerPose := []byte{64, 64, 64, 64, 64, 64}
defaultPalmPose := []byte{128, 128, 128, 128}
// 获取当前接口的手型配置
handConfig := GetHandConfig(ifName)
if err := SendFingerPose(ifName, defaultFingerPose, handConfig.HandType, handConfig.HandId); err != nil {
log.Printf("%s 重置手指姿势失败: %v", ifName, err)
}
if err := SendPalmPose(ifName, defaultPalmPose, handConfig.HandType, handConfig.HandId); err != nil {
log.Printf("%s 重置掌部姿势失败: %v", ifName, err)
}
log.Printf("✅ 已重置 %s 到默认姿势", ifName)
}

View File

@ -1,65 +0,0 @@
package hands
import (
"hands/config"
"math/rand/v2"
"sync"
"time"
)
// 传感器数据结构体
type SensorData struct {
Interface string `json:"interface"`
Thumb int `json:"thumb"`
Index int `json:"index"`
Middle int `json:"middle"`
Ring int `json:"ring"`
Pinky int `json:"pinky"`
PalmPosition []byte `json:"palmPosition"`
LastUpdate time.Time `json:"lastUpdate"`
}
var (
SensorDataMap map[string]*SensorData // 每个接口的传感器数据
SensorMutex sync.RWMutex
)
func initSensorData() {
// 初始化传感器数据映射
SensorDataMap = make(map[string]*SensorData)
for _, ifName := range config.Config.AvailableInterfaces {
SensorDataMap[ifName] = &SensorData{
Interface: ifName,
Thumb: 0,
Index: 0,
Middle: 0,
Ring: 0,
Pinky: 0,
PalmPosition: []byte{128, 128, 128, 128},
LastUpdate: time.Now(),
}
}
}
// 读取传感器数据 (模拟)
func ReadSensorData() {
go func() {
for {
SensorMutex.Lock()
// 为每个接口模拟压力数据 (0-100)
for _, ifName := range config.Config.AvailableInterfaces {
if sensorData, exists := SensorDataMap[ifName]; exists {
sensorData.Thumb = rand.IntN(101)
sensorData.Index = rand.IntN(101)
sensorData.Middle = rand.IntN(101)
sensorData.Ring = rand.IntN(101)
sensorData.Pinky = rand.IntN(101)
sensorData.LastUpdate = time.Now()
}
}
SensorMutex.Unlock()
time.Sleep(500 * time.Millisecond)
}
}()
}

View File

@ -5,7 +5,6 @@ import (
"hands/api"
"hands/cli"
"hands/config"
"hands/hands"
"log"
"os"
"time"
@ -22,9 +21,6 @@ func initService() {
log.Printf(" - 可用接口: %v", config.Config.AvailableInterfaces)
log.Printf(" - 默认接口: %s", config.Config.DefaultInterface)
// 初始化手型配置映射
hands.Init()
log.Println("✅ 控制服务初始化完成")
}
@ -82,9 +78,6 @@ func main() {
// 初始化服务
initService()
// 启动传感器数据模拟
hands.ReadSensorData()
// 设置 Gin 模式
gin.SetMode(gin.ReleaseMode)

View File

@ -1,240 +0,0 @@
package models
import (
"fmt"
"sync"
"time"
"hands/define"
"hands/pkg/communication"
"hands/pkg/component"
"hands/pkg/device"
)
// L10Hand L10 型号手部设备实现
type L10Hand struct {
id string
model string
handType define.HandType
communicator communication.Communicator
components map[device.ComponentType][]device.Component
status device.DeviceStatus
mutex sync.RWMutex
canInterface string // CAN 接口名称,如 "can0"
}
// NewL10Hand 创建 L10 手部设备实例
func NewL10Hand(config map[string]any) (device.Device, error) {
id, ok := config["id"].(string)
if !ok {
return nil, fmt.Errorf("缺少设备 ID 配置")
}
serviceURL, ok := config["can_service_url"].(string)
if !ok {
return nil, fmt.Errorf("缺少 can 服务 URL 配置")
}
canInterface, ok := config["can_interface"].(string)
if !ok {
canInterface = "can0" // 默认接口
}
handType, ok := config["hand_type"].(define.HandType)
if !ok {
handType = define.HAND_TYPE_LEFT
}
// 创建通信客户端
comm := communication.NewCanBridgeClient(serviceURL)
hand := &L10Hand{
id: id,
model: "L10",
handType: handType,
communicator: comm,
components: make(map[device.ComponentType][]device.Component),
canInterface: canInterface,
status: device.DeviceStatus{
IsConnected: false,
IsActive: false,
LastUpdate: time.Now(),
},
}
// 初始化组件
if err := hand.initializeComponents(config); err != nil {
return nil, fmt.Errorf("初始化组件失败:%w", err)
}
return hand, nil
}
func (h *L10Hand) GetHandType() define.HandType {
return h.handType
}
func (h *L10Hand) SetHandType(handType define.HandType) error {
h.handType = handType
return nil
}
func (h *L10Hand) initializeComponents(_ map[string]any) error {
// 初始化传感器组件
sensors := []device.Component{
component.NewPressureSensor("pressure_thumb", map[string]any{"location": "thumb"}),
component.NewPressureSensor("pressure_index", map[string]any{"location": "index"}),
component.NewPressureSensor("pressure_middle", map[string]any{"location": "middle"}),
component.NewPressureSensor("pressure_ring", map[string]any{"location": "ring"}),
component.NewPressureSensor("pressure_pinky", map[string]any{"location": "pinky"}),
}
h.components[device.SensorComponent] = sensors
return nil
}
func (h *L10Hand) GetID() string {
return h.id
}
func (h *L10Hand) GetModel() string {
return h.model
}
func (h *L10Hand) ExecuteCommand(cmd device.Command) error {
h.mutex.Lock()
defer h.mutex.Unlock()
// 将通用指令转换为 L10 特定的 CAN 消息
rawMsg, err := h.commandToRawMessage(cmd)
if err != nil {
return fmt.Errorf("转换指令失败:%w", err)
}
// 发送到 can-bridge 服务
if err := h.communicator.SendMessage(rawMsg); err != nil {
h.status.ErrorCount++
h.status.LastError = err.Error()
return fmt.Errorf("发送指令失败:%w", err)
}
h.status.LastUpdate = time.Now()
return nil
}
func (h *L10Hand) commandToRawMessage(cmd device.Command) (communication.RawMessage, error) {
var canID uint32
var data []byte
switch cmd.Type() {
case "SetFingerPose":
// 根据目标组件确定 CAN ID
canID = h.getFingerCanID(cmd.TargetComponent())
data = cmd.Payload()
case "SetPalmPose":
canID = h.getPalmCanID()
data = cmd.Payload()
default:
return communication.RawMessage{}, fmt.Errorf("不支持的指令类型: %s", cmd.Type())
}
return communication.RawMessage{
Interface: h.canInterface,
ID: canID,
Data: data,
}, nil
}
func (h *L10Hand) getFingerCanID(targetComponent string) uint32 {
// L10 设备的手指 CAN ID 映射
fingerIDs := map[string]uint32{
"finger_thumb": 0x100,
"finger_index": 0x101,
"finger_middle": 0x102,
"finger_ring": 0x103,
"finger_pinky": 0x104,
}
if id, exists := fingerIDs[targetComponent]; exists {
return id
}
return 0x100 // 默认拇指
}
func (h *L10Hand) getPalmCanID() uint32 {
return 0x200 // L10 设备的手掌 CAN ID
}
func (h *L10Hand) ReadSensorData(sensorID string) (device.SensorData, error) {
h.mutex.RLock()
defer h.mutex.RUnlock()
// 查找传感器组件
sensors := h.components[device.SensorComponent]
for _, comp := range sensors {
if comp.GetID() == sensorID {
if sensor, ok := comp.(component.Sensor); ok {
return sensor.ReadData()
}
}
}
return nil, fmt.Errorf("传感器 %s 不存在", sensorID)
}
func (h *L10Hand) GetComponents(componentType device.ComponentType) []device.Component {
h.mutex.RLock()
defer h.mutex.RUnlock()
if components, exists := h.components[componentType]; exists {
result := make([]device.Component, len(components))
copy(result, components)
return result
}
return []device.Component{}
}
func (h *L10Hand) GetStatus() (device.DeviceStatus, error) {
h.mutex.RLock()
defer h.mutex.RUnlock()
return h.status, nil
}
func (h *L10Hand) Connect() error {
h.mutex.Lock()
defer h.mutex.Unlock()
// 检查与 can-bridge 服务的连接
if !h.communicator.IsConnected() {
return fmt.Errorf("无法连接到 can-bridge 服务")
}
// 检查 CAN 接口状态
isActive, err := h.communicator.GetInterfaceStatus(h.canInterface)
if err != nil {
return fmt.Errorf("检查 CAN 接口状态失败:%w", err)
}
if !isActive {
return fmt.Errorf("CAN接口 %s 未激活", h.canInterface)
}
h.status.IsConnected = true
h.status.IsActive = true
h.status.LastUpdate = time.Now()
return nil
}
func (h *L10Hand) Disconnect() error {
h.mutex.Lock()
defer h.mutex.Unlock()
h.status.IsConnected = false
h.status.IsActive = false
h.status.LastUpdate = time.Now()
return nil
}