import { expect, test } from '@playwright/test'; import { resolve } from 'node:path'; import { startBridge } from './fixtures/controlBridge'; import type {} from '../physics/runner'; test('LeKiwi:真实通用 Python SDK 双向控制,3秒30Hz,无 LeRobot 依赖', async ({ page }) => { 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(process.cwd(), 'build/lekiwi')}`, ); await page.evaluate( ({ endpoint, token }) => window.lekiwiPhysics.connectBridge(endpoint, token), { endpoint: bridge.endpoint, token: bridge.token }, ); const run = await bridge.python(` import json, os, time from mujoco_control_bridge import SimRobotClient with SimRobotClient(os.environ['MUJOCO_CONTROL_ENDPOINT']) as robot: values={c['id']:0.0 for c in robot.describe()['actionChannels']} values.update({'arm_shoulder_pan.pos':.1,'arm_gripper.pos':.5}) before=robot.get_observation()['values'] start=time.monotonic(); latency=[] for i in range(90): values.update({'x.vel':.08 if i<30 else 0,'y.vel':.08 if 30<=i<60 else 0,'theta.vel':.3 if i>=60 else 0}) tick=time.monotonic(); robot.send_action(values); obs=robot.get_observation() latency.append((time.monotonic()-tick)*1000) time.sleep(max(0,start+(i+1)/30-time.monotonic())) print(json.dumps({'before':before,'after':obs['values'],'rttMeanMs':sum(latency)/len(latency),'rttMaxMs':max(latency),'seq':obs['appliedActionSeq']})) `); const result = JSON.parse(run.stdout.trim()); expect(result.after['base.x'] - result.before['base.x']).toBeGreaterThan(0.04); expect(result.after['base.y'] - result.before['base.y']).toBeGreaterThan(0.04); expect(result.after['base.yaw']).toBeGreaterThan(0.2); expect(result.after['arm_gripper.pos']).toBeCloseTo(0.5, 1); expect(result.seq).toBeGreaterThan(85); console.log('SDK bridge timing', { meanMs: result.rttMeanMs, maxMs: result.rttMaxMs }); await expect .poll( async () => (await page.evaluate(() => window.lekiwiPhysics.externalState())).observation?.paused, ) .toBe(true); expect(bridge.errors()).toBe(''); } finally { await page.evaluate(() => window.lekiwiPhysics?.dispose()).catch(() => {}); await bridge.stop(); } });