Files
Mujoco_WASM/web_platform/e2e/lekiwi.lerobot.spec.ts
T
chenlin 3ad29356c9
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
feat(lekiwi): release V0.10.1 初步集成 LeKiwi,优化碰撞模型
集成通用机器人数值接口、本机控制桥、LeRobot 插件和统一键盘遥操作。采用离线 CoACD 全臂碰撞配方 revision 4、局部装配区切分与结构自接触,限制直接关节位姿写入并保留安全看门狗。同步版本号、变更记录、来源许可证和兼容性验证。
2026-09-20 14:42:30 +08:00

216 lines
8.6 KiB
TypeScript

import { expect, test } from '@playwright/test';
import { resolve } from 'node:path';
import { existsSync } from 'node:fs';
import { writeFile } from 'node:fs/promises';
import { startBridge } from './fixtures/controlBridge';
import type {} from '../physics/runner';
const python = process.env.LEROBOT_PYTHON ?? resolve('build/venvs/lerobot/bin/python');
test('真实终端输入:底盘与六路臂/夹爪点动、反向、保持和退出撤权', async ({ page }, info) => {
expect(existsSync(python), '需要真实的独立 LeRobot 环境').toBe(true);
const bridge = await startBridge();
try {
await page.goto('/physics/runner.html');
await page.waitForFunction(() => Boolean(window.lekiwiPhysics));
await page.evaluate(
(path) => window.lekiwiPhysics.boot(path),
`/@fs${resolve('build/lekiwi')}`,
);
await page.evaluate(
({ endpoint, token }) => window.lekiwiPhysics.connectBridge(endpoint, token),
{ endpoint: bridge.endpoint, token: bridge.token },
);
const result = await bridge.python(
`
import json, os, pty, select, subprocess, sys, termios, time
from urllib.request import Request, urlopen
def observe():
request=Request(os.environ['MUJOCO_CONTROL_ENDPOINT']+'/api/control/v1/observation',
headers={'Authorization':'Bearer '+os.environ['MUJOCO_CONTROL_TOKEN']})
with urlopen(request,timeout=2) as response:
return json.load(response)
master,slave=pty.openpty()
original_terminal=termios.tcgetattr(slave)
# This checks free-space hold, not servo compliance under self contact. The
# complete CAD assembly now blocks shoulder_lift near .12rad at this posture.
# Use 5 deg/s for this free-space test; keep hold/reversal thresholds unchanged.
# armCollision/fullCollision.spec separately exercise physical obstruction.
process=subprocess.Popen([sys.executable,'-u',os.environ['KEYBOARD_DEMO'],'--arm-speed','5'],stdin=slave,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
try:
assert select.select([process.stdout],[],[],10)[0], 'terminal demo did not start'
assert 'w/s' in process.stdout.readline().decode()
initial=observe()
for _ in range(10):
os.write(master,b'wuiotyv');time.sleep(.1)
# No input for longer than the watchdog: base stops, arm holds, actions continue.
time.sleep(.35)
held=observe()
time.sleep(.4)
held_again=observe()
for _ in range(6):
os.write(master,b'jklghb');time.sleep(.1)
os.write(master,b' ')
time.sleep(.35)
reversed_pose=observe()
os.write(master,b'q')
_,err=process.communicate(timeout=5)
assert process.returncode==0,err.decode()
assert termios.tcgetattr(slave)==original_terminal, 'terminal settings were not restored'
print(json.dumps({'exit':process.returncode,'initial':initial,'held':held,
'heldAgain':held_again,'reversed':reversed_pose}))
finally:
if process.poll() is None:
process.kill();process.wait()
os.close(master);os.close(slave)
`,
python,
{ KEYBOARD_DEMO: resolve('examples/lekiwi/teleoperate_sim.py') },
);
const report = JSON.parse(result.stdout.trim());
const reportPath = info.outputPath('keyboard-teleop.json');
await writeFile(reportPath, JSON.stringify(report, null, 2));
await info.attach('keyboard-teleop.json', {
path: reportPath,
contentType: 'application/json',
});
expect(report.exit).toBe(0);
expect(report.held.values['base.x'] - report.initial.values['base.x']).toBeGreaterThan(0.01);
expect(Math.abs(report.heldAgain.values['x.vel'])).toBeLessThan(0.02);
expect(report.heldAgain.appliedActionSeq).toBeGreaterThan(report.held.appliedActionSeq);
for (const joint of [
'arm_shoulder_pan',
'arm_shoulder_lift',
'arm_elbow_flex',
'arm_wrist_flex',
'arm_wrist_roll',
'arm_gripper',
]) {
const key = `${joint}.pos`;
expect(report.held.values[key] - report.initial.values[key], `${joint} 正向`).toBeGreaterThan(
0.03,
);
expect(
Math.abs(report.heldAgain.values[key] - report.held.values[key]),
`${joint} 保持`,
).toBeLessThan(0.03);
expect(
report.heldAgain.values[key] - report.reversed.values[key],
`${joint} 反向`,
).toBeGreaterThan(0.03);
}
await expect
.poll(
async () =>
(await page.evaluate(() => window.lekiwiPhysics.externalState())).observation?.paused,
)
.toBe(true);
const final = await page.evaluate(() => window.lekiwiPhysics.externalState());
expect(final.control).toMatchObject({ enabled: false, connected: false });
expect(bridge.errors()).toBe('');
} finally {
await page.evaluate(() => window.lekiwiPhysics?.dispose()).catch(() => {});
await bridge.stop();
}
});
test('真实 LeRobot 0.6.1 工厂/插件:无硬件 demo 与实际部分动作/限幅回传', async ({ page }) => {
expect(
existsSync(python),
'请先创建隔离 LeRobot 环境或设置 LEROBOT_PYTHON;兼容门槛不能 skipped',
).toBe(true);
const bridge = await startBridge();
try {
await page.goto('/physics/runner.html');
await page.waitForFunction(() => Boolean(window.lekiwiPhysics));
await page.evaluate(
(path) => window.lekiwiPhysics.boot(path),
`/@fs${resolve('build/lekiwi')}`,
);
await page.evaluate(
({ endpoint, token }) => window.lekiwiPhysics.connectBridge(endpoint, token),
{ endpoint: bridge.endpoint, token: bridge.token },
);
const run = await bridge.python(
`
import os, runpy, sys
sys.argv=['demo_control.py','--duration','3']
runpy.run_path(os.environ['LEKIWI_DEMO'],run_name='__main__')
`,
python,
{ LEKIWI_DEMO: resolve('examples/lekiwi/demo_control.py') },
);
const result = JSON.parse(run.stdout.trim());
expect(result.lerobot).toBe('0.6.1');
expect(result.steps).toBe(90);
expect(result.maxTranslationM).toBeGreaterThan(0.05);
expect(result.timeouts).toBe(0);
expect(Object.keys(result.observation)).toHaveLength(9);
await expect
.poll(
async () =>
(await page.evaluate(() => window.lekiwiPhysics.externalState())).observation?.paused,
)
.toBe(true);
// The previous demo paused the simulation. An old paused sample must explain
// the missing play step, not misreport it as a running simulation freeze.
const paused = await bridge.python(
`
import json, os, time
from mujoco_control_bridge import RobotError
from lerobot_robot_mujoco import LeKiwiSim, LeKiwiSimConfig
time.sleep(.6)
robot=LeKiwiSim(LeKiwiSimConfig(endpoint=os.environ['MUJOCO_CONTROL_ENDPOINT']))
try:
robot.connect()
except RobotError as error:
assert not robot.is_connected
print(json.dumps({'code':error.code,'message':str(error)}))
else:
robot.disconnect()
raise AssertionError('paused simulation must refuse a new controller')
`,
python,
);
expect(JSON.parse(paused.stdout.trim())).toMatchObject({
code: 'PAUSED',
message: expect.stringContaining('播放'),
});
await page.evaluate(() => window.lekiwiPhysics.authorize());
const partial = await bridge.python(
`
import json, os, torch, sys
from lerobot.utils.import_utils import register_third_party_plugins
from lerobot.robots.config import RobotConfig
from lerobot.robots.utils import make_robot_from_config
register_third_party_plugins()
robot=make_robot_from_config(RobotConfig.get_choice_class('lekiwi_sim')(endpoint=os.environ['MUJOCO_CONTROL_ENDPOINT']))
robot.connect()
try:
first=robot.send_action({'arm_shoulder_pan.pos':180,'arm_gripper.pos':150,'x.vel':.1})
second=robot.send_action({'arm_shoulder_lift.pos':5})
observed=robot.get_observation()
assert not torch.cuda.is_initialized()
assert not any(m in sys.modules for m in ('serial','zmq','scservo_sdk','pyrealsense2'))
print(json.dumps({'first':first,'second':second,'observed':observed}))
finally:
robot.disconnect()
`,
python,
);
const values = JSON.parse(partial.stdout.trim());
expect(values.first['arm_gripper.pos']).toBe(100);
expect(values.first['arm_shoulder_pan.pos']).toBeCloseTo((1.57 * 180) / Math.PI);
expect(values.second['arm_shoulder_pan.pos']).toBe(values.first['arm_shoulder_pan.pos']);
expect(values.second['x.vel']).toBe(0);
expect(values.second['arm_gripper.pos']).toBe(100);
expect(
Math.abs(values.observed['arm_shoulder_pan.pos'] - values.first['arm_shoulder_pan.pos']),
).toBeGreaterThan(1);
expect(bridge.errors()).toBe('');
} finally {
await page.evaluate(() => window.lekiwiPhysics?.dispose()).catch(() => {});
await bridge.stop();
}
});