From 5a6649e99c6113520b1daca9f58daaddfd15c452 Mon Sep 17 00:00:00 2001 From: Eli Yip Date: Thu, 29 May 2025 09:38:52 +0800 Subject: [PATCH 1/8] chore: use api2 routers in main.go --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index fb5e036..7c98e00 100644 --- a/main.go +++ b/main.go @@ -94,7 +94,7 @@ func main() { })) // 设置 API 路由 - api.SetupRoutes(r) + api2.NewServer(device.NewDeviceManager()).SetupRoutes(r) // 启动服务器 log.Printf("🌐 CAN 控制服务运行在 http://localhost:%s", config.Config.WebPort) From 1280a1a3f3e5c7e68170a98bb42a6b7980af952c Mon Sep 17 00:00:00 2001 From: Eli Yip Date: Thu, 29 May 2025 09:39:12 +0800 Subject: [PATCH 2/8] chore: register device types explicitly --- device/models/init.go | 2 +- main.go | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/device/models/init.go b/device/models/init.go index 7bf9a3f..b9dba65 100644 --- a/device/models/init.go +++ b/device/models/init.go @@ -2,7 +2,7 @@ package models import "hands/device" -func init() { +func RegisterDeviceTypes() { // 注册 L10 设备类型 device.RegisterDeviceType("L10", NewL10Hand) } diff --git a/main.go b/main.go index 7c98e00..df72e58 100644 --- a/main.go +++ b/main.go @@ -93,6 +93,8 @@ func main() { MaxAge: 12 * time.Hour, })) + models.RegisterDeviceTypes() + // 设置 API 路由 api2.NewServer(device.NewDeviceManager()).SetupRoutes(r) From 312dd728b593be3c2a5564d3e4dc50811d35605d Mon Sep 17 00:00:00 2001 From: Eli Yip Date: Thu, 29 May 2025 09:39:58 +0800 Subject: [PATCH 3/8] chore: set l10 default connection info to true --- device/models/l10.go | 13 ++++--------- main.go | 7 +++---- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/device/models/l10.go b/device/models/l10.go index 671adc5..3da54d9 100644 --- a/device/models/l10.go +++ b/device/models/l10.go @@ -30,13 +30,7 @@ type L10Hand struct { // 在 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 - } + v := min(max(int(base)+offset, 0), 255) return byte(v) } @@ -79,8 +73,9 @@ func NewL10Hand(config map[string]any) (device.Device, error) { components: make(map[device.ComponentType][]device.Component), canInterface: canInterface, status: device.DeviceStatus{ - IsConnected: false, - IsActive: false, + // TODO: 这里需要修改,根据实际连接情况设置,因为当前还没有实现连接和断开路由,先设置为 true + IsConnected: true, + IsActive: true, LastUpdate: time.Now(), }, } diff --git a/main.go b/main.go index df72e58..4bdaa74 100644 --- a/main.go +++ b/main.go @@ -2,9 +2,11 @@ package main import ( "fmt" - "hands/api" + "hands/api2" "hands/cli" "hands/config" + "hands/device" + "hands/device/models" "log" "os" "time" @@ -70,9 +72,6 @@ func main() { log.Fatal("❌ 没有设置默认 CAN 接口") } - // 记录启动时间 - api.ServerStartTime = time.Now() - log.Printf("🚀 启动 CAN 控制服务 (支持左右手配置)") // 初始化服务 From e499729c5893be1f9b88ec0d2ad925c95ba2d3ae Mon Sep 17 00:00:00 2001 From: Eli Yip Date: Thu, 29 May 2025 10:52:40 +0800 Subject: [PATCH 4/8] chore: add timeout for can communicator --- communication/communicator.go | 15 ++++++++++++--- device/models/l10.go | 7 ++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/communication/communicator.go b/communication/communicator.go index 41d2323..978840d 100644 --- a/communication/communicator.go +++ b/communication/communicator.go @@ -2,6 +2,7 @@ package communication import ( "bytes" + "context" "encoding/json" "fmt" "io" @@ -20,7 +21,7 @@ type RawMessage struct { // Communicator 定义了与 can-bridge Web 服务进行通信的接口 type Communicator interface { // SendMessage 将 RawMessage 通过 HTTP POST 请求发送到 can-bridge 服务 - SendMessage(msg RawMessage) error + SendMessage(ctx context.Context, msg RawMessage) error // GetInterfaceStatus 获取指定 CAN 接口的状态 GetInterfaceStatus(ifName string) (isActive bool, err error) @@ -50,14 +51,22 @@ func NewCanBridgeClient(serviceURL string) Communicator { } } -func (c *CanBridgeClient) SendMessage(msg RawMessage) error { +func (c *CanBridgeClient) SendMessage(ctx context.Context, msg RawMessage) error { jsonData, err := json.Marshal(msg) if err != nil { return fmt.Errorf("序列化消息失败:%w", err) } url := fmt.Sprintf("%s/api/can", c.serviceURL) - resp, err := c.client.Post(url, "application/json", bytes.NewBuffer(jsonData)) + + // 创建带有 context 的请求 + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) + if err != nil { + return fmt.Errorf("创建 HTTP 请求失败:%w", err) + } + req.Header.Set("Content-Type", "application/json") + + resp, err := c.client.Do(req) if err != nil { return fmt.Errorf("发送 HTTP 请求失败:%w", err) } diff --git a/device/models/l10.go b/device/models/l10.go index 3da54d9..221853e 100644 --- a/device/models/l10.go +++ b/device/models/l10.go @@ -1,6 +1,7 @@ package models import ( + "context" "fmt" "log" "math/rand/v2" @@ -245,8 +246,12 @@ func (h *L10Hand) ExecuteCommand(cmd device.Command) error { return fmt.Errorf("转换指令失败:%w", err) } + // 创建带有超时的 context,设置 3 秒超时 + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + // 发送到 can-bridge 服务 - if err := h.communicator.SendMessage(rawMsg); err != nil { + if err := h.communicator.SendMessage(ctx, rawMsg); err != nil { h.status.ErrorCount++ h.status.LastError = err.Error() log.Printf("❌ %s (%s) 发送指令失败: %v (ID: 0x%X, Data: %X)", h.id, h.handType.String(), err, rawMsg.ID, rawMsg.Data) From afbb9bef2896512246d0151983eac742c8385148 Mon Sep 17 00:00:00 2001 From: Eli Yip Date: Thu, 29 May 2025 18:41:06 +0800 Subject: [PATCH 5/8] refactor: remove old api dir, rename api2 to api --- {api2 => api}/animation_handlers.go | 2 +- {api2 => api}/device_handlers.go | 2 +- api/handler.go | 468 ---------------------------- api/models.go | 120 +++++-- {api2 => api}/pose_handlers.go | 2 +- api/router.go | 96 ++++-- {api2 => api}/sensor_handlers.go | 2 +- {api2 => api}/system_handlers.go | 2 +- api2/models.go | 111 ------- api2/router.go | 88 ------ main.go | 4 +- 11 files changed, 175 insertions(+), 722 deletions(-) rename {api2 => api}/animation_handlers.go (99%) rename {api2 => api}/device_handlers.go (99%) delete mode 100644 api/handler.go rename {api2 => api}/pose_handlers.go (99%) rename {api2 => api}/sensor_handlers.go (99%) rename {api2 => api}/system_handlers.go (99%) delete mode 100644 api2/models.go delete mode 100644 api2/router.go diff --git a/api2/animation_handlers.go b/api/animation_handlers.go similarity index 99% rename from api2/animation_handlers.go rename to api/animation_handlers.go index 558d872..c842c10 100644 --- a/api2/animation_handlers.go +++ b/api/animation_handlers.go @@ -1,4 +1,4 @@ -package api2 +package api import ( "fmt" diff --git a/api2/device_handlers.go b/api/device_handlers.go similarity index 99% rename from api2/device_handlers.go rename to api/device_handlers.go index dce693b..fedb2e1 100644 --- a/api2/device_handlers.go +++ b/api/device_handlers.go @@ -1,4 +1,4 @@ -package api2 +package api import ( "fmt" diff --git a/api/handler.go b/api/handler.go deleted file mode 100644 index b3a98d1..0000000 --- a/api/handler.go +++ /dev/null @@ -1,468 +0,0 @@ -package api - -import ( - "fmt" - "hands/config" - "hands/define" - "hands/hands" - "net/http" - "time" - - "github.com/gin-gonic/gin" -) - -// 手型设置处理函数 -func HandleHandType(c *gin.Context) { - var req HandTypeRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, define.ApiResponse{ - Status: "error", - Error: "无效的手型设置请求:" + err.Error(), - }) - return - } - - // 验证接口 - if !config.IsValidInterface(req.Interface) { - c.JSON(http.StatusBadRequest, define.ApiResponse{ - Status: "error", - Error: fmt.Sprintf("无效的接口 %s,可用接口: %v", req.Interface, config.Config.AvailableInterfaces), - }) - return - } - - // 验证手型 ID - if req.HandType == "left" && req.HandId != uint32(define.HAND_TYPE_LEFT) { - req.HandId = uint32(define.HAND_TYPE_LEFT) - } else if req.HandType == "right" && req.HandId != uint32(define.HAND_TYPE_RIGHT) { - req.HandId = uint32(define.HAND_TYPE_RIGHT) - } - - // 设置手型配置 - hands.SetHandConfig(req.Interface, req.HandType, req.HandId) - - handTypeName := "右手" - if req.HandType == "left" { - handTypeName = "左手" - } - - c.JSON(http.StatusOK, define.ApiResponse{ - Status: "success", - Message: fmt.Sprintf("接口 %s 手型已设置为%s (0x%X)", req.Interface, handTypeName, req.HandId), - Data: map[string]any{ - "interface": req.Interface, - "handType": req.HandType, - "handId": req.HandId, - }, - }) -} - -// 手指姿态处理函数 -func HandleFingers(c *gin.Context) { - var req FingerPoseRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, define.ApiResponse{ - Status: "error", - Error: "无效的手指姿态数据:" + err.Error(), - }) - return - } - - // 验证每个值是否在范围内 - for _, v := range req.Pose { - if v < 0 || v > 255 { - c.JSON(http.StatusBadRequest, define.ApiResponse{ - Status: "error", - Error: "手指姿态值必须在 0-255 范围内", - }) - return - } - } - - // 如果未指定接口,使用默认接口 - if req.Interface == "" { - req.Interface = config.Config.DefaultInterface - } - - // 验证接口 - if !config.IsValidInterface(req.Interface) { - c.JSON(http.StatusBadRequest, define.ApiResponse{ - Status: "error", - Error: fmt.Sprintf("无效的接口 %s,可用接口: %v", req.Interface, config.Config.AvailableInterfaces), - }) - return - } - - hands.StopAllAnimations(req.Interface) - - if err := hands.SendFingerPose(req.Interface, req.Pose, req.HandType, req.HandId); err != nil { - c.JSON(http.StatusInternalServerError, define.ApiResponse{ - Status: "error", - Error: "发送手指姿态失败:" + err.Error(), - }) - return - } - - c.JSON(http.StatusOK, define.ApiResponse{ - Status: "success", - Message: "手指姿态指令发送成功", - Data: map[string]any{"interface": req.Interface, "pose": req.Pose}, - }) -} - -// 掌部姿态处理函数 -func HandlePalm(c *gin.Context) { - var req PalmPoseRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, define.ApiResponse{ - Status: "error", - Error: "无效的掌部姿态数据:" + err.Error(), - }) - return - } - - // 验证每个值是否在范围内 - for _, v := range req.Pose { - if v < 0 || v > 255 { - c.JSON(http.StatusBadRequest, define.ApiResponse{ - Status: "error", - Error: "掌部姿态值必须在 0-255 范围内", - }) - return - } - } - - // 如果未指定接口,使用默认接口 - if req.Interface == "" { - req.Interface = config.Config.DefaultInterface - } - - // 验证接口 - if !config.IsValidInterface(req.Interface) { - c.JSON(http.StatusBadRequest, define.ApiResponse{ - Status: "error", - Error: fmt.Sprintf("无效的接口 %s,可用接口: %v", req.Interface, config.Config.AvailableInterfaces), - }) - return - } - - hands.StopAllAnimations(req.Interface) - - if err := hands.SendPalmPose(req.Interface, req.Pose, req.HandType, req.HandId); err != nil { - c.JSON(http.StatusInternalServerError, define.ApiResponse{ - Status: "error", - Error: "发送掌部姿态失败:" + err.Error(), - }) - return - } - - c.JSON(http.StatusOK, define.ApiResponse{ - Status: "success", - Message: "掌部姿态指令发送成功", - Data: map[string]any{"interface": req.Interface, "pose": req.Pose}, - }) -} - -// 预设姿势处理函数 -func HandlePreset(c *gin.Context) { - pose := c.Param("pose") - - // 从查询参数获取接口名称和手型 - ifName := c.Query("interface") - handType := c.Query("handType") - - if ifName == "" { - ifName = config.Config.DefaultInterface - } - - // 验证接口 - if !config.IsValidInterface(ifName) { - c.JSON(http.StatusBadRequest, define.ApiResponse{ - Status: "error", - Error: fmt.Sprintf("无效的接口 %s,可用接口: %v", ifName, config.Config.AvailableInterfaces), - }) - return - } - - hands.StopAllAnimations(ifName) - - var fingerPose []byte - var message string - - switch pose { - case "fist": - fingerPose = []byte{64, 64, 64, 64, 64, 64} - message = "已设置握拳姿势" - case "open": - fingerPose = []byte{192, 192, 192, 192, 192, 192} - message = "已设置完全张开姿势" - case "pinch": - fingerPose = []byte{120, 120, 64, 64, 64, 64} - message = "已设置捏取姿势" - case "thumbsup": - fingerPose = []byte{64, 192, 192, 192, 192, 64} - message = "已设置竖起大拇指姿势" - case "point": - fingerPose = []byte{192, 64, 192, 192, 192, 64} - message = "已设置食指指点姿势" - // 数字手势 - case "1": - fingerPose = []byte{192, 64, 192, 192, 192, 64} - message = "已设置数字 1 手势" - case "2": - fingerPose = []byte{192, 64, 64, 192, 192, 64} - message = "已设置数字 2 手势" - case "3": - fingerPose = []byte{192, 64, 64, 64, 192, 64} - message = "已设置数字 3 手势" - case "4": - fingerPose = []byte{192, 64, 64, 64, 64, 64} - message = "已设置数字 4 手势" - case "5": - fingerPose = []byte{192, 192, 192, 192, 192, 192} - message = "已设置数字 5 手势" - case "6": - fingerPose = []byte{64, 192, 192, 192, 192, 64} - message = "已设置数字 6 手势" - case "7": - fingerPose = []byte{64, 64, 192, 192, 192, 64} - message = "已设置数字 7 手势" - case "8": - fingerPose = []byte{64, 64, 64, 192, 192, 64} - message = "已设置数字 8 手势" - case "9": - fingerPose = []byte{64, 64, 64, 64, 192, 64} - message = "已设置数字 9 手势" - default: - c.JSON(http.StatusBadRequest, define.ApiResponse{ - Status: "error", - Error: "无效的预设姿势", - }) - return - } - - // 解析手型 ID(从查询参数或使用接口配置) - handId := uint32(0) - if handType != "" { - handId = hands.ParseHandType(handType, 0, ifName) - } - - if err := hands.SendFingerPose(ifName, fingerPose, handType, handId); err != nil { - c.JSON(http.StatusInternalServerError, define.ApiResponse{ - Status: "error", - Error: "设置预设姿势失败:" + err.Error(), - }) - return - } - - c.JSON(http.StatusOK, define.ApiResponse{ - Status: "success", - Message: message, - Data: map[string]any{"interface": ifName, "pose": fingerPose}, - }) -} - -// 动画控制处理函数 -func HandleAnimation(c *gin.Context) { - var req AnimationRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, define.ApiResponse{ - Status: "error", - Error: "无效的动画请求:" + err.Error(), - }) - return - } - - // 如果未指定接口,使用默认接口 - if req.Interface == "" { - req.Interface = config.Config.DefaultInterface - } - - // 验证接口 - if !config.IsValidInterface(req.Interface) { - c.JSON(http.StatusBadRequest, define.ApiResponse{ - Status: "error", - Error: fmt.Sprintf("无效的接口 %s,可用接口: %v", req.Interface, config.Config.AvailableInterfaces), - }) - return - } - - // 停止当前动画 - hands.StopAllAnimations(req.Interface) - - // 如果是停止命令,直接返回 - if req.Type == "stop" { - c.JSON(http.StatusOK, define.ApiResponse{ - Status: "success", - Message: fmt.Sprintf("%s 动画已停止", req.Interface), - }) - return - } - - // 处理速度参数 - if req.Speed <= 0 { - req.Speed = 500 // 默认速度 - } - - // 根据类型启动动画 - switch req.Type { - case "wave": - hands.StartWaveAnimation(req.Interface, req.Speed, req.HandType, req.HandId) - c.JSON(http.StatusOK, define.ApiResponse{ - Status: "success", - Message: fmt.Sprintf("%s 波浪动画已启动", req.Interface), - Data: map[string]any{"interface": req.Interface, "speed": req.Speed}, - }) - case "sway": - hands.StartSwayAnimation(req.Interface, req.Speed, req.HandType, req.HandId) - c.JSON(http.StatusOK, define.ApiResponse{ - Status: "success", - Message: fmt.Sprintf("%s 横向摆动动画已启动", req.Interface), - Data: map[string]any{"interface": req.Interface, "speed": req.Speed}, - }) - default: - c.JSON(http.StatusBadRequest, define.ApiResponse{ - Status: "error", - Error: "无效的动画类型", - }) - } -} - -// 获取传感器数据处理函数 -func HandleSensors(c *gin.Context) { - // 从查询参数获取接口名称 - ifName := c.Query("interface") - - hands.SensorMutex.RLock() - defer hands.SensorMutex.RUnlock() - - if ifName != "" { - // 验证接口 - if !config.IsValidInterface(ifName) { - c.JSON(http.StatusBadRequest, define.ApiResponse{ - Status: "error", - Error: fmt.Sprintf("无效的接口 %s,可用接口: %v", ifName, config.Config.AvailableInterfaces), - }) - return - } - - // 请求特定接口的数据 - if sensorData, ok := hands.SensorDataMap[ifName]; ok { - c.JSON(http.StatusOK, define.ApiResponse{ - Status: "success", - Data: sensorData, - }) - } else { - c.JSON(http.StatusInternalServerError, define.ApiResponse{ - Status: "error", - Error: "传感器数据不存在", - }) - } - } else { - // 返回所有接口的数据 - c.JSON(http.StatusOK, define.ApiResponse{ - Status: "success", - Data: hands.SensorDataMap, - }) - } -} - -// 系统状态处理函数 -func HandleStatus(c *gin.Context) { - hands.AnimationMutex.Lock() - animationStatus := make(map[string]bool) - for _, ifName := range config.Config.AvailableInterfaces { - animationStatus[ifName] = hands.AnimationActive[ifName] - } - hands.AnimationMutex.Unlock() - - // 检查 CAN 服务状态 - canStatus := hands.CheckCanServiceStatus() - - // 获取手型配置 - hands.HandConfigMutex.RLock() - handConfigsData := make(map[string]any) - for ifName, handConfig := range hands.HandConfigs { - handConfigsData[ifName] = map[string]any{ - "handType": handConfig.HandType, - "handId": handConfig.HandId, - } - } - hands.HandConfigMutex.RUnlock() - - interfaceStatuses := make(map[string]any) - for _, ifName := range config.Config.AvailableInterfaces { - interfaceStatuses[ifName] = map[string]any{ - "active": canStatus[ifName], - "animationActive": animationStatus[ifName], - "handConfig": handConfigsData[ifName], - } - } - - c.JSON(http.StatusOK, define.ApiResponse{ - Status: "success", - Data: map[string]any{ - "interfaces": interfaceStatuses, - "uptime": time.Since(ServerStartTime).String(), - "canServiceURL": config.Config.CanServiceURL, - "defaultInterface": config.Config.DefaultInterface, - "availableInterfaces": config.Config.AvailableInterfaces, - "activeInterfaces": len(canStatus), - "handConfigs": handConfigsData, - }, - }) -} - -// 获取可用接口列表处理函数 -func HandleInterfaces(c *gin.Context) { - responseData := map[string]any{ - "availableInterfaces": config.Config.AvailableInterfaces, - "defaultInterface": config.Config.DefaultInterface, - } - - c.JSON(http.StatusOK, define.ApiResponse{ - Status: "success", - Data: responseData, - }) -} - -// 获取手型配置处理函数 -func HandleHandConfigs(c *gin.Context) { - hands.HandConfigMutex.RLock() - defer hands.HandConfigMutex.RUnlock() - - result := make(map[string]any) - for _, ifName := range config.Config.AvailableInterfaces { - if handConfig, exists := hands.HandConfigs[ifName]; exists { - result[ifName] = map[string]any{ - "handType": handConfig.HandType, - "handId": handConfig.HandId, - } - } else { - // 返回默认配置 - result[ifName] = map[string]any{ - "handType": "right", - "handId": define.HAND_TYPE_RIGHT, - } - } - } - - c.JSON(http.StatusOK, define.ApiResponse{ - Status: "success", - Data: result, - }) -} - -// 健康检查处理函数 -func HandleHealth(c *gin.Context) { - c.JSON(http.StatusOK, define.ApiResponse{ - Status: "success", - Message: "CAN Control Service is running", - Data: map[string]any{ - "timestamp": time.Now(), - "availableInterfaces": config.Config.AvailableInterfaces, - "defaultInterface": config.Config.DefaultInterface, - "serviceVersion": "1.0.0-hand-type-support", - }, - }) -} diff --git a/api/models.go b/api/models.go index 9f09550..d38193b 100644 --- a/api/models.go +++ b/api/models.go @@ -1,29 +1,111 @@ package api -type FingerPoseRequest struct { - Interface string `json:"interface,omitempty"` - Pose []byte `json:"pose" binding:"required,len=6"` - HandType string `json:"handType,omitempty"` // 新增:手型类型 - HandId uint32 `json:"handId,omitempty"` // 新增:CAN ID +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"` } -type PalmPoseRequest struct { - Interface string `json:"interface,omitempty"` - Pose []byte `json:"pose" binding:"required,len=4"` - HandType string `json:"handType,omitempty"` // 新增:手型类型 - HandId uint32 `json:"handId,omitempty"` // 新增:CAN ID +// ===== 设备管理相关模型 ===== + +// 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" } -type AnimationRequest struct { - Interface string `json:"interface,omitempty"` - Type string `json:"type" binding:"required,oneof=wave sway stop"` - Speed int `json:"speed" binding:"min=0,max=2000"` - HandType string `json:"handType,omitempty"` // 新增:手型类型 - HandId uint32 `json:"handId,omitempty"` // 新增:CAN ID +// 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 { - Interface string `json:"interface" binding:"required"` - HandType string `json:"handType" binding:"required,oneof=left right"` - HandId uint32 `json:"handId" binding:"required"` + 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"` } diff --git a/api2/pose_handlers.go b/api/pose_handlers.go similarity index 99% rename from api2/pose_handlers.go rename to api/pose_handlers.go index 1df5232..25ca358 100644 --- a/api2/pose_handlers.go +++ b/api/pose_handlers.go @@ -1,4 +1,4 @@ -package api2 +package api import ( "fmt" diff --git a/api/router.go b/api/router.go index b7987c2..bf6036d 100644 --- a/api/router.go +++ b/api/router.go @@ -1,50 +1,88 @@ package api import ( + "hands/device" "time" "github.com/gin-gonic/gin" ) -// 全局变量 -var ( - ServerStartTime time.Time -) +// Server API v2 服务器结构体 +type Server struct { + deviceManager *device.DeviceManager + startTime time.Time + version string +} -func SetupRoutes(r *gin.Engine) { +// NewServer 创建新的 API v1 服务器实例 +func NewServer(deviceManager *device.DeviceManager) *Server { + return &Server{ + deviceManager: deviceManager, + startTime: time.Now(), + version: "1.0.0", + } +} + +// SetupRoutes 设置 API v1 路由 +func (s *Server) SetupRoutes(r *gin.Engine) { r.StaticFile("/", "./static/index.html") r.Static("/static", "./static") - api := r.Group("/api") + // API v1 路由组 + v2 := r.Group("/api/v1") { - // 手型设置 API - api.POST("/hand-type", HandleHandType) + // 设备管理路由 + 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) // 设置手型 - // 手指姿态 API - api.POST("/fingers", HandleFingers) + // 设备级别的功能路由 + 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 - api.POST("/palm", HandlePalm) + // 新的预设姿势 API + poses.GET("/presets", s.GetSupportedPresets) // 获取支持的预设姿势列表 + poses.POST("/presets/:presetName", s.ExecutePresetPose) // 执行预设姿势 + } - // 预设姿势 API - api.POST("/preset/:pose", HandlePreset) + // 动画控制路由 + animations := deviceRoutes.Group("/animations") + { + animations.GET("", s.handleGetAnimations) // 获取可用动画列表 + animations.POST("/start", s.handleStartAnimation) // 启动动画 + animations.POST("/stop", s.handleStopAnimation) // 停止动画 + animations.GET("/status", s.handleAnimationStatus) // 获取动画状态 + } - // 动画控制 API - api.POST("/animation", HandleAnimation) + // 传感器数据路由 + sensors := deviceRoutes.Group("/sensors") + { + sensors.GET("", s.handleGetSensors) // 获取所有传感器数据 + sensors.GET("/:sensorId", s.handleGetSensorData) // 获取特定传感器数据 + } - // 获取传感器数据 API - api.GET("/sensors", HandleSensors) + // 设备状态路由 + deviceRoutes.GET("/status", s.handleGetDeviceStatus) // 获取设备状态 + } + } - // 系统状态 API - api.GET("/status", HandleStatus) - - // 获取可用接口列表 API - api.GET("/interfaces", HandleInterfaces) - - // 获取手型配置 API - api.GET("/hand-configs", HandleHandConfigs) - - // 健康检查端点 - api.GET("/health", HandleHealth) + // 系统管理路由 + system := v2.Group("/system") + { + system.GET("/models", s.handleGetSupportedModels) // 获取支持的设备型号 + system.GET("/status", s.handleGetSystemStatus) // 获取系统状态 + system.GET("/health", s.handleHealthCheck) // 健康检查 + } } } diff --git a/api2/sensor_handlers.go b/api/sensor_handlers.go similarity index 99% rename from api2/sensor_handlers.go rename to api/sensor_handlers.go index 6cf1322..67a35f8 100644 --- a/api2/sensor_handlers.go +++ b/api/sensor_handlers.go @@ -1,4 +1,4 @@ -package api2 +package api import ( "fmt" diff --git a/api2/system_handlers.go b/api/system_handlers.go similarity index 99% rename from api2/system_handlers.go rename to api/system_handlers.go index db11d67..012ba21 100644 --- a/api2/system_handlers.go +++ b/api/system_handlers.go @@ -1,4 +1,4 @@ -package api2 +package api import ( "net/http" diff --git a/api2/models.go b/api2/models.go deleted file mode 100644 index c94dc41..0000000 --- a/api2/models.go +++ /dev/null @@ -1,111 +0,0 @@ -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"` -} diff --git a/api2/router.go b/api2/router.go deleted file mode 100644 index bf3d2cf..0000000 --- a/api2/router.go +++ /dev/null @@ -1,88 +0,0 @@ -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) // 健康检查 - } - } -} diff --git a/main.go b/main.go index 4bdaa74..9d5dcda 100644 --- a/main.go +++ b/main.go @@ -2,7 +2,7 @@ package main import ( "fmt" - "hands/api2" + "hands/api" "hands/cli" "hands/config" "hands/device" @@ -95,7 +95,7 @@ func main() { models.RegisterDeviceTypes() // 设置 API 路由 - api2.NewServer(device.NewDeviceManager()).SetupRoutes(r) + api.NewServer(device.NewDeviceManager()).SetupRoutes(r) // 启动服务器 log.Printf("🌐 CAN 控制服务运行在 http://localhost:%s", config.Config.WebPort) From b3421189809640f2a2537150864d671d41168318 Mon Sep 17 00:00:00 2001 From: Eli Yip Date: Thu, 29 May 2025 19:10:18 +0800 Subject: [PATCH 6/8] refactor: remove duplicate pose handler --- api/pose_handlers.go | 43 ++----------------------------------------- api/router.go | 11 +++++------ 2 files changed, 7 insertions(+), 47 deletions(-) diff --git a/api/pose_handlers.go b/api/pose_handlers.go index 25ca358..3a2ef8f 100644 --- a/api/pose_handlers.go +++ b/api/pose_handlers.go @@ -235,47 +235,8 @@ func (s *Server) handleResetPose(c *gin.Context) { }) } -// 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) { +// handleGetPresetPose 获取设备支持的预设姿势列表 +func (s *Server) handleGetPresetPose(c *gin.Context) { deviceID := c.Param("deviceId") device, err := s.deviceManager.GetDevice(deviceID) diff --git a/api/router.go b/api/router.go index bf6036d..5ad18ea 100644 --- a/api/router.go +++ b/api/router.go @@ -46,14 +46,13 @@ func (s *Server) SetupRoutes(r *gin.Engine) { // 姿态控制路由 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) // 重置姿态 + poses.POST("/fingers", s.handleSetFingerPose) // 设置手指姿态 + poses.POST("/palm", s.handleSetPalmPose) // 设置手掌姿态 + poses.POST("/reset", s.handleResetPose) // 重置姿态 // 新的预设姿势 API - poses.GET("/presets", s.GetSupportedPresets) // 获取支持的预设姿势列表 - poses.POST("/presets/:presetName", s.ExecutePresetPose) // 执行预设姿势 + poses.GET("/presets", s.handleGetPresetPose) // 获取支持的预设姿势列表 + poses.POST("/presets/:presetName", s.handleSetPresetPose) // 执行预设姿势 } // 动画控制路由 From a5d495d59e6dbe0257d7547573bf7db9616b3ad4 Mon Sep 17 00:00:00 2001 From: Eli Yip Date: Thu, 29 May 2025 19:41:31 +0800 Subject: [PATCH 7/8] docs: add deisgn zh --- .gitignore | 1 + docs/contribute_CN.md | 623 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 624 insertions(+) create mode 100644 docs/contribute_CN.md diff --git a/.gitignore b/.gitignore index cb4a3fa..47c6b2c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # Custom +/temp.md /hands #################### Go.gitignore #################### diff --git a/docs/contribute_CN.md b/docs/contribute_CN.md new file mode 100644 index 0000000..2eeb08f --- /dev/null +++ b/docs/contribute_CN.md @@ -0,0 +1,623 @@ +# 当前架构详解 + +## 设备抽象层 (device 包) + +目标:统一不同型号设备的操作接口,屏蔽底层硬件差异(主要体现在指令到 RawMessage 的转换和设备特定功能的实现上)。 + +核心接口与结构体: + +**Device 接口 (device/device.go): 代表一个可控制的设备单元。** + +```go +type Device interface { + GetID() string + GetModel() string + GetHandType() define.HandType + SetHandType(handType define.HandType) error + ExecuteCommand(cmd Command) error + ReadSensorData(sensorID string) (SensorData, error) + GetComponents(componentType ComponentType) []Component + GetStatus() (DeviceStatus, error) + Connect() error + Disconnect() error + + PoseExecutor // 嵌入 PoseExecutor 接口 + GetAnimationEngine() *AnimationEngine + + GetSupportedPresets() []string + ExecutePreset(presetName string) error + GetPresetDescription(presetName string) string +} +``` + +**PoseExecutor 接口 (device/pose_executor.go): 定义了执行基本姿态指令的能力。** + +```go +type PoseExecutor interface { + SetFingerPose(pose []byte) error + SetPalmPose(pose []byte) error + ResetPose() error + GetHandType() define.HandType +} +``` + +**Command 接口 (device/device.go): 代表一个发送给设备的指令。** + +```go +type Command interface { + Type() string + Payload() []byte + TargetComponent() string // 目标组件 ID +} +``` + +具体指令实现位于 device/commands.go,如 FingerPoseCommand, PalmPoseCommand, GenericCommand。 + +**SensorData 接口 (device/device.go): 代表从传感器读取的数据。** + +```go +type SensorData interface { + Timestamp() time.Time + Values() map[string]any + SensorID() string +} +``` + +**ComponentType (device/device.go): 定义组件类型。** + +```go +const ( + SensorComponent ComponentType = "sensor" + SkinComponent ComponentType = "skin" // 示例,可扩展 + ActuatorComponent ComponentType = "actuator" // 示例,可扩展 +) +``` + +**Component 接口 (device/device.go): 代表设备的一个可插拔组件。** + +```go +type Component interface { + GetID() string + GetType() ComponentType + GetConfiguration() map[string]interface{} + IsActive() bool +} +``` + +**具体设备型号实现 (如 device/models/l10.go 中的 L10Hand):** + +1. 实现 Device 和 PoseExecutor 接口。 +2. 管理内部的 AnimationEngine 和 PresetManager。 +3. 包含将通用 Command 转换为发送给 can-bridge 的 RawMessage 的逻辑 (如 commandToRawMessage 方法)。 +4. 管理其配备的传感器等组件 (initializeComponents 方法)。 + +**DeviceManager (device/manager.go): 用于注册、发现和管理可用的设备实例。** + +```go +type DeviceManager struct { /* ... */ } +func NewDeviceManager() *DeviceManager { /* ... */ } +func (m *DeviceManager) RegisterDevice(dev Device) error { /* ... */ } +func (m *DeviceManager) GetDevice(id string) (Device, error) { /* ... */ } +``` + +## 组件化设计 (component 包) + +目标:将“皮肤”、“传感器”等视为可配置、可替换的组件。 + +核心接口与结构体: + +**传感器组件 (Sensor):** + +component/sensor.go 中定义了通用的 Sensor 接口 (嵌入了 device.Component)。 + +```go +type Sensor interface { + device.Component + ReadData() (device.SensorData, error) + GetDataType() string + GetSamplingRate() int + SetSamplingRate(rate int) error +} +``` + +具体的传感器实现,如 component/component.go 中的 PressureSensor,实现了 Sensor 接口。 + +传感器数据的实际获取方式(模拟、通过 can-bridge 的特定端点,或完全独立的数据源)在具体的 Sensor 组件实现中处理。 + +SensorDataImpl (component/sensor.go) 是 device.SensorData 的一个具体实现。 + +皮肤组件 (Skin) 及其他组件: + +如果“皮肤”影响设备的物理特性或参数范围,可以将其抽象为一个 Skin 组件,实现 device.Component 接口。 + +设备可以关联多个不同类型的组件,并在其 initializeComponents 方法中进行初始化。 + +## 动画与姿态控制 + +目标:提供灵活的动画播放和直接的姿态控制能力,与具体设备和通信方式解耦。 + +**AnimationEngine (device/engine.go):** + +每个设备实例拥有一个 AnimationEngine。 + +负责注册、启动、停止和管理动画的生命周期。 + +**使用 PoseExecutor 来执行动画中的姿态变化。** + +```go +type AnimationEngine struct { /* ... */ } +func NewAnimationEngine(executor PoseExecutor) *AnimationEngine { /* ... */ } +func (e *AnimationEngine) Register(anim Animation) { /* ... */ } +func (e *AnimationEngine) Start(name string, speedMs int) error { /* ... */ } +func (e *AnimationEngine) Stop() error { /* ... */ } +``` + +Animation 接口 (device/animation.go): 定义了动画的行为。 + +```go +type Animation interface { + Run(executor PoseExecutor, stop <-chan struct{}, speedMs int) error + Name() string +} +``` + +具体的动画实现与设备型号绑定,例如 device/models/l10_animation.go 中的 L10WaveAnimation。 + +直接姿态控制: + +通过设备实例直接调用其实现的 PoseExecutor 接口方法 (SetFingerPose, SetPalmPose, ResetPose)。 + +或者通过构造 FingerPoseCommand 或 PalmPoseCommand,然后调用 device.ExecuteCommand()。 + +预设姿势 (PresetManager - device/preset.go): + +每个设备实例拥有一个 PresetManager。 + +负责注册和管理预设姿势 (PresetPose 结构体)。 + +Device 接口提供了 GetSupportedPresets, ExecutePreset, GetPresetDescription 方法与预设姿势交互。 + +## 通信层抽象 (communication 包) + +目标:将与 can-bridge Web 服务的 HTTP 通信细节封装起来,对上层透明。 + +RawMessage 结构体 (communication/communicator.go): 匹配 can-bridge 服务期望的 JSON 格式。 + +```go +type RawMessage struct { + Interface string `json:"interface"` + ID uint32 `json:"id"` + Data []byte `json:"data"` +} +``` + +Communicator 接口 (communication/communicator.go): 定义了与 can-bridge Web 服务进行通信的接口。 + +```go +type Communicator interface { + SendMessage(ctx context.Context, msg RawMessage) error + GetInterfaceStatus(ifName string) (isActive bool, err error) + GetAllInterfaceStatuses() (statuses map[string]bool, err error) + SetServiceURL(url string) + IsConnected() bool +} +``` + +**CanBridgeClient (communication/communicator.go): Communicator 接口的实现。** + +1. 内部使用标准的 net/http 包与 can-bridge 服务交互。 +2. 负责构造 HTTP 请求 (POST 到 /api/can 用于发送,GET 到 /api/status/* 用于状态检查)。 +3. 处理 JSON 序列化/反序列化以及 HTTP 错误。 +4. 需要配置 can-bridge 服务的 URL。 + +具体设备实现 (如 L10Hand) 依赖此 Communicator 接口来发送指令。 + +## 指令生成与解析 + +指令生成:上层逻辑(如动画、直接控制)创建 device.Command 类型的对象 (如 NewFingerPoseCommand(...))。 + +设备的 ExecuteCommand 方法接收此 Command。 + +设备内部的 commandToRawMessage (或类似) 方法将通用的 Command 转换为特定于该型号的 RawMessage(包含正确的 Interface, ID, Data)。 + +传感器数据解析: + +L10Hand 的 ReadSensorData 方法委托给相应的 Sensor 组件。 + +Sensor 组件的 ReadData 方法负责获取原始数据(如果通过 CAN,则可能需要 Communicator 支持读取功能,目前 can-bridge 主要用于发送)并将其解析为高层可理解的 SensorData。当前实现中,PressureSensor 是模拟数据。 + +## 配置与注册 + +设备工厂 (device/factory.go): + +使用 DeviceFactory (defaultFactory) 来创建不同型号的 Device 实例。 + +RegisterDeviceType(modelName string, constructor func(config map[string]any) (Device, error)): 注册新的设备型号及其构造函数。 + +CreateDevice(modelName string, config map[string]any) (Device, error): 根据型号和配置创建设备实例。 + +设备构造函数 (如 NewL10Hand) 接收一个 map[string]any 类型的配置参数。 + +动画和预设姿势注册: + +动画通过 AnimationEngine.Register() 在设备实例化时注册。 + +预设姿势通过 PresetManager.RegisterPreset() 在设备实例化时注册。 + +## 如何添加新的设备实现 + +要添加对新型号设备(例如 "L20")的支持,请遵循以下步骤: + +### 创建设备模型文件: + +在 device/models/ 目录下为新设备创建一个 Go 文件,例如 l20.go。 + +如果需要设备特定的动画,创建 l20_animation.go。 + +如果需要设备特定的预设姿势,创建 l20_presets.go。 + +定义设备结构体 (l20.go): + +```go +package models + +import ( + "context" + "fmt" + "log" + "sync" + "time" + // ... 其他必要的 import + "hands/communication" + "hands/component" // 如果需要自定义组件或使用现有组件 + "hands/define" + "hands/device" +) + +type L20Hand struct { + id string + model string + handType define.HandType + communicator communication.Communicator + components map[device.ComponentType][]device.Component + status device.DeviceStatus + mutex sync.RWMutex + canInterface string + animationEngine *device.AnimationEngine + presetManager *device.PresetManager + // ... L20 特有的字段 +} +``` + +实现构造函数 (NewL20Hand): + +```go +func NewL20Hand(config map[string]any) (device.Device, error) { + // 1. 解析配置 (id, can_service_url, can_interface, hand_type 等) + // ... + + // 2. 创建 communicator + comm := communication.NewCanBridgeClient(serviceURL) // serviceURL from config + + hand := &L20Hand{ + id: id, // from config + model: "L20", + handType: handType, // from config or default + communicator: comm, + components: make(map[device.ComponentType][]device.Component), + canInterface: canInterface, // from config or default + status: device.DeviceStatus{ /* initial status */ }, + // ... 初始化 L20 特有字段 + } + + // 3. 初始化 AnimationEngine + hand.animationEngine = device.NewAnimationEngine(hand) // hand 实现了 PoseExecutor + // 注册 L20 特定的动画 (见步骤 6) + // hand.animationEngine.Register(NewL20WaveAnimation()) // 示例 + + // 4. 初始化 PresetManager + hand.presetManager = device.NewPresetManager() + // 注册 L20 特定的预设姿势 (见步骤 7) + // for _, preset := range GetL20Presets() { hand.presetManager.RegisterPreset(preset) } // 示例 + + // 5. 初始化组件 + if err := hand.initializeComponents(config); err != nil { + return nil, fmt.Errorf("L20 初始化组件失败:%w", err) + } + + log.Printf("✅ 设备 L20 (%s, %s) 创建成功", hand.id, hand.handType.String()) + return hand, nil +} +``` + +**实现 device.Device 和 device.PoseExecutor 接口:** + +基本方法:GetID(), GetModel(), GetHandType(), SetHandType(), GetStatus(), Connect(), Disconnect()。这些通常比较直接。 + +**PoseExecutor 方法:** + +1. SetFingerPose(pose []byte) error +2. SetPalmPose(pose []byte) error +3. ResetPose() error + +这些方法内部会调用 ExecuteCommand,或者直接构造 RawMessage 发送(如果 L20 的姿态设置非常特殊)。通常建议通过 ExecuteCommand。 + +```go +ExecuteCommand(cmd device.Command) error: + +func (h *L20Hand) ExecuteCommand(cmd device.Command) error { + h.mutex.Lock() + defer h.mutex.Unlock() + // 1. 检查设备状态 + // 2. 调用 h.commandToRawMessage(cmd) 将通用指令转换为 L20 特定的 RawMessage + // 3. 使用 h.communicator.SendMessage(ctx, rawMsg) 发送 + // 4. 更新设备状态和日志 + return nil // or error +} +``` + +commandToRawMessage(cmd device.Command) (communication.RawMessage, error): 这个辅助方法是设备差异化的关键。它需要根据 L20 的 CAN 协议,将 cmd.Type() 和 cmd.Payload() 转换为正确的 RawMessage.ID 和 RawMessage.Data。 + +组件和传感器方法:ReadSensorData(), GetComponents()。 + +动画和预设方法:GetAnimationEngine(), GetSupportedPresets(), ExecutePreset(), GetPresetDescription()。这些通常直接委托给内部的 animationEngine 和 presetManager。 + +实现设备特定逻辑:initializeComponents(config map[string]any) error: 根据 L20 的硬件配置,创建并注册其传感器、执行器等组件到 h.components。 + +```go +func (h *L20Hand) initializeComponents(config map[string]any) error { + // 示例:添加一个 L20 特有的传感器 + // l20Sensor := component.NewL20SpecificSensor("l20_sensor_1", nil) + // h.components[device.SensorComponent] = append(h.components[device.SensorComponent], l20Sensor) + return nil +} +``` + +添加设备特定动画 (l20_animation.go): + +定义实现 device.Animation 接口的动画结构体,如 L20WaveAnimation。 + +在 NewL20Hand 中,使用 hand.animationEngine.Register(NewL20WaveAnimation()) 注册它们。 + +添加设备特定预设姿势 (l20_presets.go): + +定义一个函数如 GetL20Presets() []device.PresetPose,返回 L20 的预设姿势列表。 + +在 NewL20Hand 中,遍历这些预设并使用 hand.presetManager.RegisterPreset(preset) 注册它们。 + +注册设备类型: + +在 device/models/init.go 的 RegisterDeviceTypes() 函数中添加一行: + +device.RegisterDeviceType("L20", NewL20Hand) + +## 如何添加新的动画/预设姿势 + +这里主要指实现项目已定义的 Go 接口,如 device.Animation 或 component.Sensor。 + +### 添加新的动画 (实现 device.Animation) + +定义动画结构体:在设备模型相关的动画文件内 (例如,若为 L10 添加新动画,则在 device/models/l10_animation.go 中),或为通用动画创建新文件。 + +示例: + +```go +// device/models/l10_animation.go +type L10GreetingAnimation struct{} + +func NewL10GreetingAnimation() *L10GreetingAnimation { return &L10GreetingAnimation{} } +``` + +实现 device.Animation 接口: + +```go +func (a *L10GreetingAnimation) Name() string { return "greeting" } + +func (a *L10GreetingAnimation) Run(executor device.PoseExecutor, stop <-chan struct{}, speedMs int) error { + log.Printf("Running %s animation on %s", a.Name(), executor.GetHandType()) + delay := time.Duration(speedMs) * time.Millisecond + + // 示例:挥手动作 + poses := [][]byte{ + {192, 192, 192, 192, 192, 192}, // 张开 + {160, 160, 160, 160, 160, 160}, // 稍弯曲 + } + palmPoses := [][]byte{ + {100, 128, 128, 128}, // 手掌姿态 1 + {150, 128, 128, 128}, // 手掌姿态 2 + } + + for i := 0; i < 3; i++ { // 重复几次 + for j, pose := range poses { + if err := executor.SetFingerPose(pose); err != nil { return err } + if err := executor.SetPalmPose(palmPoses[j%len(palmPoses)]); err != nil { return err } // 循环使用手掌姿态 + + select { + case <-stop: + log.Printf("%s animation stopped.", a.Name()) + return nil + case <-time.After(delay): + // continue + } + } + } + return nil +} +``` + +注册动画:在对应设备的构造函数中 (例如 NewL10Hand),获取 AnimationEngine 实例并注册新动画: + +```go +// 在 NewL10Hand 中: +hand.animationEngine.Register(NewL10GreetingAnimation()) +``` + +### 添加新的传感器类型 (实现 component.Sensor 和 device.Component) + +定义传感器结构体: + +在 component/ 目录下创建新文件,例如 temperature_sensor.go。 + +定义结构体: + +```go +// component/temperature_sensor.go +package component + +import ( + "hands/device" + "math/rand/v2" + "time" + "fmt" +) + +type TemperatureSensor struct { + id string + config map[string]any + isActive bool + samplingRate int // Hz +} + +func NewTemperatureSensor(id string, config map[string]any) Sensor { // 返回 Sensor 接口 + return &TemperatureSensor{ + id: id, + config: config, + isActive: true, + samplingRate: 1, // 默认 1Hz + } +} +``` + +实现 device.Component 接口: + +```go +func (ts *TemperatureSensor) GetID() string { return ts.id } +func (ts *TemperatureSensor) GetType() device.ComponentType { return device.SensorComponent } +func (ts *TemperatureSensor) GetConfiguration() map[string]any { return ts.config } +func (ts *TemperatureSensor) IsActive() bool { return ts.isActive } +``` + +实现 component.Sensor 接口: + +```go +func (ts *TemperatureSensor) ReadData() (device.SensorData, error) { + if !ts.isActive { + return nil, fmt.Errorf("sensor %s is not active", ts.id) + } + // 模拟读取温度数据 + tempValue := 20.0 + rand.Float64()*15.0 // 20-35 度 + values := map[string]any{ + "temperature": tempValue, + "unit": "Celsius", + } + return NewSensorData(ts.id, values), nil // 使用 component.NewSensorData +} + +func (ts *TemperatureSensor) GetDataType() string { return "temperature" } + +func (ts *TemperatureSensor) GetSamplingRate() int { return ts.samplingRate } + +func (ts *TemperatureSensor) SetSamplingRate(rate int) error { + if rate <= 0 { + return fmt.Errorf("sampling rate must be positive") + } + ts.samplingRate = rate + return nil +} +``` + +集成到设备:在具体设备模型 (如 L10Hand 或 L20Hand) 的 initializeComponents 方法中,创建并添加此传感器的实例: + +```go +// 在 L10Hand.initializeComponents 中: +tempSensor1 := component.NewTemperatureSensor("temp_palm", map[string]any{"location": "palm"}) +h.components[device.SensorComponent] = append(h.components[device.SensorComponent], tempSensor1) +``` + +### 如何添加新的 Component + +添加一个新的通用组件(非特指传感器)与添加传感器类似,主要区别在于它可能不会实现 component.Sensor 接口,而是直接实现 device.Component 以及任何该组件特有的接口。 + +定义组件类型 (如果需要新的 ComponentType): 在 device/device.go 中为新的组件类型添加一个常量: + +```go +const ( + // ... + MyCustomComponentType ComponentType = "my_custom_type" +) +``` + +定义组件特定接口:如果该组件有特定行为,可以在 component/ 目录下或与组件实现同文件中定义一个接口: + +```go +// component/my_custom_component.go +package component + +import "hands/device" + +type MyCustomFunctionality interface { + PerformAction(param string) (string, error) +} +``` + +定义组件结构体:在 component/ 目录下创建新文件,例如 my_custom_component.go。 + +定义结构体: + +```go +type MyCustomComponent struct { + id string + config map[string]any + isActive bool + // ... 其他字段 +} + +func NewMyCustomComponent(id string, config map[string]any) device.Component { // 返回 device.Component + return &MyCustomComponent{ + id: id, + config: config, + isActive: true, + } +} +``` + +实现 device.Component 接口: + +```go +func (mcc *MyCustomComponent) GetID() string { return mcc.id } +func (mcc *MyCustomComponent) GetType() device.ComponentType { return MyCustomComponentType } // 使用新定义的类型 +func (mcc *MyCustomComponent) GetConfiguration() map[string]any { return mcc.config } +func (mcc *MyCustomComponent) IsActive() bool { return mcc.isActive } +``` + +实现组件特定接口: + +```go +// 确保 MyCustomComponent 也实现了 MyCustomFunctionality +func (mcc *MyCustomComponent) PerformAction(param string) (string, error) { + // 实现特定功能 + return "Action performed with " + param, nil +} +``` + +在这种情况下,NewMyCustomComponent 的返回类型可能需要同时满足 device.Component 和 MyCustomFunctionality,或者在使用时进行类型断言。一个常见的做法是返回具体类型指针 *MyCustomComponent,它自然实现了所有嵌入或直接定义的方法。或者,如果希望返回接口,可以返回 device.Component,然后在需要特定功能时进行类型断言。 + +集成到设备:在具体设备模型的 initializeComponents 方法中,创建并添加此组件的实例: + +```go +// 在 L10Hand.initializeComponents 中: +customComp := component.NewMyCustomComponent("custom_1", map[string]any{"setting": "value"}) +h.components[component.MyCustomComponentType] = append(h.components[component.MyCustomComponentType], customComp) +``` + +设备代码可能需要通过 GetComponents(component.MyCustomComponentType) 获取这些组件,并进行类型断言以调用其特定方法: + +```go +comps := h.GetComponents(component.MyCustomComponentType) +for _, comp := range comps { + if customComp, ok := comp.(component.MyCustomFunctionality); ok { // 或 *component.MyCustomComponent + result, err := customComp.PerformAction("test") + // ... + } +} +``` From afb5e5ce0a58176e0acb5d74d5b2043cc0c2e1f1 Mon Sep 17 00:00:00 2001 From: Eli Yip Date: Thu, 29 May 2025 19:42:28 +0800 Subject: [PATCH 8/8] docs: refine README --- README.md | 2 ++ README_CN.md => docs/README_CN.md | 2 ++ 2 files changed, 4 insertions(+) rename README_CN.md => docs/README_CN.md (98%) diff --git a/README.md b/README.md index 6e754bb..3b61a47 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Dexterous hand-operated dashboard for LinkerHand 👋! +[中文文档](./docs/README_CN.md) [中文贡献指南](./docs/contribute_CN.md) + ## Project Overview **Dexterous Hand Dashboard** is a control dashboard service specifically developed for the LinkerHand dexterous hand device. Built with Golang, it provides a flexible RESTful API interface, enabling finger and palm pose control, execution of preset gestures, real-time sensor data monitoring, and dynamic configuration of hand type (left or right) and CAN interfaces. diff --git a/README_CN.md b/docs/README_CN.md similarity index 98% rename from README_CN.md rename to docs/README_CN.md index ecf30d5..32f7d61 100644 --- a/README_CN.md +++ b/docs/README_CN.md @@ -1,5 +1,7 @@ # Dexterous Hand Dashboard 项目文档 +[贡献指南](./contribute_CN.md) + ## 项目概述 **Dexterous Hand Dashboard** 是专为 LinkerHand 灵巧手设备开发的控制仪表盘服务。该服务基于 Golang 开发,提供灵活的 RESTful API 接口,可实现手指与掌部姿态控制、预设动作执行及实时传感器数据监控,并支持动态配置手型(左手或右手)及 CAN 接口。