f3a8a38acd
web-platform-ci / Standalone decision service (no cloud credentials) (push) Has been cancelled
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
web-platform-ci / Standalone decision service (no cloud credentials) (pull_request) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (pull_request) Has been cancelled
web-platform-ci / Playwright E2E (pull_request) Has been cancelled
lekiwi-compatibility / cpu-compatibility (pull_request) Has been cancelled
集成同源 BYOK 会话隔离、精简模型设置、官方订阅入口和 HTTPS 发布运维;保留本地训练/调参与控制能力。同步 npm 版本及 CHANGELOG,记录公网真实 API 验收仍待用户凭据。
137 lines
4.5 KiB
TypeScript
137 lines
4.5 KiB
TypeScript
import { RobotManager } from '../src/mobile/RobotManager';
|
|
import { MOBILE_ROBOTS } from '../src/mobile/RobotDescriptor';
|
|
import type { MobilePolicyMetadata } from '../src/mobile/ONNXPolicyRunner';
|
|
import { MuJoCoViewer } from '../src/viewer/MuJoCoViewer';
|
|
import { TaskDragController } from '../src/mobile/TaskDragController';
|
|
import { Vector3 } from 'three';
|
|
|
|
let viewer: MuJoCoViewer | undefined;
|
|
let drag: TaskDragController | undefined;
|
|
const manager = new RobotManager({
|
|
attach(session) {
|
|
viewer?.attach(session);
|
|
},
|
|
});
|
|
function snapshot() {
|
|
const env = manager.env!;
|
|
const observation = env.observe();
|
|
return {
|
|
robotId: env.config.id,
|
|
state: Array.from(env.state),
|
|
observation: Array.from(observation),
|
|
ctrl: Array.from(env.session.data.ctrl, Number),
|
|
qpos: Array.from(env.session.data.qpos, Number),
|
|
time: Number(env.session.data.time),
|
|
reward: env.kernel.result.reward,
|
|
stage: env.kernel.result.info.stage,
|
|
revision: env.revision,
|
|
heap: env.wasmHeapBytes,
|
|
version: env.session.module.mj_versionString(),
|
|
workspace: manager.adapter.workspace!.root,
|
|
};
|
|
}
|
|
const api = {
|
|
async boot(url: string, id: string) {
|
|
const config = MOBILE_ROBOTS.find((c) => c.id === id)!;
|
|
const response = await fetch(url);
|
|
if (!response.ok) throw new Error(`asset HTTP ${response.status}`);
|
|
const previous = manager.adapter.session;
|
|
await manager.loadZip(new File([await response.blob()], 'robot.zip'), config);
|
|
return {
|
|
...snapshot(),
|
|
retiredModelDeleted: previous?.model.isDeleted() ?? true,
|
|
retiredDataDeleted: previous?.data.isDeleted() ?? true,
|
|
};
|
|
},
|
|
async invalidCandidate(url: string) {
|
|
const config = structuredClone(MOBILE_ROBOTS[1]);
|
|
const wrong = {
|
|
...config,
|
|
armActuators: ['missing_actuator', ...config.armActuators.slice(1)],
|
|
};
|
|
await manager.loadZip(new File([await (await fetch(url)).blob()], 'robot.zip'), wrong);
|
|
},
|
|
step(n: number, action?: number[], manual = false) {
|
|
const env = manager.env!;
|
|
if (action) env.action.set(action);
|
|
const started = performance.now();
|
|
for (let i = 0; i < n; i++) env.step(env.action, manual);
|
|
return { ...snapshot(), wallMs: performance.now() - started };
|
|
},
|
|
reset() {
|
|
manager.reset();
|
|
return snapshot();
|
|
},
|
|
move(entity: 'object' | 'goal', position: number[]) {
|
|
manager.move(entity, position);
|
|
return snapshot();
|
|
},
|
|
state: snapshot,
|
|
workspaces() {
|
|
const module = manager.adapter.session!.module as unknown as {
|
|
FS: { readdir(p: string): string[] };
|
|
};
|
|
return module.FS.readdir('/workspace').filter((s) => s !== '.' && s !== '..');
|
|
},
|
|
async download() {
|
|
const bytes = await manager.exportTrainingBundle();
|
|
const href = URL.createObjectURL(new Blob([bytes.slice().buffer]));
|
|
const link = document.createElement('a');
|
|
link.href = href;
|
|
link.download = 'training.zip';
|
|
link.click();
|
|
setTimeout(() => URL.revokeObjectURL(href), 1000);
|
|
},
|
|
async policy(modelUrl: string, metadata: MobilePolicyMetadata) {
|
|
await manager.loadPolicy(new Uint8Array(await (await fetch(modelUrl)).arrayBuffer()), metadata);
|
|
manager.setMode('policy');
|
|
manager.setRunning(true);
|
|
},
|
|
tick(now: number) {
|
|
return {
|
|
...manager.advance(now),
|
|
metrics: manager.policy?.metrics,
|
|
time: Number(manager.env!.session.data.time),
|
|
};
|
|
},
|
|
show() {
|
|
const host = document.createElement('div');
|
|
host.style.cssText = 'width:1000px;height:650px;position:relative';
|
|
document.body.append(host);
|
|
const noop = () => {};
|
|
viewer = new MuJoCoViewer(host, {
|
|
advance: (now) => manager.advance(now),
|
|
onSelection: noop,
|
|
onFrame: noop,
|
|
onError: (e) => {
|
|
throw e;
|
|
},
|
|
onMapEditorSelect: noop,
|
|
onMapEditorPreviewSelect: noop,
|
|
onParametricMapSelect: noop,
|
|
onParametricMapTransform: noop,
|
|
onMapEditorTransform: noop,
|
|
});
|
|
viewer.attach(manager.adapter.session);
|
|
drag = new TaskDragController(viewer, manager);
|
|
},
|
|
screen(entity: 'object' | 'goal') {
|
|
const s = manager.env!.state,
|
|
offset = entity === 'object' ? 37 : 47;
|
|
const p = new Vector3(s[offset], s[offset + 1], s[offset + 2]).project(viewer!.camera);
|
|
const r = viewer!.renderer.domElement.getBoundingClientRect();
|
|
return { x: r.left + (p.x + 1) * 0.5 * r.width, y: r.top + (1 - p.y) * 0.5 * r.height };
|
|
},
|
|
async dispose() {
|
|
drag?.dispose();
|
|
await manager.dispose();
|
|
viewer?.dispose();
|
|
},
|
|
};
|
|
declare global {
|
|
interface Window {
|
|
mobilePhysics: typeof api;
|
|
}
|
|
}
|
|
window.mobilePhysics = api;
|