新增四类连杆统一仿真接口

This commit is contained in:
zhangshun
2026-06-16 11:40:58 +08:00
parent a66952e636
commit f142b42dbe
9 changed files with 940 additions and 248 deletions

View File

@@ -444,6 +444,65 @@ func
- `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. 四足机器人接口履历
命令文件:

File diff suppressed because it is too large Load Diff

View File

@@ -418,6 +418,71 @@ const suite = [
{ 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",

View File

@@ -122,7 +122,8 @@ json KinematicsWebAPI::dispatchCommand(const std::string &req_cmd, const json &r
return handleSpcCommand(req_cmd, req_param);
}
if (req_cmd == "Cmd_FourBar_CrankSlider" ||
if (req_cmd == "Cmd_FourBar_Simulate" ||
req_cmd == "Cmd_FourBar_CrankSlider" ||
req_cmd == "Cmd_FourBar_CrankSlider_Simulate")
{
return handleFourBarCommand(req_cmd, req_param);

View File

@@ -1,9 +1,45 @@
#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)
{
@@ -60,12 +96,14 @@ json buildTrajectoryJson(const std::vector<Vector2D> &points)
}
// 组装单帧机构状态,供实时接口和批量仿真接口复用。
json buildFrameJson(const MechanismState &state, int frame, double time, double angleDeg)
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["angleDeg"] = angleDeg;
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);
@@ -76,6 +114,260 @@ json buildFrameJson(const MechanismState &state, int frame, double time, double
}
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)
@@ -84,6 +376,11 @@ json KinematicsWebAPI::handleFourBarCommand(const std::string &req_cmd, const js
{
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);
@@ -164,7 +461,7 @@ json KinematicsWebAPI::handleFourBarCommand(const std::string &req_cmd, const js
{"failed_angleDeg", currentAngleDeg}};
}
frames_json.push_back(buildFrameJson(state, frame, currentTime, currentAngleDeg));
frames_json.push_back(buildFrameJson(state, frame, currentTime, currentAngleDeg, "angleDeg"));
}
json result;

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