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
@@ -0,0 +1,171 @@
import { useState } from 'react';
import { Button, Select } from '../components/ui';
import type { SimulationSnapshot } from '../simulation/SimulationSession';
import type { BridgeStatus } from './ExternalControlClient';
import { ROBOT_PROFILES } from './registry';
export interface ExternalControlUI {
status: BridgeStatus;
endpoint: string;
setEndpoint: (value: string) => void;
token: string;
setToken: (value: string) => void;
observationAgeMs?: number;
connect: () => Promise<void>;
disconnect: (reason?: string) => void;
authorize: () => void;
stop: () => void;
}
export function ExternalControlPanel({
snapshot,
loading,
bridge,
onApplyProfile,
}: {
snapshot: SimulationSnapshot;
loading: boolean;
bridge: ExternalControlUI;
onApplyProfile: (id?: string) => void;
}) {
const [profileId, setProfileId] = useState(snapshot.robot?.profileId ?? '');
const external = snapshot.externalControl,
ready = bridge.status.phase === 'ready';
const owner = { manual: '手动', python: 'Python', policy: 'ONNX', external: '外部' }[
snapshot.controlOwner ?? 'manual'
];
return (
<section aria-label="开源机器人控制" className="space-y-3 text-xs">
<label className="block">
机器人 profile
<Select
aria-label="控制接口 profile"
className="mt-1 w-full"
value={profileId}
disabled={loading}
onChange={(e) => setProfileId(e.target.value)}
>
<option value="">不启用(普通模型)</option>
{ROBOT_PROFILES.map((p) => (
<option key={p.id} value={p.id}>
{p.label}
</option>
))}
</Select>
</label>
<Button disabled={loading} onClick={() => onApplyProfile(profileId || undefined)}>
校验并重新编译 profile
</Button>
<p className="text-text-secondary">
仅显式选择后生效;未知模型/版本拒绝接管。LeKiwi 使用简化动力学,不是实机标定。
</p>
<p>
当前 profile:<strong>{snapshot.robot?.profileId ?? '未启用'}</strong>
</p>
{snapshot.robot && (
<p>
模型指纹:
<code title={snapshot.robot.modelFingerprint}>
{snapshot.robot.modelFingerprint.slice(0, 16)}
</code>
</p>
)}
<label className="block">
本机桥接地址
<input
aria-label="本机桥接地址"
className="field mt-1 w-full px-2 py-1"
value={bridge.endpoint}
disabled={bridge.status.phase !== 'disconnected'}
onChange={(e) => bridge.setEndpoint(e.target.value)}
/>
</label>
<label className="block">
控制 token(仅当前页面内存)
<input
aria-label="控制 token"
type="password"
autoComplete="off"
className="field mt-1 w-full px-2 py-1"
value={bridge.token}
disabled={bridge.status.phase !== 'disconnected'}
onChange={(e) => bridge.setToken(e.target.value)}
/>
</label>
<div className="flex flex-wrap gap-2">
<Button
disabled={
loading ||
!snapshot.robot ||
bridge.status.phase !== 'disconnected' ||
!bridge.token.trim()
}
onClick={() => void bridge.connect()}
>
{bridge.status.phase === 'connecting' ? '连接中…' : '连接桥接'}
</Button>
<Button
disabled={bridge.status.phase === 'disconnected'}
onClick={() => bridge.disconnect()}
>
断开桥接
</Button>
</div>
<div className="flex flex-wrap gap-2">
<Button
variant="primary"
disabled={loading || !ready || snapshot.paused || Boolean(external?.enabled)}
onClick={bridge.authorize}
>
允许外部控制
</Button>
<Button disabled={!external?.enabled} onClick={bridge.stop}>
停止外部控制
</Button>
</div>
<p>
连接:
<span aria-label="桥接连接状态">
{ready ? '已连接' : bridge.status.phase === 'connecting' ? '连接中' : '未连接'}
</span>
</p>
<p>
当前控制所有者:<strong aria-label="当前控制所有者">{owner}</strong>
</p>
<p aria-label="外部控制状态">
{external?.connected
? '外部控制者已连接'
: external?.enabled
? '等待 Python 控制者'
: '未授权'}
</p>
<p aria-label="机器人观测状态">
{snapshot.paused
? '已暂停(非实时观测)'
: bridge.observationAgeMs === undefined
? '尚无观测'
: `观测年龄 ${bridge.observationAgeMs.toFixed(0)} ms${bridge.observationAgeMs > 500 ? ' · 已过期' : ''}`}
</p>
{external && (
<p className="technical-value">
epoch {external.modelEpoch} · applied seq {external.lastActionSeq}
</p>
)}
{(bridge.status.error || (external?.error && !external.enabled)) && (
<p role="status" className="text-warning">
{bridge.status.error || external?.error}
</p>
)}
<p className="text-text-tertiary">
先播放再授权。接管会停止 Python/ONNX,并锁定
1×。暂停、重载、页面隐藏和500ms超时均撤销授权;不会自动恢复旧命令。
</p>
<p className="text-text-tertiary">
物理/观测不依赖渲染帧率。外控视图上限30FPS;检测到软件渲染时关闭阴影并降至5FPS,为控制保留时间。
</p>
<p className="text-text-tertiary">
V1:底盘/臂/夹爪和状态回读;不支持相机、锁步、LeRobot 数据采集或训练。
</p>
</section>
);
}