feat: add configurable VDO.Ninja overview and floating window focus
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
require('dotenv').config({ quiet: true });
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
|
||||
const url = new URL(process.env.DATABASE_URL);
|
||||
url.hostname = '192.168.0.20';
|
||||
url.port = '15437';
|
||||
url.pathname = '/lms';
|
||||
url.searchParams.set('connect_timeout', '5');
|
||||
const db = new PrismaClient({ datasources: { db: { url: url.toString() } } });
|
||||
const apply = process.argv.includes('--apply');
|
||||
const slug = 'futuredrive-autonomy-basic';
|
||||
const localImage = 'future-nav2-gazebo:humble';
|
||||
const remoteImage = 'future-nav2:humble-jetson_orin';
|
||||
const names = ['NAV2-Control', 'NAV2-Localization', 'NAV2-Map', 'NAV2-Perception',
|
||||
'NAV2-Perception3D', 'NAV2-Planning', 'NAV2-Gazebo', 'NAV2-SLAM', 'NAV2-SLAM3D',
|
||||
'NAV2-MKMini', 'NAV2-Rviz2'];
|
||||
const steps = [
|
||||
{ title: 'SLAM', detail: '원거리 MK-mini 플랫폼에 연결하여 모듈 소스와 파라미터를 수정하고, 센서 토픽을 확인하며 SLAM 맵을 생성·저장합니다. 로컬 MK-mini 시뮬레이션에서 먼저 검증합니다.' },
|
||||
{ title: '자율주행', detail: '저장한 맵으로 위치추정·경로 계획·제어 모듈을 구성하고 목표 지점 주행을 검증합니다. 원격 코드 편집과 빌드, 실행 로그 분석 및 파라미터 튜닝을 실습합니다.' },
|
||||
];
|
||||
|
||||
function parameters(module, remote) {
|
||||
const sim = `sim_mode:=${!remote}`;
|
||||
switch (module.name) {
|
||||
case 'NAV2-Gazebo': return ['gui:=false', 'platform:=mkmini'];
|
||||
case 'NAV2-MKMini': return [sim, 'platform:=mkmini', 'robot:=mkmini', 'level:=2d', ...(!remote ? ['simulator:=gazebo'] : [])];
|
||||
case 'NAV2-Control': return [sim, 'robot:=mkmini', 'level:=2d'];
|
||||
case 'NAV2-Perception': return [sim];
|
||||
case 'NAV2-Perception3D':
|
||||
case 'NAV2-SLAM3D': return [sim, 'robot:=mkmini'];
|
||||
case 'NAV2-Localization': return [sim, 'robot:=mkmini', 'level:=2d', 'localization_mode:=amcl'];
|
||||
case 'NAV2-SLAM':
|
||||
case 'NAV2-Planning':
|
||||
case 'NAV2-Map': return [sim, 'robot:=mkmini', 'level:=2d'];
|
||||
default: return module.defaultParameters;
|
||||
}
|
||||
}
|
||||
|
||||
async function snapshot(client) {
|
||||
return client.course.findUniqueOrThrow({ where: { slug }, include: {
|
||||
weeks: { orderBy: { week: 'asc' }, include: {
|
||||
modules: { orderBy: { sortOrder: 'asc' } }, topics: { orderBy: { sortOrder: 'asc' } },
|
||||
practices: { orderBy: { sortOrder: 'asc' } }, references: true,
|
||||
} }, environmentProfiles: { orderBy: [{ week: 'asc' }, { platformKey: 'asc' }] },
|
||||
weekMaps: true,
|
||||
} });
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const before = await snapshot(db);
|
||||
assert.equal(before.weeks.length, 2, 'Expected only weeks 1 and 2; refusing to overwrite');
|
||||
const source = before.weeks.find(w => w.week === 1);
|
||||
assert(source);
|
||||
const selected = names.map(name => {
|
||||
const matches = source.modules.filter(m => m.name === name);
|
||||
assert.equal(matches.length, 1, `Missing or ambiguous module: ${name}`);
|
||||
return matches[0];
|
||||
});
|
||||
const profiles = before.environmentProfiles.filter(p => p.week === 1);
|
||||
assert.equal(profiles.length, 2);
|
||||
assert(profiles.some(p => p.config.target === 'local'));
|
||||
assert(profiles.some(p => p.config.target === 'remote-jetson'));
|
||||
if (apply) {
|
||||
const backup = path.resolve('backups', `base-before-week3-${Date.now()}.json`);
|
||||
fs.mkdirSync(path.dirname(backup), { recursive: true });
|
||||
fs.writeFileSync(backup, JSON.stringify(before, null, 2), { mode: 0o600, flag: 'wx' });
|
||||
console.log('Backup:', backup);
|
||||
}
|
||||
let summary;
|
||||
try {
|
||||
await db.$transaction(async tx => {
|
||||
assert.deepEqual(await snapshot(tx), before, 'Source changed during preparation');
|
||||
const week = await tx.courseWeek.create({ data: {
|
||||
courseId: before.id, week: 3, title: '원격 개발', tags: ['MK-mini', '진행예정'],
|
||||
progress: 0, state: 'locked', duration: '1주', github: source.github,
|
||||
overview: '원거리 MK-mini 플랫폼을 연결하고 원격으로 코드를 편집·빌드·실행합니다. 로컬 MK-mini 시뮬레이션과 실차에서 SLAM 및 자율주행을 검증합니다.',
|
||||
deliverable: 'SLAM 저장 맵, 자율주행 검증 결과, 원격 개발 소스 변경 내역 및 실행 로그를 제출합니다.',
|
||||
requiresSourceSubmission: true,
|
||||
topics: { create: steps.map((s, i) => ({ ...s, sortOrder: i + 1 })) },
|
||||
practices: { create: steps.map((s, i) => ({ title: s.title, description: s.detail, sortOrder: i + 1 })) },
|
||||
} });
|
||||
const mapping = new Map();
|
||||
const created = [];
|
||||
for (const [i, m] of selected.entries()) {
|
||||
const { id, weekId, createdAt, updatedAt, ...data } = m;
|
||||
const next = await tx.courseWeekModule.create({ data: {
|
||||
...data, weekId: week.id, sortOrder: i + 1,
|
||||
dockerImage: localImage, defaultParameters: parameters(m, false),
|
||||
} });
|
||||
mapping.set(id, next.id);
|
||||
created.push(next);
|
||||
}
|
||||
const newProfiles = [];
|
||||
for (const p of profiles) {
|
||||
const remote = p.config.target === 'remote-jetson';
|
||||
const config = structuredClone(p.config);
|
||||
config.moduleSet.modules = p.config.moduleSet.modules.flatMap(entry => {
|
||||
if (!mapping.has(entry.id)) return [];
|
||||
const m = selected.find(m => m.id === entry.id);
|
||||
return [{ ...entry, id: mapping.get(entry.id), dockerImage: remote ? remoteImage : localImage,
|
||||
defaultParameters: parameters(m, remote) }];
|
||||
});
|
||||
config.moduleSet.includeUnlisted = false;
|
||||
assert.equal(config.moduleSet.modules.length, 9);
|
||||
assert(config.moduleSet.modules.every(m => created.some(c => c.id === m.id)));
|
||||
newProfiles.push(await tx.courseEnvironmentProfile.create({ data: {
|
||||
courseId: before.id, week: 3, platformKey: p.platformKey, profileType: p.profileType,
|
||||
name: p.name, enabled: p.enabled, config,
|
||||
} }));
|
||||
}
|
||||
for (const m of before.weekMaps.filter(m => m.week === 1)) {
|
||||
await tx.courseWeekMap.create({ data: { courseId: before.id, week: 3, mapKey: m.mapKey, sortOrder: m.sortOrder } });
|
||||
}
|
||||
const after = await snapshot(tx);
|
||||
assert.deepEqual(after.weeks.filter(w => w.week !== 3), before.weeks);
|
||||
assert.deepEqual(after.environmentProfiles.filter(p => p.week !== 3), before.environmentProfiles);
|
||||
assert.equal(after.weeks.find(w => w.week === 3).topics.length, 2);
|
||||
assert(!created.some(m => /yahboom|turtlebot/i.test(JSON.stringify(m))));
|
||||
summary = { applied: apply, course: before.title, weekId: week.id, title: week.title,
|
||||
steps: steps.map(s => s.title), modules: created.map(m => m.name),
|
||||
environments: newProfiles.map(p => ({ target: p.config.target, modules: p.config.moduleSet.modules.map(m => ({
|
||||
name: created.find(c => c.id === m.id).name, image: m.dockerImage, parameters: m.defaultParameters,
|
||||
})) })) };
|
||||
if (!apply) throw new Error('DRY_RUN_ROLLBACK');
|
||||
}, { isolationLevel: 'Serializable', timeout: 30000 });
|
||||
} catch (e) {
|
||||
if (e.message !== 'DRY_RUN_ROLLBACK') throw e;
|
||||
}
|
||||
console.log(JSON.stringify(summary, null, 2));
|
||||
}
|
||||
main().catch(e => { console.error(e.message); process.exitCode = 1; }).finally(() => db.$disconnect());
|
||||
Reference in New Issue
Block a user