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 +}