add commits..
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
UPDATE "course_environment_profiles"
|
||||
SET "config" = replace(
|
||||
replace(
|
||||
replace(
|
||||
"config"::text,
|
||||
'nav2-slam:humble',
|
||||
'future-nav2-gazebo:humble'
|
||||
),
|
||||
'runtime-images/nav2/humble/latest/nav2-slam-humble.tar.zst',
|
||||
'runtime-images/nav2/gazebo/latest/future-nav2-gazebo-humble.tar.zst'
|
||||
),
|
||||
'nav2-slam-humble.tar.zst',
|
||||
'future-nav2-gazebo-humble.tar.zst'
|
||||
)::jsonb
|
||||
WHERE "config"::text LIKE '%nav2-slam:humble%'
|
||||
OR "config"::text LIKE '%nav2-slam-humble.tar.zst%';
|
||||
@@ -0,0 +1,17 @@
|
||||
UPDATE "course_week_modules" AS module
|
||||
SET "prebuilt_artifacts" = (
|
||||
SELECT jsonb_agg(
|
||||
CASE
|
||||
WHEN artifact->>'fileName' = 'lms-nav2-gazebo-simulator-build-install.tar.zst'
|
||||
OR artifact->>'url' LIKE '%/lms-nav2-gazebo-simulator-build-install.tar.zst'
|
||||
THEN jsonb_set(
|
||||
artifact,
|
||||
'{checksumSha256}',
|
||||
to_jsonb('8432100a17d85047ab4ac79445d5eacf236ccc79a785db61d14270104af270b7'::text)
|
||||
)
|
||||
ELSE artifact
|
||||
END
|
||||
)
|
||||
FROM jsonb_array_elements(module."prebuilt_artifacts") AS artifact
|
||||
)
|
||||
WHERE module."folder_name" = '[lms-nav2-gazebo-simulator]nav2-simulator_v1.0.0_ws';
|
||||
@@ -0,0 +1,18 @@
|
||||
-- The NAV2 viewer expects the unified slam-ros-bridge API
|
||||
-- (/state, /ws/state and /ws/control). The older nav2-ros-bridge context
|
||||
-- does not expose those endpoints, so remove that override from every
|
||||
-- local NAV2 course environment profile.
|
||||
UPDATE "course_environment_profiles"
|
||||
SET
|
||||
"config" = jsonb_set(
|
||||
("config"::jsonb #- '{bridge,buildContext}'),
|
||||
'{bridge,image}',
|
||||
'"slam-ros-bridge:latest"'::jsonb,
|
||||
true
|
||||
),
|
||||
"updated_at" = CURRENT_TIMESTAMP
|
||||
WHERE "platform_key" = 'NAV2'
|
||||
AND (
|
||||
"config"::jsonb #>> '{bridge,buildContext}' = 'resources/nav2-ros-bridge'
|
||||
OR "config"::jsonb #>> '{bridge,image}' = 'nav2-ros-bridge'
|
||||
);
|
||||
@@ -0,0 +1,27 @@
|
||||
UPDATE "course_week_modules" AS module
|
||||
SET "prebuilt_artifacts" = (
|
||||
SELECT jsonb_agg(
|
||||
CASE
|
||||
WHEN artifact->>'fileName' = 'lms-nav2-gazebo-planning-build-install.tar.zst'
|
||||
OR artifact->>'url' LIKE '%/lms-nav2-gazebo-planning-build-install.tar.zst'
|
||||
THEN jsonb_set(
|
||||
artifact,
|
||||
'{checksumSha256}',
|
||||
to_jsonb('6087b12b35a4c2ea3073c6b9fad5e43175d7425c2f4a042fa654e8c42b2bf29d'::text)
|
||||
)
|
||||
WHEN artifact->>'fileName' = 'lms-nav2-gazebo-control-build-install.tar.zst'
|
||||
OR artifact->>'url' LIKE '%/lms-nav2-gazebo-control-build-install.tar.zst'
|
||||
THEN jsonb_set(
|
||||
artifact,
|
||||
'{checksumSha256}',
|
||||
to_jsonb('ce8949b8a14f6d29b3924b82d4819923368fb67029f45cc50a07175afa659a7a'::text)
|
||||
)
|
||||
ELSE artifact
|
||||
END
|
||||
)
|
||||
FROM jsonb_array_elements(module."prebuilt_artifacts") AS artifact
|
||||
)
|
||||
WHERE module."folder_name" IN (
|
||||
'[lms-nav2-gazebo-planning]nav2-planning_v1.0.0_ws',
|
||||
'[lms-nav2-gazebo-control]nav2-control_v1.0.0_ws'
|
||||
);
|
||||
@@ -0,0 +1,20 @@
|
||||
UPDATE "course_week_modules" AS module
|
||||
SET "default_parameters" = CASE
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM jsonb_array_elements(module."default_parameters") AS parameter
|
||||
WHERE parameter #>> '{}' LIKE 'localization_mode:=%'
|
||||
) THEN (
|
||||
SELECT jsonb_agg(
|
||||
CASE
|
||||
WHEN parameter #>> '{}' LIKE 'localization_mode:=%'
|
||||
THEN to_jsonb('localization_mode:=amcl'::text)
|
||||
ELSE parameter
|
||||
END
|
||||
)
|
||||
FROM jsonb_array_elements(module."default_parameters") AS parameter
|
||||
)
|
||||
ELSE COALESCE(module."default_parameters", '[]'::jsonb)
|
||||
|| jsonb_build_array('localization_mode:=amcl')
|
||||
END
|
||||
WHERE module."folder_name" = '[lms-nav2-gazebo-localization]nav2-localization_v1.0.0_ws';
|
||||
@@ -0,0 +1,7 @@
|
||||
-- The selected map determines SLAM/navigation and 2D/3D viewer mode.
|
||||
-- Environment profiles retain only runtime connection configuration.
|
||||
UPDATE "course_environment_profiles"
|
||||
SET
|
||||
"config" = "config" #- '{viewer,mode}',
|
||||
"updated_at" = CURRENT_TIMESTAMP
|
||||
WHERE "config" #> '{viewer,mode}' IS NOT NULL;
|
||||
@@ -0,0 +1,39 @@
|
||||
-- SLAM/navigation is selected from the map. Keep one runtime configuration
|
||||
-- for the remote platform instead of separate visualization profiles.
|
||||
DELETE FROM "course_environment_profiles" legacy
|
||||
USING "course_environment_profiles" platform
|
||||
WHERE legacy."course_id" = platform."course_id"
|
||||
AND legacy."week" = platform."week"
|
||||
AND platform."platform_key" = 'PLATFORM'
|
||||
AND legacy."platform_key" IN ('REAL_SLAM', 'REAL_SLAM_3D', 'REAL_NAVIGATION');
|
||||
|
||||
WITH canonical AS (
|
||||
SELECT id
|
||||
FROM (
|
||||
SELECT
|
||||
id,
|
||||
row_number() OVER (
|
||||
PARTITION BY "course_id", "week"
|
||||
ORDER BY CASE "platform_key"
|
||||
WHEN 'REAL_SLAM' THEN 1
|
||||
WHEN 'REAL_NAVIGATION' THEN 2
|
||||
WHEN 'REAL_SLAM_3D' THEN 3
|
||||
END
|
||||
) AS rank
|
||||
FROM "course_environment_profiles"
|
||||
WHERE "platform_key" IN ('REAL_SLAM', 'REAL_SLAM_3D', 'REAL_NAVIGATION')
|
||||
) ranked
|
||||
WHERE rank = 1
|
||||
)
|
||||
UPDATE "course_environment_profiles" profile
|
||||
SET
|
||||
"platform_key" = 'PLATFORM',
|
||||
"profile_type" = 'PLATFORM',
|
||||
"name" = '플랫폼',
|
||||
"config" = jsonb_set(profile."config" #- '{viewer,mode}', '{type}', '"PLATFORM"'::jsonb, true),
|
||||
"updated_at" = CURRENT_TIMESTAMP
|
||||
FROM canonical
|
||||
WHERE profile.id = canonical.id;
|
||||
|
||||
DELETE FROM "course_environment_profiles"
|
||||
WHERE "platform_key" IN ('REAL_SLAM', 'REAL_SLAM_3D', 'REAL_NAVIGATION');
|
||||
@@ -25,9 +25,6 @@ WHERE course_id IN (SELECT id FROM target_course);
|
||||
DELETE FROM course_environment_profiles
|
||||
WHERE course_id IN (SELECT id FROM target_course);
|
||||
|
||||
DELETE FROM course_week_visualization_platforms
|
||||
WHERE course_id IN (SELECT id FROM target_course);
|
||||
|
||||
DELETE FROM course_week_maps
|
||||
WHERE course_id IN (SELECT id FROM target_course);
|
||||
|
||||
@@ -153,23 +150,23 @@ WHERE w.course_id IN (SELECT id FROM target_course) AND w.week = 2;
|
||||
WITH module_definitions(category, name, folder_name, template_repository_url, prebuilt_artifacts, default_parameters, docker_image) AS (
|
||||
VALUES
|
||||
('Simulator', 'NAV2-Gazebo', '[lms-nav2-gazebo-simulator]nav2-simulator_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-simulator.git',
|
||||
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-simulator/1.0.0/prebuilt/latest/lms-nav2-gazebo-simulator-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-simulator-build-install.tar.zst', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('gui:=false'), 'future-nav2-gazebo:humble'),
|
||||
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-simulator/1.0.0/prebuilt/latest/lms-nav2-gazebo-simulator-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-simulator-build-install.tar.zst', 'checksumSha256', '7d2cfe092e81ef15a6e55525410d6f30a803d8c81bee20541dd1d7b964b1fe53', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('gui:=false'), 'future-nav2-gazebo:humble'),
|
||||
('Vehicle', 'NAV2-TurtleBot3Burger', '[lms-nav2-gazebo-vehicle]nav2-vehicle_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-vehicle.git',
|
||||
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-vehicle/1.0.0/prebuilt/latest/lms-nav2-gazebo-vehicle-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-vehicle-build-install.tar.zst', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=true', 'platform:=turtlebot3', 'level:=2d'), 'future-nav2-gazebo:humble'),
|
||||
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-vehicle/1.0.0/prebuilt/latest/lms-nav2-gazebo-vehicle-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-vehicle-build-install.tar.zst', 'checksumSha256', '42e8179172c178978e5ae5d48e189164f6b8a5030fdaa0eba497ca86a97e96bb', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=true', 'platform:=turtlebot3', 'level:=2d'), 'future-nav2-gazebo:humble'),
|
||||
('Map', 'NAV2-Map', '[lms-nav2-gazebo-map]nav2-map_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-map.git',
|
||||
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-map/1.0.0/prebuilt/latest/lms-nav2-gazebo-map-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-map-build-install.tar.zst', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), '[]'::jsonb, 'future-nav2-gazebo:humble'),
|
||||
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-map/1.0.0/prebuilt/latest/lms-nav2-gazebo-map-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-map-build-install.tar.zst', 'checksumSha256', 'f409ce54c40aeeed8cf3fc06251c2e321887b3ec5f5827dfb74897a62780582f', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), '[]'::jsonb, 'future-nav2-gazebo:humble'),
|
||||
('Localization', 'NAV2-SLAM', '[lms-nav2-gazebo-slam]nav2-slam_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-slam.git',
|
||||
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-slam/1.0.0/prebuilt/latest/lms-nav2-gazebo-slam-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-slam-build-install.tar.zst', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), '[]'::jsonb, 'future-nav2-gazebo:humble'),
|
||||
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-slam/1.0.0/prebuilt/latest/lms-nav2-gazebo-slam-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-slam-build-install.tar.zst', 'checksumSha256', '186f2aa926eb41140b2783507b521083536aee3be716cf55c591a32250ec0fce', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), '[]'::jsonb, 'future-nav2-gazebo:humble'),
|
||||
('Localization', 'NAV2-Localization', '[lms-nav2-gazebo-localization]nav2-localization_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-localization.git',
|
||||
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-localization/1.0.0/prebuilt/latest/lms-nav2-gazebo-localization-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-localization-build-install.tar.zst', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('localization_mode:=slam'), 'future-nav2-gazebo:humble'),
|
||||
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-localization/1.0.0/prebuilt/latest/lms-nav2-gazebo-localization-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-localization-build-install.tar.zst', 'checksumSha256', '44c8d50205f001a196870d03f44d435582b96c51eb4445e50cc8db6203b2c0c9', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('localization_mode:=amcl'), 'future-nav2-gazebo:humble'),
|
||||
('Perception', 'NAV2-Perception', '[lms-nav2-gazebo-perception]nav2-perception_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-perception.git',
|
||||
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-perception/1.0.0/prebuilt/latest/lms-nav2-gazebo-perception-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-perception-build-install.tar.zst', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=true'), 'future-nav2-gazebo:humble'),
|
||||
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-perception/1.0.0/prebuilt/latest/lms-nav2-gazebo-perception-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-perception-build-install.tar.zst', 'checksumSha256', '23c6523d98506caee9e982741c1c806f9f58581aba777af58eadc7cd5fca1432', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=true'), 'future-nav2-gazebo:humble'),
|
||||
('Planning', 'NAV2-Planning', '[lms-nav2-gazebo-planning]nav2-planning_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-planning.git',
|
||||
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-planning/1.0.0/prebuilt/latest/lms-nav2-gazebo-planning-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-planning-build-install.tar.zst', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), '[]'::jsonb, 'future-nav2-gazebo:humble'),
|
||||
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-planning/1.0.0/prebuilt/latest/lms-nav2-gazebo-planning-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-planning-build-install.tar.zst', 'checksumSha256', 'b0c698acf54ef71915e2707868726b998fbdbb2e2ad15fe65cefc931a9afaa78', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), '[]'::jsonb, 'future-nav2-gazebo:humble'),
|
||||
('Control', 'NAV2-Control', '[lms-nav2-gazebo-control]nav2-control_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-control.git',
|
||||
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-control/1.0.0/prebuilt/latest/lms-nav2-gazebo-control-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-control-build-install.tar.zst', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=true', 'robot:=turtlebot3', 'level:=2d'), 'future-nav2-gazebo:humble'),
|
||||
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-control/1.0.0/prebuilt/latest/lms-nav2-gazebo-control-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-control-build-install.tar.zst', 'checksumSha256', '63b3b59377c19288f47aa5619fef9b10938f400771df1805d8ca973648ac0ed8', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=true', 'robot:=turtlebot3', 'level:=2d'), 'future-nav2-gazebo:humble'),
|
||||
('Vehicle', 'NAV2-MKMini', '[lms-nav2-mkmini]nav2-mkmini_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-mkmini.git',
|
||||
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-mkmini/1.0.0/prebuilt/amd64/lms-nav2-mkmini-build-install-amd64.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-mkmini-build-install-amd64.tar.zst', 'checksumSha256', 'a11ac9e920fcbe5c5a54e086b72d453932ece32e0c7976858435c0a0ba73a8f9', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=false', 'platform:=mkmini', 'robot:=mkmini', 'level:=2d'), 'future-nav2:humble-jetson_orin')
|
||||
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-mkmini/1.0.0/prebuilt/amd64/lms-nav2-mkmini-build-install-amd64.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-mkmini-build-install-amd64.tar.zst', 'checksumSha256', '6094d2aa5188a28153452aec08e28870984b36c1bc0c06fec8091901f6d26856', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=false', 'platform:=mkmini', 'level:=2d'), 'future-nav2:humble-jetson_orin')
|
||||
)
|
||||
INSERT INTO course_week_modules (week_id, sort_order, category, name, folder_name, template_repository_url, source_type, prebuilt_artifacts, default_parameters, docker_image)
|
||||
SELECT w.id, row_number() OVER (PARTITION BY w.id ORDER BY module_definitions.folder_name), category, name, folder_name, template_repository_url, 'GIT', prebuilt_artifacts, default_parameters, docker_image
|
||||
@@ -177,19 +174,92 @@ FROM course_weeks w
|
||||
CROSS JOIN module_definitions
|
||||
WHERE w.course_id IN (SELECT id FROM target_course) AND w.week IN (1, 2);
|
||||
|
||||
INSERT INTO course_week_visualization_platforms (course_id, week, platform_key, enabled, created_at, updated_at)
|
||||
SELECT target_course.id, weeks.week, platform.platform_key, true, now(), now()
|
||||
FROM target_course
|
||||
CROSS JOIN (VALUES (1), (2)) AS weeks(week)
|
||||
JOIN visualization_platforms platform ON platform.platform_key IN ('NAV2', 'REAL_SLAM', 'REAL_NAVIGATION');
|
||||
|
||||
INSERT INTO course_environment_profiles (course_id, week, platform_key, profile_type, name, config, enabled, created_at, updated_at)
|
||||
SELECT target_course.id, weeks.week, platform.platform_key, platform.type,
|
||||
CASE WHEN platform.platform_key = 'NAV2' THEN '로컬' ELSE platform.display_name END,
|
||||
platform.default_config, true, now(), now()
|
||||
SELECT target_course.id, weeks.week, environment.platform_key, environment.profile_type,
|
||||
environment.name, environment.config, true, now(), now()
|
||||
FROM target_course
|
||||
CROSS JOIN (VALUES (1), (2)) AS weeks(week)
|
||||
JOIN visualization_platforms platform ON platform.platform_key IN ('NAV2', 'REAL_SLAM', 'REAL_NAVIGATION');
|
||||
CROSS JOIN (
|
||||
VALUES
|
||||
(
|
||||
'NAV2',
|
||||
'NAV2',
|
||||
'로컬',
|
||||
jsonb_build_object(
|
||||
'type', 'NAV2',
|
||||
'target', 'local',
|
||||
'viewer', jsonb_build_object('stack', 'nav2'),
|
||||
'bridge', jsonb_build_object(
|
||||
'name', 'nav2-ros-bridge',
|
||||
'port', 8765,
|
||||
'image', 'slam-ros-bridge:latest',
|
||||
'env', jsonb_build_object('ROBOT_STACK', 'nav2')
|
||||
),
|
||||
'runtimeImages', jsonb_build_array(jsonb_build_object(
|
||||
'url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/runtime-images/nav2/gazebo/latest/future-nav2-gazebo-humble.tar.zst',
|
||||
'image', 'future-nav2-gazebo:humble',
|
||||
'fileName', 'future-nav2-gazebo-humble.tar.zst',
|
||||
'required', true,
|
||||
'checksumSha256', '54230e039c1fa5dc82615cef3828c5acae7b8bea8e921b70cfdc5351d8301644'
|
||||
))
|
||||
)
|
||||
),
|
||||
(
|
||||
'PLATFORM',
|
||||
'PLATFORM',
|
||||
'플랫폼',
|
||||
jsonb_build_object(
|
||||
'type', 'PLATFORM',
|
||||
'target', 'remote-jetson',
|
||||
'runtimeKind', 'real',
|
||||
'viewer', jsonb_build_object('stack', 'nav2'),
|
||||
'bridge', jsonb_build_object(
|
||||
'name', 'nav2-ros-bridge',
|
||||
'port', 8765,
|
||||
'image', 'slam-ros-bridge:latest',
|
||||
'env', jsonb_build_object('ROBOT_STACK', 'nav2')
|
||||
),
|
||||
'runtimeImages', jsonb_build_array(
|
||||
jsonb_build_object(
|
||||
'url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/runtime-images/slam-ros-bridge/latest/slam-ros-bridge-latest.tar.zst',
|
||||
'image', 'slam-ros-bridge:latest',
|
||||
'fileName', 'slam-ros-bridge-latest.tar.zst',
|
||||
'required', true
|
||||
),
|
||||
jsonb_build_object(
|
||||
'url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/runtime-images/nav2/jetson-orin/latest/future-nav2-humble-jetson-orin.tar.zst',
|
||||
'image', 'future-nav2:humble-jetson_orin',
|
||||
'fileName', 'future-nav2-humble-jetson-orin.tar.zst',
|
||||
'required', true
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
) AS environment(platform_key, profile_type, name, config);
|
||||
|
||||
-- Viewer mode belongs to the selected map, not the platform environment.
|
||||
UPDATE course_environment_profiles profile
|
||||
SET config = profile.config::jsonb #- '{viewer,mode}'
|
||||
WHERE profile.course_id IN (SELECT id FROM target_course);
|
||||
|
||||
-- Real-platform modules need the Jetson NAV2 runtime in addition to the
|
||||
-- visualization bridge image. The desktop app downloads this artifact on
|
||||
-- demand before starting a remote module.
|
||||
UPDATE course_environment_profiles profile
|
||||
SET config = jsonb_set(
|
||||
profile.config::jsonb,
|
||||
'{runtimeImages}',
|
||||
COALESCE(profile.config::jsonb->'runtimeImages', '[]'::jsonb) || jsonb_build_array(jsonb_build_object(
|
||||
'url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/runtime-images/nav2/jetson-orin/latest/future-nav2-humble-jetson-orin.tar.zst',
|
||||
'image', 'future-nav2:humble-jetson_orin',
|
||||
'fileName', 'future-nav2-humble-jetson-orin.tar.zst',
|
||||
'required', true
|
||||
)),
|
||||
true
|
||||
)
|
||||
WHERE profile.course_id IN (SELECT id FROM target_course)
|
||||
AND profile.platform_key = 'PLATFORM'
|
||||
AND NOT (COALESCE(profile.config::jsonb->'runtimeImages', '[]'::jsonb) @> '[{"image":"future-nav2:humble-jetson_orin"}]'::jsonb);
|
||||
|
||||
-- Local simulation gets all Gazebo modules. Platform profiles only receive modules applicable to their workflow.
|
||||
UPDATE course_environment_profiles profile
|
||||
@@ -204,19 +274,16 @@ SET config = profile.config::jsonb || jsonb_build_object('moduleSet', jsonb_buil
|
||||
AND week.week = profile.week
|
||||
AND (
|
||||
(profile.platform_key = 'NAV2' AND module.folder_name LIKE '[lms-nav2-gazebo-%')
|
||||
OR (profile.platform_key IN ('REAL_SLAM', 'REAL_NAVIGATION') AND module.folder_name IN (
|
||||
'[lms-nav2-mkmini]nav2-mkmini_v1.0.0_ws',
|
||||
'[lms-nav2-gazebo-map]nav2-map_v1.0.0_ws',
|
||||
'[lms-nav2-gazebo-perception]nav2-perception_v1.0.0_ws',
|
||||
'[lms-nav2-gazebo-localization]nav2-localization_v1.0.0_ws',
|
||||
'[lms-nav2-gazebo-planning]nav2-planning_v1.0.0_ws',
|
||||
'[lms-nav2-gazebo-control]nav2-control_v1.0.0_ws'
|
||||
OR (profile.platform_key = 'PLATFORM' AND module.folder_name NOT IN (
|
||||
'[lms-nav2-gazebo-simulator]nav2-simulator_v1.0.0_ws',
|
||||
'[lms-nav2-gazebo-vehicle]nav2-vehicle_v1.0.0_ws',
|
||||
'[lms-nav2-gazebo-gui]nav2-rviz2_v1.0.0_ws'
|
||||
))
|
||||
)
|
||||
), '[]'::jsonb)
|
||||
))
|
||||
WHERE profile.course_id IN (SELECT id FROM target_course)
|
||||
AND profile.platform_key IN ('NAV2', 'REAL_SLAM', 'REAL_NAVIGATION');
|
||||
AND profile.platform_key IN ('NAV2', 'PLATFORM');
|
||||
|
||||
-- Control runs with TurtleBot3/Gazebo defaults locally, but uses MK-mini and the ARM64 image on a platform.
|
||||
UPDATE course_environment_profiles profile
|
||||
@@ -238,7 +305,30 @@ SET config = jsonb_set(profile.config::jsonb, '{moduleSet,modules}', (
|
||||
) AS control_module
|
||||
), true)
|
||||
WHERE profile.course_id IN (SELECT id FROM target_course)
|
||||
AND profile.platform_key IN ('REAL_SLAM', 'REAL_NAVIGATION');
|
||||
AND profile.platform_key = 'PLATFORM';
|
||||
|
||||
-- The 2D perception module defaults to Gazebo in the course module list.
|
||||
-- Override only that value for the real platform; the other added modules
|
||||
-- retain their own compatible base parameters.
|
||||
UPDATE course_environment_profiles profile
|
||||
SET config = jsonb_set(profile.config::jsonb, '{moduleSet,modules}', (
|
||||
SELECT jsonb_agg(
|
||||
CASE WHEN item->>'id' = perception_module.id::text THEN item || jsonb_build_object(
|
||||
'defaultParameters', jsonb_build_array('sim_mode:=false')
|
||||
) ELSE item END
|
||||
)
|
||||
FROM jsonb_array_elements(profile.config->'moduleSet'->'modules') AS item
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT module.id
|
||||
FROM course_weeks week
|
||||
JOIN course_week_modules module ON module.week_id = week.id
|
||||
WHERE week.course_id = profile.course_id
|
||||
AND week.week = profile.week
|
||||
AND module.name = 'NAV2-Perception'
|
||||
) AS perception_module
|
||||
), true)
|
||||
WHERE profile.course_id IN (SELECT id FROM target_course)
|
||||
AND profile.platform_key = 'PLATFORM';
|
||||
|
||||
INSERT INTO course_week_maps (id, course_id, week, map_key, sort_order, created_at, updated_at)
|
||||
SELECT gen_random_uuid(), target_course.id, weeks.week, map.map_key, map.sort_order, now(), now()
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { IsNotEmpty, IsString, MaxLength } from 'class-validator';
|
||||
|
||||
export class CreateInitialInstructorCourseDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(120)
|
||||
title!: string;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { InstructorService } from './instructor.service';
|
||||
import { AssignInstructorPlatformDto } from './dto/assign-instructor-platform.dto';
|
||||
import { CreateInstructorPlatformDto } from './dto/create-instructor-platform.dto';
|
||||
import { CreateInstructorCourseMapDto } from './dto/create-instructor-course-map.dto';
|
||||
import { CreateInitialInstructorCourseDto } from './dto/create-initial-instructor-course.dto';
|
||||
import { CreateInstructorStudentDto } from './dto/create-instructor-student.dto';
|
||||
import { SaveInstructorGroupDto } from './dto/save-instructor-group.dto';
|
||||
import { SaveSubmissionScoreDto } from './dto/save-submission-score.dto';
|
||||
@@ -26,6 +27,11 @@ export class InstructorController {
|
||||
return this.instructorService.listCourses(this.getBearerToken(request));
|
||||
}
|
||||
|
||||
@Post('courses/initial')
|
||||
createInitialCourse(@Req() request: Request, @Body() body: CreateInitialInstructorCourseDto) {
|
||||
return this.instructorService.createInitialCourse(this.getBearerToken(request), body.title);
|
||||
}
|
||||
|
||||
@Get('submissions')
|
||||
listSubmissions(@Req() request: Request) {
|
||||
return this.instructorService.listSubmissions(this.getBearerToken(request));
|
||||
|
||||
@@ -31,6 +31,31 @@ import { NAV2_SYSTEM_MAPS } from '../curriculum/course-maps';
|
||||
const INSTRUCTOR_ROLES = new Set(['INSTRUCTOR', 'LMS_ADMIN']);
|
||||
const DEFAULT_SUBMISSION_BOT_EMAIL = 'lms-bot@futuredrive.net';
|
||||
const COURSE_REFERENCE_UPLOAD_ROOT = join(process.cwd(), 'uploads', 'course-references');
|
||||
const BASE_COURSE_SLUGS = ['futuredrive-autonomy-basic', 'yeungnam-autonomy-basic'];
|
||||
|
||||
function copyJson(value: Prisma.JsonValue): Prisma.InputJsonValue | Prisma.JsonNullValueInput {
|
||||
return value === null ? Prisma.JsonNull : JSON.parse(JSON.stringify(value)) as Prisma.InputJsonValue;
|
||||
}
|
||||
|
||||
function remapProfileModuleIds(value: Prisma.JsonValue, moduleIds: Map<string, string>) {
|
||||
const config = copyJson(value);
|
||||
if (!config || typeof config !== 'object' || Array.isArray(config)) return config;
|
||||
|
||||
const moduleSet = (config as Record<string, unknown>).moduleSet;
|
||||
if (!moduleSet || typeof moduleSet !== 'object' || Array.isArray(moduleSet)) return config;
|
||||
|
||||
const modules = (moduleSet as Record<string, unknown>).modules;
|
||||
if (!Array.isArray(modules)) return config;
|
||||
|
||||
for (const module of modules) {
|
||||
if (!module || typeof module !== 'object' || Array.isArray(module)) continue;
|
||||
const id = (module as Record<string, unknown>).id;
|
||||
if (typeof id === 'string' && moduleIds.has(id)) {
|
||||
(module as Record<string, unknown>).id = moduleIds.get(id)!;
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
type SubmissionCommitFile = {
|
||||
path: string;
|
||||
@@ -3612,6 +3637,152 @@ export class InstructorService {
|
||||
return { authUser: user, instructor };
|
||||
}
|
||||
|
||||
async createInitialCourse(accessToken: string | undefined, title: string) {
|
||||
const { authUser, instructor } = await this.getInstructor(accessToken);
|
||||
const organizationName = authUser.primaryAffiliation?.organizationName?.trim();
|
||||
if (!organizationName) throw new BadRequestException('강사 소속 조직을 확인할 수 없습니다.');
|
||||
|
||||
const course = await this.createBaseCourseForInstructor(instructor.id, organizationName, title);
|
||||
return {
|
||||
course: {
|
||||
id: course.id,
|
||||
slug: course.slug,
|
||||
title: course.title,
|
||||
subtitle: course.subtitle,
|
||||
description: course.description,
|
||||
organizationName: course.organizationName,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async createBaseCourseForInstructor(instructorId: string, organizationName: string, title: string) {
|
||||
const courseTitle = title.trim();
|
||||
if (!courseTitle) throw new BadRequestException('강좌명을 입력해 주세요.');
|
||||
|
||||
const existingCourse = await this.prisma.course.findFirst({
|
||||
where: { teacherId: instructorId, isActive: true },
|
||||
select: { id: true },
|
||||
});
|
||||
if (existingCourse) throw new ConflictException('이미 생성된 강좌가 있습니다.');
|
||||
|
||||
const template = await this.prisma.course.findFirst({
|
||||
where: { slug: { in: BASE_COURSE_SLUGS }, isActive: true },
|
||||
include: {
|
||||
weeks: {
|
||||
orderBy: { week: 'asc' },
|
||||
include: {
|
||||
topics: { orderBy: { sortOrder: 'asc' } },
|
||||
practices: { orderBy: { sortOrder: 'asc' } },
|
||||
references: { orderBy: [{ sortOrder: 'asc' }, { createdAt: 'asc' }] },
|
||||
modules: { orderBy: { sortOrder: 'asc' } },
|
||||
},
|
||||
},
|
||||
environmentProfiles: { orderBy: [{ week: 'asc' }, { platformKey: 'asc' }] },
|
||||
maps: { orderBy: { key: 'asc' } },
|
||||
weekMaps: { orderBy: [{ week: 'asc' }, { sortOrder: 'asc' }] },
|
||||
},
|
||||
});
|
||||
if (!template) {
|
||||
this.logger.error('Base NAV2 course was not found; skipping instructor course initialization.');
|
||||
throw new NotFoundException('베이스 강좌를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
const courseSlug = `nav2-autonomy-${this.slug(instructorId).slice(0, 12)}`;
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
const courseAlreadyCreated = await tx.course.findFirst({
|
||||
where: { teacherId: instructorId, isActive: true },
|
||||
select: { id: true },
|
||||
});
|
||||
if (courseAlreadyCreated) throw new ConflictException('이미 생성된 강좌가 있습니다.');
|
||||
|
||||
const course = await tx.course.create({
|
||||
data: {
|
||||
slug: courseSlug,
|
||||
title: courseTitle,
|
||||
subtitle: template.subtitle,
|
||||
description: template.description,
|
||||
organizationName,
|
||||
teacherId: instructorId,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
const moduleIds = new Map<string, string>();
|
||||
for (const sourceWeek of template.weeks) {
|
||||
const week = await tx.courseWeek.create({
|
||||
data: {
|
||||
courseId: course.id,
|
||||
week: sourceWeek.week,
|
||||
title: sourceWeek.title,
|
||||
tags: sourceWeek.tags,
|
||||
progress: sourceWeek.progress,
|
||||
state: sourceWeek.state,
|
||||
duration: sourceWeek.duration,
|
||||
github: sourceWeek.github,
|
||||
overview: sourceWeek.overview,
|
||||
deliverable: sourceWeek.deliverable,
|
||||
requiresSourceSubmission: sourceWeek.requiresSourceSubmission,
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
tx.courseWeekTopic.createMany({ data: sourceWeek.topics.map((topic) => ({ weekId: week.id, title: topic.title, detail: topic.detail, sortOrder: topic.sortOrder })) }),
|
||||
tx.courseWeekPractice.createMany({ data: sourceWeek.practices.map((practice) => ({ weekId: week.id, title: practice.title, description: practice.description, sortOrder: practice.sortOrder })) }),
|
||||
tx.courseWeekReference.createMany({ data: sourceWeek.references.map((reference) => ({ weekId: week.id, kind: reference.kind, title: reference.title, originalFileName: reference.originalFileName, storedFileName: reference.storedFileName, contentType: reference.contentType, fileSizeBytes: reference.fileSizeBytes, sortOrder: reference.sortOrder })) }),
|
||||
]);
|
||||
|
||||
for (const sourceModule of sourceWeek.modules) {
|
||||
const module = await tx.courseWeekModule.create({
|
||||
data: {
|
||||
weekId: week.id,
|
||||
sortOrder: sourceModule.sortOrder,
|
||||
category: sourceModule.category,
|
||||
name: sourceModule.name,
|
||||
folderName: sourceModule.folderName,
|
||||
templateRepositoryUrl: sourceModule.templateRepositoryUrl,
|
||||
sourceType: sourceModule.sourceType,
|
||||
prebuiltArtifacts: copyJson(sourceModule.prebuiltArtifacts),
|
||||
defaultParameters: copyJson(sourceModule.defaultParameters),
|
||||
dockerImage: sourceModule.dockerImage,
|
||||
},
|
||||
});
|
||||
moduleIds.set(sourceModule.id, module.id);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
...template.environmentProfiles.map((profile) => tx.courseEnvironmentProfile.create({
|
||||
data: {
|
||||
courseId: course.id,
|
||||
week: profile.week,
|
||||
platformKey: profile.platformKey,
|
||||
profileType: profile.profileType,
|
||||
name: profile.name,
|
||||
config: remapProfileModuleIds(profile.config, moduleIds),
|
||||
enabled: profile.enabled,
|
||||
},
|
||||
})),
|
||||
...template.maps.map((map) => tx.courseMap.create({
|
||||
data: {
|
||||
courseId: course.id,
|
||||
key: map.key,
|
||||
name: map.name,
|
||||
description: map.description,
|
||||
platformKey: map.platformKey,
|
||||
worldFile: map.worldFile,
|
||||
viewerModel: map.viewerModel,
|
||||
layout: copyJson(map.layout),
|
||||
},
|
||||
})),
|
||||
...template.weekMaps.map((weekMap) => tx.courseWeekMap.create({
|
||||
data: { courseId: course.id, week: weekMap.week, mapKey: weekMap.mapKey, sortOrder: weekMap.sortOrder },
|
||||
})),
|
||||
]);
|
||||
|
||||
return course;
|
||||
});
|
||||
}
|
||||
|
||||
private toUserSyncData(user: AuthResponseUser) {
|
||||
return {
|
||||
authUserId: user.id,
|
||||
|
||||
Reference in New Issue
Block a user