Initial commit

This commit is contained in:
zhangshun
2026-06-01 15:55:59 +08:00
commit 40f9bdb590
1799 changed files with 362227 additions and 0 deletions

394
scripts/build.ps1 Normal file
View File

@@ -0,0 +1,394 @@
# build.ps1 - 智能 WASM 构建脚本
param(
[switch]$Clean,
[switch]$Serve,
[switch]$Test,
[string]$BuildType = "Release"
)
Write-Host "=========================================" -ForegroundColor Cyan
Write-Host " 智能 JSON-WASM 项目构建系统 " -ForegroundColor Cyan
Write-Host "=========================================" -ForegroundColor Cyan
Write-Host ""
# 1. 设置环境变量
$EmsdkPath = "E:\emsdk"
if (-not (Test-Path $EmsdkPath)) {
Write-Host "[错误] 未找到 Emscripten" -ForegroundColor Red
Write-Host "请确保 Emscripten 安装在: $EmsdkPath" -ForegroundColor Yellow
exit 1
}
# 设置 PATH
$env:PATH = "$EmsdkPath\upstream\emscripten;$env:PATH"
Write-Host "[✓] 设置 Emscripten 环境" -ForegroundColor Green
# 2. 清理旧构建
if ($Clean) {
Write-Host "[ ] 清理构建文件..." -ForegroundColor Cyan
Remove-Item -Path "build", "public/wasm" -Recurse -Force -ErrorAction SilentlyContinue
Write-Host "[✓] 清理完成" -ForegroundColor Green
}
# 3. 创建目录
New-Item -Path "build", "public/wasm" -ItemType Directory -Force | Out-Null
# 4. 编译 WASM
Write-Host "[ ] 编译智能 WASM 模块..." -ForegroundColor Cyan
$startTime = Get-Date
# 编译命令 - 使用当前真实业务模块源码
$compileResult = emcc `
src/KinematicsWebAPI.cpp `
src/main.cpp `
src/Robot.cpp `
src/spc_core.cpp `
src/QuadrupedRobotSimulation/RobotConfig.cpp `
src/QuadrupedRobotSimulation/BaseClass.cpp `
src/QuadrupedRobotSimulation/KinematicsSimulation.cpp `
src/QuadrupedRobotSimulation/CompleteJsonExporter.cpp `
src/QuadrupedRobotSimulation/KinematicsReverse.cpp `
src/QuadrupedRobotSimulation/KinematicsHelper.cpp `
src/QuadrupedRobotSimulation/SharedGeometry.cpp `
src/FourBarMechanism/CrankSliderMechanism.cpp `
src/FourBarMechanism/CrankRockingBlockMechanism_Forward.cpp `
src/FourBarMechanism/CrankRockingBlockMechanism_Inverse.cpp `
src/FourBarMechanism/FourBarMechanism.cpp `
src/FourBarMechanism/SliderCrankMechanism.cpp `
src/RobotManager.cpp `
src/utils.cpp `
src/math_utils.cpp `
src/smart_json_wrapper.cpp `
src/kdl_parser/src/kdl_parser.cpp `
src/kdl_parser/src/joint.cpp `
src/kdl_parser/src/link.cpp `
src/kdl_parser/src/model.cpp `
src/kdl_parser/src/pose.cpp `
src/kdl_parser/src/tinystr.cpp `
src/kdl_parser/src/tinyxml.cpp `
src/kdl_parser/src/tinyxml2.cpp `
src/kdl_parser/src/tinyxmlerror.cpp `
src/kdl_parser/src/tinyxmlparser.cpp `
src/kdl_parser/src/twist.cpp `
src/kdl_parser/src/world.cpp `
-I./inc `
-I./inc/QuadrupedRobotSimulation `
-I./inc/FourBarMechanism `
-I./inc/SharedGeometry `
-I./src/kdl_parser/include `
-I./KDL/install_wasm/include `
-I./Eigen3 `
-I./Eigen3/eigen3 `
-L./KDL/install_wasm/lib `
-lorocos-kdl `
-std=c++17 `
-s WASM=1 `
-s EXPORTED_FUNCTIONS='["_init_func","_func","_smart_process_json","_smart_get_function_list","_smart_get_function_info","_smart_test_match","_smart_get_version","_smart_free_string","_wasm_malloc","_wasm_free","_malloc","_free","_add","_subtract","_multiply","_divide","_fibonacci","_create_buffer","_destroy_buffer","_compute_sum","_get_greeting","_main"]' `
-s EXPORTED_RUNTIME_METHODS='["UTF8ToString","stringToUTF8","lengthBytesUTF8","ccall","cwrap","HEAPU8","HEAP8","HEAPF32","HEAPF64","allocateUTF8"]' `
-s MODULARIZE=1 `
-s EXPORT_NAME='createSmartMathModule' `
-s ALLOW_MEMORY_GROWTH=1 `
-s ENVIRONMENT='web,node,worker' `
-s ASSERTIONS=1 `
-s DISABLE_EXCEPTION_CATCHING=0 `
-s INITIAL_MEMORY=67108864 `
-s MAXIMUM_MEMORY=268435456 `
-s STACK_SIZE=5242880 `
-s USE_PTHREADS=0 `
-O3 `
-Wno-deprecated-literal-operator `
-o public/wasm/smart_math.js 2>&1
$endTime = Get-Date
$duration = ($endTime - $startTime).TotalSeconds
if ($LASTEXITCODE -ne 0) {
Write-Host "[✗] 编译失败" -ForegroundColor Red
Write-Host $compileResult -ForegroundColor Red
exit 1
}
Write-Host "[✓] 编译完成" -ForegroundColor Green
# 5. 检查生成的文件
$jsFile = Get-Item "public/wasm/smart_math.js" -ErrorAction SilentlyContinue
$wasmFile = Get-Item "public/wasm/smart_math.wasm" -ErrorAction SilentlyContinue
if (-not $jsFile -or -not $wasmFile) {
Write-Host "[✗] 未找到生成文件" -ForegroundColor Red
exit 1
}
$jsSize = $jsFile.Length / 1KB
$wasmSize = $wasmFile.Length / 1KB
# 6. 显示构建结果
Write-Host ""
Write-Host "🎉 构建成功!" -ForegroundColor Green
Write-Host ("" * 50) -ForegroundColor DarkGray
Write-Host "📦 生成文件:" -ForegroundColor White
Write-Host "├─ smart_math.js ($($jsSize.ToString('F1')) KB)" -ForegroundColor Gray
Write-Host "└─ smart_math.wasm ($($wasmSize.ToString('F1')) KB)" -ForegroundColor Gray
Write-Host ""
Write-Host "✨ 功能特性:" -ForegroundColor White
Write-Host "├─ ✅ 机器人正逆解业务接口 (func/init_func)" -ForegroundColor Gray
Write-Host "├─ ✅ SPC / 四足机器人 / 连杆机构业务接口" -ForegroundColor Gray
Write-Host "├─ ✅ 智能 JSON 数学测试接口" -ForegroundColor Gray
Write-Host "├─ ✅ Node / Web 双环境加载" -ForegroundColor Gray
Write-Host "├─ ✅ 与 Emscripten 内存管理兼容" -ForegroundColor Gray
Write-Host "└─ ✅ 批量回归测试可直接调用" -ForegroundColor Gray
Write-Host ""
Write-Host "📁 输出目录: $((Get-Location).Path)\public\wasm" -ForegroundColor Gray
Write-Host "⏱️ 编译耗时: $($duration.ToString('F2'))" -ForegroundColor Gray
Write-Host ""
# 7. 生成 JavaScript 包装器(可选)
Write-Host "[ ] 生成 JavaScript 包装器..." -ForegroundColor Cyan
$jsWrapper = @"
// Smart WASM JSON API JavaScript
class SmartWasmAPI {
constructor() {
this.module = null;
this.initialized = false;
}
async initialize() {
if (this.initialized) return;
// WASM
this.module = await createSmartMathModule();
this.initialized = true;
console.log('Smart WASM API ');
return this;
}
async initializeBusinessApi() {
if (!this.initialized) {
await this.initialize();
}
let responsePtr = null;
try {
responsePtr = this.module._init_func();
const responseStr = this.module.UTF8ToString(responsePtr);
return JSON.parse(responseStr);
} finally {
if (responsePtr) {
this.module._smart_free_string(responsePtr);
}
}
}
async processBusinessRequest(jsonRequest) {
if (!this.initialized) {
await this.initialize();
}
let requestPtr = null;
let responsePtr = null;
try {
requestPtr = this.module.allocateUTF8(JSON.stringify(jsonRequest));
responsePtr = this.module._func(requestPtr);
const responseStr = this.module.UTF8ToString(responsePtr);
return JSON.parse(responseStr);
} catch (error) {
console.error(' JSON :', error);
throw error;
} finally {
if (requestPtr) {
this.module._free(requestPtr);
}
if (responsePtr) {
this.module._smart_free_string(responsePtr);
}
}
}
// JSON
async processJsonRequest(jsonRequest) {
if (!this.initialized) {
await this.initialize();
}
let requestPtr = null;
let responsePtr = null;
try {
// JSON WASM
requestPtr = this.module.allocateUTF8(JSON.stringify(jsonRequest));
// WASM
responsePtr = this.module._smart_process_json(requestPtr);
// JavaScript
const responseStr = this.module.UTF8ToString(responsePtr);
// JSON
return JSON.parse(responseStr);
} catch (error) {
console.error(' JSON :', error);
throw error;
} finally {
//
if (requestPtr) {
this.module._free(requestPtr);
}
if (responsePtr) {
this.module._smart_free_string(responsePtr);
}
}
}
//
async getFunctionList() {
if (!this.initialized) {
await this.initialize();
}
let responsePtr = null;
try {
responsePtr = this.module._smart_get_function_list();
const responseStr = this.module.UTF8ToString(responsePtr);
return JSON.parse(responseStr);
} catch (error) {
console.error(':', error);
throw error;
} finally {
if (responsePtr) {
this.module._smart_free_string(responsePtr);
}
}
}
//
async callFunction(funcName, params = {}) {
const request = {
req_cmd: funcName,
...params
};
return await this.processJsonRequest(request);
}
//
async testSmartMatch(jsonRequest) {
if (!this.initialized) {
await this.initialize();
}
let requestPtr = null;
let responsePtr = null;
try {
requestPtr = this.module.allocateUTF8(JSON.stringify(jsonRequest));
responsePtr = this.module._smart_test_match(requestPtr);
const responseStr = this.module.UTF8ToString(responsePtr);
return JSON.parse(responseStr);
} catch (error) {
console.error(':', error);
throw error;
} finally {
if (requestPtr) {
this.module._free(requestPtr);
}
if (responsePtr) {
this.module._smart_free_string(responsePtr);
}
}
}
//
async getVersion() {
if (!this.initialized) {
await this.initialize();
}
let responsePtr = null;
try {
responsePtr = this.module._smart_get_version();
const responseStr = this.module.UTF8ToString(responsePtr);
return JSON.parse(responseStr);
} catch (error) {
console.error(':', error);
throw error;
} finally {
if (responsePtr) {
this.module._smart_free_string(responsePtr);
}
}
}
// JSON
callMathFunction(name, ...args) {
if (!this.initialized) {
throw new Error('WASM ');
}
switch (name.toLowerCase()) {
case 'add':
return this.module._add(args[0] || 0, args[1] || 0);
case 'subtract':
return this.module._subtract(args[0] || 0, args[1] || 0);
case 'multiply':
return this.module._multiply(args[0] || 0, args[1] || 0);
case 'divide':
return this.module._divide(args[0] || 0, args[1] || 1);
case 'fibonacci':
return this.module._fibonacci(args[0] || 0);
default:
throw new Error(': ' + name);
}
}
}
//
if (typeof window !== 'undefined') {
window.SmartWasmAPI = SmartWasmAPI;
}
export default SmartWasmAPI;
"@
$jsWrapper | Out-File -FilePath "public/wasm/smart_api_wrapper.js" -Encoding UTF8
Write-Host "[✓] 生成 JavaScript 包装器" -ForegroundColor Green
# 8. 本地测试(可选)
if ($Test) {
Write-Host "[ ] 运行本地测试..." -ForegroundColor Cyan
try {
node --version 2>&1 | Out-Null
if ($LASTEXITCODE -eq 0) {
Write-Host "[ ] 执行 WASM 回归测试..." -ForegroundColor Cyan
node scripts/run_wasm_tests.js
}
} catch {
Write-Host "[!] 本地测试跳过(需要 Node.js" -ForegroundColor Yellow
}
}
# 9. 启动开发服务器(可选)
if ($Serve) {
Write-Host "[ ] 启动开发服务器..." -ForegroundColor Cyan
Write-Host " 访问: http://localhost:8080/smart_test.html" -ForegroundColor Yellow
Write-Host " 按 Ctrl+C 停止服务器" -ForegroundColor Yellow
Write-Host ""
Set-Location "public"
python -m http.server 8080
}
Write-Host ""
Write-Host "🚀 使用提示:" -ForegroundColor White
Write-Host "├─ 在 VS Code 中按 F5 启动调试" -ForegroundColor Gray
Write-Host "├─ 使用 Ctrl+Shift+B 快速构建" -ForegroundColor Gray
Write-Host "├─ 查看 public/wasm/smart_api_wrapper.js 了解 JavaScript API 使用" -ForegroundColor Gray
Write-Host "└─ 运行 \`npm run serve\` 启动开发服务器" -ForegroundColor Gray
Write-Host ""

View File

@@ -0,0 +1,115 @@
# rebuild_with_fixes.ps1 - 重新构建并修复错误
Write-Host "=== 重新构建 WASM 模块 ===" -ForegroundColor Cyan
# 设置环境
$EmsdkPath = "E:\emsdk"
if (-not (Test-Path $EmsdkPath)) {
Write-Host "[错误] 未找到 Emscripten" -ForegroundColor Red
exit 1
}
$env:PATH = "$EmsdkPath\upstream\emscripten;$env:PATH"
# 创建目录
New-Item -Path "public/wasm" -ItemType Directory -Force | Out-Null
Write-Host "[ ] 编译 WASM 模块(包含完整的运行时方法)..." -ForegroundColor Cyan
# 完整的编译命令
$compileCmd = @'
emcc src/math_utils.cpp src/smart_json_wrapper.cpp src/main.cpp -I./src -std=c++17 -s WASM=1 -s EXPORTED_FUNCTIONS='["_smart_process_json","_smart_get_function_list","_smart_get_function_info","_smart_test_match","_smart_get_version","_smart_free_string","_main"]' -s EXPORTED_RUNTIME_METHODS='["ccall","cwrap","UTF8ToString","stringToUTF8","lengthBytesUTF8","allocateUTF8","allocate","ALLOC_NORMAL","getValue","setValue"]' -s MODULARIZE=1 -s EXPORT_NAME="createSmartMathModule" -s ALLOW_MEMORY_GROWTH=1 -s ASSERTIONS=1 -O3 -o public/wasm/smart_math.js
'@
Write-Host "执行命令: $compileCmd" -ForegroundColor Gray
Invoke-Expression $compileCmd
if ($LASTEXITCODE -eq 0) {
Write-Host "[✓] 编译成功!" -ForegroundColor Green
if (Test-Path "public/wasm/smart_math.js") {
$jsSize = (Get-Item "public/wasm/smart_math.js").Length / 1KB
$wasmSize = (Get-Item "public/wasm/smart_math.wasm").Length / 1KB
Write-Host ""
Write-Host "📦 生成文件:" -ForegroundColor White
Write-Host "├─ smart_math.js ($($jsSize.ToString('F1')) KB)" -ForegroundColor Gray
Write-Host "└─ smart_math.wasm ($($wasmSize.ToString('F1')) KB)" -ForegroundColor Gray
Write-Host ""
Write-Host "🔧 导出的运行时方法:" -ForegroundColor Cyan
Write-Host "├─ ccall" -ForegroundColor Gray
Write-Host "├─ cwrap" -ForegroundColor Gray
Write-Host "├─ UTF8ToString" -ForegroundColor Gray
Write-Host "├─ stringToUTF8" -ForegroundColor Gray
Write-Host "├─ lengthBytesUTF8" -ForegroundColor Gray
Write-Host "├─ allocateUTF8" -ForegroundColor Gray
Write-Host "├─ allocate" -ForegroundColor Gray
Write-Host "└─ ALLOC_NORMAL" -ForegroundColor Gray
Write-Host ""
# 测试模块是否正常工作
Write-Host "[ ] 创建测试文件..." -ForegroundColor Cyan
$testHtml = @"
<!DOCTYPE html>
<html>
<head>
<title>WASM </title>
<script src="wasm/smart_math.js"></script>
</head>
<body>
<h1>WASM </h1>
<div id="output">...</div>
<script>
async function testModule() {
try {
const Module = await createSmartMathModule();
console.log('Module loaded:', Module);
// allocateUTF8
if (typeof Module.allocateUTF8 === 'function') {
console.log(' allocateUTF8 is available');
//
const versionPtr = Module._smart_get_version();
const versionStr = Module.UTF8ToString(versionPtr);
console.log('Version:', versionStr);
Module._smart_free_string(versionPtr);
document.getElementById('output').innerHTML =
'<h3 style="color: green;"> WASM </h3>' +
'<pre>' + versionStr + '</pre>';
} else {
console.log(' allocateUTF8 is NOT available');
document.getElementById('output').innerHTML =
'<h3 style="color: red;"> allocateUTF8 </h3>';
}
} catch (error) {
console.error('Error:', error);
document.getElementById('output').innerHTML =
'<h3 style="color: red;"> : ' + error.message + '</h3>';
}
}
testModule();
</script>
</body>
</html>
"@
Set-Content -Path "public/test_wasm.html" -Value $testHtml -Encoding UTF8
Write-Host "[✓] 测试文件已创建: public/test_wasm.html" -ForegroundColor Green
Write-Host ""
Write-Host "🌐 启动测试服务器:" -ForegroundColor Cyan
Write-Host " cd public" -ForegroundColor Gray
Write-Host " python -m http.server 8080" -ForegroundColor Gray
Write-Host " 然后在浏览器中打开: http://localhost:8080/test_wasm.html" -ForegroundColor Gray
} else {
Write-Host "[✗] 未找到生成文件" -ForegroundColor Red
}
} else {
Write-Host "[✗] 编译失败,退出码: $LASTEXITCODE" -ForegroundColor Red
}

480
scripts/run_wasm_tests.js Normal file
View File

@@ -0,0 +1,480 @@
const fs = require("fs");
const path = require("path");
const rootDir = path.resolve(__dirname, "..");
const defaultModulePath = path.join(rootDir, "public", "wasm", "smart_math.js");
function readJson(relativePath) {
const filePath = path.join(rootDir, relativePath);
return JSON.parse(fs.readFileSync(filePath, "utf8"));
}
function ensure(condition, message) {
if (!condition) {
throw new Error(message);
}
}
function getByPath(value, pathExpression) {
const parts = pathExpression.split(".");
let current = value;
for (const part of parts) {
if (current === undefined || current === null) {
return undefined;
}
if (/^\d+$/.test(part)) {
current = current[Number(part)];
} else {
current = current[part];
}
}
return current;
}
function jointArrayToString(joints) {
return joints.join(",");
}
function poseToString(position, orientation) {
return [...position, ...orientation].join(",");
}
function positionDistance(positionA, positionB) {
const dx = positionA[0] - positionB[0];
const dy = positionA[1] - positionB[1];
const dz = positionA[2] - positionB[2];
return Math.sqrt(dx * dx + dy * dy + dz * dz);
}
function quaternionDistance(q1, q2) {
const dot = Math.abs(
q1[0] * q2[0] +
q1[1] * q2[1] +
q1[2] * q2[2] +
q1[3] * q2[3]
);
return 1 - Math.min(1, dot);
}
async function loadModule(modulePath) {
const required = require(modulePath);
if (typeof required === "function") {
return required({
noInitialRun: true,
locateFile: (fileName) => path.join(path.dirname(modulePath), fileName),
});
}
if (required && typeof required === "object") {
return required;
}
throw new Error(`Unsupported WASM loader type: ${typeof required}`);
}
function callJsonFunction(module, exportName, payload) {
const requestText = typeof payload === "string" ? payload : JSON.stringify(payload);
const requestPtr = module.allocateUTF8(requestText);
let responsePtr = 0;
try {
responsePtr = module[exportName](requestPtr);
const responseText = module.UTF8ToString(responsePtr);
return JSON.parse(responseText);
} finally {
if (responsePtr) {
module._smart_free_string(responsePtr);
}
module._free(requestPtr);
}
}
function callNoArgJsonFunction(module, exportName) {
const responsePtr = module[exportName]();
try {
const responseText = module.UTF8ToString(responsePtr);
return JSON.parse(responseText);
} finally {
module._smart_free_string(responsePtr);
}
}
function callBusinessApi(module, request) {
return callJsonFunction(module, "_func", request);
}
function buildForwardRequest(robotUuid, joints) {
return {
msg: "fk roundtrip",
req_code: "AUTO_FK",
req_from: "wasm_test",
req_cmd: "Cmd_Kinematics_forward_pose_str",
req_param: {
robot_uuid: robotUuid,
q_init_str: jointArrayToString(joints),
},
};
}
function buildInverseRequest(command, robotUuid, poseStr, qInitStr, extra = {}) {
return {
msg: "ik roundtrip",
req_code: "AUTO_IK",
req_from: "wasm_test",
req_cmd: command,
req_param: {
robot_uuid: robotUuid,
pose_str: poseStr,
q_init_str: qInitStr,
...extra,
},
};
}
function assertStaticCase(response, assertions) {
for (const assertion of assertions) {
const actual = getByPath(response, assertion.path);
if (assertion.equals !== undefined) {
ensure(
actual === assertion.equals,
`${assertion.path} expected ${JSON.stringify(assertion.equals)}, got ${JSON.stringify(actual)}`
);
}
if (assertion.exists) {
ensure(actual !== undefined && actual !== null, `${assertion.path} is missing`);
}
if (assertion.gte !== undefined) {
ensure(actual >= assertion.gte, `${assertion.path} expected >= ${assertion.gte}, got ${actual}`);
}
if (assertion.lengthEquals !== undefined) {
ensure(Array.isArray(actual), `${assertion.path} is not an array`);
ensure(
actual.length === assertion.lengthEquals,
`${assertion.path} expected length ${assertion.lengthEquals}, got ${actual.length}`
);
}
if (assertion.lengthGte !== undefined) {
ensure(Array.isArray(actual), `${assertion.path} is not an array`);
ensure(
actual.length >= assertion.lengthGte,
`${assertion.path} expected length >= ${assertion.lengthGte}, got ${actual.length}`
);
}
if (assertion.includes !== undefined) {
ensure(
String(actual).includes(assertion.includes),
`${assertion.path} expected to include ${assertion.includes}, got ${actual}`
);
}
}
}
async function runStaticCase(module, testCase) {
const request = readJson(testCase.requestFile);
const response = callBusinessApi(module, request);
assertStaticCase(response, testCase.assertions);
return response;
}
async function runRoundtripSingleCase(module, testCase) {
const seed = readJson(testCase.seedFile);
const forwardResponse = callBusinessApi(module, buildForwardRequest(seed.robot_uuid, seed.target_joints));
ensure(forwardResponse.success === true, "forward request failed");
ensure(getByPath(forwardResponse, "res_data.success") === true, "forward result is not successful");
const forwardPosition = getByPath(forwardResponse, "res_data.position");
const forwardOrientation = getByPath(forwardResponse, "res_data.orientation");
const poseStr = poseToString(forwardPosition, forwardOrientation);
const inverseResponse = callBusinessApi(
module,
buildInverseRequest(
"Cmd_Kinematics_inverse_pose_str_NoDifference",
seed.robot_uuid,
poseStr,
seed.q_init_str
)
);
ensure(inverseResponse.success === true, "inverse request failed");
ensure(getByPath(inverseResponse, "res_data.success") === true, "inverse result is not successful");
const jointSolutions = getByPath(inverseResponse, "res_data.joints");
ensure(Array.isArray(jointSolutions) && jointSolutions.length >= 1, "inverse result contains no joint solution");
const finalJoints = jointSolutions[jointSolutions.length - 1];
const forwardCheck = callBusinessApi(module, buildForwardRequest(seed.robot_uuid, finalJoints));
ensure(forwardCheck.success === true, "forward validation request failed");
ensure(getByPath(forwardCheck, "res_data.success") === true, "forward validation result is not successful");
const checkedPosition = getByPath(forwardCheck, "res_data.position");
const checkedOrientation = getByPath(forwardCheck, "res_data.orientation");
const posDiff = positionDistance(forwardPosition, checkedPosition);
const quatDiff = quaternionDistance(forwardOrientation, checkedOrientation);
ensure(
posDiff <= seed.position_tolerance,
`roundtrip position diff ${posDiff} exceeds tolerance ${seed.position_tolerance}`
);
ensure(
quatDiff <= seed.orientation_tolerance,
`roundtrip quaternion diff ${quatDiff} exceeds tolerance ${seed.orientation_tolerance}`
);
return {
response: inverseResponse,
details: {
positionDiff: posDiff,
quaternionDiff: quatDiff,
solutionCount: jointSolutions.length,
},
};
}
async function runRoundtripPathCase(module, testCase) {
const seed = readJson(testCase.seedFile);
const startForward = callBusinessApi(module, buildForwardRequest(seed.robot_uuid, seed.start_joints));
const endForward = callBusinessApi(module, buildForwardRequest(seed.robot_uuid, seed.end_joints));
const startPose = poseToString(
getByPath(startForward, "res_data.position"),
getByPath(startForward, "res_data.orientation")
);
const endPose = poseToString(
getByPath(endForward, "res_data.position"),
getByPath(endForward, "res_data.orientation")
);
const inverseResponse = callBusinessApi(
module,
buildInverseRequest(
"Cmd_Kinematics_inverse_pose_str_2PSteps",
seed.robot_uuid,
`${startPose};${endPose}`,
seed.q_init_str,
{ steps_str: String(seed.steps) }
)
);
ensure(inverseResponse.success === true, "path inverse request failed");
ensure(getByPath(inverseResponse, "res_data.success") === true, "path inverse result is not successful");
const jointSolutions = getByPath(inverseResponse, "res_data.joints");
ensure(Array.isArray(jointSolutions), "path inverse result is not an array");
ensure(
jointSolutions.length === seed.steps,
`path inverse expected ${seed.steps} solutions, got ${jointSolutions.length}`
);
const firstForwardCheck = callBusinessApi(module, buildForwardRequest(seed.robot_uuid, jointSolutions[0]));
const lastForwardCheck = callBusinessApi(
module,
buildForwardRequest(seed.robot_uuid, jointSolutions[jointSolutions.length - 1])
);
const startPosDiff = positionDistance(
getByPath(startForward, "res_data.position"),
getByPath(firstForwardCheck, "res_data.position")
);
const endPosDiff = positionDistance(
getByPath(endForward, "res_data.position"),
getByPath(lastForwardCheck, "res_data.position")
);
ensure(
startPosDiff <= seed.position_tolerance,
`path start position diff ${startPosDiff} exceeds tolerance ${seed.position_tolerance}`
);
ensure(
endPosDiff <= seed.position_tolerance,
`path end position diff ${endPosDiff} exceeds tolerance ${seed.position_tolerance}`
);
return {
response: inverseResponse,
details: {
solutionCount: jointSolutions.length,
startPositionDiff: startPosDiff,
endPositionDiff: endPosDiff,
},
};
}
const suite = [
{
id: "list_robots_after_init",
type: "static",
requestFile: "tests/testdata/kinematics/list_robots.json",
assertions: [
{ path: "success", equals: true },
{ path: "res_data.success", equals: true },
{ path: "res_data.count", gte: 2 },
],
},
{
id: "kinematics_forward_zero",
type: "static",
requestFile: "tests/testdata/kinematics/forward_zero.json",
assertions: [
{ path: "success", equals: true },
{ path: "res_data.success", equals: true },
{ path: "res_data.position", lengthEquals: 3 },
{ path: "res_data.orientation", lengthEquals: 4 },
],
},
{
id: "kinematics_forward_all_joints_path",
type: "static",
requestFile: "tests/testdata/kinematics/forward_all_joints_path.json",
assertions: [
{ path: "success", equals: true },
{ path: "res_data.success", equals: true },
{ path: "res_data.OPERATION.frames", lengthEquals: 2 },
],
},
{
id: "spc_basic_5x30",
type: "static",
requestFile: "tests/testdata/spc/basic_5x30.json",
assertions: [
{ path: "success", equals: true },
{ path: "res_data.XR.n", equals: 5 },
{ path: "res_data.XR.k", equals: 30 },
{ path: "res_data.Cpk.USL", equals: 1.7 },
{ path: "res_data.Cpk.LSL", equals: 1.5 },
{ path: "res_data.Cpk.Cpk", gte: 0.1 },
],
},
{
id: "fourbar_valid",
type: "static",
requestFile: "tests/testdata/fourbar/crank_slider_valid.json",
assertions: [
{ path: "success", equals: true },
{ path: "res_data.success", equals: true },
{ path: "res_data.points.B.x", exists: true },
{ path: "res_data.trajectory", lengthGte: 1 },
{ path: "res_data.slider_trajectory", lengthGte: 1 },
],
},
{
id: "fourbar_invalid",
type: "static",
requestFile: "tests/testdata/fourbar/crank_slider_invalid.json",
assertions: [
{ path: "success", equals: true },
{ path: "res_data.success", equals: false },
{ path: "res_data.error", includes: "Invalid parameters" },
{ path: "res_data.validation_errors", lengthGte: 1 },
],
},
{
id: "quadruped_points_from_motor_angles",
type: "static",
requestFile: "tests/testdata/quadruped/calculate_points_from_motor_angles.json",
assertions: [
{ path: "success", equals: true },
{ path: "res_data.objStates", lengthGte: 20 },
],
},
{
id: "quadruped_forward_gait",
type: "static",
requestFile: "tests/testdata/quadruped/perform_forward_kinematics.json",
assertions: [
{ path: "success", equals: true },
{ path: "res_data.GaitInfo.TotalFrames", equals: 40 },
{ path: "res_data.ConstraintData.AB1_AB_Constraint.IsValid", equals: true },
],
},
{
id: "ik_roundtrip_single_pose",
type: "roundtrip_single",
seedFile: "tests/testdata/kinematics/roundtrip_single_seed.json",
},
{
id: "ik_roundtrip_two_pose_path",
type: "roundtrip_path",
seedFile: "tests/testdata/kinematics/roundtrip_path_seed.json",
},
];
async function runCase(module, testCase) {
switch (testCase.type) {
case "static":
return runStaticCase(module, testCase);
case "roundtrip_single":
return runRoundtripSingleCase(module, testCase);
case "roundtrip_path":
return runRoundtripPathCase(module, testCase);
default:
throw new Error(`Unknown test type: ${testCase.type}`);
}
}
async function main() {
const modulePath = path.resolve(process.argv[2] || defaultModulePath);
ensure(fs.existsSync(modulePath), `WASM JS loader not found: ${modulePath}`);
console.log(`Loading module: ${modulePath}`);
const module = await loadModule(modulePath);
ensure(typeof module._init_func === "function", "_init_func is not exported");
ensure(typeof module._func === "function", "_func is not exported");
ensure(typeof module.allocateUTF8 === "function", "allocateUTF8 is not exported");
ensure(typeof module.UTF8ToString === "function", "UTF8ToString is not exported");
const initResponse = callNoArgJsonFunction(module, "_init_func");
ensure(initResponse.success === true, `init_func failed: ${JSON.stringify(initResponse)}`);
console.log("Initialization complete.");
let passed = 0;
const failures = [];
for (const testCase of suite) {
try {
const result = await runCase(module, testCase);
passed += 1;
if (result && result.details) {
console.log(`[PASS] ${testCase.id} ${JSON.stringify(result.details)}`);
} else {
console.log(`[PASS] ${testCase.id}`);
}
} catch (error) {
failures.push({ id: testCase.id, message: error.message });
console.error(`[FAIL] ${testCase.id}: ${error.message}`);
}
}
console.log("");
console.log(`Result: ${passed}/${suite.length} passed`);
if (failures.length > 0) {
console.log("Failed cases:");
for (const failure of failures) {
console.log(`- ${failure.id}: ${failure.message}`);
}
process.exitCode = 1;
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

67
scripts/serve.ps1 Normal file
View File

@@ -0,0 +1,67 @@
# serve.ps1 - 开发服务器启动脚本
Write-Host "=== 启动开发服务器 ===" -ForegroundColor Cyan
Write-Host ""
# 检查是否已构建
if (-not (Test-Path "public/wasm/smart_math.js")) {
Write-Host "[!] 未找到 WASM 文件,正在构建..." -ForegroundColor Yellow
& "$PSScriptRoot\build.ps1"
if (-not (Test-Path "public/wasm/smart_math.js")) {
Write-Host "[✗] 构建失败,无法启动服务器" -ForegroundColor Red
exit 1
}
}
# 检查 Python
try {
python --version 2>&1 | Out-Null
$hasPython = $true
} catch {
$hasPython = $false
}
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
break
} catch {
continue
}
}
}
if (-not $hasPython) {
Write-Host "[✗] 未找到 Python请安装 Python 3.7+ 并添加到 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
Set-Location "public"
python -m http.server 8080

42
scripts/test_compile.ps1 Normal file
View File

@@ -0,0 +1,42 @@
# test_compile.ps1 - 测试编译
Write-Host "=== 测试编译 ===" -ForegroundColor Cyan
# 设置环境
$EmsdkPath = "E:\emsdk"
if (-not (Test-Path $EmsdkPath)) {
Write-Host "[错误] 未找到 Emscripten" -ForegroundColor Red
exit 1
}
$env:PATH = "$EmsdkPath\upstream\emscripten;$env:PATH"
# 测试 1: 编译 math_utils.cpp
Write-Host "[ ] 测试编译 math_utils.cpp..." -ForegroundColor Cyan
emcc src/math_utils.cpp -I./src -s WASM=1 -c -o test_math.o 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host "[✓] math_utils.cpp 编译成功" -ForegroundColor Green
} else {
Write-Host "[✗] math_utils.cpp 编译失败" -ForegroundColor Red
}
# 测试 2: 编译 smart_json_wrapper.cpp
Write-Host "[ ] 测试编译 smart_json_wrapper.cpp..." -ForegroundColor Cyan
emcc src/smart_json_wrapper.cpp -I./src -s WASM=1 -c -o test_wrapper.o 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host "[✓] smart_json_wrapper.cpp 编译成功" -ForegroundColor Green
} else {
Write-Host "[✗] smart_json_wrapper.cpp 编译失败" -ForegroundColor Red
}
# 测试 3: 编译 main.cpp
Write-Host "[ ] 测试编译 main.cpp..." -ForegroundColor Cyan
emcc src/main.cpp -I./src -s WASM=1 -c -o test_main.o 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host "[✓] main.cpp 编译成功" -ForegroundColor Green
} else {
Write-Host "[✗] main.cpp 编译失败" -ForegroundColor Red
}
# 清理
Remove-Item test_*.o -ErrorAction SilentlyContinue