新增曲柄滑块批量仿真接口

This commit is contained in:
zhangshun
2026-06-16 11:17:25 +08:00
parent c9e76d0850
commit a66952e636
6 changed files with 310 additions and 73 deletions

View File

@@ -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`

View File

@@ -579,20 +579,24 @@
<input id="angleInput" type="number" step="1" />
</div>
<div class="field">
<label for="stepInput">扫描步长</label>
<input id="stepInput" type="number" min="1" max="90" step="1" />
<label for="stepInput">stepTime 步长时间</label>
<input id="stepInput" type="number" min="0.01" max="1" step="0.01" />
</div>
<div class="field">
<label for="scanEndInput">扫描终止角</label>
<input id="scanEndInput" type="number" min="1" max="720" step="1" />
</div>
<div class="field">
<label for="durationInput">duration 总时长</label>
<input id="durationInput" type="number" min="0.02" max="60" step="0.01" />
</div>
</div>
<div class="actions">
<button class="secondary" id="validButton" type="button">有效样例</button>
<button class="secondary" id="invalidButton" type="button">异常样例</button>
<button class="primary" id="runButton" type="button">调用接口</button>
<button class="primary" id="scanButton" type="button">角度扫描</button>
<button class="primary" id="scanButton" type="button">仿真扫描</button>
</div>
<div class="message" id="inputMessage" aria-live="polite"></div>
@@ -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: "有效样例预览",

View File

@@ -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",

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_CrankSlider" ||
req_cmd == "Cmd_FourBar_CrankSlider_Simulate")
{
return handleFourBarCommand(req_cmd, req_param);
}

View File

@@ -1,5 +1,82 @@
#include "KinematicsWebAPI.h"
#include "FourBarMechanism/CrankSliderMechanism.h"
#include <cmath>
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<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 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<CrankSliderMechanism> 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<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));
}
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},

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