diff --git a/src/frontend/components/_organisms/workspace-activity-bar/default.tsx b/src/frontend/components/_organisms/workspace-activity-bar/default.tsx index c4779c750..276eda3a1 100644 --- a/src/frontend/components/_organisms/workspace-activity-bar/default.tsx +++ b/src/frontend/components/_organisms/workspace-activity-bar/default.tsx @@ -371,9 +371,10 @@ export const DefaultWorkspaceActivityBar = ({ zoom }: DefaultWorkspaceActivityBa if (response === 0) { const runtimeIpAddress = deviceDefinitions.configuration.runtimeIpAddress || null const runtimeJwtToken = useOpenPLCStore.getState().runtimeConnection.jwtToken || null + const latestProjectData = useOpenPLCStore.getState().project.data const compileResult = await compiler.compileProgram( { - projectData, + projectData: latestProjectData, boardTarget, projectPath, compileOnly: false, diff --git a/src/renderer/components/_organisms/workspace-activity-bar/default.tsx b/src/renderer/components/_organisms/workspace-activity-bar/default.tsx new file mode 100644 index 000000000..044b90225 --- /dev/null +++ b/src/renderer/components/_organisms/workspace-activity-bar/default.tsx @@ -0,0 +1,1028 @@ +import { StopIcon } from '@root/renderer/assets' +import { compileOnlySelectors } from '@root/renderer/hooks' +import { useOpenPLCStore } from '@root/renderer/store' +import type { RuntimeConnection } from '@root/renderer/store/slices/device/types' +import { + buildDebugVariableTreeMap, + buildFbInstanceMap, + buildVariableIndexMap, + connectAndActivateDebugger, + disconnectDebugger, +} from '@root/renderer/utils/debugger-session' +import type { PLCProjectData } from '@root/types/PLC/project/data' +import type { DebugTreeNode } from '@root/types/debugger' +import { BufferToStringArray, cn, isOpenPLCRuntimeTarget, isSimulatorTarget } from '@root/utils' +import { parseDebugFile } from '@root/utils/debug-parser' +import { preprocessPous } from '@root/utils/PLC/preprocess-pous' +import { parsePlcStatus } from '@root/utils/plc-status' +import { useEffect, useRef, useState } from 'react' + +import { + DebuggerButton, + DownloadButton, + PlayButton, + SearchButton, + ZoomButton, +} from '../../_molecules/workspace-activity-bar/default' +import { TooltipSidebarWrapperButton } from '../../_molecules/workspace-activity-bar/tooltip-button' + +const showDebuggerMessage = ( + type: 'info' | 'warning' | 'error' | 'question', + title: string, + message: string, + buttons: string[], +): Promise => { + return new Promise((resolve) => { + useOpenPLCStore.getState().modalActions.openModal('debugger-message', { + type, + title, + message, + buttons, + onResponse: (buttonIndex: number) => resolve(buttonIndex), + }) + }) +} + +const showDebuggerIpInput = (title: string, message: string, defaultValue: string): Promise => { + return new Promise((resolve) => { + useOpenPLCStore.getState().modalActions.openModal('debugger-ip-input', { + title, + message, + defaultValue, + onSubmit: (value: string) => resolve(value), + onCancel: () => resolve(null), + }) + }) +} + +type DefaultWorkspaceActivityBarProps = { + zoom?: { + onClick: () => void + } +} + +export const DefaultWorkspaceActivityBar = ({ zoom }: DefaultWorkspaceActivityBarProps) => { + const { + project: { data: projectData, meta: projectMeta }, + deviceDefinitions, + deviceAvailableOptions: { availableBoards }, + workspace: { editingState }, + consoleActions: { addLog }, + sharedWorkspaceActions: { saveProject }, + } = useOpenPLCStore() + + const [isCompiling, setIsCompiling] = useState(false) + const [isDebuggerProcessing, setIsDebuggerProcessing] = useState(false) + const [simulatorRunning, setSimulatorRunning] = useState(false) + const pendingSimulatorDebugRef = useRef(false) + + const disabledButtonClass = 'disabled cursor-not-allowed opacity-50 [&>*:first-child]:hover:bg-transparent' + + const compileOnly = compileOnlySelectors.useCompileOnly() + const connectionStatus = useOpenPLCStore((state) => state.runtimeConnection.connectionStatus) + const plcStatus = useOpenPLCStore((state): RuntimeConnection['plcStatus'] => state.runtimeConnection.plcStatus) + const jwtToken = useOpenPLCStore((state) => state.runtimeConnection.jwtToken) + const runtimeIpAddress = useOpenPLCStore((state) => state.deviceDefinitions.configuration.runtimeIpAddress) + const isDebuggerVisible = useOpenPLCStore((state) => state.workspace.isDebuggerVisible) + + const currentBoardInfo = availableBoards.get(deviceDefinitions.configuration.deviceBoard) + const isCurrentBoardSimulator = isSimulatorTarget(currentBoardInfo) + + const prepareProjectForCompile = (data: PLCProjectData) => { + return preprocessPous(data, isCurrentBoardSimulator, (level, message) => + addLog({ id: crypto.randomUUID(), level, message }), + ) + } + + // Sync simulatorRunning when the main process stops the simulator + // (e.g. on project open/create) so the UI reflects the actual state. + useEffect(() => { + const cleanup = (window.bridge.onSimulatorStopped as (cb: () => void) => () => void)(() => { + pendingSimulatorDebugRef.current = false + setSimulatorRunning(false) + // Also clean up debugger state if it was connected via simulator + const { workspace, workspaceActions } = useOpenPLCStore.getState() + if (workspace.isDebuggerVisible) { + void disconnectDebugger(workspaceActions) + } + }) + return cleanup + }, []) + + const handleRequest = () => { + const boardCore = availableBoards.get(deviceDefinitions.configuration.deviceBoard)?.core || null + + const { projectData: processedProjectData, validationFailed } = prepareProjectForCompile(projectData) + if (validationFailed) { + setIsCompiling(false) + return + } + + const runtimeIpAddress = deviceDefinitions.configuration.runtimeIpAddress || null + const runtimeJwtToken = useOpenPLCStore.getState().runtimeConnection.jwtToken || null + window.bridge.runCompileProgram( + [ + projectMeta.path, + deviceDefinitions.configuration.deviceBoard, + boardCore, + compileOnly, + processedProjectData, + runtimeIpAddress, + runtimeJwtToken, + ], + (data: { + logLevel?: 'info' | 'error' | 'warning' + message: string | Buffer + plcStatus?: string + closePort?: boolean + simulatorFirmwarePath?: string + }) => { + setIsCompiling(true) + + if (data.plcStatus) { + const status = parsePlcStatus(data.plcStatus) + if (status) { + useOpenPLCStore.getState().deviceActions.setPlcRuntimeStatus(status) + } + } + + if (typeof data.message === 'string') { + data.message + .trim() + .split('\n') + .forEach((line) => { + addLog({ + id: crypto.randomUUID(), + level: data.logLevel ?? 'info', + message: line, + }) + }) + } + if (data.message && typeof data.message !== 'string') { + BufferToStringArray(data.message).forEach((message) => { + addLog({ + id: crypto.randomUUID(), + level: data.logLevel ?? 'info', + message, + }) + }) + } + + // Load firmware into simulator when compilation finishes with a HEX path + if (data.simulatorFirmwarePath) { + ;(window.bridge.simulatorLoadFirmware as (p: string) => Promise<{ success: boolean; error?: string }>)( + data.simulatorFirmwarePath, + ) + .then(async (result) => { + if (result.success) { + setSimulatorRunning(true) + addLog({ id: crypto.randomUUID(), level: 'info', message: 'Simulator is running.' }) + + // Auto-connect debugger after build when triggered from the Start button + if (pendingSimulatorDebugRef.current) { + pendingSimulatorDebugRef.current = false + await connectDebuggerAfterBuild() + } + } else { + pendingSimulatorDebugRef.current = false + addLog({ + id: crypto.randomUUID(), + level: 'error', + message: `Failed to start simulator: ${result.error}`, + }) + } + }) + .catch((err: unknown) => { + pendingSimulatorDebugRef.current = false + addLog({ + id: crypto.randomUUID(), + level: 'error', + message: `Simulator error: ${err instanceof Error ? err.message : String(err)}`, + }) + }) + } + + if (data.closePort) { + setIsCompiling(false) + } + }, + ) + } + + const verifyAndCompile = async () => { + if (editingState === 'unsaved') { + const res = await saveProject({ data: projectData, meta: projectMeta }, deviceDefinitions) + if (!res.success) { + return + } + + handleRequest() + } else { + handleRequest() + } + } + + const handlePlcControl = async (): Promise => { + if (!runtimeIpAddress || !jwtToken || connectionStatus !== 'connected') { + return + } + + try { + if (plcStatus === 'RUNNING') { + const result = await window.bridge.runtimeStopPlc(runtimeIpAddress, jwtToken) + if (!result.success) { + addLog({ + id: crypto.randomUUID(), + level: 'error', + message: `Failed to stop PLC: ${String(result.error) || 'Unknown error'}`, + }) + return + } + } else { + const result = await window.bridge.runtimeStartPlc(runtimeIpAddress, jwtToken) + if (!result.success) { + addLog({ + id: crypto.randomUUID(), + level: 'error', + message: `Failed to start PLC: ${String(result.error) || 'Unknown error'}`, + }) + return + } + } + + const statusResult = await window.bridge.runtimeGetStatus(runtimeIpAddress, jwtToken) + if (statusResult.success && statusResult.status) { + const status = parsePlcStatus(statusResult.status) + if (status) { + useOpenPLCStore.getState().deviceActions.setPlcRuntimeStatus(status) + } + } + } catch (error) { + addLog({ + id: crypto.randomUUID(), + level: 'error', + message: `PLC control error: ${String(error)}`, + }) + } + } + + const handleSimulatorControl = async (): Promise => { + try { + if (simulatorRunning) { + // Stop: disconnect debugger first, then stop simulator + const { workspace, workspaceActions } = useOpenPLCStore.getState() + if (workspace.isDebuggerVisible) { + await disconnectDebugger(workspaceActions) + } + await (window.bridge.simulatorStop as () => Promise<{ success: boolean }>)() + setSimulatorRunning(false) + addLog({ id: crypto.randomUUID(), level: 'info', message: 'Simulator stopped.' }) + } else { + // Start: build, load firmware, then auto-connect debugger + pendingSimulatorDebugRef.current = true + verifyAndCompile().catch(() => { + pendingSimulatorDebugRef.current = false + }) + } + } catch (error) { + pendingSimulatorDebugRef.current = false + addLog({ + id: crypto.randomUUID(), + level: 'error', + message: `Simulator control error: ${String(error)}`, + }) + } + } + + const connectDebuggerAfterBuild = async () => { + const { project, workspaceActions, consoleActions } = useOpenPLCStore.getState() + const boardTarget = deviceDefinitions.configuration.deviceBoard + const projectPath = project.meta.path + + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'info', + message: 'Starting debugger for simulator...', + }) + + const debugFileResult = await window.bridge.readDebugFile(projectPath, boardTarget) + + if (!debugFileResult.success || !debugFileResult.content) { + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'error', + message: 'Failed to read debug.c file after compilation.', + }) + return + } + + const parsed = parseDebugFile(debugFileResult.content) + const instances = project.data.configuration.resource.instances + + // Build variable index map + const { indexMap, warnings } = buildVariableIndexMap(project.data.pous, instances, parsed) + for (const w of warnings) { + consoleActions.addLog({ id: crypto.randomUUID(), level: 'warning', message: w }) + } + + // Build debug variable tree + let treeMap = new Map() + try { + const treeResult = buildDebugVariableTreeMap(project.data.pous, instances, parsed.variables, project) + treeMap = treeResult.treeMap + + if (process.env.NODE_ENV === 'development') { + ;(window as Window & { debugTrees?: DebugTreeNode[] }).debugTrees = treeResult.trees + } + + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'info', + message: `Debug tree builder: Built ${treeResult.trees.length} trees (${treeResult.complexCount} complex).`, + }) + } catch { + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'warning', + message: 'Debug tree builder encountered errors.', + }) + } + + // Build FB instance map + const fbDebugInstancesMap = buildFbInstanceMap(project.data.pous, instances) + + const fbTypesCount = fbDebugInstancesMap.size + const totalFbInstances = Array.from(fbDebugInstancesMap.values()).reduce((sum, list) => sum + list.length, 0) + if (fbTypesCount > 0) { + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'info', + message: `FB instance map: Found ${totalFbInstances} instances across ${fbTypesCount} FB types.`, + }) + } + + // Connect and activate debugger + const connectResult = await connectAndActivateDebugger( + { + connectionType: 'simulator', + connectionParams: {}, + indexMap, + treeMap, + fbDebugInstancesMap, + }, + workspaceActions, + ) + + if (!connectResult.success) { + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'error', + message: `Failed to establish debugger connection: ${connectResult.error || 'Unknown error'}`, + }) + return + } + + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'info', + message: `Debugger started successfully. Found ${indexMap.size} debug variables.`, + }) + } + + const handleDebuggerClick = async () => { + // Simulator target uses the unified Start/Stop flow instead + if (isCurrentBoardSimulator) return + + const { workspace, project, deviceDefinitions, workspaceActions, consoleActions, deviceActions } = + useOpenPLCStore.getState() + + if (workspace.isDebuggerVisible) { + await disconnectDebugger(workspaceActions) + return + } + + if (isDebuggerProcessing) { + return + } + + setIsDebuggerProcessing(true) + + try { + if (editingState === 'unsaved') { + await saveProject({ data: projectData, meta: projectMeta }, deviceDefinitions) + } + + const boardTarget = deviceDefinitions.configuration.deviceBoard + const projectPath = project.meta.path + const currentBoardInfo = availableBoards.get(boardTarget) + const isRuntimeTarget = isOpenPLCRuntimeTarget(currentBoardInfo) + const isRuntimeV4 = boardTarget === 'OpenPLC Runtime v4' + + let targetIpAddress: string | undefined + let connectionType: 'tcp' | 'rtu' | 'websocket' | 'simulator' = 'tcp' + let rtuPort: string | undefined + let rtuBaudRate: number | undefined + let rtuSlaveId: number | undefined + let jwtToken: string | undefined + + if (isSimulatorTarget(currentBoardInfo)) { + // Check if simulator has firmware loaded + const running = await (window.bridge.simulatorIsRunning as () => Promise)() + if (!running) { + const response = await showDebuggerMessage( + 'warning', + 'Simulator Empty', + 'No firmware is running on the simulator. Would you like to build and upload the project first?', + ['Build & Upload', 'Cancel'], + ) + if (response === 0) { + // Trigger full build, then restart debugger flow + setIsDebuggerProcessing(false) + void verifyAndCompile() + return + } else { + setIsDebuggerProcessing(false) + return + } + } + connectionType = 'simulator' + } else if (isRuntimeTarget) { + const connectionStatus = useOpenPLCStore.getState().runtimeConnection.connectionStatus + const runtimeIpAddress = deviceDefinitions.configuration.runtimeIpAddress + + if (connectionStatus !== 'connected' || !runtimeIpAddress) { + await showDebuggerMessage( + 'warning', + 'Connection Required', + 'You need to connect to the target before starting a debugger session.', + ['OK'], + ) + setIsDebuggerProcessing(false) + return + } + + targetIpAddress = runtimeIpAddress + + if (isRuntimeV4) { + connectionType = 'websocket' + jwtToken = useOpenPLCStore.getState().runtimeConnection.jwtToken || undefined + if (!jwtToken) { + await showDebuggerMessage( + 'error', + 'Authentication Required', + 'JWT token is missing. Please reconnect to the runtime.', + ['OK'], + ) + setIsDebuggerProcessing(false) + return + } + } + } else { + const { modbusTCP, communicationPreferences } = deviceDefinitions.configuration.communicationConfiguration + const rtuEnabled = communicationPreferences.enabledRTU + const tcpEnabled = communicationPreferences.enabledTCP + + if (!rtuEnabled && !tcpEnabled) { + await showDebuggerMessage( + 'warning', + 'Modbus Required', + 'Modbus must be enabled on the target to start a debugger session.', + ['OK'], + ) + setIsDebuggerProcessing(false) + return + } + + let useModbusTcp = false + + if (rtuEnabled && tcpEnabled) { + const response = await showDebuggerMessage( + 'question', + 'Select Modbus Protocol', + 'Both Modbus RTU and Modbus TCP are enabled. Which would you like to use?', + ['Modbus RTU (Serial)', 'Modbus TCP'], + ) + useModbusTcp = response === 1 + } else { + useModbusTcp = tcpEnabled + } + + if (useModbusTcp) { + const dhcpEnabled = communicationPreferences.enabledDHCP + + if (dhcpEnabled) { + const previousIp = deviceDefinitions.temporaryDhcpIp || '' + const result = await showDebuggerIpInput( + 'Target IP Address', + 'Enter the IP address of the target device:', + previousIp, + ) + + if (result === null) { + setIsDebuggerProcessing(false) + return + } + + targetIpAddress = result + if (!targetIpAddress) { + setIsDebuggerProcessing(false) + return + } + + deviceActions.setTemporaryDhcpIp(targetIpAddress) + } else { + targetIpAddress = modbusTCP.tcpStaticHostConfiguration.ipAddress || undefined + + if (!targetIpAddress) { + await showDebuggerMessage('error', 'Configuration Error', 'No IP address configured for Modbus TCP.', [ + 'OK', + ]) + setIsDebuggerProcessing(false) + return + } + } + } else { + const { modbusRTU } = deviceDefinitions.configuration.communicationConfiguration + connectionType = 'rtu' + + rtuPort = deviceDefinitions.configuration.communicationPort + rtuBaudRate = parseInt(modbusRTU.rtuBaudRate, 10) + rtuSlaveId = modbusRTU.rtuSlaveId ?? undefined + + if (!rtuPort) { + await showDebuggerMessage( + 'error', + 'Configuration Error', + 'No communication port selected for Modbus RTU.', + ['OK'], + ) + setIsDebuggerProcessing(false) + return + } + + if (rtuSlaveId === undefined) { + await showDebuggerMessage('error', 'Configuration Error', 'No slave ID configured for Modbus RTU.', ['OK']) + setIsDebuggerProcessing(false) + return + } + + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'info', + message: `Using Modbus RTU: Port=${rtuPort}, Baud=${rtuBaudRate}, SlaveID=${rtuSlaveId}`, + }) + } + } + + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'info', + message: 'Starting debug compilation...', + }) + + const { projectData: processedProjectData, validationFailed } = prepareProjectForCompile(projectData) + if (validationFailed) { + setIsDebuggerProcessing(false) + return + } + + window.bridge.runDebugCompilation( + [projectPath, boardTarget, processedProjectData], + (data: { logLevel?: 'info' | 'error' | 'warning'; message: string | Buffer; closePort?: boolean }) => { + if (typeof data.message === 'string') { + data.message + .trim() + .split('\n') + .forEach((line) => { + consoleActions.addLog({ + id: crypto.randomUUID(), + level: data.logLevel ?? 'info', + message: line, + }) + }) + } + if (data.message && typeof data.message !== 'string') { + BufferToStringArray(data.message).forEach((message) => { + consoleActions.addLog({ + id: crypto.randomUUID(), + level: data.logLevel ?? 'info', + message, + }) + }) + } + + if (data.closePort) { + void handleMd5Verification( + projectPath, + boardTarget, + connectionType, + { + ipAddress: targetIpAddress, + port: rtuPort, + baudRate: rtuBaudRate, + slaveId: rtuSlaveId, + jwtToken, + }, + targetIpAddress, + isRuntimeTarget, + ) + } + }, + ) + } catch (error) { + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'error', + message: `Error during debugger initialization: ${String(error)}`, + }) + setIsDebuggerProcessing(false) + } + } + + const handleMd5Verification = async ( + projectPath: string, + boardTarget: string, + connectionType: 'tcp' | 'rtu' | 'websocket' | 'simulator', + connectionParams: { + ipAddress?: string + port?: string + baudRate?: number + slaveId?: number + jwtToken?: string + }, + targetIpAddress: string | undefined, + isRuntimeTarget: boolean, + ) => { + const { consoleActions, workspaceActions, runtimeConnection, deviceActions } = useOpenPLCStore.getState() + + try { + if (isRuntimeTarget) { + const plcStatus = runtimeConnection.plcStatus + const jwtToken = runtimeConnection.jwtToken + + if (plcStatus === 'STOPPED' && jwtToken) { + const response = await showDebuggerMessage( + 'question', + 'PLC Stopped', + 'The PLC is currently stopped. The debugger requires the PLC to be running. Would you like to start the PLC now?', + ['Yes', 'No'], + ) + + if (response === 1) { + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'info', + message: 'Debugger session cancelled.', + }) + setIsDebuggerProcessing(false) + return + } + + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'info', + message: 'Starting PLC...', + }) + + const startResult = await window.bridge.runtimeStartPlc(targetIpAddress!, jwtToken) + if (!startResult.success) { + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'error', + message: `Failed to start PLC: ${startResult.error || 'Unknown error'}`, + }) + await showDebuggerMessage( + 'error', + 'Start PLC Failed', + `Could not start the PLC: ${startResult.error || 'Unknown error'}`, + ['OK'], + ) + setIsDebuggerProcessing(false) + return + } + + deviceActions.setPlcRuntimeStatus('RUNNING') + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'info', + message: 'PLC started successfully. Waiting 2 seconds...', + }) + + await new Promise((resolve) => setTimeout(resolve, 2000)) + } + } + + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'info', + message: 'Extracting MD5 from compiled program...', + }) + + const programStResult = await window.bridge.debuggerReadProgramStMd5(projectPath, boardTarget) + + if (!programStResult.success || !programStResult.md5) { + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'error', + message: `Failed to extract MD5: ${programStResult.error ?? 'Unknown error'}`, + }) + + await showDebuggerMessage( + 'error', + 'MD5 Extraction Failed', + programStResult.error ?? 'Could not extract MD5 from program.st', + ['OK'], + ) + setIsDebuggerProcessing(false) + return + } + + const expectedMd5 = programStResult.md5 + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'info', + message: `Program MD5: ${expectedMd5}`, + }) + + const targetDisplay = + connectionType === 'simulator' + ? 'simulator' + : connectionType === 'tcp' || connectionType === 'websocket' + ? targetIpAddress + : connectionParams.port + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'info', + message: `Requesting MD5 from target at ${targetDisplay}...`, + }) + + const verifyResult = await window.bridge.debuggerVerifyMd5(connectionType, connectionParams, expectedMd5) + + if (!verifyResult.success) { + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'error', + message: `MD5 verification failed: ${verifyResult.error ?? 'Unknown error'}`, + }) + + await showDebuggerMessage( + 'error', + 'Connection Error', + `Could not verify MD5 with target: ${verifyResult.error ?? 'Unknown error'}`, + ['OK'], + ) + setIsDebuggerProcessing(false) + return + } + + if (verifyResult.match) { + console.log('MD5 matched:', expectedMd5) + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'info', + message: 'MD5 verification successful. Starting debugger...', + }) + + const debugFileResult = await window.bridge.readDebugFile(projectPath, boardTarget) + + if (debugFileResult.success && debugFileResult.content) { + const parsed = parseDebugFile(debugFileResult.content) + + const { project } = useOpenPLCStore.getState() + const instances = project.data.configuration.resource.instances + + // Build variable index map + const { indexMap, warnings } = buildVariableIndexMap(project.data.pous, instances, parsed) + for (const w of warnings) { + consoleActions.addLog({ id: crypto.randomUUID(), level: 'warning', message: w }) + } + + // Build debug variable tree + let treeMap = new Map() + try { + const treeResult = buildDebugVariableTreeMap(project.data.pous, instances, parsed.variables, project) + treeMap = treeResult.treeMap + + if (process.env.NODE_ENV === 'development') { + ;(window as Window & { debugTrees?: DebugTreeNode[] }).debugTrees = treeResult.trees + } + + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'info', + message: `Debug tree builder: Built ${treeResult.trees.length} trees (${treeResult.complexCount} complex).`, + }) + } catch { + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'warning', + message: 'Debug tree builder encountered errors.', + }) + } + + // Build FB instance map + const fbDebugInstancesMap = buildFbInstanceMap(project.data.pous, instances) + + const fbTypesCount = fbDebugInstancesMap.size + const totalFbInstances = Array.from(fbDebugInstancesMap.values()).reduce((sum, list) => sum + list.length, 0) + if (fbTypesCount > 0) { + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'info', + message: `FB instance map: Found ${totalFbInstances} instances across ${fbTypesCount} FB types.`, + }) + } + + // Connect and activate debugger + const connectResult = await connectAndActivateDebugger( + { + connectionType, + connectionParams, + indexMap, + treeMap, + fbDebugInstancesMap, + targetIpAddress, + isRuntimeTarget, + }, + workspaceActions, + ) + + if (!connectResult.success) { + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'error', + message: `Failed to establish debugger connection: ${connectResult.error || 'Unknown error'}`, + }) + setIsDebuggerProcessing(false) + return + } + + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'info', + message: `Debugger started successfully. Found ${indexMap.size} debug variables.`, + }) + } else { + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'error', + message: 'Failed to read debug.c file after compilation.', + }) + } + setIsDebuggerProcessing(false) + } else { + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'warning', + message: `MD5 mismatch. Target: ${verifyResult.targetMd5}, Expected: ${expectedMd5}`, + }) + + const response = await showDebuggerMessage( + 'warning', + 'Program Mismatch', + 'The program running on the target does not match the program opened in the editor. Would you like to upload the current project to the target?', + ['Yes', 'No'], + ) + + if (response === 0) { + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'info', + message: 'Uploading program to target...', + }) + + const latestProjectData = useOpenPLCStore.getState().project.data + const boardCore = availableBoards.get(boardTarget)?.core || null + const runtimeJwtToken = useOpenPLCStore.getState().runtimeConnection.jwtToken || null + const { projectData: processedProjectData, validationFailed } = prepareProjectForCompile(latestProjectData) + + if (validationFailed) { + setIsDebuggerProcessing(false) + return + } + + window.bridge.runCompileProgram( + [projectPath, boardTarget, boardCore, false, processedProjectData, targetIpAddress ?? '', runtimeJwtToken], + (data: { + logLevel?: 'info' | 'error' | 'warning' + message: string | Buffer + plcStatus?: string + closePort?: boolean + }) => { + if (typeof data.message === 'string') { + data.message + .trim() + .split('\n') + .forEach((line) => { + consoleActions.addLog({ + id: crypto.randomUUID(), + level: data.logLevel ?? 'info', + message: line, + }) + }) + } + if (data.message && typeof data.message !== 'string') { + BufferToStringArray(data.message).forEach((message) => { + consoleActions.addLog({ + id: crypto.randomUUID(), + level: data.logLevel ?? 'info', + message, + }) + }) + } + + if (data.closePort) { + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'info', + message: 'Upload completed. Restarting debugger verification...', + }) + + setTimeout(() => { + void handleMd5Verification( + projectPath, + boardTarget, + connectionType, + connectionParams, + targetIpAddress, + isRuntimeTarget, + ) + }, 2000) + } + }, + ) + } else { + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'info', + message: 'Debugger session cancelled.', + }) + setIsDebuggerProcessing(false) + } + } + } catch (error) { + consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'error', + message: `Unexpected error during MD5 verification: ${String(error)}`, + }) + setIsDebuggerProcessing(false) + } + } + + return ( + <> + + + + + + + + verifyAndCompile()} + /> + + + void handleSimulatorControl() : () => void handlePlcControl()} + disabled={isCurrentBoardSimulator ? isCompiling : connectionStatus !== 'connected'} + className={cn( + isCurrentBoardSimulator + ? isCompiling + ? disabledButtonClass + : '' + : connectionStatus !== 'connected' + ? disabledButtonClass + : '', + )} + > + {(isCurrentBoardSimulator ? simulatorRunning : plcStatus === 'RUNNING') ? : null} + + + + void handleDebuggerClick()} + disabled={isDebuggerProcessing || isCurrentBoardSimulator} + isActive={isDebuggerVisible} + className={cn((isDebuggerProcessing || isCurrentBoardSimulator) && 'cursor-not-allowed opacity-50')} + /> + + + ) +}