feat(lekiwi): release V0.10.1 初步集成 LeKiwi,优化碰撞模型
集成通用机器人数值接口、本机控制桥、LeRobot 插件和统一键盘遥操作。采用离线 CoACD 全臂碰撞配方 revision 4、局部装配区切分与结构自接触,限制直接关节位姿写入并保留安全看门狗。同步版本号、变更记录、来源许可证和兼容性验证。
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user