feat(lekiwi): release V0.10.1 初步集成 LeKiwi,优化碰撞模型
集成通用机器人数值接口、本机控制桥、LeRobot 插件和统一键盘遥操作。采用离线 CoACD 全臂碰撞配方 revision 4、局部装配区切分与结构自接触,限制直接关节位姿写入并保留安全看门狗。同步版本号、变更记录、来源许可证和兼容性验证。
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
import fixture from '../../../contracts/fixtures/single-joint.json';
|
||||
import { RobotRuntime } from './RobotRuntime';
|
||||
import type { RobotAdapter } from './types';
|
||||
import { validateDescriptor, validateValues } from './validation';
|
||||
|
||||
function setup() {
|
||||
const descriptor = validateDescriptor(fixture.descriptor);
|
||||
let clock = 0,
|
||||
paused = false,
|
||||
measured = 0.12;
|
||||
const adapter: RobotAdapter = {
|
||||
describe: () => descriptor,
|
||||
readObservation: () => ({ 'slider.position': measured }),
|
||||
validateAction: (v) => validateValues(v, descriptor.actionChannels, true),
|
||||
applyAction: vi.fn((v) => validateValues(v, descriptor.actionChannels, true)),
|
||||
safeStop: vi.fn(),
|
||||
reset: () => {
|
||||
measured = 0;
|
||||
},
|
||||
dispose: vi.fn(),
|
||||
};
|
||||
const fault = vi.fn(() => {
|
||||
paused = true;
|
||||
});
|
||||
const runtime = new RobotRuntime(
|
||||
adapter,
|
||||
'sim-test',
|
||||
() => paused,
|
||||
fault,
|
||||
() => clock,
|
||||
);
|
||||
runtime.authorize();
|
||||
const identity = runtime.claim('lease-test');
|
||||
const action = (seq = 1) => ({ ...fixture.validAction, ...identity, actionSeq: seq });
|
||||
return {
|
||||
runtime,
|
||||
adapter,
|
||||
fault,
|
||||
action,
|
||||
tick: (ms: number) => {
|
||||
clock += ms;
|
||||
},
|
||||
pause: () => {
|
||||
paused = true;
|
||||
},
|
||||
};
|
||||
}
|
||||
describe('RobotRuntime bounded mailbox', () => {
|
||||
it('applies only at a step, confirms after the step, observations are not targets', async () => {
|
||||
const s = setup();
|
||||
let confirmed = false;
|
||||
const ack = s.runtime.enqueue(s.action()).then((r) => {
|
||||
confirmed = true;
|
||||
return r;
|
||||
});
|
||||
expect(s.adapter.applyAction).not.toHaveBeenCalled();
|
||||
expect(s.runtime.beforeStep()).toBe(true);
|
||||
expect(s.adapter.applyAction).toHaveBeenCalledOnce();
|
||||
expect(confirmed).toBe(false);
|
||||
s.runtime.afterStep(0.002);
|
||||
expect((await ack).values['slider.position']).toBe(0.3);
|
||||
expect(s.runtime.latest()?.values['slider.position']).toBe(0.12);
|
||||
expect(s.runtime.latest()?.appliedActionSeq).toBe(1);
|
||||
});
|
||||
it('only newest pending target survives and old sequences cannot replay', async () => {
|
||||
const s = setup();
|
||||
const old = s.runtime.enqueue(s.action()).catch((e) => e.code);
|
||||
const next = s.runtime.enqueue(s.action(2));
|
||||
expect(await old).toBe('SUPERSEDED');
|
||||
await expect(s.runtime.enqueue(s.action())).rejects.toMatchObject({ code: 'STALE' });
|
||||
s.runtime.beforeStep();
|
||||
s.runtime.afterStep(0.002);
|
||||
expect((await next).actionSeq).toBe(2);
|
||||
});
|
||||
it('wall clock watchdog cancels stale actions before the first resumed step', async () => {
|
||||
const s = setup();
|
||||
const ack = s.runtime.enqueue(s.action()).catch((e) => e.code);
|
||||
s.tick(501);
|
||||
expect(s.runtime.beforeStep()).toBe(false);
|
||||
expect(await ack).toBe('DISCONNECTED');
|
||||
expect(s.adapter.applyAction).not.toHaveBeenCalled();
|
||||
expect(s.fault).toHaveBeenCalledOnce();
|
||||
expect(s.runtime.status().enabled).toBe(false);
|
||||
await expect(s.runtime.enqueue(s.action(2))).rejects.toMatchObject({ code: 'UNAUTHORIZED' });
|
||||
});
|
||||
it('a socket event delivered after freeze cannot renew an expired lease', async () => {
|
||||
const s = setup();
|
||||
s.runtime.capture(0);
|
||||
s.tick(501);
|
||||
expect(s.runtime.status().observationAgeMs).toBe(501);
|
||||
await expect(s.runtime.enqueue(s.action())).rejects.toMatchObject({ code: 'TIMEOUT' });
|
||||
expect(s.adapter.applyAction).not.toHaveBeenCalled();
|
||||
expect(s.fault).toHaveBeenCalledOnce();
|
||||
expect(s.runtime.status().enabled).toBe(false);
|
||||
});
|
||||
it('reset/dispose reject outstanding requests and invalidate epoch', async () => {
|
||||
const s = setup();
|
||||
const pending = s.runtime.enqueue(s.action()).catch((e) => e.code);
|
||||
s.runtime.reset();
|
||||
expect(await pending).toBe('DISCONNECTED');
|
||||
expect(s.runtime.status().modelEpoch).toBe(1);
|
||||
s.runtime.authorize();
|
||||
s.runtime.claim('new-lease');
|
||||
await expect(s.runtime.enqueue(s.action(2))).rejects.toMatchObject({ code: 'STALE' });
|
||||
s.runtime.dispose();
|
||||
s.runtime.dispose();
|
||||
expect(s.adapter.dispose).toHaveBeenCalledOnce();
|
||||
});
|
||||
it('paused/multiple owners denied and nonowner revocation does not write', async () => {
|
||||
const s = setup();
|
||||
expect(() => s.runtime.claim('other')).toThrow(/已有/);
|
||||
s.pause();
|
||||
await expect(s.runtime.enqueue(s.action())).rejects.toMatchObject({ code: 'PAUSED' });
|
||||
vi.mocked(s.adapter.safeStop).mockClear();
|
||||
s.runtime.revoke('not owner', false);
|
||||
expect(s.adapter.safeStop).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user