3ad29356c9
集成通用机器人数值接口、本机控制桥、LeRobot 插件和统一键盘遥操作。采用离线 CoACD 全臂碰撞配方 revision 4、局部装配区切分与结构自接触,限制直接关节位姿写入并保留安全看门狗。同步版本号、变更记录、来源许可证和兼容性验证。
172 lines
5.8 KiB
TypeScript
172 lines
5.8 KiB
TypeScript
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>
|
||
);
|
||
}
|