Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 76a87fc20d | |||
| 978b782606 |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "lms",
|
"name": "lms",
|
||||||
"version": "1.1.26",
|
"version": "1.1.27",
|
||||||
"description": "Autonomy Studio LMS Electron app",
|
"description": "Autonomy Studio LMS Electron app",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": {
|
"author": {
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { strict as assert } from 'node:assert'
|
||||||
|
import { test } from 'node:test'
|
||||||
|
import { bridgeArtifact, jetsonBridgeImage, localBridgeImage } from './bridgeArtifacts'
|
||||||
|
|
||||||
|
test('PC and Jetson bridges have separate pinned downloads', () => {
|
||||||
|
const pc = bridgeArtifact(localBridgeImage)!
|
||||||
|
const jetson = bridgeArtifact(jetsonBridgeImage)!
|
||||||
|
assert.equal(pc.architecture, 'amd64')
|
||||||
|
assert.equal(jetson.architecture, 'arm64')
|
||||||
|
assert.notEqual(pc.url, jetson.url)
|
||||||
|
assert.notEqual(pc.fileName, jetson.fileName)
|
||||||
|
for (const artifact of [pc, jetson]) {
|
||||||
|
assert.match(artifact.checksumSha256, /^[a-f0-9]{64}$/)
|
||||||
|
assert.equal(new URL(artifact.url).protocol, 'https:')
|
||||||
|
assert.ok(artifact.url.includes(artifact.checksumSha256.slice(0, 12)))
|
||||||
|
assert.equal(artifact.required, true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('custom images do not resolve to an unrelated built-in artifact', () => {
|
||||||
|
assert.equal(bridgeArtifact('custom-bridge:test'), undefined)
|
||||||
|
})
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
const root = 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/runtime-images/slam-ros-bridge'
|
||||||
|
|
||||||
|
export const localBridgeImage = 'slam-ros-bridge:20260813-3d-map-v2'
|
||||||
|
export const jetsonBridgeImage = 'slam-ros-bridge:jetson_orin'
|
||||||
|
|
||||||
|
const artifacts = [
|
||||||
|
{
|
||||||
|
image: localBridgeImage,
|
||||||
|
architecture: 'amd64',
|
||||||
|
url: `${root}/amd64/20260915-489a3e8b370c/slam-ros-bridge-amd64.tar.zst`,
|
||||||
|
fileName: 'slam-ros-bridge-amd64-489a3e8b370c.tar.zst',
|
||||||
|
checksumSha256: '489a3e8b370c19903043a39e92935f2e32e4b4423c28210f130df1acab1a9889',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: jetsonBridgeImage,
|
||||||
|
architecture: 'arm64',
|
||||||
|
url: `${root}/arm64/20260819-0659d4cf5578/slam-ros-bridge-jetson-orin.tar.zst`,
|
||||||
|
fileName: 'slam-ros-bridge-jetson-orin-0659d4cf5578.tar.zst',
|
||||||
|
checksumSha256: '0659d4cf5578c43bc9c25e4a4fc5d226ccd5b7bec43becfdff83b5390717114e',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export function bridgeArtifact(image: string) {
|
||||||
|
return artifacts.find((artifact) => artifact.image === image)
|
||||||
|
}
|
||||||
+53
-23
@@ -1,10 +1,12 @@
|
|||||||
import { app, BrowserWindow, dialog, Menu, globalShortcut, ipcMain, screen, shell } from 'electron'
|
import { app, BrowserWindow, dialog, Menu, globalShortcut, ipcMain, screen, shell } from 'electron'
|
||||||
import { checkDockerStatus } from './dockerStatus'
|
import { checkDockerStatus } from './dockerStatus'
|
||||||
|
import { visualizationProgress, reportVisualizationProgress } from './visualizationProgress'
|
||||||
|
import { bridgeArtifact, jetsonBridgeImage, localBridgeImage } from './bridgeArtifacts'
|
||||||
import type { WebContents } from 'electron'
|
import type { WebContents } from 'electron'
|
||||||
import { spawn } from 'child_process'
|
import { spawn } from 'child_process'
|
||||||
import * as pty from 'node-pty'
|
import * as pty from 'node-pty'
|
||||||
import type { IPty } from 'node-pty'
|
import type { IPty } from 'node-pty'
|
||||||
import { appendFileSync, createWriteStream, existsSync, mkdirSync } from 'fs'
|
import { appendFileSync, createReadStream, createWriteStream, existsSync, mkdirSync } from 'fs'
|
||||||
import { cp, mkdir, readdir, readFile, rename, rm, stat, writeFile } from 'fs/promises'
|
import { cp, mkdir, readdir, readFile, rename, rm, stat, writeFile } from 'fs/promises'
|
||||||
import { homedir } from 'os'
|
import { homedir } from 'os'
|
||||||
import { basename, dirname, join, relative, resolve } from 'path'
|
import { basename, dirname, join, relative, resolve } from 'path'
|
||||||
@@ -12,6 +14,7 @@ import { is } from '@electron-toolkit/utils'
|
|||||||
import { createHash } from 'crypto'
|
import { createHash } from 'crypto'
|
||||||
import http from 'http'
|
import http from 'http'
|
||||||
import https from 'https'
|
import https from 'https'
|
||||||
|
import { pipeline } from 'node:stream/promises'
|
||||||
|
|
||||||
let mainWindow: BrowserWindow | null = null
|
let mainWindow: BrowserWindow | null = null
|
||||||
let splashWindow: BrowserWindow | null = null
|
let splashWindow: BrowserWindow | null = null
|
||||||
@@ -35,7 +38,7 @@ const autowareRosBridgeImage = 'autoware-ros-bridge'
|
|||||||
// Keep the bundled bridge image revisioned. Reusing `latest` leaves an old
|
// Keep the bundled bridge image revisioned. Reusing `latest` leaves an old
|
||||||
// Docker image in place after an LMS update, even when bridge dependencies or
|
// Docker image in place after an LMS update, even when bridge dependencies or
|
||||||
// protocol code changed.
|
// protocol code changed.
|
||||||
const slamRosBridgeImage = 'slam-ros-bridge:20260813-3d-map-v2'
|
const slamRosBridgeImage = localBridgeImage
|
||||||
const slamSimulationRuntimeImage = 'future-nav2-gazebo:humble'
|
const slamSimulationRuntimeImage = 'future-nav2-gazebo:humble'
|
||||||
const slamSimulationRuntimeContainerName = 'nav2-slam'
|
const slamSimulationRuntimeContainerName = 'nav2-slam'
|
||||||
const rosBridgePort = 8765
|
const rosBridgePort = 8765
|
||||||
@@ -658,7 +661,7 @@ async function ensureSlamRosBridgeImage(): Promise<void> {
|
|||||||
const exists = await runCommand('docker', ['image', 'inspect', slamRosBridgeImage], undefined, undefined, 10_000)
|
const exists = await runCommand('docker', ['image', 'inspect', slamRosBridgeImage], undefined, undefined, 10_000)
|
||||||
.then(() => true)
|
.then(() => true)
|
||||||
.catch(() => false)
|
.catch(() => false)
|
||||||
if (!exists) await ensureBridgeImage(slamRosBridgeImage, 'resources/slam-ros-bridge')
|
if (!exists) await ensureBridgeImage(slamRosBridgeImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function exportWorkspaceMapPointCloud(workspaceRoot: string, databaseFileName: string): Promise<Buffer> {
|
async function exportWorkspaceMapPointCloud(workspaceRoot: string, databaseFileName: string): Promise<Buffer> {
|
||||||
@@ -1906,14 +1909,19 @@ async function downloadArtifact(url: string, targetPath: string, redirects = 0):
|
|||||||
}
|
}
|
||||||
|
|
||||||
const file = createWriteStream(targetPath)
|
const file = createWriteStream(targetPath)
|
||||||
response.pipe(file)
|
void pipeline(response, file).then(resolvePromise, rejectPromise)
|
||||||
file.on('finish', () => file.close(() => resolvePromise()))
|
|
||||||
file.on('error', rejectPromise)
|
|
||||||
})
|
})
|
||||||
|
request.setTimeout(60000, () => request.destroy(new Error('artifact download timed out')))
|
||||||
request.on('error', rejectPromise)
|
request.on('error', rejectPromise)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function artifactSha256(filePath: string): Promise<string> {
|
||||||
|
const hash = createHash('sha256')
|
||||||
|
for await (const chunk of createReadStream(filePath)) hash.update(chunk)
|
||||||
|
return hash.digest('hex')
|
||||||
|
}
|
||||||
|
|
||||||
async function ensureArtifactFile(url?: string | null, fileName?: string | null, checksumSha256?: string | null): Promise<string | null> {
|
async function ensureArtifactFile(url?: string | null, fileName?: string | null, checksumSha256?: string | null): Promise<string | null> {
|
||||||
const artifactUrl = url?.trim()
|
const artifactUrl = url?.trim()
|
||||||
if (!artifactUrl) return null
|
if (!artifactUrl) return null
|
||||||
@@ -1924,14 +1932,18 @@ async function ensureArtifactFile(url?: string | null, fileName?: string | null,
|
|||||||
// Do not reuse a same-named local file, or a source update can run with an
|
// Do not reuse a same-named local file, or a source update can run with an
|
||||||
// older install tree even though R2 already contains the current artifact.
|
// older install tree even though R2 already contains the current artifact.
|
||||||
await rm(targetPath, { force: true })
|
await rm(targetPath, { force: true })
|
||||||
|
reportVisualizationProgress('다운로드 중...')
|
||||||
await downloadArtifact(artifactUrl, targetPath)
|
await downloadArtifact(artifactUrl, targetPath)
|
||||||
if (expected) {
|
if (expected) {
|
||||||
let actual = createHash('sha256').update(await readFile(targetPath)).digest('hex').toLowerCase()
|
reportVisualizationProgress('다운로드 파일 확인 중...')
|
||||||
|
let actual = await artifactSha256(targetPath)
|
||||||
if (actual !== expected) {
|
if (actual !== expected) {
|
||||||
// Artifact URLs are stable, so an existing filename can be an older cached release.
|
// Artifact URLs are stable, so an existing filename can be an older cached release.
|
||||||
await rm(targetPath, { force: true })
|
await rm(targetPath, { force: true })
|
||||||
|
reportVisualizationProgress('다운로드 중...')
|
||||||
await downloadArtifact(artifactUrl, targetPath)
|
await downloadArtifact(artifactUrl, targetPath)
|
||||||
actual = createHash('sha256').update(await readFile(targetPath)).digest('hex').toLowerCase()
|
reportVisualizationProgress('다운로드 파일 확인 중...')
|
||||||
|
actual = await artifactSha256(targetPath)
|
||||||
if (actual !== expected) {
|
if (actual !== expected) {
|
||||||
await rm(targetPath, { force: true }).catch(() => undefined)
|
await rm(targetPath, { force: true }).catch(() => undefined)
|
||||||
throw new Error(`artifact checksum mismatch: ${safeArtifactFileName(artifactUrl, fileName)}`)
|
throw new Error(`artifact checksum mismatch: ${safeArtifactFileName(artifactUrl, fileName)}`)
|
||||||
@@ -2000,12 +2012,14 @@ async function prepareRuntimeImages(images?: RuntimeImageArtifact[] | null): Pro
|
|||||||
if (image.required) throw new Error(`runtime image artifact url is missing: ${imageName}`)
|
if (image.required) throw new Error(`runtime image artifact url is missing: ${imageName}`)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
reportVisualizationProgress('이미지 불러오는 중...')
|
||||||
if (filePath.toLowerCase().endsWith('.tar.zst') || filePath.toLowerCase().endsWith('.tzst')) {
|
if (filePath.toLowerCase().endsWith('.tar.zst') || filePath.toLowerCase().endsWith('.tzst')) {
|
||||||
const zstdPath = getBundledZstdPath() ?? 'zstd'
|
const zstdPath = getBundledZstdPath() ?? 'zstd'
|
||||||
await runCommand('bash', ['-lc', '"$1" -dc "$2" | docker load', 'autonomy-studio-runtime-image', zstdPath, filePath], undefined, undefined, 900000)
|
await runCommand('bash', ['-o', 'pipefail', '-lc', '"$1" -dc "$2" | docker load', 'autonomy-studio-runtime-image', zstdPath, filePath], undefined, undefined, 900000)
|
||||||
} else {
|
} else {
|
||||||
await runCommand('docker', ['load', '-i', filePath], undefined, undefined, 900000)
|
await runCommand('docker', ['load', '-i', filePath], undefined, undefined, 900000)
|
||||||
}
|
}
|
||||||
|
reportVisualizationProgress('시각화 시작 중...')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2378,11 +2392,16 @@ async function startRemoteVisualizationBridge(
|
|||||||
))
|
))
|
||||||
?.image
|
?.image
|
||||||
?.trim()
|
?.trim()
|
||||||
if (stack !== 'autoware' && image === slamRosBridgeImage && remoteJetsonBridgeImage) {
|
if (stack !== 'autoware' && image === slamRosBridgeImage) {
|
||||||
image = remoteJetsonBridgeImage
|
image = remoteJetsonBridgeImage || jetsonBridgeImage
|
||||||
}
|
}
|
||||||
const imageArtifact = environmentProfile?.runtimeImages?.find((artifact) => artifact?.image?.trim() === image)
|
const imageArtifact = bridgeArtifact(image)
|
||||||
if (imageArtifact?.url?.trim()) {
|
?? environmentProfile?.runtimeImages?.find((artifact) => artifact?.image?.trim() === image)
|
||||||
|
const remoteImageExists = imageArtifact?.url?.trim()
|
||||||
|
? (await runSshOutput(target, `docker image inspect ${quoteShellSingle(image)} >/dev/null 2>&1 && printf present || true`, 10000)).trim() === 'present'
|
||||||
|
: false
|
||||||
|
if (imageArtifact?.url?.trim() && !remoteImageExists) {
|
||||||
|
reportVisualizationProgress('다운로드 및 이미지 준비 중...')
|
||||||
await runSsh(target, [
|
await runSsh(target, [
|
||||||
'set -e',
|
'set -e',
|
||||||
`IMAGE=${quoteShellSingle(image)}`,
|
`IMAGE=${quoteShellSingle(image)}`,
|
||||||
@@ -2396,13 +2415,18 @@ async function startRemoteVisualizationBridge(
|
|||||||
'if [ -f "$ARCHIVE" ] && [ -n "$EXPECTED_SHA256" ]; then',
|
'if [ -f "$ARCHIVE" ] && [ -n "$EXPECTED_SHA256" ]; then',
|
||||||
' [ "$(sha256sum "$ARCHIVE" | awk \'{print $1}\')" = "$EXPECTED_SHA256" ] || rm -f "$ARCHIVE"',
|
' [ "$(sha256sum "$ARCHIVE" | awk \'{print $1}\')" = "$EXPECTED_SHA256" ] || rm -f "$ARCHIVE"',
|
||||||
'fi',
|
'fi',
|
||||||
'if [ ! -f "$ARCHIVE" ]; then curl -fL "$URL" -o "$ARCHIVE"; fi',
|
'if [ ! -f "$ARCHIVE" ]; then',
|
||||||
|
' PART=$(mktemp "$ARCHIVE.XXXXXX")',
|
||||||
|
' trap \'rm -f "$PART"\' EXIT',
|
||||||
|
' curl -fL --connect-timeout 15 --max-time 750 "$URL" -o "$PART"',
|
||||||
|
' mv "$PART" "$ARCHIVE"',
|
||||||
|
'fi',
|
||||||
'if [ -n "$EXPECTED_SHA256" ] && [ "$(sha256sum "$ARCHIVE" | awk \'{print $1}\')" != "$EXPECTED_SHA256" ]; then',
|
'if [ -n "$EXPECTED_SHA256" ] && [ "$(sha256sum "$ARCHIVE" | awk \'{print $1}\')" != "$EXPECTED_SHA256" ]; then',
|
||||||
' echo "시각화 이미지 체크섬이 일치하지 않습니다: $FILE_NAME" >&2',
|
' echo "시각화 이미지 체크섬이 일치하지 않습니다: $FILE_NAME" >&2',
|
||||||
' exit 1',
|
' exit 1',
|
||||||
'fi',
|
'fi',
|
||||||
'case "$ARCHIVE" in',
|
'case "$ARCHIVE" in',
|
||||||
' *.tar.zst|*.tzst) zstd -dc "$ARCHIVE" | docker load ;;',
|
' *.tar.zst|*.tzst) bash -o pipefail -c \'zstd -dc "$1" | docker load\' _ "$ARCHIVE" ;;',
|
||||||
' *) docker load -i "$ARCHIVE" ;;',
|
' *) docker load -i "$ARCHIVE" ;;',
|
||||||
'esac',
|
'esac',
|
||||||
'docker image inspect "$IMAGE" >/dev/null 2>&1 || { echo "시각화 이미지 로드에 실패했습니다: $IMAGE" >&2; exit 1; }',
|
'docker image inspect "$IMAGE" >/dev/null 2>&1 || { echo "시각화 이미지 로드에 실패했습니다: $IMAGE" >&2; exit 1; }',
|
||||||
@@ -2410,6 +2434,8 @@ async function startRemoteVisualizationBridge(
|
|||||||
}
|
}
|
||||||
const command = bridgeContainerCommand(stack)
|
const command = bridgeContainerCommand(stack)
|
||||||
|
|
||||||
|
reportVisualizationProgress('시각화 시작 중...')
|
||||||
|
|
||||||
await runSsh(target, [
|
await runSsh(target, [
|
||||||
'set -e',
|
'set -e',
|
||||||
`ROOT=${quoteShellSingle(root)}`,
|
`ROOT=${quoteShellSingle(root)}`,
|
||||||
@@ -3513,11 +3539,15 @@ async function resolveDockerResourceContextPath(contextPath = 'resources/slam-ro
|
|||||||
return targetPath
|
return targetPath
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureBridgeImage(image: string, buildContext?: string | null): Promise<void> {
|
async function ensureBridgeImage(image: string): Promise<void> {
|
||||||
if (!buildContext) return
|
const artifact = bridgeArtifact(image)
|
||||||
const contextPath = await resolveDockerResourceContextPath(buildContext)
|
if (!artifact) throw new Error(`브리지 이미지의 다운로드 설정이 없습니다: ${image}`)
|
||||||
if (!contextPath) throw new Error(`브릿지 Docker context를 찾을 수 없습니다: ${buildContext}`)
|
const architecture = process.arch === 'x64' ? 'amd64' : process.arch
|
||||||
await runCommand('docker', ['build', '-t', image, contextPath], undefined, undefined, 120000)
|
if (architecture !== artifact.architecture) {
|
||||||
|
throw new Error(`브리지 이미지 아키텍처가 PC와 다릅니다: ${artifact.architecture} / ${architecture}`)
|
||||||
|
}
|
||||||
|
await prepareRuntimeImages([artifact])
|
||||||
|
await runCommand('docker', ['image', 'inspect', image], undefined, undefined, 10000)
|
||||||
}
|
}
|
||||||
|
|
||||||
function expandDockerVolume(volume: string): string {
|
function expandDockerVolume(volume: string): string {
|
||||||
@@ -3823,7 +3853,7 @@ async function startVisualizationBridge(
|
|||||||
.then(() => true)
|
.then(() => true)
|
||||||
.catch(() => false)
|
.catch(() => false)
|
||||||
if (!bridgeImageExists && stack !== 'autoware' && stack !== 'slam') {
|
if (!bridgeImageExists && stack !== 'autoware' && stack !== 'slam') {
|
||||||
await ensureBridgeImage(bridgeImage, visualizationBridgeResourceContext(stack, bridge))
|
await ensureBridgeImage(bridgeImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
const args = [
|
const args = [
|
||||||
@@ -6327,7 +6357,7 @@ function registerLocalWorkspaceHandlers(): void {
|
|||||||
ipcMain.handle('remote-workspace:modules:restart', (_event, target: RemoteWorkspaceTarget, moduleName: string, folderName?: string | null, rosParams?: string | null, gpuEnabled?: boolean | null, rosDomainId?: string | null, runtimeImages?: RuntimeImageArtifact[] | null, rosNetwork?: RosNetworkConfig | null) => restartRemoteWorkspaceModule(target, moduleName, folderName, rosParams, gpuEnabled, rosDomainId, runtimeImages, rosNetwork))
|
ipcMain.handle('remote-workspace:modules:restart', (_event, target: RemoteWorkspaceTarget, moduleName: string, folderName?: string | null, rosParams?: string | null, gpuEnabled?: boolean | null, rosDomainId?: string | null, runtimeImages?: RuntimeImageArtifact[] | null, rosNetwork?: RosNetworkConfig | null) => restartRemoteWorkspaceModule(target, moduleName, folderName, rosParams, gpuEnabled, rosDomainId, runtimeImages, rosNetwork))
|
||||||
ipcMain.handle('remote-workspace:modules:logs', (_event, target: RemoteWorkspaceTarget, moduleName: string, folderName?: string | null) => readRemoteWorkspaceModuleLogs(target, moduleName, folderName))
|
ipcMain.handle('remote-workspace:modules:logs', (_event, target: RemoteWorkspaceTarget, moduleName: string, folderName?: string | null) => readRemoteWorkspaceModuleLogs(target, moduleName, folderName))
|
||||||
ipcMain.handle('remote-workspace:visualization:status', (_event, target: RemoteWorkspaceTarget) => readRemoteVisualizationBridgeStatus(target))
|
ipcMain.handle('remote-workspace:visualization:status', (_event, target: RemoteWorkspaceTarget) => readRemoteVisualizationBridgeStatus(target))
|
||||||
ipcMain.handle('remote-workspace:visualization:start', (_event, target: RemoteWorkspaceTarget, stack?: string | null, rosDomainId?: string | null, environmentProfile?: EnvironmentProfileConfig | null, rosNetwork?: RosNetworkConfig | null) => startRemoteVisualizationBridge(target, stack, rosDomainId, environmentProfile, rosNetwork))
|
ipcMain.handle('remote-workspace:visualization:start', (_event, target: RemoteWorkspaceTarget, stack?: string | null, rosDomainId?: string | null, environmentProfile?: EnvironmentProfileConfig | null, rosNetwork?: RosNetworkConfig | null, requestId?: string) => visualizationProgress.run((message) => { if (!_event.sender.isDestroyed()) _event.sender.send('visualization:progress', requestId, message) }, () => startRemoteVisualizationBridge(target, stack, rosDomainId, environmentProfile, rosNetwork)))
|
||||||
ipcMain.handle('remote-workspace:visualization:stop', (_event, target: RemoteWorkspaceTarget) => stopRemoteVisualizationBridge(target))
|
ipcMain.handle('remote-workspace:visualization:stop', (_event, target: RemoteWorkspaceTarget) => stopRemoteVisualizationBridge(target))
|
||||||
ipcMain.handle('remote-workspace:git:status', (_event, target: RemoteWorkspaceTarget, moduleName: string, folderName?: string | null, selectedFilePath?: string | null) => readRemoteWorkspaceGitStatus(target, moduleName, folderName, selectedFilePath))
|
ipcMain.handle('remote-workspace:git:status', (_event, target: RemoteWorkspaceTarget, moduleName: string, folderName?: string | null, selectedFilePath?: string | null) => readRemoteWorkspaceGitStatus(target, moduleName, folderName, selectedFilePath))
|
||||||
ipcMain.handle('remote-workspace:git:submit', (_event, target: RemoteWorkspaceTarget, moduleName: string, folderName?: string | null, message?: string | null, authorName?: string | null, authorEmail?: string | null, submissionId?: string | null, repositoryUrl?: string | null) => submitRemoteWorkspaceGitChanges(target, moduleName, folderName, message, authorName, authorEmail, submissionId, repositoryUrl))
|
ipcMain.handle('remote-workspace:git:submit', (_event, target: RemoteWorkspaceTarget, moduleName: string, folderName?: string | null, message?: string | null, authorName?: string | null, authorEmail?: string | null, submissionId?: string | null, repositoryUrl?: string | null) => submitRemoteWorkspaceGitChanges(target, moduleName, folderName, message, authorName, authorEmail, submissionId, repositoryUrl))
|
||||||
@@ -6370,7 +6400,7 @@ function registerLocalWorkspaceHandlers(): void {
|
|||||||
return { ok: true }
|
return { ok: true }
|
||||||
})
|
})
|
||||||
ipcMain.handle('local-workspace:visualization:status', () => readVisualizationBridgeStatus())
|
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, savedMapDatabasePath?: string | null, savedMapWorldLayoutPath?: string | null) => startVisualizationBridge(stack, workspaceRoot, rosDomainId, environmentProfile, rosNetwork, savedMapDatabasePath, savedMapWorldLayoutPath))
|
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, savedMapWorldLayoutPath?: string | null, requestId?: string) => visualizationProgress.run((message) => { if (!_event.sender.isDestroyed()) _event.sender.send('visualization:progress', requestId, message) }, () => startVisualizationBridge(stack, workspaceRoot, rosDomainId, environmentProfile, rosNetwork, savedMapDatabasePath, savedMapWorldLayoutPath)))
|
||||||
ipcMain.handle('local-workspace:visualization:stop', (_event, environmentProfile?: EnvironmentProfileConfig | null) => stopVisualizationBridge(environmentProfile))
|
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: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))
|
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))
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
||||||
|
|
||||||
|
export const visualizationProgress = new AsyncLocalStorage<(message: string) => void>()
|
||||||
|
|
||||||
|
export function reportVisualizationProgress(message: string): void {
|
||||||
|
visualizationProgress.getStore()?.(message)
|
||||||
|
}
|
||||||
+16
-2
@@ -2,6 +2,20 @@ import { contextBridge, ipcRenderer } from 'electron'
|
|||||||
import { electronAPI } from '@electron-toolkit/preload'
|
import { electronAPI } from '@electron-toolkit/preload'
|
||||||
import { readFileSync, rmSync } from 'fs'
|
import { readFileSync, rmSync } from 'fs'
|
||||||
|
|
||||||
|
let visualizationRequestId = 0
|
||||||
|
async function invokeVisualizationStart(channel: string, args: unknown[], onProgress?: (message: string) => void) {
|
||||||
|
const requestId = `${Date.now()}:${++visualizationRequestId}`
|
||||||
|
const listener = (_event: Electron.IpcRendererEvent, id: string, message: string) => {
|
||||||
|
if (id === requestId) onProgress?.(message)
|
||||||
|
}
|
||||||
|
ipcRenderer.on('visualization:progress', listener)
|
||||||
|
try {
|
||||||
|
return await ipcRenderer.invoke(channel, ...args, requestId)
|
||||||
|
} finally {
|
||||||
|
ipcRenderer.removeListener('visualization:progress', listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type LauncherAuthPayload = {
|
type LauncherAuthPayload = {
|
||||||
appUrl?: string | null
|
appUrl?: string | null
|
||||||
handoffToken?: string | null
|
handoffToken?: string | null
|
||||||
@@ -232,7 +246,7 @@ contextBridge.exposeInMainWorld('localWorkspace', {
|
|||||||
},
|
},
|
||||||
visualization: {
|
visualization: {
|
||||||
status: () => ipcRenderer.invoke('local-workspace:visualization:status'),
|
status: () => ipcRenderer.invoke('local-workspace:visualization:status'),
|
||||||
start: (stack?: 'autoware' | 'nav2' | 'slam' | 'slam3d', workspaceRoot?: string, rosDomainId?: string, environmentProfile?: unknown, rosNetwork?: RosNetworkConfig | null, savedMapDatabasePath?: string | null, savedMapWorldLayoutPath?: string | null) => ipcRenderer.invoke('local-workspace:visualization:start', stack, workspaceRoot, rosDomainId, environmentProfile, rosNetwork, savedMapDatabasePath, savedMapWorldLayoutPath),
|
start: (stack?: 'autoware' | 'nav2' | 'slam' | 'slam3d', workspaceRoot?: string, rosDomainId?: string, environmentProfile?: unknown, rosNetwork?: RosNetworkConfig | null, savedMapDatabasePath?: string | null, savedMapWorldLayoutPath?: string | null, onProgress?: (message: string) => void) => invokeVisualizationStart('local-workspace:visualization:start', [stack, workspaceRoot, rosDomainId, environmentProfile, rosNetwork, savedMapDatabasePath, savedMapWorldLayoutPath], onProgress),
|
||||||
stop: (environmentProfile?: unknown) => ipcRenderer.invoke('local-workspace:visualization:stop', environmentProfile)
|
stop: (environmentProfile?: unknown) => ipcRenderer.invoke('local-workspace:visualization:stop', environmentProfile)
|
||||||
},
|
},
|
||||||
git: {
|
git: {
|
||||||
@@ -303,7 +317,7 @@ contextBridge.exposeInMainWorld('remoteWorkspace', {
|
|||||||
},
|
},
|
||||||
visualization: {
|
visualization: {
|
||||||
status: (target: RemoteWorkspaceTarget) => ipcRenderer.invoke('remote-workspace:visualization:status', target),
|
status: (target: RemoteWorkspaceTarget) => ipcRenderer.invoke('remote-workspace:visualization:status', target),
|
||||||
start: (target: RemoteWorkspaceTarget, stack?: 'autoware' | 'nav2' | 'slam' | 'slam3d', rosDomainId?: string, environmentProfile?: unknown, rosNetwork?: RosNetworkConfig | null) => ipcRenderer.invoke('remote-workspace:visualization:start', target, stack, rosDomainId, environmentProfile, rosNetwork),
|
start: (target: RemoteWorkspaceTarget, stack?: 'autoware' | 'nav2' | 'slam' | 'slam3d', rosDomainId?: string, environmentProfile?: unknown, rosNetwork?: RosNetworkConfig | null, onProgress?: (message: string) => void) => invokeVisualizationStart('remote-workspace:visualization:start', [target, stack, rosDomainId, environmentProfile, rosNetwork], onProgress),
|
||||||
stop: (target: RemoteWorkspaceTarget) => ipcRenderer.invoke('remote-workspace:visualization:stop', target)
|
stop: (target: RemoteWorkspaceTarget) => ipcRenderer.invoke('remote-workspace:visualization:stop', target)
|
||||||
},
|
},
|
||||||
git: {
|
git: {
|
||||||
|
|||||||
@@ -406,6 +406,7 @@ type WorkspaceVisualizationHost = {
|
|||||||
available: boolean
|
available: boolean
|
||||||
running: boolean
|
running: boolean
|
||||||
busy: boolean
|
busy: boolean
|
||||||
|
statusMessage: string
|
||||||
viewerUrl: string
|
viewerUrl: string
|
||||||
onStart: () => Promise<void>
|
onStart: () => Promise<void>
|
||||||
onTerminate: () => Promise<void>
|
onTerminate: () => Promise<void>
|
||||||
@@ -1052,6 +1053,7 @@ function LocalPcWorkspaceMockup({
|
|||||||
setVisualizationStack,
|
setVisualizationStack,
|
||||||
visualizationRunning,
|
visualizationRunning,
|
||||||
visualizationBusy,
|
visualizationBusy,
|
||||||
|
visualizationStatusMessage,
|
||||||
visualizationViewerUrl,
|
visualizationViewerUrl,
|
||||||
nav2VisualizationMode,
|
nav2VisualizationMode,
|
||||||
setNav2VisualizationMode,
|
setNav2VisualizationMode,
|
||||||
@@ -2258,6 +2260,7 @@ function LocalPcWorkspaceMockup({
|
|||||||
available: visualizationStacks.length > 0,
|
available: visualizationStacks.length > 0,
|
||||||
running: visualizationRunning,
|
running: visualizationRunning,
|
||||||
busy: visualizationBusy,
|
busy: visualizationBusy,
|
||||||
|
statusMessage: visualizationStatusMessage,
|
||||||
viewerUrl: visualizationViewerUrl,
|
viewerUrl: visualizationViewerUrl,
|
||||||
onStart: () => startVisualizationBridge(),
|
onStart: () => startVisualizationBridge(),
|
||||||
onTerminate: async () => {
|
onTerminate: async () => {
|
||||||
@@ -2285,6 +2288,7 @@ function LocalPcWorkspaceMockup({
|
|||||||
textDim,
|
textDim,
|
||||||
textStrong,
|
textStrong,
|
||||||
visualizationBusy,
|
visualizationBusy,
|
||||||
|
visualizationStatusMessage,
|
||||||
visualizationRunning,
|
visualizationRunning,
|
||||||
visualizationStacks.length,
|
visualizationStacks.length,
|
||||||
visualizationViewerUrl,
|
visualizationViewerUrl,
|
||||||
@@ -6249,6 +6253,7 @@ export default function App(): React.JSX.Element {
|
|||||||
available={workspaceVisualizationHost.available}
|
available={workspaceVisualizationHost.available}
|
||||||
running={workspaceVisualizationHost.running}
|
running={workspaceVisualizationHost.running}
|
||||||
busy={workspaceVisualizationHost.busy}
|
busy={workspaceVisualizationHost.busy}
|
||||||
|
statusMessage={workspaceVisualizationHost.statusMessage}
|
||||||
viewerUrl={workspaceVisualizationHost.viewerUrl}
|
viewerUrl={workspaceVisualizationHost.viewerUrl}
|
||||||
onDragStart={(event) => workspaceVisualizationHostRef.current?.onDragStart(event)}
|
onDragStart={(event) => workspaceVisualizationHostRef.current?.onDragStart(event)}
|
||||||
onToggleLarge={() => workspaceVisualizationHostRef.current?.onToggleLarge()}
|
onToggleLarge={() => workspaceVisualizationHostRef.current?.onToggleLarge()}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { createPortal } from 'react-dom'
|
|||||||
|
|
||||||
type DockerResult = Awaited<ReturnType<Window['localWorkspace']['docker']['status']>>
|
type DockerResult = Awaited<ReturnType<Window['localWorkspace']['docker']['status']>>
|
||||||
|
|
||||||
|
const DOCKER_NOTICE_SEEN_KEY = 'lms:docker-setup-notice-seen'
|
||||||
|
|
||||||
const messages = {
|
const messages = {
|
||||||
missing: 'Docker 명령을 찾을 수 없습니다.',
|
missing: 'Docker 명령을 찾을 수 없습니다.',
|
||||||
permission: '현재 사용자의 Docker 접근 권한을 확인해주세요.',
|
permission: '현재 사용자의 Docker 접근 권한을 확인해주세요.',
|
||||||
@@ -14,7 +16,13 @@ const messages = {
|
|||||||
|
|
||||||
export default function DockerSetupNotice(): React.JSX.Element | null {
|
export default function DockerSetupNotice(): React.JSX.Element | null {
|
||||||
const [result, setResult] = useState<DockerResult | null>(null)
|
const [result, setResult] = useState<DockerResult | null>(null)
|
||||||
const [dismissed, setDismissed] = useState(false)
|
const [dismissed, setDismissed] = useState(() => {
|
||||||
|
try {
|
||||||
|
return localStorage.getItem(DOCKER_NOTICE_SEEN_KEY) === 'true'
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
const [busy, setBusy] = useState(false)
|
const [busy, setBusy] = useState(false)
|
||||||
const [copied, setCopied] = useState(false)
|
const [copied, setCopied] = useState(false)
|
||||||
const [actionError, setActionError] = useState('')
|
const [actionError, setActionError] = useState('')
|
||||||
@@ -22,6 +30,7 @@ export default function DockerSetupNotice(): React.JSX.Element | null {
|
|||||||
const retryPending = useRef(false)
|
const retryPending = useRef(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (dismissed) return
|
||||||
const docker = window.localWorkspace?.docker
|
const docker = window.localWorkspace?.docker
|
||||||
if (!docker) return
|
if (!docker) return
|
||||||
let active = true
|
let active = true
|
||||||
@@ -31,11 +40,17 @@ export default function DockerSetupNotice(): React.JSX.Element | null {
|
|||||||
if (active) setResult({ status: 'error', platform: '' })
|
if (active) setResult({ status: 'error', platform: '' })
|
||||||
})
|
})
|
||||||
return () => { active = false }
|
return () => { active = false }
|
||||||
}, [])
|
}, [dismissed])
|
||||||
|
|
||||||
const visible = Boolean(result && result.status !== 'ready' && !dismissed)
|
const visible = Boolean(result && result.status !== 'ready' && !dismissed)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible) dialogRef.current?.showModal()
|
if (!visible || !dialogRef.current) return
|
||||||
|
dialogRef.current.showModal()
|
||||||
|
try {
|
||||||
|
localStorage.setItem(DOCKER_NOTICE_SEEN_KEY, 'true')
|
||||||
|
} catch {
|
||||||
|
// Keep the notice dismissible when browser storage is unavailable.
|
||||||
|
}
|
||||||
}, [visible])
|
}, [visible])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
Vendored
+2
-2
@@ -243,7 +243,7 @@ interface Window {
|
|||||||
stack: 'autoware' | 'nav2' | 'slam' | 'slam3d' | null
|
stack: 'autoware' | 'nav2' | 'slam' | 'slam3d' | null
|
||||||
message?: string
|
message?: string
|
||||||
}>
|
}>
|
||||||
start: (stack?: 'autoware' | 'nav2' | 'slam' | 'slam3d', workspaceRoot?: string, rosDomainId?: string, environmentProfile?: unknown, rosNetwork?: RosNetworkConfig | null, savedMapDatabasePath?: string | null, savedMapWorldLayoutPath?: string | null) => Promise<{
|
start: (stack?: 'autoware' | 'nav2' | 'slam' | 'slam3d', workspaceRoot?: string, rosDomainId?: string, environmentProfile?: unknown, rosNetwork?: RosNetworkConfig | null, savedMapDatabasePath?: string | null, savedMapWorldLayoutPath?: string | null, onProgress?: (message: string) => void) => Promise<{
|
||||||
ok: boolean
|
ok: boolean
|
||||||
running: boolean
|
running: boolean
|
||||||
stack: 'autoware' | 'nav2' | 'slam' | 'slam3d' | null
|
stack: 'autoware' | 'nav2' | 'slam' | 'slam3d' | null
|
||||||
@@ -354,7 +354,7 @@ interface Window {
|
|||||||
}
|
}
|
||||||
visualization: {
|
visualization: {
|
||||||
status: (target: RemoteWorkspaceTarget) => Promise<{ ok: boolean; running: boolean; stack: 'autoware' | 'nav2' | 'slam' | 'slam3d' | null; message?: string }>
|
status: (target: RemoteWorkspaceTarget) => Promise<{ ok: boolean; running: boolean; stack: 'autoware' | 'nav2' | 'slam' | 'slam3d' | null; message?: string }>
|
||||||
start: (target: RemoteWorkspaceTarget, stack?: 'autoware' | 'nav2' | 'slam' | 'slam3d', rosDomainId?: string, environmentProfile?: unknown, rosNetwork?: RosNetworkConfig | null) => Promise<{ ok: boolean; running: boolean; stack: 'autoware' | 'nav2' | 'slam' | 'slam3d' | null; message?: string }>
|
start: (target: RemoteWorkspaceTarget, stack?: 'autoware' | 'nav2' | 'slam' | 'slam3d', rosDomainId?: string, environmentProfile?: unknown, rosNetwork?: RosNetworkConfig | null, onProgress?: (message: string) => void) => Promise<{ ok: boolean; running: boolean; stack: 'autoware' | 'nav2' | 'slam' | 'slam3d' | null; message?: string }>
|
||||||
stop: (target: RemoteWorkspaceTarget) => Promise<{ ok: boolean; running: boolean; stack: 'autoware' | 'nav2' | 'slam' | 'slam3d' | null; message?: string }>
|
stop: (target: RemoteWorkspaceTarget) => Promise<{ ok: boolean; running: boolean; stack: 'autoware' | 'nav2' | 'slam' | 'slam3d' | null; message?: string }>
|
||||||
}
|
}
|
||||||
git: {
|
git: {
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ function WorkspaceSettingsView({
|
|||||||
'relative ml-3 rounded-md border px-3 py-2 text-[11px] font-semibold leading-relaxed',
|
'relative ml-3 rounded-md border px-3 py-2 text-[11px] font-semibold leading-relaxed',
|
||||||
'before:absolute before:-top-1 before:left-4 before:size-2 before:rotate-45 before:border-l before:border-t',
|
'before:absolute before:-top-1 before:left-4 before:size-2 before:rotate-45 before:border-l before:border-t',
|
||||||
dark
|
dark
|
||||||
? 'border-amber-300/20 bg-amber-300/[0.08] text-amber-100/80 before:border-amber-300/20 before:bg-[#252525]'
|
? 'border-amber-300/20 bg-[#363328] text-amber-100/80 before:border-amber-300/20 before:bg-[#363328]'
|
||||||
: 'border-amber-300/50 bg-amber-50 text-amber-900/75 before:border-amber-300/50 before:bg-amber-50'
|
: 'border-amber-300/50 bg-amber-50 text-amber-900/75 before:border-amber-300/50 before:bg-amber-50'
|
||||||
].join(' ')}>
|
].join(' ')}>
|
||||||
NVIDIA Container Toolkit과 Docker GPU 런타임이 준비된 PC에서만 실행됩니다. 준비되지 않은 경우 GPU 가속을 끄세요.
|
NVIDIA Container Toolkit과 Docker GPU 런타임이 준비된 PC에서만 실행됩니다. 준비되지 않은 경우 GPU 가속을 끄세요.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CirclePlay, Minus, PanelLeft, Power, Square, X } from 'lucide-react'
|
import { CirclePlay, LoaderCircle, Minus, PanelLeft, Power, Square, X } from 'lucide-react'
|
||||||
import type React from 'react'
|
import type React from 'react'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@ type WorkspaceVisualizationPanelProps = {
|
|||||||
available: boolean
|
available: boolean
|
||||||
running: boolean
|
running: boolean
|
||||||
busy: boolean
|
busy: boolean
|
||||||
|
statusMessage: string
|
||||||
viewerUrl: string
|
viewerUrl: string
|
||||||
onDragStart: (event: React.PointerEvent<HTMLDivElement>) => void
|
onDragStart: (event: React.PointerEvent<HTMLDivElement>) => void
|
||||||
onToggleLarge: () => void
|
onToggleLarge: () => void
|
||||||
@@ -38,6 +39,7 @@ export default function WorkspaceVisualizationPanel({
|
|||||||
available,
|
available,
|
||||||
running,
|
running,
|
||||||
busy,
|
busy,
|
||||||
|
statusMessage,
|
||||||
viewerUrl,
|
viewerUrl,
|
||||||
onDragStart,
|
onDragStart,
|
||||||
onToggleLarge,
|
onToggleLarge,
|
||||||
@@ -154,10 +156,10 @@ export default function WorkspaceVisualizationPanel({
|
|||||||
type="button"
|
type="button"
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
onClick={onStart}
|
onClick={onStart}
|
||||||
className="absolute left-1/2 top-1/2 inline-flex h-28 w-40 -translate-x-1/2 -translate-y-1/2 cursor-pointer flex-col items-center justify-center gap-2 text-[15px] font-bold text-white/56 transition hover:text-white disabled:cursor-default disabled:opacity-45 disabled:hover:text-white/56"
|
className="absolute left-1/2 top-1/2 inline-flex min-h-28 w-64 max-w-full -translate-x-1/2 -translate-y-1/2 cursor-pointer flex-col items-center justify-center gap-2 px-3 text-[15px] font-bold text-white/80 transition hover:text-white disabled:cursor-default"
|
||||||
>
|
>
|
||||||
<CirclePlay size={40} />
|
{busy ? <LoaderCircle size={40} className="animate-spin" aria-hidden="true" /> : <CirclePlay size={40} />}
|
||||||
{busy ? '처리 중' : '실행하기'}
|
<span role="status" aria-live="polite">{busy ? statusMessage : '실행하기'}</span>
|
||||||
</button>
|
</button>
|
||||||
<div className="absolute inset-x-0 top-[70%] px-5 text-center">
|
<div className="absolute inset-x-0 top-[70%] px-5 text-center">
|
||||||
<p className="text-[13px] font-bold text-white/72">지도와 차량 상태가 표시됩니다.</p>
|
<p className="text-[13px] font-bold text-white/72">지도와 차량 상태가 표시됩니다.</p>
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ type UseWorkspaceVisualizationResult = {
|
|||||||
setVisualizationStack: (stack: VisualizationStack) => void
|
setVisualizationStack: (stack: VisualizationStack) => void
|
||||||
visualizationRunning: boolean
|
visualizationRunning: boolean
|
||||||
visualizationBusy: boolean
|
visualizationBusy: boolean
|
||||||
|
visualizationStatusMessage: string
|
||||||
visualizationViewerUrl: string
|
visualizationViewerUrl: string
|
||||||
nav2VisualizationMode: Nav2VisualizationMode
|
nav2VisualizationMode: Nav2VisualizationMode
|
||||||
setNav2VisualizationMode: (mode: Nav2VisualizationMode) => void
|
setNav2VisualizationMode: (mode: Nav2VisualizationMode) => void
|
||||||
@@ -117,7 +118,7 @@ export function useWorkspaceVisualization({
|
|||||||
const [visualizationStack, setVisualizationStack] = useState<VisualizationStack>(fallbackStack)
|
const [visualizationStack, setVisualizationStack] = useState<VisualizationStack>(fallbackStack)
|
||||||
const [nav2VisualizationMode, setNav2VisualizationMode] = useState<Nav2VisualizationMode>('slam')
|
const [nav2VisualizationMode, setNav2VisualizationMode] = useState<Nav2VisualizationMode>('slam')
|
||||||
const [visualizationRunning, setVisualizationRunning] = useState(false)
|
const [visualizationRunning, setVisualizationRunning] = useState(false)
|
||||||
const [, setVisualizationStatusMessage] = useState('대기')
|
const [visualizationStatusMessage, setVisualizationStatusMessage] = useState('대기')
|
||||||
const [visualizationBusy, setVisualizationBusy] = useState(false)
|
const [visualizationBusy, setVisualizationBusy] = useState(false)
|
||||||
|
|
||||||
const runtimeProfiles = useMemo(() => {
|
const runtimeProfiles = useMemo(() => {
|
||||||
@@ -253,7 +254,7 @@ export function useWorkspaceVisualization({
|
|||||||
const bridgeSavedMapDatabasePath = preparedMap?.savedMapDatabasePath ?? savedMapDatabasePath
|
const bridgeSavedMapDatabasePath = preparedMap?.savedMapDatabasePath ?? savedMapDatabasePath
|
||||||
const status = bridgeRunsOnPlatform
|
const status = bridgeRunsOnPlatform
|
||||||
? bridgeTarget?.host
|
? bridgeTarget?.host
|
||||||
? await window.remoteWorkspace.visualization.start(bridgeTarget, visualizationStack, rosDomainId, activeEnvironmentProfile.config, rosNetwork)
|
? await window.remoteWorkspace.visualization.start(bridgeTarget, visualizationStack, rosDomainId, activeEnvironmentProfile.config, rosNetwork, setVisualizationStatusMessage)
|
||||||
: { ok: false, running: false, stack: null, message: '원격 플랫폼 SSH 주소를 확인하지 못했습니다.' }
|
: { ok: false, running: false, stack: null, message: '원격 플랫폼 SSH 주소를 확인하지 못했습니다.' }
|
||||||
: await window.localWorkspace.visualization.start(
|
: await window.localWorkspace.visualization.start(
|
||||||
visualizationRuntimeStack,
|
visualizationRuntimeStack,
|
||||||
@@ -263,6 +264,7 @@ export function useWorkspaceVisualization({
|
|||||||
rosNetwork,
|
rosNetwork,
|
||||||
bridgeSavedMapDatabasePath,
|
bridgeSavedMapDatabasePath,
|
||||||
preparedMap?.savedMapWorldLayoutPath,
|
preparedMap?.savedMapWorldLayoutPath,
|
||||||
|
setVisualizationStatusMessage,
|
||||||
)
|
)
|
||||||
if (!status.ok) throw new Error(status.message ?? '시작 실패')
|
if (!status.ok) throw new Error(status.message ?? '시작 실패')
|
||||||
setVisualizationRunning(Boolean(status.running))
|
setVisualizationRunning(Boolean(status.running))
|
||||||
@@ -310,6 +312,7 @@ export function useWorkspaceVisualization({
|
|||||||
setVisualizationStack: selectVisualizationStack,
|
setVisualizationStack: selectVisualizationStack,
|
||||||
visualizationRunning,
|
visualizationRunning,
|
||||||
visualizationBusy,
|
visualizationBusy,
|
||||||
|
visualizationStatusMessage,
|
||||||
visualizationViewerUrl,
|
visualizationViewerUrl,
|
||||||
nav2VisualizationMode,
|
nav2VisualizationMode,
|
||||||
setNav2VisualizationMode,
|
setNav2VisualizationMode,
|
||||||
|
|||||||
Reference in New Issue
Block a user