feat(lekiwi): release V0.10.1 初步集成 LeKiwi,优化碰撞模型
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
lekiwi-compatibility / cpu-compatibility (push) Has been cancelled

集成通用机器人数值接口、本机控制桥、LeRobot 插件和统一键盘遥操作。采用离线 CoACD 全臂碰撞配方 revision 4、局部装配区切分与结构自接触,限制直接关节位姿写入并保留安全看门狗。同步版本号、变更记录、来源许可证和兼容性验证。
This commit is contained in:
2026-09-20 14:42:30 +08:00
parent da4d59c31a
commit 3ad29356c9
103 changed files with 23055 additions and 280 deletions
+97
View File
@@ -0,0 +1,97 @@
import { expect, test } from '@playwright/test';
import { resolve } from 'node:path';
import { writeFile } from 'node:fs/promises';
import type {} from '../physics/runner';
test('真实 WASM 3.11:LeKiwi 站稳、全向轮、臂夹爪与障碍阻挡', async ({ page }, info) => {
const errors: string[] = [];
page.on('pageerror', (e) => errors.push(e.message));
page.on('console', (msg) => {
if (msg.type() === 'error') console.log(msg.text());
});
await page.goto('/physics/runner.html');
await page.waitForFunction(() => Boolean(window.lekiwiPhysics));
const assetBase = `/@fs${resolve(process.cwd(), 'build/lekiwi')}`;
const initial = await page.evaluate((path) => window.lekiwiPhysics.boot(path), assetBase);
console.log('initial', initial);
expect(initial.version).toBe('3.11.0');
expect(initial.nu).toBe(9);
expect(initial.njnt).toBe(46);
await writeFile(
info.outputPath('lekiwi-profile.xml'),
await page.evaluate(() => window.lekiwiPhysics.xml()),
);
const stable = await page.evaluate(() => window.lekiwiPhysics.step(30));
console.log('stable', stable);
expect(stable.finite).toBe(true);
expect(Math.abs(stable.roll)).toBeLessThan(0.15);
expect(Math.abs(stable.pitch)).toBeLessThan(0.15);
const traces: unknown[] = [{ initial, stable }];
for (const [x, y, w] of [
[0.1, 0, 0],
[-0.1, 0, 0],
[0, 0.1, 0],
[0, -0.1, 0],
[0, 0, Math.PI / 6],
[0, 0, -Math.PI / 6],
]) {
const start = await page.evaluate(() => window.lekiwiPhysics.reset());
const end = await page.evaluate(
([x, y, w]) => window.lekiwiPhysics.drive(x, y, w, 2),
[x, y, w],
);
console.log('drive', x, y, w, 'start', start.position, start.yaw, 'end', end.position, end.yaw);
traces.push({ command: [x, y, w], start, end });
expect(end.finite).toBe(true);
if (x || y) {
expect(Math.abs(end.position[0] - start.position[0] - x * 2)).toBeLessThan(0.05);
expect(Math.abs(end.position[1] - start.position[1] - y * 2)).toBeLessThan(0.05);
} else expect(Math.abs(end.yaw - start.yaw - w * 2)).toBeLessThan((10 * Math.PI) / 180);
}
await page.evaluate(() => window.lekiwiPhysics.reset());
// Free-space tracking; negative elbow folding now has its own obstruction test.
const targets = [0.1, 0.1, 0.1, 0.1, 0.1, 0.3];
const arm = await page.evaluate((values) => window.lekiwiPhysics.arm(values, 2), targets);
console.log('arm', arm.arm);
traces.push({ targets, arm });
arm.arm.forEach((value, i) => expect(Math.abs(value - targets[i])).toBeLessThan(0.015));
await page.evaluate(() => window.lekiwiPhysics.wall());
const blocked = await page.evaluate(() => window.lekiwiPhysics.drive(0.1, 0, 0, 5));
console.log('blocked', blocked);
const contacts = await page.evaluate(() => window.lekiwiPhysics.contactPairs());
traces.push({ blocked, contacts });
expect(contacts.some((c) => c.geom1 === 'test_wall' || c.geom2 === 'test_wall')).toBe(true);
expect(blocked.maxContacts).toBeGreaterThan(0);
expect(blocked.position[0]).toBeLessThan(0.22);
// The complete arm/accessory outline reaches the wall before the old chassis proxy.
expect(blocked.position[0]).toBeGreaterThan(0.02);
expect(blocked.finite).toBe(true);
await writeFile(info.outputPath('physics-traces.json'), JSON.stringify(traces, null, 2));
expect(errors).toEqual([]);
await page.evaluate(() => window.lekiwiPhysics.dispose());
});
test('真实 Python/WASM:控制权、旧回调隔离、安全保持、reset 与导出重载', async ({ page }) => {
await page.goto('/physics/runner.html');
await page.waitForFunction(() => Boolean(window.lekiwiPhysics));
await page.evaluate(
(path) => window.lekiwiPhysics.boot(path),
`/@fs${resolve(process.cwd(), 'build/lekiwi')}`,
);
const result = await page.evaluate(() => window.lekiwiPhysics.runtimeChecks());
expect(result.pythonTarget).toBe(0.1);
expect(result.accepted.values['arm_shoulder_pan.pos']).toBe(0.3);
expect(result.afterOldDispose).toBe(0.3);
expect(result.manualBlocked).toBe(true);
expect(result.held).toBeCloseTo(result.actual, 10);
expect(result.paused?.enabled).toBe(false);
expect(result.pausedOwner).toBe('manual');
expect(result.cancel).toBe('DISCONNECTED');
expect(result.reset?.modelEpoch).toBe(1);
expect(result.reset?.paused).toBe(true);
const reloaded = await page.evaluate(() => window.lekiwiPhysics.reimport());
expect(reloaded.nu).toBe(9);
expect(reloaded.njnt).toBe(46);
expect(reloaded.robot?.profileId).toBe('lekiwi-v1');
await page.evaluate(() => window.lekiwiPhysics.dispose());
});