Compare commits

...

34 Commits

Author SHA1 Message Date
zhangshun
9192ccbe83 拆分SPC直方图和正态曲线页面 2026-06-29 09:37:23 +08:00
zhangshun
78c80cf5f7 新增SPC基本趋势图页面 2026-06-29 09:29:25 +08:00
zhangshun
5ca2657536 优化SPC直方图坐标范围 2026-06-29 09:23:34 +08:00
zhangshun
37517d5279 新增SPC Vue控制图项目 2026-06-29 09:18:24 +08:00
zhangshun
856a7250f2 屏蔽算法运行时冗余日志 2026-06-16 16:00:36 +08:00
zhangshun
5aa855d7aa 补充机器人算法接口文档 2026-06-16 15:51:52 +08:00
zhangshun
0c7dc9e05b 增加机器人奇异点检测 2026-06-16 12:56:26 +08:00
zhangshun
a100e44ac0 增加机器人运动学能力说明文档 2026-06-16 12:45:27 +08:00
zhangshun
e26b04e915 启用机器人关节上下限检查 2026-06-16 12:41:36 +08:00
zhangshun
c2d79a74b9 自动推导URDF运动学链 2026-06-16 12:34:17 +08:00
zhangshun
a3efcd66a5 固定threejs测试页场景高度 2026-06-16 12:23:13 +08:00
zhangshun
aaf2c0cc41 增加URDF文件注册测试入口 2026-06-16 12:20:15 +08:00
zhangshun
eeef7594fd 调整threejs测试页相机坐标系 2026-06-16 12:14:06 +08:00
zhangshun
71bf027fde 优化四连杆三维动画播放控制 2026-06-16 12:08:17 +08:00
zhangshun
c24c4e5b64 调整四连杆三维视图为Z轴向上 2026-06-16 11:58:10 +08:00
zhangshun
5dfe4aaec9 优化四连杆测试页三维视图交互 2026-06-16 11:53:26 +08:00
zhangshun
59a7954015 新增threejs机器人运动学测试页面 2026-06-16 11:44:31 +08:00
zhangshun
f142b42dbe 新增四类连杆统一仿真接口 2026-06-16 11:40:58 +08:00
zhangshun
a66952e636 新增曲柄滑块批量仿真接口 2026-06-16 11:17:25 +08:00
zhangshun
c9e76d0850 修正三维视图导轨长度 2026-06-16 11:03:03 +08:00
zhangshun
58d4c6660e 增强曲柄滑块三维验证视图 2026-06-16 10:59:04 +08:00
zhangshun
5e6bdc3ecf 新增曲柄滑块验证页面 2026-06-16 10:48:36 +08:00
zhangshun
d15b09a0c9 优化业务错误顶层响应 2026-06-16 10:36:34 +08:00
zhangshun
4b45cd6d99 补充SPC测试页图形说明 2026-06-16 10:29:51 +08:00
zhangshun
41381b9577 增加SPC扩展控制图验证 2026-06-16 10:24:33 +08:00
zhangshun
e9c8bde14a 为SPC验证页增加ECharts图表 2026-06-16 10:11:36 +08:00
zhangshun
6f45353b21 新增SPC前端验证页面 2026-06-16 09:53:10 +08:00
zhangshun
bcd94b3307 修改测试页 2026-06-16 09:39:36 +08:00
zhangshun
b5382163e0 补充接口调用履历文档 2026-06-01 18:25:11 +08:00
zhangshun
6aa98241ef 按功能拆分接口并清理核心乱码注释 2026-06-01 17:59:51 +08:00
zhangshun
12310de782 补充调试页接口测试样例 2026-06-01 17:38:29 +08:00
zhangshun
3df459cb33 优化调试页响应展示格式 2026-06-01 17:32:38 +08:00
zhangshun
4c1ec24be8 补充本地调试页面入口 2026-06-01 17:25:10 +08:00
zhangshun
2d9bd1d4d7 修复本地服务启动脚本 2026-06-01 17:10:36 +08:00
63 changed files with 13697 additions and 1055 deletions

View File

@@ -0,0 +1,695 @@
# 接口调用履历
本文档用于快速梳理当前项目里“前端请求 -> WASM 导出函数 -> C++ 分发层 -> 业务实现文件”的执行链路,便于对照源码阅读。
## 1. 建议先看的总入口
建议按下面顺序看文件:
1. `src/main.cpp`
说明这个工程最终是一个 WASM 可执行模块,`main` 主要用于本地构建提示。
2. `src/smart_json_wrapper.cpp`
这里是所有 WASM 导出函数的真正入口。
3. `include/KinematicsWebAPI.h`
业务接口类声明,能看见分发结构。
4. `src/api/KinematicsWebAPI.Core.cpp`
业务 JSON 请求的总分发入口。
5. `src/api/KinematicsWebAPI.*Commands.cpp`
按功能拆开的命令处理层。
6. 对应底层业务文件
机器人看 `src/Robot.cpp``src/RobotManager.cpp`
SPC 看 `src/spc_core.cpp`
四连杆看 `src/FourBarMechanism/`
四足看 `src/QuadrupedRobotSimulation/`
## 2. 顶层 WASM 导出接口
### 2.1 业务接口导出
前端主调的是这两个导出函数:
- `init_func`
- `func`
调用链如下:
```text
JS/HTML
-> public/wasm/smart_api_wrapper.js
-> WASM 导出函数 init_func / func
-> src/smart_json_wrapper.cpp
-> KinematicsWebAPI
-> src/api/KinematicsWebAPI.Core.cpp
-> 各功能 Commands 文件
-> 底层业务实现
```
### 2.2 智能数学/测试接口导出
这一套和业务接口是并行的另一条线:
- `smart_process_json`
- `smart_get_function_list`
- `smart_get_function_info`
- `smart_test_match`
- `smart_get_version`
调用链如下:
```text
JS/HTML
-> public/wasm/smart_api_wrapper.js
-> smart_* 导出函数
-> src/smart_json_wrapper.cpp
-> SmartJsonProcessor / FunctionRegistry
-> math_utils / 注册的测试函数
```
这一套主要是“数学函数演示 + 智能参数匹配”,不走 `KinematicsWebAPI`
## 3. 业务总分发链路
### 3.1 初始化
`src/smart_json_wrapper.cpp` 中:
- `init_func()`
- 首次调用时创建全局单例 `global_server = new KinematicsWebAPI()`
- `KinematicsWebAPI` 构造函数里会继续调用 `KinematicsWebAPI::init_func()`
`src/api/KinematicsWebAPI.Core.cpp` 中:
- `KinematicsWebAPI::init_func()`
-`include/URDFStrings.h` 读取默认 URDF
- 调用 `RobotManager::initRobot(...)`
- 默认会初始化两个机器人实例
### 3.2 请求处理
`src/smart_json_wrapper.cpp`
- `func(const char* json_request)`
- 把前端 JSON 字符串转成 `std::string`
-`global_server->func(request_str)`
`src/api/KinematicsWebAPI.Core.cpp`
- `KinematicsWebAPI::func(std::string sanitized_body)`
- 解析 JSON
- 取出 `msg / req_code / req_from / req_cmd / req_param`
-`dispatchCommand(req_cmd, req_param)`
- 最后统一封装为 `utils::create_api_response(...)`
- `KinematicsWebAPI::dispatchCommand(...)`
- 机器人类命令 -> `handleRobotCommand(...)`
- SPC -> `handleSpcCommand(...)`
- 四连杆 -> `handleFourBarCommand(...)`
- 四足 -> `handleQuadrupedCommand(...)`
## 4. 机器人相关接口履历
机器人相关命令都在:
- `src/api/KinematicsWebAPI.RobotCommands.cpp`
底层核心文件:
- `src/RobotManager.cpp`
- `src/Robot.cpp`
### 4.1 `Cmd_InitRobot`
调用履历:
```text
func
-> KinematicsWebAPI::dispatchCommand
-> KinematicsWebAPI::handleRobotCommand
-> Cmd_InitRobot 分支
-> utils::base64_to_urdf
-> utils::validate_urdf_base64
-> RobotManager::initRobot
-> Robot::initRobot
```
底层细节:
- `RobotManager::initRobot`
- 创建 `std::shared_ptr<Robot>`
-`robot->initRobot(urdf_robot)`
- 生成或使用 `robot_uuid`
- 计算 URDF 哈希
-`_validateKinematics(robot)` 做基本正解验证
- 存入 `_kinematics_table`
- `Robot::initRobot`
- `kdl_parser::treeFromString(...)`
- `tree.getChain("base", "tool0", kinematicChain)`
- `parseJointChildLinkUuidsFromUrdf(...)`
- 初始化 FK / IK 求解器
### 4.2 `Cmd_GetRobot`
调用履历:
```text
func
-> dispatchCommand
-> handleRobotCommand
-> Cmd_GetRobot
-> RobotManager::getRobot
-> Robot::isInitialized / Robot::getNumberOfJoints
```
### 4.3 `Cmd_RemoveRobot`
调用履历:
```text
func
-> dispatchCommand
-> handleRobotCommand
-> Cmd_RemoveRobot
-> RobotManager::removeRobot
```
### 4.4 `Cmd_ListRobots`
调用履历:
```text
func
-> dispatchCommand
-> handleRobotCommand
-> Cmd_ListRobots
-> RobotManager::listRobots
-> detail=true 时内部还会调 RobotManager::_validateKinematics
-> Robot::calculateFK_TCP
```
### 4.5 `Cmd_Kinematics_forward_pose_str`
用途:
- 输入一组 6 轴关节角
- 输出 TCP 位姿
调用履历:
```text
func
-> dispatchCommand
-> handleRobotCommand
-> Cmd_Kinematics_forward_pose_str
-> RobotManager::getRobot
-> Robot::parseJointString
-> Robot::calculateFK_TCP
```
`Robot::calculateFK_TCP` 内部主要做:
- 关节数组转 `KDL::JntArray`
- `fkSolver->JntToCart(...)`
- 取出位置和四元数
### 4.6 `Cmd_Kinematics_forward_all_joints`
用途:
- 输入一帧或多帧关节角列表
- 输出每个关节节点的位姿,组织成前端使用的 `OPERATION.frames[].objStates`
调用履历:
```text
func
-> dispatchCommand
-> handleRobotCommand
-> Cmd_Kinematics_forward_all_joints
-> RobotManager::getRobot
-> Robot::handleKinematicsForwardAllJoints
-> Robot::parseJointListString
-> Robot::kinematicsForwardAllJointsList
-> Robot::forwardKinematics
-> Robot::getJointUuidByIndex
```
补充说明:
- `Robot::handleKinematicsForwardAllJoints(...)`
- 遍历每一帧关节角
- 每帧都调一次 `kinematicsForwardAllJointsList(...)`
- `Robot::kinematicsForwardAllJointsList(...)`
-`forwardKinematics(...)`
- 把 6 个关节的位姿打包成 `objStates`
### 4.7 `Cmd_Kinematics_inverse_pose_str_NoDifference`
用途:
- 输入一个或多个姿态点
- 不做插补,直接逐点逆解
调用履历:
```text
func
-> dispatchCommand
-> handleRobotCommand
-> Cmd_Kinematics_inverse_pose_str_NoDifference
-> RobotManager::getRobot
-> Robot::inversePoseStrNoDifference
-> Robot::parsePoseString
-> Robot::parseJointString
-> Robot::inverse(vector)
-> Robot::calculateIK_LMA
```
### 4.8 `Cmd_Kinematics_inverse_pose_str`
用途:
- 输入多个姿态点
- 相邻姿态间先自动插补轨迹,再逐点逆解
调用履历:
```text
func
-> dispatchCommand
-> handleRobotCommand
-> Cmd_Kinematics_inverse_pose_str
-> RobotManager::getRobot
-> Robot::inversePoseStr
-> Robot::parsePoseString
-> Robot::parseJointString
-> Robot::calculateAutoSteps
-> Robot::trajectoryPlanning
-> Robot::inverse(vector)
-> Robot::calculateIK_LMA
```
关键点:
- `calculateAutoSteps(...)` 根据位置差和姿态差自动算插补点数
- `trajectoryPlanning(...)` 调 KDL 轨迹插补
- 每个插补点再进入 `inverse(...)`
### 4.9 `Cmd_Kinematics_inverse_pose_str_2PSteps`
用途:
- 只取两个姿态点
- 按指定 `steps` 做轨迹插补
调用履历:
```text
func
-> dispatchCommand
-> handleRobotCommand
-> Cmd_Kinematics_inverse_pose_str_2PSteps
-> RobotManager::getRobot
-> Robot::inversePoseStr2PSteps
-> Robot::parsePoseString
-> Robot::parseJointString
-> Robot::trajectoryPlanning(..., steps)
-> Robot::inverse(vector)
-> Robot::calculateIK_LMA
```
### 4.10 `Cmd_SelectCraftTree` / `Cmd_AddOperationTree`
这两个目前是轻量占位接口。
调用履历:
```text
func
-> dispatchCommand
-> handleRobotCommand
-> 直接在 KinematicsWebAPI.RobotCommands.cpp 内拼装返回 JSON
```
当前没有继续下钻到独立业务类。
## 5. SPC 接口履历
命令文件:
- `src/api/KinematicsWebAPI.SpcCommands.cpp`
底层文件:
- `include/spc_core.h`
- `src/spc_core.cpp`
### 5.1 `Cmd_Spc`
调用履历:
```text
func
-> dispatchCommand
-> handleSpcCommand
-> SpcCalculator::Spc
-> SpcTestData::FromJson
-> SpcCalculator::CalculateXR
-> SpcCalculator::CalculateXS
-> SpcCalculator::CalculateCpk
-> SpcCalculator::RoundSpcData
-> SpcResultJson::ToJson
```
底层职责分布:
- `SpcTestData::FromJson`
- 从请求中读取 `n / k / usl / lsl / x`
- `CalculateXR`
- 算 X-R 控制图
- `CalculateXS`
- 算 X-S 控制图
- `CalculateCpk`
- 算过程能力指数和直方图数据
- `RoundSpcData`
- 做统一小数位保留
## 6. 四连杆接口履历
命令文件:
- `src/api/KinematicsWebAPI.FourBarCommands.cpp`
底层文件:
- `include/FourBarMechanism/CrankSliderMechanism.h`
- `src/FourBarMechanism/CrankSliderMechanism.cpp`
### 6.1 `Cmd_FourBar_CrankSlider`
调用履历:
```text
func
-> dispatchCommand
-> handleFourBarCommand
-> createCrankSliderMechanism
-> CrankSliderMechanism::setL_AB / setL_BS / setS_OFS
-> CrankSliderMechanism::validateParameters
-> CrankSliderMechanism::calculate
-> CrankSliderMechanism::getTrajectoryPoints
-> CrankSliderMechanism::getSliderTrajectory
```
说明:
- 命令层负责把参数取出来、调机制对象、再把结果组装成 JSON
- 真正的机构计算在 `src/FourBarMechanism/CrankSliderMechanism.cpp`
### 6.2 `Cmd_FourBar_CrankSlider_Simulate`
用途:
- 输入曲柄滑块初始参数、起始角度、结束角度、总时长和步长时间
- 一次性生成整段仿真帧数据,适合前端播放、导出轨迹和离线验证
调用履历:
```text
func
-> dispatchCommand
-> handleFourBarCommand
-> createCrankSliderMechanism
-> CrankSliderMechanism::setL_AB / setL_BS / setS_OFS
-> CrankSliderMechanism::validateParameters
-> for each frame:
-> CrankSliderMechanism::calculate(currentAngleDeg)
-> CrankSliderMechanism::getTrajectoryPoints
-> CrankSliderMechanism::getSliderTrajectory
```
主要入参:
- `L_AB`:曲柄长度
- `L_BS`:连杆长度
- `S_OFS`:滑块偏移
- `startAngleDeg`:起始曲柄角度
- `endAngleDeg`:结束曲柄角度
- `duration`:仿真总时长,单位秒
- `stepTime`:仿真步长时间,单位秒
主要出参:
- `frames[]`:逐帧仿真数据,每帧包含 `frame``time``angleDeg``points``poses``angles`
- `trajectory`B 点整段轨迹
- `slider_trajectory`S 点整段滑块轨迹
### 6.3 `Cmd_FourBar_Simulate`
用途:
- 统一仿真 PDPS 连杆定义界面中的 `PRRR / RPRR / RRRP / RRRR` 四类结构
- 输入机构类型、起始输入、结束输入、总时长和步长时间
- 一次性返回逐帧点位、姿态、角度和轨迹,适合前端 Three.js 播放和 ECharts 曲线验证
调用履历:
```text
func
-> dispatchCommand
-> handleFourBarCommand
-> simulateUnifiedFourBar
-> 按 mechanismType 创建或调用对应机构类
-> validateParameters
-> for each frame:
-> calculate(currentInput)
-> 组装 frames / trajectory / parameters
```
类型映射:
- `RRRP`:调用 `CrankSliderMechanism`,角度驱动曲柄滑块正解
- `PRRR`:调用 `SliderCrankMechanism`,滑块位移驱动曲柄滑块逆解
- `RPRR`:调用 `CrankRockingBlockMechanism_Forward`,当前按曲柄摇块正解承接
- `RRRR`:调用 `FourBarMechanism`,角度驱动四转副四杆正解
主要公共入参:
- `mechanismType``PRRR``RPRR``RRRP``RRRR`
- `startValue` / `endValue`:统一输入范围,角度驱动类型表示角度,`PRRR` 表示滑块 X
- `duration`:仿真总时长,单位秒
- `stepTime`:仿真步长时间,单位秒
各类型参数:
- `RRRP``L_AB``L_BS``S_OFS`
- `PRRR``L_AB``L_BS``S_OFS`,也可使用 `startSliderX` / `endSliderX`
- `RPRR``OA``AB``OC`
- `RRRR``L1``L2``L3``L4`
主要出参:
- `mechanismType`:实际仿真的机构类型
- `inputName`:当前输入字段名,`angleDeg``sliderX`
- `frames[]`:逐帧仿真数据,每帧包含 `frame``time``inputValue``points``poses``angles`
- `trajectory`:主运动点轨迹
- `slider_trajectory`:滑块轨迹,适用于 `RRRP / PRRR`
- `alternative_trajectory`:备选逆解轨迹,适用于 `PRRR`
- `trajectory_c`C 点轨迹,适用于 `RRRR`
- `crank_circle`:曲柄端点轨迹,适用于 `RPRR / RRRR`
- `rocker_trajectory`:摇块轨迹,适用于 `RPRR`
注意:
- `RPRR` 当前先复用现有曲柄摇块正解类PDPS 中输入/输出链节的方向、坐标系和安装约定后续如有更精确资料,可继续对齐参数语义。
## 7. 四足机器人接口履历
命令文件:
- `src/api/KinematicsWebAPI.QuadrupedCommands.cpp`
中间帮助层:
- `include/QuadrupedRobotSimulation/KinematicsHelper.h`
- `src/QuadrupedRobotSimulation/KinematicsHelper.cpp`
底层实现文件:
- `src/QuadrupedRobotSimulation/KinematicsSimulation.cpp`
- `src/QuadrupedRobotSimulation/KinematicsReverse.cpp`
- `src/QuadrupedRobotSimulation/CompleteJsonExporter.cpp`
- `src/QuadrupedRobotSimulation/RobotConfig.cpp`
- `src/QuadrupedRobotSimulation/SharedGeometry.cpp`
### 7.1 `Cmd_QuadrupedRobot_CalculateAllPointsFromMotorAngles`
用途:
- 输入电机角
- 反推出各关键点坐标
调用履历:
```text
func
-> dispatchCommand
-> handleQuadrupedCommand
-> KinematicsHelper::QuadrupedRobot_CalculateAllPointsFromMotorAngles
-> KinematicsHelper::RobotGaitDataManagerFromJson
-> ReverseKinematicsCalculator
-> ReverseKinematicsCalculator::CalculateAllPointsFromMotorAnglesJsonStr
-> json::parse
```
说明:
- 数据管理入口在 `RobotGaitDataManagerFromJson(...)`
- 反解主逻辑在 `src/QuadrupedRobotSimulation/KinematicsReverse.cpp`
### 7.2 `Cmd_QuadrupedRobot_PerformForwardKinematics`
用途:
- 输入步态/系统参数
- 正向生成四足机器人整段步态轨迹
调用履历:
```text
func
-> dispatchCommand
-> handleQuadrupedCommand
-> KinematicsHelper::QuadrupedRobot_PerformForwardKinematics
-> KinematicsHelper::RobotGaitDataManagerFromJson
-> QuadrupedRobotConfiguration
-> QuadrupedRobotSimulation
-> QuadrupedRobotSimulation::CalculateAllTrajectoriesJsonString
-> CompleteJsonExporter::ExportCompleteDataJsonString
-> json::parse
```
说明:
- 配置拼装在 `RobotConfig.cpp`
- 轨迹生成主体在 `KinematicsSimulation.cpp`
- 最终 JSON 打包在 `CompleteJsonExporter.cpp`
## 8. 智能数学接口履历
这部分不是机器人业务,但也常会看到。
核心文件:
- `src/smart_json_wrapper.cpp`
- `include/smart_json_wrapper.h`
- `src/math_utils.cpp`
### 8.1 `smart_process_json`
调用履历:
```text
smart_process_json
-> SmartJsonProcessor::processRequest
-> 解析 req_cmd
-> FunctionRegistry::getFunction
-> SmartJsonProcessor::parseJsonParams
-> FunctionRegistry::smartMatchParams
-> FunctionInfo::validateParams
-> function handler lambda
-> math_utils 中的 add/subtract/multiply/divide/fibonacci 等
```
### 8.2 `smart_get_function_list`
调用履历:
```text
smart_get_function_list
-> SmartJsonProcessor::getAvailableFunctions
-> FunctionRegistry::getAllFunctions
```
### 8.3 `smart_get_function_info`
调用履历:
```text
smart_get_function_info
-> SmartJsonProcessor::getFunctionInfo
-> FunctionRegistry::getFunction
```
### 8.4 `smart_test_match`
调用履历:
```text
smart_test_match
-> 解析 req_cmd
-> FunctionRegistry::getFunction
-> SmartJsonProcessor::parseJsonParams
-> FunctionRegistry::smartMatchParams
-> 返回匹配结果,不真正执行业务函数
```
## 9. 文件职责一览
### 9.1 入口层
- `src/main.cpp`
- 构建入口,基本不承接业务
- `src/smart_json_wrapper.cpp`
- 所有 WASM 导出函数
- 业务接口入口和智能数学接口入口都在这里
### 9.2 业务分发层
- `src/api/KinematicsWebAPI.Core.cpp`
- 总分发
- `src/api/KinematicsWebAPI.RobotCommands.cpp`
- 机器人接口分支
- `src/api/KinematicsWebAPI.SpcCommands.cpp`
- SPC 接口分支
- `src/api/KinematicsWebAPI.FourBarCommands.cpp`
- 四连杆接口分支
- `src/api/KinematicsWebAPI.QuadrupedCommands.cpp`
- 四足接口分支
### 9.3 业务实现层
- `src/RobotManager.cpp`
- 机器人实例生命周期管理
- `src/Robot.cpp`
- 机器人 KDL 正解、逆解、轨迹插补、前端返回格式组装
- `src/spc_core.cpp`
- SPC 计算主体
- `src/FourBarMechanism/*.cpp`
- 连杆/曲柄滑块算法
- `src/QuadrupedRobotSimulation/*.cpp`
- 四足机器人轨迹与反解
## 10. 如果你要继续顺着看,推荐顺序
如果你想先搞清“机器人接口”:
1. `src/smart_json_wrapper.cpp` 里的 `func`
2. `src/api/KinematicsWebAPI.Core.cpp`
3. `src/api/KinematicsWebAPI.RobotCommands.cpp`
4. `src/RobotManager.cpp`
5. `src/Robot.cpp`
如果你想先搞清“四足机器人接口”:
1. `src/api/KinematicsWebAPI.QuadrupedCommands.cpp`
2. `src/QuadrupedRobotSimulation/KinematicsHelper.cpp`
3. `src/QuadrupedRobotSimulation/KinematicsSimulation.cpp`
4. `src/QuadrupedRobotSimulation/KinematicsReverse.cpp`
5. `src/QuadrupedRobotSimulation/CompleteJsonExporter.cpp`
如果你想先搞清“SPC 接口”:
1. `src/api/KinematicsWebAPI.SpcCommands.cpp`
2. `src/spc_core.cpp`
3. `include/spc_core.h`

View File

