From a66952e6367814c2cb6858aa85f4022e960c9858 Mon Sep 17 00:00:00 2001
From: zhangshun <453905631@qq.com>
Date: Tue, 16 Jun 2026 11:17:25 +0800
Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=9B=B2=E6=9F=84=E6=BB=91?=
=?UTF-8?q?=E5=9D=97=E6=89=B9=E9=87=8F=E4=BB=BF=E7=9C=9F=E6=8E=A5=E5=8F=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/notes/接口调用履历.md | 39 +++-
public/fourbar_test.html | 99 +++++++--
scripts/run_wasm_tests.js | 17 ++
src/api/KinematicsWebAPI.Core.cpp | 3 +-
src/api/KinematicsWebAPI.FourBarCommands.cpp | 210 ++++++++++++++----
.../fourbar/crank_slider_simulate.json | 15 ++
6 files changed, 310 insertions(+), 73 deletions(-)
create mode 100644 tests/testdata/fourbar/crank_slider_simulate.json
diff --git a/docs/notes/接口调用履历.md b/docs/notes/接口调用履历.md
index 130c536..b912fa4 100644
--- a/docs/notes/接口调用履历.md
+++ b/docs/notes/接口调用履历.md
@@ -406,6 +406,44 @@ func
- 命令层负责把参数取出来、调机制对象、再把结果组装成 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 点整段滑块轨迹
+
## 7. 四足机器人接口履历
命令文件:
@@ -596,4 +634,3 @@ smart_test_match
1. `src/api/KinematicsWebAPI.SpcCommands.cpp`
2. `src/spc_core.cpp`
3. `include/spc_core.h`
-
diff --git a/public/fourbar_test.html b/public/fourbar_test.html
index 5e63704..08d8710 100644
--- a/public/fourbar_test.html
+++ b/public/fourbar_test.html
@@ -579,20 +579,24 @@
-
-
+
+
+
+
+
+
-
+
@@ -756,6 +760,7 @@
const angleInput = document.getElementById("angleInput");
const stepInput = document.getElementById("stepInput");
const scanEndInput = document.getElementById("scanEndInput");
+ const durationInput = document.getElementById("durationInput");
const requestOutput = document.getElementById("requestOutput");
const topSuccess = document.getElementById("topSuccess");
const businessSuccess = document.getElementById("businessSuccess");
@@ -813,6 +818,25 @@
};
}
+ // 按批量仿真接口格式组装曲柄滑块请求。
+ function buildSimulationRequest(input, sim, reqCode = `FOURBAR_SIM_${Date.now()}`) {
+ return {
+ msg: "fourbar crank slider simulate",
+ req_code: reqCode,
+ req_from: "fourbar_test_page",
+ req_cmd: "Cmd_FourBar_CrankSlider_Simulate",
+ req_param: {
+ L_AB: input.L_AB,
+ L_BS: input.L_BS,
+ S_OFS: input.S_OFS,
+ startAngleDeg: input.angleDeg,
+ endAngleDeg: sim.endAngleDeg,
+ duration: sim.duration,
+ stepTime: sim.stepTime,
+ },
+ };
+ }
+
// 将当前表单同步到请求 JSON 文本框。
function refreshRequestPreview() {
try {
@@ -828,8 +852,9 @@
lBsInput.value = sample.L_BS;
sOfsInput.value = sample.S_OFS;
angleInput.value = sample.angleDeg;
- stepInput.value = stepInput.value || "10";
+ stepInput.value = stepInput.value || "0.02";
scanEndInput.value = scanEndInput.value || "360";
+ durationInput.value = durationInput.value || "2";
refreshRequestPreview();
}
@@ -1116,6 +1141,22 @@
}
}
+ // 调用曲柄滑块批量仿真接口并解析 JSON 响应。
+ async function callFourBarSimulation(input, sim) {
+ const module = await ensureWasm();
+ const payload = JSON.stringify(buildSimulationRequest(input, sim));
+ let requestPtr = 0;
+ let responsePtr = 0;
+ try {
+ requestPtr = allocString(module, payload);
+ responsePtr = module._func(requestPtr);
+ return JSON.parse(module.UTF8ToString(responsePtr));
+ } finally {
+ if (responsePtr) module._smart_free_string(responsePtr);
+ if (requestPtr) module._free(requestPtr);
+ }
+ }
+
// 计算前端几何校验值,用于和 WASM 返回点位对照。
function calculateLocalGeometry(input) {
const angle = (input.angleDeg * Math.PI) / 180;
@@ -1419,43 +1460,52 @@
}
}
- // 多角度调用接口,生成轨迹和位移曲线。
+ // 调用批量仿真接口,生成轨迹和位移曲线。
async function runScan() {
try {
setMessage("");
const baseInput = readInput();
- const step = Number(stepInput.value);
+ const stepTime = Number(stepInput.value);
const scanEnd = Number(scanEndInput.value);
- if (!Number.isInteger(step) || step < 1 || step > 90) {
- throw new Error("扫描步长必须是 1 到 90 之间的整数");
+ const duration = Number(durationInput.value);
+ if (!Number.isFinite(stepTime) || stepTime <= 0 || stepTime > 1) {
+ throw new Error("stepTime 必须是 0 到 1 秒之间的有效数字");
}
if (!Number.isFinite(scanEnd) || scanEnd <= 0 || scanEnd > 720) {
throw new Error("扫描终止角必须在 1 到 720 之间");
}
+ if (!Number.isFinite(duration) || duration <= 0 || duration > 60) {
+ throw new Error("duration 必须在 0 到 60 秒之间");
+ }
requestStatus.textContent = "扫描中";
requestStatus.className = "badge warn";
- const rows = [];
- for (let angle = 0; angle <= scanEnd + 0.000001; angle += step) {
- const input = { ...baseInput, angleDeg: angle };
- const response = await callFourBar(input, `FOURBAR_SCAN_${angle}`);
- const data = response.res_data || {};
- rows.push({
- angle,
- ok: response.success === true && data.success === true && !data.error,
- B: data.points?.B,
- S: data.points?.S,
- error: data.error || response.msg || "",
- });
+ const response = await callFourBarSimulation(baseInput, {
+ endAngleDeg: scanEnd,
+ duration,
+ stepTime,
+ });
+ const data = response.res_data || {};
+ if (response.success !== true || data.success !== true || data.error) {
+ throw new Error(data.error || response.msg || "批量仿真接口返回失败");
}
+ const rows = (data.frames || []).map((frame) => ({
+ frame: frame.frame,
+ time: frame.time,
+ angle: frame.angleDeg,
+ ok: Boolean(frame.points?.B && frame.points?.S),
+ B: frame.points?.B,
+ S: frame.points?.S,
+ error: "",
+ }));
latestScan = rows;
- scanOutput.textContent = JSON.stringify(rows, null, 2);
+ scanOutput.textContent = JSON.stringify(response, null, 2);
renderScanCharts(rows);
scanAnimation.frame = 0;
requestStatus.textContent = "扫描完成";
requestStatus.className = "badge ok";
- setMessage(`扫描完成:${rows.filter((row) => row.ok).length}/${rows.length} 个角度有效`, "is-ok");
+ setMessage(`仿真完成:${rows.filter((row) => row.ok).length}/${rows.length} 帧有效`, "is-ok");
} catch (error) {
requestStatus.textContent = "扫描失败";
requestStatus.className = "badge error";
@@ -1472,7 +1522,7 @@
scanOutput.classList.toggle("hidden", isResponse);
}
- [lAbInput, lBsInput, sOfsInput, angleInput].forEach((input) => {
+ [lAbInput, lBsInput, sOfsInput, angleInput, stepInput, scanEndInput, durationInput].forEach((input) => {
input.addEventListener("input", refreshRequestPreview);
});
@@ -1495,8 +1545,9 @@
scanTab.addEventListener("click", () => setActiveTab("scan"));
loadSample(validSample);
- stepInput.value = "10";
+ stepInput.value = "0.02";
scanEndInput.value = "360";
+ durationInput.value = "2";
refreshRequestPreview();
renderThreeMechanism(calculateLocalGeometry(validSample), validSample, {
title: "有效样例预览",
diff --git a/scripts/run_wasm_tests.js b/scripts/run_wasm_tests.js
index e661740..15496fe 100644
--- a/scripts/run_wasm_tests.js
+++ b/scripts/run_wasm_tests.js
@@ -401,6 +401,23 @@ const suite = [
{ 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: "quadruped_points_from_motor_angles",
type: "static",
diff --git a/src/api/KinematicsWebAPI.Core.cpp b/src/api/KinematicsWebAPI.Core.cpp
index 5b23684..12ee8ec 100644
--- a/src/api/KinematicsWebAPI.Core.cpp
+++ b/src/api/KinematicsWebAPI.Core.cpp
@@ -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_CrankSlider" ||
+ req_cmd == "Cmd_FourBar_CrankSlider_Simulate")
{
return handleFourBarCommand(req_cmd, req_param);
}
diff --git a/src/api/KinematicsWebAPI.FourBarCommands.cpp b/src/api/KinematicsWebAPI.FourBarCommands.cpp
index d685c35..2f02009 100644
--- a/src/api/KinematicsWebAPI.FourBarCommands.cpp
+++ b/src/api/KinematicsWebAPI.FourBarCommands.cpp
@@ -1,5 +1,82 @@
#include "KinematicsWebAPI.h"
#include "FourBarMechanism/CrankSliderMechanism.h"
+#include
+
+namespace
+{
+// 将机构点位转换为接口统一 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 &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 angleDeg)
+{
+ json frame_json;
+ frame_json["frame"] = frame;
+ frame_json["time"] = time;
+ frame_json["angleDeg"] = angleDeg;
+ 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;
+}
+} // namespace
json KinematicsWebAPI::handleFourBarCommand(const std::string &req_cmd, const json &req_param)
{
@@ -11,11 +88,19 @@ json KinematicsWebAPI::handleFourBarCommand(const std::string &req_cmd, const js
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));
+ ", 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 mechanism(createCrankSliderMechanism());
mechanism->setL_AB(L_AB);
@@ -32,6 +117,78 @@ json KinematicsWebAPI::handleFourBarCommand(const std::string &req_cmd, const js
{"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(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));
+ }
+
+ 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())
{
@@ -43,35 +200,9 @@ json KinematicsWebAPI::handleFourBarCommand(const std::string &req_cmd, const js
json result;
result["success"] = true;
- 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}};
- }
- result["points"] = points_json;
-
- 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}};
- }
- 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["points"] = buildPointsJson(state);
+ result["poses"] = buildPosesJson(state);
+ result["angles"] = buildAnglesJson(state);
result["input_value"] = state.InputValue;
if (state.hasWarning())
@@ -79,23 +210,8 @@ json KinematicsWebAPI::handleFourBarCommand(const std::string &req_cmd, const js
result["warning"] = state.WarningMessage;
}
- json trajectory_json = json::array();
- for (const auto &point : mechanism->getTrajectoryPoints())
- {
- trajectory_json.push_back({
- {"x", point.X},
- {"y", point.Y}});
- }
- result["trajectory"] = trajectory_json;
-
- json slider_trajectory_json = json::array();
- for (const auto &point : mechanism->getSliderTrajectory())
- {
- slider_trajectory_json.push_back({
- {"x", point.X},
- {"y", point.Y}});
- }
- result["slider_trajectory"] = slider_trajectory_json;
+ result["trajectory"] = buildTrajectoryJson(mechanism->getTrajectoryPoints());
+ result["slider_trajectory"] = buildTrajectoryJson(mechanism->getSliderTrajectory());
result["parameters"] = {
{"L_AB", L_AB},
diff --git a/tests/testdata/fourbar/crank_slider_simulate.json b/tests/testdata/fourbar/crank_slider_simulate.json
new file mode 100644
index 0000000..973a3cc
--- /dev/null
+++ b/tests/testdata/fourbar/crank_slider_simulate.json
@@ -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
+ }
+}