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
+107 -2
View File
@@ -8,6 +8,17 @@ import type { PolicyDeployment, TrainingTerrain } from '../rl/deployment';
import type { MainModule } from '@mujoco/mujoco';
import type { ProjectFile, ProjectManifest } from '../project/types';
import { prepareProjectForMujoco } from '../project/importer';
import { prepareRobotProject } from '../project/robotProfiles';
import { sha256 } from '../robot/registry';
import type { ExternalControlStatus } from '../robot/RobotRuntime';
import {
RobotError,
type RobotDescriptor,
type RobotObservation,
type RobotIdentity,
type RobotActionResult,
} from '../robot/types';
import { enhanceLeKiwiMjcf, type LeKiwiJointGeometry } from '../project/robotProfiles/lekiwi';
import {
enhanceConvertedMjcf,
groundConvertedMjcf,
@@ -43,6 +54,8 @@ export interface PhysicsLoadProgress {
}
export interface PhysicsLoadOptions {
/** Explicit built-in profile; never inferred from actuator count. */
robotProfileId?: string;
trainingDeployment?: PolicyDeployment;
/** Candidate policy is initialized/validated before replacing the active session. */
trainingPolicy?: { data: Uint8Array; path: string };
@@ -67,6 +80,13 @@ export interface PhysicsAdapter {
setSpeed(speed: number): void;
reset(): void;
singleStep(): void;
describeRobot(): RobotDescriptor | undefined;
robotObservation(): RobotObservation | undefined;
externalControlStatus(): ExternalControlStatus | undefined;
setExternalControlEnabled(enabled: boolean): void;
claimExternalControlLease(leaseId: string): RobotIdentity;
sendRobotAction(action: unknown): Promise<RobotActionResult>;
stopExternalControl(reason?: string): void;
setActuator(id: number, value: number): void;
setActuatorParameters(id: number, parameters: ActuatorParameters): boolean;
setJointPosition(id: number, value: number): boolean;
@@ -149,6 +169,8 @@ export class MainThreadPhysicsAdapter implements PhysicsAdapter {
options: PhysicsLoadOptions = {},
): Promise<SimulationSnapshot> {
if (this.disposed) throw new Error('物理适配器已释放');
if (this.session?.describeRobot?.())
this.session.stopExternalControl('模型正在重载,请重新授权');
const generation = ++this.loadGeneration;
const urdfMode = options.urdfMode ?? 'mjcf';
const baseMode = options.baseMode ?? 'floating';
@@ -169,7 +191,15 @@ export class MainThreadPhysicsAdapter implements PhysicsAdapter {
if (this.disposed || generation !== this.loadGeneration) throw new Error('模型加载已取消');
report(0.32, '校验并准备模型资源');
const workspace = new MemfsWorkspace(module, `${manifest.id}_stage_${generation}`);
const prepared = await prepareProjectForMujoco(manifest, entryPath);
if (
options.robotProfileId &&
(urdfMode !== 'mjcf' || baseMode !== 'floating' || options.trainingDeployment)
)
throw new Error('机器人 profile 需要浮动基座 URDF 转换,不能与 Go2 部署混用');
const profileManifest = options.robotProfileId
? await prepareRobotProject(manifest, entryPath, options.robotProfileId)
: manifest;
const prepared = await prepareProjectForMujoco(profileManifest, entryPath);
const supportFiles = prepared.manifest.files.filter(
(file) => !manifest.files.some((original) => original.path === file.path),
);
@@ -199,7 +229,37 @@ export class MainThreadPhysicsAdapter implements PhysicsAdapter {
baseMode,
);
const enhanced = enhanceConvertedMjcf(grounded, enhancements);
workspace.writeGenerated(convertedPath, enhanced.data);
let convertedData = enhanced.data;
if (options.robotProfileId) {
const geometry: LeKiwiJointGeometry[] = [];
for (let id = 0; id < intermediate.model.njnt; id++) {
const joint = intermediate.model.jnt(id);
const bodyId = Number(intermediate.model.jnt_bodyid[id]);
const body = intermediate.model.body(bodyId);
try {
geometry.push({
name: joint.name,
body: body.name,
position: Array.from(
intermediate.data.xpos.slice(bodyId * 3, bodyId * 3 + 3),
Number,
),
rotation: Array.from(
intermediate.data.xmat.slice(bodyId * 9, bodyId * 9 + 9),
Number,
),
});
} finally {
joint.delete();
body.delete();
}
}
convertedData = enhanceLeKiwiMjcf(convertedData, geometry);
warnings.unshift(
'LeKiwi v1:仿真估计参数,简化被动滚子/碰撞与九路伺服;超大轮 STL 已替换为简化视觉,不代表实机标定',
);
}
workspace.writeGenerated(convertedPath, convertedData);
modelRelativePath = convertedPath;
modelPath = workspace.path(modelRelativePath);
warnings.push(
@@ -285,6 +345,12 @@ export class MainThreadPhysicsAdapter implements PhysicsAdapter {
report(0.84, '编译模型与物理数据');
console.info('[MuJoCo] 编译模型', modelPath);
nextSession = new SimulationSession(module, modelPath, warnings);
if (options.robotProfileId) {
const fingerprint = await sha256(
new TextEncoder().encode(workspace.readText(modelRelativePath)),
);
nextSession.configureRobot(options.robotProfileId, fingerprint);
}
if (options.trainingDeployment) nextSession.configureDeployment(options.trainingDeployment);
if (entry?.format === 'urdf' && urdfMode === 'native') {
const offset = nextSession.alignLowestPointToGround();
@@ -413,6 +479,29 @@ export class MainThreadPhysicsAdapter implements PhysicsAdapter {
singleStep(): void {
this.session?.singleStep();
}
describeRobot(): RobotDescriptor | undefined {
return this.session?.describeRobot();
}
externalControlStatus(): ExternalControlStatus | undefined {
return this.session?.externalControlStatus();
}
robotObservation(): RobotObservation | undefined {
return this.session?.robotObservation();
}
setExternalControlEnabled(enabled: boolean): void {
this.session?.setExternalControlEnabled(enabled);
}
claimExternalControlLease(leaseId: string): RobotIdentity {
if (!this.session) throw new RobotError('DISCONNECTED', '没有仿真会话');
return this.session.claimExternalControlLease(leaseId);
}
sendRobotAction(action: unknown): Promise<RobotActionResult> {
if (!this.session) return Promise.reject(new RobotError('DISCONNECTED', '没有仿真会话'));
return this.session.sendRobotAction(action);
}
stopExternalControl(reason?: string): void {
this.session?.stopExternalControl(reason);
}
setActuator(id: number, value: number): void {
this.session?.setActuator(id, value);
}
@@ -539,6 +628,22 @@ export class MainThreadPhysicsAdapter implements PhysicsAdapter {
actuatorSection = document.querySelector('mujoco > actuator');
if (document.querySelector('parsererror')) return new TextEncoder().encode(source);
const snapshot = this.session.snapshot();
if (snapshot.robot) {
const custom = document.querySelector('mujoco > custom') ?? document.createElement('custom');
if (!custom.parentNode) document.documentElement.append(custom);
for (const [name, value] of [
['platform_robot_profile', snapshot.robot.profileId],
['platform_robot_profile_version', String(snapshot.robot.profileVersion)],
// Provenance of the loaded source, not a self-referential export hash.
['platform_robot_source_fingerprint', snapshot.robot.modelFingerprint],
]) {
const marker =
custom.querySelector(`text[name="${name}"]`) ?? document.createElement('text');
marker.setAttribute('name', name);
marker.setAttribute('data', value);
custom.append(marker);
}
}
if (actuatorSection) {
for (const info of snapshot.actuators) {
const element = Array.from(actuatorSection.children).find(