@@ -0,0 +1,496 @@
# URDF/Orocos KDL 机器人算法接口文档
本文档说明当前机器人运动学模块对外暴露的业务接口,包括功能、参数、返回值和单位约定。接口定义以 `src/api/KinematicsWebAPI.RobotCommands.cpp` 当前实现为准。
## 1. 通用调用格式
所有业务命令都通过统一 JSON 请求进入 `KinematicsWebAPI::func`
```json
{
"msg": "request message",
"req_code": "REQ_001",
"req_from": "client",
"req_cmd": "Cmd_Name",
"req_param": {}
}
```
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `msg` | string | 否 | 调用说明。失败时如果业务层没有更具体错误,可能回传为响应 `msg`。 |
| `req_code` | string | 否 | 调用方请求编号,响应中原样返回。 |
| `req_from` | string | 否 | 调用来源,响应中原样返回。 |
| `req_cmd` | string | 是 | 命令名称。 |
| `req_param` | object | 否 | 命令参数。 |
统一响应外层格式如下:
```json
{
"success": true,
"code": 0,
"msg": "request message",
"req_code": "REQ_001",
"req_from": "client",
"req_cmd": "Cmd_Name",
"timestamp": 1792137600,
"res_data": {}
}
```
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `success` | boolean | 外层调用是否成功。业务层返回 `success:false``error` 时为 `false`。 |
| `code` | number | `0` 表示成功,`1000` 表示业务失败,`400` 表示 JSON 格式错误,`500` 表示处理异常。 |
| `msg` | string | 响应消息。失败时优先取业务层 `message``error`。 |
| `res_data` | object | 业务命令返回体。本文后续“返回值”均指 `res_data` 内部结构。 |
## 2. 单位和格式约定
| 数据 | 格式 | 单位 |
| --- | --- | --- |
| 关节角 | `j1,j2,j3,j4,j5,j6` | 弧度 rad |
| 关节角序列 | `j1,j2,j3,j4,j5,j6;j1,j2,j3,j4,j5,j6` | 弧度 rad |
| TCP 位姿 | `x,y,z,qx,qy,qz,qw` | 位置为米 m姿态为四元数 |
| TCP 位姿序列 | `x,y,z,qx,qy,qz,qw;x,y,z,qx,qy,qz,qw` | 位置为米 m姿态为四元数 |
| URDF 关节上下限 | URDF `<limit lower upper>` | 旋转关节为弧度 rad |
| `objStates` 位姿 | `tx,ty,tz,qx,qy,qz,qw` | 位置为米 m姿态为四元数 |
注意WASM/KDL 侧返回的 IK 关节结果是弧度。如果前端工艺数据使用角度,需要在前端显式做 `rad -> deg` 转换。
## 3. 机器人管理接口
### 3.1 `Cmd_InitRobot`
功能:注册或更新机器人 URDF并初始化 KDL Tree、6 轴运动学链、FK/IK 求解器、关节上下限和关节 child link 映射。
参数:
| 字段 | 类型 | 必填 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| `robot_uuid` | string | 否 | `""` | 机器人实例 ID。为空时内部使用 URDF 内容哈希作为 ID。 |
| `urdf_base64` | string | 是 | `""` | URDF 文件内容的 base64 字符串。 |
| `force_update` | boolean | 否 | `true` | 是否强制更新。为 `false` 且内容未变化时跳过更新。 |
返回值:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `success` | boolean | 初始化是否成功。 |
| `message` | string | 初始化结果说明。 |
| `timestamp` | string | 当前时间字符串。 |
示例:
```json
{
"req_cmd": "Cmd_InitRobot",
"req_param": {
"robot_uuid": "abb_irb120_3_58",
"urdf_base64": "PD94bWwgdmVyc2lvbj0iMS4wIj8+...",
"force_update": true
}
}
```
### 3.2 `Cmd_GetRobot`
功能:查询指定机器人实例是否存在、是否已初始化以及关节数量。
参数:
| 字段 | 类型 | 必填 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| `uuid` | string | 是 | `""` | 机器人实例 ID。注意该接口当前字段名为 `uuid`,不是 `robot_uuid`。 |
返回值:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `success` | boolean | 查询是否成功。 |
| `uuid` | string | 机器人实例 ID。 |
| `initialized` | boolean | 机器人是否已初始化。 |
| `joints_count` | number | 当前运动学链关节数量。 |
| `timestamp` | string | 当前时间字符串。 |
失败时返回:
```json
{
"error": "Robot not found"
}
```
### 3.3 `Cmd_RemoveRobot`
功能:删除指定机器人实例和对应 URDF 哈希记录。
参数:
| 字段 | 类型 | 必填 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| `uuid` | string | 是 | `""` | 机器人实例 ID。注意该接口当前字段名为 `uuid`,不是 `robot_uuid`。 |
返回值:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `success` | boolean | 删除是否成功。 |
| `message` | string | 删除结果说明。 |
| `timestamp` | string | 当前时间字符串。 |
### 3.4 `Cmd_ListRobots`
功能:列出当前已注册的机器人实例。
参数:
| 字段 | 类型 | 必填 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| `detail` | boolean | 否 | `false` | 是否返回关节数量、校验状态和 URDF 哈希摘要。 |
返回值:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `success` | boolean | 是否查询成功。 |
| `robots` | object | 机器人字典。键为机器人 ID值为实例说明或详细信息字符串。 |
| `count` | number | 当前机器人数量。 |
| `timestamp` | string | 当前时间字符串。 |
## 4. 正运动学接口
### 4.1 `Cmd_Kinematics_forward_pose_str`
功能:输入 6 个关节角,计算 TCP 位姿。
参数:
| 字段 | 类型 | 必填 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| `robot_uuid` | string | 否 | `"default"` | 机器人实例 ID。 |
| `q_init_str` | string | 否 | `"0,0,0,0,0,0"` | 6 个关节角,单位为弧度。该字段名为历史命名,实际表示 FK 输入关节。 |
返回值:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `position` | number[] | TCP 位置 `[x,y,z]`,单位为米。 |
| `orientation` | number[] | TCP 四元数 `[qx,qy,qz,qw]`。 |
| `joints` | number[] | 输入关节角,单位为弧度。 |
| `success` | boolean | 正解是否成功。 |
失败时常见错误:
| 错误 | 说明 |
| --- | --- |
| `Robot not found or not initialized` | 机器人未注册或初始化失败。 |
| `Invalid joints format` | 关节字符串不是 6 个数值。 |
| `Forward kinematics calculation failed` | FK 失败,常见原因包括关节超限。 |
示例:
```json
{
"req_cmd": "Cmd_Kinematics_forward_pose_str",
"req_param": {
"robot_uuid": "abb_irb120_3_58",
"q_init_str": "0.15,-0.25,0.35,0.1,-0.2,0.3"
}
}
```
### 4.2 `Cmd_Kinematics_forward_all_joints`
功能:输入一帧或多帧 6 轴关节角,计算每一帧中各关节节点位姿,并按前端 `OPERATION.frames.objStates` 格式输出。
参数:
| 字段 | 类型 | 必填 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| `robot_uuid` | string | 否 | `"default"` | 机器人实例 ID。 |
| `joints_str` | string | 否 | `"0,0,0,0,0,0"` | 关节角序列,格式为 `j1,j2,j3,j4,j5,j6;...`,单位为弧度。 |
返回值:
当前返回存在一层历史嵌套,结构如下:
```json
{
"success": true,
"OPERATION": {
"success": true,
"count": 2,
"OPERATION": {
"frames": [
{
"time": "0.020000",
"objStates": [
{
"i": "child_link_uuid",
"tx": 0,
"ty": 0,
"tz": 0,
"qx": 0,
"qy": 0,
"qz": 0,
"qw": 1
}
]
}
]
}
}
}
```
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `OPERATION.success` | boolean | 批量 FK 是否成功。 |
| `OPERATION.count` | number | 输入关节帧数量。 |
| `OPERATION.OPERATION.frames` | array | 前端播放帧。 |
| `frames[].time` | string | 当前实现固定为 `"0.020000"`。 |
| `frames[].objStates` | array | 每个关节 child link 对应的对象位姿。 |
| `objStates[].i` | string | URDF 中解析出的 child link UUID。 |
| `objStates[].tx/ty/tz` | number | link 位置,单位为米。 |
| `objStates[].qx/qy/qz/qw` | number | link 姿态四元数。 |
失败时常见错误:
| 错误 | 说明 |
| --- | --- |
| `Empty joints input` | 未解析到有效关节帧。 |
| `每组关节角都必须包含 6 个值` | 某一帧关节数量不是 6。 |
| `Forward kinematics calculation for all joints failed` | FK 失败,常见原因包括关节超限。 |
## 5. 逆运动学接口
### 5.1 `Cmd_Kinematics_inverse_pose_str`
功能:输入一个或多个 TCP 位姿,执行逆运动学。单点时直接求解;多点时对相邻位姿自动估算步数并插补,再逐点求 IK。
参数:
| 字段 | 类型 | 必填 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| `robot_uuid` | string | 否 | `"default"` | 机器人实例 ID。 |
| `pose_str` | string | 是 | `""` | 位姿或位姿序列,格式为 `x,y,z,qx,qy,qz,qw;...`。位置单位为米。 |
| `q_init_str` | string | 否 | `"0,0,0,0,0,0"` | IK 初始关节角,单位为弧度。用于影响多解选择和求解连续性。 |
返回值:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `joints` | number[][] | 逆解关节序列,每组 6 个关节角,单位为弧度。 |
| `success` | boolean | IK 是否成功。 |
实现说明:
- 单点输入返回 1 组关节解。
- 多点输入会对相邻点自动插补,自动步数范围当前为 10 到 100。
- 逐点 IK 成功后,会把上一点结果作为下一点初值。
- 逐点 IK 失败时,若已有成功结果则复用上一组结果,否则复用初始关节角,避免轨迹数组中断。
示例:
```json
{
"req_cmd": "Cmd_Kinematics_inverse_pose_str",
"req_param": {
"robot_uuid": "abb_irb120_3_58",
"pose_str": "0.374,0,0.63,0,0,0,1",
"q_init_str": "0,0,0,0,0,0"
}
}
```
### 5.2 `Cmd_Kinematics_inverse_pose_str_2PSteps`
功能:输入两个 TCP 位姿,按调用方指定步数进行插补,并对插补点逐点求 IK。主要用于 MoveL 直线路径的离散关节解生成。
参数:
| 字段 | 类型 | 必填 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| `robot_uuid` | string | 否 | `"default"` | 机器人实例 ID。 |
| `pose_str` | string | 是 | `""` | 起点和终点位姿,格式为 `pose1;pose2`。如果只传 1 个点,则直接求单点 IK。 |
| `q_init_str` | string | 否 | `"0,0,0,0,0,0"` | IK 初始关节角,单位为弧度。 |
| `steps_str` | string | 是 | `"default"` | 插补点数量字符串,例如 `"30"`。当前实现会调用 `stoi` 转整数,不能传非数字。 |
返回值:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `joints` | number[][] | 逆解关节序列,每组 6 个关节角,单位为弧度。 |
| `success` | boolean | IK 是否成功。 |
实现说明:
- 返回值是弧度,不是角度。
- 插补过程中每一点 IK 成功后,会更新下一点的初值。
- 如果某个插补点 IK 失败,会复用上一组成功结果;若还没有成功结果,则复用初始关节角。
示例:
```json
{
"req_cmd": "Cmd_Kinematics_inverse_pose_str_2PSteps",
"req_param": {
"robot_uuid": "abb_irb120_3_58",
"pose_str": "0.374,0,0.63,0,0,0,1;0.4626,0.2209,0.334,0.148283,0.435718,0.079062,0.884258",
"q_init_str": "0,0,0,0,0,0",
"steps_str": "30"
}
}
```
### 5.3 `Cmd_Kinematics_inverse_pose_str_NoDifference`
功能:直接对输入位姿点执行 IK不做相邻点插补。适用于调用方已经生成了离散轨迹点只需要逐点求关节解的场景。
参数:
| 字段 | 类型 | 必填 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| `robot_uuid` | string | 否 | `"default"` | 机器人实例 ID。 |
| `pose_str` | string | 是 | `""` | 位姿或位姿序列,格式为 `x,y,z,qx,qy,qz,qw;...`。 |
| `q_init_str` | string | 否 | `"0,0,0,0,0,0"` | IK 初始关节角,单位为弧度。 |
返回值:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `joints` | number[][] | 逆解关节序列,每组 6 个关节角,单位为弧度。 |
| `success` | boolean | IK 是否成功。 |
实现说明:
- 单点输入返回 1 组关节解。
- 多点输入不插补,逐点求解,并用上一点成功结果作为下一点初值。
- 当前多点实现循环到 `posePoints.size() - 1`,最后一个输入位姿不会被求解;如果业务需要完整多点结果,建议后续修正。
## 6. 奇异点检测接口
### 6.1 `Cmd_Kinematics_check_singularity`
功能:基于当前关节姿态计算 TCP 雅可比矩阵,对奇异值、条件数和可操作度进行评估,返回 `normal``warning``singular` 风险等级。
参数:
| 字段 | 类型 | 必填 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| `robot_uuid` | string | 否 | `"default"` | 机器人实例 ID。 |
| `joints_str` | string | 否 | `q_init_str``"0,0,0,0,0,0"` | 6 个关节角,单位为弧度。 |
| `q_init_str` | string | 否 | `"0,0,0,0,0,0"` | 兼容字段。未传 `joints_str` 时使用。 |
| `singular_threshold` | number | 否 | `1e-4` | 最小奇异值低于或等于该阈值时判定为奇异。 |
| `warning_threshold` | number | 否 | `1e-2` | 最小奇异值低于或等于该阈值时判定为接近奇异。 |
| `condition_threshold` | number | 否 | `1e6` | 条件数大于或等于该阈值时判定为奇异。 |
| `condition_warning_threshold` | number | 否 | `1e4` | 条件数大于或等于该阈值时判定为接近奇异。 |
返回值:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `success` | boolean | 检测是否成功。 |
| `is_singular` | boolean | 是否判定为奇异。 |
| `is_near_singular` | boolean | 是否判定为接近奇异。 |
| `risk_level` | string | 风险等级:`normal``warning``singular`。 |
| `rank` | number | 雅可比矩阵秩。 |
| `joint_count` | number | 运动学链关节数量。 |
| `min_singular_value` | number | 最小奇异值。 |
| `max_singular_value` | number | 最大奇异值。 |
| `condition_number` | number | 条件数。最小奇异值为 0 时为无穷大。 |
| `manipulability` | number | 可操作度指标,当前为全部奇异值乘积。 |
| `singular_values` | number[] | 雅可比矩阵奇异值。 |
| `thresholds` | object | 本次检测使用的阈值。 |
| `joints` | number[] | 输入关节角,单位为弧度。 |
| `jacobian` | number[][] | TCP 雅可比矩阵。 |
示例:
```json
{
"req_cmd": "Cmd_Kinematics_check_singularity",
"req_param": {
"robot_uuid": "abb_irb120_3_58",
"joints_str": "0.15,-0.25,0.35,0.1,-0.2,0.3"
}
}
```
风险判断逻辑:
- `singular``min_singular_value <= singular_threshold`,或 `condition_number >= condition_threshold`,或 `rank < joint_count`
- `warning`:未达到 `singular`,但 `min_singular_value <= warning_threshold``condition_number >= condition_warning_threshold`
- `normal`:不满足以上风险条件。
失败时常见错误:
| 错误 | 说明 |
| --- | --- |
| `Invalid joints format` | 关节字符串不是 6 个数值。 |
| `Joint value out of limits` | 输入关节超出 URDF 上下限。 |
| `Jacobian calculation failed` | 雅可比矩阵计算失败。 |
## 7. 非运动学占位接口
以下命令目前也由机器人命令处理器识别,但不是机器人运动学算法接口,当前仅返回固定成功结构。
### 7.1 `Cmd_SelectCraftTree`
功能:占位接口,表示选择工艺树。
参数:
| 字段 | 类型 | 必填 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| `tree_id` | string | 否 | `""` | 工艺树 ID。 |
返回值:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `success` | boolean | 固定为 `true`。 |
| `tree_id` | string | 输入工艺树 ID。 |
| `message` | string | 固定成功消息。 |
| `timestamp` | string | 当前时间字符串。 |
### 7.2 `Cmd_AddOperationTree`
功能:占位接口,表示新增操作树。
参数:
| 字段 | 类型 | 必填 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| `name` | string | 否 | `""` | 操作树名称。 |
| `operations` | array | 否 | `[]` | 操作列表。 |
返回值:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `success` | boolean | 固定为 `true`。 |
| `tree_name` | string | 输入操作树名称。 |
| `operations_count` | number | 输入操作数量。 |
| `message` | string | 固定成功消息。 |
| `timestamp` | string | 当前时间字符串。 |
## 8. 关节上下限和初始化约束
初始化机器人时会执行以下检查和准备:
- URDF 必须能解析为 KDL Tree。
- 模块会自动推导一条 6 轴串联运动学链,不再固定依赖 `base_link``base``tool0` 等名称。
- 当前只支持 6 轴机器人链,非 6 轴会初始化失败。
- 会从 URDF `<limit lower="..." upper="...">` 读取关节上下限。
- FK 输入、全关节 FK 输入、IK 初值、IK 结果、奇异点检测输入都会执行关节上下限检查。
## 9. 常见接入建议
- 前端 MoveJ 工艺通常使用角度数据做关节空间插值,调用 FK 前需要统一转为弧度。
- 前端 MoveL 工艺应把 TCP 位姿传给 IK 接口IK 返回弧度后如需存入角度工艺数据,需要转为角度。
- 连续 MoveL 或 MoveJ 后接 MoveL 时,建议把上一段末尾关节作为下一段 `q_init_str`,保证 IK 多解选择更连续。
- 奇异点检测只能发现风险,不会自动规避路径;规避需要结合多解选择、路径调整、姿态微调或工艺点重规划。
- `Cmd_Kinematics_forward_pose_str` 的入参字段名 `q_init_str` 属于历史命名,实际含义是 FK 输入关节角。

View File

@@ -0,0 +1,314 @@
# URDF/Orocos KDL 工业机器人运动学能力说明
本文档说明当前 URDF/Orocos KDL 机器人运动学模块已经支持的能力,以及距离完整工业级机器人控制器还需要补齐的能力。
## 当前已支持能力
### 多机器人 URDF 注册
模块支持通过 `Cmd_InitRobot` 动态注册 URDF并用 `robot_uuid` 管理不同机器人实例。当前也支持查询机器人、列出机器人和删除机器人。
相关能力:
- `Cmd_InitRobot`:注册或更新机器人 URDF。
- `Cmd_GetRobot`:查询指定机器人实例。
- `Cmd_ListRobots`:列出当前机器人实例。
- `Cmd_RemoveRobot`:删除机器人实例。
### 自动推导运动学链
初始化时不再固定依赖 `base``tool0``base_link` 等特定 link 名称。模块会基于 KDL Tree 自动推导 6 轴串联运动学链。
当前策略:
- 优先兼容旧命名链路 `base -> tool0`
- 如果旧链路不存在,则从 URDF 的真实根节点出发,扫描叶子 link。
- 选择一条正好包含 6 个活动关节的串联链。
### 正运动学 FK
模块支持输入 6 个关节角,计算 TCP 位姿。
输出内容:
- TCP 位置:`x, y, z`
- TCP 姿态:四元数 `qx, qy, qz, qw`
对应接口:
- `Cmd_Kinematics_forward_pose_str`
### 全关节位姿 FK
模块支持计算每个关节节点的位姿,并按前端需要的 `OPERATION.frames.objStates` 格式输出。
该能力适用于:
- three.js 机器人显示。
- 关节动画回放。
- 数字孪生场景姿态同步。
- 简单离线仿真预览。
对应接口:
- `Cmd_Kinematics_forward_all_joints`
### 逆运动学 IK
模块支持输入目标 TCP 位姿和初始关节角,计算对应关节解。
当前主路径使用 KDL LMA 求解器,另保留 NR 求解接口。
对应接口:
- `Cmd_Kinematics_inverse_pose_str`
- `Cmd_Kinematics_inverse_pose_str_2PSteps`
- `Cmd_Kinematics_inverse_pose_str_NoDifference`
### 姿态轨迹插补和逐点 IK
模块支持对两个姿态点之间进行插补,然后逐点执行 IK生成一串关节解。
当前支持两种方式:
- 自动估算插补步数。
- 由调用方指定插补步数。
适用场景:
- 前端拖动目标位姿后的简单路径预览。
- 离线验证 TCP 从起点到终点的可达性。
- 生成用于动画显示的关节序列。
### 不插补的多点 IK
模块支持直接对输入的多个目标姿态点逐点逆解,不在相邻点之间插补。
适用场景:
- 已有外部轨迹点。
- 只需要验证离散点位是否可达。
- 调试 IK 解算稳定性。
对应接口:
- `Cmd_Kinematics_inverse_pose_str_NoDifference`
### 关节上下限检查
模块已支持从 URDF 的 `<limit lower="..." upper="...">` 中读取关节上下限。
当前检查范围:
- FK 输入关节值。
- 全关节 FK 输入关节值。
- IK 初始关节值。
- IK 求解结果。
如果关节值超出 URDF 定义范围,计算会返回失败,不会继续输出不可用结果。
### three.js 前端测试页面
当前已有 three.js 测试页面,用于验证 URDF 注册、机器人显示、FK、IK 和姿态回放。
页面能力包括:
- 加载并注册 URDF。
- 显示 3D 机器人模型。
- 测试 FK。
- 测试 IK。
- 应用 IK 结果到关节状态。
- 查看 JSON 请求和响应。
## 当前定位
当前模块更接近“工业机器人运动学仿真和前端验证内核”。
它已经具备工业机器人常见的基础运动学能力:
- URDF 模型注册。
- 运动学链自动推导。
- FK。
- 全关节 FK。
- IK。
- 简单轨迹插补。
- 关节上下限约束。
- 前端 3D 可视化测试。
这些能力适合用于:
- 机器人模型可用性检查。
- 前端 3D 调试。
- 数字孪生姿态同步。
- 离线点位可达性验证。
- 简单路径和动画预览。
## 尚未支持的完整工业级能力
以下能力目前尚未完整支持,如果要接近真实工业机器人控制器,需要继续补齐。
### 碰撞检测
当前不处理碰撞相关内容。
尚未支持:
- 机器人自碰撞检测。
- 机器人与环境碰撞检测。
- 工具与工件碰撞检测。
- 基于 URDF collision 几何的碰撞模型构建。
### 奇异点检测
模块已支持基于 TCP 雅可比矩阵的奇异点检测。
当前支持:
- 计算 6x6 TCP 雅可比矩阵。
- 计算雅可比矩阵奇异值。
- 输出最小奇异值、最大奇异值、条件数和 manipulability。
- 输出 rank。
- 根据阈值给出 `normal``warning``singular` 风险等级。
- 在 three.js 测试页面中通过“奇异点”按钮直接查看当前关节姿态风险。
对应接口:
- `Cmd_Kinematics_check_singularity`
请求示例:
```json
{
"msg": "singularity check",
"req_code": "CHECK_SINGULARITY_001",
"req_from": "client",
"req_cmd": "Cmd_Kinematics_check_singularity",
"req_param": {
"robot_uuid": "abb_irb120_3_58",
"joints_str": "0.15,-0.25,0.35,0.1,-0.2,0.3"
}
}
```
可选阈值参数:
```json
{
"singular_threshold": 0.0001,
"warning_threshold": 0.01,
"condition_threshold": 1000000,
"condition_warning_threshold": 10000
}
```
响应中的关键字段:
- `risk_level``normal``warning``singular`
- `is_singular`:是否已判定为奇异。
- `is_near_singular`:是否接近奇异。
- `rank`:雅可比矩阵秩。
- `singular_values`:奇异值数组。
- `min_singular_value`:最小奇异值。
- `condition_number`:条件数。
- `manipulability`:可操作度指标。
- `jacobian`6x6 TCP 雅可比矩阵。
注意:当前检测给出数值风险等级,尚未进一步分类为腕部奇异、肩部奇异或肘部奇异。
### 速度、加速度和 jerk 约束
当前主要约束位置上下限,尚未完整处理工业轨迹中的速度、加速度和 jerk 约束。
尚未支持:
- 关节速度限制。
- 关节加速度限制。
- 笛卡尔速度限制。
- 笛卡尔加速度限制。
- jerk 限制。
- 时间最优轨迹规划。
### 多 IK 解管理
工业机器人通常存在多个 IK 分支。当前模块没有显式管理多解分支。
尚未支持:
- 肘上/肘下选择。
- 腕翻转/不翻转选择。
- 肩部左右构型选择。
- 最接近当前姿态的解选择。
- 解连续性筛选。
### 轨迹连续性和最短路径
当前插补和 IK 可以生成关节序列,但还不是完整工业级轨迹规划。
尚未支持:
- 关节空间最短路径选择。
- 关节跨越 `+-pi` 时的连续性处理。
- 多点轨迹平滑。
- 速度连续和加速度连续约束。
- 轨迹失败点定位和恢复。
### 工具坐标系和工件坐标系
当前主要使用 URDF 链路末端作为 TCP尚未形成完整坐标系管理能力。
尚未支持:
- 工具坐标系 TCP 管理。
- 工件坐标系/用户坐标系管理。
- 基坐标、世界坐标、工具坐标之间的统一转换。
- 多 TCP 切换。
### 工业机器人指令语义
当前支持基础运动学计算,但还没有完整工业机器人指令层。
尚未支持:
- PTP 指令。
- LIN 指令。
- CIRC 指令。
- Blend/Zone 过渡。
- 等待、IO、夹具动作与轨迹同步。
- 程序段级离线编程。
### 动力学和负载
当前模块不处理动力学。
尚未支持:
- 质量和惯量参与计算。
- 关节力矩计算。
- 重力补偿。
- 负载模型。
- 动态可达性判断。
### 控制器通讯和实时控制
当前模块是 WASM 运动学计算内核,不是实时控制器。
尚未支持:
- 与真实机器人控制器通讯。
- 实时伺服控制。
- 关节反馈闭环。
- 安全互锁。
- 急停、安全区、限位开关等控制器级安全能力。
## 建议后续增强顺序
如果继续按工业机器人实际使用场景增强,建议优先级如下:
1. 增加多 IK 解和构型选择。
2. 增加关节连续性和最短路径处理。
3. 增加速度、加速度和轨迹时间参数化。
4. 增加工具坐标系和工件坐标系管理。
5. 增加 PTP、LIN、CIRC 等工业运动指令。
6. 增加碰撞检测。
7. 增加奇异类型分类,例如腕部、肩部、肘部奇异。
8. 增加控制器通讯和实时执行相关能力。

309
docs/urdf.xml Normal file
View File

@@ -0,0 +1,309 @@
<robot
xmlns:xacro="http://ros.org/wiki/xacro" name="ABB_IRB_120" uuid="8bc48ac0-6570-4232-aa91-3367771c1069">
<link name="base_link" uuid="9c88ec70-a337-4d16-94c5-92ccff72c1a5">
<inertial>
<mass value="1.0"/>
<origin rpy="0 0 0" xyz="0 0 0"/>
<inertia ixx="0.001" ixy="0" ixz="0" iyy="0.001" iyz="0" izz="0.001"/>
</inertial>
<visual>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="package://abb_irb120_support/meshes/irb120_3_58/visual/link_1.stl"/>
</geometry>
<material name="">
<color rgba="0.7372549 0.3490196 0.1607843 1"/>
</material>
</visual>
<collision>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="package://abb_irb120_support/meshes/irb120_3_58/collision/link_1.stl"/>
</geometry>
<material name="">
<color rgba="1 1 0 1"/>
</material>
</collision>
</link>
<link name="link_1" uuid="09059ca4-6c99-4a5f-a474-8ba964e612c4">
<inertial>
<mass value="1.0"/>
<origin rpy="0 0 0" xyz="0 0 0"/>
<inertia ixx="0.001" ixy="0" ixz="0" iyy="0.001" iyz="0" izz="0.001"/>
</inertial>
<visual>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="package://abb_irb120_support/meshes/irb120_3_58/visual/link_2.stl"/>
</geometry>
<material name="">
<color rgba="0.7372549 0.3490196 0.1607843 1"/>
</material>
</visual>
<collision>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="package://abb_irb120_support/meshes/irb120_3_58/collision/link_2.stl"/>
</geometry>
<material name="">
<color rgba="1 1 0 1"/>
</material>
</collision>
</link>
<link name="link_2" uuid="3f38944a-9e19-4aba-a46e-2cc2d2611b91">
<inertial>
<mass value="1.0"/>
<origin rpy="0 0 0" xyz="0 0 0"/>
<inertia ixx="0.001" ixy="0" ixz="0" iyy="0.001" iyz="0" izz="0.001"/>
</inertial>
<visual>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="package://abb_irb120_support/meshes/irb120_3_58/visual/link_3.stl"/>
</geometry>
<material name="">
<color rgba="0.7372549 0.3490196 0.1607843 1"/>
</material>
</visual>
<collision>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="package://abb_irb120_support/meshes/irb120_3_58/collision/link_3.stl"/>
</geometry>
<material name="">
<color rgba="1 1 0 1"/>
</material>
</collision>
</link>
<link name="link_3" uuid="801f8e99-88d8-4a92-b848-3c573c5e6ddb">
<inertial>
<mass value="1.0"/>
<origin rpy="0 0 0" xyz="0 0 0"/>
<inertia ixx="0.001" ixy="0" ixz="0" iyy="0.001" iyz="0" izz="0.001"/>
</inertial>
<visual>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="package://abb_irb120_support/meshes/irb120_3_58/visual/link_4.stl"/>
</geometry>
<material name="">
<color rgba="0.7372549 0.3490196 0.1607843 1"/>
</material>
</visual>
<collision>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="package://abb_irb120_support/meshes/irb120_3_58/collision/link_4.stl"/>
</geometry>
<material name="">
<color rgba="1 1 0 1"/>
</material>
</collision>
</link>
<link name="link_4" uuid="c0146e6e-b9c7-4ada-9e96-b7bb141f6feb">
<inertial>
<mass value="1.0"/>
<origin rpy="0 0 0" xyz="0 0 0"/>
<inertia ixx="0.001" ixy="0" ixz="0" iyy="0.001" iyz="0" izz="0.001"/>
</inertial>
<visual>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="package://abb_irb120_support/meshes/irb120_3_58/visual/link_5.stl"/>
</geometry>
<material name="">
<color rgba="0.7372549 0.3490196 0.1607843 1"/>
</material>
</visual>
<collision>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="package://abb_irb120_support/meshes/irb120_3_58/collision/link_5.stl"/>
</geometry>
<material name="">
<color rgba="1 1 0 1"/>
</material>
</collision>
</link>
<link name="link_5" uuid="5a650225-e5cf-44ae-b2fc-aad215668177">
<inertial>
<mass value="1.0"/>
<origin rpy="0 0 0" xyz="0 0 0"/>
<inertia ixx="0.001" ixy="0" ixz="0" iyy="0.001" iyz="0" izz="0.001"/>
</inertial>
<visual>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="package://abb_irb120_support/meshes/irb120_3_58/visual/link_6.stl"/>
</geometry>
<material name="">
<color rgba="0.7372549 0.3490196 0.1607843 1"/>
</material>
</visual>
<collision>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="package://abb_irb120_support/meshes/irb120_3_58/collision/link_6.stl"/>
</geometry>
<material name="">
<color rgba="1 1 0 1"/>
</material>
</collision>
</link>
<link name="link_6" uuid="4d20b9a3-23a5-48d8-bed6-b50439da66c0">
<inertial>
<mass value="1.0"/>
<origin rpy="0 0 0" xyz="0 0 0"/>
<inertia ixx="0.001" ixy="0" ixz="0" iyy="0.001" iyz="0" izz="0.001"/>
</inertial>
<visual>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="package://abb_irb120_support/meshes/irb120_3_58/visual/link_7.stl"/>
</geometry>
<material name="">
<color rgba="0.7372549 0.3490196 0.1607843 1"/>
</material>
</visual>
<collision>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="package://abb_irb120_support/meshes/irb120_3_58/collision/link_7.stl"/>
</geometry>
<material name="">
<color rgba="1 1 0 1"/>
</material>
</collision>
</link>
<joint name="joint_1" innerId="D0F7B07F-0438-4392-B775-42512D10BBD4" type="revolute">
<origin rpy="0.000000 0.000000 0.000000" xyz="0.000000 0.000000 0.000000"/>
<parent link="base_link" uuid="9c88ec70-a337-4d16-94c5-92ccff72c1a5"/>
<child link="link_1" uuid="09059ca4-6c99-4a5f-a474-8ba964e612c4"/>
<axis xyz="0 0 1"/>
<limit effort="0" lower="-6.283185" upper="6.283185" velocity="1.570796"/>
<dynamics damping="0.0" friction="0.0"/>
</joint>
<joint name="joint_2" innerId="3D80EDAC-92C7-492C-91FD-8108A298E175" type="revolute">
<origin rpy="0.000000 0.000000 0.000000" xyz="0.000000 0.000000 0.290000"/>
<parent link="link_1" uuid="09059ca4-6c99-4a5f-a474-8ba964e612c4"/>
<child link="link_2" uuid="3f38944a-9e19-4aba-a46e-2cc2d2611b91"/>
<axis xyz="1 0 0"/>
<limit effort="0" lower="-6.283185" upper="6.283185" velocity="1.570796"/>
<dynamics damping="0.0" friction="0.0"/>
</joint>
<joint name="joint_3" innerId="0A098978-9EAC-4F7B-B8DB-A1FFF640B6FF" type="revolute">
<origin rpy="0.000000 0.000000 0.000000" xyz="0.000000 0.000000 0.270000"/>
<parent link="link_2" uuid="3f38944a-9e19-4aba-a46e-2cc2d2611b91"/>
<child link="link_3" uuid="801f8e99-88d8-4a92-b848-3c573c5e6ddb"/>
<axis xyz="1 0 0"/>
<limit effort="0" lower="-6.283185" upper="6.283185" velocity="1.570796"/>
<dynamics damping="0.0" friction="0.0"/>
</joint>
<joint name="joint_4" innerId="823DDDE0-DBFA-4F5E-B3A6-9A87C06C8DB9" type="revolute">
<origin rpy="0.000000 0.000000 0.000000" xyz="0.000000 -0.134000 0.070000"/>
<parent link="link_3" uuid="801f8e99-88d8-4a92-b848-3c573c5e6ddb"/>
<child link="link_4" uuid="c0146e6e-b9c7-4ada-9e96-b7bb141f6feb"/>
<axis xyz="0 1 0"/>
<limit effort="0" lower="-6.283185" upper="6.283185" velocity="1.570796"/>
<dynamics damping="0.0" friction="0.0"/>
</joint>
<joint name="joint_5" innerId="A3DD936A-C88D-4267-8926-1C29C144D52E" type="revolute">
<origin rpy="0.000000 0.000000 0.000000" xyz="0.000000 -0.168000 0.000000"/>
<parent link="link_4" uuid="c0146e6e-b9c7-4ada-9e96-b7bb141f6feb"/>
<child link="link_5" uuid="5a650225-e5cf-44ae-b2fc-aad215668177"/>
<axis xyz="1 0 0"/>
<limit effort="0" lower="-6.283185" upper="6.283185" velocity="1.570796"/>
<dynamics damping="0.0" friction="0.0"/>
</joint>
<joint name="joint_6" innerId="85949D90-0C7B-463A-A18D-281A71FA4C19" type="revolute">
<origin rpy="0.000000 0.000000 0.000000" xyz="0.000000 -0.072000 0.000000"/>
<parent link="link_5" uuid="5a650225-e5cf-44ae-b2fc-aad215668177"/>
<child link="link_6" uuid="4d20b9a3-23a5-48d8-bed6-b50439da66c0"/>
<axis xyz="0 1 0"/>
<limit effort="0" lower="-6.283185" upper="6.283185" velocity="1.570796"/>
<dynamics damping="0.0" friction="0.0"/>
</joint>
<link name="base" uuidBase="f25bddf0-6533-448a-a1f5-b1ebbabe7d3c"/>
<joint name="base_link-base" type="fixed">
<origin xyz="0.000000 0.000000 0.000000" rpy="0.000000 0.000000 0.000000"/>
<parent link="base"/>
<child link="base_link"/>
</joint>
<link name="flange"/>
<joint name="joint_6-flange" type="fixed">
<origin rpy="0.000000 0.000000 0.000000" xyz="0.000000 0.000000 0.000000"/>
<parent link="link_6"/>
<child link="flange"/>
</joint>
<link name="tool0" uuidTool="5272238a-a622-412b-aa83-3e748fe57798"/>
<joint name="link_6-tool0" type="fixed">
<origin rpy="0.000000 0.000000 0.000000" xyz="0.000000 0.000000 0.000000"/>
<parent link="flange"/>
<child link="tool0"/>
</joint>
<transmission name="joint_1">
<type>transmission_interface/SimpleTransmission</type>
<joint>
<hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface>
</joint>
<actuator name="joint_1_motor">
<hardwareInterface>1</hardwareInterface>
<mechanicalReduction/>
</actuator>
</transmission>
<transmission name="joint_2">
<type>transmission_interface/SimpleTransmission</type>
<joint>
<hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface>
</joint>
<actuator name="joint_2_motor">
<hardwareInterface>1</hardwareInterface>
<mechanicalReduction/>
</actuator>
</transmission>
<transmission name="joint_3">
<type>transmission_interface/SimpleTransmission</type>
<joint>
<hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface>
</joint>
<actuator name="joint_3_motor">
<hardwareInterface>1</hardwareInterface>
<mechanicalReduction/>
</actuator>
</transmission>
<transmission name="joint_4">
<type>transmission_interface/SimpleTransmission</type>
<joint>
<hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface>
</joint>
<actuator name="joint_4_motor">
<hardwareInterface>1</hardwareInterface>
<mechanicalReduction/>
</actuator>
</transmission>
<transmission name="joint_5">
<type>transmission_interface/SimpleTransmission</type>
<joint>
<hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface>
</joint>
<actuator name="joint_5_motor">
<hardwareInterface>1</hardwareInterface>
<mechanicalReduction/>
</actuator>
</transmission>
<transmission name="joint_6">
<type>transmission_interface/SimpleTransmission</type>
<joint>
<hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface>
</joint>
<actuator name="joint_6_motor">
<hardwareInterface>1</hardwareInterface>
<mechanicalReduction/>
</actuator>
</transmission>
<gazebo>
<plugin name="gazebo_ros_control" filename="libgazebo_ros_control.so">
<robotNamespace>/</robotNamespace>
</plugin>
</gazebo>
</robot>

