From 0ce6a973a48873677f4e565c60e8d5553ee5b4e0 Mon Sep 17 00:00:00 2001 From: Eli Yip Date: Wed, 28 May 2025 11:03:27 +0800 Subject: [PATCH 01/20] ci: fix Dockerfile bin location --- .goreleaser.yaml | 2 ++ Dockerfile | 4 ++-- Dockerfile.goreleaser | 8 ++++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 46acb6e..d7e2972 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -8,6 +8,8 @@ before: builds: - id: dashboard-server + env: + - CGO_ENABLED=0 goos: - linux goarch: diff --git a/Dockerfile b/Dockerfile index 9537283..6dfa4f3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] \ No newline at end of file +CMD ["/app/dashboard-server"] \ No newline at end of file diff --git a/Dockerfile.goreleaser b/Dockerfile.goreleaser index fcf31a5..6a8f670 100644 --- a/Dockerfile.goreleaser +++ b/Dockerfile.goreleaser @@ -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"] From 44dff010d833b48bf4de3745479e91cb302fe849 Mon Sep 17 00:00:00 2001 From: Eli Yip Date: Wed, 28 May 2025 09:05:57 +0800 Subject: [PATCH 02/20] fix: l10 hand type config --- pkg/device/models/l10.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/device/models/l10.go b/pkg/device/models/l10.go index 1711d58..59839e7 100644 --- a/pkg/device/models/l10.go +++ b/pkg/device/models/l10.go @@ -40,8 +40,9 @@ func NewL10Hand(config map[string]any) (device.Device, error) { canInterface = "can0" // 默认接口 } - handType, ok := config["hand_type"].(define.HandType) - if !ok { + handTypeStr, ok := config["hand_type"].(string) + handType := define.HAND_TYPE_RIGHT // 默认右手 + if ok && handTypeStr == "left" { handType = define.HAND_TYPE_LEFT } From 5491756a0ca2eca661444d76baace04e2e47d13e Mon Sep 17 00:00:00 2001 From: Eli Yip Date: Wed, 28 May 2025 09:39:40 +0800 Subject: [PATCH 03/20] feat: implement new animation iface --- pkg/device/animation.go | 12 ++ pkg/device/device.go | 4 + pkg/device/engine.go | 191 ++++++++++++++++++++ pkg/device/models/l10.go | 277 +++++++++++++++++++---------- pkg/device/models/l10_animation.go | 125 +++++++++++++ pkg/device/pose_executor.go | 20 +++ 6 files changed, 535 insertions(+), 94 deletions(-) create mode 100644 pkg/device/animation.go create mode 100644 pkg/device/engine.go create mode 100644 pkg/device/models/l10_animation.go create mode 100644 pkg/device/pose_executor.go diff --git a/pkg/device/animation.go b/pkg/device/animation.go new file mode 100644 index 0000000..1cf1718 --- /dev/null +++ b/pkg/device/animation.go @@ -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 +} diff --git a/pkg/device/device.go b/pkg/device/device.go index 0a4d31c..fe27b56 100644 --- a/pkg/device/device.go +++ b/pkg/device/device.go @@ -17,6 +17,10 @@ type Device interface { GetStatus() (DeviceStatus, error) // 获取设备状态 Connect() error // 连接设备 Disconnect() error // 断开设备连接 + + // --- 新增 --- + PoseExecutor // 嵌入 PoseExecutor 接口,Device 需实现它 + GetAnimationEngine() *AnimationEngine // 获取设备的动画引擎 } // Command 代表一个发送给设备的指令 diff --git a/pkg/device/engine.go b/pkg/device/engine.go new file mode 100644 index 0000000..e30d568 --- /dev/null +++ b/pkg/device/engine.go @@ -0,0 +1,191 @@ +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 +} + +// 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) + } +} diff --git a/pkg/device/models/l10.go b/pkg/device/models/l10.go index 59839e7..6f0f429 100644 --- a/pkg/device/models/l10.go +++ b/pkg/device/models/l10.go @@ -2,6 +2,8 @@ package models import ( "fmt" + "log" + "math/rand/v2" "sync" "time" @@ -13,14 +15,28 @@ import ( // 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" + 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 // 动画引擎 +} + +// 在 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 手部设备实例 @@ -63,23 +79,179 @@ func NewL10Hand(config map[string]any) (device.Device, error) { }, } + // 初始化动画引擎,将 hand 自身作为 PoseExecutor + hand.animationEngine = device.NewAnimationEngine(hand) + + // 注册默认动画 + hand.animationEngine.Register(NewL10WaveAnimation()) + hand.animationEngine.Register(NewL10SwayAnimation()) + // 初始化组件 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{ @@ -90,7 +262,6 @@ func (h *L10Hand) initializeComponents(_ map[string]any) error { component.NewPressureSensor("pressure_pinky", map[string]any{"location": "pinky"}), } h.components[device.SensorComponent] = sensors - return nil } @@ -102,75 +273,10 @@ 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 { @@ -179,7 +285,6 @@ func (h *L10Hand) ReadSensorData(sensorID string) (device.SensorData, error) { } } } - return nil, fmt.Errorf("传感器 %s 不存在", sensorID) } @@ -192,14 +297,12 @@ func (h *L10Hand) GetComponents(componentType device.ComponentType) []device.Com 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 } @@ -207,25 +310,11 @@ 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) - } - + // TODO: 假设连接总是成功,除非有显式错误 h.status.IsConnected = true h.status.IsActive = true h.status.LastUpdate = time.Now() - + log.Printf("🔗 设备 %s 已连接", h.id) return nil } @@ -236,6 +325,6 @@ func (h *L10Hand) Disconnect() error { h.status.IsConnected = false h.status.IsActive = false h.status.LastUpdate = time.Now() - + log.Printf("🔌 设备 %s 已断开", h.id) return nil } diff --git a/pkg/device/models/l10_animation.go b/pkg/device/models/l10_animation.go new file mode 100644 index 0000000..db729d0 --- /dev/null +++ b/pkg/device/models/l10_animation.go @@ -0,0 +1,125 @@ +package models + +import ( + "hands/pkg/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 // 完成一个周期 +} diff --git a/pkg/device/pose_executor.go b/pkg/device/pose_executor.go new file mode 100644 index 0000000..eb79dcb --- /dev/null +++ b/pkg/device/pose_executor.go @@ -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 +} From 2893f3cc56894d47eb4402c32710a1f0397d3e9e Mon Sep 17 00:00:00 2001 From: Eli Yip Date: Wed, 28 May 2025 09:44:39 +0800 Subject: [PATCH 04/20] refactor: remove unused hands package --- hands/animation.go | 268 --------------------------------------------- hands/can.go | 95 ---------------- hands/hands.go | 241 ---------------------------------------- hands/sensor.go | 65 ----------- 4 files changed, 669 deletions(-) delete mode 100644 hands/animation.go delete mode 100644 hands/can.go delete mode 100644 hands/hands.go delete mode 100644 hands/sensor.go diff --git a/hands/animation.go b/hands/animation.go deleted file mode 100644 index 27500a4..0000000 --- a/hands/animation.go +++ /dev/null @@ -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) - } -} diff --git a/hands/can.go b/hands/can.go deleted file mode 100644 index 15e3baf..0000000 --- a/hands/can.go +++ /dev/null @@ -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 -} diff --git a/hands/hands.go b/hands/hands.go deleted file mode 100644 index 363f86e..0000000 --- a/hands/hands.go +++ /dev/null @@ -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) -} diff --git a/hands/sensor.go b/hands/sensor.go deleted file mode 100644 index a8185d7..0000000 --- a/hands/sensor.go +++ /dev/null @@ -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) - } - }() -} From a8a88214893ec60b6daa142ba69093057a6b591d Mon Sep 17 00:00:00 2001 From: Eli Yip Date: Wed, 28 May 2025 09:45:01 +0800 Subject: [PATCH 05/20] chore: add String method for hand type --- define/hands.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/define/hands.go b/define/hands.go index 54cd472..fb821ae 100644 --- a/define/hands.go +++ b/define/hands.go @@ -6,3 +6,10 @@ const ( HAND_TYPE_LEFT HandType = 0x28 HAND_TYPE_RIGHT HandType = 0x27 ) + +func (ht HandType) String() string { + if ht == HAND_TYPE_LEFT { + return "左手" + } + return "右手" +} From 53eae1cc513dddba7efea3319c04329aa89cce2e Mon Sep 17 00:00:00 2001 From: Eli Yip Date: Wed, 28 May 2025 09:47:16 +0800 Subject: [PATCH 06/20] refactor: remove pkg directory for better project layout --- communication/communicator.go | 122 ++++++++++++ component/pressure_sensor.go | 78 ++++++++ component/sensor.go | 42 +++++ device/animation.go | 12 ++ device/commands.go | 80 ++++++++ device/device.go | 64 +++++++ device/engine.go | 191 +++++++++++++++++++ device/factory.go | 35 ++++ device/manager.go | 63 +++++++ device/models/init.go | 8 + device/models/l10.go | 330 +++++++++++++++++++++++++++++++++ device/models/l10_animation.go | 125 +++++++++++++ device/pose_executor.go | 20 ++ 13 files changed, 1170 insertions(+) create mode 100644 communication/communicator.go create mode 100644 component/pressure_sensor.go create mode 100644 component/sensor.go create mode 100644 device/animation.go create mode 100644 device/commands.go create mode 100644 device/device.go create mode 100644 device/engine.go create mode 100644 device/factory.go create mode 100644 device/manager.go create mode 100644 device/models/init.go create mode 100644 device/models/l10.go create mode 100644 device/models/l10_animation.go create mode 100644 device/pose_executor.go diff --git a/communication/communicator.go b/communication/communicator.go new file mode 100644 index 0000000..41d2323 --- /dev/null +++ b/communication/communicator.go @@ -0,0 +1,122 @@ +package communication + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +// TODO: ID 的作用是什么 +// RawMessage 代表发送给 can-bridge 服务或从其接收的原始消息结构 +type RawMessage struct { + Interface string `json:"interface"` // 目标 CAN 接口名,例如 "can0", "vcan1" + ID uint32 `json:"id"` // CAN 帧的 ID + Data []byte `json:"data"` // CAN 帧的数据负载 +} + +// Communicator 定义了与 can-bridge Web 服务进行通信的接口 +type Communicator interface { + // SendMessage 将 RawMessage 通过 HTTP POST 请求发送到 can-bridge 服务 + SendMessage(msg RawMessage) error + + // GetInterfaceStatus 获取指定 CAN 接口的状态 + GetInterfaceStatus(ifName string) (isActive bool, err error) + + // GetAllInterfaceStatuses 获取所有已知 CAN 接口的状态 + GetAllInterfaceStatuses() (statuses map[string]bool, err error) + + // SetServiceURL 设置 can-bridge 服务的 URL + SetServiceURL(url string) + + // IsConnected 检查与 can-bridge 服务的连接状态 + IsConnected() bool +} + +// CanBridgeClient 实现与 can-bridge 服务的 HTTP 通信 +type CanBridgeClient struct { + serviceURL string + client *http.Client +} + +func NewCanBridgeClient(serviceURL string) Communicator { + return &CanBridgeClient{ + serviceURL: serviceURL, + client: &http.Client{ + Timeout: 5 * time.Second, + }, + } +} + +func (c *CanBridgeClient) SendMessage(msg RawMessage) error { + jsonData, err := json.Marshal(msg) + if err != nil { + return fmt.Errorf("序列化消息失败:%w", err) + } + + url := fmt.Sprintf("%s/api/can", c.serviceURL) + resp, err := c.client.Post(url, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + return fmt.Errorf("发送 HTTP 请求失败:%w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("can-bridge服务返回错误: %d, %s", resp.StatusCode, string(body)) + } + + return nil +} + +func (c *CanBridgeClient) GetInterfaceStatus(ifName string) (bool, error) { + url := fmt.Sprintf("%s/api/status/%s", c.serviceURL, ifName) + resp, err := c.client.Get(url) + if err != nil { + return false, fmt.Errorf("获取接口状态失败:%w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return false, fmt.Errorf("can-bridge 服务返回错误:%d", resp.StatusCode) + } + + var status struct { + Active bool `json:"active"` + } + + if err := json.NewDecoder(resp.Body).Decode(&status); err != nil { + return false, fmt.Errorf("解析状态响应失败:%w", err) + } + + return status.Active, nil +} + +func (c *CanBridgeClient) GetAllInterfaceStatuses() (map[string]bool, error) { + url := fmt.Sprintf("%s/api/status", c.serviceURL) + resp, err := c.client.Get(url) + if err != nil { + return nil, fmt.Errorf("获取所有接口状态失败:%w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("can-bridge 服务返回错误:%d", resp.StatusCode) + } + + var statuses map[string]bool + if err := json.NewDecoder(resp.Body).Decode(&statuses); err != nil { + return nil, fmt.Errorf("解析状态响应失败:%w", err) + } + + return statuses, nil +} + +func (c *CanBridgeClient) SetServiceURL(url string) { c.serviceURL = url } + +func (c *CanBridgeClient) IsConnected() bool { + _, err := c.GetAllInterfaceStatuses() + return err == nil +} diff --git a/component/pressure_sensor.go b/component/pressure_sensor.go new file mode 100644 index 0000000..1c74a69 --- /dev/null +++ b/component/pressure_sensor.go @@ -0,0 +1,78 @@ +package component + +import ( + "fmt" + "hands/device" + "math/rand/v2" + "time" +) + +// PressureSensor 压力传感器实现 +type PressureSensor struct { + id string + config map[string]any + isActive bool + samplingRate int + lastReading time.Time +} + +func NewPressureSensor(id string, config map[string]any) *PressureSensor { + return &PressureSensor{ + id: id, + config: config, + isActive: true, + samplingRate: 100, + lastReading: time.Now(), + } +} + +func (p *PressureSensor) GetID() string { + return p.id +} + +func (p *PressureSensor) GetType() device.ComponentType { + return device.SensorComponent +} + +func (p *PressureSensor) GetConfiguration() map[string]any { + return p.config +} + +func (p *PressureSensor) IsActive() bool { + return p.isActive +} + +func (p *PressureSensor) ReadData() (device.SensorData, error) { + if !p.isActive { + return nil, fmt.Errorf("传感器 %s 未激活", p.id) + } + + // 模拟压力数据读取 + // 在实际实现中,这里应该从 can-bridge 或其他数据源读取真实数据 + pressure := rand.Float64() * 100 // 0-100 的随机压力值 + + values := map[string]any{ + "pressure": pressure, + "unit": "kPa", + "location": p.config["location"], + } + + p.lastReading = time.Now() + return NewSensorData(p.id, values), nil +} + +func (p *PressureSensor) GetDataType() string { + return "pressure" +} + +func (p *PressureSensor) GetSamplingRate() int { + return p.samplingRate +} + +func (p *PressureSensor) SetSamplingRate(rate int) error { + if rate <= 0 || rate > 1000 { + return fmt.Errorf("采样率必须在 1-1000Hz 之间") + } + p.samplingRate = rate + return nil +} diff --git a/component/sensor.go b/component/sensor.go new file mode 100644 index 0000000..66b9265 --- /dev/null +++ b/component/sensor.go @@ -0,0 +1,42 @@ +package component + +import ( + "hands/device" + "time" +) + +// Sensor 传感器组件接口 +type Sensor interface { + device.Component + ReadData() (device.SensorData, error) + GetDataType() string + GetSamplingRate() int + SetSamplingRate(rate int) error +} + +// SensorDataImpl 传感器数据的具体实现 +type SensorDataImpl struct { + timestamp time.Time + values map[string]any + sensorID string +} + +func NewSensorData(sensorID string, values map[string]any) *SensorDataImpl { + return &SensorDataImpl{ + timestamp: time.Now(), + values: values, + sensorID: sensorID, + } +} + +func (s *SensorDataImpl) Timestamp() time.Time { + return s.timestamp +} + +func (s *SensorDataImpl) Values() map[string]any { + return s.values +} + +func (s *SensorDataImpl) SensorID() string { + return s.sensorID +} diff --git a/device/animation.go b/device/animation.go new file mode 100644 index 0000000..1cf1718 --- /dev/null +++ b/device/animation.go @@ -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 +} diff --git a/device/commands.go b/device/commands.go new file mode 100644 index 0000000..fb14a71 --- /dev/null +++ b/device/commands.go @@ -0,0 +1,80 @@ +package device + +// FingerPoseCommand 手指姿态指令 +type FingerPoseCommand struct { + fingerID string + poseData []byte + targetComp string +} + +func NewFingerPoseCommand(fingerID string, poseData []byte) *FingerPoseCommand { + return &FingerPoseCommand{ + fingerID: fingerID, + poseData: poseData, + targetComp: "finger_" + fingerID, + } +} + +func (c *FingerPoseCommand) Type() string { + return "SetFingerPose" +} + +func (c *FingerPoseCommand) Payload() []byte { + return c.poseData +} + +func (c *FingerPoseCommand) TargetComponent() string { + return c.targetComp +} + +// PalmPoseCommand 手掌姿态指令 +type PalmPoseCommand struct { + poseData []byte + targetComp string +} + +func NewPalmPoseCommand(poseData []byte) *PalmPoseCommand { + return &PalmPoseCommand{ + poseData: poseData, + targetComp: "palm", + } +} + +func (c *PalmPoseCommand) Type() string { + return "SetPalmPose" +} + +func (c *PalmPoseCommand) Payload() []byte { + return c.poseData +} + +func (c *PalmPoseCommand) TargetComponent() string { + return c.targetComp +} + +// GenericCommand 通用指令 +type GenericCommand struct { + cmdType string + payload []byte + targetComp string +} + +func NewGenericCommand(cmdType string, payload []byte, targetComp string) *GenericCommand { + return &GenericCommand{ + cmdType: cmdType, + payload: payload, + targetComp: targetComp, + } +} + +func (c *GenericCommand) Type() string { + return c.cmdType +} + +func (c *GenericCommand) Payload() []byte { + return c.payload +} + +func (c *GenericCommand) TargetComponent() string { + return c.targetComp +} diff --git a/device/device.go b/device/device.go new file mode 100644 index 0000000..fe27b56 --- /dev/null +++ b/device/device.go @@ -0,0 +1,64 @@ +package device + +import ( + "hands/define" + "time" +) + +// Device 代表一个可控制的设备单元 +type Device interface { + GetID() string // 获取设备唯一标识 + GetModel() string // 获取设备型号 (例如 "L10", "L20") + GetHandType() define.HandType // 获取设备手型 + SetHandType(handType define.HandType) error // 设置设备手型 + ExecuteCommand(cmd Command) error // 执行一个通用指令 + ReadSensorData(sensorID string) (SensorData, error) // 读取特定传感器数据 + GetComponents(componentType ComponentType) []Component // 获取指定类型的组件 + GetStatus() (DeviceStatus, error) // 获取设备状态 + Connect() error // 连接设备 + Disconnect() error // 断开设备连接 + + // --- 新增 --- + PoseExecutor // 嵌入 PoseExecutor 接口,Device 需实现它 + GetAnimationEngine() *AnimationEngine // 获取设备的动画引擎 +} + +// Command 代表一个发送给设备的指令 +type Command interface { + Type() string // 指令类型,例如 "SetFingerPose", "SetPalmAngle" + Payload() []byte // 指令的实际数据 + TargetComponent() string // 目标组件 ID +} + +// SensorData 代表从传感器读取的数据 +type SensorData interface { + Timestamp() time.Time + Values() map[string]any // 例如 {"pressure": 100, "angle": 30.5} + SensorID() string +} + +// ComponentType 定义组件类型 +type ComponentType string + +const ( + SensorComponent ComponentType = "sensor" + SkinComponent ComponentType = "skin" + ActuatorComponent ComponentType = "actuator" +) + +// Component 代表设备的一个可插拔组件 +type Component interface { + GetID() string + GetType() ComponentType + GetConfiguration() map[string]interface{} // 组件的特定配置 + IsActive() bool +} + +// DeviceStatus 代表设备状态 +type DeviceStatus struct { + IsConnected bool + IsActive bool + LastUpdate time.Time + ErrorCount int + LastError string +} diff --git a/device/engine.go b/device/engine.go new file mode 100644 index 0000000..e30d568 --- /dev/null +++ b/device/engine.go @@ -0,0 +1,191 @@ +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 +} + +// 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) + } +} diff --git a/device/factory.go b/device/factory.go new file mode 100644 index 0000000..b147563 --- /dev/null +++ b/device/factory.go @@ -0,0 +1,35 @@ +package device + +import "fmt" + +// DeviceFactory 设备工厂 +type DeviceFactory struct { + constructors map[string]func(config map[string]any) (Device, error) +} + +var defaultFactory = &DeviceFactory{ + constructors: make(map[string]func(config map[string]any) (Device, error)), +} + +// RegisterDeviceType 注册设备类型 +func RegisterDeviceType(modelName string, constructor func(config map[string]any) (Device, error)) { + defaultFactory.constructors[modelName] = constructor +} + +// CreateDevice 创建设备实例 +func CreateDevice(modelName string, config map[string]any) (Device, error) { + constructor, ok := defaultFactory.constructors[modelName] + if !ok { + return nil, fmt.Errorf("未知的设备型号: %s", modelName) + } + return constructor(config) +} + +// GetSupportedModels 获取支持的设备型号列表 +func GetSupportedModels() []string { + models := make([]string, 0, len(defaultFactory.constructors)) + for model := range defaultFactory.constructors { + models = append(models, model) + } + return models +} diff --git a/device/manager.go b/device/manager.go new file mode 100644 index 0000000..d7d6e8c --- /dev/null +++ b/device/manager.go @@ -0,0 +1,63 @@ +package device + +import ( + "fmt" + "sync" +) + +// DeviceManager 管理设备实例 +type DeviceManager struct { + devices map[string]Device + mutex sync.RWMutex +} + +func NewDeviceManager() *DeviceManager { return &DeviceManager{devices: make(map[string]Device)} } + +func (m *DeviceManager) RegisterDevice(dev Device) error { + m.mutex.Lock() + defer m.mutex.Unlock() + + id := dev.GetID() + if _, exists := m.devices[id]; exists { + return fmt.Errorf("设备 %s 已存在", id) + } + + m.devices[id] = dev + return nil +} + +func (m *DeviceManager) GetDevice(id string) (Device, error) { + m.mutex.RLock() + defer m.mutex.RUnlock() + + dev, exists := m.devices[id] + if !exists { + return nil, fmt.Errorf("设备 %s 不存在", id) + } + + return dev, nil +} + +func (m *DeviceManager) GetAllDevices() []Device { + m.mutex.RLock() + defer m.mutex.RUnlock() + + devices := make([]Device, 0, len(m.devices)) + for _, dev := range m.devices { + devices = append(devices, dev) + } + + return devices +} + +func (m *DeviceManager) RemoveDevice(id string) error { + m.mutex.Lock() + defer m.mutex.Unlock() + + if _, exists := m.devices[id]; !exists { + return fmt.Errorf("设备 %s 不存在", id) + } + + delete(m.devices, id) + return nil +} diff --git a/device/models/init.go b/device/models/init.go new file mode 100644 index 0000000..7bf9a3f --- /dev/null +++ b/device/models/init.go @@ -0,0 +1,8 @@ +package models + +import "hands/device" + +func init() { + // 注册 L10 设备类型 + device.RegisterDeviceType("L10", NewL10Hand) +} diff --git a/device/models/l10.go b/device/models/l10.go new file mode 100644 index 0000000..ecfdcb1 --- /dev/null +++ b/device/models/l10.go @@ -0,0 +1,330 @@ +package models + +import ( + "fmt" + "log" + "math/rand/v2" + "sync" + "time" + + "hands/define" + "hands/communication" + "hands/component" + "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 // 动画引擎 +} + +// 在 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 手部设备实例 +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()) + + // 初始化组件 + 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 +} diff --git a/device/models/l10_animation.go b/device/models/l10_animation.go new file mode 100644 index 0000000..b4e8b56 --- /dev/null +++ b/device/models/l10_animation.go @@ -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 // 完成一个周期 +} diff --git a/device/pose_executor.go b/device/pose_executor.go new file mode 100644 index 0000000..eb79dcb --- /dev/null +++ b/device/pose_executor.go @@ -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 +} From 7d0d779bf40dbde1504e463d7d2ab9fc32fc306d Mon Sep 17 00:00:00 2001 From: Eli Yip Date: Wed, 28 May 2025 09:52:41 +0800 Subject: [PATCH 07/20] chore: use new device in main.go --- main.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/main.go b/main.go index 1f6c93e..fb5e036 100644 --- a/main.go +++ b/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) From 228c9ca07f7c4fd5fb3a8a263b4f964f3d8b4d6c Mon Sep 17 00:00:00 2001 From: Eli Yip Date: Wed, 28 May 2025 10:54:57 +0800 Subject: [PATCH 08/20] chore: remove unused file --- pkg/communication/communicator.go | 122 ----------- pkg/component/pressure_sensor.go | 78 ------- pkg/component/sensor.go | 42 ---- pkg/device/animation.go | 12 -- pkg/device/commands.go | 80 ------- pkg/device/device.go | 64 ------ pkg/device/engine.go | 191 ----------------- pkg/device/factory.go | 35 --- pkg/device/manager.go | 63 ------ pkg/device/models/init.go | 8 - pkg/device/models/l10.go | 330 ----------------------------- pkg/device/models/l10_animation.go | 125 ----------- pkg/device/pose_executor.go | 20 -- 13 files changed, 1170 deletions(-) delete mode 100644 pkg/communication/communicator.go delete mode 100644 pkg/component/pressure_sensor.go delete mode 100644 pkg/component/sensor.go delete mode 100644 pkg/device/animation.go delete mode 100644 pkg/device/commands.go delete mode 100644 pkg/device/device.go delete mode 100644 pkg/device/engine.go delete mode 100644 pkg/device/factory.go delete mode 100644 pkg/device/manager.go delete mode 100644 pkg/device/models/init.go delete mode 100644 pkg/device/models/l10.go delete mode 100644 pkg/device/models/l10_animation.go delete mode 100644 pkg/device/pose_executor.go diff --git a/pkg/communication/communicator.go b/pkg/communication/communicator.go deleted file mode 100644 index 41d2323..0000000 --- a/pkg/communication/communicator.go +++ /dev/null @@ -1,122 +0,0 @@ -package communication - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "time" -) - -// TODO: ID 的作用是什么 -// RawMessage 代表发送给 can-bridge 服务或从其接收的原始消息结构 -type RawMessage struct { - Interface string `json:"interface"` // 目标 CAN 接口名,例如 "can0", "vcan1" - ID uint32 `json:"id"` // CAN 帧的 ID - Data []byte `json:"data"` // CAN 帧的数据负载 -} - -// Communicator 定义了与 can-bridge Web 服务进行通信的接口 -type Communicator interface { - // SendMessage 将 RawMessage 通过 HTTP POST 请求发送到 can-bridge 服务 - SendMessage(msg RawMessage) error - - // GetInterfaceStatus 获取指定 CAN 接口的状态 - GetInterfaceStatus(ifName string) (isActive bool, err error) - - // GetAllInterfaceStatuses 获取所有已知 CAN 接口的状态 - GetAllInterfaceStatuses() (statuses map[string]bool, err error) - - // SetServiceURL 设置 can-bridge 服务的 URL - SetServiceURL(url string) - - // IsConnected 检查与 can-bridge 服务的连接状态 - IsConnected() bool -} - -// CanBridgeClient 实现与 can-bridge 服务的 HTTP 通信 -type CanBridgeClient struct { - serviceURL string - client *http.Client -} - -func NewCanBridgeClient(serviceURL string) Communicator { - return &CanBridgeClient{ - serviceURL: serviceURL, - client: &http.Client{ - Timeout: 5 * time.Second, - }, - } -} - -func (c *CanBridgeClient) SendMessage(msg RawMessage) error { - jsonData, err := json.Marshal(msg) - if err != nil { - return fmt.Errorf("序列化消息失败:%w", err) - } - - url := fmt.Sprintf("%s/api/can", c.serviceURL) - resp, err := c.client.Post(url, "application/json", bytes.NewBuffer(jsonData)) - if err != nil { - return fmt.Errorf("发送 HTTP 请求失败:%w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return fmt.Errorf("can-bridge服务返回错误: %d, %s", resp.StatusCode, string(body)) - } - - return nil -} - -func (c *CanBridgeClient) GetInterfaceStatus(ifName string) (bool, error) { - url := fmt.Sprintf("%s/api/status/%s", c.serviceURL, ifName) - resp, err := c.client.Get(url) - if err != nil { - return false, fmt.Errorf("获取接口状态失败:%w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return false, fmt.Errorf("can-bridge 服务返回错误:%d", resp.StatusCode) - } - - var status struct { - Active bool `json:"active"` - } - - if err := json.NewDecoder(resp.Body).Decode(&status); err != nil { - return false, fmt.Errorf("解析状态响应失败:%w", err) - } - - return status.Active, nil -} - -func (c *CanBridgeClient) GetAllInterfaceStatuses() (map[string]bool, error) { - url := fmt.Sprintf("%s/api/status", c.serviceURL) - resp, err := c.client.Get(url) - if err != nil { - return nil, fmt.Errorf("获取所有接口状态失败:%w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("can-bridge 服务返回错误:%d", resp.StatusCode) - } - - var statuses map[string]bool - if err := json.NewDecoder(resp.Body).Decode(&statuses); err != nil { - return nil, fmt.Errorf("解析状态响应失败:%w", err) - } - - return statuses, nil -} - -func (c *CanBridgeClient) SetServiceURL(url string) { c.serviceURL = url } - -func (c *CanBridgeClient) IsConnected() bool { - _, err := c.GetAllInterfaceStatuses() - return err == nil -} diff --git a/pkg/component/pressure_sensor.go b/pkg/component/pressure_sensor.go deleted file mode 100644 index 1c1d454..0000000 --- a/pkg/component/pressure_sensor.go +++ /dev/null @@ -1,78 +0,0 @@ -package component - -import ( - "fmt" - "hands/pkg/device" - "math/rand/v2" - "time" -) - -// PressureSensor 压力传感器实现 -type PressureSensor struct { - id string - config map[string]any - isActive bool - samplingRate int - lastReading time.Time -} - -func NewPressureSensor(id string, config map[string]any) *PressureSensor { - return &PressureSensor{ - id: id, - config: config, - isActive: true, - samplingRate: 100, - lastReading: time.Now(), - } -} - -func (p *PressureSensor) GetID() string { - return p.id -} - -func (p *PressureSensor) GetType() device.ComponentType { - return device.SensorComponent -} - -func (p *PressureSensor) GetConfiguration() map[string]any { - return p.config -} - -func (p *PressureSensor) IsActive() bool { - return p.isActive -} - -func (p *PressureSensor) ReadData() (device.SensorData, error) { - if !p.isActive { - return nil, fmt.Errorf("传感器 %s 未激活", p.id) - } - - // 模拟压力数据读取 - // 在实际实现中,这里应该从 can-bridge 或其他数据源读取真实数据 - pressure := rand.Float64() * 100 // 0-100 的随机压力值 - - values := map[string]any{ - "pressure": pressure, - "unit": "kPa", - "location": p.config["location"], - } - - p.lastReading = time.Now() - return NewSensorData(p.id, values), nil -} - -func (p *PressureSensor) GetDataType() string { - return "pressure" -} - -func (p *PressureSensor) GetSamplingRate() int { - return p.samplingRate -} - -func (p *PressureSensor) SetSamplingRate(rate int) error { - if rate <= 0 || rate > 1000 { - return fmt.Errorf("采样率必须在 1-1000Hz 之间") - } - p.samplingRate = rate - return nil -} diff --git a/pkg/component/sensor.go b/pkg/component/sensor.go deleted file mode 100644 index c684e19..0000000 --- a/pkg/component/sensor.go +++ /dev/null @@ -1,42 +0,0 @@ -package component - -import ( - "hands/pkg/device" - "time" -) - -// Sensor 传感器组件接口 -type Sensor interface { - device.Component - ReadData() (device.SensorData, error) - GetDataType() string - GetSamplingRate() int - SetSamplingRate(rate int) error -} - -// SensorDataImpl 传感器数据的具体实现 -type SensorDataImpl struct { - timestamp time.Time - values map[string]any - sensorID string -} - -func NewSensorData(sensorID string, values map[string]any) *SensorDataImpl { - return &SensorDataImpl{ - timestamp: time.Now(), - values: values, - sensorID: sensorID, - } -} - -func (s *SensorDataImpl) Timestamp() time.Time { - return s.timestamp -} - -func (s *SensorDataImpl) Values() map[string]any { - return s.values -} - -func (s *SensorDataImpl) SensorID() string { - return s.sensorID -} diff --git a/pkg/device/animation.go b/pkg/device/animation.go deleted file mode 100644 index 1cf1718..0000000 --- a/pkg/device/animation.go +++ /dev/null @@ -1,12 +0,0 @@ -package device - -// Animation 定义了一个动画序列的行为 -type Animation interface { - // Run 执行动画的一个周期或直到被停止 - // executor: 用于执行姿态指令 - // stop: 接收停止信号的通道 - // speedMs: 动画执行的速度(毫秒) - Run(executor PoseExecutor, stop <-chan struct{}, speedMs int) error - // Name 返回动画的名称 - Name() string -} diff --git a/pkg/device/commands.go b/pkg/device/commands.go deleted file mode 100644 index fb14a71..0000000 --- a/pkg/device/commands.go +++ /dev/null @@ -1,80 +0,0 @@ -package device - -// FingerPoseCommand 手指姿态指令 -type FingerPoseCommand struct { - fingerID string - poseData []byte - targetComp string -} - -func NewFingerPoseCommand(fingerID string, poseData []byte) *FingerPoseCommand { - return &FingerPoseCommand{ - fingerID: fingerID, - poseData: poseData, - targetComp: "finger_" + fingerID, - } -} - -func (c *FingerPoseCommand) Type() string { - return "SetFingerPose" -} - -func (c *FingerPoseCommand) Payload() []byte { - return c.poseData -} - -func (c *FingerPoseCommand) TargetComponent() string { - return c.targetComp -} - -// PalmPoseCommand 手掌姿态指令 -type PalmPoseCommand struct { - poseData []byte - targetComp string -} - -func NewPalmPoseCommand(poseData []byte) *PalmPoseCommand { - return &PalmPoseCommand{ - poseData: poseData, - targetComp: "palm", - } -} - -func (c *PalmPoseCommand) Type() string { - return "SetPalmPose" -} - -func (c *PalmPoseCommand) Payload() []byte { - return c.poseData -} - -func (c *PalmPoseCommand) TargetComponent() string { - return c.targetComp -} - -// GenericCommand 通用指令 -type GenericCommand struct { - cmdType string - payload []byte - targetComp string -} - -func NewGenericCommand(cmdType string, payload []byte, targetComp string) *GenericCommand { - return &GenericCommand{ - cmdType: cmdType, - payload: payload, - targetComp: targetComp, - } -} - -func (c *GenericCommand) Type() string { - return c.cmdType -} - -func (c *GenericCommand) Payload() []byte { - return c.payload -} - -func (c *GenericCommand) TargetComponent() string { - return c.targetComp -} diff --git a/pkg/device/device.go b/pkg/device/device.go deleted file mode 100644 index fe27b56..0000000 --- a/pkg/device/device.go +++ /dev/null @@ -1,64 +0,0 @@ -package device - -import ( - "hands/define" - "time" -) - -// Device 代表一个可控制的设备单元 -type Device interface { - GetID() string // 获取设备唯一标识 - GetModel() string // 获取设备型号 (例如 "L10", "L20") - GetHandType() define.HandType // 获取设备手型 - SetHandType(handType define.HandType) error // 设置设备手型 - ExecuteCommand(cmd Command) error // 执行一个通用指令 - ReadSensorData(sensorID string) (SensorData, error) // 读取特定传感器数据 - GetComponents(componentType ComponentType) []Component // 获取指定类型的组件 - GetStatus() (DeviceStatus, error) // 获取设备状态 - Connect() error // 连接设备 - Disconnect() error // 断开设备连接 - - // --- 新增 --- - PoseExecutor // 嵌入 PoseExecutor 接口,Device 需实现它 - GetAnimationEngine() *AnimationEngine // 获取设备的动画引擎 -} - -// Command 代表一个发送给设备的指令 -type Command interface { - Type() string // 指令类型,例如 "SetFingerPose", "SetPalmAngle" - Payload() []byte // 指令的实际数据 - TargetComponent() string // 目标组件 ID -} - -// SensorData 代表从传感器读取的数据 -type SensorData interface { - Timestamp() time.Time - Values() map[string]any // 例如 {"pressure": 100, "angle": 30.5} - SensorID() string -} - -// ComponentType 定义组件类型 -type ComponentType string - -const ( - SensorComponent ComponentType = "sensor" - SkinComponent ComponentType = "skin" - ActuatorComponent ComponentType = "actuator" -) - -// Component 代表设备的一个可插拔组件 -type Component interface { - GetID() string - GetType() ComponentType - GetConfiguration() map[string]interface{} // 组件的特定配置 - IsActive() bool -} - -// DeviceStatus 代表设备状态 -type DeviceStatus struct { - IsConnected bool - IsActive bool - LastUpdate time.Time - ErrorCount int - LastError string -} diff --git a/pkg/device/engine.go b/pkg/device/engine.go deleted file mode 100644 index e30d568..0000000 --- a/pkg/device/engine.go +++ /dev/null @@ -1,191 +0,0 @@ -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 -} - -// 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) - } -} diff --git a/pkg/device/factory.go b/pkg/device/factory.go deleted file mode 100644 index b147563..0000000 --- a/pkg/device/factory.go +++ /dev/null @@ -1,35 +0,0 @@ -package device - -import "fmt" - -// DeviceFactory 设备工厂 -type DeviceFactory struct { - constructors map[string]func(config map[string]any) (Device, error) -} - -var defaultFactory = &DeviceFactory{ - constructors: make(map[string]func(config map[string]any) (Device, error)), -} - -// RegisterDeviceType 注册设备类型 -func RegisterDeviceType(modelName string, constructor func(config map[string]any) (Device, error)) { - defaultFactory.constructors[modelName] = constructor -} - -// CreateDevice 创建设备实例 -func CreateDevice(modelName string, config map[string]any) (Device, error) { - constructor, ok := defaultFactory.constructors[modelName] - if !ok { - return nil, fmt.Errorf("未知的设备型号: %s", modelName) - } - return constructor(config) -} - -// GetSupportedModels 获取支持的设备型号列表 -func GetSupportedModels() []string { - models := make([]string, 0, len(defaultFactory.constructors)) - for model := range defaultFactory.constructors { - models = append(models, model) - } - return models -} diff --git a/pkg/device/manager.go b/pkg/device/manager.go deleted file mode 100644 index d7d6e8c..0000000 --- a/pkg/device/manager.go +++ /dev/null @@ -1,63 +0,0 @@ -package device - -import ( - "fmt" - "sync" -) - -// DeviceManager 管理设备实例 -type DeviceManager struct { - devices map[string]Device - mutex sync.RWMutex -} - -func NewDeviceManager() *DeviceManager { return &DeviceManager{devices: make(map[string]Device)} } - -func (m *DeviceManager) RegisterDevice(dev Device) error { - m.mutex.Lock() - defer m.mutex.Unlock() - - id := dev.GetID() - if _, exists := m.devices[id]; exists { - return fmt.Errorf("设备 %s 已存在", id) - } - - m.devices[id] = dev - return nil -} - -func (m *DeviceManager) GetDevice(id string) (Device, error) { - m.mutex.RLock() - defer m.mutex.RUnlock() - - dev, exists := m.devices[id] - if !exists { - return nil, fmt.Errorf("设备 %s 不存在", id) - } - - return dev, nil -} - -func (m *DeviceManager) GetAllDevices() []Device { - m.mutex.RLock() - defer m.mutex.RUnlock() - - devices := make([]Device, 0, len(m.devices)) - for _, dev := range m.devices { - devices = append(devices, dev) - } - - return devices -} - -func (m *DeviceManager) RemoveDevice(id string) error { - m.mutex.Lock() - defer m.mutex.Unlock() - - if _, exists := m.devices[id]; !exists { - return fmt.Errorf("设备 %s 不存在", id) - } - - delete(m.devices, id) - return nil -} diff --git a/pkg/device/models/init.go b/pkg/device/models/init.go deleted file mode 100644 index c3910c7..0000000 --- a/pkg/device/models/init.go +++ /dev/null @@ -1,8 +0,0 @@ -package models - -import "hands/pkg/device" - -func init() { - // 注册 L10 设备类型 - device.RegisterDeviceType("L10", NewL10Hand) -} diff --git a/pkg/device/models/l10.go b/pkg/device/models/l10.go deleted file mode 100644 index 6f0f429..0000000 --- a/pkg/device/models/l10.go +++ /dev/null @@ -1,330 +0,0 @@ -package models - -import ( - "fmt" - "log" - "math/rand/v2" - "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" - animationEngine *device.AnimationEngine // 动画引擎 -} - -// 在 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 手部设备实例 -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()) - - // 初始化组件 - 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 -} diff --git a/pkg/device/models/l10_animation.go b/pkg/device/models/l10_animation.go deleted file mode 100644 index db729d0..0000000 --- a/pkg/device/models/l10_animation.go +++ /dev/null @@ -1,125 +0,0 @@ -package models - -import ( - "hands/pkg/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 // 完成一个周期 -} diff --git a/pkg/device/pose_executor.go b/pkg/device/pose_executor.go deleted file mode 100644 index eb79dcb..0000000 --- a/pkg/device/pose_executor.go +++ /dev/null @@ -1,20 +0,0 @@ -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 -} From 929ece2db1acdf9100993566266e1fdaaf5f969c Mon Sep 17 00:00:00 2001 From: Eli Yip Date: Wed, 28 May 2025 10:55:19 +0800 Subject: [PATCH 09/20] chore: stash temp files --- cmd/hand_controller/main.go | 183 ++++++++++++++++++++++++++++++++++++ pkg/config/config.go | 105 +++++++++++++++++++++ 2 files changed, 288 insertions(+) create mode 100644 cmd/hand_controller/main.go create mode 100644 pkg/config/config.go diff --git a/cmd/hand_controller/main.go b/cmd/hand_controller/main.go new file mode 100644 index 0000000..2bb2d6f --- /dev/null +++ b/cmd/hand_controller/main.go @@ -0,0 +1,183 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + "os/signal" + "syscall" + "time" + + "hands/pkg/config" + "hands/pkg/control" + "hands/pkg/control/modes" + "hands/pkg/device" + _ "hands/pkg/device/models" // 导入以注册设备类型 +) + +func main() { + // 解析命令行参数 + configPath := flag.String("config", "config.json", "配置文件路径") + flag.Parse() + + // 加载配置 + cfg, err := loadOrCreateConfig(*configPath) + if err != nil { + log.Fatalf("加载配置失败: %v", err) + } + + // 创建设备管理器 + deviceManager := device.NewDeviceManager() + + // 创建并注册设备 + for _, deviceCfg := range cfg.Devices { + dev, err := createDevice(deviceCfg, cfg.CanServiceURL) + if err != nil { + log.Printf("创建设备 %s 失败: %v", deviceCfg.ID, err) + continue + } + + if err := deviceManager.RegisterDevice(dev); err != nil { + log.Printf("注册设备 %s 失败: %v", deviceCfg.ID, err) + continue + } + + log.Printf("成功注册设备: %s (%s)", deviceCfg.ID, deviceCfg.Model) + } + + // 创建操作模式管理器 + modeManager := control.NewModeManager() + + // 注册操作模式 + modeManager.RegisterMode(modes.NewDirectPoseMode()) + + animFactory := modes.NewAnimationFactory() + for _, animType := range animFactory.GetSupportedAnimations() { + anim, _ := animFactory.CreateAnimation(animType) + modeManager.RegisterMode(anim) + } + + // 设置默认设备 + if len(cfg.Devices) > 0 { + defaultDev, err := deviceManager.GetDevice(cfg.DefaultDevice.ID) + if err != nil { + log.Printf("获取默认设备失败: %v", err) + } else { + modeManager.SetDevice(defaultDev) + + // 连接设备 + if err := defaultDev.Connect(); err != nil { + log.Printf("连接设备失败: %v", err) + } else { + log.Printf("成功连接设备: %s", cfg.DefaultDevice.ID) + } + } + } + + // 启动默认模式 + if err := modeManager.SwitchMode("DirectPose", nil); err != nil { + log.Printf("启动默认模式失败: %v", err) + } else { + log.Println("成功启动直接姿态控制模式") + } + + // 演示功能 + demonstrateFeatures(modeManager, deviceManager) + + // 等待中断信号 + waitForShutdown() + + log.Println("应用程序正在关闭...") +} + +func loadOrCreateConfig(configPath string) (*config.Config, error) { + if _, err := os.Stat(configPath); os.IsNotExist(err) { + // 配置文件不存在,创建默认配置 + cfg := config.GetDefaultConfig() + if err := config.SaveConfig(cfg, configPath); err != nil { + return nil, fmt.Errorf("保存默认配置失败:%w", err) + } + log.Printf("创建默认配置文件: %s", configPath) + return cfg, nil + } + + return config.LoadConfig(configPath) +} + +func createDevice(deviceCfg config.DeviceConfig, canServiceURL string) (device.Device, error) { + deviceConfig := map[string]interface{}{ + "id": deviceCfg.ID, + "can_service_url": canServiceURL, + "can_interface": deviceCfg.CanInterface, + } + + // 合并设备特定参数 + for k, v := range deviceCfg.Parameters { + deviceConfig[k] = v + } + + return device.CreateDevice(deviceCfg.Model, deviceConfig) +} + +func demonstrateFeatures(modeManager *control.ModeManager, deviceManager *device.DeviceManager) { + log.Println("开始功能演示...") + + // 演示直接姿态控制 + log.Println("演示直接姿态控制...") + if activeMode := modeManager.GetActiveMode(); activeMode != nil { + if poseMode, ok := activeMode.(*modes.DirectPoseMode); ok { + // 获取设备 + devices := deviceManager.GetAllDevices() + if len(devices) > 0 { + dev := devices[0] + + // 发送手指姿态 + fingerPoses := map[string][]byte{ + "thumb": {45}, + "index": {30}, + "middle": {60}, + "ring": {45}, + "pinky": {30}, + } + + inputs := map[string]interface{}{ + "finger_poses": fingerPoses, + } + + if err := poseMode.Update(dev, inputs); err != nil { + log.Printf("发送手指姿态失败: %v", err) + } else { + log.Println("成功发送手指姿态") + } + + time.Sleep(2 * time.Second) + } + } + } + + // 演示动画模式 + log.Println("演示波浪动画...") + params := map[string]interface{}{ + "duration": 3 * time.Second, + "frames": 30, + } + + if err := modeManager.SwitchMode("Animation_wave", params); err != nil { + log.Printf("切换到波浪动画失败: %v", err) + } else { + log.Println("成功启动波浪动画") + time.Sleep(4 * time.Second) + } + + // 切换回直接控制模式 + if err := modeManager.SwitchMode("DirectPose", nil); err != nil { + log.Printf("切换回直接控制模式失败: %v", err) + } +} + +func waitForShutdown() { + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + <-sigChan +} diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..e4029d3 --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,105 @@ +package config + +import ( + "encoding/json" + "fmt" + "os" +) + +// Config 应用配置 +type Config struct { + CanServiceURL string `json:"can_service_url"` + DefaultDevice DeviceConfig `json:"default_device"` + Devices []DeviceConfig `json:"devices"` + Server ServerConfig `json:"server"` +} + +// DeviceConfig 设备配置 +type DeviceConfig struct { + ID string `json:"id"` + Model string `json:"model"` + CanInterface string `json:"can_interface"` + Parameters map[string]interface{} `json:"parameters"` +} + +// ServerConfig 服务器配置 +type ServerConfig struct { + Port int `json:"port"` + Host string `json:"host"` + LogLevel string `json:"log_level"` + EnableCORS bool `json:"enable_cors"` +} + +// LoadConfig 从文件加载配置 +func LoadConfig(configPath string) (*Config, error) { + file, err := os.Open(configPath) + if err != nil { + return nil, fmt.Errorf("打开配置文件失败:%w", err) + } + defer file.Close() + + var config Config + decoder := json.NewDecoder(file) + if err := decoder.Decode(&config); err != nil { + return nil, fmt.Errorf("解析配置文件失败:%w", err) + } + + // 设置默认值 + if config.Server.Port == 0 { + config.Server.Port = 8080 + } + if config.Server.Host == "" { + config.Server.Host = "localhost" + } + if config.Server.LogLevel == "" { + config.Server.LogLevel = "info" + } + + return &config, nil +} + +// SaveConfig 保存配置到文件 +func SaveConfig(config *Config, configPath string) error { + file, err := os.Create(configPath) + if err != nil { + return fmt.Errorf("创建配置文件失败:%w", err) + } + defer file.Close() + + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + if err := encoder.Encode(config); err != nil { + return fmt.Errorf("保存配置文件失败:%w", err) + } + + return nil +} + +// GetDefaultConfig 获取默认配置 +func GetDefaultConfig() *Config { + return &Config{ + CanServiceURL: "http://localhost:8081", + DefaultDevice: DeviceConfig{ + ID: "left_hand", + Model: "L10", + CanInterface: "can0", + Parameters: make(map[string]interface{}), + }, + Devices: []DeviceConfig{ + { + ID: "left_hand", + Model: "L10", + CanInterface: "can0", + Parameters: map[string]interface{}{ + "hand_type": "left", + }, + }, + }, + Server: ServerConfig{ + Port: 8080, + Host: "localhost", + LogLevel: "info", + EnableCORS: true, + }, + } +} From 1d1cff4805bc882c7b63f8097136b0029f5c702c Mon Sep 17 00:00:00 2001 From: Eli Yip Date: Wed, 28 May 2025 11:03:27 +0800 Subject: [PATCH 10/20] ci: fix Dockerfile bin location --- .goreleaser.yaml | 2 ++ Dockerfile | 4 ++-- Dockerfile.goreleaser | 8 ++++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 46acb6e..d7e2972 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -8,6 +8,8 @@ before: builds: - id: dashboard-server + env: + - CGO_ENABLED=0 goos: - linux goarch: diff --git a/Dockerfile b/Dockerfile index 9537283..6dfa4f3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] \ No newline at end of file +CMD ["/app/dashboard-server"] \ No newline at end of file diff --git a/Dockerfile.goreleaser b/Dockerfile.goreleaser index fcf31a5..6a8f670 100644 --- a/Dockerfile.goreleaser +++ b/Dockerfile.goreleaser @@ -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"] From fc9ec7c165ced0a4b3c42b20e9af36745a00f45f Mon Sep 17 00:00:00 2001 From: Eli Yip Date: Wed, 28 May 2025 14:15:21 +0800 Subject: [PATCH 11/20] docs: add config field desc for NewL10Hand --- cmd/hand_controller/main.go | 183 ------------------------------------ device/models/l10.go | 7 +- pkg/config/config.go | 105 --------------------- 3 files changed, 6 insertions(+), 289 deletions(-) delete mode 100644 cmd/hand_controller/main.go delete mode 100644 pkg/config/config.go diff --git a/cmd/hand_controller/main.go b/cmd/hand_controller/main.go deleted file mode 100644 index 2bb2d6f..0000000 --- a/cmd/hand_controller/main.go +++ /dev/null @@ -1,183 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "log" - "os" - "os/signal" - "syscall" - "time" - - "hands/pkg/config" - "hands/pkg/control" - "hands/pkg/control/modes" - "hands/pkg/device" - _ "hands/pkg/device/models" // 导入以注册设备类型 -) - -func main() { - // 解析命令行参数 - configPath := flag.String("config", "config.json", "配置文件路径") - flag.Parse() - - // 加载配置 - cfg, err := loadOrCreateConfig(*configPath) - if err != nil { - log.Fatalf("加载配置失败: %v", err) - } - - // 创建设备管理器 - deviceManager := device.NewDeviceManager() - - // 创建并注册设备 - for _, deviceCfg := range cfg.Devices { - dev, err := createDevice(deviceCfg, cfg.CanServiceURL) - if err != nil { - log.Printf("创建设备 %s 失败: %v", deviceCfg.ID, err) - continue - } - - if err := deviceManager.RegisterDevice(dev); err != nil { - log.Printf("注册设备 %s 失败: %v", deviceCfg.ID, err) - continue - } - - log.Printf("成功注册设备: %s (%s)", deviceCfg.ID, deviceCfg.Model) - } - - // 创建操作模式管理器 - modeManager := control.NewModeManager() - - // 注册操作模式 - modeManager.RegisterMode(modes.NewDirectPoseMode()) - - animFactory := modes.NewAnimationFactory() - for _, animType := range animFactory.GetSupportedAnimations() { - anim, _ := animFactory.CreateAnimation(animType) - modeManager.RegisterMode(anim) - } - - // 设置默认设备 - if len(cfg.Devices) > 0 { - defaultDev, err := deviceManager.GetDevice(cfg.DefaultDevice.ID) - if err != nil { - log.Printf("获取默认设备失败: %v", err) - } else { - modeManager.SetDevice(defaultDev) - - // 连接设备 - if err := defaultDev.Connect(); err != nil { - log.Printf("连接设备失败: %v", err) - } else { - log.Printf("成功连接设备: %s", cfg.DefaultDevice.ID) - } - } - } - - // 启动默认模式 - if err := modeManager.SwitchMode("DirectPose", nil); err != nil { - log.Printf("启动默认模式失败: %v", err) - } else { - log.Println("成功启动直接姿态控制模式") - } - - // 演示功能 - demonstrateFeatures(modeManager, deviceManager) - - // 等待中断信号 - waitForShutdown() - - log.Println("应用程序正在关闭...") -} - -func loadOrCreateConfig(configPath string) (*config.Config, error) { - if _, err := os.Stat(configPath); os.IsNotExist(err) { - // 配置文件不存在,创建默认配置 - cfg := config.GetDefaultConfig() - if err := config.SaveConfig(cfg, configPath); err != nil { - return nil, fmt.Errorf("保存默认配置失败:%w", err) - } - log.Printf("创建默认配置文件: %s", configPath) - return cfg, nil - } - - return config.LoadConfig(configPath) -} - -func createDevice(deviceCfg config.DeviceConfig, canServiceURL string) (device.Device, error) { - deviceConfig := map[string]interface{}{ - "id": deviceCfg.ID, - "can_service_url": canServiceURL, - "can_interface": deviceCfg.CanInterface, - } - - // 合并设备特定参数 - for k, v := range deviceCfg.Parameters { - deviceConfig[k] = v - } - - return device.CreateDevice(deviceCfg.Model, deviceConfig) -} - -func demonstrateFeatures(modeManager *control.ModeManager, deviceManager *device.DeviceManager) { - log.Println("开始功能演示...") - - // 演示直接姿态控制 - log.Println("演示直接姿态控制...") - if activeMode := modeManager.GetActiveMode(); activeMode != nil { - if poseMode, ok := activeMode.(*modes.DirectPoseMode); ok { - // 获取设备 - devices := deviceManager.GetAllDevices() - if len(devices) > 0 { - dev := devices[0] - - // 发送手指姿态 - fingerPoses := map[string][]byte{ - "thumb": {45}, - "index": {30}, - "middle": {60}, - "ring": {45}, - "pinky": {30}, - } - - inputs := map[string]interface{}{ - "finger_poses": fingerPoses, - } - - if err := poseMode.Update(dev, inputs); err != nil { - log.Printf("发送手指姿态失败: %v", err) - } else { - log.Println("成功发送手指姿态") - } - - time.Sleep(2 * time.Second) - } - } - } - - // 演示动画模式 - log.Println("演示波浪动画...") - params := map[string]interface{}{ - "duration": 3 * time.Second, - "frames": 30, - } - - if err := modeManager.SwitchMode("Animation_wave", params); err != nil { - log.Printf("切换到波浪动画失败: %v", err) - } else { - log.Println("成功启动波浪动画") - time.Sleep(4 * time.Second) - } - - // 切换回直接控制模式 - if err := modeManager.SwitchMode("DirectPose", nil); err != nil { - log.Printf("切换回直接控制模式失败: %v", err) - } -} - -func waitForShutdown() { - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) - <-sigChan -} diff --git a/device/models/l10.go b/device/models/l10.go index ecfdcb1..e808a76 100644 --- a/device/models/l10.go +++ b/device/models/l10.go @@ -7,9 +7,9 @@ import ( "sync" "time" - "hands/define" "hands/communication" "hands/component" + "hands/define" "hands/device" ) @@ -40,6 +40,11 @@ func perturb(base byte, delta int) byte { } // 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 { diff --git a/pkg/config/config.go b/pkg/config/config.go deleted file mode 100644 index e4029d3..0000000 --- a/pkg/config/config.go +++ /dev/null @@ -1,105 +0,0 @@ -package config - -import ( - "encoding/json" - "fmt" - "os" -) - -// Config 应用配置 -type Config struct { - CanServiceURL string `json:"can_service_url"` - DefaultDevice DeviceConfig `json:"default_device"` - Devices []DeviceConfig `json:"devices"` - Server ServerConfig `json:"server"` -} - -// DeviceConfig 设备配置 -type DeviceConfig struct { - ID string `json:"id"` - Model string `json:"model"` - CanInterface string `json:"can_interface"` - Parameters map[string]interface{} `json:"parameters"` -} - -// ServerConfig 服务器配置 -type ServerConfig struct { - Port int `json:"port"` - Host string `json:"host"` - LogLevel string `json:"log_level"` - EnableCORS bool `json:"enable_cors"` -} - -// LoadConfig 从文件加载配置 -func LoadConfig(configPath string) (*Config, error) { - file, err := os.Open(configPath) - if err != nil { - return nil, fmt.Errorf("打开配置文件失败:%w", err) - } - defer file.Close() - - var config Config - decoder := json.NewDecoder(file) - if err := decoder.Decode(&config); err != nil { - return nil, fmt.Errorf("解析配置文件失败:%w", err) - } - - // 设置默认值 - if config.Server.Port == 0 { - config.Server.Port = 8080 - } - if config.Server.Host == "" { - config.Server.Host = "localhost" - } - if config.Server.LogLevel == "" { - config.Server.LogLevel = "info" - } - - return &config, nil -} - -// SaveConfig 保存配置到文件 -func SaveConfig(config *Config, configPath string) error { - file, err := os.Create(configPath) - if err != nil { - return fmt.Errorf("创建配置文件失败:%w", err) - } - defer file.Close() - - encoder := json.NewEncoder(file) - encoder.SetIndent("", " ") - if err := encoder.Encode(config); err != nil { - return fmt.Errorf("保存配置文件失败:%w", err) - } - - return nil -} - -// GetDefaultConfig 获取默认配置 -func GetDefaultConfig() *Config { - return &Config{ - CanServiceURL: "http://localhost:8081", - DefaultDevice: DeviceConfig{ - ID: "left_hand", - Model: "L10", - CanInterface: "can0", - Parameters: make(map[string]interface{}), - }, - Devices: []DeviceConfig{ - { - ID: "left_hand", - Model: "L10", - CanInterface: "can0", - Parameters: map[string]interface{}{ - "hand_type": "left", - }, - }, - }, - Server: ServerConfig{ - Port: 8080, - Host: "localhost", - LogLevel: "info", - EnableCORS: true, - }, - } -} From d62581999d51fb87d82fdaede29d35d6ab338c38 Mon Sep 17 00:00:00 2001 From: Eli Yip Date: Wed, 28 May 2025 14:29:27 +0800 Subject: [PATCH 12/20] chore: add gitignore --- .gitignore | 361 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 361 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cb4a3fa --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file From 13ae6aaa0abc03b8d1fc3d73be7b41eea407e855 Mon Sep 17 00:00:00 2001 From: Eli Yip Date: Wed, 28 May 2025 16:31:36 +0800 Subject: [PATCH 13/20] feat: list all animations --- device/engine.go | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/device/engine.go b/device/engine.go index e30d568..f96e2a8 100644 --- a/device/engine.go +++ b/device/engine.go @@ -128,6 +128,25 @@ func (e *AnimationEngine) IsRunning() bool { 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() @@ -175,7 +194,7 @@ func (e *AnimationEngine) handleLoopExit(stopChan <-chan struct{}, deviceName, a // 这种情况下,旧的 Goroutine 不应该修改引擎状态或重置姿态, // 以避免干扰新动画。 if stopChan == e.stopChan { - // 只有当自己仍然是“活跃”的动画时,才更新状态并重置姿态 + // 只有当自己仍然是"活跃"的动画时,才更新状态并重置姿态 e.isRunning = false e.current = "" log.Printf("👋 %s 动画 %s 已完成或停止,正在重置姿态...", deviceName, animName) From bbbd4db6b16d0420e5b267f7860597e187e805a4 Mon Sep 17 00:00:00 2001 From: Eli Yip Date: Wed, 28 May 2025 16:44:18 +0800 Subject: [PATCH 14/20] feat: add router and model definition --- api2/models.go | 111 +++++++++++++++++++++++++++++++++++++++++++++++++ api2/router.go | 84 +++++++++++++++++++++++++++++++++++++ 2 files changed, 195 insertions(+) create mode 100644 api2/models.go create mode 100644 api2/router.go diff --git a/api2/models.go b/api2/models.go new file mode 100644 index 0000000..c94dc41 --- /dev/null +++ b/api2/models.go @@ -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"` +} diff --git a/api2/router.go b/api2/router.go new file mode 100644 index 0000000..39a26bd --- /dev/null +++ b/api2/router.go @@ -0,0 +1,84 @@ +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) // 重置姿态 + } + + // 动画控制路由 + 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) // 健康检查 + } + } +} From 085e8868e46c0b03342af5ee59eb9a59f6e163cf Mon Sep 17 00:00:00 2001 From: Eli Yip Date: Wed, 28 May 2025 16:52:48 +0800 Subject: [PATCH 15/20] feat: implement device handler --- api2/device_handlers.go | 251 ++++++++++++++++++++++++++++++++++++++++ define/hands.go | 16 ++- 2 files changed, 265 insertions(+), 2 deletions(-) create mode 100644 api2/device_handlers.go diff --git a/api2/device_handlers.go b/api2/device_handlers.go new file mode 100644 index 0000000..dce693b --- /dev/null +++ b/api2/device_handlers.go @@ -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, + }, + }) +} diff --git a/define/hands.go b/define/hands.go index fb821ae..04e3c8e 100644 --- a/define/hands.go +++ b/define/hands.go @@ -3,8 +3,9 @@ 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 { @@ -13,3 +14,14 @@ func (ht HandType) String() string { } 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 + } +} From 58b6900f9f3b6a04467f79eb72a0b450d4c00c90 Mon Sep 17 00:00:00 2001 From: Eli Yip Date: Wed, 28 May 2025 16:59:36 +0800 Subject: [PATCH 16/20] feat: add animation handlers --- api2/animation_handlers.go | 197 +++++++++++++++++++++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 api2/animation_handlers.go diff --git a/api2/animation_handlers.go b/api2/animation_handlers.go new file mode 100644 index 0000000..558d872 --- /dev/null +++ b/api2/animation_handlers.go @@ -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, + }) +} From a3958b79ec5e67a7604536224a83e60b65eeb1fe Mon Sep 17 00:00:00 2001 From: Eli Yip Date: Wed, 28 May 2025 16:59:57 +0800 Subject: [PATCH 17/20] feat: add system handlers --- api2/system_handlers.go | 114 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 api2/system_handlers.go diff --git a/api2/system_handlers.go b/api2/system_handlers.go new file mode 100644 index 0000000..db11d67 --- /dev/null +++ b/api2/system_handlers.go @@ -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, + }) +} From ea1dfc4e0eb1dd01468df31294d1a52b7c1a7b37 Mon Sep 17 00:00:00 2001 From: Eli Yip Date: Wed, 28 May 2025 17:10:22 +0800 Subject: [PATCH 18/20] feat: add presets manager --- device/device.go | 5 +++ device/models/l10.go | 45 ++++++++++++++++++++ device/models/l10_presets.go | 82 ++++++++++++++++++++++++++++++++++++ device/preset.go | 45 ++++++++++++++++++++ 4 files changed, 177 insertions(+) create mode 100644 device/models/l10_presets.go create mode 100644 device/preset.go diff --git a/device/device.go b/device/device.go index fe27b56..2f9d54f 100644 --- a/device/device.go +++ b/device/device.go @@ -21,6 +21,11 @@ type Device interface { // --- 新增 --- PoseExecutor // 嵌入 PoseExecutor 接口,Device 需实现它 GetAnimationEngine() *AnimationEngine // 获取设备的动画引擎 + + // --- 预设姿势相关方法 --- + GetSupportedPresets() []string // 获取支持的预设姿势列表 + ExecutePreset(presetName string) error // 执行预设姿势 + GetPresetDescription(presetName string) string // 获取预设姿势描述 } // Command 代表一个发送给设备的指令 diff --git a/device/models/l10.go b/device/models/l10.go index e808a76..671adc5 100644 --- a/device/models/l10.go +++ b/device/models/l10.go @@ -24,6 +24,7 @@ type L10Hand struct { mutex sync.RWMutex canInterface string // CAN 接口名称,如 "can0" animationEngine *device.AnimationEngine // 动画引擎 + presetManager *device.PresetManager // 预设姿势管理器 } // 在 base 基础上进行 ±delta 的扰动,范围限制在 [0, 255] @@ -91,6 +92,14 @@ func NewL10Hand(config map[string]any) (device.Device, error) { 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) @@ -333,3 +342,39 @@ func (h *L10Hand) Disconnect() error { 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) +} diff --git a/device/models/l10_presets.go b/device/models/l10_presets.go new file mode 100644 index 0000000..f8237be --- /dev/null +++ b/device/models/l10_presets.go @@ -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}, + }, + } +} diff --git a/device/preset.go b/device/preset.go new file mode 100644 index 0000000..4931a39 --- /dev/null +++ b/device/preset.go @@ -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 "" +} From 4ebd69d9965a466a0731083b201ad3026e1ef910 Mon Sep 17 00:00:00 2001 From: Eli Yip Date: Wed, 28 May 2025 17:11:06 +0800 Subject: [PATCH 19/20] feat: implement pose handlers --- api2/pose_handlers.go | 312 ++++++++++++++++++++++++++++++++++++++++++ api2/router.go | 4 + 2 files changed, 316 insertions(+) create mode 100644 api2/pose_handlers.go diff --git a/api2/pose_handlers.go b/api2/pose_handlers.go new file mode 100644 index 0000000..1df5232 --- /dev/null +++ b/api2/pose_handlers.go @@ -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), + }, + }) +} diff --git a/api2/router.go b/api2/router.go index 39a26bd..bf3d2cf 100644 --- a/api2/router.go +++ b/api2/router.go @@ -50,6 +50,10 @@ func (s *Server) SetupRoutes(r *gin.Engine) { 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) // 执行预设姿势 } // 动画控制路由 From e666f54e688d598391b66ad193cbf80a80b04951 Mon Sep 17 00:00:00 2001 From: Eli Yip Date: Wed, 28 May 2025 17:12:59 +0800 Subject: [PATCH 20/20] feat: implement sensor handlers --- api2/sensor_handlers.go | 180 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 api2/sensor_handlers.go diff --git a/api2/sensor_handlers.go b/api2/sensor_handlers.go new file mode 100644 index 0000000..6cf1322 --- /dev/null +++ b/api2/sensor_handlers.go @@ -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, + }) +}