220 lines
9.2 KiB
TypeScript
220 lines
9.2 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react'
|
|
import type { EnvironmentProfile } from '../platforms/types'
|
|
import type { RuntimeImageArtifact } from './types'
|
|
import type { VisualizationStack } from './WorkspaceVisualizationPanel'
|
|
|
|
export type Nav2VisualizationMode = 'slam' | 'navigation'
|
|
|
|
const DEFAULT_NAV2_VISUALIZATION_PROFILE: EnvironmentProfile = {
|
|
id: 'default-nav2-visualization',
|
|
week: 0,
|
|
platformKey: 'futuredrive-local-pc',
|
|
profileType: 'NAV2',
|
|
name: 'NAV2',
|
|
config: {
|
|
type: 'NAV2',
|
|
target: 'local',
|
|
viewer: { stack: 'nav2' },
|
|
bridge: {
|
|
name: 'nav2-ros-bridge',
|
|
port: 8765,
|
|
image: 'slam-ros-bridge:latest',
|
|
buildContext: 'resources/slam-ros-bridge',
|
|
env: { ROBOT_STACK: 'nav2' }
|
|
}
|
|
}
|
|
}
|
|
|
|
function resolveVisualizationStack(profileType: string): VisualizationStack | null {
|
|
const type = profileType.toLowerCase()
|
|
if (type === 'real_slam') return 'slam'
|
|
if (type === 'real_navigation') return 'nav2'
|
|
return type === 'autoware' || type === 'nav2' || type === 'slam' ? type : null
|
|
}
|
|
|
|
function resolveNav2Mode(profile: EnvironmentProfile | null): Nav2VisualizationMode {
|
|
return profile?.profileType.toLowerCase() === 'real_navigation' ? 'navigation' : 'slam'
|
|
}
|
|
|
|
function isRealPlatformProfile(profile: EnvironmentProfile | null): boolean {
|
|
const type = profile?.profileType.toLowerCase()
|
|
return type === 'real_slam' || type === 'real_navigation'
|
|
}
|
|
|
|
function selectRuntimeProfiles(profiles: EnvironmentProfile[], remoteWorkspace: boolean): EnvironmentProfile[] {
|
|
const matchingProfiles = profiles.filter((profile) => (
|
|
remoteWorkspace ? isRealPlatformProfile(profile) : !isRealPlatformProfile(profile)
|
|
))
|
|
return matchingProfiles.length ? matchingProfiles : profiles
|
|
}
|
|
|
|
type NoticeTone = 'info' | 'warning' | 'error'
|
|
|
|
type UseWorkspaceVisualizationOptions = {
|
|
profiles: EnvironmentProfile[]
|
|
clonePathPreview: string
|
|
rosDomainId: string
|
|
remoteWorkspace: boolean
|
|
mapKey?: string | null
|
|
worldLayoutVersion?: number
|
|
onTitlebarNotice: (title: string, tone?: NoticeTone, addToList?: boolean) => void
|
|
}
|
|
|
|
type UseWorkspaceVisualizationResult = {
|
|
visualizationStacks: VisualizationStack[]
|
|
visualizationStack: VisualizationStack
|
|
setVisualizationStack: (stack: VisualizationStack) => void
|
|
visualizationRunning: boolean
|
|
visualizationBusy: boolean
|
|
visualizationViewerUrl: string
|
|
nav2VisualizationMode: Nav2VisualizationMode
|
|
setNav2VisualizationMode: (mode: Nav2VisualizationMode) => void
|
|
activeEnvironmentProfile: EnvironmentProfile | null
|
|
activeRuntimeImages: RuntimeImageArtifact[]
|
|
refreshVisualizationStatus: () => Promise<void>
|
|
startVisualizationBridge: () => Promise<void>
|
|
stopVisualizationBridge: () => Promise<void>
|
|
}
|
|
|
|
function formatVisualizationStackLabel(stack: VisualizationStack | null | undefined): string {
|
|
if (stack === 'nav2') return 'Nav2'
|
|
if (stack === 'slam') return 'SLAM'
|
|
return 'Autoware'
|
|
}
|
|
|
|
export function useWorkspaceVisualization({
|
|
profiles,
|
|
clonePathPreview,
|
|
rosDomainId,
|
|
remoteWorkspace,
|
|
mapKey,
|
|
worldLayoutVersion,
|
|
onTitlebarNotice
|
|
}: UseWorkspaceVisualizationOptions): UseWorkspaceVisualizationResult {
|
|
const [visualizationStack, setVisualizationStack] = useState<VisualizationStack>('autoware')
|
|
const [nav2VisualizationMode, setNav2VisualizationMode] = useState<Nav2VisualizationMode>('slam')
|
|
const [visualizationRunning, setVisualizationRunning] = useState(false)
|
|
const [, setVisualizationStatusMessage] = useState('대기')
|
|
const [visualizationBusy, setVisualizationBusy] = useState(false)
|
|
|
|
const runtimeProfiles = useMemo(() => {
|
|
const candidates = selectRuntimeProfiles(profiles, remoteWorkspace)
|
|
const nav2Profiles = candidates.filter((profile) => resolveVisualizationStack(profile.profileType) !== 'autoware')
|
|
return nav2Profiles.length ? nav2Profiles : [DEFAULT_NAV2_VISUALIZATION_PROFILE]
|
|
}, [profiles, remoteWorkspace])
|
|
const visualizationStacks = useMemo(() => (
|
|
runtimeProfiles
|
|
.map((profile) => resolveVisualizationStack(profile.profileType))
|
|
.filter((stack): stack is VisualizationStack => stack !== null)
|
|
), [runtimeProfiles])
|
|
|
|
const activeEnvironmentProfile = runtimeProfiles.find((profile) => resolveVisualizationStack(profile.profileType) === visualizationStack) ?? runtimeProfiles[0] ?? null
|
|
const activeEnvironmentConfig = activeEnvironmentProfile?.config ?? {}
|
|
const activeRuntimeImages = Array.isArray(activeEnvironmentConfig.runtimeImages)
|
|
? activeEnvironmentConfig.runtimeImages.filter((image): image is RuntimeImageArtifact => (
|
|
typeof image === 'object' && image !== null && typeof (image as RuntimeImageArtifact).image === 'string'
|
|
))
|
|
: []
|
|
const activeEnvironmentAgent = typeof activeEnvironmentConfig.agent === 'object' && activeEnvironmentConfig.agent !== null
|
|
? activeEnvironmentConfig.agent as Record<string, unknown>
|
|
: null
|
|
const activeEnvironmentAgentUrl = typeof activeEnvironmentAgent?.url === 'string'
|
|
? activeEnvironmentAgent.url.replace(/\/+$/, '')
|
|
: ''
|
|
const visualizationRemote = activeEnvironmentConfig.target === 'remote-jetson' && activeEnvironmentAgentUrl
|
|
const visualizationRemoteBridgeHttpUrl = (() => {
|
|
if (!visualizationRemote) return ''
|
|
try {
|
|
const url = new URL(activeEnvironmentAgentUrl)
|
|
url.port = '8765'
|
|
return url.toString().replace(/\/+$/, '')
|
|
} catch {
|
|
return ''
|
|
}
|
|
})()
|
|
const visualizationBridgeHttpUrl = visualizationRemoteBridgeHttpUrl || 'http://localhost:8765'
|
|
const visualizationBridgeUrl = visualizationRemote
|
|
? visualizationBridgeHttpUrl.replace(/^http:/, 'ws:').replace(/^https:/, 'wss:')
|
|
: 'ws://localhost:8765'
|
|
const visualizationBackendUrl = visualizationBridgeHttpUrl
|
|
const visualizationViewerUrl = visualizationStack === 'slam' || visualizationStack === 'nav2'
|
|
? `/slam-viewer/index.html?directBridge=1&mode=${nav2VisualizationMode}&runtime=${isRealPlatformProfile(activeEnvironmentProfile) ? 'real' : 'simulation'}&bridgeWs=${encodeURIComponent(`${visualizationBridgeUrl}/ws/state`)}&bridgeHttp=${encodeURIComponent(visualizationBridgeHttpUrl)}&mapKey=${encodeURIComponent(mapKey ?? 'nav2-turtlebot3-world')}&layoutVersion=${worldLayoutVersion ?? 0}`
|
|
: `/autoware-viewer/index.html?source=ros&stack=${visualizationStack}&bridge=${encodeURIComponent(visualizationBridgeUrl)}&backend=${encodeURIComponent(visualizationBackendUrl)}`
|
|
|
|
useEffect(() => {
|
|
const stack = runtimeProfiles[0] ? resolveVisualizationStack(runtimeProfiles[0].profileType) : null
|
|
if (stack) setVisualizationStack(stack)
|
|
setNav2VisualizationMode(resolveNav2Mode(runtimeProfiles[0] ?? null))
|
|
}, [runtimeProfiles])
|
|
|
|
const refreshVisualizationStatus = async (): Promise<void> => {
|
|
const status = await window.localWorkspace.visualization.status().catch((error) => ({
|
|
ok: false,
|
|
running: false,
|
|
stack: null,
|
|
message: error instanceof Error ? error.message : String(error)
|
|
}))
|
|
setVisualizationRunning(Boolean(status.running))
|
|
if (status.stack) setVisualizationStack(status.stack)
|
|
setVisualizationStatusMessage(status.running
|
|
? formatVisualizationStackLabel(status.stack)
|
|
: status.message
|
|
? status.message
|
|
: '대기')
|
|
}
|
|
|
|
const startVisualizationBridge = async (): Promise<void> => {
|
|
if (visualizationBusy) return
|
|
setVisualizationBusy(true)
|
|
setVisualizationStatusMessage('시작 중')
|
|
try {
|
|
const status = await window.localWorkspace.visualization.start(visualizationStack, clonePathPreview, rosDomainId, activeEnvironmentProfile.config)
|
|
if (!status.ok) throw new Error(status.message ?? '시작 실패')
|
|
setVisualizationRunning(Boolean(status.running))
|
|
if (status.stack) setVisualizationStack(status.stack)
|
|
setVisualizationStatusMessage(formatVisualizationStackLabel(status.stack))
|
|
onTitlebarNotice('화면이 시작되었습니다', 'info')
|
|
} catch (error) {
|
|
setVisualizationRunning(false)
|
|
setVisualizationStatusMessage(error instanceof Error ? error.message : '시작 실패')
|
|
onTitlebarNotice('화면 시작에 실패했습니다', 'error')
|
|
} finally {
|
|
setVisualizationBusy(false)
|
|
}
|
|
}
|
|
|
|
const stopVisualizationBridge = async (): Promise<void> => {
|
|
if (visualizationBusy) return
|
|
setVisualizationBusy(true)
|
|
setVisualizationStatusMessage('종료 중')
|
|
try {
|
|
const status = await window.localWorkspace.visualization.stop(activeEnvironmentProfile?.config)
|
|
if (!status.ok) throw new Error(status.message ?? '종료 실패')
|
|
setVisualizationRunning(false)
|
|
setVisualizationStatusMessage('대기')
|
|
onTitlebarNotice('화면이 종료되었습니다', 'info')
|
|
} catch (error) {
|
|
setVisualizationStatusMessage(error instanceof Error ? error.message : '종료 실패')
|
|
onTitlebarNotice('화면 종료에 실패했습니다', 'error')
|
|
} finally {
|
|
setVisualizationBusy(false)
|
|
}
|
|
}
|
|
|
|
return {
|
|
visualizationStacks,
|
|
visualizationStack,
|
|
setVisualizationStack,
|
|
visualizationRunning,
|
|
visualizationBusy,
|
|
visualizationViewerUrl,
|
|
nav2VisualizationMode,
|
|
setNav2VisualizationMode,
|
|
activeEnvironmentProfile,
|
|
activeRuntimeImages,
|
|
refreshVisualizationStatus,
|
|
startVisualizationBridge,
|
|
stopVisualizationBridge
|
|
}
|
|
}
|