View File

@@ -19,6 +19,24 @@ private:
std::function<void(const std::string &)> onLog;
// 按功能域分发请求
json dispatchCommand(const std::string &req_cmd, const json &req_param);
// 机器人运动学与机器人管理接口
json handleRobotCommand(const std::string &req_cmd, const json &req_param);
// SPC 接口
json handleSpcCommand(const std::string &req_cmd, const json &req_param);
// 连杆机构接口
json handleFourBarCommand(const std::string &req_cmd, const json &req_param);
// 四足机器人接口
json handleQuadrupedCommand(const std::string &req_cmd, const json &req_param);
// 未知命令的默认响应
json createUnknownCommandResponse(const std::string &req_cmd, const json &req_param) const;
public:
KinematicsWebAPI();

View File

@@ -1,246 +1,291 @@
#ifndef ROBOT_H
#ifndef ROBOT_H
#define ROBOT_H
#include <unordered_map>
#include <kdl/chain.hpp>
#include <kdl/chainfksolverpos_recursive.hpp>
#include <kdl/chainiksolvervel_pinv.hpp>
#include <kdl/chainiksolverpos_lma.hpp>
#include <kdl/chainiksolverpos_nr.hpp>
#include <kdl/chainiksolverpos_lma.hpp> // 锟斤拷锟斤拷LMA锟斤拷锟斤拷锟酵凤拷募锟<E58B9F>
#include <kdl/chainiksolverpos_nr_jl.hpp>
#include <kdl/chainjnttojacsolver.hpp>
#include <kdl/chainiksolvervel_pinv.hpp>
#include <kdl/frames.hpp>
#include <kdl/jntarray.hpp>
#include <kdl/tree.hpp>
#include <string>
// 锟斤拷 Robot.h 锟斤拷锟斤拷锟斤拷
#include <unordered_map>
#include <vector>
#include "utils.h"
class Robot
{
private:
KDL::Chain kinematicChain;
KDL::ChainFkSolverPos_recursive *fkSolver;
KDL::ChainIkSolverVel_pinv *ikVelSolver;
KDL::ChainIkSolverPos_NR *ikSolverNR;
KDL::ChainIkSolverPos_LMA *ikSolverLMA; // 锟斤拷锟斤拷LMA锟斤拷锟斤拷锟<E68BB7>
KDL::ChainIkSolverPos_NR_JL *ikSolverNR;
KDL::ChainIkSolverPos_LMA *ikSolverLMA;
bool m_initialized;
KDL::JntArray jointLowerLimits;
KDL::JntArray jointUpperLimits;
std::vector<std::string> activeJointNames;
bool m_hasJointLimits;
std::unordered_map<std::string, std::string> jointChildLinkUuidMap; // joint锟斤拷锟狡碉拷child link UUID锟斤拷映锟斤拷
// 记录 joint 名称到 child link UUID 的映射,便于前端回写对象姿态。
std::unordered_map<std::string, std::string> jointChildLinkUuidMap;
// 私锟叫革拷锟斤拷锟斤拷锟斤拷
// 内部工具方法。
double quaternionAngleDifference(const double q1[4], const double q2[4]);
int calculateAutoSteps(const double startPose[7], const double endPose[7],
int minSteps, int maxSteps,
double positionResolution, double orientationResolution);
// 锟斤拷锟斤拷私锟叫凤拷锟斤拷
bool parseJointChildLinkUuidsFromUrdf(const std::string &urdfString);
// 自动收集 KDL Tree 中没有子节点的末端 link 名称。
std::vector<std::string> collectLeafSegmentNames(const KDL::Tree &tree) const;
// 自动选择可用于正逆运动学的 6 轴串联链,避免固定依赖 base/tool0 命名。
bool selectKinematicChain(const KDL::Tree &tree, KDL::Chain &selectedChain) const;
// 从 URDF 中提取当前运动学链的关节上下限。
bool parseJointLimitsFromUrdf(const std::string &urdfString);
// 检查关节数组是否处于 URDF 定义的上下限范围内。
bool validateJointLimits(const double joints[6], const std::string &context) const;
// 检查关节向量是否处于 URDF 定义的上下限范围内。
bool validateJointLimits(const std::vector<double> &joints, const std::string &context) const;
public:
/**
* @brief 默锟较癸拷锟届函锟斤拷
* @brief 默认构造函数。
*/
Robot();
/**
* @brief 锟斤拷URDF锟街凤拷锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷锟<EFBFBD>
* @param urdfString URDF锟斤拷式锟斤拷锟街凤拷锟斤拷
* @brief 使用 URDF 字符串直接初始化机器人。
* @param urdfString URDF 格式字符串。
*/
explicit Robot(const std::string &urdfString);
/**
* @brief 锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷源
* @brief 释放运动学求解器资源。
*/
~Robot();
int numberOfJoints;
/**
* @brief 锟斤拷URDF锟街凤拷锟斤拷锟斤拷始锟斤拷锟斤拷锟斤拷锟斤拷
* @param urdfString URDF锟斤拷式锟斤拷锟街凤拷锟斤拷
* @return 锟缴癸拷锟斤拷锟斤拷true锟斤拷失锟杰凤拷锟斤拷false
* @brief 根据 URDF 字符串初始化机器人运动学链。
* @param urdfString URDF 格式字符串。
* @return 初始化成功返回 true否则返回 false
*/
bool initRobot(const std::string &urdfString);
/**
* @brief 锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷呕锟饺<EFBFBD>joint锟斤拷应锟斤拷child link UUID
* @param jointIndex joint锟斤拷锟<EFBFBD> (1, 2, 3, ...)
* @return 锟斤拷应锟斤拷UUID锟街凤拷锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷蚍祷乜锟斤拷址锟斤拷锟<EFBFBD>
* @brief 根据关节序号获取 child link UUID
* @param jointIndex 关节序号,从 1 开始。
* @return 找到时返回对应 UUID未找到返回空字符串。
*/
std::string getJointUuidByIndex(int jointIndex) const;
/**
* @brief 锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷呕锟饺<EFBFBD>joint锟斤拷锟斤拷
* @param jointIndex joint锟斤拷锟<EFBFBD> (1, 2, 3, ...)
* @return joint锟斤拷锟斤拷锟街凤拷锟斤拷锟斤拷锟斤拷 "joint_1", "joint_2" 锟斤拷
* @brief 根据关节序号生成标准关节名称。
* @param jointIndex 关节序号,从 1 开始。
* @return 如 `joint_1`、`joint_2` 这样的关节名称。
*/
static std::string getJointNameByIndex(int jointIndex);
/**
* @brief 锟斤拷取锟斤拷锟叫匡拷锟斤拷joint锟斤拷锟斤拷斜锟<EFBFBD>
* @return 锟斤拷锟斤拷锟斤拷锟叫匡拷锟斤拷joint锟斤拷诺锟斤拷锟斤拷锟<EFBFBD>
* @brief 获取当前已解析到的全部关节序号列表。
* @return 已排序且去重后的关节序号集合。
*/
std::vector<int> getAvailableJointIndices() const;
// 锟斤拷Robot.h锟斤拷锟斤拷锟斤拷
/**
* @brief 兼容多种命名格式查找关节 UUID。
* @param jointIndex 关节序号,从 1 开始。
* @return 找到时返回 UUID未找到返回空字符串。
*/
std::string findJointUuidByAnyFormat(int jointIndex) const;
/**
* @brief 对姿态序列做逆解,自动进行轨迹插补。
*/
json inversePoseStr(const std::string &pose_str, const std::string &q_init_str);
// 锟斤拷锟斤拷锟斤拷锟斤拷筒锟街碉拷锟斤拷锟斤拷锟斤拷锟斤拷锟截诧拷趾锟斤拷锟斤拷锟<E68BB7>
/**
* @brief 对两点姿态按指定步数插补后做逆解。
*/
json inversePoseStr2PSteps(const std::string &pose_str, const std::string &q_init_str, const std::int32_t steps);
// 锟斤拷锟斤拷锟斤拷牡锟街<E9949F>锟戒不锟斤拷锟叫诧拷趾锟街憋拷臃锟斤拷囟锟接︼拷锟斤拷锟斤拷锟<E68BB7>
/**
* @brief 直接对输入姿态点做逆解,不做相邻点插补。
*/
json inversePoseStrNoDifference(const std::string &pose_str, const std::string &q_init_str);
/**
* @brief 解析姿态字符串,格式为 `x,y,z,qx,qy,qz,qw;...`。
*/
std::vector<std::vector<double>> parsePoseString(const std::string &pose_str);
/**
* @brief 解析关节列表字符串,格式为 `j1,j2,j3,j4,j5,j6;...`。
*/
std::vector<std::vector<double>> parseJointListString(const std::string &pose_str);
std::unordered_map<std::string, std::string> getJointChildLinkUuidMap() const { return jointChildLinkUuidMap; }
std::string getJointChildLinkUuid(const std::string &jointName) const;
std::vector<double> parseJointString(const std::string &joint_str);
json kinematicsForwardAllJointsList(std::vector<double> joints);
json handleKinematicsForwardAllJoints(const std::string &joints_str);
json handleKinematicsForwardAllJoints_objStates(const std::string &joints_str);
/**
* @brief 锟斤拷锟斤拷锟斤拷锟斤拷锟角凤拷锟窖筹拷始锟斤拷
* @return 锟窖筹拷始锟斤拷锟斤拷锟斤拷true
* @brief 计算所有关节位姿,并按前端需要的 objStates 格式组织数据。
*/
json kinematicsForwardAllJointsList(std::vector<double> joints);
/**
* @brief 批量计算多帧关节正解,输出 OPERATION 结构。
*/
json handleKinematicsForwardAllJoints(const std::string &joints_str);
/**
* @brief 兼容旧格式的全部关节正解输出。
*/
json handleKinematicsForwardAllJoints_objStates(const std::string &joints_str);
/**
* @brief 判断机器人是否已初始化。
* @return 已初始化返回 true。
*/
bool isInitialized() const { return m_initialized; }
/**
* @brief 锟斤拷锟斤拷锟剿讹拷学锟斤拷锟<EFBFBD> (NR锟斤拷锟斤拷)
* @param pose 目锟斤拷位锟剿o拷锟斤拷锟斤拷锟斤拷式[x, y, z, qx, qy, qz, qw]
* @param iniJ 锟斤拷始锟截节角度o拷锟斤拷锟斤拷锟斤拷式[6锟斤拷锟截节角讹拷]
* @param resultJoints 锟斤拷锟斤拷锟斤拷锟截节角度o拷锟斤拷锟斤拷锟斤拷式[6锟斤拷锟截节角讹拷]
* @return 锟缴癸拷锟斤拷锟斤拷true锟斤拷失锟杰凤拷锟斤拷false
* @brief 使用 NR 求解器计算逆运动学。
* @param pose 目标位姿,格式为 `[x, y, z, qx, qy, qz, qw]`。
* @param iniJ 初始关节角,格式为 `[j1, ..., j6]`。
* @param resultJoints 输出关节角结果。
* @return 求解成功返回 true否则返回 false
*/
bool calculateIK_NR(const double pose[7], const double iniJ[6], double resultJoints[6]);
/**
* @brief 锟斤拷锟斤拷锟剿讹拷学锟斤拷锟<EFBFBD> (LMA锟斤拷锟斤拷)
* @param pose 目锟斤拷位锟剿o拷锟斤拷锟斤拷锟斤拷式[x, y, z, qx, qy, qz, qw]
* @param iniJ 锟斤拷始锟截节角度o拷锟斤拷锟斤拷锟斤拷式[6锟斤拷锟截节角讹拷]
* @param resultJoints 锟斤拷锟斤拷锟斤拷锟截节角度o拷锟斤拷锟斤拷锟斤拷式[6锟斤拷锟截节角讹拷]
* @param eps 锟斤拷锟斤拷锟斤拷锟斤拷 (默锟斤拷: 1e-8)
* @param maxiter 锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷 (默锟斤拷: 3000)
* @param eps_joints 锟截斤拷锟捷诧拷 (默锟斤拷: 1e-12)
* @return 锟缴癸拷锟斤拷锟斤拷true锟斤拷失锟杰凤拷锟斤拷false
* @brief 使用 LMA 求解器计算逆运动学。
* @param pose 目标位姿,格式为 `[x, y, z, qx, qy, qz, qw]`。
* @param iniJ 初始关节角,格式为 `[j1, ..., j6]`。
* @param resultJoints 输出关节角结果。
* @param eps 迭代收敛精度。
* @param maxiter 最大迭代次数。
* @param eps_joints 关节收敛阈值。
* @return 求解成功返回 true否则返回 false
*/
bool calculateIK_LMA(const double pose[7], const double iniJ[6], double resultJoints[6],
double eps = 1e-8, int maxiter = 3000, double eps_joints = 1e-12);
/**
* @brief 锟斤拷锟斤拷锟剿讹拷学锟斤拷锟<EFBFBD> - TCP位锟斤拷
* @param joints 锟斤拷锟斤拷亟诮嵌龋锟斤拷锟斤拷锟斤拷锟绞絒6锟斤拷锟截节角讹拷]
* @param tcpPose 锟斤拷锟絋CP位锟剿拷锟斤拷锟斤拷锟斤拷式[x, y, z, qx, qy, qz, qw]
* @return 锟缴癸拷锟斤拷锟斤拷true锟斤拷失锟杰凤拷锟斤拷false
* @brief 计算 TCP 的正运动学结果。
* @param joints 输入关节角,格式为 `[j1, ..., j6]`。
* @param tcpPose 输出 TCP 位姿,格式为 `[x, y, z, qx, qy, qz, qw]`。
* @return 计算成功返回 true否则返回 false
*/
bool calculateFK_TCP(const double joints[6], double tcpPose[7]);
/**
* @brief 锟斤拷锟斤拷锟剿讹拷学锟斤拷锟<EFBFBD> - 锟斤拷锟叫关斤拷位锟斤拷
* @param joints 锟斤拷锟斤拷亟诮嵌龋锟斤拷锟斤拷锟斤拷锟绞絒6锟斤拷锟截节角讹拷]
* @param jointPoses 锟斤拷锟斤拷锟斤拷泄亟锟轿伙拷耍锟斤拷锟斤拷锟斤拷锟绞絒6*7锟斤拷元锟斤拷]
* @return 锟缴癸拷锟斤拷锟斤拷true锟斤拷失锟杰凤拷锟斤拷false
* @brief 计算每个关节节点的正运动学结果。
* @param joints 输入关节角,格式为 `[j1, ..., j6]`。
* @param jointPoses 输出全部关节位姿,共 6 组,每组 7 个值。
* @return 计算成功返回 true否则返回 false
*/
bool calculateFK_AllJointsforwardKinematics(const double joints[6], double jointPoses[42]);
bool forwardKinematics(const double joints[6], double jointPoses[42]);
/**
* @brief 锟斤拷取锟斤拷锟斤拷锟斤拷锟剿讹拷锟斤拷
* @return KDL锟剿讹拷锟斤拷锟斤拷锟斤拷
* @brief 检查当前关节姿态是否接近奇异点。
* @param joints_str 输入关节角,格式为 `j1,j2,j3,j4,j5,j6`。
* @param singularThreshold 最小奇异值低于该阈值时判定为奇异。
* @param warningThreshold 最小奇异值低于该阈值时判定为接近奇异。
* @param conditionThreshold 条件数超过该阈值时判定为奇异。
* @param conditionWarningThreshold 条件数超过该阈值时判定为接近奇异。
* @return 奇异点检测结果 JSON。
*/
json checkSingularity(const std::string &joints_str,
double singularThreshold = 1e-4,
double warningThreshold = 1e-2,
double conditionThreshold = 1e6,
double conditionWarningThreshold = 1e4);
/**
* @brief 获取当前 KDL 运动学链。
*/
const KDL::Chain &getKinematicChain() const { return kinematicChain; }
/**
* @brief 锟斤拷取锟截斤拷锟斤拷锟斤拷
* @return 锟截斤拷锟斤拷锟斤拷
* @brief 获取机器人关节数量。
*/
int getNumberOfJoints() const { return kinematicChain.getNrOfJoints(); }
// 锟届迹锟芥划锟斤拷锟斤拷
/**
* @brief 锟斤拷锟斤拷锟斤拷态锟斤拷墓旒拷婊<EFBFBD>锟斤拷锟斤拷锟斤拷姹撅拷锟<EFBFBD>
* @param pose1 锟斤拷始位锟斤拷 [x, y, z, qx, qy, qz, qw]
* @param pose2 锟斤拷止位锟斤拷 [x, y, z, qx, qy, qz, qw]
* @param outputPoses 锟斤拷锟斤拷旒拷锟斤拷锟斤拷锟<EFBFBD>
* @param npoint 锟届迹锟斤拷锟斤拷锟斤拷锟斤拷0锟斤拷示锟皆讹拷锟斤拷锟姐
* @return 实锟斤拷锟斤拷锟缴的轨迹锟斤拷锟斤拷锟斤拷锟斤拷失锟杰凤拷锟斤拷-1
* @brief 生成姿态轨迹,输出为固定数组。
* @param pose1 起点位姿。
* @param pose2 终点位姿。
* @param outputPoses 输出轨迹数组。
* @param npoint 轨迹点数量,传 0 表示自动计算。
* @return 实际生成的轨迹点数量,失败返回 -1
*/
int trajectoryPlanning(const double pose1[7], const double pose2[7],
double outputPoses[][7], int npoint = 0);
/**
* @brief 锟斤拷锟斤拷锟斤拷态锟斤拷墓旒拷婊<EFBFBD>锟斤拷锟斤拷锟斤拷姹撅拷锟斤拷锟斤拷锟斤拷vector锟斤拷
* @param pose1 锟斤拷始位锟斤拷 [x, y, z, qx, qy, qz, qw]
* @param pose2 锟斤拷止位锟斤拷 [x, y, z, qx, qy, qz, qw]
* @param outputPoses 锟斤拷锟斤拷旒拷锟絭ector锟斤拷锟斤拷锟斤拷预锟饺凤拷锟斤拷锟节存
* @param npoint 锟届迹锟斤拷锟斤拷锟斤拷锟斤拷0锟斤拷示锟皆讹拷锟斤拷锟姐
* @return 实锟斤拷锟斤拷锟缴的轨迹锟斤拷锟斤拷锟斤拷锟斤拷失锟杰凤拷锟斤拷-1
* @brief 生成姿态轨迹,输出为二维向量。
* @param pose1 起点位姿。
* @param pose2 终点位姿。
* @param outputPoses 输出轨迹点集合。
* @param npoint 轨迹点数量,传 0 表示自动计算。
* @return 实际生成的轨迹点数量,失败返回 -1
*/
int trajectoryPlanning(const double pose1[7], const double pose2[7],
std::vector<std::vector<double>> &outputPoses,
int npoint = 0);
/**
* @brief 锟斤拷锟斤拷锟斤拷态锟斤拷墓旒拷婊<EFBFBD>锟斤拷vector锟芥本锟斤拷
* @param pose1 锟斤拷始位锟斤拷
* @param pose2 锟斤拷止位锟斤拷
* @param outputPoses 锟斤拷锟斤拷旒拷锟斤拷锟斤拷锟<EFBFBD>
* @param npoint 锟届迹锟斤拷锟斤拷锟斤拷锟斤拷0锟斤拷示锟皆讹拷锟斤拷锟姐
* @return 实锟斤拷锟斤拷锟缴的轨迹锟斤拷锟斤拷锟斤拷锟斤拷失锟杰凤拷锟斤拷-1
* @brief 使用 `std::vector<double>` 形式输入位姿生成轨迹。
* @param pose1 起点位姿。
* @param pose2 终点位姿。
* @param outputPoses 输出轨迹点集合。
* @param npoint 轨迹点数量,传 0 表示自动计算。
* @return 实际生成的轨迹点数量,失败返回 -1
*/
int trajectoryPlanning(const std::vector<double> &pose1, const std::vector<double> &pose2,
std::vector<std::vector<double>> &outputPoses, int npoint = 0);
// 锟斤拷锟竭猴拷锟斤拷
/**
* @brief 锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷元锟斤拷之锟斤拷慕嵌炔锟<E78294>
* @param q1 锟斤拷元锟斤拷1 [qx, qy, qz, qw]
* @param q2 锟斤拷元锟斤拷2 [qx, qy, qz, qw]
* @return 锟角度差(锟斤拷锟饺o拷
*/
// static double quaternionAngleDifference(const double q1[4], const double q2[4]);
/**
* @brief 锟皆讹拷锟斤拷锟斤拷旒拷婊<E68BB7>锟斤拷锟斤拷锟斤拷锟<E68BB7>
* @param startPose 锟斤拷始位锟斤拷 [x, y, z, qx, qy, qz, qw]
* @param endPose 锟斤拷止位锟斤拷 [x, y, z, qx, qy, qz, qw]
* @param minSteps 锟斤拷小锟斤拷锟斤拷
* @param maxSteps 锟斤拷锟斤拷锟<E68BB7>
* @param positionResolution 位锟矫分憋拷锟绞o拷锟阶o拷
* @param orientationResolution 锟斤拷态锟街憋拷锟绞o拷锟斤拷锟饺o拷
* @return 锟狡硷拷锟侥轨迹锟斤拷锟斤拷锟斤拷
*/
// static int calculateAutoSteps(const double startPose[7], const double endPose[7],
// int minSteps = 10, int maxSteps = 100,
// double positionResolution = 0.01,
// double orientationResolution = 0.1);
// 锟斤拷取锟斤拷锟斤拷锟斤拷锟斤拷息
// const KDL::Chain& getKinematicChain() const { return kinematicChain; }
// int getNumberOfJoints() const { return kinematicChain.getNrOfJoints(); }
int calculateAutoSteps(const std::vector<double> &startPose, const std::vector<double> &endPose,
int minSteps, int maxSteps,
double positionResolution, double orientationResolution);
double quaternionAngleDifference(const std::vector<double> &q1, const std::vector<double> &q2);
/**
* @brief 锟斤拷锟斤拷锟剿讹拷学锟斤拷锟<EFBFBD> (LMA锟斤拷锟斤拷) - 锟斤拷锟斤拷vector锟芥本
* @param pose 目锟斤拷位锟斤拷 [x, y, z, qx, qy, qz, qw]
* @param iniJ 锟斤拷始锟截节角讹拷 [6锟斤拷锟截节角讹拷]
* @param eps 锟斤拷锟斤拷锟斤拷锟斤拷
* @param maxiter 锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷
* @param eps_joints 锟截斤拷锟捷诧拷
* @return 锟截节角讹拷vector
* @brief 使用 LMA 求解器计算逆解,输入为数组。
*/
std::vector<double> inverse(const double pose[7], const double iniJ[6],
double eps = 1e-8, int maxiter = 3000,
double eps_joints = 1e-12);
/**
* @brief 使用 LMA 求解器计算逆解,输入为向量。
*/
std::vector<double> inverse(const std::vector<double> &pose,
const std::vector<double> &iniJ,
double eps = 1e-8, int maxiter = 3000,
double eps_joints = 1e-12);
/**
* @brief 使用默认零位初值计算逆解,输入为数组。
*/
std::vector<double> inverse(const double pose[7],
double eps = 1e-8, int maxiter = 3000,
double eps_joints = 1e-12);
/**
* @brief 使用默认零位初值计算逆解,输入为向量。
*/
std::vector<double> inverse(const std::vector<double> &pose,
double eps = 1e-8, int maxiter = 3000,
double eps_joints = 1e-12);
};
#endif // ROBOT_H
#endif // ROBOT_H

View File

@@ -1,37 +1,33 @@
#ifndef UTILS_H
#define UTILS_H
#include <string>
#include <chrono>
#include "nlohmann/json.hpp"
#include <string>
// // ǰ<><C7B0><EFBFBD><EFBFBD><EFBFBD><EFBFBD> httplib <20><> Response <20><>
// namespace httplib {
// class Response;
// }
#include "nlohmann/json.hpp"
using json = nlohmann::json;
namespace utils
{
// ʱ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>غ<EFBFBD><EFBFBD><EFBFBD>
// 时间相关工具。
std::string get_current_time();
std::string get_current_timestamp();
// JSON <EFBFBD><EFBFBD>Ӧ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
// 通用 JSON 响应构造。
json create_error_response(int code, const std::string &message, const std::string &details = "");
json create_success_response(const json &data = {}, const std::string &message = "Success");
// UTF-8 <EFBFBD><EFBFBD>֤
// UTF-8 校验与清洗。
bool is_valid_utf8(const std::string &str);
std::string sanitize_utf8(const std::string &str);
// API <EFBFBD><EFBFBD>Ӧ<EFBFBD><EFBFBD>ʽ
// 业务 API 响应格式。
json create_api_response(bool success, int code, const std::string &msg,
const std::string &req_code = "", const std::string &req_from = "",
const std::string &req_cmd = "", const json &res_data = json::object());
// Base64 <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>URDF<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
// Base64 与 URDF 处理。
std::string base64_decode(const std::string &encoded_string);
std::string base64_to_urdf(const std::string &base64_urdf);
bool save_base64_urdf_to_file(const std::string &base64_urdf, const std::string &filename);
@@ -39,35 +35,34 @@ namespace utils
bool save_urdf_string_to_file(const std::string &urdf_content, const std::string &filename);
} // namespace utils
class StringUtils
{
public:
/**
* @brief 将包含个分隔符的字符串拆分两部分
* @param str 要拆分的字符串
* @param delimiter 分隔符,默认为'_'
* @return std::pair<std::string, std::string> 包含两个部分的pair
* 第一个元素是分隔符前的部分,第二个是分隔符后的部分
* 如果未找到分隔符,第二个元素为空字符串
* @brief 将包含个分隔符的字符串拆分两部分
* @param str 要拆分的字符串
* @param delimiter 分隔符,默认为 `_`。
* @return 第一个元素为分隔符前的内容,第二个元素为分隔符后的内容。
*/
static std::pair<std::string, std::string> splitByDelimiter(
const std::string &str,
char delimiter = '_');
/**
* @brief 专门拆分"A_B"格式的字符串
* @param str 要拆分的字符串
* @return std::pair<std::string, std::string> 包含两个部分的pair
* @brief 专门拆分 `A_B` 形式的字符串
* @param str 要拆分的字符串
* @return 拆分后的两段内容。
*/
static std::pair<std::string, std::string> splitA_B(const std::string &str);
/**
* @brief 将字符串按分隔符拆成两个部分,支持引用参数返回
* @param str 要拆分的字符串
* @param part1 返回第一部分
* @param part2 返回第二部分
* @param delimiter 分隔符,默认为'_'
* @return bool 如果找到分隔符返回true否则返回false
* @brief 将字符串按分隔符拆成两段,并通过引用返回结果。
* @param str 要拆分的字符串
* @param part1 返回第一段。
* @param part2 返回第二段。
* @param delimiter 分隔符,默认为 `_`。
* @return 找到分隔符返回 true否则返回 false
*/
static bool splitToTwoParts(
const std::string &str,
@@ -76,14 +71,15 @@ public:
char delimiter = '_');
/**
* @brief 严格拆分,必须包含且只包含一个分隔符
* @param str 要拆分的字符串
* @param delimiter 分隔符,默认为'_'
* @return std::pair<std::string, std::string> 包含两个部分的pair
* @throws std::invalid_argument 如果没有找到分隔符或找到多个分隔符
* @brief 严格拆分字符串,要求只出现一个分隔符
* @param str 要拆分的字符串
* @param delimiter 分隔符,默认为 `_`。
* @return 拆分后的两段内容。
* @throws std::invalid_argument 当分隔符不存在或出现多次时抛出异常。
*/
static std::pair<std::string, std::string> splitStrict(
const std::string &str,
char delimiter = '_');
};
#endif // UTILS_H
#endif // UTILS_H

1816
public/fourbar_test.html Normal file

