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