diff --git a/README.md b/README.md index 1e7c2d4..5ca2e66 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,8 @@ > 专注于人工智能和机器人解决方案,帮助开发者、企业、科研机构快速实现真实场景落地 -LinkerHand-CPP-SDK 由灵心巧手(北京)科技有限公司开发,提供完整的 C++ API,用于控制 L6、L7、L10、L20、L21、L25、O6、O20、G20等型号的灵巧手设备,支持 CAN / CAN-FD / Modbus 多种总线。 +灵心巧手(北京)科技有限公司提供的一套 C++ API,用于控制(L6、L7、L20_Lite、L20、L20_10/11、L21、L25、L30v6、O6、O20)灵巧手设备,支持 CAN / CAN-FD / Modbus 多种总线。 + --- @@ -18,7 +19,7 @@ LinkerHand-CPP-SDK 由灵心巧手(北京)科技有限公司开发,提供 - [快速开始](#-快速开始) - [API 文档](#-api-文档) - [通信协议](#-通信协议) -- [支持的型号](#-支持的型号) +- [支持的协议](#-支持的协议) - [网页示教器](#-网页示教器) - [项目结构](#-项目结构) - [示例程序列表](#-示例程序列表) @@ -31,7 +32,7 @@ LinkerHand-CPP-SDK 由灵心巧手(北京)科技有限公司开发,提供 ## ✨ 特性 -- 🎯 **多型号支持** — L6 / L7 / L10 / L20 / L21 / L25 / O6 / O20 / G20 +- 🎯 **多型号支持** — L6、L7、L20_Lite、L20、L20_10/11、L21、L25、L30v6、O6、O20 - 🔌 **多总线** — CAN、CAN-FD、Modbus - 🚀 **易用** — 简洁的 C++ API,回调注入式通信层,便于嵌入到自有应用 - 📊 **传感数据** — 实时获取压力、温度、电流等传感器读数 @@ -122,12 +123,12 @@ sudo BITRATE=1000000 DBITRATE=2000000 ./can-autocfg.sh # 改 FD 数据段波特 cd build/bin ./linker_hand_example # CLI 工具 ./test_o6_can_0 -./test_l10_can_0 +./test_l20_lite_can_0 ./test_o20_canfd_0 # Windows build\bin\linker_hand_example.exe -build\bin\test_l10_can_0.exe +build\bin\test_l20_lite_can_0.exe ``` 更多示例见 [`examples/`](examples/) 目录。 @@ -263,30 +264,31 @@ cmake --build . -j |------|--------|---------| | CAN | `COMM_TYPE::CAN` | 全系列 | | CAN-FD | `COMM_TYPE::CAN`(搭配 `CanFD` 实现) | O20(不支持Linux aarch64) | -| Modbus | `COMM_TYPE::MODBUS` | L7 / L10 / O6 | +| Modbus | `COMM_TYPE::MODBUS` | L7 / L20_Lite / O6 | 通信层通过 `Communication::CommFactory` 构造(旧名 `CanBusFactory` 保留为兼容别名),调用方通过 `LinkerHandApi::setCanTxCallback` / `setCanRxCallback`(或 Modbus 等价回调)注入收发逻辑,SDK 本身不持有任何 socket。 -## 🤖 支持的型号 - -| 型号 | 通信方式 | 备注 | -|------|----------|------| -| LinkerHand G20 | CAN | G20 series robotic hand | -| LinkerHand L6 | CAN | L6 series robotic hand | -| LinkerHand L7 | CAN / Modbus | L7 series robotic hand | -| LinkerHand L10 | CAN / Modbus | L10 series robotic hand | -| LinkerHand L20 | CAN | L20 series robotic hand | -| LinkerHand L21 | CAN | L21 series robotic hand | -| LinkerHand L25 | CAN | L25 series robotic hand | -| LinkerHand O6 | CAN / Modbus | O6 series robotic hand | -| LinkerHand O20 | CAN-FD | O20 series robotic hand(不支持Linux aarch64) | +## 🤖 支持的协议 + +| 机械手型号 | CAN 协议 | Modbus 协议 | CAN-FD 协议 | +| :--- | :---: | :---: | :---: | +| LinkerHand O6 | ✅ | ✅ | | +| LinkerHand L6 / L6S | ✅ | | | +| LinkerHand O7 | ✅ | ✅ | | +| LinkerHand L20 Lite | ✅ | ✅ | | +| LinkerHand L20 | ✅ | | | +| LinkerHand L20 V10+ | ✅ | | | +| LinkerHand L21 | ✅ | | | +| LinkerHand L25 | ✅ | | | +| LinkerHand O20 | | | ✅ | +| LinkerHand L30 V6 | | | ✅ | ## 🌐 网页示教器 -`webui/` 是一套开箱即用的网页控制界面,全型号(L6 / L7 / L10 / L20 / L21 / L25 / G20 / O6 / O20)通用,O20 走 CAN-FD。 +`webui/` 是一套开箱即用的网页控制界面,全型号(L6 / L7 / L20_Lite / L20 / L20_10/11 / L21 / L25 / L30v6 / O6 / O20)通用,O20 走 CAN-FD。 -> **`./build.sh -b` 编译完成后即可直接使用**,无需额外构建,`python3 webui/run.py` 起服务即用。 +> **一条命令即用**:`./start-webui.sh` 会自动检测 `web_bridge` 是否已编译,未编译则先跑 `./build.sh -b`,随后拉起服务;也可 `./build.sh -b` 后直接 `python3 webui/run.py`。 @@ -297,29 +299,36 @@ cmake --build . -j - 按型号动态渲染**关节滑块**,实时位置回读 - **速度 / 力矩**设置与回读 -- **触觉压感热力图**(含掌心,O6 / G20),维度按型号自适应 +- **触觉压感热力图**(含掌心,O6 / L20_10/11),维度按型号自适应 - **温度 / 故障**监控表(温度 >50 黄、>60 红,故障码非 0 红) - 版本信息展示、明暗主题切换 - 在线连接:前端「设置」面板选择型号 / 侧别 / 通信方式,支持热重连与主动断开 ### 运行 -编译完成后,拉起对应总线、给手上电,启动服务即可: +拉起对应总线、给手上电,启动服务即可。推荐用 `start-webui.sh`,它在启动前自动确保 `web_bridge` 已编译(缺失则先构建): ```bash -./build.sh -b # 一次编译,webui 随之就绪 sudo ip link set can0 up type can bitrate 1000000 # CAN 型号;Modbus 见下 +./start-webui.sh # 未编译则自动构建再起服务,参数透传给 run.py +``` + +或手动分步(已编译时): + +```bash +./build.sh -b # 一次编译,webui 随之就绪 python3 webui/run.py # 起服务,型号/通信在前端「设置」里选 ``` -浏览器打开 `http://<本机IP>:8080/`。也可命令行直连指定型号: +浏览器打开 `http://<本机IP>:8080/`。也可命令行直连指定型号(`start-webui.sh` 与 `run.py` 参数一致,可直接透传): ```bash +./start-webui.sh --model O6 --side left python3 webui/run.py --model O6 --side left -python3 webui/run.py --model L10 --side left --channel can0 +python3 webui/run.py --model L20_LITE --side left --channel can0 python3 webui/run.py --model O20 --side right # 厂商 CAN-FD 设备 python3 webui/run.py --model O20 --side right --channel socketcan:can0 -python3 webui/run.py --model L10 --comm modbus --channel /dev/ttyUSB0 +python3 webui/run.py --model L20_LITE --comm modbus --channel /dev/ttyUSB0 ``` 主要参数:`--model`(不填则在前端在线连接)、`--side left|right`、`--comm can|canfd|modbus`(缺省按型号自动)、`--channel`(CAN 接口名 / Modbus 串口)、`--host`(默认 `0.0.0.0`)、`--port`(默认 `8080`)。更多用法见 [`webui/README.md`](webui/README.md)。 @@ -372,15 +381,15 @@ linkerhand-cpp-sdk/ | 示例文件 | 说明 | |----------|------| | `linker_hand_example.cpp` | 多型号 / 多协议巡检 | -| `test_g20_can_0.cpp` | G20 CAN 基础示例 | -| `test_g20_can_1.cpp` | G20 CAN 进阶示例(直连具体类) | +| `test_l20_10_can_0.cpp` | L20_10/11 CAN 基础示例 | +| `test_l20_10_can_1.cpp` | L20_10/11 CAN 进阶示例(直连具体类) | | `test_l7_can_0.cpp` | L7 CAN 基础示例 | | `test_l7_can_1.cpp` | L7 CAN 进阶示例 | | `test_l7_modbus_0.cpp` | L7 Modbus 基础示例 | | `test_l7_modbus_1.cpp` | L7 Modbus 进阶示例 | -| `test_l10_can_0.cpp` | L10 CAN 基础示例 | -| `test_l10_can_1.cpp` | L10 CAN 进阶示例 | -| `test_l10_modbus.cpp` | L10 Modbus 示例 | +| `test_l20_lite_can_0.cpp` | L20_Lite CAN 基础示例 | +| `test_l20_lite_can_1.cpp` | L20_Lite CAN 进阶示例 | +| `test_l20_lite_modbus.cpp` | L20_Lite Modbus 示例 | | `test_l20_can_0.cpp` | L20 CAN 示例 | | `test_l21_can_0.cpp` | L21 CAN 示例 | | `test_o6_can_0.cpp` | O6 CAN 基础示例 | @@ -391,7 +400,7 @@ linkerhand-cpp-sdk/ | `test_o6_modbus_2.cpp` | O6 Modbus 高级示例 | | `test_o20_canfd_0.cpp` | O20 CAN-FD 基础示例(不支持Linux aarch64) | | `test_o20_canfd_1.cpp` | O20 CAN-FD 进阶示例(不支持Linux aarch64) | -| `L10/action_group_show.cpp` | L10 动作组演示 | +| `L20_Lite/action_group_show.cpp` | L20_Lite 动作组演示 | ## 📖 关节映射表 @@ -405,13 +414,13 @@ linkerhand-cpp-sdk/ ["大拇指弯曲", "大拇指横摆", "食指弯曲", "中指弯曲", "无名指弯曲", "小拇指弯曲", "拇指旋转"] ``` -- L10 +- L20_Lite ``` ["拇指根部", "拇指侧摆", "食指根部", "中指根部", "无名指根部", "小指根部", "食指侧摆", "无名指侧摆", "小指侧摆", "拇指旋转"] ``` -- G20 +- L20_10/11 ``` ["大拇指根部", "食指根部", "中指根部","无名指根部","小拇指根部", "大拇指侧摆","食指侧摆","中指侧摆","无名指侧摆","小拇指侧摆", diff --git a/docs/API-Reference.md b/docs/API-Reference.md index 6fa9dfe..edf1c8c 100644 --- a/docs/API-Reference.md +++ b/docs/API-Reference.md @@ -15,7 +15,7 @@ LinkerHandApi(const LINKER_HAND &handJoint, const HAND_TYPE &handType, const COM LinkerHandApi 类的构造函数,用于初始化 Linker 机械手 API。 参数 -- handJoint: 机械手型号,可选值 `LINKER_HAND::{L6, L7, L10, L20, L21, L25, O6, G20, O20}`。 +- handJoint: 机械手型号,可选值 `LINKER_HAND::{L6, L7, L10, L20, L21, L25, O6, L20_10, O20}`。 - handType: 左手或右手(`HAND_TYPE::LEFT` 或 `HAND_TYPE::RIGHT`)。 - commType: 通信协议类型,可选值 `COMM_TYPE::{CAN, MODBUS, ETHERCAT}`,默认为 `CAN`。CAN-FD 走 `COMM_TYPE::CAN`,由 `Communication::CommFactory` 在底层选择 `CanFD` 实现。 @@ -45,7 +45,7 @@ void setPosition(const std::vector &pose); **Description**: 设置关节的目标位置,用于控制手指的运动。 **Parameters**: -- `pose`: 一个包含目标位置数据长度为(O6/L6=6、L7=7、L10=10、L20=20、G20=16、L21=21、L25=25) +- `pose`: 一个包含目标位置数据长度为(O6/L6=6、L7=7、L10=10、L20=20、L20_10=16、L21=21、L25=25) 的 `std::vector`数组,范围是0~255。 ``` O6/L6: ["拇指根部", "拇指侧摆", "食指根部", "中指根部", "无名指根部", "小指根部"] @@ -58,7 +58,7 @@ void setPosition(const std::vector &pose); L21: ["大拇指根部", "食指根部", "中指根部","无名指根部","小拇指根部","大拇指侧摆","食指侧摆","中指侧摆","无名指侧摆","小拇指侧摆","大拇指横滚","预留","预留","预留","预留","大拇指中部","预留","预留","预留","预留","大拇指指尖","食指指尖","中指指尖","无名指指尖","小拇指指尖"] - G20: ["大拇指根部", "食指根部", "中指根部","无名指根部","小拇指根部","大拇指侧摆","食指侧摆","中指侧摆","无名指侧摆","小拇指侧摆","大拇指横滚","大拇指指尖","食指指尖","中指指尖","无名指指尖","小拇指指尖"] + L20_10: ["大拇指根部", "食指根部", "中指根部","无名指根部","小拇指根部","大拇指侧摆","食指侧摆","中指侧摆","无名指侧摆","小拇指侧摆","大拇指横滚","大拇指指尖","食指指尖","中指指尖","无名指指尖","小拇指指尖"] L25: ["大拇指根部", "食指根部", "中指根部","无名指根部","小拇指根部","大拇指侧摆","食指侧摆","中指侧摆","无名指侧摆","小拇指侧摆","大拇指横滚","预留","预留","预留","预留","大拇指中部","食指中部","中指中部","无名指中部","小拇指中部","大拇指指尖","食指指尖","中指指尖","无名指指尖","小拇指指尖"] ``` @@ -71,7 +71,7 @@ void setPositionArc(const std::vector &pose); **Description**: 设置关节的目标位置,用于控制手指的运动。 **Parameters**: -- `pose`: 一个包含目标位置数据长度为(O6/L6=6、L7=7、L10=10、L20=20、G20=16、L21=21、L25=25) +- `pose`: 一个包含目标位置数据长度为(O6/L6=6、L7=7、L10=10、L20=20、L20_10=16、L21=21、L25=25) 的 `std::vector`数组,范围为urdf限位。 --- @@ -120,7 +120,7 @@ std::vector> getPalmForce(); ``` **Description**: 获取手掌区域的触觉压感点阵。矩阵维度随掌部传感器类型而定:`TSSP_JZG` 为 20 行 × 28 列,`TSSP_HWK` 为 14 行 × 16 列。 -**支持型号**:L6 / O6 / G20(其余型号返回空并提示不支持)。 +**支持型号**:L6 / O6 / L20_10(其余型号返回空并提示不支持)。 **Returns**: - 返回二维向量 `std::vector>`:外层为行、内层为列,每个元素为该传感单元的压感值(uint8_t)。 @@ -132,7 +132,7 @@ std::vector> getFingerForceSum(); ``` **Description**: 获取五根手指的法向合力值。每指分量数随传感器类型而定:`TSSP_JZG` 每指 3 个,`TSSP_HWK` 每指 1 个。 -**支持型号**:G20(其余型号返回空并提示不支持)。 +**支持型号**:L20_10(其余型号返回空并提示不支持)。 **Returns**: - 返回二维向量 `std::vector>`:外层大小 = 5(五根手指),内层为该指的合力分量。 @@ -144,7 +144,7 @@ std::vector getPalmForceSum(); ``` **Description**: 获取整个手掌的法向合力值。分量数随传感器类型而定:`TSSP_JZG` 为 2 个,`TSSP_HWK` 为 1 个。 -**支持型号**:G20(其余型号返回空并提示不支持)。 +**支持型号**:L20_10(其余型号返回空并提示不支持)。 **Returns**: - 返回一个 `std::vector`,为手掌合力分量。 diff --git a/examples/L10/action_group_show.cpp b/examples/L20_Lite/action_group_show.cpp similarity index 98% rename from examples/L10/action_group_show.cpp rename to examples/L20_Lite/action_group_show.cpp index fbec178..bd3a9f1 100644 --- a/examples/L10/action_group_show.cpp +++ b/examples/L20_Lite/action_group_show.cpp @@ -1,4 +1,4 @@ -// L10 / CAN / 右手 —— 动作组状态机演示(球形拇指根部关节版本) +// L20_Lite / CAN / 右手 —— 动作组状态机演示(球形拇指根部关节版本) // 如需左手,请将 HAND_TYPE::RIGHT 改为 HAND_TYPE::LEFT #include @@ -422,7 +422,7 @@ vector showLeft() { int main() { // 调用API接口 - LinkerHandApi hand(LINKER_HAND::L10, HAND_TYPE::RIGHT); + LinkerHandApi hand(LINKER_HAND::L20_Lite, HAND_TYPE::RIGHT); hand.setSpeed({100, 100, 100, 100, 100}); hand.setTorque({200, 200, 200, 200, 200}); diff --git a/examples/linker_hand_example.cpp b/examples/linker_hand_example.cpp index 6e5eb1f..48564c7 100644 --- a/examples/linker_hand_example.cpp +++ b/examples/linker_hand_example.cpp @@ -409,21 +409,21 @@ void interactiveMode(LinkerHandApi &hand) hand.setPosition(pos1_3); std::this_thread::sleep_for(std::chrono::seconds(1)); } - else if (hand.handJoint_ == LINKER_HAND::G20) + else if (hand.handJoint_ == LINKER_HAND::L20_10) { hand.setSpeed(std::vector(5, HAND_SPEED)); // L20 need 5 speed //--------------------------------------------------------- - std::cout << "G20 - Execute action - Make a fist" << std::endl; - std::vector G20_POSE_CLOSE_1 = {255, 0, 0, 0, 0,255,255,178,84,0,255,255,0,0,0,0}; - hand.setPosition(G20_POSE_CLOSE_1); + std::cout << "L20_10 - Execute action - Make a fist" << std::endl; + std::vector L20_10_POSE_CLOSE_1 = {255, 0, 0, 0, 0,255,255,178,84,0,255,255,0,0,0,0}; + hand.setPosition(L20_10_POSE_CLOSE_1); std::this_thread::sleep_for(std::chrono::milliseconds(50)); - std::vector G20_POSE_CLOSE_2 = {117, 0, 0, 0, 0,47,255,178,84,0,115,104,0,0,0,0}; - hand.setPosition(G20_POSE_CLOSE_2); + std::vector L20_10_POSE_CLOSE_2 = {117, 0, 0, 0, 0,47,255,178,84,0,115,104,0,0,0,0}; + hand.setPosition(L20_10_POSE_CLOSE_2); std::this_thread::sleep_for(std::chrono::seconds(1)); //--------------------------------------------------------- - std::cout << "G20 - Execute action - Open hand" << std::endl; - std::vector G20_POSE_OPEN = {255, 255, 255, 255, 255, 255, 255, 172, 74, 0, 255, 255, 255, 255, 255, 255}; - hand.setPosition(G20_POSE_OPEN); + std::cout << "L20_10 - Execute action - Open hand" << std::endl; + std::vector L20_10_POSE_OPEN = {255, 255, 255, 255, 255, 255, 255, 172, 74, 0, 255, 255, 255, 255, 255, 255}; + hand.setPosition(L20_10_POSE_OPEN); std::this_thread::sleep_for(std::chrono::seconds(1)); } else if (hand.handJoint_ == LINKER_HAND::O20) @@ -473,7 +473,7 @@ int main(int argc, char *argv[]) printColorLine1(); std::cout << BLUE << "[7]: O6\n" << RESET; printColorLine1(); - std::cout << BLUE << "[8]: G20\n" << RESET; + std::cout << BLUE << "[8]: L20_10\n" << RESET; printColorLine1(); std::cout << BLUE << "[9]: O20\n" << RESET; printColorLine1(); @@ -506,7 +506,7 @@ int main(int argc, char *argv[]) linkerhand = LINKER_HAND::O6; break; case 8: - linkerhand = LINKER_HAND::G20; + linkerhand = LINKER_HAND::L20_10; break; case 9: linkerhand = LINKER_HAND::O20; diff --git a/examples/sources.cmake b/examples/sources.cmake index 8f03a76..726fd3f 100644 --- a/examples/sources.cmake +++ b/examples/sources.cmake @@ -6,18 +6,18 @@ set(LINKERHAND_EXAMPLES test test_0 - test_l10 - test_l10_can_0 - test_l10_can_1 - test_l10_modbus + test_l20_lite + test_l20_lite_can_0 + test_l20_lite_can_1 + test_l20_lite_modbus test_l7_can_0 test_l7_can_1 test_l7_modbus_0 test_l7_modbus_1 test_l20_can_0 test_l21_can_0 - test_g20_can_0 - test_g20_can_1 + test_l20_10_can_0 + test_l20_10_can_1 test_l6_can_0 test_o6_can_0 test_o6_can_1 @@ -25,7 +25,7 @@ set(LINKERHAND_EXAMPLES test_o6_modbus_0 test_o6_modbus_1 test_o6_modbus_2 - L10/action_group_show + L20_Lite/action_group_show range_to_arc/range_to_arc test_conversion ) @@ -35,6 +35,7 @@ set(LINKERHAND_EXAMPLES_CANFD test_o20_canfd_0 test_o20_canfd_1 test_o20_canfd_double + test_l30_canfd_0 linker_hand_example ) @@ -42,4 +43,6 @@ set(LINKERHAND_EXAMPLES_CANFD # 无需 libcanbus,不受 USE_CANFD 门控。 set(LINKERHAND_EXAMPLES_LINUX test_o20_canfd_socket_0 + test_l30_canfd_socket_0 + test_l30_canfd_socket_auto_0 ) diff --git a/examples/test_conversion.cpp b/examples/test_conversion.cpp index e05a2b4..f6d3c3c 100644 --- a/examples/test_conversion.cpp +++ b/examples/test_conversion.cpp @@ -36,7 +36,7 @@ double scale_value(double original_value, double a_min, double a_max, double b_m int main() { try { // 初始化灵巧手 - LinkerHandApi hand(LINKER_HAND::G20, HAND_TYPE::LEFT); + LinkerHandApi hand(LINKER_HAND::L20_10, HAND_TYPE::LEFT); // 创建 CAN 总线对象 std::shared_ptr bus = Communication::CommFactory::createCanBus("can0", 1000000); diff --git a/examples/test_g20_can_0.cpp b/examples/test_l20_10_can_0.cpp similarity index 69% rename from examples/test_g20_can_0.cpp rename to examples/test_l20_10_can_0.cpp index ffd1589..0eeaf84 100644 --- a/examples/test_g20_can_0.cpp +++ b/examples/test_l20_10_can_0.cpp @@ -1,4 +1,4 @@ -// G20 / CAN / 左手 —— 握拳/张开动作循环 +// L20_10 / CAN / 左手 —— 握拳/张开动作循环 #include #include #include @@ -13,7 +13,7 @@ int main(int argc, char *argv[]) { try { std::cout << "=========================\n"; - std::cout << " G20 CAN Test\n"; + std::cout << " L20_10 CAN Test\n"; std::cout << "=========================\n"; std::string channel_name = "can0"; @@ -24,8 +24,8 @@ int main(int argc, char *argv[]) { // 创建 CAN 总线对象(工厂按平台分流 SocketCAN / PCAN) std::shared_ptr bus = Communication::CommFactory::createCanBus(channel_name, 1000000); - // 创建 G20 手对象 - LinkerHandApi hand(LINKER_HAND::G20, HAND_TYPE::LEFT, COMM_TYPE::CAN); + // 创建 L20_10 手对象 + LinkerHandApi hand(LINKER_HAND::L20_10, HAND_TYPE::LEFT, COMM_TYPE::CAN); // 设置 CAN 发送回调 hand.setCanTxCallback([bus](uint32_t can_id, const uint8_t *data, uintptr_t data_len) -> int32_t { @@ -52,20 +52,20 @@ int main(int argc, char *argv[]) { // 获取版本信息 std::cout << "Version: " << hand.getVersion() << std::endl; - // G20 握拳动作 - std::cout << "G20 - Execute action - Make a fist" << std::endl; - std::vector G20_POSE_CLOSE_1 = {255, 0, 0, 0, 0, 255, 255, 178, 84, 0, 255, 255, 0, 0, 0, 0}; - hand.setPosition(G20_POSE_CLOSE_1); + // L20_10 握拳动作 + std::cout << "L20_10 - Execute action - Make a fist" << std::endl; + std::vector L20_10_POSE_CLOSE_1 = {255, 0, 0, 0, 0, 255, 255, 178, 84, 0, 255, 255, 0, 0, 0, 0}; + hand.setPosition(L20_10_POSE_CLOSE_1); std::this_thread::sleep_for(std::chrono::milliseconds(50)); - std::vector G20_POSE_CLOSE_2 = {117, 0, 0, 0, 0, 47, 255, 178, 84, 0, 115, 104, 0, 0, 0, 0}; - hand.setPosition(G20_POSE_CLOSE_2); + std::vector L20_10_POSE_CLOSE_2 = {117, 0, 0, 0, 0, 47, 255, 178, 84, 0, 115, 104, 0, 0, 0, 0}; + hand.setPosition(L20_10_POSE_CLOSE_2); std::this_thread::sleep_for(std::chrono::seconds(1)); - // G20 张开动作 - std::cout << "G20 - Execute action - Open hand" << std::endl; - std::vector G20_POSE_OPEN = {255, 255, 255, 255, 255, 255, 255, 172, 74, 0, 255, 255, 255, 255, 255, 255}; - hand.setPosition(G20_POSE_OPEN); + // L20_10 张开动作 + std::cout << "L20_10 - Execute action - Open hand" << std::endl; + std::vector L20_10_POSE_OPEN = {255, 255, 255, 255, 255, 255, 255, 172, 74, 0, 255, 255, 255, 255, 255, 255}; + hand.setPosition(L20_10_POSE_OPEN); std::this_thread::sleep_for(std::chrono::seconds(1)); // 循环测试 @@ -74,14 +74,14 @@ int main(int argc, char *argv[]) { // 握拳 std::cout << "Make a fist..." << std::endl; - hand.setPosition(G20_POSE_CLOSE_1); + hand.setPosition(L20_10_POSE_CLOSE_1); std::this_thread::sleep_for(std::chrono::milliseconds(50)); - hand.setPosition(G20_POSE_CLOSE_2); + hand.setPosition(L20_10_POSE_CLOSE_2); std::this_thread::sleep_for(std::chrono::seconds(1)); // 张开 std::cout << "Open hand..." << std::endl; - hand.setPosition(G20_POSE_OPEN); + hand.setPosition(L20_10_POSE_OPEN); std::this_thread::sleep_for(std::chrono::seconds(1)); } diff --git a/examples/test_g20_can_1.cpp b/examples/test_l20_10_can_1.cpp similarity index 91% rename from examples/test_g20_can_1.cpp rename to examples/test_l20_10_can_1.cpp index 40e92fb..c6bdd2f 100644 --- a/examples/test_g20_can_1.cpp +++ b/examples/test_l20_10_can_1.cpp @@ -1,4 +1,4 @@ -// G20 / CAN / 左手 —— 多线程并发测试(直连具体总线类的进阶示例) +// L20_10 / CAN / 左手 —— 多线程并发测试(直连具体总线类的进阶示例) #include #include #include @@ -72,7 +72,7 @@ void threadSetParameters(LinkerHandApi& hand, int iteration) { try { auto start = std::chrono::steady_clock::now(); - // G20 需要 5 个速度/扭矩值 + // L20_10 需要 5 个速度/扭矩值 std::vector torque_values = {200, 200, 200, 200, 200}; std::vector speed_values = {200, 200, 200, 200, 200}; @@ -209,30 +209,30 @@ void threadGetTouchData(LinkerHandApi& hand, int iteration) { } } -// 线程5:G20运动控制(握拳和张开) +// 线程5:L20_10运动控制(握拳和张开) void threadMotionControl(LinkerHandApi& hand, int iteration) { try { auto start = std::chrono::steady_clock::now(); - // G20 握拳动作(两步) - std::vector G20_POSE_CLOSE_1 = {255, 0, 0, 0, 0, 255, 255, 178, 84, 0, 255, 255, 0, 0, 0, 0}; - hand.setPosition(G20_POSE_CLOSE_1); + // L20_10 握拳动作(两步) + std::vector L20_10_POSE_CLOSE_1 = {255, 0, 0, 0, 0, 255, 255, 178, 84, 0, 255, 255, 0, 0, 0, 0}; + hand.setPosition(L20_10_POSE_CLOSE_1); std::this_thread::sleep_for(std::chrono::milliseconds(50)); - std::vector G20_POSE_CLOSE_2 = {117, 0, 0, 0, 0, 47, 255, 178, 84, 0, 115, 104, 0, 0, 0, 0}; - hand.setPosition(G20_POSE_CLOSE_2); + std::vector L20_10_POSE_CLOSE_2 = {117, 0, 0, 0, 0, 47, 255, 178, 84, 0, 115, 104, 0, 0, 0, 0}; + hand.setPosition(L20_10_POSE_CLOSE_2); std::this_thread::sleep_for(std::chrono::milliseconds(1000)); - // G20 张开动作 - std::vector G20_POSE_OPEN = {255, 255, 255, 255, 255, 255, 255, 172, 74, 0, 255, 255, 255, 255, 255, 255}; - hand.setPosition(G20_POSE_OPEN); + // L20_10 张开动作 + std::vector L20_10_POSE_OPEN = {255, 255, 255, 255, 255, 255, 255, 172, 74, 0, 255, 255, 255, 255, 255, 255}; + hand.setPosition(L20_10_POSE_OPEN); std::this_thread::sleep_for(std::chrono::milliseconds(1000)); auto end = std::chrono::steady_clock::now(); auto duration = std::chrono::duration_cast(end - start); std::stringstream ss; - ss << "[Thread5-Iter" << iteration << "] G20运动控制完成 (" << duration.count() << "ms)"; + ss << "[Thread5-Iter" << iteration << "] L20_10运动控制完成 (" << duration.count() << "ms)"; printWithLock(ss.str()); } catch (const std::exception& e) { @@ -245,7 +245,7 @@ void threadMotionControl(LinkerHandApi& hand, int iteration) { int main() { try { std::cout << "=========================\n"; - std::cout << " G20 CAN 多线程测试\n"; + std::cout << " L20_10 CAN 多线程测试\n"; std::cout << "=========================\n"; // 创建 CAN 总线对象 @@ -255,8 +255,8 @@ int main() { std::shared_ptr bus = std::make_shared("can0", 1000000); #endif - // 创建 G20 手对象 - LinkerHandApi hand(LINKER_HAND::G20, HAND_TYPE::LEFT, COMM_TYPE::CAN); + // 创建 L20_10 手对象 + LinkerHandApi hand(LINKER_HAND::L20_10, HAND_TYPE::LEFT, COMM_TYPE::CAN); // 设置 CAN 发送回调 hand.setCanTxCallback([bus](uint32_t can_id, const uint8_t *data, uintptr_t data_len) -> int32_t { diff --git a/examples/test_l10.cpp b/examples/test_l20_lite.cpp similarity index 91% rename from examples/test_l10.cpp rename to examples/test_l20_lite.cpp index 8563a48..0b64e25 100644 --- a/examples/test_l10.cpp +++ b/examples/test_l20_lite.cpp @@ -1,4 +1,4 @@ -// L10 / CAN / 左手 —— 入门最小示例(版本、单次动作、状态) +// L20_Lite / CAN / 左手 —— 入门最小示例(版本、单次动作、状态) #include "_win_console_utf8.h" #include "LinkerHandApi.h" #include "CommFactory.h" @@ -6,7 +6,7 @@ int main() { try { // 初始化灵巧手 - LinkerHandApi hand(LINKER_HAND::L10, HAND_TYPE::LEFT, COMM_TYPE::CAN); + LinkerHandApi hand(LINKER_HAND::L20_Lite, HAND_TYPE::LEFT, COMM_TYPE::CAN); // 创建 CAN 总线对象 std::shared_ptr bus = Communication::CommFactory::createCanBus("can0", 1000000); diff --git a/examples/test_l10_can_0.cpp b/examples/test_l20_lite_can_0.cpp similarity index 96% rename from examples/test_l10_can_0.cpp rename to examples/test_l20_lite_can_0.cpp index a2ed230..3eea392 100644 --- a/examples/test_l10_can_0.cpp +++ b/examples/test_l20_lite_can_0.cpp @@ -1,4 +1,4 @@ -// L10 / CAN / 左手 —— 读取版本、力矩、速度、状态与触觉(带 TX/RX 日志) +// L20_Lite / CAN / 左手 —— 读取版本、力矩、速度、状态与触觉(带 TX/RX 日志) #include #include @@ -31,7 +31,7 @@ std::string getCurrentTime() { int main() { try { // 初始化灵巧手 - LinkerHandApi hand(LINKER_HAND::L10, HAND_TYPE::LEFT); + LinkerHandApi hand(LINKER_HAND::L20_Lite, HAND_TYPE::LEFT); // 创建 CAN 总线对象 std::shared_ptr bus = Communication::CommFactory::createCanBus("can0", 1000000); diff --git a/examples/test_l10_can_1.cpp b/examples/test_l20_lite_can_1.cpp similarity index 98% rename from examples/test_l10_can_1.cpp rename to examples/test_l20_lite_can_1.cpp index 1a3b6e7..6179029 100644 --- a/examples/test_l10_can_1.cpp +++ b/examples/test_l20_lite_can_1.cpp @@ -1,4 +1,4 @@ -// L10 / CAN / 左手 —— 多线程并发测试(版本/参数/触觉/运动并行) +// L20_Lite / CAN / 左手 —— 多线程并发测试(版本/参数/触觉/运动并行) #include #include #include @@ -238,7 +238,7 @@ int main() { try { // 调用API接口 - LinkerHandApi hand(LINKER_HAND::L10, HAND_TYPE::LEFT); + LinkerHandApi hand(LINKER_HAND::L20_Lite, HAND_TYPE::LEFT); // 创建CAN总线对象用于通信 std::shared_ptr bus = Communication::CommFactory::createCanBus("can0", 1000000); diff --git a/examples/test_l10_modbus.cpp b/examples/test_l20_lite_modbus.cpp similarity index 95% rename from examples/test_l10_modbus.cpp rename to examples/test_l20_lite_modbus.cpp index dc93df4..c16cddb 100644 --- a/examples/test_l10_modbus.cpp +++ b/examples/test_l20_lite_modbus.cpp @@ -1,4 +1,4 @@ -// L10 / Modbus / 左手 —— 速度/扭矩/动作序列与各类状态读取 +// L20_Lite / Modbus / 左手 —— 速度/扭矩/动作序列与各类状态读取 #include #include #include @@ -58,7 +58,7 @@ int main(int argc, char* argv[]) { // 2. 初始化手部 API try { - LinkerHandApi hand(LINKER_HAND::L10, HAND_TYPE::LEFT, COMM_TYPE::MODBUS); + LinkerHandApi hand(LINKER_HAND::L20_Lite, HAND_TYPE::LEFT, COMM_TYPE::MODBUS); // 3. 设置回调函数 setupHandCallbacks(hand); @@ -70,7 +70,7 @@ int main(int argc, char* argv[]) { std::cout << "----------------------------------------" << std::endl; std::cout << "测试 1: 设置速度" << std::endl; std::cout << "----------------------------------------" << std::endl; - std::vector speed(10, 128); // 10个关节,速度值 128 (L10有10个关节) + std::vector speed(10, 128); // 10个关节,速度值 128 (L20_Lite有10个关节) hand.setSpeed(speed); printVector("设置速度", speed); std::this_thread::sleep_for(std::chrono::milliseconds(100)); @@ -91,7 +91,7 @@ int main(int argc, char* argv[]) { std::cout << "测试 3: 手指动作序列" << std::endl; std::cout << "----------------------------------------" << std::endl; - // L10 有 10 个关节: 大拇指弯曲、大拇指偏航、食指弯曲、中指弯曲、无名指弯曲、小拇指弯曲、食指偏航、无名指偏航、小拇指偏航、大拇指翻转 + // L20_Lite 有 10 个关节: 大拇指弯曲、大拇指偏航、食指弯曲、中指弯曲、无名指弯曲、小拇指弯曲、食指偏航、无名指偏航、小拇指偏航、大拇指翻转 std::vector> poses = { {255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, // 完全张开 {0, 255, 255, 255, 255, 255, 255, 255, 255, 255}, // 大拇指弯曲 @@ -216,7 +216,7 @@ int main(int argc, char* argv[]) { } std::cout << "========================================" << std::endl; - std::cout << "L10 测试完成!" << std::endl; + std::cout << "L20_Lite 测试完成!" << std::endl; std::cout << "========================================" << std::endl; std::cout << std::endl; } catch (const std::exception& e) { diff --git a/examples/test_l30_canfd_0.cpp b/examples/test_l30_canfd_0.cpp new file mode 100644 index 0000000..e6674f9 --- /dev/null +++ b/examples/test_l30_canfd_0.cpp @@ -0,0 +1,86 @@ +// L30 (L30V6) / CAN-FD / 右手 —— 版本/动作/速度最小示例 +#include +#include +#include +#include +#include +#include "_win_console_utf8.h" +#include "LinkerHandApi.h" +#include "CommunicationCallbacks.h" +#include "CanFD.h" + +int main() { + std::cout << "L30 CANFD Test" << std::endl; + + // 初始化 CANFD 设备 + Communication::CanFD canfd(0, 0); + if (!canfd.init()) { + std::cerr << "Failed to initialize CANFD" << std::endl; + return 1; + } + std::cout << "CANFD initialized successfully" << std::endl; + + try { + LinkerHandApi api(LINKER_HAND::L30, HAND_TYPE::RIGHT, COMM_TYPE::CAN); + std::cout << "LinkerHandApi created" << std::endl; + + auto can_tx_callback = [&](uint32_t can_id, const uint8_t* data, uintptr_t data_len) -> int32_t { + std::vector data_vec(data, data + data_len); + canfd.send(data_vec, can_id, true); + return 0; + }; + + auto can_rx_callback = [&](uint32_t* can_id_out, uint8_t* data_out, uint8_t* data_len_out) -> int32_t { + Communication::CanFDFrame frame = canfd.recv(10); + if (frame.valid) { + *can_id_out = frame.can_id; + *data_len_out = Communication::CanFD::dlcToLen(frame.can_dlc); + memcpy(data_out, frame.data, *data_len_out); + return 0; + } + return -1; + }; + + api.setCanTxCallback(can_tx_callback); + api.setCanRxCallback(can_rx_callback); + + std::cout << "Callbacks registered, sleeping 1 second..." << std::endl; + std::this_thread::sleep_for(std::chrono::seconds(1)); + + std::cout << "Calling getVersion()..." << std::endl; + std::string version = api.getVersion(); + std::cout << "Version:\n" << version << std::endl; + + // 17 个关节,0-255(半握) + std::vector pos(17, 128); + std::cout << "Calling setPosition()..." << std::endl; + api.setPosition(pos); + + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + + std::cout << "Calling getPosition()..." << std::endl; + std::vector cur = api.getPosition(); + for (size_t i = 0; i < cur.size(); i++) { + std::cout << (int)cur[i] << " "; + } + std::cout << std::endl; + + std::cout << "Calling getSpeed()..." << std::endl; + std::vector speed = api.getSpeed(); + for (size_t i = 0; i < speed.size(); i++) { + std::cout << (int)speed[i] << " "; + } + std::cout << std::endl; + + std::this_thread::sleep_for(std::chrono::seconds(1)); + + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + canfd.close(); + return 1; + } + + canfd.close(); + std::cout << "Test completed" << std::endl; + return 0; +} diff --git a/examples/test_l30_canfd_socket_0.cpp b/examples/test_l30_canfd_socket_0.cpp new file mode 100644 index 0000000..ac0cb28 --- /dev/null +++ b/examples/test_l30_canfd_socket_0.cpp @@ -0,0 +1,132 @@ +// L30 (L30V6) / SocketCAN 原生 CAN-FD / 右手 —— 联调测试 +// +// 使用前先在内核侧配置 can0(协议要求 arb 1Mbps / data 5Mbps): +// sudo ip link set can0 down +// sudo ip link set can0 type can bitrate 1000000 dbitrate 5000000 fd on restart-ms 100 +// sudo ip link set can0 up +// sudo ip link set can0 txqueuelen 1000 +#include +#include +#include +#include +#include +#include "_win_console_utf8.h" +#include "LinkerHandApi.h" +#include "CommunicationCallbacks.h" +#include "CanFDSocket.h" + +int main(int argc, char** argv) { + const char* ifname = (argc > 1) ? argv[1] : "can0"; + std::cout << "L30 SocketCAN CANFD Test on " << ifname << std::endl; + + Communication::CanFDSocket canfd(ifname); + if (!canfd.init()) { + std::cerr << "Failed to open SocketCAN CANFD (" << ifname << ")" << std::endl; + return 1; + } + std::cout << "SocketCAN CANFD opened successfully" << std::endl; + + try { + LinkerHandApi api(LINKER_HAND::L30, HAND_TYPE::RIGHT, COMM_TYPE::CAN); + std::cout << "LinkerHandApi created" << std::endl; + + auto can_tx_callback = [&](uint32_t can_id, const uint8_t* data, uintptr_t data_len) -> int32_t { + std::vector data_vec(data, data + data_len); + canfd.send(data_vec, can_id, true); + return 0; + }; + + auto can_rx_callback = [&](uint32_t* can_id_out, uint8_t* data_out, uint8_t* data_len_out) -> int32_t { + Communication::CanFDFrame frame = canfd.recv(10); + if (frame.valid) { + *can_id_out = frame.can_id; + *data_len_out = frame.can_dlc; // SocketCAN 变体:can_dlc 即实际字节长度 + memcpy(data_out, frame.data, *data_len_out); + return 0; + } + return -1; + }; + + api.setCanTxCallback(can_tx_callback); + api.setCanRxCallback(can_rx_callback); + + std::cout << "Callbacks registered, sleeping 1 second..." << std::endl; + std::this_thread::sleep_for(std::chrono::seconds(1)); + + std::cout << "Calling getVersion()..." << std::endl; + std::string version = api.getVersion(); + std::cout << "Version:\n" << version << std::endl; + + std::cout << "Calling getPosition()..." << std::endl; + std::vector pos0 = api.getPosition(); + std::cout << "pos(" << pos0.size() << "): "; + for (size_t i = 0; i < pos0.size(); i++) std::cout << (int)pos0[i] << " "; + std::cout << std::endl; + + std::cout << "Calling getTemperature()..." << std::endl; + std::vector temp = api.getTemperature(); + std::cout << "temp(" << temp.size() << "): "; + for (size_t i = 0; i < temp.size(); i++) std::cout << (int)temp[i] << " "; + std::cout << std::endl; + + std::cout << "Calling getFaultCode()..." << std::endl; + std::vector err = api.getFaultCode(); + std::cout << "err(" << err.size() << "): "; + for (size_t i = 0; i < err.size(); i++) std::cout << (int)err[i] << " "; + std::cout << std::endl; + + // 使能全部关节:先失能再使能,制造 0→1 边沿(实测该固件使能为边沿触发, + // 使能标志已为1时再写1不会重新上电,须先失能再使能) + std::cout << "Calling setDisable() then setMotorEnable()..." << std::endl; + api.setDisable(std::vector(17, 0)); + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + api.setEnable(std::vector(17, 1)); + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + + // 设置速度(0-255 → 0~32767;取较低速,避免快速运动堵转) + std::cout << "Calling setSpeed(50)..." << std::endl; + api.setSpeed(std::vector(17, 50)); + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + + // 张开 / 半握循环。 + // 注意:当前各关节 0-255 → 原始量程为“占位满量程”映射,会把小行程关节 + // (侧摆/旋转)顶到机械极限而堵转,固件会静默保护性关闭个别电机。 + // 因此这里目标取适中值(120/20),不顶到底;待接入真实 URDF 量程后可放开。 + for (int cycle = 0; cycle < 2; ++cycle) { + uint8_t val = (cycle % 2 == 0) ? 120 : 20; + std::cout << "setPosition(" << (int)val << ")..." << std::endl; + api.setPosition(std::vector(17, val)); + std::this_thread::sleep_for(std::chrono::milliseconds(1500)); + + std::vector cur = api.getPosition(); // 同步读,返回新到应答 + std::cout << " pos: "; + for (size_t i = 0; i < cur.size(); i++) std::cout << (int)cur[i] << " "; + std::cout << std::endl; + } + + std::cout << "Calling getSpeed()..." << std::endl; + std::vector speed = api.getSpeed(); + std::cout << "speed(" << speed.size() << "): "; + for (size_t i = 0; i < speed.size(); i++) std::cout << (int)speed[i] << " "; + std::cout << std::endl; + + std::cout << "Calling getForce() (thumb sample)..." << std::endl; + auto force = api.getForce(); + if (!force.empty()) { + std::cout << " thumb[0]: "; + for (size_t k = 0; k < force[0][0].size(); ++k) std::cout << (int)force[0][0][k] << " "; + std::cout << std::endl; + } + + std::this_thread::sleep_for(std::chrono::seconds(1)); + + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + canfd.close(); + return 1; + } + + canfd.close(); + std::cout << "Test completed" << std::endl; + return 0; +} diff --git a/examples/test_l30_canfd_socket_auto_0.cpp b/examples/test_l30_canfd_socket_auto_0.cpp new file mode 100644 index 0000000..32ab5da --- /dev/null +++ b/examples/test_l30_canfd_socket_auto_0.cpp @@ -0,0 +1,79 @@ +// L30 (L30V6) / SocketCAN 原生 CAN-FD / 自动检测左右手 —— 联调测试 +// +// 演示 CanFDSocket(HAND_TYPE) 自动检测构造:遍历所有已启动的 CAN 接口, +// 逐一发 0xC3(CANFDID/左右手) 读请求,命中与期望侧别匹配的接口后自动打开。 +// 参考 CanBus(HAND_TYPE) 的 auto_detect_channel 语义。 +// +// 使用前先在内核侧把两路 canfd 都配好(协议要求 arb 1Mbps / data 5Mbps): +// sudo ip link set canX down +// sudo ip link set canX type can bitrate 1000000 dbitrate 5000000 fd on restart-ms 100 +// sudo ip link set canX up +// sudo ip link set canX txqueuelen 1000 +// +// 运行: ./test_l30_canfd_socket_auto_0 [left|right] (默认 right) +#include +#include +#include +#include +#include +#include +#include "_win_console_utf8.h" +#include "LinkerHandApi.h" +#include "CommunicationCallbacks.h" +#include "CanFDSocket.h" + +int main(int argc, char** argv) { + HAND_TYPE side = HAND_TYPE::RIGHT; + if (argc > 1) { + std::string arg = argv[1]; + if (arg == "left" || arg == "LEFT" || arg == "l") side = HAND_TYPE::LEFT; + } + const char* side_name = (side == HAND_TYPE::LEFT) ? "LEFT" : "RIGHT"; + std::cout << "L30 SocketCAN CANFD auto-detect test, target = " << side_name << std::endl; + + try { + // 自动检测左右手:内部遍历已启动的 CAN 接口并打开命中的那一路 + Communication::CanFDSocket canfd(side); + std::cout << "SocketCAN CANFD opened successfully (auto-detected)" << std::endl; + + LinkerHandApi api(LINKER_HAND::L30, side, COMM_TYPE::CAN); + std::cout << "LinkerHandApi created" << std::endl; + + auto can_tx_callback = [&](uint32_t can_id, const uint8_t* data, uintptr_t data_len) -> int32_t { + std::vector data_vec(data, data + data_len); + canfd.send(data_vec, can_id, true); + return 0; + }; + + auto can_rx_callback = [&](uint32_t* can_id_out, uint8_t* data_out, uint8_t* data_len_out) -> int32_t { + Communication::CanFDFrame frame = canfd.recv(10); + if (frame.valid) { + *can_id_out = frame.can_id; + *data_len_out = frame.can_dlc; + memcpy(data_out, frame.data, *data_len_out); + return 0; + } + return -1; + }; + + api.setCanTxCallback(can_tx_callback); + api.setCanRxCallback(can_rx_callback); + + std::this_thread::sleep_for(std::chrono::seconds(1)); + + std::cout << "Version:\n" << api.getVersion() << std::endl; + + std::vector pos = api.getPosition(); + std::cout << "pos(" << pos.size() << "): "; + for (size_t i = 0; i < pos.size(); i++) std::cout << (int)pos[i] << " "; + std::cout << std::endl; + + canfd.close(); + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + return 1; + } + + std::cout << "Test completed" << std::endl; + return 0; +} diff --git a/examples/web_bridge.cpp b/examples/web_bridge.cpp index 0b93ebf..0f62ea0 100644 --- a/examples/web_bridge.cpp +++ b/examples/web_bridge.cpp @@ -3,11 +3,13 @@ // 姿势/速度/力矩,周期性把实测位置、速度、力矩、触觉压感(含掌心)以文本/JSON 行写到 stdout。 // // 用法: web_bridge [comm] [channel] -// MODEL L6/L7/L10/L20/L21/L25/G20/O6/O20 +// MODEL L6/L7/L20_LITE/L20/L21/L25/L20_10/O6/O20 // side left|right // comm 可选,缺省 can。can | canfd | modbus。 -// modbus 仅支持 O6/L7/L10;O20 请用 canfd。 +// modbus 仅支持 O6/L7/L20_Lite;O20 请用 canfd。 // channel 可选。can:CAN 接口名(如 can0),留空按 side 自动探测。 +// canfd(L30):留空按 side 自动检测左右手所在的 CANFD 通道(socketcan 自检); +// "socketcan:can0" 指定内核原生接口;"socketcan:" 留空同样自检。 // canfd(O20):留空用厂商 CAN-FD 设备(0,0);"socketcan:can0" 用内核原生 CAN-FD。 // modbus:串口路径(如 /dev/ttyUSB0),留空按 side 自动探测。 // @@ -23,7 +25,7 @@ // POS v0..vN 关节实测位置,~10Hz // SPD v0..vN / TRQ v0..vN 当前速度/力矩回读,低频 // FORCE {"fingers":[[[..]]]} 触觉矩阵(维度按型号自适应),有传感器时持续 ~5Hz(全 0 也发) -// PALM {"palm":[[..]]} 掌心矩阵(O6/G20),有传感器时持续 ~5Hz(全 0 也发) +// PALM {"palm":[[..]]} 掌心矩阵(O6/L20_10),有传感器时持续 ~5Hz(全 0 也发) // TEMP v0..vN 逐关节温度(°C),~1Hz,非空才发 // FAULT v0..vN 逐关节故障码,~1Hz,非空才发 // @@ -54,17 +56,18 @@ static std::mutex g_qmtx; static std::deque g_queue; static std::atomic g_running{true}; -// 型号字符串 -> 枚举(与 hand_teach_pendant/src/main.cpp 的 -t 解析保持一致)。 +// 型号字符串 -> 枚举(对外用 L20_10 / L20_LITE 标识;示教器 -t 仍兼容旧的 G20 / L10 别名,二者同值)。 static bool parse_model(const std::string& s, LINKER_HAND& out) { if (s == "L6") out = LINKER_HAND::L6; else if (s == "L7") out = LINKER_HAND::L7; - else if (s == "L10") out = LINKER_HAND::L10; + else if (s == "L20_LITE" || s == "L20_Lite" || s == "L10") out = LINKER_HAND::L20_Lite; else if (s == "L20") out = LINKER_HAND::L20; else if (s == "L21") out = LINKER_HAND::L21; else if (s == "L25") out = LINKER_HAND::L25; - else if (s == "G20") out = LINKER_HAND::G20; + else if (s == "L20_10") out = LINKER_HAND::L20_10; else if (s == "O6") out = LINKER_HAND::O6; else if (s == "O20") out = LINKER_HAND::O20; + else if (s == "L30") out = LINKER_HAND::L30; else return false; return true; } @@ -78,8 +81,9 @@ static int dof_of(LINKER_HAND m) { case L10: return 10; case L20: return 20; case L21: case L25: return 25; - case G20: return 16; + case L20_10: return 16; case O20: return 16; + case L30: return 17; default: return 10; } } @@ -135,16 +139,18 @@ int main(int argc, char** argv) { // canfd 是 CAN 之上的传输变体(仅 O20 有意义),SDK 仍以 COMM_TYPE::CAN 构造。 const bool is_modbus = (comm == "modbus"); - const bool is_canfd = (comm == "canfd") || (model == LINKER_HAND::O20 && comm != "modbus"); + const bool is_canfd = (comm == "canfd") + || (model == LINKER_HAND::O20 && comm != "modbus") + || (model == LINKER_HAND::L30 && comm != "modbus"); try { COMM_TYPE ct = is_modbus ? COMM_TYPE::MODBUS : COMM_TYPE::CAN; auto hand = std::make_shared(model, side, ct); if (is_modbus) { - // Modbus 仅 O6/L7/L10 有对应 hand 实现(HandFactory 其余会抛异常)。 - if (model != LINKER_HAND::O6 && model != LINKER_HAND::L7 && model != LINKER_HAND::L10) { - std::cerr << "BRIDGE_ERROR: Modbus 仅支持 O6/L7/L10(当前 " << model_str << ")" << std::endl; + // Modbus 仅 O6/L7/L20_Lite 有对应 hand 实现(HandFactory 其余会抛异常)。 + if (model != LINKER_HAND::O6 && model != LINKER_HAND::L7 && model != LINKER_HAND::L20_Lite) { + std::cerr << "BRIDGE_ERROR: Modbus 仅支持 O6/L7/L20_Lite(当前 " << model_str << ")" << std::endl; return 1; } // channel 为串口路径;留空按 side 自动探测。 @@ -165,14 +171,26 @@ int main(int argc, char** argv) { return 0; }); } else if (is_canfd) { - // O20 走 CAN-FD。默认厂商 libcanbus;channel="socketcan:can0" 时用内核原生。 + // O20/L30 走 CAN-FD。channel 语义: + // "socketcan:canX" 或留空 -> 内核原生 CanFDSocket;留空/"socketcan:" 后无接口名时 + // 自动检测左右手所在通道(CanFDSocket(side) 自检,仅 L30 协议可自检)。 + // 其余非空字符串 -> 厂商 libcanbus CAN-FD(O20 默认设备(0,0) 走此路,写 "0" 即可)。 bool use_socketcan = channel.rfind("socketcan:", 0) == 0; - if (use_socketcan) { + // 留空:L30 走 socketcan 自动检测;O20 无 socketcan 自检协议,仍回退厂商默认(0,0)。 + bool auto_socketcan = channel.empty() && (model == LINKER_HAND::L30); + if (use_socketcan || auto_socketcan) { #if defined(__linux__) - std::string iface = channel.substr(std::string("socketcan:").size()); - if (iface.empty()) iface = "can0"; - auto cf = std::make_shared(iface); - if (!cf->init()) { std::cerr << "BRIDGE_ERROR: CanFDSocket init failed (" << iface << ")" << std::endl; return 1; } + std::string iface; + if (use_socketcan) iface = channel.substr(std::string("socketcan:").size()); + std::shared_ptr cf; + if (iface.empty()) { + // 未指定接口名:自动检测左右手所在的 CANFD 通道(内部遍历已启动接口, + // 发 0xC3 读请求校验左右手,命中后打开;失败抛异常→BRIDGE_ERROR)。 + cf = std::make_shared(side); + } else { + cf = std::make_shared(iface); + if (!cf->init()) { std::cerr << "BRIDGE_ERROR: CanFDSocket init failed (" << iface << ")" << std::endl; return 1; } + } hand->setCanTxCallback([cf](uint32_t id, const uint8_t* d, uintptr_t n) -> int32_t { try { std::vector v(d, d + n); cf->send(v, id, true); } catch (...) { return -1; } return 0; @@ -236,8 +254,18 @@ int main(int argc, char** argv) { } // 保守初始化:中等速度/力矩,长度 = DOF。 - hand->setSpeed(std::vector(dof, 180)); - hand->setTorque(std::vector(dof, 180)); + // L30(L30V6) 使能为边沿触发:先失能再使能制造 0→1 边沿,电机方能真正上电; + // 并取较低速,减小把小行程关节顶到机械极限而堵转的概率。 + if (model == LINKER_HAND::L30) { + hand->setDisable(std::vector(dof, 0)); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + hand->setEnable(std::vector(dof, 1)); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + hand->setSpeed(std::vector(dof, 60)); + } else { + hand->setSpeed(std::vector(dof, 180)); + hand->setTorque(std::vector(dof, 180)); + } // 版本串各字段以换行分隔;压进单行 JSON 用 ';' 保留字段边界(前端按 ';' 切段),引号替空格。 auto sanitize = [](std::string v) { @@ -249,13 +277,32 @@ int main(int argc, char** argv) { int pos_ms = 100, st_ms = 200, force_ms = 33, temp_ms = 1000, fault_ms = 1000; auto hz_of = [](int period_ms) { return period_ms > 0 ? 1000 / period_ms : 0; }; + + // 原始值直控:若手型提供各关节原始量程,则前端滑块按真实 int16 行程铺满、 + // 发送原始值(绕过 0-255 量化,大行程关节低速更平顺)。目前 L30 支持。 + std::vector> raw_ranges = hand->getPositionRangeRaw(); + const bool raw_mode = !raw_ranges.empty() && (int)raw_ranges.size() >= dof; + // 速度/扭矩实际量程(raw 模式下前端滑块按此铺满、发原始值) + std::pair spd_range = hand->getSpeedRangeRaw(); + std::pair trq_range = hand->getTorqueRangeRaw(); + auto emit_meta = [&](const std::string& v) { std::cout << "META {\"model\":\"" << model_str << "\",\"dof\":" << dof << ",\"version\":\"" << v << "\"" << ",\"rates\":{\"pos\":" << hz_of(pos_ms) << ",\"st\":" << hz_of(st_ms) << ",\"force\":" << hz_of(force_ms) << ",\"temp\":" << hz_of(temp_ms) - << ",\"fault\":" << hz_of(fault_ms) << "}" - << "}" << std::endl; + << ",\"fault\":" << hz_of(fault_ms) << "}"; + if (raw_mode) { + std::cout << ",\"raw\":true,\"ranges\":["; + for (int i = 0; i < dof; ++i) { + if (i) std::cout << ","; + std::cout << "[" << (int)raw_ranges[i].first << "," << (int)raw_ranges[i].second << "]"; + } + std::cout << "]"; + std::cout << ",\"speed_range\":[" << (int)spd_range.first << "," << (int)spd_range.second << "]"; + std::cout << ",\"torque_range\":[" << (int)trq_range.first << "," << (int)trq_range.second << "]"; + } + std::cout << "}" << std::endl; }; // getVersion 发请求后读缓存,版本回帧由 rx 线程异步填充;刚上电时手在预热,可能迟迟不到。 @@ -302,6 +349,23 @@ int main(int argc, char** argv) { return; } if (cmd != "P" && cmd != "S" && cmd != "T") return; + // 原始模式:位置/速度/扭矩均按 int16 原始值下发(允许负值,不做 0-255 限幅) + if (raw_mode) { + std::vector raw; + int x; + while (ss >> x) { + if (x < -32768) x = -32768; + if (x > 32767) x = 32767; + raw.push_back(static_cast(x)); + } + if ((int)raw.size() != dof) return; + try { + if (cmd == "P") hand->setPositionRaw(raw); + else if (cmd == "S") hand->setSpeedRaw(raw); + else hand->setTorqueRaw(raw); + } catch (const std::exception& e) { std::cerr << "BRIDGE_WARN: apply raw failed: " << e.what() << std::endl; } + return; + } std::vector vals; int x; while (ss >> x) { @@ -346,15 +410,27 @@ int main(int argc, char** argv) { try { if (now >= next_pos) { // 位置回读(周期可调) next_pos = now + ms(pos_ms); - std::vector pos = hand->getPosition(); - if (!pos.empty()) { std::ostringstream o; o << "POS"; for (auto v : pos) o << ' ' << (int)v; std::cout << o.str() << std::endl; } + if (raw_mode) { + std::vector pos = hand->getPositionRaw(); + if (!pos.empty()) { std::ostringstream o; o << "POS"; for (auto v : pos) o << ' ' << (int)v; std::cout << o.str() << std::endl; } + } else { + std::vector pos = hand->getPosition(); + if (!pos.empty()) { std::ostringstream o; o << "POS"; for (auto v : pos) o << ' ' << (int)v; std::cout << o.str() << std::endl; } + } } if (now >= next_st) { // 速度/力矩回读(周期可调) next_st = now + ms(st_ms); - std::vector sp = hand->getSpeed(); - std::vector tq = hand->getTorque(); - if (!sp.empty()) { std::ostringstream o; o << "SPD"; for (auto v : sp) o << ' ' << (int)v; std::cout << o.str() << std::endl; } - if (!tq.empty()) { std::ostringstream o; o << "TRQ"; for (auto v : tq) o << ' ' << (int)v; std::cout << o.str() << std::endl; } + if (raw_mode) { + std::vector sp = hand->getSpeedRaw(); + std::vector tq = hand->getTorqueRaw(); + if (!sp.empty()) { std::ostringstream o; o << "SPD"; for (auto v : sp) o << ' ' << (int)v; std::cout << o.str() << std::endl; } + if (!tq.empty()) { std::ostringstream o; o << "TRQ"; for (auto v : tq) o << ' ' << (int)v; std::cout << o.str() << std::endl; } + } else { + std::vector sp = hand->getSpeed(); + std::vector tq = hand->getTorque(); + if (!sp.empty()) { std::ostringstream o; o << "SPD"; for (auto v : sp) o << ' ' << (int)v; std::cout << o.str() << std::endl; } + if (!tq.empty()) { std::ostringstream o; o << "TRQ"; for (auto v : tq) o << ' ' << (int)v; std::cout << o.str() << std::endl; } + } } if (now >= next_force) { // 触觉/掌心,周期由前端可调 next_force = now + ms(force_ms); @@ -381,6 +457,15 @@ int main(int argc, char** argv) { std::this_thread::sleep_for(ms(5)); } + + // webui 退出/断开时主动失能,释放手部(仅 webui 后端行为,SDK 库本身不自动失能)。 + // L30 使能为边沿触发,此处显式下发全失能;留时间让发送线程把帧发出后再退出。 + if (model == LINKER_HAND::L30) { + try { + hand->setDisable(std::vector(dof, 0)); + std::this_thread::sleep_for(ms(80)); + } catch (...) {} + } } catch (const std::exception& e) { std::cerr << "BRIDGE_ERROR: " << e.what() << std::endl; return 1; diff --git a/include/api/LinkerHandApi.h b/include/api/LinkerHandApi.h index 41ba90d..c2b9ce7 100644 --- a/include/api/LinkerHandApi.h +++ b/include/api/LinkerHandApi.h @@ -11,6 +11,8 @@ #include #include #include +#include +#include #include #include "Common.h" @@ -55,6 +57,17 @@ class LINKERHAND_API LinkerHandApi std::vector getPosition(); std::vector getPositionArc(); std::vector testPositionArc(const std::vector &pose); + // 原始值直控(绕过 0-255 量化,完整分辨率;目前 L30 支持) + void setPositionRaw(const std::vector &raw); + std::vector getPositionRaw(); + std::vector> getPositionRangeRaw(); + // 速度/扭矩原始值直控(实际量程;目前 L30 支持) + void setSpeedRaw(const std::vector &raw); + std::vector getSpeedRaw(); + std::pair getSpeedRangeRaw(); + void setTorqueRaw(const std::vector &raw); + std::vector getTorqueRaw(); + std::pair getTorqueRangeRaw(); // 设置速度 void setSpeed(const std::vector &speed); diff --git a/include/communication/CanFDSocket.h b/include/communication/CanFDSocket.h index 036a95b..95fdc07 100644 --- a/include/communication/CanFDSocket.h +++ b/include/communication/CanFDSocket.h @@ -11,7 +11,9 @@ #include #include #include +#include #include "communication/ICanFD.h" +#include "core/Common.h" #include "core/LinkerHandExport.h" namespace linkerhand { @@ -25,6 +27,10 @@ namespace communication { public: explicit CanFDSocket(const std::string& interface = "can0"); + // 自动检测左右手:遍历已启动的 CAN 接口,逐一发 0xC3(CANFDID/左右手) 读请求, + // 命中与 hand_type 匹配的接口后打开该接口(参考 CanBus 的 auto_detect_channel)。 + // L30V6 协议:arb 1Mbps / data 5Mbps FD 帧,需内核侧已 fd on。 + explicit CanFDSocket(const HAND_TYPE& hand_type); ~CanFDSocket(); bool init(); @@ -35,6 +41,13 @@ namespace communication CanFDFrame recv(int timeout_ms = 100) override; private: + // —— 自动检测左右手辅助(仅供 CanFDSocket(HAND_TYPE) 使用)—— + static std::vector detect_can_interfaces(); + static bool is_interface_up(const std::string& ifname); + // 在 ifname 上发 0xC3 读请求,若能读到与 hand_type 匹配的应答则返回 true + static bool probe_hand(const std::string& ifname, HAND_TYPE hand_type); + static std::string auto_detect_channel(HAND_TYPE hand_type); + int socket_fd = -1; std::string interface; std::atomic is_open{false}; diff --git a/include/core/Common.h b/include/core/Common.h index c9a9384..b730b27 100644 --- a/include/core/Common.h +++ b/include/core/Common.h @@ -12,7 +12,11 @@ enum LINKER_HAND { L25, O6, G20, - O20 + O20, + L30, + L20_10 = G20, + L20_11 = G20, + L20_Lite = L10 }; enum HAND_TYPE { @@ -40,7 +44,11 @@ enum class HandModel : uint8_t { L25, G20, O6, - O20 + O20, + L30, + L20_10 = G20, + L20_11 = G20, + L20_Lite = L10 }; enum class HandType : uint8_t { @@ -65,6 +73,7 @@ inline LINKER_HAND to_legacy(HandModel m) { case HandModel::G20: return LINKER_HAND::G20; case HandModel::O6: return LINKER_HAND::O6; case HandModel::O20: return LINKER_HAND::O20; + case HandModel::L30: return LINKER_HAND::L30; } return LINKER_HAND::L10; } diff --git a/lib/linux/arm64/linkerhand_cpp_sdk.so.2 b/lib/linux/arm64/linkerhand_cpp_sdk.so.2 index d427e22..7405b82 120000 --- a/lib/linux/arm64/linkerhand_cpp_sdk.so.2 +++ b/lib/linux/arm64/linkerhand_cpp_sdk.so.2 @@ -1 +1 @@ -linkerhand_cpp_sdk.so.2.1.9 \ No newline at end of file +linkerhand_cpp_sdk.so.2.2.0 \ No newline at end of file diff --git a/lib/linux/arm64/linkerhand_cpp_sdk.so.2.1.9 b/lib/linux/arm64/linkerhand_cpp_sdk.so.2.1.9 deleted file mode 100644 index b353985..0000000 Binary files a/lib/linux/arm64/linkerhand_cpp_sdk.so.2.1.9 and /dev/null differ diff --git a/lib/linux/arm64/linkerhand_cpp_sdk.so.2.2.0 b/lib/linux/arm64/linkerhand_cpp_sdk.so.2.2.0 new file mode 100644 index 0000000..1caf1e8 Binary files /dev/null and b/lib/linux/arm64/linkerhand_cpp_sdk.so.2.2.0 differ diff --git a/lib/linux/x86_64/linkerhand_cpp_sdk.so.2 b/lib/linux/x86_64/linkerhand_cpp_sdk.so.2 index d427e22..7405b82 120000 --- a/lib/linux/x86_64/linkerhand_cpp_sdk.so.2 +++ b/lib/linux/x86_64/linkerhand_cpp_sdk.so.2 @@ -1 +1 @@ -linkerhand_cpp_sdk.so.2.1.9 \ No newline at end of file +linkerhand_cpp_sdk.so.2.2.0 \ No newline at end of file diff --git a/lib/linux/x86_64/linkerhand_cpp_sdk.so.2.1.9 b/lib/linux/x86_64/linkerhand_cpp_sdk.so.2.1.9 deleted file mode 100644 index 281c474..0000000 Binary files a/lib/linux/x86_64/linkerhand_cpp_sdk.so.2.1.9 and /dev/null differ diff --git a/lib/linux/x86_64/linkerhand_cpp_sdk.so.2.2.0 b/lib/linux/x86_64/linkerhand_cpp_sdk.so.2.2.0 new file mode 100644 index 0000000..aa09b24 Binary files /dev/null and b/lib/linux/x86_64/linkerhand_cpp_sdk.so.2.2.0 differ diff --git a/lib/win64/mingw/linkerhand_cpp_sdk.dll b/lib/win64/mingw/linkerhand_cpp_sdk.dll index 3d5d47e..2c1ac30 100644 Binary files a/lib/win64/mingw/linkerhand_cpp_sdk.dll and b/lib/win64/mingw/linkerhand_cpp_sdk.dll differ diff --git a/lib/win64/msvc/linkerhand_cpp_sdk.dll b/lib/win64/msvc/linkerhand_cpp_sdk.dll index 6d939dd..1d069ad 100644 Binary files a/lib/win64/msvc/linkerhand_cpp_sdk.dll and b/lib/win64/msvc/linkerhand_cpp_sdk.dll differ diff --git a/lib/win64/msvc/linkerhand_cpp_sdk.lib b/lib/win64/msvc/linkerhand_cpp_sdk.lib index 3722ccc..ed0738d 100644 Binary files a/lib/win64/msvc/linkerhand_cpp_sdk.lib and b/lib/win64/msvc/linkerhand_cpp_sdk.lib differ diff --git a/start-webui.sh b/start-webui.sh new file mode 100755 index 0000000..e39fa79 --- /dev/null +++ b/start-webui.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# 启动 Web 示教器:先确保 C++ 桥 web_bridge 已编译,未编译则自动构建,再拉起 webui。 +# 透传所有参数给 run.py(如 --model O6 --side left --port 8080)。 +set -euo pipefail + +REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BRIDGE="$REPO/build/bin/web_bridge" + +# web_bridge 由 examples 一站式构建产出(BUILD_EXAMPLES=ON)。缺失则先编译。 +if [ ! -x "$BRIDGE" ]; then + echo "[start-webui] 未找到已编译的 web_bridge ($BRIDGE),开始构建 SDK..." + "$REPO/build.sh" -b +fi + +if [ ! -x "$BRIDGE" ]; then + echo "[start-webui] 构建后仍未找到 web_bridge,请检查构建输出。" >&2 + exit 1 +fi + +exec python3 "$REPO/webui/run.py" "$@" diff --git a/webui/README.md b/webui/README.md index 6be1bae..9301713 100644 --- a/webui/README.md +++ b/webui/README.md @@ -1,8 +1,8 @@ # Web 示教器(webui/) -一套开箱即用的灵巧手网页控制界面,全型号通用,`./build.sh -b` 编译后 `python3 webui/run.py` 即可使用。 +一套开箱即用的灵巧手网页控制界面,全型号通用,`./build.sh -b` 编译后 `python3 webui/run.py` 即可使用;也可用仓库根目录的 `./start-webui.sh` 一条命令启动(未编译会自动先构建)。 -支持型号:L6 / L7 / L10 / L20 / L21 / L25 / G20 / O6 / O20(O20 走 CAN-FD)。 +支持型号:L6 / L7 / L20_Lite / L20 / L21 / L25 / L20_10 / O6 / O20(O20 走 CAN-FD)。 ## 前置 @@ -28,20 +28,27 @@ 两种方式,任选其一。 -**① 在线连接(推荐)**:不带 `--model` 启动,浏览器打开后在页面「设置」面板里选择型号 / 侧别 / 通信方式并连接,支持热重连与主动断开。 +**① 一条命令(推荐)**:仓库根目录的 `./start-webui.sh` 会先检测 `build/bin/web_bridge` 是否已编译,未编译则自动跑 `./build.sh -b`,再启动服务。所有参数原样透传给 `run.py`: + +```bash +./start-webui.sh # 在前端「设置」面板在线连接 +./start-webui.sh --model O6 --side left +``` + +**② 在线连接**:不带 `--model` 启动,浏览器打开后在页面「设置」面板里选择型号 / 侧别 / 通信方式并连接,支持热重连与主动断开。 ```bash python3 webui/run.py ``` -**② 命令行直连**:启动即按参数连接指定型号。 +**③ 命令行直连**:启动即按参数连接指定型号。 ```bash python3 webui/run.py --model O6 --side left -python3 webui/run.py --model L10 --side left --channel can0 +python3 webui/run.py --model L20_LITE --side left --channel can0 python3 webui/run.py --model O20 --side right # 厂商 CAN-FD 设备 python3 webui/run.py --model O20 --side right --channel socketcan:can0 -python3 webui/run.py --model L10 --comm modbus --channel /dev/ttyUSB0 +python3 webui/run.py --model L20_LITE --comm modbus --channel /dev/ttyUSB0 ``` 启动后浏览器打开 `http://<本机IP>:8080/`。 @@ -50,7 +57,7 @@ python3 webui/run.py --model L10 --comm modbus --channel /dev/ttyUSB0 | 参数 | 说明 | 默认 | |------|------|------| -| `--model` | 启动即连的型号(L6/O6/L7/L10/L20/G20/L21/L25/O20);不填则在前端在线连接 | 无 | +| `--model` | 启动即连的型号(L6/O6/L7/L20_LITE/L20/L20_10/L21/L25/O20);不填则在前端在线连接 | 无 | | `--side` | `left` / `right` | `left` | | `--comm` | `can` / `canfd` / `modbus`;缺省按型号自动(O20→canfd,其余→can) | 自动 | | `--channel` | CAN 接口名(如 `can0`)/ Modbus 串口(如 `/dev/ttyUSB0`);O20 可用 `socketcan:can0` | 自动 | @@ -61,7 +68,7 @@ python3 webui/run.py --model L10 --comm modbus --channel /dev/ttyUSB0 - 按型号动态渲染关节滑块,实时位置回读 - 速度 / 力矩设置与回读 -- 触觉压感热力图(含掌心,O6 / G20) +- 触觉压感热力图(含掌心,O6 / L20_10) - 温度 / 故障监控(温度 >50 黄、>60 红,故障码非 0 红) - 版本信息展示、明暗主题切换 diff --git a/webui/run.py b/webui/run.py index c337cfb..14df575 100644 --- a/webui/run.py +++ b/webui/run.py @@ -29,12 +29,12 @@ "L6": ["拇指根部", "拇指侧摆", "食指根部", "中指根部", "无名指根部", "小指根部"], "O6": ["拇指根部", "拇指侧摆", "食指根部", "中指根部", "无名指根部", "小指根部"], "L7": ["大拇指弯曲", "大拇指横摆", "食指弯曲", "中指弯曲", "无名指弯曲", "小拇指弯曲", "拇指旋转"], - "L10": ["拇指根部", "拇指侧摆", "食指根部", "中指根部", "无名指根部", "小指根部", + "L20_LITE": ["拇指根部", "拇指侧摆", "食指根部", "中指根部", "无名指根部", "小指根部", "食指侧摆", "无名指侧摆", "小指侧摆", "拇指旋转"], "L20": ["拇指根部", "食指根部", "中指根部", "无名指根部", "小指根部", "拇指侧摆", "食指侧摆", "中指侧摆", "无名指侧摆", "小指侧摆", "拇指横摆", "预留1", "预留2", "预留3", "预留4", "拇指尖部", "食指末端", "中指末端", "无名指末端", "小指末端"], - "G20": ["拇指根部", "食指根部", "中指根部", "无名指根部", "小指根部", + "L20_10": ["拇指根部", "食指根部", "中指根部", "无名指根部", "小指根部", "拇指侧摆", "食指侧摆", "中指侧摆", "无名指侧摆", "小指侧摆", "拇指横摆", "拇指指尖", "食指指尖", "中指指尖", "无名指指尖", "小指指尖"], "L21": ["大拇指根部", "食指根部", "中指根部", "无名指根部", "小拇指根部", "大拇指侧摆", @@ -50,6 +50,10 @@ "中指侧摆", "中指指根", "中指指尖", "无名指侧摆", "无名指指根", "无名指指尖", "小指侧摆", "小指指根", "小指指尖"], + "L30": ["拇指指根", "拇指指尖", "拇指侧摆", "拇指旋转", + "无名指侧摆", "无名指指尖", "无名指指根", + "中指指根", "中指指尖", "小指指根", "小指指尖", + "小指侧摆", "中指侧摆", "食指侧摆", "食指指根", "食指指尖", "手腕"], } # 与 JOINT_NAMES 同型号、同顺序的英文关节名,供前端英文界面使用。 @@ -57,12 +61,12 @@ "L6": ["Thumb base", "Thumb abd.", "Index base", "Middle base", "Ring base", "Pinky base"], "O6": ["Thumb base", "Thumb abd.", "Index base", "Middle base", "Ring base", "Pinky base"], "L7": ["Thumb flex", "Thumb roll", "Index flex", "Middle flex", "Ring flex", "Pinky flex", "Thumb rot."], - "L10": ["Thumb base", "Thumb abd.", "Index base", "Middle base", "Ring base", "Pinky base", + "L20_LITE": ["Thumb base", "Thumb abd.", "Index base", "Middle base", "Ring base", "Pinky base", "Index abd.", "Ring abd.", "Pinky abd.", "Thumb rot."], "L20": ["Thumb base", "Index base", "Middle base", "Ring base", "Pinky base", "Thumb abd.", "Index abd.", "Middle abd.", "Ring abd.", "Pinky abd.", "Thumb roll", "Reserved 1", "Reserved 2", "Reserved 3", "Reserved 4", "Thumb tip", "Index tip", "Middle tip", "Ring tip", "Pinky tip"], - "G20": ["Thumb base", "Index base", "Middle base", "Ring base", "Pinky base", + "L20_10": ["Thumb base", "Index base", "Middle base", "Ring base", "Pinky base", "Thumb abd.", "Index abd.", "Middle abd.", "Ring abd.", "Pinky abd.", "Thumb roll", "Thumb tip", "Index tip", "Middle tip", "Ring tip", "Pinky tip"], "L21": ["Thumb base", "Index base", "Middle base", "Ring base", "Pinky base", "Thumb abd.", @@ -78,6 +82,10 @@ "Middle abd.", "Middle base", "Middle tip", "Ring abd.", "Ring base", "Ring tip", "Pinky abd.", "Pinky base", "Pinky tip"], + "L30": ["Thumb base", "Thumb tip", "Thumb abd.", "Thumb rot.", + "Ring abd.", "Ring tip", "Ring base", + "Middle base", "Middle tip", "Pinky base", "Pinky tip", + "Pinky abd.", "Middle abd.", "Index abd.", "Index base", "Index tip", "Wrist"], } bridge = None @@ -88,18 +96,19 @@ current = None # 当前连接参数 {"model","side","comm","channel"};未连接为 None # 型号 -> 可选通信方式(前端下拉联动 + /connect 校验依据)。 -# CAN 全型号支持;CAN-FD 仅 O20;Modbus 仅 O6/L7/L10(HandFactory 其余会抛异常)。 +# CAN 全型号支持;CAN-FD 仅 O20 / L30;Modbus 仅 O6/L7/L20_Lite(HandFactory 其余会抛异常)。 # EtherCAT 当前 SDK 未接通(工厂直接抛异常),不列入。 COMM_SUPPORT = { "L6": ["can"], "O6": ["can", "modbus"], "L7": ["can", "modbus"], - "L10": ["can", "modbus"], + "L20_LITE": ["can", "modbus"], "L20": ["can"], "L21": ["can"], "L25": ["can"], - "G20": ["can"], + "L20_10": ["can"], "O20": ["canfd"], + "L30": ["canfd"], } meta = {} # {"model","dof","version"} @@ -234,18 +243,35 @@ def _clamp_vec(vals): return [max(0, min(255, int(v))) for v in vals] +def _clamp_i16(vals): + return [max(-32768, min(32767, int(v))) for v in vals] + + def send_pose(vals): _write("P " + " ".join(str(v) for v in _clamp_vec(vals)) + "\n") +def send_pose_raw(vals): + # 原始值直控:按 int16 下发,不做 0-255 限幅 + _write("P " + " ".join(str(v) for v in _clamp_i16(vals)) + "\n") + + def send_speed(vals): _write("S " + " ".join(str(v) for v in _clamp_vec(vals)) + "\n") +def send_speed_raw(vals): + _write("S " + " ".join(str(v) for v in _clamp_i16(vals)) + "\n") + + def send_torque(vals): _write("T " + " ".join(str(v) for v in _clamp_vec(vals)) + "\n") +def send_torque_raw(vals): + _write("T " + " ".join(str(v) for v in _clamp_i16(vals)) + "\n") + + def _dof(): return int(meta.get("dof", 0)) or 10 @@ -298,14 +324,20 @@ def do_GET(self): connected = bridge is not None and bridge.poll() is None with state_lock: model = meta.get("model", "?") + cur_side = (current or {}).get("side", "") m = { "connected": connected, "model": model, + "side": cur_side, "dof": _dof(), "version": meta.get("version", ""), "rates": meta.get("rates", {}), "names": JOINT_NAMES.get(model, [f"关节{i}" for i in range(_dof())]), "names_en": JOINT_NAMES_EN.get(model, [f"Joint {i}" for i in range(_dof())]), + "raw": bool(meta.get("raw", False)), + "ranges": meta.get("ranges", []), + "speed_range": meta.get("speed_range", []), + "torque_range": meta.get("torque_range", []), } self._send(200, json.dumps(m), "application/json") elif self.path == "/options": @@ -342,14 +374,26 @@ def do_POST(self): _write(f"R {chan} {hz}\n") return self._send(200, json.dumps({"ok": True, "chan": chan, "hz": hz}), "application/json") if self.path == "/pose": - vals = _clamp_vec(_vec_from_payload(payload)) - send_pose(vals) + if meta.get("raw"): + vals = _clamp_i16(_vec_from_payload(payload)) + send_pose_raw(vals) + else: + vals = _clamp_vec(_vec_from_payload(payload)) + send_pose(vals) elif self.path == "/speed": - vals = _clamp_vec(_vec_from_payload(payload)) - send_speed(vals) + if meta.get("raw"): + vals = _clamp_i16(_vec_from_payload(payload)) + send_speed_raw(vals) + else: + vals = _clamp_vec(_vec_from_payload(payload)) + send_speed(vals) elif self.path == "/torque": - vals = _clamp_vec(_vec_from_payload(payload)) - send_torque(vals) + if meta.get("raw"): + vals = _clamp_i16(_vec_from_payload(payload)) + send_torque_raw(vals) + else: + vals = _clamp_vec(_vec_from_payload(payload)) + send_torque(vals) else: return self._send(404, "not found", "text/plain") self._send(200, json.dumps({"ok": True, "vals": vals}), "application/json") @@ -388,7 +432,7 @@ def main(): ap.add_argument("--side", choices=["left", "right"], default="left") ap.add_argument("--comm", choices=["can", "canfd", "modbus"], default="", help="通信方式;缺省按型号自动(O20→canfd,其余→can)") - ap.add_argument("--channel", default="", help="CAN 接口名(如 can0)/ Modbus 串口(如 /dev/ttyUSB0);O20 可用 'socketcan:can0'") + ap.add_argument("--channel", default="", help="设备通道;留空则按左右手自动检测自选(CAN:接口名 can0;CAN-FD/L30:socketcan 自检或 socketcan:can0;Modbus:串口 /dev/ttyUSB0)") ap.add_argument("--host", default="0.0.0.0") ap.add_argument("--port", type=int, default=8080) ap.add_argument("--bridge", default=DEFAULT_BRIDGE, help="web_bridge 可执行文件路径") diff --git a/webui/static/app.js b/webui/static/app.js index 7668a78..56e0419 100644 --- a/webui/static/app.js +++ b/webui/static/app.js @@ -15,11 +15,12 @@ const I18N = { about_sdk: 'SDK 版本', about_site: '官网', about_desc: 'LinkerHand 系列 C++ SDK 的 Web 控制界面。', f_model: '型号', f_comm: '通信方式', f_side: '左右手', f_chan: '设备', side_left: '左手', side_right: '右手', connect: '连接', connecting: '连接中…', + chan_ph_can: '留空自动检测 / can0', chan_ph_canfd: '留空自动检测 / socketcan:can0', chan_ph_modbus: '留空自动检测 / /dev/ttyUSB0', connected_ok: '连接成功', not_connected: '未连接——点击右上角 ⚙ 选择型号与通信方式后连接', disconnect: '断开连接', disconnecting: '断开中…', disconnected: '已断开', ver_info: '设备信息', joints_panel: '监控 & 控制', sp_tq_panel: '速度 & 扭矩', speed: '速度', torque: '扭矩', force_panel: '压感数据', rate: '频率', - rate_pos: '位置', rate_st: '速度/扭矩', rate_force: '触觉', rate_temp: '温度', rate_fault: '故障码', + rate_send: '控制下发', rate_pos: '位置', rate_st: '速度/扭矩', rate_force: '触觉', rate_temp: '温度', rate_fault: '故障码', no_force: '暂无触觉数据(该型号无传感器或未接入)。', ready: '就绪', h_joint: '关节', h_ctrl: '控制', h_target: '目标', h_pos: '实际', h_speed: '速度', h_torque: '扭矩', h_temp: '温度°C', h_fault: '故障码', @@ -41,11 +42,12 @@ const I18N = { about_sdk: 'SDK Version', about_site: 'Website', about_desc: 'Web control UI for the LinkerHand C++ SDK.', f_model: 'Model', f_comm: 'Comm', f_side: 'Hand', f_chan: 'Device', side_left: 'Left', side_right: 'Right', connect: 'Connect', connecting: 'Connecting…', + chan_ph_can: 'blank = auto-detect / can0', chan_ph_canfd: 'blank = auto-detect / socketcan:can0', chan_ph_modbus: 'blank = auto-detect / /dev/ttyUSB0', connected_ok: 'Connected', not_connected: 'Not connected — click ⚙ to pick model & comm, then connect', disconnect: 'Disconnect', disconnecting: 'Disconnecting…', disconnected: 'Disconnected', ver_info: 'Device Info', joints_panel: 'Monitor & Control', sp_tq_panel: 'Speed & Torque', speed: 'Speed', torque: 'Torque', force_panel: 'Pressure Data', rate: 'Frequency', - rate_pos: 'Position', rate_st: 'Speed/Torque', rate_force: 'Tactile', rate_temp: 'Temp', rate_fault: 'Fault', + rate_send: 'Command', rate_pos: 'Position', rate_st: 'Speed/Torque', rate_force: 'Tactile', rate_temp: 'Temp', rate_fault: 'Fault', no_force: 'No tactile data (this model has no sensor or is not connected).', ready: 'Ready', h_joint: 'Joint', h_ctrl: 'Control', h_target: 'Target', h_pos: 'Actual', h_speed: 'Speed', h_torque: 'Torque', h_temp: 'Temp°C', h_fault: 'Fault', @@ -119,7 +121,21 @@ const connMsg = document.getElementById('connMsg'); const disconnectBtn = document.getElementById('disconnectBtn'); let OPTS = null; const COMM_LABEL = {can: 'CAN', canfd: 'CAN-FD', modbus: 'Modbus', ethercat: 'EtherCAT'}; -const CHAN_PH = {can: 'can0', canfd: 'socketcan:can0', modbus: '/dev/ttyUSB0'}; +const CHAN_PH = {can: 'chan_ph_can', canfd: 'chan_ph_canfd', modbus: 'chan_ph_modbus'}; +// 按型号的初始默认姿势(0-255 滑块值,长度须 = 该型号 DOF)。 +// 未列出的型号默认全张开(255)。 +const DEFAULT_POSE = { + L30: [0, 0, 255, 0, 75, 0, 0, 0, 0, 0, 0, 0, 96, 255, 0, 0, 88], // 非原始模式回退用 + // L20_10 左手初始位置;右手暂用左手默认值先占位(DEFAULT_POSE 不区分左右手) + L20_10: [255, 255, 255, 255, 255, 255, 121, 127, 123, 121, 255, 255, 255, 255, 255, 255], +}; +// 原始值模式下按左右手的完整默认姿势(原始 int16,17 关节)。左右手默认位不同。 +const DEFAULT_POSE_RAW_FULL = { + L30: { + right: [0, 0, 1000, 0, -82, 0, 0, 0, 0, 0, 0, -200, -49, 200, 0, 0, -80], + left: [0, 0, 0, 0, 79, 0, 0, 0, 0, 0, 0, 148, -49, -200, 0, 0, -80], + }, +}; async function openSettings(){ setModal.hidden = false; @@ -153,7 +169,7 @@ function onModelChange(){ }); onCommChange(); } -function onCommChange(){ mChan.placeholder = CHAN_PH[mComm.value] || ''; } +function onCommChange(){ const k = CHAN_PH[mComm.value]; mChan.placeholder = k ? t(k) : ''; } async function doConnect(){ connMsg.className = 'conn-msg'; connMsg.textContent = t('connecting'); const body = {model: mModel.value, side: mSide.value, comm: mComm.value, channel: mChan.value.trim()}; @@ -204,7 +220,9 @@ function buildJoints(){ const tr = document.createElement('tr'); const nameTd = document.createElement('td'); nameTd.textContent = jointName(i); const ctlTd = document.createElement('td'); ctlTd.className = 'ctl'; - const s = document.createElement('input'); s.type = 'range'; s.min = 0; s.max = 255; s.value = vals[i]; + const s = document.createElement('input'); s.type = 'range'; + const rng = (META.raw && META.ranges && META.ranges[i]) ? META.ranges[i] : [0, 255]; + s.min = rng[0]; s.max = rng[1]; s.step = 1; s.value = vals[i]; ctlTd.appendChild(s); const valTd = document.createElement('td'); valTd.className = 'num'; valTd.textContent = vals[i]; s.addEventListener('input', () => { vals[i] = +s.value; valTd.textContent = s.value; sendThrottled(); }); @@ -224,10 +242,11 @@ function buildJoints(){ function refresh(){ for(let i = 0; i < META.dof; i++){ sliders[i].value = vals[i]; outs[i].textContent = vals[i]; } } let timer = null, pending = false; +let sendThrottleMs = 30; // 控制下发节流(ms),由"设置-频率-控制下发"调节,默认 ~33Hz function sendThrottled(){ if(timer){ pending = true; return; } sendPose(); - timer = setTimeout(() => { timer = null; if(pending){ pending = false; sendThrottled(); } }, 60); + timer = setTimeout(() => { timer = null; if(pending){ pending = false; sendThrottled(); } }, sendThrottleMs); } async function sendPose(){ try{ const j = await post('/pose', {vals}); status(j.ok ? (t('sent') + ' [' + j.vals.join(', ') + ']') : (t('err') + ': ' + j.error)); } @@ -247,6 +266,10 @@ rateEls.forEach(el => { const chan = el.dataset.chan, out = el.parentElement.querySelector('.rval b'); el.addEventListener('input', () => { out.textContent = el.value; + if (chan === 'send') { // "控制下发":仅调前端下发节流,不发 /rate + sendThrottleMs = Math.round(1000 / Math.max(1, +el.value)); + return; + } clearTimeout(rateTimers[chan]); // 拖动去抖,避免刷爆 /rate rateTimers[chan] = setTimeout(() => post('/rate', {chan, hz:+el.value}), 120); }); @@ -438,7 +461,10 @@ function renderMonitor(pos, spd, trq, temp, fault){ const p = get(pos, i); // 实际值与目标偏差超过容差时标橙,提示尚未到位(容差避开反馈抖动导致的常亮) c.pos.textContent = p == null ? '--' : p; - c.pos.className = (p != null && Math.abs(p - vals[i]) > 5) ? 'num warn' : 'num'; + // 原始模式量程大,容差放宽(按各关节行程比例),避免常亮橙色 + let thr = 5; + if (META.raw && META.ranges && META.ranges[i]) thr = Math.max(20, (META.ranges[i][1] - META.ranges[i][0]) * 0.03); + c.pos.className = (p != null && Math.abs(p - vals[i]) > thr) ? 'num warn' : 'num'; setNum(c.spd, get(spd, i)); setNum(c.trq, get(trq, i)); const tp = get(temp, i), f = get(fault, i); c.temp.textContent = tp == null ? '--' : tp; @@ -531,8 +557,42 @@ async function init(){ return; } verReady = renderVersion(document.getElementById('infoVer'), META.version); - vals = new Array(META.dof).fill(255); + const dp = DEFAULT_POSE[META.model]; + // 侧别以服务端实际连接为准(META.side),避免刷新时下拉默认(left)导致误用左手姿势 + const side = META.side || ((mSide && mSide.value) ? mSide.value : 'right'); + if (mSide && META.side) mSide.value = META.side; // 同步下拉显示 + const rawFull = DEFAULT_POSE_RAW_FULL[META.model] && DEFAULT_POSE_RAW_FULL[META.model][side]; + if (META.raw && META.ranges && META.ranges.length === META.dof) { + if (Array.isArray(rawFull) && rawFull.length === META.dof) { + // 按左右手的完整原始值默认姿势(各关节量程钳位) + vals = rawFull.map((v, i) => Math.max(META.ranges[i][0], Math.min(META.ranges[i][1], v))); + } else { + // 无预设原始默认:默认姿势(0-255)按各关节 ranges 换算,否则取行程中点 + vals = META.ranges.map((r, i) => { + if (dp && dp.length === META.dof) return Math.round(r[0] + (dp[i] / 255) * (r[1] - r[0])); + return Math.round((r[0] + r[1]) / 2); + }); + } + } else { + vals = (dp && dp.length === META.dof) ? dp.slice() : new Array(META.dof).fill(255); + } + // 速度/扭矩滑块量程:原始模式按实际量程(int16)铺满并给合理默认;否则沿用 0-255 + const cfgScalar = (el, valEl, range, rawDef, defVal) => { + if (META.raw && Array.isArray(range) && range.length === 2) { + el.min = range[0]; el.max = range[1]; el.step = 1; + el.value = Math.max(range[0], Math.min(range[1], rawDef)); + } else { + el.min = 0; el.max = 255; el.value = defVal; + } + valEl.textContent = el.value; + }; + const spdMax = (Array.isArray(META.speed_range) && META.speed_range.length === 2) ? META.speed_range[1] : 255; + cfgScalar(speedEl, speedVal, META.speed_range, spdMax, 255); // 速度默认取最大 + cfgScalar(torqueEl, torqueVal, META.torque_range, 1024, 180); // 扭矩默认 1024(-2047~2047) + buildJoints(); drawSaved(); - sendPose(); // 推初始张开姿势 + sendPose(); // 推初始姿势 + // 原始模式下把速度/扭矩初值也下发,使设备与滑块一致 + if (META.raw) { post('/speed', {val:+speedEl.value}); post('/torque', {val:+torqueEl.value}); } } init(); diff --git a/webui/static/index.html b/webui/static/index.html index 4fa591d..6756ee4 100644 --- a/webui/static/index.html +++ b/webui/static/index.html @@ -100,6 +100,11 @@

设置