File diff suppressed because it is too large Load Diff

760
public/index.html Normal file
View File

@@ -0,0 +1,760 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>smart_wasm Local Debug</title>
<style>
:root {
--bg: #f3efe5;
--panel: #fffaf0;
--line: #d2c7b3;
--text: #1f2a30;
--muted: #6e7a80;
--accent: #0d6b5f;
--accent-2: #d7852f;
--error: #b7402a;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
color: var(--text);
background:
radial-gradient(
circle at top left,
rgba(215, 133, 47, 0.18),
transparent 30%
),
linear-gradient(135deg, #f7f1e3 0%, #e8efe9 100%);
min-height: 100vh;
}
.shell {
max-width: 1100px;
margin: 0 auto;
padding: 32px 20px 48px;
}
.hero {
display: grid;
gap: 16px;
margin-bottom: 24px;
}
.hero h1 {
margin: 0;
font-size: clamp(28px, 5vw, 48px);
line-height: 1.05;
}
.hero p {
margin: 0;
color: var(--muted);
max-width: 720px;
font-size: 16px;
}
.layout {
display: grid;
grid-template-columns: 1.05fr 0.95fr;
gap: 20px;
}
.card {
background: rgba(255, 250, 240, 0.9);
backdrop-filter: blur(10px);
border: 1px solid var(--line);
border-radius: 20px;
box-shadow: 0 20px 40px rgba(38, 50, 56, 0.08);
overflow: hidden;
}
.card-head {
padding: 18px 20px;
border-bottom: 1px solid var(--line);
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.card-head h2 {
margin: 0;
font-size: 18px;
}
.status {
font-size: 13px;
color: var(--muted);
padding: 6px 10px;
border: 1px solid var(--line);
border-radius: 999px;
background: rgba(255, 255, 255, 0.7);
}
.body {
padding: 20px;
}
label {
display: block;
font-size: 13px;
color: var(--muted);
margin-bottom: 8px;
}
textarea {
width: 100%;
min-height: 320px;
resize: vertical;
border: 1px solid var(--line);
border-radius: 14px;
padding: 14px;
font:
13px/1.5 Consolas,
'Courier New',
monospace;
color: var(--text);
background: #fff;
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 16px;
}
.sample-bar {
display: grid;
gap: 10px;
margin-top: 16px;
padding: 14px;
border: 1px solid var(--line);
border-radius: 14px;
background: rgba(255, 255, 255, 0.72);
}
.sample-row {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
select {
width: 100%;
border: 1px solid var(--line);
border-radius: 12px;
padding: 11px 12px;
font-size: 14px;
color: var(--text);
background: #fff;
}
button {
border: 0;
border-radius: 12px;
padding: 12px 16px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
}
.primary {
background: var(--accent);
color: #fff;
}
.secondary {
background: #fff;
color: var(--text);
border: 1px solid var(--line);
}
pre {
margin: 0;
min-height: 420px;
padding: 16px;
overflow: auto;
background: #172126;
color: #e8f2ef;
font:
13px/1.5 Consolas,
'Courier New',
monospace;
}
.response-shell {
display: grid;
gap: 0;
}
.response-meta {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
padding: 16px 20px;
background: rgba(17, 30, 35, 0.04);
border-bottom: 1px solid var(--line);
}
.response-meta-item {
padding: 10px 12px;
border: 1px solid rgba(210, 199, 179, 0.8);
border-radius: 12px;
background: rgba(255, 255, 255, 0.75);
}
.response-meta-label {
font-size: 12px;
color: var(--muted);
margin-bottom: 4px;
}
.response-meta-value {
font-size: 14px;
font-weight: 600;
color: var(--text);
word-break: break-all;
}
.response-meta-value.is-ok {
color: var(--accent);
}
.response-meta-value.is-error {
color: var(--error);
}
.json-key {
color: #7dd3fc;
}
.json-string {
color: #f9d65c;
}
.json-number {
color: #f59e0b;
}
.json-boolean {
color: #fb7185;
font-weight: 700;
}
.json-null {
color: #c084fc;
font-weight: 700;
}
.tips {
display: grid;
gap: 10px;
margin-top: 18px;
color: var(--muted);
font-size: 13px;
}
.tips code {
color: var(--accent);
}
@media (max-width: 900px) {
.layout {
grid-template-columns: 1fr;
}
textarea,
pre {
min-height: 260px;
}
.response-meta {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div style="width: 200px; height: 300px">
<iframe
id="ModelLibraryPanel_iframe"
title="模型库"
src="https://www.claudedt.com/dmtModelLibrary"
style="width: 100%; height: 100%; border: 0; display: block"
></iframe>
</div>
<div class="shell">
<section class="hero">
<h1>smart_wasm 本地调试页</h1>
<p>
这个页面直接加载
<code>public/wasm/smart_math.js</code>,可以在本地验证 WASM
初始化、命令调用和返回 JSON。
</p>
</section>
<section class="layout">
<div class="card">
<div class="card-head">
<h2>请求</h2>
<div class="status" id="module-status">WASM 未初始化</div>
</div>
<div class="body">
<label for="request">JSON Request</label>
<textarea id="request">
{
"msg": "list robots after init",
"req_code": "CASE_LIST_001",
"req_from": "local_debug",
"req_cmd": "Cmd_ListRobots",
"req_param": {}
}</textarea
>
<div class="sample-bar">
<label for="sample-select">接口测试样例</label>
<div class="sample-row">
<select id="sample-select">
<option value="listRobots">机器人列表 Cmd_ListRobots</option>
<option value="kinematicsForwardZero">
运动学正解 Cmd_Kinematics_forward_pose_str
</option>
<option value="kinematicsForwardAllJoints">
全关节正解 Cmd_Kinematics_forward_all_joints
</option>
<option value="spcBasic">SPC 计算 Cmd_Spc</option>
<option value="fourBarValid">
四连杆有效样例 Cmd_FourBar_CrankSlider
</option>
<option value="fourBarInvalid">
四连杆异常样例 Cmd_FourBar_CrankSlider
</option>
<option value="quadrupedPoints">
四足姿态点位
Cmd_QuadrupedRobot_CalculateAllPointsFromMotorAngles
</option>
<option value="quadrupedForwardGait">
四足步态正解 Cmd_QuadrupedRobot_PerformForwardKinematics
</option>
</select>
</div>
<div class="sample-row">
<button class="secondary" id="sample-btn" type="button">
填充选中样例
</button>
</div>
</div>
<div class="actions">
<button class="primary" id="init-btn" type="button">
初始化 WASM
</button>
<button class="primary" id="run-btn" type="button">
发送请求
</button>
<button class="secondary" id="clear-btn" type="button">
清空输出
</button>
</div>
<div class="tips">
<div>
1. 先点 <code>初始化 WASM</code>,会调用
<code>_init_func()</code>
</div>
<div>
2. 再点 <code>发送请求</code>,会调用 <code>_func()</code>
</div>
<div>3. 默认地址使用 <code>http://localhost:8080/</code></div>
</div>
</div>
</div>
<div class="card">
<div class="card-head">
<h2>响应</h2>
<div class="status" id="request-status">等待操作</div>
</div>
<div class="response-shell">
<div class="response-meta" id="response-meta">
<div class="response-meta-item">
<div class="response-meta-label">状态</div>
<div class="response-meta-value" id="meta-success">未执行</div>
</div>
<div class="response-meta-item">
<div class="response-meta-label">代码</div>
<div class="response-meta-value" id="meta-code">-</div>
</div>
<div class="response-meta-item">
<div class="response-meta-label">命令</div>
<div class="response-meta-value" id="meta-cmd">-</div>
</div>
<div class="response-meta-item">
<div class="response-meta-label">请求号</div>
<div class="response-meta-value" id="meta-req">-</div>
</div>
</div>
<pre id="output">No output yet.</pre>
</div>
</div>
</section>
</div>
<script src="./wasm/smart_math.js"></script>
<script>
let wasmModule = null;
const moduleStatus = document.getElementById('module-status');
const requestStatus = document.getElementById('request-status');
const requestBox = document.getElementById('request');
const output = document.getElementById('output');
const sampleSelect = document.getElementById('sample-select');
const metaSuccess = document.getElementById('meta-success');
const metaCode = document.getElementById('meta-code');
const metaCmd = document.getElementById('meta-cmd');
const metaReq = document.getElementById('meta-req');
const requestSamples = {
listRobots: {
msg: 'list robots after init',
req_code: 'CASE_LIST_001',
req_from: 'local_debug',
req_cmd: 'Cmd_ListRobots',
req_param: {
detail: true,
},
},
kinematicsForwardZero: {
msg: 'fk zero pose',
req_code: 'CASE_FK_001',
req_from: 'local_debug',
req_cmd: 'Cmd_Kinematics_forward_pose_str',
req_param: {
robot_uuid: 'abb_irb120_3_58',
q_init_str: '0,0,0,0,0,0',
},
},
kinematicsForwardAllJoints: {
msg: 'fk all joints path',
req_code: 'CASE_FK_ALL_001',
req_from: 'local_debug',
req_cmd: 'Cmd_Kinematics_forward_all_joints',
req_param: {
robot_uuid: 'abb_irb120_3_58',
joints_str: '0,0,0,0,0,0;10,20,30,40,50,60',
},
},
spcBasic: {
msg: 'spc basic 5x30',
req_code: 'CASE_SPC_001',
req_from: 'local_debug',
req_cmd: 'Cmd_Spc',
req_param: {
n: 5,
k: 30,
usl: 1.7,
lsl: 1.5,
x: [
1.55, 1.58, 1.61, 1.6, 1.6, 1.58, 1.63, 1.63, 1.62, 1.63, 1.62,
1.63, 1.62, 1.59, 1.58, 1.58, 1.6, 1.61, 1.62, 1.63, 1.58, 1.64,
1.63, 1.62, 1.62, 1.62, 1.62, 1.63, 1.61, 1.57, 1.64, 1.62, 1.61,
1.6, 1.58, 1.57, 1.59, 1.61, 1.62, 1.63, 1.58, 1.61, 1.6, 1.62,
1.63, 1.6, 1.61, 1.64, 1.64, 1.63, 1.58, 1.6, 1.62, 1.63, 1.65,
1.62, 1.58, 1.59, 1.57, 1.58, 1.57, 1.57, 1.58, 1.59, 1.64, 1.61,
1.64, 1.62, 1.6, 1.59, 1.65, 1.62, 1.62, 1.6, 1.58, 1.57, 1.59,
1.57, 1.59, 1.62, 1.56, 1.57, 1.57, 1.61, 1.62, 1.56, 1.58, 1.59,
1.6, 1.62, 1.58, 1.6, 1.6, 1.62, 1.63, 1.58, 1.59, 1.6, 1.63,
1.62, 1.58, 1.59, 1.62, 1.63, 1.64, 1.58, 1.59, 1.62, 1.63, 1.61,
1.58, 1.59, 1.6, 1.61, 1.63, 1.57, 1.59, 1.61, 1.61, 1.62, 1.58,
1.58, 1.6, 1.61, 1.63, 1.62, 1.58, 1.58, 1.58, 1.57, 1.63, 1.59,
1.57, 1.58, 1.57, 1.58, 1.62, 1.61, 1.63, 1.61, 1.58, 1.57, 1.59,
1.6, 1.62, 1.62, 1.6, 1.6, 1.57, 1.57,
],
},
},
fourBarValid: {
msg: 'fourbar valid',
req_code: 'CASE_FB_001',
req_from: 'local_debug',
req_cmd: 'Cmd_FourBar_CrankSlider',
req_param: {
L_AB: 0.5,
L_BS: 2.0,
S_OFS: 0.0,
angleDeg: 45.0,
},
},
fourBarInvalid: {
msg: 'fourbar invalid',
req_code: 'CASE_FB_002',
req_from: 'local_debug',
req_cmd: 'Cmd_FourBar_CrankSlider',
req_param: {
L_AB: 0.0,
L_BS: 0.2,
S_OFS: 0.5,
angleDeg: 30.0,
},
},
quadrupedPoints: {
msg: 'quadruped points from motor angles',
req_code: 'CASE_QP_001',
req_from: 'local_debug',
req_cmd: 'Cmd_QuadrupedRobot_CalculateAllPointsFromMotorAngles',
req_param: {
Param: {
LF: {
thigh_angle_deg: -37.4784578338516,
shank_angle_deg: -22.4917532451628,
ankle_angle_deg: 0.0,
},
LH: {
thigh_angle_deg: -34.0564623655754,
shank_angle_deg: -22.1353106546916,
ankle_angle_deg: 0.0,
},
RF: {
thigh_angle_deg: 34.0564623655754,
shank_angle_deg: 22.1353106546916,
ankle_angle_deg: 0.0,
},
RH: {
thigh_angle_deg: 37.4784578338516,
shank_angle_deg: 22.4917532451628,
ankle_angle_deg: 0.0,
},
},
},
},
quadrupedForwardGait: {
msg: 'quadruped forward gait',
req_code: 'CASE_QP_002',
req_from: 'local_debug',
req_cmd: 'Cmd_QuadrupedRobot_PerformForwardKinematics',
req_param: {
GaitInfo: {
GaitType: 'trot',
Period: 0.8,
Frequency: 50.0,
StepTime: 0.02,
SupportTime: 0.4,
SwingTime: 0.4,
SupportRatio: 0.5,
SwingRatio: 0.5,
TotalFrames: 40,
TotalTime: 0.8,
},
SystemParameters: {
A_x: 0.0,
A_y: 500.0,
L10: 300.0,
L20: 286.0,
L30: 55.0,
StepLength: 60.0,
StepHeight: 50.0,
X: 30.0,
DeltaX: 0.0,
L21: 70.0,
L22: 292.0,
L23: 80.0,
Beta1: 166.0,
BB2_BC_Angle: 3.3859387489,
C1_B_Length: 107.0,
CC1: 179.0,
L31: 32.0,
C2C3_Length: 32.0,
Lead: 10.0,
D1_L_Offset: 90.0,
D2_L_Offset: 50.0,
C4_D2_Offset: 18.0,
ThighMotorReduction: 1.0,
ShankMotorReduction: 1.0,
AnkleMotorReduction: 1.0,
},
},
},
};
function escapeHtml(value) {
return value
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;');
}
function resetMeta() {
metaSuccess.textContent = '未执行';
metaSuccess.className = 'response-meta-value';
metaCode.textContent = '-';
metaCode.className = 'response-meta-value';
metaCmd.textContent = '-';
metaReq.textContent = '-';
}
function setMetaFromJson(data) {
const success =
data && typeof data.success === 'boolean' ? data.success : null;
metaSuccess.textContent =
success === null ? '未知' : success ? '成功' : '失败';
metaSuccess.className =
'response-meta-value ' +
(success ? 'is-ok' : success === false ? 'is-error' : '');
metaCode.textContent = data && 'code' in data ? String(data.code) : '-';
metaCode.className =
'response-meta-value ' + (success === false ? 'is-error' : '');
metaCmd.textContent = data && data.req_cmd ? data.req_cmd : '-';
metaReq.textContent = data && data.req_code ? data.req_code : '-';
}
function highlightJson(value) {
const escaped = escapeHtml(value);
return escaped.replace(
/("(?:\\u[a-fA-F0-9]{4}|\\[^u]|[^\\"])*")(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d+)?(?:[eE][+\-]?\d+)?/g,
(match, stringToken, keySuffix, literalToken) => {
if (stringToken) {
if (keySuffix) {
return `<span class="json-key">${stringToken}</span>${keySuffix}`;
}
return `<span class="json-string">${stringToken}</span>`;
}
if (literalToken === 'true' || literalToken === 'false') {
return `<span class="json-boolean">${match}</span>`;
}
if (literalToken === 'null') {
return `<span class="json-null">${match}</span>`;
}
return `<span class="json-number">${match}</span>`;
},
);
}
function renderOutput(value) {
if (typeof value !== 'string') {
const jsonText = JSON.stringify(value, null, 2);
output.innerHTML = highlightJson(jsonText);
setMetaFromJson(value);
return;
}
try {
const parsed = JSON.parse(value);
renderOutput(parsed);
} catch {
resetMeta();
output.textContent = value;
}
}
function allocString(value) {
const size = wasmModule.lengthBytesUTF8(value) + 1;
const ptr = wasmModule._malloc(size);
wasmModule.stringToUTF8(value, ptr, size);
return ptr;
}
function loadSelectedSample() {
const selectedKey = sampleSelect.value;
const sample = requestSamples[selectedKey];
if (!sample) {
return;
}
requestBox.value = JSON.stringify(sample, null, 2);
}
async function ensureModule() {
if (wasmModule) {
return wasmModule;
}
requestStatus.textContent = '正在加载模块';
wasmModule = await createSmartMathModule();
moduleStatus.textContent = 'WASM 已加载';
requestStatus.textContent = '模块加载完成';
return wasmModule;
}
async function initializeBusinessApi() {
const mod = await ensureModule();
let responsePtr = null;
try {
responsePtr = mod._init_func();
const text = mod.UTF8ToString(responsePtr);
renderOutput(text);
moduleStatus.textContent = '业务接口已初始化';
requestStatus.textContent = '初始化成功';
} catch (error) {
moduleStatus.textContent = '初始化失败';
requestStatus.textContent = '初始化失败';
resetMeta();
output.textContent = String(
error && error.stack ? error.stack : error,
);
} finally {
if (responsePtr) {
mod._smart_free_string(responsePtr);
}
}
}
async function runRequest() {
const mod = await ensureModule();
let requestPtr = null;
let responsePtr = null;
try {
const raw = requestBox.value.trim();
JSON.parse(raw);
requestPtr = allocString(raw);
responsePtr = mod._func(requestPtr);
const text = mod.UTF8ToString(responsePtr);
renderOutput(text);
requestStatus.textContent = '请求成功';
} catch (error) {
requestStatus.textContent = '请求失败';
resetMeta();
output.textContent = String(
error && error.stack ? error.stack : error,
);
} finally {
if (requestPtr) {
mod._free(requestPtr);
}
if (responsePtr) {
mod._smart_free_string(responsePtr);
}
}
}
document
.getElementById('init-btn')
.addEventListener('click', initializeBusinessApi);
document.getElementById('run-btn').addEventListener('click', runRequest);
document
.getElementById('sample-btn')
.addEventListener('click', loadSelectedSample);
document.getElementById('clear-btn').addEventListener('click', () => {
requestStatus.textContent = '等待操作';
resetMeta();
output.textContent = 'No output yet.';
});
resetMeta();
loadSelectedSample();
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

11
public/smart_test.html Normal file
View File

@@ -0,0 +1,11 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta http-equiv="refresh" content="0; url=./index.html" />
<title>Redirecting</title>
</head>
<body>
<p>Redirecting to <a href="./index.html">index.html</a> ...</p>
</body>
</html>

1681
public/spc_test.html Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -74,7 +74,11 @@ $startTime = Get-Date
# 编译命令 - 使用当前真实业务模块源码
$compileResult = emcc `
src/KinematicsWebAPI.cpp `
src/api/KinematicsWebAPI.Core.cpp `
src/api/KinematicsWebAPI.RobotCommands.cpp `
src/api/KinematicsWebAPI.SpcCommands.cpp `
src/api/KinematicsWebAPI.FourBarCommands.cpp `
src/api/KinematicsWebAPI.QuadrupedCommands.cpp `
src/main.cpp `
src/Robot.cpp `
src/spc_core.cpp `
@@ -129,6 +133,7 @@ $compileResult = emcc `
-s USE_PTHREADS=0 `
-O3 `
-Wno-deprecated-literal-operator `
-Wno-deprecated-declarations `
-o public/wasm/smart_math.js 2>&1
$endTime = Get-Date

View File

@@ -142,6 +142,91 @@ function buildInverseRequest(command, robotUuid, poseStr, qInitStr, extra = {})
};
}
function buildSingularityRequest(robotUuid, joints) {
return {
msg: "singularity check",
req_code: "AUTO_SINGULARITY",
req_from: "wasm_test",
req_cmd: "Cmd_Kinematics_check_singularity",
req_param: {
robot_uuid: robotUuid,
joints_str: jointArrayToString(joints),
},
};
}
// 构造 link 名称被整体替换的 URDF用于验证初始化能自动推导运动学链。
function buildRenamedUrdfFromDocs() {
const urdfPath = path.join(rootDir, "docs", "urdf.xml");
return fs
.readFileSync(urdfPath, "utf8")
.replaceAll("base_link-base", "renamed_root_link-renamed_world")
.replaceAll("base_link", "renamed_root_link")
.replaceAll("tool0", "renamed_tcp")
.replaceAll('link name="base"', 'link name="renamed_world"')
.replaceAll('parent link="base"', 'parent link="renamed_world"');
}
// 注册改名后的 URDF 并执行一次 FK确认底层不再依赖固定 base/tool0。
async function runRenamedUrdfChainCase(module) {
const robotUuid = "renamed_chain_robot";
const urdfBase64 = Buffer.from(buildRenamedUrdfFromDocs(), "utf8").toString("base64");
const initResponse = callBusinessApi(module, {
msg: "init renamed urdf",
req_code: "AUTO_RENAMED_INIT",
req_from: "wasm_test",
req_cmd: "Cmd_InitRobot",
req_param: {
robot_uuid: robotUuid,
urdf_base64: urdfBase64,
force_update: true,
},
});
ensure(initResponse.success === true, "renamed URDF init request failed");
ensure(getByPath(initResponse, "res_data.success") === true, "renamed URDF init result is not successful");
const forwardResponse = callBusinessApi(module, buildForwardRequest(robotUuid, [0, 0, 0, 0, 0, 0]));
ensure(forwardResponse.success === true, "renamed URDF forward request failed");
ensure(getByPath(forwardResponse, "res_data.success") === true, "renamed URDF forward result is not successful");
ensure(Array.isArray(getByPath(forwardResponse, "res_data.position")), "renamed URDF forward position is missing");
return {
details: {
position: getByPath(forwardResponse, "res_data.position"),
orientation: getByPath(forwardResponse, "res_data.orientation"),
},
};
}
// 发送超过 URDF 关节上下限的 FK 请求,确认底层会拒绝计算。
async function runJointLimitCase(module) {
const response = callBusinessApi(module, buildForwardRequest("abb_irb120_3_58", [7, 0, 0, 0, 0, 0]));
ensure(response.success === false, "joint limit case should fail at response level");
ensure(String(getByPath(response, "res_data.error")).includes("Forward kinematics calculation failed"), "joint limit error is missing");
return response;
}
// 检查奇异点分析接口会返回完整的雅可比 SVD 指标。
async function runSingularityCheckCase(module) {
const response = callBusinessApi(module, buildSingularityRequest("abb_irb120_3_58", [0.15, -0.25, 0.35, 0.1, -0.2, 0.3]));
ensure(response.success === true, "singularity check request failed");
ensure(getByPath(response, "res_data.success") === true, "singularity check result is not successful");
ensure(Array.isArray(getByPath(response, "res_data.singular_values")), "singular values are missing");
ensure(getByPath(response, "res_data.singular_values").length === 6, "singular value count should be 6");
ensure(typeof getByPath(response, "res_data.condition_number") === "number", "condition number is missing");
ensure(typeof getByPath(response, "res_data.risk_level") === "string", "risk level is missing");
return {
details: {
riskLevel: getByPath(response, "res_data.risk_level"),
rank: getByPath(response, "res_data.rank"),
minSingularValue: getByPath(response, "res_data.min_singular_value"),
conditionNumber: getByPath(response, "res_data.condition_number"),
},
};
}
function assertStaticCase(response, assertions) {
for (const assertion of assertions) {
const actual = getByPath(response, assertion.path);
@@ -353,6 +438,18 @@ const suite = [
{ path: "res_data.OPERATION.OPERATION.frames", lengthEquals: 2 },
],
},
{
id: "kinematics_renamed_urdf_chain",
type: "renamed_urdf_chain",
},
{
id: "kinematics_joint_limit_rejects_fk",
type: "joint_limit",
},
{
id: "kinematics_singularity_check",
type: "singularity_check",
},
{
id: "spc_basic_5x30",
type: "static",
@@ -366,6 +463,17 @@ const suite = [
{ path: "res_data.Cpk.Cpk", gte: 0.1 },
],
},
{
id: "spc_invalid_count",
type: "static",
requestFile: "tests/testdata/spc/invalid_count.json",
assertions: [
{ path: "success", equals: false },
{ path: "code", equals: 1000 },
{ path: "res_data.error", includes: "SPC calculation failed" },
{ path: "res_data.error", includes: "数据个数不匹配" },
],
},
{
id: "fourbar_valid",
type: "static",
@@ -383,12 +491,95 @@ const suite = [
type: "static",
requestFile: "tests/testdata/fourbar/crank_slider_invalid.json",
assertions: [
{ path: "success", equals: true },
{ path: "success", equals: false },
{ path: "code", equals: 1000 },
{ path: "res_data.success", equals: false },
{ path: "res_data.error", includes: "Invalid parameters" },
{ path: "res_data.validation_errors", lengthGte: 1 },
],
},
{
id: "fourbar_simulate",
type: "static",
requestFile: "tests/testdata/fourbar/crank_slider_simulate.json",
assertions: [
{ path: "success", equals: true },
{ path: "res_data.success", equals: true },
{ path: "res_data.frame_count", equals: 5 },
{ path: "res_data.frames", lengthEquals: 5 },
{ path: "res_data.frames.0.angleDeg", equals: 0 },
{ path: "res_data.frames.4.angleDeg", equals: 180 },
{ path: "res_data.frames.0.points.B.x", exists: true },
{ path: "res_data.frames.0.poses.TCP.tx", exists: true },
{ path: "res_data.trajectory", lengthEquals: 5 },
{ path: "res_data.slider_trajectory", lengthEquals: 5 },
],
},
{
id: "fourbar_unified_rrrp",
type: "static",
requestFile: "tests/testdata/fourbar/simulate_rrrp.json",
assertions: [
{ path: "success", equals: true },
{ path: "res_data.success", equals: true },
{ path: "res_data.mechanismType", equals: "RRRP" },
{ path: "res_data.frame_count", equals: 5 },
{ path: "res_data.frames", lengthEquals: 5 },
{ path: "res_data.frames.0.points.B.x", exists: true },
{ path: "res_data.frames.0.points.S.x", exists: true },
{ path: "res_data.trajectory", lengthEquals: 5 },
{ path: "res_data.slider_trajectory", lengthEquals: 5 },
],
},
{
id: "fourbar_unified_prrr",
type: "static",
requestFile: "tests/testdata/fourbar/simulate_prrr.json",
assertions: [
{ path: "success", equals: true },
{ path: "res_data.success", equals: true },
{ path: "res_data.mechanismType", equals: "PRRR" },
{ path: "res_data.inputName", equals: "sliderX" },
{ path: "res_data.frame_count", equals: 5 },
{ path: "res_data.frames.0.points.B.x", exists: true },
{ path: "res_data.frames.0.points.S.x", exists: true },
{ path: "res_data.trajectory", lengthEquals: 5 },
{ path: "res_data.slider_trajectory", lengthEquals: 5 },
{ path: "res_data.alternative_trajectory", lengthEquals: 5 },
],
},
{
id: "fourbar_unified_rprr",
type: "static",
requestFile: "tests/testdata/fourbar/simulate_rprr.json",
assertions: [
{ path: "success", equals: true },
{ path: "res_data.success", equals: true },
{ path: "res_data.mechanismType", equals: "RPRR" },
{ path: "res_data.frame_count", equals: 5 },
{ path: "res_data.frames.0.points.O.x", exists: true },
{ path: "res_data.frames.0.points.B.x", exists: true },
{ path: "res_data.trajectory", lengthEquals: 5 },
{ path: "res_data.crank_circle", lengthEquals: 5 },
{ path: "res_data.rocker_trajectory", lengthEquals: 5 },
],
},
{
id: "fourbar_unified_rrrr",
type: "static",
requestFile: "tests/testdata/fourbar/simulate_rrrr.json",
assertions: [
{ path: "success", equals: true },
{ path: "res_data.success", equals: true },
{ path: "res_data.mechanismType", equals: "RRRR" },
{ path: "res_data.frame_count", equals: 5 },
{ path: "res_data.frames.0.points.A.x", exists: true },
{ path: "res_data.frames.0.points.D.x", exists: true },
{ path: "res_data.trajectory", lengthEquals: 5 },
{ path: "res_data.trajectory_c", lengthEquals: 5 },
{ path: "res_data.crank_circle", lengthEquals: 5 },
],
},
{
id: "quadruped_points_from_motor_angles",
type: "static",
@@ -428,6 +619,12 @@ async function runCase(module, testCase) {
return runRoundtripSingleCase(module, testCase);
case "roundtrip_path":
return runRoundtripPathCase(module, testCase);
case "renamed_urdf_chain":
return runRenamedUrdfChainCase(module, testCase);
case "joint_limit":
return runJointLimitCase(module, testCase);
case "singularity_check":
return runSingularityCheckCase(module, testCase);
default:
throw new Error(`Unknown test type: ${testCase.type}`);
}

View File

@@ -1,67 +1,55 @@
# serve.ps1 - 开发服务器启动脚本
Write-Host "=== 启动开发服务器 ===" -ForegroundColor Cyan
# serve.ps1 - development server launcher
Write-Host "=== Start Dev Server ===" -ForegroundColor Cyan
Write-Host ""
# 检查是否已构建
if (-not (Test-Path "public/wasm/smart_math.js")) {
Write-Host "[!] 未找到 WASM 文件,正在构建..." -ForegroundColor Yellow
Write-Host "[!] WASM output not found, building first..." -ForegroundColor Yellow
& "$PSScriptRoot\build.ps1"
if (-not (Test-Path "public/wasm/smart_math.js")) {
Write-Host "[] 构建失败,无法启动服务器" -ForegroundColor Red
Write-Host "[x] Build failed, cannot start server" -ForegroundColor Red
exit 1
}
}
# 检查 Python
try {
python --version 2>&1 | Out-Null
$hasPython = $true
} catch {
$hasPython = $false
}
$pythonCommand = $null
$pythonCandidates = @(
"python",
"py",
"python3",
"C:\Python39\python.exe",
"C:\Python38\python.exe",
"C:\Python37\python.exe"
)
if (-not $hasPython) {
Write-Host "[!] 未找到 Python尝试使用系统 Python..." -ForegroundColor Yellow
# 尝试常见的 Python 路径
$pythonPaths = @(
"python",
"python3",
"py",
"C:\Python39\python.exe",
"C:\Python38\python.exe",
"C:\Python37\python.exe"
)
foreach ($path in $pythonPaths) {
try {
& $path --version 2>&1 | Out-Null
$env:PATH = "$(Split-Path $path -ErrorAction SilentlyContinue);$env:PATH"
$hasPython = $true
Write-Host "[✓] 找到 Python: $path" -ForegroundColor Green
foreach ($candidate in $pythonCandidates) {
try {
& $candidate --version 2>&1 | Out-Null
if ($LASTEXITCODE -eq 0) {
$pythonCommand = $candidate
Write-Host "[ok] Python found: $candidate" -ForegroundColor Green
break
} catch {
continue
}
} catch {
continue
}
}
if (-not $hasPython) {
Write-Host "[✗] 未找到 Python请安装 Python 3.7+ 并添加到 PATH" -ForegroundColor Red
if (-not $pythonCommand) {
Write-Host "[x] Python 3.7+ not found in PATH" -ForegroundColor Red
exit 1
}
# 启动服务器
Write-Host "[ ] 启动开发服务器..." -ForegroundColor Cyan
Write-Host ""
Write-Host "🌐 服务器信息:" -ForegroundColor White
Write-Host "├─ 地址: http://localhost:8080" -ForegroundColor Cyan
Write-Host "├─ 目录: $(Resolve-Path "public")" -ForegroundColor Gray
Write-Host "├─ 主页: /smart_test.html" -ForegroundColor Gray
Write-Host "└─ 按 Ctrl+C 停止服务器" -ForegroundColor Yellow
Write-Host ""
Write-Host ("" * 50) -ForegroundColor DarkGray
$publicDir = Resolve-Path "public"
Set-Location "public"
python -m http.server 8080
Write-Host "[ ] Starting dev server..." -ForegroundColor Cyan
Write-Host ""
Write-Host "Server info:" -ForegroundColor White
Write-Host " - URL: http://localhost:8080/index.html" -ForegroundColor Cyan
Write-Host " - Root: $publicDir" -ForegroundColor Gray
Write-Host " - Home: /index.html" -ForegroundColor Gray
Write-Host " - Stop: Ctrl+C" -ForegroundColor Yellow
Write-Host ""
Set-Location $publicDir
& $pythonCommand -m http.server 8080

3
spc-vue/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
node_modules
dist
*.local

34
spc-vue/README.md Normal file
View File

@@ -0,0 +1,34 @@
# SPC 控制图 Vue 验证项目
这个目录是一个独立的 Vue 3 + Vite + ECharts 项目,用于验证 `Cmd_Spc` 的 WASM 计算结果。每一种控制图都拆成独立路由,方便后续把单个页面或组件集成到其他项目。
## 本地运行
```powershell
cd spc-vue
npm install
npm run dev
```
开发环境会通过 Vite 插件把主项目的 `../public/wasm` 映射到 `/wasm`。执行 `npm run build` 时会自动把 `smart_math.js``smart_math.wasm` 复制到 `dist/wasm`
## 页面路由
- `/overview`SPC 总览和关键指标
- `/run-chart`:基本趋势图,显示原始序列、均值线、中位线、规格限和移动平均线
- `/histogram`:频数直方图,只显示分箱频数和规格限,不叠加正态曲线
- `/normal-curve`:正态曲线,单独显示拟合分布和规格限
- `/xbar-r`Xbar 控制图,控制限来自 XR 结果
- `/r-chart`R 极差控制图
- `/xbar-s`Xbar 控制图,控制限来自 XS 结果
- `/s-chart`S 标准差控制图
- `/cpk`:过程能力指标
## 接口校验重点
`KinematicsWebAPI::func` 会把业务返回统一包装成顶层 `success: true`,所以页面不会只判断顶层 `success`。SPC 调用成功必须同时满足:
- 顶层 `success !== false`
- `res_data` 存在
- `res_data.error` 不存在
- `res_data.XR``res_data.XS``res_data.Cpk` 都存在

13
spc-vue/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="data:," />
<title>SPC 控制图 Vue 验证项目</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

2182
spc-vue/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

23
spc-vue/package.json Normal file
View File

@@ -0,0 +1,23 @@
{
"name": "spc-control-chart-vue",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview --host 0.0.0.0"
},
"dependencies": {
"echarts": "^5.5.1",
"vue": "^3.4.38",
"vue-router": "^4.4.3"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.1.2",
"playwright": "^1.61.1",
"typescript": "^5.5.4",
"vite": "^5.4.2",
"vue-tsc": "^2.0.29"
}
}

39
spc-vue/src/App.vue Normal file
View File

@@ -0,0 +1,39 @@
<script setup lang="ts">
import { computed, onMounted } from "vue";
import { RouterLink, RouterView } from "vue-router";
import SpcInputPanel from "./components/SpcInputPanel.vue";
import { useSpc } from "./composables/useSpc";
import { routes } from "./router";
const { result, runSpc } = useSpc();
const navRoutes = computed(() => routes.filter((route) => route.path !== "/" && route.meta?.title));
onMounted(() => {
if (!result.value) {
void runSpc();
}
});
</script>
<template>
<div class="app-shell">
<header class="topbar">
<div>
<p class="eyebrow">SPC Vue</p>
<h1>SPC 控制图验证</h1>
</div>
<nav class="nav-tabs" aria-label="控制图页面">
<RouterLink v-for="route in navRoutes" :key="route.path" :to="route.path">
{{ route.meta?.title }}
</RouterLink>
</nav>
</header>
<main class="workspace">
<SpcInputPanel />
<section class="content-pane">
<RouterView />
</section>
</main>
</div>
</template>

View File

@@ -0,0 +1,59 @@
<script setup lang="ts">
import * as echarts from "echarts";
import type { EChartsOption } from "echarts";
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
const props = defineProps<{
title: string;
eyebrow: string;
description: string;
option: EChartsOption | null;
emptyText?: string;
}>();
const chartElement = ref<HTMLDivElement | null>(null);
let chart: echarts.ECharts | null = null;
let resizeObserver: ResizeObserver | null = null;
// 初始化图表实例,并监听容器尺寸变化保持 ECharts 自适应。
function ensureChart() {
if (!chartElement.value || chart) return;
chart = echarts.init(chartElement.value);
resizeObserver = new ResizeObserver(() => chart?.resize());
resizeObserver.observe(chartElement.value);
}
// 按当前 option 刷新图表,等待 v-if 容器挂载后再初始化,支持直接进入子路由。
async function renderChart() {
if (!props.option) {
chart?.dispose();
chart = null;
return;
}
await nextTick();
ensureChart();
chart?.setOption(props.option, true);
}
onMounted(renderChart);
watch(() => props.option, renderChart, { deep: true });
onBeforeUnmount(() => {
resizeObserver?.disconnect();
chart?.dispose();
});
</script>
<template>
<section class="chart-card">
<header class="chart-card__header">
<div>
<p class="eyebrow">{{ eyebrow }}</p>
<h2>{{ title }}</h2>
</div>
</header>
<div v-if="option" ref="chartElement" class="chart-card__canvas" />
<div v-else class="chart-card__empty">{{ emptyText || "等待计算" }}</div>
<p class="chart-card__description">{{ description }}</p>
</section>
</template>

View File

@@ -0,0 +1,12 @@
<script setup lang="ts">
defineProps<{
value: unknown;
}>();
</script>
<template>
<details class="json-preview">
<summary>查看原始接口响应</summary>
<pre>{{ JSON.stringify(value || {}, null, 2) }}</pre>
</details>
</template>

View File

@@ -0,0 +1,14 @@
<script setup lang="ts">
defineProps<{
metrics: Array<{ label: string; value: string | number; tone?: "ok" | "warn" | "neutral" }>;
}>();
</script>
<template>
<div class="metric-strip">
<div v-for="metric in metrics" :key="metric.label" class="metric-tile">
<span>{{ metric.label }}</span>
<strong :class="metric.tone || 'neutral'">{{ metric.value }}</strong>
</div>
</div>
</template>

View File

@@ -0,0 +1,54 @@
<script setup lang="ts">
import { useSpc } from "../composables/useSpc";
const { form, parsedCount, loading, statusText, errorMessage, formatValues, loadSample, runSpc } = useSpc();
</script>
<template>
<aside class="input-panel" aria-label="SPC 输入">
<div class="panel-title-row">
<div>
<p class="eyebrow">Cmd_Spc</p>
<h2>计算输入</h2>
</div>
<span class="status-pill">{{ statusText }}</span>
</div>
<div class="form-grid">
<label>
<span>n 子组容量</span>
<input v-model.number="form.n" type="number" min="2" step="1" />
</label>
<label>
<span>k 子组数</span>
<input v-model.number="form.k" type="number" min="1" step="1" />
</label>
<label>
<span>USL 上规格限</span>
<input v-model.number="form.usl" type="number" step="0.001" />
</label>
<label>
<span>LSL 下规格限</span>
<input v-model.number="form.lsl" type="number" step="0.001" />
</label>
</div>
<label class="data-field">
<span>测量数据 x</span>
<textarea v-model="form.valuesText" spellcheck="false" />
</label>
<div class="input-panel__footer">
<span class="count-text">当前 {{ parsedCount }} 个数据</span>
<div class="button-row">
<button type="button" class="secondary-button" @click="loadSample">载入样例</button>
<button type="button" class="secondary-button" @click="formatValues">格式化</button>
<button type="button" class="primary-button" :disabled="loading" @click="runSpc">
{{ loading ? "计算中" : "调用接口" }}
</button>
</div>
</div>
<p v-if="errorMessage" class="error-message" role="alert">{{ errorMessage }}</p>
</aside>
</template>

View File

@@ -0,0 +1,133 @@
import { computed, reactive, readonly, ref } from "vue";
import { sampleSpcInput } from "../data/sampleSpc";
import { callSpc } from "../services/wasmSpcClient";
import type { SpcInput, SpcResult, WebApiResponse } from "../types/spc";
const form = reactive({
n: sampleSpcInput.n,
k: sampleSpcInput.k,
usl: sampleSpcInput.usl,
lsl: sampleSpcInput.lsl,
valuesText: sampleSpcInput.x.join(", ")
});
const result = ref<SpcResult | null>(null);
const rawResponse = ref<WebApiResponse | null>(null);
const errorMessage = ref("");
const statusText = ref("未执行");
const loading = ref(false);
// 将文本框中的数字解析成数组,支持逗号、空格和换行分隔。
function parseValues(text: string): number[] {
const values = text
.split(/[\s,;]+/)
.map((item) => item.trim())
.filter(Boolean)
.map(Number);
if (values.some((value) => !Number.isFinite(value))) {
throw new Error("测量数据中存在非数字内容");
}
return values;
}
// 从表单生成 SPC 输入,并校验 n、k、规格限和数据量是否匹配。
function readInput(): SpcInput {
const input: SpcInput = {
n: Number(form.n),
k: Number(form.k),
usl: Number(form.usl),
lsl: Number(form.lsl),
x: parseValues(form.valuesText)
};
if (!Number.isInteger(input.n) || input.n < 2) {
throw new Error("n 必须是大于等于 2 的整数");
}
if (!Number.isInteger(input.k) || input.k < 1) {
throw new Error("k 必须是大于等于 1 的整数");
}
if (!Number.isFinite(input.usl) || !Number.isFinite(input.lsl) || input.usl <= input.lsl) {
throw new Error("USL 必须大于 LSL");
}
if (input.x.length !== input.n * input.k) {
throw new Error(`数据量应为 n*k=${input.n * input.k},当前为 ${input.x.length}`);
}
return input;
}
// 格式化输入数据,按子组容量 n 分行,方便核对原始数据。
function formatValues() {
const values = parseValues(form.valuesText);
const lines: string[] = [];
for (let index = 0; index < values.length; index += Number(form.n) || 5) {
lines.push(values.slice(index, index + (Number(form.n) || 5)).join(", "));
}
form.valuesText = lines.join("\n");
}
// 恢复内置样例数据。
function loadSample() {
form.n = sampleSpcInput.n;
form.k = sampleSpcInput.k;
form.usl = sampleSpcInput.usl;
form.lsl = sampleSpcInput.lsl;
form.valuesText = sampleSpcInput.x.join(", ");
errorMessage.value = "";
statusText.value = "样例已载入";
}
// 执行 SPC 接口调用,并保存原始响应和解包后的业务结果。
async function runSpc() {
loading.value = true;
errorMessage.value = "";
statusText.value = "计算中";
try {
const input = readInput();
const response = await callSpc(input);
rawResponse.value = response.response;
result.value = response.result;
statusText.value = "计算完成";
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
errorMessage.value = message;
statusText.value = "计算失败";
} finally {
loading.value = false;
}
}
export function useSpc() {
const parsedCount = computed(() => {
try {
return parseValues(form.valuesText).length;
} catch {
return 0;
}
});
const currentInput = computed(() => {
try {
return readInput();
} catch {
return null;
}
});
return {
form,
result: readonly(result),
rawResponse: readonly(rawResponse),
errorMessage: readonly(errorMessage),
statusText: readonly(statusText),
loading: readonly(loading),
parsedCount,
currentInput,
formatValues,
loadSample,
runSpc
};
}

3
spc-vue/src/config.ts Normal file
View File

@@ -0,0 +1,3 @@
export const appConfig = {
wasmScriptUrl: "/wasm/smart_math.js"
};

View File

@@ -0,0 +1,25 @@
import type { SpcInput } from "../types/spc";
export const sampleSpcInput: SpcInput = {
n: 5,
k: 30,
usl: 1.7,
lsl: 1.5,
x: [
1.55, 1.58, 1.61, 1.6, 1.6, 1.58, 1.63, 1.63, 1.62, 1.63,
1.62, 1.63, 1.62, 1.59, 1.58, 1.58, 1.6, 1.61, 1.62, 1.63,
1.58, 1.64, 1.63, 1.62, 1.62, 1.62, 1.62, 1.63, 1.61, 1.57,
1.64, 1.62, 1.61, 1.6, 1.58, 1.57, 1.59, 1.61, 1.62, 1.63,
1.58, 1.61, 1.6, 1.62, 1.63, 1.6, 1.61, 1.64, 1.64, 1.63,
1.58, 1.6, 1.62, 1.63, 1.65, 1.62, 1.58, 1.59, 1.57, 1.58,
1.57, 1.57, 1.58, 1.59, 1.64, 1.61, 1.64, 1.62, 1.6, 1.59,
1.65, 1.62, 1.62, 1.6, 1.58, 1.57, 1.59, 1.57, 1.59, 1.62,
1.56, 1.57, 1.57, 1.61, 1.62, 1.56, 1.58, 1.59, 1.6, 1.62,
1.58, 1.6, 1.6, 1.62, 1.63, 1.58, 1.59, 1.6, 1.63, 1.62,
1.58, 1.59, 1.62, 1.63, 1.64, 1.58, 1.59, 1.62, 1.63, 1.61,
1.58, 1.59, 1.6, 1.61, 1.63, 1.57, 1.59, 1.61, 1.61, 1.62,
1.58, 1.58, 1.6, 1.61, 1.63, 1.62, 1.58, 1.58, 1.58, 1.57,
1.63, 1.59, 1.57, 1.58, 1.57, 1.58, 1.62, 1.61, 1.63, 1.61,
1.58, 1.57, 1.59, 1.6, 1.62, 1.62, 1.6, 1.6, 1.57, 1.57
]
};

6
spc-vue/src/main.ts Normal file
View File

@@ -0,0 +1,6 @@
import { createApp } from "vue";
import App from "./App.vue";
import { router } from "./router";
import "./styles.css";
createApp(App).use(router).mount("#app");

28
spc-vue/src/router.ts Normal file
View File

@@ -0,0 +1,28 @@
import { createRouter, createWebHashHistory } from "vue-router";
import OverviewView from "./views/OverviewView.vue";
import XBarRView from "./views/XBarRView.vue";
import RChartView from "./views/RChartView.vue";
import XBarSView from "./views/XBarSView.vue";
import SChartView from "./views/SChartView.vue";
import CpkView from "./views/CpkView.vue";
import RunChartView from "./views/RunChartView.vue";
import HistogramView from "./views/HistogramView.vue";
import NormalCurveView from "./views/NormalCurveView.vue";
export const routes = [
{ path: "/", redirect: "/overview" },
{ path: "/overview", component: OverviewView, meta: { title: "总览" } },
{ path: "/run-chart", component: RunChartView, meta: { title: "趋势图" } },
{ path: "/histogram", component: HistogramView, meta: { title: "直方图" } },
{ path: "/normal-curve", component: NormalCurveView, meta: { title: "正态曲线" } },
{ path: "/xbar-r", component: XBarRView, meta: { title: "Xbar-R" } },
{ path: "/r-chart", component: RChartView, meta: { title: "R 图" } },
{ path: "/xbar-s", component: XBarSView, meta: { title: "Xbar-S" } },
{ path: "/s-chart", component: SChartView, meta: { title: "S 图" } },
{ path: "/cpk", component: CpkView, meta: { title: "Cpk" } }
];
export const router = createRouter({
history: createWebHashHistory(),
routes
});

View File

@@ -0,0 +1,136 @@
import { appConfig } from "../config";
import type { SpcInput, SpcRequest, SpcResult, WebApiResponse } from "../types/spc";
type SmartMathModule = {
_init_func: () => number;
_func: (requestPtr: number) => number;
_smart_free_string: (ptr: number) => void;
_malloc: (size: number) => number;
_free: (ptr: number) => void;
lengthBytesUTF8: (value: string) => number;
stringToUTF8: (value: string, ptr: number, size: number) => void;
UTF8ToString: (ptr: number) => string;
};
declare global {
interface Window {
createSmartMathModule?: (options?: {
locateFile?: (fileName: string) => string;
noInitialRun?: boolean;
}) => Promise<SmartMathModule>;
}
}
let scriptPromise: Promise<void> | null = null;
let modulePromise: Promise<SmartMathModule> | null = null;
// 动态加载 Emscripten 生成的 JS 包,避免 Vue 项目直接绑定全局 script 标签。
function loadWasmScript(scriptUrl: string): Promise<void> {
if (window.createSmartMathModule) {
return Promise.resolve();
}
if (!scriptPromise) {
scriptPromise = new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = scriptUrl;
script.async = true;
script.onload = () => resolve();
script.onerror = () => reject(new Error(`WASM 脚本加载失败: ${scriptUrl}`));
document.head.appendChild(script);
});
}
return scriptPromise;
}
// 初始化 WASM 模块,内部只执行一次 _init_func。
async function getWasmModule(): Promise<SmartMathModule> {
if (!modulePromise) {
modulePromise = (async () => {
await loadWasmScript(appConfig.wasmScriptUrl);
if (!window.createSmartMathModule) {
throw new Error("WASM 工厂函数 createSmartMathModule 不存在");
}
const wasmDir = appConfig.wasmScriptUrl.replace(/\/[^/]*$/, "");
const module = await window.createSmartMathModule({
noInitialRun: true,
locateFile: (fileName) => `${wasmDir}/${fileName}`
});
const initPtr = module._init_func();
try {
module.UTF8ToString(initPtr);
} finally {
module._smart_free_string(initPtr);
}
return module;
})();
}
return modulePromise;
}
// 在 WASM 线性内存中写入 UTF-8 字符串,并返回指针。
function allocString(module: SmartMathModule, value: string): number {
const size = module.lengthBytesUTF8(value) + 1;
const ptr = module._malloc(size);
module.stringToUTF8(value, ptr, size);
return ptr;
}
// 构建 SPC 业务请求,保持与现有 Cmd_Spc 测试数据一致。
export function buildSpcRequest(input: SpcInput): SpcRequest {
return {
msg: "spc vue validation",
req_code: "SPC_VUE_VALIDATE",
req_from: "spc_vue",
req_cmd: "Cmd_Spc",
req_param: input
};
}
// 校验业务层 SPC 返回,不能只看顶层 success要检查 res_data 的错误和完整字段。
export function unwrapSpcResponse(response: WebApiResponse): SpcResult {
if (response.success === false) {
throw new Error(typeof response.error === "string" ? response.error : "接口顶层 success=false");
}
const data = response.res_data;
if (!data) {
throw new Error("接口缺少 res_data");
}
if (data.error) {
throw new Error(data.error);
}
if (!data.XR || !data.XS || !data.Cpk) {
throw new Error("SPC 结果不完整,需要同时包含 res_data.XR、res_data.XS、res_data.Cpk");
}
return data;
}
// 调用 WASM _func 并返回经过业务完整性校验的 SPC 结果。
export async function callSpc(input: SpcInput): Promise<{ response: WebApiResponse; result: SpcResult }> {
const module = await getWasmModule();
const payload = JSON.stringify(buildSpcRequest(input));
let requestPtr = 0;
let responsePtr = 0;
try {
requestPtr = allocString(module, payload);
responsePtr = module._func(requestPtr);
const response = JSON.parse(module.UTF8ToString(responsePtr)) as WebApiResponse;
return {
response,
result: unwrapSpcResponse(response)
};
} finally {
if (responsePtr) {
module._smart_free_string(responsePtr);
}
if (requestPtr) {
module._free(requestPtr);
}
}
}

478
spc-vue/src/styles.css Normal file
View File

@@ -0,0 +1,478 @@
:root {
color-scheme: light;
--bg: #f4f6f8;
--surface: #ffffff;
--surface-soft: #f8fafc;
--line: #d7dde5;
--line-strong: #b8c2cf;
--text: #172033;
--muted: #667085;
--primary: #0f766e;
--primary-strong: #115e59;
--danger: #b42318;
--warning: #b45309;
--ok: #15803d;
--code-bg: #111827;
--code-text: #edf2f7;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
background: var(--bg);
color: var(--text);
font-family: "Segoe UI", "Microsoft YaHei", Arial, sans-serif;
}
button,
input,
textarea {
font: inherit;
}
button {
min-height: 44px;
border: 1px solid transparent;
border-radius: 8px;
padding: 9px 14px;
font-weight: 700;
cursor: pointer;
transition:
background-color 160ms ease,
border-color 160ms ease,
color 160ms ease;
}
button:disabled {
cursor: not-allowed;
opacity: 0.55;
}
button:focus-visible,
a:focus-visible,
input:focus-visible,
textarea:focus-visible,
summary:focus-visible {
outline: 3px solid rgba(15, 118, 110, 0.24);
outline-offset: 2px;
}
h1,
h2,
h3,
p {
margin: 0;
}
h1 {
font-size: 24px;
line-height: 1.2;
}
h2 {
font-size: 19px;
line-height: 1.3;
}
h3 {
font-size: 16px;
line-height: 1.35;
}
.app-shell {
width: min(1480px, 100%);
margin: 0 auto;
padding: 20px;
}
.topbar {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 18px;
margin-bottom: 16px;
}
.eyebrow {
color: var(--primary);
font-size: 12px;
font-weight: 800;
letter-spacing: 0;
text-transform: uppercase;
}
.nav-tabs {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 8px;
}
.nav-tabs a {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 40px;
border: 1px solid var(--line);
border-radius: 8px;
padding: 8px 12px;
background: var(--surface);
color: var(--muted);
text-decoration: none;
font-size: 14px;
font-weight: 700;
}
.nav-tabs a.router-link-active {
border-color: var(--primary);
background: var(--primary);
color: #ffffff;
}
.workspace {
display: grid;
grid-template-columns: minmax(340px, 420px) minmax(0, 1fr);
gap: 16px;
align-items: start;
}
.input-panel,
.content-pane,
.chart-card,
.info-panel,
.json-preview {
border: 1px solid var(--line);
border-radius: 8px;
background: var(--surface);
}
.input-panel {
position: sticky;
top: 16px;
padding: 16px;
}
.panel-title-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin-bottom: 14px;
}
.status-pill {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 28px;
border: 1px solid var(--line);
border-radius: 999px;
padding: 4px 10px;
color: var(--muted);
background: var(--surface-soft);
font-size: 12px;
font-weight: 700;
white-space: nowrap;
}
.form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
label {
display: grid;
gap: 6px;
color: var(--muted);
font-size: 13px;
font-weight: 700;
}
input,
textarea {
width: 100%;
border: 1px solid var(--line);
border-radius: 8px;
background: #ffffff;
color: var(--text);
}
input {
min-height: 44px;
padding: 9px 11px;
}
textarea {
min-height: 300px;
resize: vertical;
padding: 12px;
font:
13px/1.55 Consolas,
"Courier New",
monospace;
}
.data-field {
margin-top: 12px;
}
.input-panel__footer {
display: grid;
gap: 10px;
margin-top: 12px;
}
.count-text {
color: var(--muted);
font-size: 13px;
font-weight: 700;
}
.button-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.primary-button {
background: var(--primary);
color: #ffffff;
}
.primary-button:hover:not(:disabled) {
background: var(--primary-strong);
}
.secondary-button {
border-color: var(--line);
background: #ffffff;
color: var(--text);
}
.secondary-button:hover:not(:disabled) {
border-color: var(--primary);
color: var(--primary);
}
.error-message {
margin-top: 12px;
color: var(--danger);
font-size: 13px;
line-height: 1.5;
}
.content-pane {
min-width: 0;
padding: 16px;
}
.page-stack {
display: grid;
gap: 14px;
}
.page-intro {
display: grid;
gap: 6px;
}
.page-intro p:last-child {
max-width: 920px;
color: var(--muted);
font-size: 14px;
line-height: 1.65;
}
.metric-strip {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 10px;
}
.metric-tile {
min-width: 0;
border: 1px solid var(--line);
border-radius: 8px;
padding: 11px 12px;
background: var(--surface-soft);
}
.metric-tile span {
display: block;
color: var(--muted);
font-size: 12px;
font-weight: 800;
}
.metric-tile strong {
display: block;
margin-top: 5px;
overflow-wrap: anywhere;
font-size: 20px;
font-variant-numeric: tabular-nums;
line-height: 1.2;
}
.metric-tile strong.ok {
color: var(--ok);
}
.metric-tile strong.warn {
color: var(--warning);
}
.metric-tile strong.neutral {
color: var(--text);
}
.chart-card {
overflow: hidden;
}
.chart-card__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
min-height: 62px;
padding: 14px 16px;
border-bottom: 1px solid var(--line);
background: var(--surface-soft);
}
.chart-card__canvas,
.chart-card__empty {
width: 100%;
height: 460px;
}
.chart-card__empty {
display: grid;
place-items: center;
color: var(--muted);
font-size: 14px;
}
.chart-card__description {
padding: 0 16px 16px;
color: var(--muted);
font-size: 14px;
line-height: 1.65;
}
.info-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
}
.info-panel {
padding: 14px;
}
.info-panel p {
margin-top: 8px;
color: var(--muted);
font-size: 14px;
line-height: 1.6;
}
.two-column {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.two-column .chart-card__canvas,
.two-column .chart-card__empty {
height: 390px;
}
.json-preview {
overflow: hidden;
}
.json-preview summary {
min-height: 44px;
padding: 12px 14px;
cursor: pointer;
color: var(--muted);
font-weight: 800;
}
.json-preview pre {
max-height: 460px;
margin: 0;
overflow: auto;
padding: 14px;
background: var(--code-bg);
color: var(--code-text);
font:
13px/1.55 Consolas,
"Courier New",
monospace;
}
.empty-state {
display: grid;
place-items: center;
min-height: 160px;
border: 1px dashed var(--line-strong);
border-radius: 8px;
color: var(--muted);
background: var(--surface-soft);
}
@media (max-width: 1180px) {
.workspace,
.two-column {
grid-template-columns: 1fr;
}
.input-panel {
position: static;
}
.metric-strip,
.info-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 760px) {
.app-shell {
padding: 14px;
}
.topbar {
align-items: stretch;
flex-direction: column;
}
.nav-tabs {
justify-content: flex-start;
}
.nav-tabs a {
flex: 1 1 92px;
}
.form-grid,
.metric-strip,
.info-grid {
grid-template-columns: 1fr;
}
.chart-card__canvas,
.chart-card__empty,
.two-column .chart-card__canvas,
.two-column .chart-card__empty {
height: 340px;
}
}

96
spc-vue/src/types/spc.ts Normal file
View File

@@ -0,0 +1,96 @@
export interface SpcInput {
n: number;
k: number;
usl: number;
lsl: number;
x: number[];
}
export interface SpcRequest {
msg: string;
req_code: string;
req_from: string;
req_cmd: "Cmd_Spc";
req_param: SpcInput;
}
export interface XrResult {
n: number;
k: number;
CL_X: number;
UCL_X: number;
LCL_X: number;
CL_R: number;
UCL_R: number;
LCL_R: number;
CL_Xk: readonly number[];
CL_Rk: readonly number[];
}
export interface XsResult {
n: number;
k: number;
CL_X: number;
UCL_X: number;
LCL_X: number;
CL_S: number;
UCL_S: number;
LCL_S: number;
CL_Xk: readonly number[];
CL_Sk: readonly number[];
}
export interface CpkResult {
n: number;
k: number;
SL: number;
USL: number;
LSL: number;
Singma: number;
SingmaS: number;
Ca: number;
Cp: number;
CPU: number;
CPL: number;
CR: number;
Cpk: number;
Pp: number;
PPU: number;
PPL: number;
PR: number;
Ppk: number;
ProcessSpread: number;
GroupWidth: number;
GroupCount: number;
ValueMax: number;
ValueMin: number;
Xk: readonly number[];
XkUp: readonly number[];
XkDown: readonly number[];
Yk: readonly number[];
YkCount: readonly number[];
NormalDistributionX: readonly number[];
NormalDistributionY: readonly number[];
}
export interface SpcResult {
XR: XrResult;
XS: XsResult;
Cpk: CpkResult;
}
export interface WebApiResponse {
success?: boolean;
res_data?: SpcResult & { error?: string };
error?: string;
[key: string]: unknown;
}
export interface ControlLimitChart {
title: string;
valueName: string;
values: readonly number[];
center: number;
upper: number;
lower: number;
}

View File

@@ -0,0 +1,376 @@
import type { EChartsOption } from "echarts";
import type { ControlLimitChart, CpkResult } from "../types/spc";
const chartColors = ["#0f766e", "#b45309", "#991b1b", "#2563eb"];
export interface RunChartInput {
values: readonly number[];
usl: number;
lsl: number;
target: number;
movingAverageWindow?: number;
}
// 生成从 1 开始的子组序号,用于控制图横轴。
export function sequenceLabels(length: number): string[] {
return Array.from({ length }, (_, index) => String(index + 1));
}
// 生成固定值线,用于 CL、UCL、LCL。
function constantSeries(length: number, value: number): number[] {
return Array.from({ length }, () => value);
}
// 计算算术平均值,用于基本趋势图的均值线。
function mean(values: readonly number[]): number {
if (!values.length) return 0;
return values.reduce((sum, value) => sum + value, 0) / values.length;
}
// 计算中位数,用于基本趋势图的中心参考线。
function median(values: readonly number[]): number {
if (!values.length) return 0;
const sorted = [...values].sort((a, b) => a - b);
const middle = Math.floor(sorted.length / 2);
return sorted.length % 2 === 0 ? (sorted[middle - 1] + sorted[middle]) / 2 : sorted[middle];
}
// 计算移动平均线,窗口不足时使用当前已有数据,避免前几项为空。
function movingAverage(values: readonly number[], windowSize: number): number[] {
return values.map((_, index) => {
const start = Math.max(0, index - windowSize + 1);
return mean(values.slice(start, index + 1));
});
}
// 找出超出规格限的原始数据点,用于基本趋势图标红。
function outOfSpecPoints(values: readonly number[], usl: number, lsl: number): Array<[number, number]> {
return values
.map((value, index) => ({ value, index }))
.filter((point) => point.value > usl || point.value < lsl)
.map((point) => [point.index, point.value]);
}
// 计算直方图 X 轴范围,覆盖分箱、规格限、正态曲线和原始极值,并保留少量边距。
function calculateHistogramXAxisRange(cpk: CpkResult): { min: number; max: number } {
const values = [
...cpk.Xk,
...cpk.XkUp,
...cpk.XkDown,
...cpk.NormalDistributionX,
cpk.LSL,
cpk.USL,
cpk.SL,
cpk.ValueMin,
cpk.ValueMax
].filter((value) => Number.isFinite(value));
if (!values.length) {
return { min: 0, max: 1 };
}
const rawMin = Math.min(...values);
const rawMax = Math.max(...values);
const span = rawMax - rawMin;
const padding = span > 0 ? Math.max(span * 0.08, cpk.GroupWidth || 0) : Math.max(Math.abs(rawMin) * 0.08, 0.1);
return {
min: rawMin - padding,
max: rawMax + padding
};
}
// 找出越过控制限的点,作为红色散点叠加在控制图上。
function outOfLimitPoints(values: readonly number[], upper: number, lower: number): Array<[number, number]> {
return values
.map((value, index) => ({ value, index }))
.filter((point) => point.value > upper || point.value < lower)
.map((point) => [point.index, point.value]);
}
// 创建单张控制图配置,包含数据线、中心线、上下控制限和越界点。
export function createControlLimitOption(chart: ControlLimitChart): EChartsOption {
const labels = sequenceLabels(chart.values.length);
const outliers = outOfLimitPoints(chart.values, chart.upper, chart.lower);
return {
color: chartColors,
tooltip: { trigger: "axis" },
legend: { top: 0, right: 0 },
grid: { left: 56, right: 28, top: 46, bottom: 44 },
xAxis: {
type: "category",
name: "子组",
data: labels,
boundaryGap: false,
axisLabel: { color: "#667085" }
},
yAxis: {
type: "value",
name: chart.valueName,
scale: true,
axisLabel: { color: "#667085" },
splitLine: { lineStyle: { color: "#e5e7eb" } }
},
series: [
{
name: chart.valueName,
type: "line",
data: [...chart.values],
symbolSize: 6,
lineStyle: { width: 2 }
},
{
name: "CL",
type: "line",
data: constantSeries(chart.values.length, chart.center),
symbol: "none",
lineStyle: { type: "dashed", width: 1.5 }
},
{
name: "UCL",
type: "line",
data: constantSeries(chart.values.length, chart.upper),
symbol: "none",
lineStyle: { type: "dotted", width: 1.5 }
},
{
name: "LCL",
type: "line",
data: constantSeries(chart.values.length, chart.lower),
symbol: "none",
lineStyle: { type: "dotted", width: 1.5 }
},
{
name: "越界点",
type: "scatter",
data: outliers,
symbolSize: 10,
itemStyle: { color: "#dc2626" }
}
]
};
}
// 创建基本趋势图配置,按原始顺序展示数据并叠加规格限、均值线、中位线和移动平均线。
export function createRunChartOption(input: RunChartInput): EChartsOption {
const labels = sequenceLabels(input.values.length);
const average = mean(input.values);
const middle = median(input.values);
const windowSize = Math.max(2, Math.floor(input.movingAverageWindow || 5));
const outliers = outOfSpecPoints(input.values, input.usl, input.lsl);
return {
color: chartColors,
tooltip: { trigger: "axis" },
legend: { top: 0, right: 0 },
grid: { left: 56, right: 34, top: 46, bottom: 44 },
xAxis: {
type: "category",
name: "样本序号",
data: labels,
boundaryGap: false,
axisLabel: { color: "#667085" }
},
yAxis: {
type: "value",
name: "测量值",
scale: true,
axisLabel: { color: "#667085" },
splitLine: { lineStyle: { color: "#e5e7eb" } }
},
series: [
{
name: "原始数据",
type: "line",
data: [...input.values],
symbolSize: 4,
lineStyle: { width: 1.8 }
},
{
name: `${windowSize}点移动平均`,
type: "line",
data: movingAverage(input.values, windowSize),
smooth: true,
symbol: "none",
lineStyle: { width: 2 }
},
{
name: "均值",
type: "line",
data: constantSeries(input.values.length, average),
symbol: "none",
lineStyle: { type: "dashed", width: 1.4 }
},
{
name: "中位线",
type: "line",
data: constantSeries(input.values.length, middle),
symbol: "none",
lineStyle: { type: "dashed", width: 1.4 }
},
{
name: "超规格点",
type: "scatter",
data: outliers,
symbolSize: 10,
itemStyle: { color: "#dc2626" }
},
{
name: "USL",
type: "line",
data: constantSeries(input.values.length, input.usl),
symbol: "none",
lineStyle: { type: "dotted", width: 1.5 }
},
{
name: "LSL",
type: "line",
data: constantSeries(input.values.length, input.lsl),
symbol: "none",
lineStyle: { type: "dotted", width: 1.5 }
},
{
name: "目标值",
type: "line",
data: constantSeries(input.values.length, input.target),
symbol: "none",
lineStyle: { type: "dotted", width: 1.5 }
}
]
};
}
// 创建过程能力指标柱状图,便于快速查看 Cp/Cpk/Pp/Ppk 门槛。
export function createCapabilityBarOption(cpk: CpkResult): EChartsOption {
const labels = ["Cp", "Cpk", "Pp", "Ppk", "CPU", "CPL"];
return {
color: ["#0f766e"],
tooltip: { trigger: "axis" },
grid: { left: 48, right: 24, top: 36, bottom: 42 },
xAxis: {
type: "category",
data: labels,
axisLabel: { color: "#667085" }
},
yAxis: {
type: "value",
axisLabel: { color: "#667085" },
splitLine: { lineStyle: { color: "#e5e7eb" } }
},
series: [
{
name: "能力指标",
type: "bar",
data: labels.map((label) => cpk[label as keyof CpkResult] as number),
barMaxWidth: 34,
itemStyle: {
color: (params) => {
const value = Number(params.value);
if (value >= 1.33) return "#15803d";
if (value >= 1) return "#b45309";
return "#b91c1c";
}
},
markLine: {
symbol: "none",
data: [
{ yAxis: 1, name: "1.00" },
{ yAxis: 1.33, name: "1.33" }
],
label: { color: "#667085" },
lineStyle: { color: "#b45309", type: "dashed" }
}
}
]
};
}
// 创建单独直方图配置,只展示频数分布和规格限,不叠加正态曲线。
export function createFrequencyHistogramOption(cpk: CpkResult): EChartsOption {
const xRange = calculateHistogramXAxisRange(cpk);
return {
color: ["#0f766e"],
tooltip: { trigger: "axis" },
legend: { top: 0, right: 0 },
grid: { left: 52, right: 48, top: 46, bottom: 42 },
xAxis: {
type: "value",
name: "测量值",
min: xRange.min,
max: xRange.max,
axisLabel: { color: "#667085" }
},
yAxis: {
type: "value",
name: "频数",
minInterval: 1,
axisLabel: { color: "#667085" },
splitLine: { lineStyle: { color: "#e5e7eb" } }
},
series: [
{
name: "频数",
type: "bar",
data: cpk.Xk.map((value, index) => [value, cpk.YkCount[index]]),
barMaxWidth: 34,
markLine: {
symbol: "none",
data: [
{ xAxis: cpk.LSL, name: "LSL" },
{ xAxis: cpk.USL, name: "USL" },
{ xAxis: cpk.SL, name: "目标值" }
],
label: { color: "#667085" },
lineStyle: { color: "#991b1b", type: "dashed" }
}
}
]
};
}
// 创建正态曲线配置,单独查看拟合曲线与规格限关系。
export function createNormalCurveOption(cpk: CpkResult): EChartsOption {
const xRange = calculateHistogramXAxisRange(cpk);
return {
color: ["#b45309"],
tooltip: { trigger: "axis" },
legend: { top: 0, right: 0 },
grid: { left: 52, right: 48, top: 46, bottom: 42 },
xAxis: {
type: "value",
name: "测量值",
min: xRange.min,
max: xRange.max,
axisLabel: { color: "#667085" }
},
yAxis: {
type: "value",
name: "概率密度",
axisLabel: { color: "#667085" },
splitLine: { lineStyle: { color: "#e5e7eb" } }
},
series: [
{
name: "正态曲线",
type: "line",
smooth: true,
symbolSize: 4,
data: cpk.NormalDistributionX.map((value, index) => [value, cpk.NormalDistributionY[index]]),
markLine: {
symbol: "none",
data: [
{ xAxis: cpk.LSL, name: "LSL" },
{ xAxis: cpk.USL, name: "USL" },
{ xAxis: cpk.SL, name: "目标值" }
],
label: { color: "#667085" },
lineStyle: { color: "#991b1b", type: "dashed" }
}
}
]
};
}

View File

@@ -0,0 +1,36 @@
<script setup lang="ts">
import { computed } from "vue";
import EChartPanel from "../components/EChartPanel.vue";
import MetricStrip from "../components/MetricStrip.vue";
import { useSpc } from "../composables/useSpc";
import { createCapabilityBarOption } from "../utils/chartOptions";
const { result } = useSpc();
const metrics = computed(() => {
const cpk = result.value?.Cpk;
if (!cpk) return [];
return [
{ label: "Cp", value: cpk.Cp, tone: cpk.Cp >= 1.33 ? ("ok" as const) : ("warn" as const) },
{ label: "Cpk", value: cpk.Cpk, tone: cpk.Cpk >= 1.33 ? ("ok" as const) : ("warn" as const) },
{ label: "Pp", value: cpk.Pp, tone: cpk.Pp >= 1.33 ? ("ok" as const) : ("warn" as const) },
{ label: "Ppk", value: cpk.Ppk, tone: cpk.Ppk >= 1.33 ? ("ok" as const) : ("warn" as const) },
{ label: "USL", value: cpk.USL },
{ label: "LSL", value: cpk.LSL }
];
});
const capabilityOption = computed(() => (result.value?.Cpk ? createCapabilityBarOption(result.value.Cpk) : null));
</script>
<template>
<article class="page-stack">
<MetricStrip v-if="metrics.length" :metrics="metrics" />
<EChartPanel
title="过程能力指标"
eyebrow="Capability"
description="能力柱状图用于快速比较 Cp、Cpk、Pp、Ppk。页面保留 1.00 与 1.33 参考线,方便验证常见能力门槛;分布形态请在直方图和正态曲线页面单独查看。"
:option="capabilityOption"
/>
</article>
</template>

View File

@@ -0,0 +1,38 @@
<script setup lang="ts">
import { computed } from "vue";
import EChartPanel from "../components/EChartPanel.vue";
import MetricStrip from "../components/MetricStrip.vue";
import { useSpc } from "../composables/useSpc";
import { createFrequencyHistogramOption } from "../utils/chartOptions";
const { result } = useSpc();
const option = computed(() => (result.value?.Cpk ? createFrequencyHistogramOption(result.value.Cpk) : null));
const metrics = computed(() => {
const cpk = result.value?.Cpk;
if (!cpk) return [];
const maxCount = Math.max(...cpk.YkCount);
return [
{ label: "分箱数", value: cpk.GroupCount },
{ label: "组距", value: cpk.GroupWidth },
{ label: "最大频数", value: maxCount },
{ label: "最小值", value: cpk.ValueMin },
{ label: "最大值", value: cpk.ValueMax },
{ label: "规格限", value: `${cpk.LSL} ~ ${cpk.USL}` }
];
});
</script>
<template>
<article class="page-stack">
<MetricStrip v-if="metrics.length" :metrics="metrics" />
<EChartPanel
title="频数直方图"
eyebrow="Histogram"
description="直方图只展示各测量区间的频数分布,并标出规格限和目标值。它适合先观察真实分布形态、偏态、双峰、离群区间以及数据是否靠近规格边界。"
:option="option"
/>
</article>
</template>

View File

@@ -0,0 +1,37 @@
<script setup lang="ts">
import { computed } from "vue";
import EChartPanel from "../components/EChartPanel.vue";
import MetricStrip from "../components/MetricStrip.vue";
import { useSpc } from "../composables/useSpc";
import { createNormalCurveOption } from "../utils/chartOptions";
const { result } = useSpc();
const option = computed(() => (result.value?.Cpk ? createNormalCurveOption(result.value.Cpk) : null));
const metrics = computed(() => {
const cpk = result.value?.Cpk;
if (!cpk) return [];
return [
{ label: "Sigma", value: cpk.Singma },
{ label: "SigmaS", value: cpk.SingmaS },
{ label: "Ca", value: cpk.Ca },
{ label: "SL", value: cpk.SL },
{ label: "USL", value: cpk.USL },
{ label: "LSL", value: cpk.LSL }
];
});
</script>
<template>
<article class="page-stack">
<MetricStrip v-if="metrics.length" :metrics="metrics" />
<EChartPanel
title="正态曲线"
eyebrow="Normal Curve"
description="正态曲线单独展示当前 SPC 结果中的拟合分布,并标出规格限和目标值。它用于辅助判断能力指标解释是否依赖正态分布假设。"
:option="option"
/>
</article>
</template>

View File

@@ -0,0 +1,56 @@
<script setup lang="ts">
import { computed } from "vue";
import JsonPreview from "../components/JsonPreview.vue";
import MetricStrip from "../components/MetricStrip.vue";
import { useSpc } from "../composables/useSpc";
const { result, rawResponse } = useSpc();
const metrics = computed(() => {
const cpk = result.value?.Cpk;
const xr = result.value?.XR;
const xs = result.value?.XS;
if (!cpk || !xr || !xs) return [];
return [
{ label: "Cpk", value: cpk.Cpk, tone: cpk.Cpk >= 1.33 ? ("ok" as const) : ("warn" as const) },
{ label: "Cp", value: cpk.Cp, tone: cpk.Cp >= 1.33 ? ("ok" as const) : ("warn" as const) },
{ label: "Ppk", value: cpk.Ppk, tone: cpk.Ppk >= 1.33 ? ("ok" as const) : ("warn" as const) },
{ label: "XR 均值CL", value: xr.CL_X },
{ label: "R 均值CL", value: xr.CL_R },
{ label: "S 均值CL", value: xs.CL_S }
];
});
</script>
<template>
<article class="page-stack">
<section class="page-intro">
<p class="eyebrow">Overview</p>
<h2>SPC 结果总览</h2>
<p>
页面启动后会自动调用一次 `Cmd_Spc`左侧可以替换 nk规格限和测量数据所有控制图页面共用同一份计算结果
</p>
</section>
<MetricStrip v-if="metrics.length" :metrics="metrics" />
<div v-else class="empty-state">等待 SPC 结果</div>
<section class="info-grid">
<div class="info-panel">
<h3>Xbar-R / R</h3>
<p>使用子组均值和极差验证过程中心与短期组内波动适合小子组连续型数据</p>
</div>
<div class="info-panel">
<h3>Xbar-S / S</h3>
<p>使用子组均值和样本标准差验证过程稳定性适合子组容量较大或需要标准差口径的场景</p>
</div>
<div class="info-panel">
<h3>Cpk 能力</h3>
<p>结合规格限直方图和正态曲线检查过程能力指标与数据分布是否相互印证</p>
</div>
</section>
<JsonPreview :value="rawResponse" />
</article>
</template>

View File

@@ -0,0 +1,45 @@
<script setup lang="ts">
import { computed } from "vue";
import EChartPanel from "../components/EChartPanel.vue";
import MetricStrip from "../components/MetricStrip.vue";
import { useSpc } from "../composables/useSpc";
import { createControlLimitOption } from "../utils/chartOptions";
const { result } = useSpc();
const option = computed(() => {
const xr = result.value?.XR;
if (!xr) return null;
return createControlLimitOption({
title: "R 极差控制图",
valueName: "子组极差",
values: xr.CL_Rk,
center: xr.CL_R,
upper: xr.UCL_R,
lower: xr.LCL_R
});
});
const metrics = computed(() => {
const xr = result.value?.XR;
if (!xr) return [];
return [
{ label: "CL_R", value: xr.CL_R },
{ label: "UCL_R", value: xr.UCL_R },
{ label: "LCL_R", value: xr.LCL_R },
{ label: "子组容量", value: xr.n }
];
});
</script>
<template>
<article class="page-stack">
<MetricStrip v-if="metrics.length" :metrics="metrics" />
<EChartPanel
title="R 极差控制图"
eyebrow="Range"
description="R 图使用每个子组的最大值减最小值观察组内波动。若 R 图先失控Xbar 控制限的解释需要谨慎。"
:option="option"
/>
</article>
</template>

View File

@@ -0,0 +1,49 @@
<script setup lang="ts">
import { computed } from "vue";
import EChartPanel from "../components/EChartPanel.vue";
import MetricStrip from "../components/MetricStrip.vue";
import { useSpc } from "../composables/useSpc";
import { createRunChartOption } from "../utils/chartOptions";
const { currentInput } = useSpc();
const option = computed(() => {
const input = currentInput.value;
if (!input) return null;
return createRunChartOption({
values: input.x,
usl: input.usl,
lsl: input.lsl,
target: (input.usl + input.lsl) / 2,
movingAverageWindow: input.n
});
});
const metrics = computed(() => {
const input = currentInput.value;
if (!input) return [];
const overSpecCount = input.x.filter((value) => value > input.usl || value < input.lsl).length;
const average = input.x.reduce((sum, value) => sum + value, 0) / input.x.length;
return [
{ label: "样本数", value: input.x.length },
{ label: "移动平均窗口", value: input.n },
{ label: "均值", value: Number(average.toFixed(6)) },
{ label: "目标值", value: Number(((input.usl + input.lsl) / 2).toFixed(6)) },
{ label: "超规格点", value: overSpecCount, tone: overSpecCount === 0 ? ("ok" as const) : ("warn" as const) },
{ label: "规格限", value: `${input.lsl} ~ ${input.usl}` }
];
});
</script>
<template>
<article class="page-stack">
<MetricStrip v-if="metrics.length" :metrics="metrics" />
<EChartPanel
title="基本趋势图"
eyebrow="Run Chart"
description="基本趋势图按原始采集顺序展示测量值,叠加均值线、中位线、目标值、规格限和移动平均线。它不替代控制图,主要用于先观察漂移、跳变、周期波动和超规格点。"
:option="option"
/>
</article>
</template>

View File

@@ -0,0 +1,45 @@
<script setup lang="ts">
import { computed } from "vue";
import EChartPanel from "../components/EChartPanel.vue";
import MetricStrip from "../components/MetricStrip.vue";
import { useSpc } from "../composables/useSpc";
import { createControlLimitOption } from "../utils/chartOptions";
const { result } = useSpc();
const option = computed(() => {
const xs = result.value?.XS;
if (!xs) return null;
return createControlLimitOption({
title: "S 标准差控制图",
valueName: "子组标准差",
values: xs.CL_Sk,
center: xs.CL_S,
upper: xs.UCL_S,
lower: xs.LCL_S
});
});
const metrics = computed(() => {
const xs = result.value?.XS;
if (!xs) return [];
return [
{ label: "CL_S", value: xs.CL_S },
{ label: "UCL_S", value: xs.UCL_S },
{ label: "LCL_S", value: xs.LCL_S },
{ label: "子组容量", value: xs.n }
];
});
</script>
<template>
<article class="page-stack">
<MetricStrip v-if="metrics.length" :metrics="metrics" />
<EChartPanel
title="S 标准差控制图"
eyebrow="Standard Deviation"
description="S 图使用每个子组的样本标准差观察组内离散程度,对标准差变化更敏感,常用于较大子组数据。"
:option="option"
/>
</article>
</template>

View File

@@ -0,0 +1,46 @@
<script setup lang="ts">
import { computed } from "vue";
import EChartPanel from "../components/EChartPanel.vue";
import MetricStrip from "../components/MetricStrip.vue";
import { useSpc } from "../composables/useSpc";
import { createControlLimitOption } from "../utils/chartOptions";
const { result } = useSpc();
const chart = computed(() => {
const xr = result.value?.XR;
if (!xr) return null;
return {
title: "Xbar 控制图XR 控制限)",
valueName: "子组均值",
values: xr.CL_Xk,
center: xr.CL_X,
upper: xr.UCL_X,
lower: xr.LCL_X
};
});
const option = computed(() => (chart.value ? createControlLimitOption(chart.value) : null));
const metrics = computed(() => {
const xr = result.value?.XR;
if (!xr) return [];
return [
{ label: "CL_X", value: xr.CL_X },
{ label: "UCL_X", value: xr.UCL_X },
{ label: "LCL_X", value: xr.LCL_X },
{ label: "子组数", value: xr.k }
];
});
</script>
<template>
<article class="page-stack">
<MetricStrip v-if="metrics.length" :metrics="metrics" />
<EChartPanel
title="Xbar-R均值控制图"
eyebrow="Xbar-R"
description="Xbar 图使用每个子组的均值观察过程中心是否稳定;这里的均值控制限来自 XR 结果,适合与 R 图一起验证小子组数据。"
:option="option"
/>
</article>
</template>

View File

@@ -0,0 +1,45 @@
<script setup lang="ts">
import { computed } from "vue";
import EChartPanel from "../components/EChartPanel.vue";
import MetricStrip from "../components/MetricStrip.vue";
import { useSpc } from "../composables/useSpc";
import { createControlLimitOption } from "../utils/chartOptions";
const { result } = useSpc();
const option = computed(() => {
const xs = result.value?.XS;
if (!xs) return null;
return createControlLimitOption({
title: "Xbar 控制图XS 控制限)",
valueName: "子组均值",
values: xs.CL_Xk,
center: xs.CL_X,
upper: xs.UCL_X,
lower: xs.LCL_X
});
});
const metrics = computed(() => {
const xs = result.value?.XS;
if (!xs) return [];
return [
{ label: "CL_X", value: xs.CL_X },
{ label: "UCL_X", value: xs.UCL_X },
{ label: "LCL_X", value: xs.LCL_X },
{ label: "子组数", value: xs.k }
];
});
</script>
<template>
<article class="page-stack">
<MetricStrip v-if="metrics.length" :metrics="metrics" />
<EChartPanel
title="Xbar-S均值控制图"
eyebrow="Xbar-S"
description="Xbar-S 中的均值图同样观察过程中心,但控制限由样本标准差口径计算,适合和 S 图配套检查过程稳定性。"
:option="option"
/>
</article>
</template>

19
spc-vue/tsconfig.json Normal file
View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"strict": true,
"types": ["vite/client"]
},
"include": ["src/**/*.ts", "src/**/*.vue"],
"references": []
}

48
spc-vue/vite.config.ts Normal file
View File

@@ -0,0 +1,48 @@
import fs from "node:fs";
import path from "node:path";
import vue from "@vitejs/plugin-vue";
import { defineConfig, type Plugin } from "vite";
const repoWasmDir = path.resolve(__dirname, "../public/wasm");
// 复用主项目已经构建好的 WASM 文件,开发时映射 /wasm打包时复制到 dist/wasm。
function repoWasmAssets(): Plugin {
return {
name: "repo-wasm-assets",
configureServer(server) {
server.middlewares.use("/wasm", (req, res, next) => {
const requestPath = decodeURIComponent((req.url || "").split("?")[0].replace(/^\/+/, ""));
const filePath = path.resolve(repoWasmDir, requestPath);
if (!filePath.startsWith(repoWasmDir) || !fs.existsSync(filePath)) {
next();
return;
}
if (filePath.endsWith(".wasm")) {
res.setHeader("Content-Type", "application/wasm");
} else if (filePath.endsWith(".js")) {
res.setHeader("Content-Type", "application/javascript; charset=utf-8");
}
fs.createReadStream(filePath).pipe(res);
});
},
closeBundle() {
const outDir = path.resolve(__dirname, "dist/wasm");
fs.mkdirSync(outDir, { recursive: true });
for (const assetName of ["smart_math.js", "smart_math.wasm"]) {
const source = path.join(repoWasmDir, assetName);
if (fs.existsSync(source)) {
fs.copyFileSync(source, path.join(outDir, assetName));
}
}
}
};
}
export default defineConfig({
plugins: [vue(), repoWasmAssets()],
server: {
port: 5174
}
});

View File

@@ -1,609 +0,0 @@
#include "QuadrupedRobotSimulation/KinematicsHelper.h"
#include "KinematicsWebAPI.h"
#include "FourBarMechanism/CrankSliderMechanism.h"
#include "URDFStrings.h"
#include <iostream>
#include <thread>
#include <chrono>
#include <iomanip>
#include <sstream>
#include <codecvt>
#include <locale>
#include <csignal>
// inversePoseStr2PSteps
KinematicsWebAPI::KinematicsWebAPI()
{
init_func();
}
std::string KinematicsWebAPI::init_func()
{
std::string response_string;
std::string urdf_string = URDFStrings::abb120_urdf; // 使用新的命名空间访问
auto result = RobotManager::initRobot(urdf_string, "9D7EAEF4-1AAB-499E-8783-B6CE016BC6D1");
auto result1 = RobotManager::initRobot(urdf_string, "abb_irb120_3_58");
json error_response = utils::create_api_response(true, 200, "init_func", "", "", "");
response_string = error_response.dump();
return response_string;
}
std::string KinematicsWebAPI::func(std::string sanitized_body)
{
// 添加调试输出
log("func called with body: " + sanitized_body.substr(0, 100));
assert(!sanitized_body.empty() && "sanitized_body should not be empty");
std::string response_string;
try
{
json request_json;
// 构建响应数据
json result;
try
{
request_json = json::parse(sanitized_body);
}
catch (const std::exception &e)
{
json error_response = utils::create_api_response(false, 400, "Invalid JSON format", "", "", "");
response_string = error_response.dump();
return response_string;
}
std::string msg = request_json.value("msg", "");
std::string req_code = request_json.value("req_code", "");
std::string req_from = request_json.value("req_from", "");
std::string req_cmd = request_json.value("req_cmd", "");
json req_param = request_json.value("req_param", json::object());
json res_data;
// 使用if语句直接处理对应的命令不通过中间函数
if (req_cmd == "Cmd_Kinematics_inverse_pose_str")
{
try
{
log("Handling Cmd_Kinematics_inverse_pose_str command");
std::string pose_str = req_param.value("pose_str", "");
std::string q_init_str = req_param.value("q_init_str", "0,0,0,0,0,0");
std::string robot_uuid = req_param.value("robot_uuid", "default");
auto robot = RobotManager::getRobot(robot_uuid);
if (!robot || !robot->isInitialized())
{
res_data = {{"error", "Robot not found or not initialized"}};
}
else
{
result = robot->inversePoseStr(pose_str, q_init_str);
if (result.empty())
{
res_data = {{"error", "Inverse kinematics calculation failed"}};
}
else
{
res_data = {{"joints", result}, {"success", true}};
}
}
}
catch (const std::exception &e)
{
res_data = {{"error", "Failed to calculate inverse kinematics: " + std::string(e.what())}};
}
}
else if (req_cmd == "Cmd_Kinematics_inverse_pose_str_2PSteps")
{
try
{
log("Handling Cmd_Kinematics_inverse_pose_str_2PSteps command");
std::string pose_str = req_param.value("pose_str", "");
std::string q_init_str = req_param.value("q_init_str", "0,0,0,0,0,0");
std::string robot_uuid = req_param.value("robot_uuid", "default");
std::string steps_str = req_param.value("steps_str", "default");
int steps = std::stoi(steps_str);
auto robot = RobotManager::getRobot(robot_uuid);
if (!robot || !robot->isInitialized())
{
res_data = {{"error", "Robot not found or not initialized"}};
}
else
{
result = robot->inversePoseStr2PSteps(pose_str, q_init_str, steps);
if (result.empty())
{
res_data = {{"error", "Inverse kinematics calculation failed"}};
}
else
{
res_data = {{"joints", result}, {"success", true}};
}
}
}
catch (const std::exception &e)
{
res_data = {{"error", "Failed to calculate inverse kinematics: " + std::string(e.what())}};
}
}
else if (req_cmd == "Cmd_Kinematics_inverse_pose_str_NoDifference")
{
try
{
log("Handling Cmd_Kinematics_inverse_pose_strNoDifference command");
std::string pose_str = req_param.value("pose_str", "");
std::string q_init_str = req_param.value("q_init_str", "0,0,0,0,0,0");
std::string robot_uuid = req_param.value("robot_uuid", "default");
auto robot = RobotManager::getRobot(robot_uuid);
if (!robot || !robot->isInitialized())
{
res_data = {{"error", "Robot not found or not initialized"}};
}
else
{
result = robot->inversePoseStrNoDifference(pose_str, q_init_str);
if (result.empty())
{
res_data = {{"error", "Inverse kinematics calculation failed"}};
}
else
{
res_data = {{"joints", result}, {"success", true}};
}
}
}
catch (const std::exception &e)
{
res_data = {{"error", "Failed to calculate inverse kinematics: " + std::string(e.what())}};
}
}
else if (req_cmd == "Cmd_Kinematics_forward_pose_str")
{
try
{
log("Handling Cmd_Kinematics_forward_pose_str command");
std::string joints_str = req_param.value("q_init_str", "0,0,0,0,0,0");
std::string robot_uuid = req_param.value("robot_uuid", "default");
auto robot = RobotManager::getRobot(robot_uuid);
if (!robot || !robot->isInitialized())
{
res_data = {{"error", "Robot not found or not initialized"}};
}
else
{
auto joints = robot->parseJointString(joints_str);
if (joints.size() != 6)
{
res_data = {{"error", "Invalid joints format"}};
}
else
{
double tcp_pose[7];
if (robot->calculateFK_TCP(joints.data(), tcp_pose))
{
res_data = {
{"position", {tcp_pose[0], tcp_pose[1], tcp_pose[2]}},
{"orientation", {tcp_pose[3], tcp_pose[4], tcp_pose[5], tcp_pose[6]}},
{"joints", joints},
{"success", true}};
}
else
{
res_data = {{"error", "Forward kinematics calculation failed"}};
}
}
}
}
catch (const std::exception &e)
{
res_data = {{"error", "Failed to calculate forward kinematics: " + std::string(e.what())}};
}
}
else if (req_cmd == "Cmd_SelectCraftTree")
{
try
{
log("Handling Cmd_SelectCraftTree command");
std::string tree_id = req_param.value("tree_id", "");
res_data = {
{"success", true},
{"tree_id", tree_id},
{"message", "Craft tree selected successfully"},
{"timestamp", utils::get_current_time()}};
}
catch (const std::exception &e)
{
res_data = {{"error", "Failed to select craft tree: " + std::string(e.what())}};
}
}
else if (req_cmd == "Cmd_AddOperationTree")
{
try
{
log("Handling Cmd_AddOperationTree command");
std::string tree_name = req_param.value("name", "");
json operations = req_param.value("operations", json::array());
res_data = {
{"success", true},
{"tree_name", tree_name},
{"operations_count", operations.size()},
{"message", "Operation tree added successfully"},
{"timestamp", utils::get_current_time()}};
}
catch (const std::exception &e)
{
res_data = {{"error", "Failed to add operation tree: " + std::string(e.what())}};
}
}
else if (req_cmd == "Cmd_InitRobot")
{
try
{
log("Handling Cmd_InitRobot command");
std::string urdf_base64 = req_param.value("urdf_base64", "");
std::string uuid = req_param.value("robot_uuid", "");
bool force_update = req_param.value("force_update", true);
std::string urdf_content = utils::base64_to_urdf(urdf_base64);
if (!utils::validate_urdf_base64(urdf_base64))
{
res_data = {
{"success", false},
{"message", "Invalid URDF format"},
{"timestamp", utils::get_current_time()}};
}
else
{
auto result = RobotManager::initRobot(urdf_content, uuid, force_update);
res_data = {
{"success", result.first},
{"message", result.second},
{"timestamp", utils::get_current_time()}};
}
}
catch (const std::exception &e)
{
res_data = {{"error", "Failed to initialize robot: " + std::string(e.what())}};
}
}
else if (req_cmd == "Cmd_GetRobot")
{
try
{
log("Handling Cmd_GetRobot command");
std::string uuid = req_param.value("uuid", "");
auto robot = RobotManager::getRobot(uuid);
if (!robot)
{
res_data = {{"error", "Robot not found"}};
}
else
{
res_data = {
{"success", true},
{"uuid", uuid},
{"initialized", robot->isInitialized()},
{"joints_count", robot->getNumberOfJoints()},
{"timestamp", utils::get_current_time()}};
}
}
catch (const std::exception &e)
{
res_data = {{"error", "Failed to get robot: " + std::string(e.what())}};
}
}
else if (req_cmd == "Cmd_RemoveRobot")
{
try
{
log("Handling Cmd_RemoveRobot command");
std::string uuid = req_param.value("uuid", "");
auto result = RobotManager::removeRobot(uuid);
res_data = {
{"success", result.first},
{"message", result.second},
{"timestamp", utils::get_current_time()}};
}
catch (const std::exception &e)
{
res_data = {{"error", "Failed to remove robot: " + std::string(e.what())}};
}
}
else if (req_cmd == "Cmd_ListRobots")
{
try
{
log("Handling Cmd_ListRobots command");
bool detail = req_param.value("detail", false);
auto robots = RobotManager::listRobots(detail);
res_data = {
{"success", true},
{"robots", robots},
{"count", robots.size()},
{"timestamp", utils::get_current_time()}};
}
catch (const std::exception &e)
{
res_data = {{"error", "Failed to list robots: " + std::string(e.what())}};
}
}
else if (req_cmd == "Cmd_Kinematics_forward_all_joints")
{
try
{
log("Handling Cmd_Kinematics_forward_all_joints command");
std::string joints_str = req_param.value("joints_str", "0,0,0,0,0,0");
std::string robot_uuid = req_param.value("robot_uuid", "default");
auto robot = RobotManager::getRobot(robot_uuid);
if (!robot || !robot->isInitialized())
{
res_data = {{"error", "Robot not found or not initialized"}};
}
else
{
auto joints_poses_array = robot->handleKinematicsForwardAllJoints(joints_str);
res_data = {
{"OPERATION", joints_poses_array},
{"success", true}};
}
}
catch (const std::exception &e)
{
res_data = {{"error", "Failed to calculate forward kinematics for all joints: " + std::string(e.what())}};
}
}
else if (req_cmd == "Cmd_Spc")
{
try
{
log("Handling " + req_cmd + " command");
json result = SpcCalculator::Spc(req_param);
res_data = result;
}
catch (const std::exception &e)
{
res_data = {{"error", "Failed to Cmd_Spc: " + std::string(e.what())}};
}
}
else if (req_cmd == "Cmd_FourBar_CrankSlider")
{
// 请求参数示例:
// {
// "L_AB": 0.5,
// "L_BS": 2.0,
// "S_OFS": 0.0,
// "angleDeg": 45.0
// }
try
{
log("Handling " + req_cmd + " command");
// 从请求参数中提取值
double L_AB = req_param.value("L_AB", 0.5);
double L_BS = req_param.value("L_BS", 2.0);
double S_OFS = req_param.value("S_OFS", 0.0);
double angleDeg = req_param.value("angleDeg", 0.0);
log("Parameters: L_AB=" + std::to_string(L_AB) +
", L_BS=" + std::to_string(L_BS) +
", S_OFS=" + std::to_string(S_OFS) +
", angleDeg=" + std::to_string(angleDeg));
// 创建曲柄滑块机构实例
std::unique_ptr<CrankSliderMechanism> mechanism(createCrankSliderMechanism());
// 设置连杆参数
mechanism->setL_AB(L_AB);
mechanism->setL_BS(L_BS);
mechanism->setS_OFS(S_OFS);
// 验证参数
ValidationResult validation = mechanism->validateParameters();
if (!validation.isValid())
{
res_data = {
{"success", false},
{"error", "Invalid parameters"},
{"validation_errors", validation.Errors},
{"validation_warnings", validation.Warnings}};
}
else
{
// 执行计算
MechanismState state = mechanism->calculate(angleDeg);
// 检查是否有错误
if (state.hasError())
{
res_data = {
{"success", false},
{"error", state.ErrorMessage}};
}
else
{
// 构建响应数据
json result;
result["success"] = true;
// 添加点坐标
json points_json;
for (const auto &point_pair : state.Points)
{
json point;
point["x"] = point_pair.second.X;
point["y"] = point_pair.second.Y;
points_json[point_pair.first] = point;
}
result["points"] = points_json;
// 添加姿态信息
json poses_json;
for (const auto &pose_pair : state.Poses)
{
json pose;
pose["tx"] = pose_pair.second.tx;
pose["ty"] = pose_pair.second.ty;
pose["tz"] = pose_pair.second.tz;
pose["qx"] = pose_pair.second.qx;
pose["qy"] = pose_pair.second.qy;
pose["qz"] = pose_pair.second.qz;
pose["qw"] = pose_pair.second.qw;
poses_json[pose_pair.first] = pose;
}
result["poses"] = poses_json;
// 添加角度信息
json angles_json;
for (const auto &angle_pair : state.Angles)
{
angles_json[angle_pair.first] = angle_pair.second;
}
result["angles"] = angles_json;
// 添加输入值
result["input_value"] = state.InputValue;
// 添加警告信息(如果有)
if (state.hasWarning())
{
result["warning"] = state.WarningMessage;
}
// 添加轨迹信息
std::vector<Vector2D> trajectory = mechanism->getTrajectoryPoints();
std::vector<Vector2D> slider_trajectory = mechanism->getSliderTrajectory();
json trajectory_json;
for (size_t i = 0; i < trajectory.size(); i++)
{
json point;
point["x"] = trajectory[i].X;
point["y"] = trajectory[i].Y;
trajectory_json.push_back(point);
}
result["trajectory"] = trajectory_json;
json slider_trajectory_json;
for (size_t i = 0; i < slider_trajectory.size(); i++)
{
json point;
point["x"] = slider_trajectory[i].X;
point["y"] = slider_trajectory[i].Y;
slider_trajectory_json.push_back(point);
}
result["slider_trajectory"] = slider_trajectory_json;
// 添加参数信息
result["parameters"] = {
{"L_AB", L_AB},
{"L_BS", L_BS},
{"S_OFS", S_OFS},
{"angleDeg", angleDeg}};
res_data = result;
}
}
}
catch (const std::exception &e)
{
res_data = {
{"success", false},
{"error", "Failed to Cmd_FourBar_CrankSlider: " + std::string(e.what())}};
}
}
else if (req_cmd == "Cmd_QuadrupedRobot_CalculateAllPointsFromMotorAngles")
{
// json j = json::parse(jsonStr);
std::cout << "[DEBUG 0001] Cmd_QuadrupedRobot_CalculateAllPointsFromMotorAngles 步骤1: 初始化数据结构" << std::endl;
std::string jsonInput = req_param.dump();
result = KinematicsHelper::QuadrupedRobot_CalculateAllPointsFromMotorAngles(jsonInput);
// if (req_param.empty())
// {
// result = KinematicsHelper::QuadrupedRobot_CalculateAllPointsFromMotorAngles();
// }
// else
// {
// std::string jsonInput = req_param.dump();
// result = KinematicsHelper::QuadrupedRobot_CalculateAllPointsFromMotorAngles(jsonInput);
// }
res_data = result;
}
else if (req_cmd == "Cmd_QuadrupedRobot_PerformForwardKinematics")
{
std::cout << "[DEBUG] Cmd_QuadrupedRobot_PerformForwardKinematics = " << std::endl;
std::string jsonInput = req_param.dump();
result = KinematicsHelper::QuadrupedRobot_PerformForwardKinematics(jsonInput);
// if (req_param.empty())
// {
// result = KinematicsHelper::QuadrupedRobot_PerformForwardKinematics();
// }
// else
// {
// std::string jsonInput = req_param.dump();
// result = KinematicsHelper::QuadrupedRobot_PerformForwardKinematics(jsonInput);
// }
res_data = result;
}
else
{
// 未知命令,调用默认处理函数
res_data["success"] = false;
res_data["error"] = "Unknown command: " + req_cmd;
res_data["received_params"] = req_param;
res_data["timestamp"] = std::time(nullptr);
}
json response = utils::create_api_response(true, 0, msg, req_code, req_from, req_cmd, res_data);
response_string = response.dump();
}
catch (const std::exception &e)
{
json error_response = utils::create_api_response(false, 500, "Processing error: " + std::string(e.what()), "", "", "");
response_string = error_response.dump();
}
return response_string;
}
void KinematicsWebAPI::log(const std::string &message)
{
std::cout << "[" << getCurrentTimestamp() << "] " << message << std::endl;
if (onLog)
{
onLog("[" + getCurrentTimestamp() + "] " + message);
}
}
std::string KinematicsWebAPI::getCurrentTimestamp()
{
return utils::get_current_timestamp();
}
bool KinematicsWebAPI::is_running() const
{
return running_;
}

View File

@@ -84,9 +84,6 @@ void KinematicsHelper::SimRobot()
json KinematicsHelper::QuadrupedRobot_CalculateAllPointsFromMotorAngles(const std::string &jsonInput)
{
std::cout << "[DEBUG] json KinematicsHelper::QuadrupedRobot_CalculateAllPointsFromMotorAngles(const std::string &jsonInput) " << std::endl;
std::string jsonStr = "{}";
auto manager = RobotGaitDataManagerFromJson(jsonInput);
ReverseKinematicsCalculator simulation(manager);
@@ -96,9 +93,6 @@ json KinematicsHelper::QuadrupedRobot_CalculateAllPointsFromMotorAngles(const st
}
json KinematicsHelper::QuadrupedRobot_PerformForwardKinematics(const std::string &jsonInput)
{
std::cout << "[DEBUG] json KinematicsHelper::QuadrupedRobot_PerformForwardKinematics(const std::string &jsonInput) " << std::endl;
auto manager = RobotGaitDataManagerFromJson(jsonInput);
QuadrupedRobotConfiguration config(manager->GetGaitInfo(), manager->GetSystemParameters());
QuadrupedRobotSimulation simulation(config);

View File

@@ -972,8 +972,6 @@ std::string ReverseKinematicsCalculator::CalculateAllPointsFromMotorAnglesJsonSt
if (paramDict.empty())
{
std::cout << "[DEBUG] 没有Param数据 json KinematicsHelper::QuadrupedRobot_CalculateAllPointsFromMotorAngles(const std::string &jsonInput) " << std::endl;
return "没有Param数据";
}
// debugPrintParamDict(paramDict);
@@ -991,14 +989,6 @@ std::string ReverseKinematicsCalculator::CalculateAllPointsFromMotorAnglesJsonSt
auto points_dict = CalculateAllPointsFromMotorAnglesReverse(
thigh_angle_deg, shank_angle_deg, ankle_angle_deg, leg_type);
// std::cout << "计算得到的点数量: " << points_dict.size() << "\n";
// std::cout << "包含的点: ";
for (const auto &p : points_dict)
{
std::cout << p.first << " ";
}
// std::cout << "\n";
// 改为:
auto modelID = _manager->GetModelIDObject(); // 使用新方法

View File

@@ -886,11 +886,6 @@ void QuadrupedRobotSimulation::CalculateMotorData()
int phase_RF = static_cast<int>(timeRF * n_frames);
int phase_RH = static_cast<int>(timeRH * n_frames);
std::cout << "步态类型: " << gait_type << std::endl;
std::cout << "总帧数: " << n_frames << std::endl;
std::cout << "相位偏移(帧): LF=0, LH=" << phase_LH
<< ", RF=" << phase_RF << ", RH=" << phase_RH << std::endl;
// 1. 计算左前腿基准数据
motor_data_LF_raw = CalculateIncrementsAndVelocities();

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,167 @@
#include "KinematicsWebAPI.h"
#include "URDFStrings.h"
#include <cassert>
#include <ctime>
#include <iostream>
#include <string>
namespace
{
bool isRobotCommand(const std::string &req_cmd)
{
return req_cmd == "Cmd_Kinematics_inverse_pose_str" ||
req_cmd == "Cmd_Kinematics_inverse_pose_str_2PSteps" ||
req_cmd == "Cmd_Kinematics_inverse_pose_str_NoDifference" ||
req_cmd == "Cmd_Kinematics_forward_pose_str" ||
req_cmd == "Cmd_Kinematics_forward_all_joints" ||
req_cmd == "Cmd_Kinematics_check_singularity" ||
req_cmd == "Cmd_InitRobot" ||
req_cmd == "Cmd_GetRobot" ||
req_cmd == "Cmd_RemoveRobot" ||
req_cmd == "Cmd_ListRobots" ||
req_cmd == "Cmd_SelectCraftTree" ||
req_cmd == "Cmd_AddOperationTree";
}
// 从业务返回体推导顶层 API 是否成功,避免把业务错误包装成成功响应。
bool isBusinessResultSuccess(const json &res_data)
{
if (!res_data.is_object())
{
return true;
}
if (res_data.contains("success") && res_data["success"].is_boolean())
{
return res_data["success"].get<bool>();
}
return !res_data.contains("error");
}
// 提取业务错误信息,用于同步顶层 msg 字段。
std::string getBusinessResultMessage(const json &res_data, const std::string &fallback)
{
if (isBusinessResultSuccess(res_data))
{
return fallback;
}
if (res_data.contains("message") && res_data["message"].is_string())
{
return res_data["message"].get<std::string>();
}
if (res_data.contains("error") && res_data["error"].is_string())
{
return res_data["error"].get<std::string>();
}
return fallback.empty() ? "Business command failed" : fallback;
}
} // namespace
KinematicsWebAPI::KinematicsWebAPI()
{
init_func();
}
std::string KinematicsWebAPI::init_func()
{
std::string urdf_string = URDFStrings::abb120_urdf;
RobotManager::initRobot(urdf_string, "9D7EAEF4-1AAB-499E-8783-B6CE016BC6D1");
RobotManager::initRobot(urdf_string, "abb_irb120_3_58");
return utils::create_api_response(true, 200, "init_func", "", "", "").dump();
}
std::string KinematicsWebAPI::func(std::string sanitized_body)
{
log("func called with body: " + sanitized_body.substr(0, 100));
assert(!sanitized_body.empty() && "sanitized_body should not be empty");
try
{
json request_json;
try
{
request_json = json::parse(sanitized_body);
}
catch (const std::exception &)
{
return utils::create_api_response(false, 400, "Invalid JSON format", "", "", "").dump();
}
const std::string msg = request_json.value("msg", "");
const std::string req_code = request_json.value("req_code", "");
const std::string req_from = request_json.value("req_from", "");
const std::string req_cmd = request_json.value("req_cmd", "");
const json req_param = request_json.value("req_param", json::object());
json res_data = dispatchCommand(req_cmd, req_param);
const bool business_success = isBusinessResultSuccess(res_data);
const int code = business_success ? 0 : 1000;
const std::string response_msg = getBusinessResultMessage(res_data, msg);
return utils::create_api_response(business_success, code, response_msg, req_code, req_from, req_cmd, res_data).dump();
}
catch (const std::exception &e)
{
return utils::create_api_response(false, 500, "Processing error: " + std::string(e.what()), "", "", "").dump();
}
}
json KinematicsWebAPI::dispatchCommand(const std::string &req_cmd, const json &req_param)
{
if (isRobotCommand(req_cmd))
{
return handleRobotCommand(req_cmd, req_param);
}
if (req_cmd == "Cmd_Spc")
{
return handleSpcCommand(req_cmd, req_param);
}
if (req_cmd == "Cmd_FourBar_Simulate" ||
req_cmd == "Cmd_FourBar_CrankSlider" ||
req_cmd == "Cmd_FourBar_CrankSlider_Simulate")
{
return handleFourBarCommand(req_cmd, req_param);
}
if (req_cmd == "Cmd_QuadrupedRobot_CalculateAllPointsFromMotorAngles" ||
req_cmd == "Cmd_QuadrupedRobot_PerformForwardKinematics")
{
return handleQuadrupedCommand(req_cmd, req_param);
}
return createUnknownCommandResponse(req_cmd, req_param);
}
json KinematicsWebAPI::createUnknownCommandResponse(const std::string &req_cmd, const json &req_param) const
{
return {
{"success", false},
{"error", "Unknown command: " + req_cmd},
{"received_params", req_param},
{"timestamp", std::time(nullptr)}};
}
void KinematicsWebAPI::log(const std::string &message)
{
if (onLog)
{
onLog("[" + getCurrentTimestamp() + "] " + message);
}
}
std::string KinematicsWebAPI::getCurrentTimestamp()
{
return utils::get_current_timestamp();
}
bool KinematicsWebAPI::is_running() const
{
return running_;
}

View File

@@ -0,0 +1,527 @@
#include "KinematicsWebAPI.h"
#include "FourBarMechanism/CrankSliderMechanism.h"
#include "FourBarMechanism/CrankRockingBlockMechanism_Forward.h"
#include "FourBarMechanism/FourBarMechanism.h"
#include "FourBarMechanism/SliderCrankMechanism.h"
#include <cmath>
#include <memory>
namespace
{
// 读取兼容字段,便于新统一接口沿用旧曲柄滑块参数名。
double valueAny(const json &param, std::initializer_list<const char *> keys, double defaultValue)
{
for (const char *key : keys)
{
if (param.contains(key) && param[key].is_number())
{
return param[key].get<double>();
}
}
return defaultValue;
}
// 构造统一的业务失败返回,供四类连杆仿真复用。
json buildErrorJson(const std::string &error, const json &validationErrors = json::array(), const json &validationWarnings = json::array())
{
json result = {
{"success", false},
{"error", error}};
if (!validationErrors.empty())
{
result["validation_errors"] = validationErrors;
}
if (!validationWarnings.empty())
{
result["validation_warnings"] = validationWarnings;
}
return result;
}
// 将机构点位转换为接口统一 JSON。
json buildPointsJson(const MechanismState &state)
{
json points_json;
for (const auto &point_pair : state.Points)
{
points_json[point_pair.first] = {
{"x", point_pair.second.X},
{"y", point_pair.second.Y}};
}
return points_json;
}
// 将机构姿态转换为接口统一 JSON。
json buildPosesJson(const MechanismState &state)
{
json poses_json;
for (const auto &pose_pair : state.Poses)
{
poses_json[pose_pair.first] = {
{"tx", pose_pair.second.tx},
{"ty", pose_pair.second.ty},
{"tz", pose_pair.second.tz},
{"qx", pose_pair.second.qx},
{"qy", pose_pair.second.qy},
{"qz", pose_pair.second.qz},
{"qw", pose_pair.second.qw}};
}
return poses_json;
}
// 将机构角度数据转换为接口统一 JSON。
json buildAnglesJson(const MechanismState &state)
{
json angles_json;
for (const auto &angle_pair : state.Angles)
{
angles_json[angle_pair.first] = angle_pair.second;
}
return angles_json;
}
// 将二维点列表转换为轨迹 JSON 数组。
json buildTrajectoryJson(const std::vector<Vector2D> &points)
{
json trajectory_json = json::array();
for (const auto &point : points)
{
trajectory_json.push_back({
{"x", point.X},
{"y", point.Y}});
}
return trajectory_json;
}
// 组装单帧机构状态,供实时接口和批量仿真接口复用。
json buildFrameJson(const MechanismState &state, int frame, double time, double inputValue, const std::string &inputName)
{
json frame_json;
frame_json["frame"] = frame;
frame_json["time"] = time;
frame_json[inputName] = inputValue;
frame_json["inputName"] = inputName;
frame_json["inputValue"] = inputValue;
frame_json["input_value"] = state.InputValue;
frame_json["points"] = buildPointsJson(state);
frame_json["poses"] = buildPosesJson(state);
frame_json["angles"] = buildAnglesJson(state);
if (state.hasWarning())
{
frame_json["warning"] = state.WarningMessage;
}
return frame_json;
}
// 校验仿真时间参数并返回帧数。
json validateSimulationTimeline(double duration, double stepTime, int &frameCount)
{
if (duration <= 0.0)
{
return buildErrorJson("Invalid simulation parameters", json::array({"duration 必须大于 0"}));
}
if (stepTime <= 0.0 || stepTime > duration)
{
return buildErrorJson("Invalid simulation parameters", json::array({"stepTime 必须大于 0 且不能大于 duration"}));
}
frameCount = static_cast<int>(std::ceil(duration / stepTime)) + 1;
if (frameCount < 2 || frameCount > 1000)
{
return buildErrorJson("Invalid simulation parameters", json::array({"仿真帧数必须在 2 到 1000 之间"}));
}
return json();
}
// 将 ValidationResult 转换为统一业务错误。
json validateOrError(const ValidationResult &validation)
{
if (validation.isValid())
{
return json();
}
return buildErrorJson("Invalid parameters", validation.Errors, validation.Warnings);
}
// 按统一格式生成逐帧仿真数据。
template <typename CalculateFunc>
json buildSimulationFrames(int frameCount, double duration, double stepTime, double startValue, double endValue, const std::string &inputName, CalculateFunc calculate)
{
json frames_json = json::array();
for (int frame = 0; frame < frameCount; ++frame)
{
const double currentTime = frame == frameCount - 1 ? duration : stepTime * frame;
const double progress = duration <= 0.0 ? 0.0 : currentTime / duration;
const double currentValue = startValue + (endValue - startValue) * progress;
MechanismState state = calculate(currentValue);
if (state.hasError())
{
return {
{"success", false},
{"error", state.ErrorMessage},
{"failed_frame", frame},
{"failed_time", currentTime},
{"failed_input", currentValue},
{"failed_input_name", inputName}};
}
frames_json.push_back(buildFrameJson(state, frame, currentTime, currentValue, inputName));
}
return frames_json;
}
// 统一四连杆仿真接口,覆盖 PDPS 的 PRRR/RPRR/RRRP/RRRR 四类结构。
json simulateUnifiedFourBar(const json &req_param)
{
const std::string mechanismType = req_param.value("mechanismType", "RRRP");
const double duration = req_param.value("duration", 2.0);
const double stepTime = req_param.value("stepTime", 0.02);
int frameCount = 0;
json timelineError = validateSimulationTimeline(duration, stepTime, frameCount);
if (!timelineError.is_null())
{
return timelineError;
}
if (mechanismType == "RRRP")
{
const double L_AB = req_param.value("L_AB", 0.5);
const double L_BS = req_param.value("L_BS", 2.0);
const double S_OFS = req_param.value("S_OFS", 0.0);
const double startAngleDeg = valueAny(req_param, {"startAngleDeg", "startValue", "angleDeg"}, 0.0);
const double endAngleDeg = valueAny(req_param, {"endAngleDeg", "endValue"}, 360.0);
std::unique_ptr<CrankSliderMechanism> mechanism(createCrankSliderMechanism());
mechanism->setL_AB(L_AB);
mechanism->setL_BS(L_BS);
mechanism->setS_OFS(S_OFS);
json validationError = validateOrError(mechanism->validateParameters());
if (!validationError.is_null())
{
return validationError;
}
json frames_json = buildSimulationFrames(frameCount, duration, stepTime, startAngleDeg, endAngleDeg, "angleDeg", [&](double value) {
return mechanism->calculate(value);
});
if (!frames_json.is_array())
{
return frames_json;
}
return {
{"success", true},
{"mechanismType", mechanismType},
{"inputName", "angleDeg"},
{"frames", frames_json},
{"frame_count", frameCount},
{"duration", duration},
{"stepTime", stepTime},
{"startValue", startAngleDeg},
{"endValue", endAngleDeg},
{"startAngleDeg", startAngleDeg},
{"endAngleDeg", endAngleDeg},
{"trajectory", buildTrajectoryJson(mechanism->getTrajectoryPoints())},
{"slider_trajectory", buildTrajectoryJson(mechanism->getSliderTrajectory())},
{"parameters", {{"mechanismType", mechanismType}, {"L_AB", L_AB}, {"L_BS", L_BS}, {"S_OFS", S_OFS}, {"startAngleDeg", startAngleDeg}, {"endAngleDeg", endAngleDeg}, {"duration", duration}, {"stepTime", stepTime}}}};
}
if (mechanismType == "PRRR")
{
const double L_AB = req_param.value("L_AB", 0.5);
const double L_BS = req_param.value("L_BS", 2.0);
const double S_OFS = req_param.value("S_OFS", 0.0);
std::unique_ptr<SliderCrankMechanism> mechanism(createSliderCrankMechanism());
mechanism->setLink(L_AB, L_BS, S_OFS);
json validationError = validateOrError(mechanism->validateParameters());
if (!validationError.is_null())
{
return validationError;
}
const auto inputRange = mechanism->getInputRange();
const double startSliderX = valueAny(req_param, {"startSliderX", "startValue", "sliderX"}, inputRange.first);
const double endSliderX = valueAny(req_param, {"endSliderX", "endValue"}, inputRange.second);
json frames_json = buildSimulationFrames(frameCount, duration, stepTime, startSliderX, endSliderX, "sliderX", [&](double value) {
return mechanism->calculate(value);
});
if (!frames_json.is_array())
{
return frames_json;
}
return {
{"success", true},
{"mechanismType", mechanismType},
{"inputName", "sliderX"},
{"inputRange", {{"min", inputRange.first}, {"max", inputRange.second}}},
{"frames", frames_json},
{"frame_count", frameCount},
{"duration", duration},
{"stepTime", stepTime},
{"startValue", startSliderX},
{"endValue", endSliderX},
{"startSliderX", startSliderX},
{"endSliderX", endSliderX},
{"trajectory", buildTrajectoryJson(mechanism->getTrajectoryPoints())},
{"slider_trajectory", buildTrajectoryJson(mechanism->getSliderTrajectory())},
{"alternative_trajectory", buildTrajectoryJson(mechanism->getAlternativeTrajectory())},
{"parameters", {{"mechanismType", mechanismType}, {"L_AB", L_AB}, {"L_BS", L_BS}, {"S_OFS", S_OFS}, {"startSliderX", startSliderX}, {"endSliderX", endSliderX}, {"duration", duration}, {"stepTime", stepTime}}}};
}
if (mechanismType == "RRRR")
{
const double L1 = valueAny(req_param, {"L1", "L_AB"}, 1.0);
const double L2 = valueAny(req_param, {"L2", "L_BS"}, 3.0);
const double L3 = req_param.value("L3", 2.5);
const double L4 = req_param.value("L4", 3.5);
const double startAngleDeg = valueAny(req_param, {"startAngleDeg", "startValue", "angleDeg"}, 0.0);
const double endAngleDeg = valueAny(req_param, {"endAngleDeg", "endValue"}, 180.0);
std::unique_ptr<FourBarMechanism> mechanism(createFourBarMechanism());
mechanism->setLink(L1, L2, L3, L4);
json validationError = validateOrError(mechanism->validateParameters());
if (!validationError.is_null())
{
return validationError;
}
json frames_json = buildSimulationFrames(frameCount, duration, stepTime, startAngleDeg, endAngleDeg, "angleDeg", [&](double value) {
return mechanism->calculate(value);
});
if (!frames_json.is_array())
{
return frames_json;
}
return {
{"success", true},
{"mechanismType", mechanismType},
{"inputName", "angleDeg"},
{"frames", frames_json},
{"frame_count", frameCount},
{"duration", duration},
{"stepTime", stepTime},
{"startValue", startAngleDeg},
{"endValue", endAngleDeg},
{"startAngleDeg", startAngleDeg},
{"endAngleDeg", endAngleDeg},
{"trajectory", buildTrajectoryJson(mechanism->getTrajectoryPoints())},
{"trajectory_c", buildTrajectoryJson(mechanism->getTrajectoryC())},
{"crank_circle", buildTrajectoryJson(mechanism->getCrankCirclePoints())},
{"parameters", {{"mechanismType", mechanismType}, {"L1", L1}, {"L2", L2}, {"L3", L3}, {"L4", L4}, {"startAngleDeg", startAngleDeg}, {"endAngleDeg", endAngleDeg}, {"duration", duration}, {"stepTime", stepTime}}}};
}
if (mechanismType == "RPRR")
{
const double OA = valueAny(req_param, {"OA", "L_AB"}, 1.0);
const double AB = valueAny(req_param, {"AB", "L_BS"}, 3.0);
const double OC = valueAny(req_param, {"OC", "L4"}, 3.0);
const double startAngleDeg = valueAny(req_param, {"startAngleDeg", "startValue", "angleDeg"}, 0.0);
const double endAngleDeg = valueAny(req_param, {"endAngleDeg", "endValue"}, 180.0);
CrankRockingBlockMechanism_Forward mechanism;
mechanism.setLink(OA, AB, OC);
json validationError = validateOrError(mechanism.validateParameters());
if (!validationError.is_null())
{
return validationError;
}
json frames_json = buildSimulationFrames(frameCount, duration, stepTime, startAngleDeg, endAngleDeg, "angleDeg", [&](double value) {
return mechanism.calculate(value);
});
if (!frames_json.is_array())
{
return frames_json;
}
return {
{"success", true},
{"mechanismType", mechanismType},
{"inputName", "angleDeg"},
{"frames", frames_json},
{"frame_count", frameCount},
{"duration", duration},
{"stepTime", stepTime},
{"startValue", startAngleDeg},
{"endValue", endAngleDeg},
{"startAngleDeg", startAngleDeg},
{"endAngleDeg", endAngleDeg},
{"trajectory", buildTrajectoryJson(mechanism.getTrajectoryPoints())},
{"crank_circle", buildTrajectoryJson(mechanism.getCrankCirclePoints())},
{"rocker_trajectory", buildTrajectoryJson(mechanism.getRockerTrajectory())},
{"parameters", {{"mechanismType", mechanismType}, {"OA", OA}, {"AB", AB}, {"OC", OC}, {"startAngleDeg", startAngleDeg}, {"endAngleDeg", endAngleDeg}, {"duration", duration}, {"stepTime", stepTime}}}};
}
return buildErrorJson("Unsupported mechanismType: " + mechanismType, json::array({"mechanismType 必须是 PRRR、RPRR、RRRP、RRRR 之一"}));
}
} // namespace
json KinematicsWebAPI::handleFourBarCommand(const std::string &req_cmd, const json &req_param)
{
try
{
log("Handling " + req_cmd + " command");
if (req_cmd == "Cmd_FourBar_Simulate")
{
return simulateUnifiedFourBar(req_param);
}
double L_AB = req_param.value("L_AB", 0.5);
double L_BS = req_param.value("L_BS", 2.0);
double S_OFS = req_param.value("S_OFS", 0.0);
double angleDeg = req_param.value("angleDeg", 0.0);
double startAngleDeg = req_param.value("startAngleDeg", 0.0);
double endAngleDeg = req_param.value("endAngleDeg", 360.0);
double duration = req_param.value("duration", 2.0);
double stepTime = req_param.value("stepTime", 0.02);
log("Parameters: L_AB=" + std::to_string(L_AB) +
", L_BS=" + std::to_string(L_BS) +
", S_OFS=" + std::to_string(S_OFS) +
", angleDeg=" + std::to_string(angleDeg) +
", startAngleDeg=" + std::to_string(startAngleDeg) +
", endAngleDeg=" + std::to_string(endAngleDeg) +
", duration=" + std::to_string(duration) +
", stepTime=" + std::to_string(stepTime));
std::unique_ptr<CrankSliderMechanism> mechanism(createCrankSliderMechanism());
mechanism->setL_AB(L_AB);
mechanism->setL_BS(L_BS);
mechanism->setS_OFS(S_OFS);
ValidationResult validation = mechanism->validateParameters();
if (!validation.isValid())
{
return {
{"success", false},
{"error", "Invalid parameters"},
{"validation_errors", validation.Errors},
{"validation_warnings", validation.Warnings}};
}
if (req_cmd == "Cmd_FourBar_CrankSlider_Simulate")
{
if (duration <= 0.0)
{
return {
{"success", false},
{"error", "Invalid simulation parameters"},
{"validation_errors", json::array({"duration 必须大于 0"})},
{"validation_warnings", json::array()}};
}
if (stepTime <= 0.0 || stepTime > duration)
{
return {
{"success", false},
{"error", "Invalid simulation parameters"},
{"validation_errors", json::array({"stepTime 必须大于 0 且不能大于 duration"})},
{"validation_warnings", json::array()}};
}
const int frameCount = static_cast<int>(std::ceil(duration / stepTime)) + 1;
if (frameCount < 2 || frameCount > 1000)
{
return {
{"success", false},
{"error", "Invalid simulation parameters"},
{"validation_errors", json::array({"仿真帧数必须在 2 到 1000 之间"})},
{"validation_warnings", json::array()}};
}
json frames_json = json::array();
for (int frame = 0; frame < frameCount; ++frame)
{
const double currentTime = frame == frameCount - 1 ? duration : stepTime * frame;
const double progress = duration <= 0.0 ? 0.0 : currentTime / duration;
const double currentAngleDeg = startAngleDeg + (endAngleDeg - startAngleDeg) * progress;
MechanismState state = mechanism->calculate(currentAngleDeg);
if (state.hasError())
{
return {
{"success", false},
{"error", state.ErrorMessage},
{"failed_frame", frame},
{"failed_time", currentTime},
{"failed_angleDeg", currentAngleDeg}};
}
frames_json.push_back(buildFrameJson(state, frame, currentTime, currentAngleDeg, "angleDeg"));
}
json result;
result["success"] = true;
result["frames"] = frames_json;
result["frame_count"] = frameCount;
result["duration"] = duration;
result["stepTime"] = stepTime;
result["startAngleDeg"] = startAngleDeg;
result["endAngleDeg"] = endAngleDeg;
result["trajectory"] = buildTrajectoryJson(mechanism->getTrajectoryPoints());
result["slider_trajectory"] = buildTrajectoryJson(mechanism->getSliderTrajectory());
result["parameters"] = {
{"L_AB", L_AB},
{"L_BS", L_BS},
{"S_OFS", S_OFS},
{"startAngleDeg", startAngleDeg},
{"endAngleDeg", endAngleDeg},
{"duration", duration},
{"stepTime", stepTime}};
return result;
}
MechanismState state = mechanism->calculate(angleDeg);
if (state.hasError())
{
return {
{"success", false},
{"error", state.ErrorMessage}};
}
json result;
result["success"] = true;
result["points"] = buildPointsJson(state);
result["poses"] = buildPosesJson(state);
result["angles"] = buildAnglesJson(state);
result["input_value"] = state.InputValue;
if (state.hasWarning())
{
result["warning"] = state.WarningMessage;
}
result["trajectory"] = buildTrajectoryJson(mechanism->getTrajectoryPoints());
result["slider_trajectory"] = buildTrajectoryJson(mechanism->getSliderTrajectory());
result["parameters"] = {
{"L_AB", L_AB},
{"L_BS", L_BS},
{"S_OFS", S_OFS},
{"angleDeg", angleDeg}};
return result;
}
catch (const std::exception &e)
{
return {
{"success", false},
{"error", "Failed to Cmd_FourBar_CrankSlider: " + std::string(e.what())}};
}
}

View File

@@ -0,0 +1,19 @@
#include "KinematicsWebAPI.h"
#include "QuadrupedRobotSimulation/KinematicsHelper.h"
#include <iostream>
json KinematicsWebAPI::handleQuadrupedCommand(const std::string &req_cmd, const json &req_param)
{
if (req_cmd == "Cmd_QuadrupedRobot_CalculateAllPointsFromMotorAngles")
{
return KinematicsHelper::QuadrupedRobot_CalculateAllPointsFromMotorAngles(req_param.dump());
}
if (req_cmd == "Cmd_QuadrupedRobot_PerformForwardKinematics")
{
return KinematicsHelper::QuadrupedRobot_PerformForwardKinematics(req_param.dump());
}
return createUnknownCommandResponse(req_cmd, req_param);
}

View File

@@ -0,0 +1,349 @@
#include "KinematicsWebAPI.h"
json KinematicsWebAPI::handleRobotCommand(const std::string &req_cmd, const json &req_param)
{
json res_data;
if (req_cmd == "Cmd_Kinematics_inverse_pose_str")
{
try
{
log("Handling Cmd_Kinematics_inverse_pose_str command");
std::string pose_str = req_param.value("pose_str", "");
std::string q_init_str = req_param.value("q_init_str", "0,0,0,0,0,0");
std::string robot_uuid = req_param.value("robot_uuid", "default");
auto robot = RobotManager::getRobot(robot_uuid);
if (!robot || !robot->isInitialized())
{
res_data = {{"error", "Robot not found or not initialized"}};
}
else
{
json result = robot->inversePoseStr(pose_str, q_init_str);
if (result.empty())
{
res_data = {{"error", "Inverse kinematics calculation failed"}};
}
else
{
res_data = {{"joints", result}, {"success", true}};
}
}
}
catch (const std::exception &e)
{
res_data = {{"error", "Failed to calculate inverse kinematics: " + std::string(e.what())}};
}
}
else if (req_cmd == "Cmd_Kinematics_inverse_pose_str_2PSteps")
{
try
{
log("Handling Cmd_Kinematics_inverse_pose_str_2PSteps command");
std::string pose_str = req_param.value("pose_str", "");
std::string q_init_str = req_param.value("q_init_str", "0,0,0,0,0,0");
std::string robot_uuid = req_param.value("robot_uuid", "default");
std::string steps_str = req_param.value("steps_str", "default");
int steps = std::stoi(steps_str);
auto robot = RobotManager::getRobot(robot_uuid);
if (!robot || !robot->isInitialized())
{
res_data = {{"error", "Robot not found or not initialized"}};
}
else
{
json result = robot->inversePoseStr2PSteps(pose_str, q_init_str, steps);
if (result.empty())
{
res_data = {{"error", "Inverse kinematics calculation failed"}};
}
else
{
res_data = {{"joints", result}, {"success", true}};
}
}
}
catch (const std::exception &e)
{
res_data = {{"error", "Failed to calculate inverse kinematics: " + std::string(e.what())}};
}
}
else if (req_cmd == "Cmd_Kinematics_inverse_pose_str_NoDifference")
{
try
{
log("Handling Cmd_Kinematics_inverse_pose_str_NoDifference command");
std::string pose_str = req_param.value("pose_str", "");
std::string q_init_str = req_param.value("q_init_str", "0,0,0,0,0,0");
std::string robot_uuid = req_param.value("robot_uuid", "default");
auto robot = RobotManager::getRobot(robot_uuid);
if (!robot || !robot->isInitialized())
{
res_data = {{"error", "Robot not found or not initialized"}};
}
else
{
json result = robot->inversePoseStrNoDifference(pose_str, q_init_str);
if (result.empty())
{
res_data = {{"error", "Inverse kinematics calculation failed"}};
}
else
{
res_data = {{"joints", result}, {"success", true}};
}
}
}
catch (const std::exception &e)
{
res_data = {{"error", "Failed to calculate inverse kinematics: " + std::string(e.what())}};
}
}
else if (req_cmd == "Cmd_Kinematics_forward_pose_str")
{
try
{
log("Handling Cmd_Kinematics_forward_pose_str command");
std::string joints_str = req_param.value("q_init_str", "0,0,0,0,0,0");
std::string robot_uuid = req_param.value("robot_uuid", "default");
auto robot = RobotManager::getRobot(robot_uuid);
if (!robot || !robot->isInitialized())
{
res_data = {{"error", "Robot not found or not initialized"}};
}
else
{
auto joints = robot->parseJointString(joints_str);
if (joints.size() != 6)
{
res_data = {{"error", "Invalid joints format"}};
}
else
{
double tcp_pose[7];
if (robot->calculateFK_TCP(joints.data(), tcp_pose))
{
res_data = {
{"position", {tcp_pose[0], tcp_pose[1], tcp_pose[2]}},
{"orientation", {tcp_pose[3], tcp_pose[4], tcp_pose[5], tcp_pose[6]}},
{"joints", joints},
{"success", true}};
}
else
{
res_data = {{"error", "Forward kinematics calculation failed"}};
}
}
}
}
catch (const std::exception &e)
{
res_data = {{"error", "Failed to calculate forward kinematics: " + std::string(e.what())}};
}
}
else if (req_cmd == "Cmd_Kinematics_forward_all_joints")
{
try
{
log("Handling Cmd_Kinematics_forward_all_joints command");
std::string joints_str = req_param.value("joints_str", "0,0,0,0,0,0");
std::string robot_uuid = req_param.value("robot_uuid", "default");
auto robot = RobotManager::getRobot(robot_uuid);
if (!robot || !robot->isInitialized())
{
res_data = {{"error", "Robot not found or not initialized"}};
}
else
{
res_data = {
{"OPERATION", robot->handleKinematicsForwardAllJoints(joints_str)},
{"success", true}};
}
}
catch (const std::exception &e)
{
res_data = {{"error", "Failed to calculate forward kinematics for all joints: " + std::string(e.what())}};
}
}
else if (req_cmd == "Cmd_Kinematics_check_singularity")
{
try
{
log("Handling Cmd_Kinematics_check_singularity command");
std::string joints_str = req_param.value("joints_str", req_param.value("q_init_str", "0,0,0,0,0,0"));
std::string robot_uuid = req_param.value("robot_uuid", "default");
double singular_threshold = req_param.value("singular_threshold", 1e-4);
double warning_threshold = req_param.value("warning_threshold", 1e-2);
double condition_threshold = req_param.value("condition_threshold", 1e6);
double condition_warning_threshold = req_param.value("condition_warning_threshold", 1e4);
auto robot = RobotManager::getRobot(robot_uuid);
if (!robot || !robot->isInitialized())
{
res_data = {{"error", "Robot not found or not initialized"}};
}
else
{
res_data = robot->checkSingularity(
joints_str,
singular_threshold,
warning_threshold,
condition_threshold,
condition_warning_threshold);
}
}
catch (const std::exception &e)
{
res_data = {{"error", "Failed to check singularity: " + std::string(e.what())}};
}
}
else if (req_cmd == "Cmd_InitRobot")
{
try
{
log("Handling Cmd_InitRobot command");
std::string urdf_base64 = req_param.value("urdf_base64", "");
std::string uuid = req_param.value("robot_uuid", "");
bool force_update = req_param.value("force_update", true);
std::string urdf_content = utils::base64_to_urdf(urdf_base64);
if (!utils::validate_urdf_base64(urdf_base64))
{
res_data = {
{"success", false},
{"message", "Invalid URDF format"},
{"timestamp", utils::get_current_time()}};
}
else
{
auto result = RobotManager::initRobot(urdf_content, uuid, force_update);
res_data = {
{"success", result.first},
{"message", result.second},
{"timestamp", utils::get_current_time()}};
}
}
catch (const std::exception &e)
{
res_data = {{"error", "Failed to initialize robot: " + std::string(e.what())}};
}
}
else if (req_cmd == "Cmd_GetRobot")
{
try
{
log("Handling Cmd_GetRobot command");
std::string uuid = req_param.value("uuid", "");
auto robot = RobotManager::getRobot(uuid);
if (!robot)
{
res_data = {{"error", "Robot not found"}};
}
else
{
res_data = {
{"success", true},
{"uuid", uuid},
{"initialized", robot->isInitialized()},
{"joints_count", robot->getNumberOfJoints()},
{"timestamp", utils::get_current_time()}};
}
}
catch (const std::exception &e)
{
res_data = {{"error", "Failed to get robot: " + std::string(e.what())}};
}
}
else if (req_cmd == "Cmd_RemoveRobot")
{
try
{
log("Handling Cmd_RemoveRobot command");
std::string uuid = req_param.value("uuid", "");
auto result = RobotManager::removeRobot(uuid);
res_data = {
{"success", result.first},
{"message", result.second},
{"timestamp", utils::get_current_time()}};
}
catch (const std::exception &e)
{
res_data = {{"error", "Failed to remove robot: " + std::string(e.what())}};
}
}
else if (req_cmd == "Cmd_ListRobots")
{
try
{
log("Handling Cmd_ListRobots command");
bool detail = req_param.value("detail", false);
auto robots = RobotManager::listRobots(detail);
res_data = {
{"success", true},
{"robots", robots},
{"count", robots.size()},
{"timestamp", utils::get_current_time()}};
}
catch (const std::exception &e)
{
res_data = {{"error", "Failed to list robots: " + std::string(e.what())}};
}
}
else if (req_cmd == "Cmd_SelectCraftTree")
{
try
{
log("Handling Cmd_SelectCraftTree command");
std::string tree_id = req_param.value("tree_id", "");
res_data = {
{"success", true},
{"tree_id", tree_id},
{"message", "Craft tree selected successfully"},
{"timestamp", utils::get_current_time()}};
}
catch (const std::exception &e)
{
res_data = {{"error", "Failed to select craft tree: " + std::string(e.what())}};
}
}
else if (req_cmd == "Cmd_AddOperationTree")
{
try
{
log("Handling Cmd_AddOperationTree command");
std::string tree_name = req_param.value("name", "");
json operations = req_param.value("operations", json::array());
res_data = {
{"success", true},
{"tree_name", tree_name},
{"operations_count", operations.size()},
{"message", "Operation tree added successfully"},
{"timestamp", utils::get_current_time()}};
}
catch (const std::exception &e)
{
res_data = {{"error", "Failed to add operation tree: " + std::string(e.what())}};
}
}
return res_data;
}

View File

@@ -0,0 +1,14 @@
#include "KinematicsWebAPI.h"
json KinematicsWebAPI::handleSpcCommand(const std::string &req_cmd, const json &req_param)
{
try
{
log("Handling " + req_cmd + " command");
return SpcCalculator::Spc(req_param);
}
catch (const std::exception &e)
{
return {{"error", "Failed to Cmd_Spc: " + std::string(e.what())}};
}
}

View File

@@ -703,9 +703,32 @@ json SpcCalculator::Spc(const json &param)
// 使用测试数据
const std::vector<double> &testData = req_param.x; // 改为小写 x
int subgroupSize = req_param.n;
int subgroupCount = req_param.k;
double usl = req_param.usl;
double lsl = req_param.lsl;
if (subgroupSize < MIN_SUBGROUP_SIZE || subgroupSize > MAX_SUBGROUP_SIZE)
{
throw std::invalid_argument("子组大小 n 必须在 2 到 25 之间");
}
if (subgroupCount <= 0)
{
throw std::invalid_argument("子组个数 k 必须大于 0");
}
if (testData.empty())
{
throw std::invalid_argument("测量数据 x 不能为空");
}
const size_t expectedDataCount = static_cast<size_t>(subgroupSize) * static_cast<size_t>(subgroupCount);
if (testData.size() != expectedDataCount)
{
throw std::invalid_argument("数据个数不匹配:期望 n*k=" + std::to_string(expectedDataCount) +
",实际 x=" + std::to_string(testData.size()));
}
// 计算并获取整合的 JSON
SpcDataXR xr = CalculateXR(testData, subgroupSize);
SpcDataXS xs = CalculateXS(testData, subgroupSize);
@@ -1419,4 +1442,4 @@ int main_spc()
std::cerr << "错误: " << ex.what() << std::endl;
return 1;
}
}
}

View File

@@ -8,8 +8,6 @@
#include <algorithm>
#include <stdexcept>
#include <stdexcept>
// 实现 splitByDelimiter 方法
std::pair<std::string, std::string> StringUtils::splitByDelimiter(
const std::string &str,
@@ -88,13 +86,13 @@ std::pair<std::string, std::string> StringUtils::splitStrict(
namespace utils
{
// Base64<EFBFBD>ַ<EFBFBD><EFBFBD><EFBFBD>
// Base64 字符表
const std::string base64_chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/";
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƿ<EFBFBD>Ϊ<EFBFBD><EFBFBD>Ч<EFBFBD><EFBFBD>base64<EFBFBD>ַ<EFBFBD>
// 判断字符是否为有效的 Base64 字符
static bool is_base64(unsigned char c)
{
return (isalnum(c) || (c == '+') || (c == '/'));
@@ -134,7 +132,7 @@ namespace utils
// res.set_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH");
// res.set_header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With, X-API-Key, Accept, Origin");
// res.set_header("Access-Control-Expose-Headers", "Content-Length, Content-Type, X-Request-Id");
// res.set_header("Access-Control-Max-Age", "86400"); // 24Сʱ
// res.set_header("Access-Control-Max-Age", "86400"); // 24 小时
// res.set_header("Vary", "Origin");
// }
@@ -171,11 +169,11 @@ namespace utils
unsigned char c = str[i];
if (c <= 0x7F)
{
continue; // ASCII<EFBFBD>ַ<EFBFBD>
continue; // ASCII 字符
}
else if ((c & 0xE0) == 0xC0)
{
// 2<EFBFBD>ֽ<EFBFBD>UTF-8
// 2 字节 UTF-8
if (i + 1 >= str.size() || (str[i + 1] & 0xC0) != 0x80)
{
return false;
@@ -184,7 +182,7 @@ namespace utils
}
else if ((c & 0xF0) == 0xE0)
{
// 3<EFBFBD>ֽ<EFBFBD>UTF-8
// 3 字节 UTF-8
if (i + 2 >= str.size() || (str[i + 1] & 0xC0) != 0x80 || (str[i + 2] & 0xC0) != 0x80)
{
return false;
@@ -193,7 +191,7 @@ namespace utils
}
else if ((c & 0xF8) == 0xF0)
{
// 4<EFBFBD>ֽ<EFBFBD>UTF-8
// 4 字节 UTF-8
if (i + 3 >= str.size() || (str[i + 1] & 0xC0) != 0x80 ||
(str[i + 2] & 0xC0) != 0x80 || (str[i + 3] & 0xC0) != 0x80)
{
@@ -203,7 +201,7 @@ namespace utils
}
else
{
return false; // <EFBFBD><EFBFBD>Ч<EFBFBD><EFBFBD>UTF-8<EFBFBD>ֽ<EFBFBD>
return false; // 非法 UTF-8 字节
}
}
return true;
@@ -219,11 +217,11 @@ namespace utils
unsigned char c = str[i];
if (c <= 0x7F)
{
result += c; // ASCII<EFBFBD>ַ<EFBFBD>
result += c; // ASCII 字符
}
else if ((c & 0xE0) == 0xC0)
{
// 2<EFBFBD>ֽ<EFBFBD>UTF-8
// 2 字节 UTF-8
if (i + 1 < str.size() && (str[i + 1] & 0xC0) == 0x80)
{
result += c;
@@ -233,7 +231,7 @@ namespace utils
}
else if ((c & 0xF0) == 0xE0)
{
// 3<EFBFBD>ֽ<EFBFBD>UTF-8
// 3 字节 UTF-8
if (i + 2 < str.size() && (str[i + 1] & 0xC0) == 0x80 && (str[i + 2] & 0xC0) == 0x80)
{
result += c;
@@ -244,7 +242,7 @@ namespace utils
}
else if ((c & 0xF8) == 0xF0)
{
// 4<EFBFBD>ֽ<EFBFBD>UTF-8
// 4 字节 UTF-8
if (i + 3 < str.size() && (str[i + 1] & 0xC0) == 0x80 &&
(str[i + 2] & 0xC0) == 0x80 && (str[i + 3] & 0xC0) == 0x80)
{
@@ -255,7 +253,7 @@ namespace utils
i += 3;
}
}
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ч<EFBFBD><EFBFBD>UTF-8<EFBFBD>ֽ<EFBFBD>
// 跳过非法 UTF-8 字节
}
return result;
}
@@ -285,7 +283,7 @@ namespace utils
return response;
}
// Base64<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
// Base64 解码函数
std::string base64_decode(const std::string &encoded_string)
{
int in_len = encoded_string.size();
@@ -295,7 +293,7 @@ namespace utils
unsigned char char_array_4[4], char_array_3[3];
std::string ret;
// <EFBFBD>Ƴ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ܵĻ<EFBFBD><EFBFBD>з<EFBFBD><EFBFBD>Ϳո<EFBFBD>
// 去掉输入中的换行和空格
std::string clean_encoded;
for (char c : encoded_string)
{
@@ -346,7 +344,7 @@ namespace utils
size_t pos = base64_chars.find(char_array_4[j]);
if (pos == std::string::npos && j >= i)
{
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ַ<EFBFBD>λ<EFBFBD><EFBFBD>Ϊ0
// 补齐的占位字符按 0 处理
char_array_4[j] = 0;
}
else if (pos != std::string::npos)
@@ -372,14 +370,14 @@ namespace utils
return ret;
}
// <EFBFBD><EFBFBD>base64<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>URDFת<EFBFBD><EFBFBD>Ϊ<EFBFBD>ַ<EFBFBD><EFBFBD><EFBFBD>
// 将 Base64 编码的 URDF 转成字符串
std::string base64_to_urdf(const std::string &base64_urdf)
{
try
{
std::string urdf_content = base64_decode(base64_urdf);
// <EFBFBD><EFBFBD>֤<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƿ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ч<EFBFBD><EFBFBD>URDF/XML
// 校验解码结果是否像合法的 URDF/XML
if (urdf_content.find("<?xml") != std::string::npos ||
urdf_content.find("<robot") != std::string::npos)
{
@@ -396,7 +394,7 @@ namespace utils
}
}
// <EFBFBD><EFBFBD>base64<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>URDF<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD>
// 将 Base64 编码的 URDF 保存到文件
bool save_base64_urdf_to_file(const std::string &base64_urdf, const std::string &filename)
{
try
@@ -411,7 +409,7 @@ namespace utils
}
}
// <EFBFBD><EFBFBD>URDF<EFBFBD>ַ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD>
// URDF 字符串保存到文件
bool save_urdf_string_to_file(const std::string &urdf_content, const std::string &filename)
{
try
@@ -437,14 +435,14 @@ namespace utils
}
}
// <EFBFBD><EFBFBD>֤base64<EFBFBD>ַ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƿ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ч<EFBFBD><EFBFBD>URDF
// 校验 Base64 字符串是否可解析为合法 URDF
bool validate_urdf_base64(const std::string &base64_urdf)
{
try
{
std::string urdf_content = base64_to_urdf(base64_urdf);
// <EFBFBD>򵥵<EFBFBD>URDF<EFBFBD><EFBFBD>֤
// 基础 URDF 结构校验
bool has_xml_decl = urdf_content.find("<?xml") != std::string::npos;
bool has_robot_tag = urdf_content.find("<robot") != std::string::npos;
bool has_link_tag = urdf_content.find("<link") != std::string::npos;
@@ -458,4 +456,4 @@ namespace utils
}
}
} // namespace utils
} // namespace utils

View File

@@ -0,0 +1,15 @@
{
"msg": "fourbar simulate",
"req_code": "CASE_FB_SIM_001",
"req_from": "wasm_test",
"req_cmd": "Cmd_FourBar_CrankSlider_Simulate",
"req_param": {
"L_AB": 0.5,
"L_BS": 2.0,
"S_OFS": 0.0,
"startAngleDeg": 0.0,
"endAngleDeg": 180.0,
"duration": 1.0,
"stepTime": 0.25
}
}

View File

@@ -0,0 +1,16 @@
{
"msg": "fourbar unified simulate PRRR",
"req_code": "CASE_FB_UNIFIED_PRRR",
"req_from": "wasm_test",
"req_cmd": "Cmd_FourBar_Simulate",
"req_param": {
"mechanismType": "PRRR",
"L_AB": 0.5,
"L_BS": 2.0,
"S_OFS": 0.0,
"startSliderX": 1.5,
"endSliderX": 2.5,
"duration": 1.0,
"stepTime": 0.25
}
}

View File

@@ -0,0 +1,16 @@
{
"msg": "fourbar unified simulate RPRR",
"req_code": "CASE_FB_UNIFIED_RPRR",
"req_from": "wasm_test",
"req_cmd": "Cmd_FourBar_Simulate",
"req_param": {
"mechanismType": "RPRR",
"OA": 1.0,
"AB": 3.0,
"OC": 3.0,
"startAngleDeg": 0.0,
"endAngleDeg": 180.0,
"duration": 1.0,
"stepTime": 0.25
}
}

View File

@@ -0,0 +1,16 @@
{
"msg": "fourbar unified simulate RRRP",
"req_code": "CASE_FB_UNIFIED_RRRP",
"req_from": "wasm_test",
"req_cmd": "Cmd_FourBar_Simulate",
"req_param": {
"mechanismType": "RRRP",
"L_AB": 0.5,
"L_BS": 2.0,
"S_OFS": 0.0,
"startAngleDeg": 0.0,
"endAngleDeg": 180.0,
"duration": 1.0,
"stepTime": 0.25
}
}

View File

@@ -0,0 +1,17 @@
{
"msg": "fourbar unified simulate RRRR",
"req_code": "CASE_FB_UNIFIED_RRRR",
"req_from": "wasm_test",
"req_cmd": "Cmd_FourBar_Simulate",
"req_param": {
"mechanismType": "RRRR",
"L1": 1.0,
"L2": 3.0,
"L3": 2.5,
"L4": 3.5,
"startAngleDeg": 0.0,
"endAngleDeg": 180.0,
"duration": 1.0,
"stepTime": 0.25
}
}

13
tests/testdata/spc/invalid_count.json vendored Normal file
View File

@@ -0,0 +1,13 @@
{
"msg": "spc invalid count",
"req_code": "CASE_SPC_INVALID_001",
"req_from": "wasm_test",
"req_cmd": "Cmd_Spc",
"req_param": {
"n": 5,
"k": 2,
"usl": 1.7,
"lsl": 1.5,
"x": [1.55, 1.58, 1.61, 1.6, 1.6]
}
}