dexterous-hand-dashboard/docs/contribute_CN.md
2025-06-03 09:58:13 +08:00

625 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 当前架构详解
## 设备抽象层 (device 包)
目标:统一不同型号设备的操作接口,屏蔽底层硬件差异(主要体现在指令到 RawMessage 的转换和设备特定功能的实现上)。
核心接口与结构体:
**Device 接口 (device/device.go): 代表一个可控制的设备单元。**
```go
type Device interface {
GetID() string
GetModel() string
GetHandType() define.HandType
SetHandType(handType define.HandType) error
ExecuteCommand(cmd Command) error
ReadSensorData(sensorID string) (SensorData, error)
GetComponents(componentType ComponentType) []Component
GetStatus() (DeviceStatus, error)
Connect() error
Disconnect() error
PoseExecutor // 嵌入 PoseExecutor 接口
GetAnimationEngine() *AnimationEngine
GetSupportedPresets() []string
ExecutePreset(presetName string) error
GetPresetDescription(presetName string) string
GetPresetDetails(presetName string) (PresetPose, bool)
}
```
**PoseExecutor 接口 (device/pose_executor.go): 定义了执行基本姿态指令的能力。**
```go
type PoseExecutor interface {
SetFingerPose(pose []byte) error
SetPalmPose(pose []byte) error
ResetPose() error
GetHandType() define.HandType
}
```
**Command 接口 (device/device.go): 代表一个发送给设备的指令。**
```go
type Command interface {
Type() string
Payload() []byte
TargetComponent() string // 目标组件 ID
}
```
具体指令实现位于 device/commands.go如 FingerPoseCommand, PalmPoseCommand, GenericCommand。
**SensorData 接口 (device/device.go): 代表从传感器读取的数据。**
```go
type SensorData interface {
Timestamp() time.Time
Values() map[string]any
SensorID() string
}
```
**ComponentType (device/device.go): 定义组件类型。**
```go
const (
SensorComponent ComponentType = "sensor"
SkinComponent ComponentType = "skin" // 示例,可扩展
ActuatorComponent ComponentType = "actuator" // 示例,可扩展
)
```
**Component 接口 (device/device.go): 代表设备的一个可插拔组件。**
```go
type Component interface {
GetID() string
GetType() ComponentType
GetConfiguration() map[string]interface{}
IsActive() bool
}
```
**具体设备型号实现 (如 device/models/l10.go 中的 L10Hand):**
1. 实现 Device 和 PoseExecutor 接口。
2. 管理内部的 AnimationEngine 和 PresetManager。
3. 包含将通用 Command 转换为发送给 can-bridge 的 RawMessage 的逻辑 (如 commandToRawMessage 方法)。
4. 管理其配备的传感器等组件 (initializeComponents 方法)。
**DeviceManager (device/manager.go): 用于注册、发现和管理可用的设备实例。**
```go
type DeviceManager struct { /* ... */ }
func NewDeviceManager() *DeviceManager { /* ... */ }
func (m *DeviceManager) RegisterDevice(dev Device) error { /* ... */ }
func (m *DeviceManager) GetDevice(id string) (Device, error) { /* ... */ }
```
## 组件化设计 (component 包)
目标:将“皮肤”、“传感器”等视为可配置、可替换的组件。
核心接口与结构体:
**传感器组件 (Sensor):**
component/sensor.go 中定义了通用的 Sensor 接口 (嵌入了 device.Component)。
```go
type Sensor interface {
device.Component
ReadData() (device.SensorData, error)
GetDataType() string
GetSamplingRate() int
SetSamplingRate(rate int) error
}
```
具体的传感器实现,如 component/component.go 中的 PressureSensor实现了 Sensor 接口。
传感器数据的实际获取方式(模拟、通过 can-bridge 的特定端点,或完全独立的数据源)在具体的 Sensor 组件实现中处理。
SensorDataImpl (component/sensor.go) 是 device.SensorData 的一个具体实现。
皮肤组件 (Skin) 及其他组件:
如果“皮肤”影响设备的物理特性或参数范围,可以将其抽象为一个 Skin 组件,实现 device.Component 接口。
设备可以关联多个不同类型的组件,并在其 initializeComponents 方法中进行初始化。
## 动画与姿态控制
目标:提供灵活的动画播放和直接的姿态控制能力,与具体设备和通信方式解耦。
**AnimationEngine (device/engine.go):**
每个设备实例拥有一个 AnimationEngine。
负责注册、启动、停止和管理动画的生命周期。
**使用 PoseExecutor 来执行动画中的姿态变化。**
```go
type AnimationEngine struct { /* ... */ }
func NewAnimationEngine(executor PoseExecutor) *AnimationEngine { /* ... */ }
func (e *AnimationEngine) Register(anim Animation) { /* ... */ }
func (e *AnimationEngine) Start(name string, speedMs int) error { /* ... */ }
func (e *AnimationEngine) Stop() error { /* ... */ }
```
Animation 接口 (device/animation.go): 定义了动画的行为。
```go
type Animation interface {
Run(executor PoseExecutor, stop <-chan struct{}, speedMs int) error
Name() string
}
```
具体的动画实现与设备型号绑定,例如 device/models/l10_animation.go 中的 L10WaveAnimation。
直接姿态控制:
通过设备实例直接调用其实现的 PoseExecutor 接口方法 (SetFingerPose, SetPalmPose, ResetPose)。
或者通过构造 FingerPoseCommand 或 PalmPoseCommand然后调用 device.ExecuteCommand()。
预设姿势 (PresetManager - device/preset.go):
每个设备实例拥有一个 PresetManager。
负责注册和管理预设姿势 (PresetPose 结构体)。
Device 接口提供了 GetSupportedPresets, ExecutePreset, GetPresetDescription 方法与预设姿势交互。
## 通信层抽象 (communication 包)
目标:将与 can-bridge Web 服务的 HTTP 通信细节封装起来,对上层透明。
RawMessage 结构体 (communication/communicator.go): 匹配 can-bridge 服务期望的 JSON 格式。
```go
type RawMessage struct {
Interface string `json:"interface"`
ID uint32 `json:"id"`
Data []byte `json:"data"`
}
```
Communicator 接口 (communication/communicator.go): 定义了与 can-bridge Web 服务进行通信的接口。
```go
type Communicator interface {
SendMessage(ctx context.Context, msg RawMessage) error
GetInterfaceStatus(ifName string) (isActive bool, err error)
GetAllInterfaceStatuses() (statuses map[string]bool, err error)
SetServiceURL(url string)
IsConnected() bool
}
```
**CanBridgeClient (communication/communicator.go): Communicator 接口的实现。**
1. 内部使用标准的 net/http 包与 can-bridge 服务交互。
2. 负责构造 HTTP 请求 (POST 到 /api/can 用于发送GET 到 /api/status/* 用于状态检查)。
3. 处理 JSON 序列化/反序列化以及 HTTP 错误。
4. 需要配置 can-bridge 服务的 URL。
具体设备实现 (如 L10Hand) 依赖此 Communicator 接口来发送指令。
## 指令生成与解析
指令生成:上层逻辑(如动画、直接控制)创建 device.Command 类型的对象 (如 NewFingerPoseCommand(...))。
设备的 ExecuteCommand 方法接收此 Command。
设备内部的 commandToRawMessage (或类似) 方法将通用的 Command 转换为特定于该型号的 RawMessage包含正确的 Interface, ID, Data
传感器数据解析:
L10Hand 的 ReadSensorData 方法委托给相应的 Sensor 组件。
Sensor 组件的 ReadData 方法负责获取原始数据(如果通过 CAN则可能需要 Communicator 支持读取功能,目前 can-bridge 主要用于发送)并将其解析为高层可理解的 SensorData。当前实现中PressureSensor 是模拟数据。
## 配置与注册
设备工厂 (device/factory.go):
使用 DeviceFactory (defaultFactory) 来创建不同型号的 Device 实例。
RegisterDeviceType(modelName string, constructor func(config map[string]any) (Device, error)): 注册新的设备型号及其构造函数。
CreateDevice(modelName string, config map[string]any) (Device, error): 根据型号和配置创建设备实例。
设备构造函数 (如 NewL10Hand) 接收一个 map[string]any 类型的配置参数。
动画和预设姿势注册:
动画通过 AnimationEngine.Register() 在设备实例化时注册。
预设姿势通过 PresetManager.RegisterPreset() 在设备实例化时注册。
## 如何添加新的设备实现
要添加对新型号设备(例如 "L20")的支持,请遵循以下步骤:
### 创建设备模型文件:
在 device/models/ 目录下为新设备创建一个 Go 文件,例如 l20.go。
如果需要设备特定的动画,创建 l20_animation.go。
如果需要设备特定的预设姿势,创建 l20_presets.go。
定义设备结构体 (l20.go):
```go
package models
import (
"context"
"fmt"
"log"
"sync"
"time"
// ... 其他必要的 import
"hands/communication"
"hands/component" // 如果需要自定义组件或使用现有组件
"hands/define"
"hands/device"
)
type L20Hand struct {
id string
model string
handType define.HandType
communicator communication.Communicator
components map[device.ComponentType][]device.Component
status device.DeviceStatus
mutex sync.RWMutex
canInterface string
animationEngine *device.AnimationEngine
presetManager *device.PresetManager
// ... L20 特有的字段
}
```
实现构造函数 (NewL20Hand):
```go
func NewL20Hand(config map[string]any) (device.Device, error) {
// 1. 解析配置 (id, can_service_url, can_interface, hand_type 等)
// ...
// 2. 创建 communicator
comm := communication.NewCanBridgeClient(serviceURL) // serviceURL from config
hand := &L20Hand{
id: id, // from config
model: "L20",
handType: handType, // from config or default
communicator: comm,
components: make(map[device.ComponentType][]device.Component),
canInterface: canInterface, // from config or default
status: device.DeviceStatus{ /* initial status */ },
// ... 初始化 L20 特有字段
}
// 3. 初始化 AnimationEngine
hand.animationEngine = device.NewAnimationEngine(hand) // hand 实现了 PoseExecutor
// 注册 L20 特定的动画 (见步骤 6)
// hand.animationEngine.Register(NewL20WaveAnimation()) // 示例
// 4. 初始化 PresetManager
hand.presetManager = device.NewPresetManager()
// 注册 L20 特定的预设姿势 (见步骤 7)
// for _, preset := range GetL20Presets() { hand.presetManager.RegisterPreset(preset) } // 示例
// 5. 初始化组件
if err := hand.initializeComponents(config); err != nil {
return nil, fmt.Errorf("L20 初始化组件失败:%w", err)
}
log.Printf("✅ 设备 L20 (%s, %s) 创建成功", hand.id, hand.handType.String())
return hand, nil
}
```
**实现 device.Device 和 device.PoseExecutor 接口:**
基本方法GetID(), GetModel(), GetHandType(), SetHandType(), GetStatus(), Connect(), Disconnect()。这些通常比较直接。
**PoseExecutor 方法:**
1. SetFingerPose(pose []byte) error
2. SetPalmPose(pose []byte) error
3. ResetPose() error
这些方法内部会调用 ExecuteCommand或者直接构造 RawMessage 发送(如果 L20 的姿态设置非常特殊)。通常建议通过 ExecuteCommand。
```go
ExecuteCommand(cmd device.Command) error:
func (h *L20Hand) ExecuteCommand(cmd device.Command) error {
h.mutex.Lock()
defer h.mutex.Unlock()
// 1. 检查设备状态
// 2. 调用 h.commandToRawMessage(cmd) 将通用指令转换为 L20 特定的 RawMessage
// 3. 使用 h.communicator.SendMessage(ctx, rawMsg) 发送
// 4. 更新设备状态和日志
return nil // or error
}
```
commandToRawMessage(cmd device.Command) (communication.RawMessage, error): 这个辅助方法是设备差异化的关键。它需要根据 L20 的 CAN 协议,将 cmd.Type() 和 cmd.Payload() 转换为正确的 RawMessage.ID 和 RawMessage.Data。
组件和传感器方法ReadSensorData(), GetComponents()。
动画和预设方法GetAnimationEngine(), GetSupportedPresets(), ExecutePreset(), GetPresetDescription()。这些通常直接委托给内部的 animationEngine 和 presetManager。
实现设备特定逻辑initializeComponents(config map[string]any) error: 根据 L20 的硬件配置,创建并注册其传感器、执行器等组件到 h.components。
```go
func (h *L20Hand) initializeComponents(config map[string]any) error {
// 示例:添加一个 L20 特有的传感器
// l20Sensor := component.NewL20SpecificSensor("l20_sensor_1", nil)
// h.components[device.SensorComponent] = append(h.components[device.SensorComponent], l20Sensor)
return nil
}
```
添加设备特定动画 (l20_animation.go):
定义实现 device.Animation 接口的动画结构体,如 L20WaveAnimation。
在 NewL20Hand 中,使用 hand.animationEngine.Register(NewL20WaveAnimation()) 注册它们。
添加设备特定预设姿势 (l20_presets.go):
定义一个函数如 GetL20Presets() []device.PresetPose返回 L20 的预设姿势列表。
在 NewL20Hand 中,遍历这些预设并使用 hand.presetManager.RegisterPreset(preset) 注册它们。
注册设备类型:
在 device/models/init.go 的 RegisterDeviceTypes() 函数中添加一行:
device.RegisterDeviceType("L20", NewL20Hand)
## 如何添加新的动画/预设姿势
这里主要指实现项目已定义的 Go 接口,如 device.Animation 或 component.Sensor。
### 添加新的动画 (实现 device.Animation)
定义动画结构体:在设备模型相关的动画文件内 (例如,若为 L10 添加新动画,则在 device/models/l10_animation.go 中),或为通用动画创建新文件。
示例:
```go
// device/models/l10_animation.go
type L10GreetingAnimation struct{}
func NewL10GreetingAnimation() *L10GreetingAnimation { return &L10GreetingAnimation{} }
```
实现 device.Animation 接口:
```go
func (a *L10GreetingAnimation) Name() string { return "greeting" }
func (a *L10GreetingAnimation) Run(executor device.PoseExecutor, stop <-chan struct{}, speedMs int) error {
log.Printf("Running %s animation on %s", a.Name(), executor.GetHandType())
delay := time.Duration(speedMs) * time.Millisecond
// 示例:挥手动作
poses := [][]byte{
{192, 192, 192, 192, 192, 192}, // 张开
{160, 160, 160, 160, 160, 160}, // 稍弯曲
}
palmPoses := [][]byte{
{100, 128, 128, 128}, // 手掌姿态 1
{150, 128, 128, 128}, // 手掌姿态 2
}
for i := 0; i < 3; i++ { // 重复几次
for j, pose := range poses {
if err := executor.SetFingerPose(pose); err != nil { return err }
if err := executor.SetPalmPose(palmPoses[j%len(palmPoses)]); err != nil { return err } // 循环使用手掌姿态
select {
case <-stop:
log.Printf("%s animation stopped.", a.Name())
return nil
case <-time.After(delay):
// continue
}
}
}
return nil
}
```
注册动画:在对应设备的构造函数中 (例如 NewL10Hand),获取 AnimationEngine 实例并注册新动画:
```go
// 在 NewL10Hand 中:
hand.animationEngine.Register(NewL10GreetingAnimation())
```
### 添加新的传感器类型 (实现 component.Sensor 和 device.Component)
定义传感器结构体:
在 component/ 目录下创建新文件,例如 temperature_sensor.go。
定义结构体:
```go
// component/temperature_sensor.go
package component
import (
"hands/device"
"math/rand/v2"
"time"
"fmt"
)
type TemperatureSensor struct {
id string
config map[string]any
isActive bool
samplingRate int // Hz
}
func NewTemperatureSensor(id string, config map[string]any) Sensor { // 返回 Sensor 接口
return &TemperatureSensor{
id: id,
config: config,
isActive: true,
samplingRate: 1, // 默认 1Hz
}
}
```
实现 device.Component 接口:
```go
func (ts *TemperatureSensor) GetID() string { return ts.id }
func (ts *TemperatureSensor) GetType() device.ComponentType { return device.SensorComponent }
func (ts *TemperatureSensor) GetConfiguration() map[string]any { return ts.config }
func (ts *TemperatureSensor) IsActive() bool { return ts.isActive }
```
实现 component.Sensor 接口:
```go
func (ts *TemperatureSensor) ReadData() (device.SensorData, error) {
if !ts.isActive {
return nil, fmt.Errorf("sensor %s is not active", ts.id)
}
// 模拟读取温度数据
tempValue := 20.0 + rand.Float64()*15.0 // 20-35 度
values := map[string]any{
"temperature": tempValue,
"unit": "Celsius",
}
return NewSensorData(ts.id, values), nil // 使用 component.NewSensorData
}
func (ts *TemperatureSensor) GetDataType() string { return "temperature" }
func (ts *TemperatureSensor) GetSamplingRate() int { return ts.samplingRate }
func (ts *TemperatureSensor) SetSamplingRate(rate int) error {
if rate <= 0 {
return fmt.Errorf("sampling rate must be positive")
}
ts.samplingRate = rate
return nil
}
```
集成到设备:在具体设备模型 (如 L10Hand 或 L20Hand) 的 initializeComponents 方法中,创建并添加此传感器的实例:
```go
// 在 L10Hand.initializeComponents 中:
tempSensor1 := component.NewTemperatureSensor("temp_palm", map[string]any{"location": "palm"})
h.components[device.SensorComponent] = append(h.components[device.SensorComponent], tempSensor1)
```
### 如何添加新的 Component
添加一个新的通用组件(非特指传感器)与添加传感器类似,主要区别在于它可能不会实现 component.Sensor 接口,而是直接实现 device.Component 以及任何该组件特有的接口。
定义组件类型 (如果需要新的 ComponentType): 在 device/device.go 中为新的组件类型添加一个常量:
```go
const (
// ...
MyCustomComponentType ComponentType = "my_custom_type"
)
```
定义组件特定接口:如果该组件有特定行为,可以在 component/ 目录下或与组件实现同文件中定义一个接口:
```go
// component/my_custom_component.go
package component
import "hands/device"
type MyCustomFunctionality interface {
PerformAction(param string) (string, error)
}
```
定义组件结构体:在 component/ 目录下创建新文件,例如 my_custom_component.go。
定义结构体:
```go
type MyCustomComponent struct {
id string
config map[string]any
isActive bool
// ... 其他字段
}
func NewMyCustomComponent(id string, config map[string]any) device.Component { // 返回 device.Component
return &MyCustomComponent{
id: id,
config: config,
isActive: true,
}
}
```
实现 device.Component 接口:
```go
func (mcc *MyCustomComponent) GetID() string { return mcc.id }
func (mcc *MyCustomComponent) GetType() device.ComponentType { return MyCustomComponentType } // 使用新定义的类型
func (mcc *MyCustomComponent) GetConfiguration() map[string]any { return mcc.config }
func (mcc *MyCustomComponent) IsActive() bool { return mcc.isActive }
```
实现组件特定接口:
```go
// 确保 MyCustomComponent 也实现了 MyCustomFunctionality
func (mcc *MyCustomComponent) PerformAction(param string) (string, error) {
// 实现特定功能
return "Action performed with " + param, nil
}
```
在这种情况下NewMyCustomComponent 的返回类型可能需要同时满足 device.Component 和 MyCustomFunctionality或者在使用时进行类型断言。一个常见的做法是返回具体类型指针 *MyCustomComponent它自然实现了所有嵌入或直接定义的方法。或者如果希望返回接口可以返回 device.Component然后在需要特定功能时进行类型断言。
集成到设备:在具体设备模型的 initializeComponents 方法中,创建并添加此组件的实例:
```go
// 在 L10Hand.initializeComponents 中:
customComp := component.NewMyCustomComponent("custom_1", map[string]any{"setting": "value"})
h.components[component.MyCustomComponentType] = append(h.components[component.MyCustomComponentType], customComp)
```
设备代码可能需要通过 GetComponents(component.MyCustomComponentType) 获取这些组件,并进行类型断言以调用其特定方法:
```go
comps := h.GetComponents(component.MyCustomComponentType)
for _, comp := range comps {
if customComp, ok := comp.(component.MyCustomFunctionality); ok { // 或 *component.MyCustomComponent
result, err := customComp.PerformAction("test")
// ...
}
}
```