synchronize source..
This commit is contained in:
+105
-13
@@ -352,7 +352,11 @@ async function syncWorkspaceSavedMapArtifacts(
|
||||
}
|
||||
await Promise.all(artifacts.map(async (artifact) => {
|
||||
const normalized = validateSavedMapDownload(artifact)
|
||||
await downloadArtifact(normalized.url, resolve(mapsDirectory, normalized.fileName))
|
||||
const targetPath = resolve(mapsDirectory, normalized.fileName)
|
||||
// SLAM containers can leave an artifact owned by root. Removing it first
|
||||
// lets the download recreate the file with the desktop user's ownership.
|
||||
await rm(targetPath, { force: true })
|
||||
await downloadArtifact(normalized.url, targetPath)
|
||||
}))
|
||||
return { ok: true }
|
||||
}
|
||||
@@ -1335,6 +1339,45 @@ async function readRemoteSavedMapArtifacts(
|
||||
}))
|
||||
}
|
||||
|
||||
async function copyRemoteActiveRtabmapDatabaseToLocal(
|
||||
targetInput: RemoteWorkspaceTarget,
|
||||
localWorkspaceRoot: string | null | undefined,
|
||||
destinationPath: string,
|
||||
): Promise<{ databasePath: string }> {
|
||||
const target = normalizeRemoteTarget(targetInput)
|
||||
const destinationFileName = savedMapArtifactFileName('database', destinationPath)
|
||||
const sourcePath = `${target.workspaceRoot}/maps/rtabmap-active.db`
|
||||
const snapshotScript = [
|
||||
'import sqlite3, sys',
|
||||
'source, destination = sys.argv[1:3]',
|
||||
'source_db = sqlite3.connect(f"file:{source}?mode=ro", uri=True)',
|
||||
'destination_db = sqlite3.connect(destination)',
|
||||
'source_db.backup(destination_db)',
|
||||
'destination_db.close()',
|
||||
'source_db.close()',
|
||||
].join('; ')
|
||||
const remoteCommand = [
|
||||
'set -e',
|
||||
`SOURCE=${quoteShellSingle(sourcePath)}`,
|
||||
'test -f "$SOURCE" || { echo "3D SLAM DB를 찾을 수 없습니다: $SOURCE" >&2; exit 1; }',
|
||||
'SNAPSHOT=$(mktemp)',
|
||||
`python3 -c ${quoteShellSingle(snapshotScript)} "$SOURCE" "$SNAPSHOT"`,
|
||||
'base64 -w0 "$SNAPSHOT"',
|
||||
'rm -f "$SNAPSHOT"',
|
||||
].join('\n')
|
||||
const base64 = (await runSshOutput(target, remoteCommand, 120_000)).trim()
|
||||
if (!base64) throw new Error('3D SLAM DB 스냅샷을 읽지 못했습니다.')
|
||||
|
||||
await syncWorkspaceSavedMapArtifacts(localWorkspaceRoot, [])
|
||||
const root = expandUserPath(localWorkspaceRoot)
|
||||
if (!root) throw new Error('로컬 워크스페이스 경로가 설정되어 있지 않습니다.')
|
||||
const mapsDirectory = resolve(root, 'maps')
|
||||
const destination = resolve(mapsDirectory, destinationFileName)
|
||||
if (!destination.startsWith(`${mapsDirectory}/`)) throw new Error('저장된 3D 맵 경로가 올바르지 않습니다.')
|
||||
await writeFile(destination, Buffer.from(base64, 'base64'))
|
||||
return { databasePath: `/workspace/maps/${destinationFileName}` }
|
||||
}
|
||||
|
||||
async function syncRemoteSavedMapArtifacts(
|
||||
targetInput: RemoteWorkspaceTarget,
|
||||
artifacts: SavedMapArtifactDownload[],
|
||||
@@ -1347,7 +1390,13 @@ async function syncRemoteSavedMapArtifacts(
|
||||
`ROOT=${quoteShellSingle(target.workspaceRoot)}`,
|
||||
`MAPS_DIRECTORY=${quoteShellSingle(mapsDirectory)}`,
|
||||
'if [ ! -d "$MAPS_DIRECTORY" ] || [ ! -w "$MAPS_DIRECTORY" ]; then',
|
||||
' IMAGE="$(docker image inspect future-nav2:humble-jetson_orin --format {{.Id}} >/dev/null 2>&1 && printf future-nav2:humble-jetson_orin || docker image inspect future-nav2:humble --format {{.Id}} >/dev/null 2>&1 && printf future-nav2:humble || true)"',
|
||||
' if docker image inspect future-nav2:humble-jetson_orin >/dev/null 2>&1; then',
|
||||
' IMAGE="future-nav2:humble-jetson_orin"',
|
||||
' elif docker image inspect future-nav2:humble >/dev/null 2>&1; then',
|
||||
' IMAGE="future-nav2:humble"',
|
||||
' else',
|
||||
' IMAGE=""',
|
||||
' fi',
|
||||
' if [ -z "$IMAGE" ]; then echo "저장된 맵 디렉터리에 쓸 권한이 없고 권한 복구용 NAV2 이미지를 찾을 수 없습니다." >&2; exit 1; fi',
|
||||
' docker run --rm --user 0:0 -v "$ROOT:/workspace" --entrypoint /bin/sh "$IMAGE" -lc "mkdir -p /workspace/maps && chown -R $(id -u):$(id -g) /workspace/maps"',
|
||||
'fi',
|
||||
@@ -1460,7 +1509,7 @@ async function startRemoteVisualizationBridge(
|
||||
`-e ROBOT_STACK=${quoteShellSingle(stack === 'autoware' ? 'autoware' : 'nav2')}`,
|
||||
'-e ODOM_TOPIC=/odom -e SCAN_TOPIC=/scan -e MAP_TOPIC=/map -e CMD_VEL_TOPIC=/cmd_vel -e NAVIGATE_TO_POSE_ACTION=/navigate_to_pose',
|
||||
...(stack === 'slam3d'
|
||||
? ['-e POINT_CLOUD_TOPIC=/cloud_map -e FALLBACK_POINT_CLOUD_TOPIC=/lidar_filtered -e ICP_ODOM_TOPIC=/icp_odom -e POINT_CLOUD_LIMIT=50000 -e READ_ONLY=true -e RTABMAP_MAP_SAVE_ENABLED=true']
|
||||
? ['-e POINT_CLOUD_TOPIC=/cloud_map -e FALLBACK_POINT_CLOUD_TOPIC=/lidar_filtered -e ICP_ODOM_TOPIC=/icp_odom -e POINT_CLOUD_LIMIT=50000 -e RTABMAP_MAP_SAVE_ENABLED=true']
|
||||
: []),
|
||||
bridgeEnvArgs,
|
||||
'-e WORLD_LAYOUT_PATH=/workspace/world-layout.json -e SAVED_MAPS_PATH=/workspace/maps -e RTABMAP_DATABASE_PATH=/workspace/maps/rtabmap-active.db',
|
||||
@@ -2450,7 +2499,7 @@ function bridgeContainerCommand(stack: VisualizationStack): string {
|
||||
return [
|
||||
'source /opt/ros/humble/setup.bash',
|
||||
setupWorkspace,
|
||||
`python3 -c "import fastapi, uvicorn, websockets" || python3 -m pip install --no-cache-dir -r ${requirementPath}`,
|
||||
`(python3 -c "import fastapi, uvicorn, websockets" || python3 -m pip install --no-cache-dir -r ${requirementPath})`,
|
||||
`python3 ${bridgePath}`
|
||||
].filter(Boolean).join(' && ')
|
||||
}
|
||||
@@ -2739,6 +2788,7 @@ async function startVisualizationBridge(
|
||||
rosDomainId?: string | null,
|
||||
environmentProfile?: EnvironmentProfileConfig | null,
|
||||
rosNetwork?: RosNetworkConfig | null,
|
||||
savedMapDatabasePath?: string | null,
|
||||
): Promise<VisualizationBridgeStatus> {
|
||||
const stack = normalizeVisualizationStack(environmentProfile?.type?.toLowerCase() ?? stackInput)
|
||||
const resolvedProfile = withRuntimeProfileDefaults(environmentProfile, stack)
|
||||
@@ -2840,6 +2890,10 @@ async function startVisualizationBridge(
|
||||
'-e', 'RTABMAP_DATABASE_PATH=/workspace/maps/rtabmap-active.db',
|
||||
)
|
||||
if (stack === 'slam3d') args.push('-e', 'RTABMAP_MAP_SAVE_ENABLED=true')
|
||||
const savedMapFileName = savedMapDatabasePath ? safeSavedMapFileName(basename(savedMapDatabasePath)) : null
|
||||
if (savedMapFileName?.endsWith('.db') && existsSync(resolve(layoutWorkspaceRoot, 'maps', savedMapFileName))) {
|
||||
args.push('-e', `SAVED_MAP_DATABASE_PATH=/workspace/maps/${savedMapFileName}`)
|
||||
}
|
||||
}
|
||||
const fastDdsConfigPath = await prepareLocalRosNetworkConfig(rosNetwork, requestedRosDomainId)
|
||||
if (fastDdsConfigPath && existsSync(fastDdsConfigPath)) {
|
||||
@@ -3101,8 +3155,15 @@ async function installRemoteWorkspaceModule(
|
||||
' expected = (item.get("checksumSha256") or "").strip().lower()',
|
||||
' if expected:',
|
||||
' import hashlib',
|
||||
' with open(path, "rb") as fp: actual = hashlib.sha256(fp.read()).hexdigest().lower()',
|
||||
' if actual != expected: raise SystemExit(f"artifact checksum mismatch: {name}")',
|
||||
' def checksum(file_path):',
|
||||
' with open(file_path, "rb") as fp: return hashlib.sha256(fp.read()).hexdigest().lower()',
|
||||
' actual = checksum(path)',
|
||||
' if actual != expected:',
|
||||
' # Stable artifact URLs may have a newer object than the local cache.',
|
||||
' os.unlink(path)',
|
||||
' subprocess.run(["curl", "-fL", url, "-o", path], check=True)',
|
||||
' actual = checksum(path)',
|
||||
' if actual != expected: raise SystemExit(f"artifact checksum mismatch: {name}")',
|
||||
' entries = subprocess.check_output(["tar", *tar_args(path, "list"), path], text=True)',
|
||||
' for entry in entries.splitlines():',
|
||||
' entry = entry.strip()',
|
||||
@@ -3512,13 +3573,13 @@ function buildNav2DockerRunScript(): string {
|
||||
'esac',
|
||||
'fi',
|
||||
'',
|
||||
'if [[ "$MODULE_FOLDER" == *lms-nav2-gazebo-slam3d* ]]; then',
|
||||
'if [[ "$MODULE_FOLDER" == *lms-nav2-gazebo-slam3d* || "$MODULE_FOLDER" == *lms-nav2-gazebo-localization* ]]; then',
|
||||
' mkdir -p "$MODULE_ROOT/maps"',
|
||||
' has_database_path=false',
|
||||
' for arg in "$@"; do',
|
||||
' if [[ "$arg" == database_path:=* ]]; then has_database_path=true; break; fi',
|
||||
' done',
|
||||
' if [ "$has_database_path" = false ]; then',
|
||||
' if [[ "$MODULE_FOLDER" == *lms-nav2-gazebo-slam3d* && "$has_database_path" = false ]] || [[ "$MODULE_FOLDER" == *lms-nav2-gazebo-localization* && "$has_database_path" = false ]]; then',
|
||||
' set -- "database_path:=/modules/maps/rtabmap-active.db" "$@"',
|
||||
' fi',
|
||||
'fi',
|
||||
@@ -3621,11 +3682,19 @@ function buildNav2DockerBuildScript(): string {
|
||||
' set -e',
|
||||
' source /opt/ros/humble/setup.bash',
|
||||
' mapfile -t paths < <(colcon list --base-paths src --paths-only)',
|
||||
" rosdep install --from-paths \"${paths[@]}\" --ignore-src \\",
|
||||
' ROSDEP_SKIP_ARGS=()',
|
||||
' if ! apt-cache show ros-humble-gazebo-ros >/dev/null 2>&1; then',
|
||||
' ROSDEP_SKIP_ARGS=(--skip-keys gazebo_ros)',
|
||||
' fi',
|
||||
" rosdep install --from-paths \"${paths[@]}\" --ignore-src \"${ROSDEP_SKIP_ARGS[@]}\" \\",
|
||||
" --dependency-types buildtool --dependency-types build \\",
|
||||
' --dependency-types build_export --dependency-types exec -r -y',
|
||||
' BUILD_TARGETS=("$2")',
|
||||
' if [[ "$1" == *lms-nav2-gazebo-slam* && "$1" != *lms-nav2-gazebo-slam3d* ]]; then',
|
||||
' BUILD_TARGETS=(slam_pkg slam2d_pkg slam3d_pkg)',
|
||||
' fi',
|
||||
" colcon --log-base log build --base-paths src --build-base build --install-base install \\",
|
||||
' --executor sequential --cmake-args -DBUILD_TESTING=OFF --packages-up-to "$2"',
|
||||
' --executor sequential --cmake-args -DBUILD_TESTING=OFF --packages-up-to "${BUILD_TARGETS[@]}"',
|
||||
"' bash \"$MODULE_FOLDER\" \"$TARGET\"",
|
||||
''
|
||||
].join('\n')
|
||||
@@ -3785,6 +3854,22 @@ async function findRunningNav2SimulatorContainer(): Promise<string | null> {
|
||||
}
|
||||
|
||||
async function findNav2SimulatorRobotModel(containerName: string): Promise<string> {
|
||||
// Query Gazebo directly first. The ROS GetModelList service can be slow or
|
||||
// unavailable while DDS participants are still discovering each other, even
|
||||
// though the simulator itself is already able to accept a pose update.
|
||||
const knownModels = ['mkmini_gazebo', 'turtlebot3_burger_3d_lidar', 'turtlebot3_burger']
|
||||
for (const modelName of knownModels) {
|
||||
const pose = await runCommandOutput(
|
||||
'docker',
|
||||
['exec', containerName, 'gz', 'model', '-m', modelName, '-p'],
|
||||
undefined,
|
||||
undefined,
|
||||
5_000,
|
||||
).catch(() => '')
|
||||
const values = pose.trim().split(/\s+/).map(Number)
|
||||
if (values.length === 6 && values.every(Number.isFinite)) return modelName
|
||||
}
|
||||
|
||||
const output = await runCommandOutput(
|
||||
'docker',
|
||||
[
|
||||
@@ -3798,7 +3883,7 @@ async function findNav2SimulatorRobotModel(containerName: string): Promise<strin
|
||||
const modelList = output.match(/model_names=\[([^\]]*)\]/)?.[1] ?? ''
|
||||
const modelNames = Array.from(modelList.matchAll(/'([^']+)'/g), (match) => match[1])
|
||||
const robotModel = modelNames.find((modelName) => (
|
||||
/turtlebot|burger|robot/i.test(modelName) && !/world/i.test(modelName)
|
||||
/turtlebot|burger|mkmini|mk-mini|robot/i.test(modelName) && !/world/i.test(modelName)
|
||||
))
|
||||
if (!robotModel) throw new Error('Gazebo 로봇 모델을 찾을 수 없습니다.')
|
||||
return robotModel
|
||||
@@ -3815,6 +3900,10 @@ async function setNav2SimulatorPose(
|
||||
const containerName = await findRunningNav2SimulatorContainer()
|
||||
if (!containerName) throw new Error('실행 중인 NAV2 Simulator를 찾을 수 없습니다.')
|
||||
const robotModel = await findNav2SimulatorRobotModel(containerName)
|
||||
// MK-mini's model origin is higher than TurtleBot's. Teleporting it at the
|
||||
// TurtleBot ground height puts its chassis and wheels into the floor, which
|
||||
// makes Gazebo eject or tip the vehicle while resolving the collision.
|
||||
const spawnHeight = /^mkmini/i.test(robotModel) ? '0.16' : '0.01'
|
||||
|
||||
await runCommand(
|
||||
'docker',
|
||||
@@ -3823,7 +3912,7 @@ async function setNav2SimulatorPose(
|
||||
'gz', 'model', '-m', robotModel,
|
||||
'-x', String(x),
|
||||
'-y', String(y),
|
||||
'-z', '0.01',
|
||||
'-z', spawnHeight,
|
||||
'-R', '0',
|
||||
'-P', '0',
|
||||
'-Y', String(yaw),
|
||||
@@ -5141,6 +5230,9 @@ function registerLocalWorkspaceHandlers(): void {
|
||||
ipcMain.handle('remote-workspace:maps:read-artifacts', (_event, target: RemoteWorkspaceTarget, artifacts: SavedMapArtifactPath[]) => (
|
||||
readRemoteSavedMapArtifacts(target, artifacts)
|
||||
))
|
||||
ipcMain.handle('remote-workspace:maps:copy-active-rtabmap-database', (_event, target: RemoteWorkspaceTarget, localWorkspaceRoot: string | null | undefined, destinationPath: string) => (
|
||||
copyRemoteActiveRtabmapDatabaseToLocal(target, localWorkspaceRoot, destinationPath)
|
||||
))
|
||||
ipcMain.handle('remote-workspace:maps:sync', (_event, target: RemoteWorkspaceTarget, artifacts: SavedMapArtifactDownload[]) => (
|
||||
syncRemoteSavedMapArtifacts(target, artifacts)
|
||||
))
|
||||
@@ -5204,7 +5296,7 @@ function registerLocalWorkspaceHandlers(): void {
|
||||
return { ok: true }
|
||||
})
|
||||
ipcMain.handle('local-workspace:visualization:status', () => readVisualizationBridgeStatus())
|
||||
ipcMain.handle('local-workspace:visualization:start', (_event, stack?: string | null, workspaceRoot?: string | null, rosDomainId?: string | null, environmentProfile?: EnvironmentProfileConfig | null, rosNetwork?: RosNetworkConfig | null) => startVisualizationBridge(stack, workspaceRoot, rosDomainId, environmentProfile, rosNetwork))
|
||||
ipcMain.handle('local-workspace:visualization:start', (_event, stack?: string | null, workspaceRoot?: string | null, rosDomainId?: string | null, environmentProfile?: EnvironmentProfileConfig | null, rosNetwork?: RosNetworkConfig | null, savedMapDatabasePath?: string | null) => startVisualizationBridge(stack, workspaceRoot, rosDomainId, environmentProfile, rosNetwork, savedMapDatabasePath))
|
||||
ipcMain.handle('local-workspace:visualization:stop', (_event, environmentProfile?: EnvironmentProfileConfig | null) => stopVisualizationBridge(environmentProfile))
|
||||
ipcMain.handle('local-workspace:git:status', (_event, moduleName: string, workspaceRoot?: string | null, folderName?: string | null, selectedFilePath?: string | null) => readWorkspaceGitStatus(moduleName, workspaceRoot, folderName, selectedFilePath))
|
||||
ipcMain.handle('local-workspace:git:submit', (_event, moduleName: string, workspaceRoot?: string | null, folderName?: string | null, message?: string | null, authorName?: string | null, authorEmail?: string | null, submissionId?: string | null, repositoryUrl?: string | null) => submitWorkspaceGitChanges(moduleName, workspaceRoot, folderName, message, authorName, authorEmail, submissionId, repositoryUrl))
|
||||
|
||||
Reference in New Issue
Block a user