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