feat: add LMS web client
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import type { EnvironmentProfile } from '../platforms/types'
|
||||
import type { RuntimeImageArtifact } from './types'
|
||||
import type { VisualizationStack } from './WorkspaceVisualizationPanel'
|
||||
|
||||
type NoticeTone = 'info' | 'warning' | 'error'
|
||||
|
||||
type UseWorkspaceVisualizationOptions = {
|
||||
profiles: EnvironmentProfile[]
|
||||
clonePathPreview: string
|
||||
rosDomainId: string
|
||||
onTitlebarNotice: (title: string, tone?: NoticeTone, addToList?: boolean) => void
|
||||
}
|
||||
|
||||
type UseWorkspaceVisualizationResult = {
|
||||
visualizationStacks: VisualizationStack[]
|
||||
visualizationStack: VisualizationStack
|
||||
setVisualizationStack: (stack: VisualizationStack) => void
|
||||
visualizationRunning: boolean
|
||||
visualizationBusy: boolean
|
||||
visualizationViewerUrl: string
|
||||
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,
|
||||
onTitlebarNotice
|
||||
}: UseWorkspaceVisualizationOptions): UseWorkspaceVisualizationResult {
|
||||
const [visualizationStack, setVisualizationStack] = useState<VisualizationStack>('autoware')
|
||||
const [visualizationRunning, setVisualizationRunning] = useState(false)
|
||||
const [, setVisualizationStatusMessage] = useState('대기')
|
||||
const [visualizationBusy, setVisualizationBusy] = useState(false)
|
||||
|
||||
const visualizationStacks = useMemo(() => (
|
||||
profiles
|
||||
.map((profile) => profile.profileType.toLowerCase())
|
||||
.filter((stack): stack is VisualizationStack => stack === 'autoware' || stack === 'nav2' || stack === 'slam')
|
||||
), [profiles])
|
||||
|
||||
const activeEnvironmentProfile = profiles.find((profile) => profile.profileType.toLowerCase() === visualizationStack) ?? profiles[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'
|
||||
? `/slam-viewer/index.html?directBridge=1&bridgeWs=${encodeURIComponent(`${visualizationBridgeUrl}/ws/state`)}&bridgeHttp=${encodeURIComponent(visualizationBridgeHttpUrl)}`
|
||||
: `/autoware-viewer/index.html?source=ros&stack=${visualizationStack}&bridge=${encodeURIComponent(visualizationBridgeUrl)}&backend=${encodeURIComponent(visualizationBackendUrl)}`
|
||||
|
||||
useEffect(() => {
|
||||
const profileType = profiles[0]?.profileType?.toLowerCase()
|
||||
if (profileType === 'slam' || profileType === 'nav2' || profileType === 'autoware') {
|
||||
setVisualizationStack(profileType)
|
||||
}
|
||||
}, [profiles.map((profile) => profile.id).join('|')])
|
||||
|
||||
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
|
||||
if (!activeEnvironmentProfile) {
|
||||
setVisualizationStatusMessage('지원 안함')
|
||||
onTitlebarNotice('해당 주차는 시각화를 지원하지 않습니다', 'warning')
|
||||
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,
|
||||
activeEnvironmentProfile,
|
||||
activeRuntimeImages,
|
||||
refreshVisualizationStatus,
|
||||
startVisualizationBridge,
|
||||
stopVisualizationBridge
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user