commit
debd5bc0a6
361
.gitignore
vendored
Normal file
361
.gitignore
vendored
Normal 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
|
@ -8,6 +8,8 @@ before:
|
||||
|
||||
builds:
|
||||
- id: dashboard-server
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
|
@ -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"]
|
@ -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
197
api2/animation_handlers.go
Normal 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
251
api2/device_handlers.go
Normal 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
111
api2/models.go
Normal 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
312
api2/pose_handlers.go
Normal 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
88
api2/router.go
Normal 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
180
api2/sensor_handlers.go
Normal 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
114
api2/system_handlers.go
Normal 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,
|
||||
})
|
||||
}
|
@ -2,7 +2,7 @@ package component
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"hands/pkg/device"
|
||||
"hands/device"
|
||||
"math/rand/v2"
|
||||
"time"
|
||||
)
|
@ -1,7 +1,7 @@
|
||||
package component
|
||||
|
||||
import (
|
||||
"hands/pkg/device"
|
||||
"hands/device"
|
||||
"time"
|
||||
)
|
||||
|
@ -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
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
|
||||
}
|
@ -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
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)
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
package models
|
||||
|
||||
import "hands/pkg/device"
|
||||
import "hands/device"
|
||||
|
||||
func init() {
|
||||
// 注册 L10 设备类型
|
380
device/models/l10.go
Normal file
380
device/models/l10.go
Normal 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)
|
||||
}
|
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,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)
|
||||
}
|
||||
}
|
95
hands/can.go
95
hands/can.go
@ -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
|
||||
}
|
241
hands/hands.go
241
hands/hands.go
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}()
|
||||
}
|
7
main.go
7
main.go
@ -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)
|
||||
|
||||
|
@ -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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user