From 86fe466b550839e2cc6288df9a69606ecdcef693 Mon Sep 17 00:00:00 2001 From: arDTDev Date: Thu, 27 Nov 2025 23:22:56 +0900 Subject: [PATCH] feat: Add VisionData panel, HW error display, and reel handler 3D model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add hardware error banner with priority system (motion > i/o > emergency) - Add DIO status to HW status display with backend integration - Remove status text from HW status, keep only LED indicators - Add VisionDataPanel showing real-time recognized data for L/C/R ports - Add GetVisionData API in MachineBridge with batch field support - Add BroadcastVisionData function (250ms interval) - Replace 3D model with detailed reel handler equipment - Use OrthographicCamera with front view for distortion-free display - Fix ProcessedDataPanel layout to avoid right sidebar overlap - Show log viewer filename in error message when file not found ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- FrontEnd/components/Machine3D.tsx | 1010 +++++++++++------------ FrontEnd/components/VisionDataPanel.tsx | 156 ++++ FrontEnd/components/layout/Footer.tsx | 96 ++- FrontEnd/pages/HomePage.tsx | 10 +- Handler/Project/Dialog/fWebView.cs | 32 + Handler/Project/WebUI/MachineBridge.cs | 106 ++- Handler/Project/fMain.cs | 2 +- 7 files changed, 843 insertions(+), 569 deletions(-) create mode 100644 FrontEnd/components/VisionDataPanel.tsx diff --git a/FrontEnd/components/Machine3D.tsx b/FrontEnd/components/Machine3D.tsx index ce6115e..b14395f 100644 --- a/FrontEnd/components/Machine3D.tsx +++ b/FrontEnd/components/Machine3D.tsx @@ -1,7 +1,6 @@ - -import React, { useRef, useMemo } from 'react'; +import React, { useRef } from 'react'; import { Canvas, useFrame } from '@react-three/fiber'; -import { OrbitControls, Grid, PerspectiveCamera, Text, Box, Environment, RoundedBox } from '@react-three/drei'; +import { OrbitControls, Grid, OrthographicCamera, Text, Box, Environment, RoundedBox, Cylinder, Plane } from '@react-three/drei'; import * as THREE from 'three'; import { RobotTarget, IOPoint } from '../types'; @@ -16,10 +15,260 @@ interface Machine3DProps { }; } -// -- Parts Components -- +// Reusable Aluminum Profile Component +const Profile = ({ position, size, rotation = [0, 0, 0] }: { position: [number, number, number], size: [number, number, number], rotation?: [number, number, number] }) => ( + + + +); +// Industrial Linear Actuator Rail (Black body + Silver guides) +const RobotRail = ({ + length, + orientation = 'horizontal', + profile = [0.25, 0.25] +}: { + length: number, + orientation?: 'horizontal' | 'vertical', + profile?: [number, number] +}) => { + const isHoriz = orientation === 'horizontal'; + const [dim1, dim2] = profile; + const size = isHoriz ? [length, dim1, dim2] : [dim1, length, dim2]; + const railW = isHoriz ? length : dim1 * 0.4; + const railH = isHoriz ? dim1 * 0.4 : length; + const railD = 0.02; + const guideZ = dim2 / 2 + 0.005; + + return ( + + + + + + + + + + + + + + ); +}; + +// Detailed Industrial Camera Component +const IndustrialCamera = ({ position, rotation = [0, 0, 0], isActive = false }: { position: [number, number, number], rotation?: [number, number, number], isActive?: boolean }) => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + MA-D200B + DC 12V + + + + + {isActive && ( + + + + )} + + ); +}; + +// SATO CL4NX Plus Printer Component +const SatoPrinter = ({ position, rotation = [0, 0, 0] }: { position: [number, number, number], rotation?: [number, number, number] }) => { + return ( + + + + + + + + + + + + + + + + + + + + + SATO + CL4NX Plus + + + + + + ); +}; + +// Industrial Cart Component +const Cart = ({ position, label }: { position: [number, number, number], label?: string }) => { + return ( + + + + + + + + + + + + + + + + + + + {Array.from({ length: 15 }).map((_, i) => ( + + + + + + + + + ))} + {[0, 120, 240].map((angle, idx) => { + const rad = 0.65; + const x = Math.cos(angle * Math.PI / 180) * rad; + const z = Math.sin(angle * Math.PI / 180) * rad; + return ( + + + + ); + })} + + {label && ( + + {label} + + )} + + ); +}; + +// Glass Door Component +const Door = ({ position, size, isOpen, hingeDirection = 1 }: { position: [number, number, number], size: [number, number, number], isOpen: boolean, hingeDirection?: number }) => { + const meshRef = useRef(null); + + useFrame(() => { + if (meshRef.current) { + const targetRotation = isOpen ? Math.PI / 2 * hingeDirection : 0; + meshRef.current.rotation.y = THREE.MathUtils.lerp(meshRef.current.rotation.y, targetRotation, 0.1); + } + }); + + return ( + + + + + + + + + + + + + + ); +}; + +// Reusable Labeler Unit Component +const LabelerUnit = ({ side, posY, posZ }: { side: 'left' | 'right', posY: number, posZ: number }) => { + const isLeft = side === 'left'; + const wallOffsetX = isLeft ? -1.4 : 1.4; + const printerOffset = isLeft ? 0.3 : -0.3; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +// Tower Lamp Component const TowerLamp = ({ ioState }: { ioState: IOPoint[] }) => { - // Outputs 0,1,2 mapped to Red, Yellow, Green const redOn = ioState.find(io => io.id === 0 && io.type === 'output')?.state; const yellowOn = ioState.find(io => io.id === 1 && io.type === 'output')?.state; const greenOn = ioState.find(io => io.id === 2 && io.type === 'output')?.state; @@ -30,13 +279,13 @@ const TowerLamp = ({ ioState }: { ioState: IOPoint[] }) => { useFrame((state) => { const time = state.clock.elapsedTime; - const pulse = (Math.sin(time * 6) * 0.5 + 0.5); + const pulse = (Math.sin(time * 6) * 0.5 + 0.5); const intensity = 0.5 + (pulse * 3.0); if (redMat.current) { redMat.current.emissiveIntensity = redOn ? intensity : 0; redMat.current.opacity = redOn ? 1.0 : 0.3; - redMat.current.color.setHex(redOn ? 0xff0000 : 0x550000); + redMat.current.color.setHex(redOn ? 0xff0000 : 0x550000); } if (yelMat.current) { yelMat.current.emissiveIntensity = yellowOn ? intensity : 0; @@ -51,583 +300,264 @@ const TowerLamp = ({ ioState }: { ioState: IOPoint[] }) => { }); return ( - - {/* Pole */} + - {/* Green */} - + - {/* Yellow */} - + - {/* Red */} - + ); }; -// -- DIPPING STATION COMPONENTS -- +// Main Machine Model +const MachineModel = ({ target, doorStates }: { target: RobotTarget, doorStates: Machine3DProps['doorStates'] }) => { + const FRAME_WIDTH = 10; + const TOTAL_HEIGHT = 6.5; + const DOCK_HEIGHT = 2.5; + const OPERATIONAL_HEIGHT = TOTAL_HEIGHT - DOCK_HEIGHT; + const FRAME_DEPTH = 3.0; + const CONVEYOR_Y = DOCK_HEIGHT + 0.2; + const LABELER_BASE_Y = CONVEYOR_Y; + const PICKER_Y = 4.8; -const LiquidBath = ({ - position, - label, - liquidColor, -}: { - position: [number, number, number], - label: string, - liquidColor: string, -}) => { - const width = 1.0; - const depth = 0.8; - const height = 0.3; - const wallThick = 0.03; + const minZLength = 0.6; + const zRailLength = Math.max(minZLength, target.z + 0.4); - const SteelMaterial = ( - - ); + const Z_XRAIL = -0.6; + const Z_CARRIAGE = -0.45; + const Z_ZRAIL = -0.3; + const Z_HEAD = 0; - return ( - - - - - {SteelMaterial} - - - - {SteelMaterial} - - - - {SteelMaterial} - - - - {SteelMaterial} - - - - {SteelMaterial} - - - - - - - - {label} - - - ); -}; - -const HakkoFX305 = ({ position }: { position: [number, number, number] }) => { - return ( - - {/* Main Blue Chassis */} - - - - - - - - - - - - - - - - - - - - - HAKKO - - - FX-305 - - - - - - - - - - - - - - - - - - - - {[-0.12, 0, 0.12, 0.24].map((x, i) => ( - - - - - ))} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 2. HAKKO SOLDER - - - ); -}; - -const DippingStations = () => { - return ( - - - - - - - - - - ); -}; - -const Door = ({ - position, - rotation = [0,0,0], - size, - isOpen -}: { - position: [number, number, number], - rotation?: [number, number, number], - size: [number, number], - isOpen: boolean -}) => { - const meshRef = useRef(null); - - useFrame((state, delta) => { - if (!meshRef.current) return; - const targetY = isOpen ? position[1] + 2 : position[1]; - meshRef.current.position.y = THREE.MathUtils.lerp(meshRef.current.position.y, targetY, delta * 5); - }); - - return ( - - - - - - - - - - - - - ); -}; - -const MachineFrame = ({ doorStates }: { doorStates: { front: boolean, right: boolean, left: boolean, back: boolean } }) => { - return ( - - - - - - - - {[[-2, -2], [2, -2], [2, 2], [-2, 2]].map(([x, z], i) => ( - - - - - ))} - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; - -// --- MISUMI STYLE INDUSTRIAL ACTUATOR COMPONENT --- - -const IndustrialActuatorRail = ({ length, label, hasMotor = true }: { length: number, label?: string, hasMotor?: boolean }) => { - const width = 0.14; // Actuator Width - const height = 0.08; // Actuator Height (Profile) + // Convert target coordinates + const pickerX = target.x; + const pickerZ = target.z; + const leftLabelY = target.y; + const leftLabelZ = 0.5; + const rightLabelY = target.y; + const rightLabelZ = 0.5; return ( - {/* 1. Main Aluminum Body (White/Light Grey Matte) */} - - - - - {/* 2. Top Stainless Steel Cover Strip */} - - - - - - {/* 3. Black Gap/Slit for Slider */} - - - - - - {/* 4. End Caps (Plastic White) */} - - - - - - - - - - - - {/* 5. Servo Motor Unit (Black & Silver) */} - {hasMotor && ( - - {/* Flange */} - - - - - {/* Motor Body */} - - - - - {/* Encoder Cap */} - - - - - {/* Cable Gland */} - - - - - - )} - - {/* 6. Label/Branding */} - {label && ( - - {label} - - )} - - ); -} - -// The Moving Block on top of the actuator -const IndustrialSlider = ({ width = 0.16, length = 0.18, height = 0.03 }) => { - return ( - - - - - {/* Mounting Holes (Visual) */} - {[-1, 1].map(x => [-1, 1].map(z => ( - - - - - )))} - - ) -} - -// -- MAIN ROBOT ASSEMBLY -- - -const Robot = ({ target }: { target: RobotTarget }) => { - const bridgeGroup = useRef(null); - const carriageGroup = useRef(null); - const zAxisGroup = useRef(null); - - useFrame((state, delta) => { - if (bridgeGroup.current) { - // Y-Axis Movement - bridgeGroup.current.position.z = THREE.MathUtils.lerp(bridgeGroup.current.position.z, target.y, delta * 3); - } - if (carriageGroup.current) { - // X-Axis Movement - carriageGroup.current.position.x = THREE.MathUtils.lerp(carriageGroup.current.position.x, target.x, delta * 3); - } - if (zAxisGroup.current) { - // Z-Axis Movement - zAxisGroup.current.position.y = THREE.MathUtils.lerp(zAxisGroup.current.position.y, target.z, delta * 3); - } - }); - - return ( - - - {/* --- Y-AXIS (Left & Right Fixed Actuators) --- */} + {/* --- STRUCTURAL FRAME --- */} - - - - - + + + + + + + + + + + + + + + + + - {/* --- MOVING BRIDGE (Y-AXIS CARRIAGE + X-AXIS ACTUATOR) --- */} - - - {/* Y-Sliders (Connecting Bridge to Y-Rails) */} - - - - {/* Bridge Beam Structure */} - - - - + {/* --- DOCKING AREA (LOWER LEVEL) --- */} + + + - {/* X-AXIS ACTUATOR (Mounted on top of Bridge) */} - - + {[-3.5, 0, 3.5].map((xPos, idx) => ( + + + + + + + + {idx === 0 ? "SPARE L" : idx === 1 ? "LOADING" : "SPARE R"} + + + ))} + + + + + + + {/* --- DOORS (UPPER LEVEL ONLY) --- */} + + + + + + + {/* --- TOP COVER (ROOF) --- */} + + + + + + + + + + + {/* --- CONVEYORS (Left & Right - UPPER LEVEL) --- */} + + + + + + + + Left Conveyor + + + + PLACE REEL + + + + + + + + + + Right Conveyor + + + + PLACE REEL + + + {/* --- MAIN PICKER (Robot Design) --- */} + + + + + + + + - {/* --- MOVING CARRIAGE (X-AXIS SLIDER + Z-AXIS) --- */} - - - {/* X-Slider */} - - - + + + - {/* --- Z-AXIS ACTUATOR (Vertical) --- */} - - {/* Z-Actuator Body (Fixed to X-Slider) */} - - - {/* Z-Motor on Top */} - - - - - - + + + + + + + + + + + + + + + {Array.from({ length: 8 }).map((_, i) => { + const angle = (i / 8) * Math.PI * 2; + const radius = 0.4; + const x = Math.cos(angle) * radius; + const z = Math.sin(angle) * radius; + return ( + + + + + + + + + ); + })} - - {/* MOVING Z-HEAD (The Slider of Z-Axis) */} - - - {/* Connection Plate */} - - - - - - {/* PICKER MECHANISM */} - - - - - - {/* Fingers */} - - - - - - - - - - {/* PCB STRIP */} - - - - - - - - - - - - - - - - - - + + {/* --- LABELER UNITS --- */} + + + + + + + + {/* --- CAMERAS (Roof Mounted) --- */} + + + + + + {/* --- REAR DISCHARGE CHUTES --- */} + + + + + + + + {/* Grid Floor */} + ); }; export const Machine3D: React.FC = ({ target, ioState, doorStates }) => { return ( - - - + {/* ์ •๋ฉด์—์„œ ๋ฐ”๋ผ๋ณด๋Š” ์ง๊ต ์นด๋ฉ”๋ผ (์™œ๊ณก ์—†์Œ) */} + + - - - - - - - + + + + + + - ); }; diff --git a/FrontEnd/components/VisionDataPanel.tsx b/FrontEnd/components/VisionDataPanel.tsx new file mode 100644 index 0000000..c80b78f --- /dev/null +++ b/FrontEnd/components/VisionDataPanel.tsx @@ -0,0 +1,156 @@ +import React, { useState, useEffect } from 'react'; +import { Eye, CheckCircle, XCircle } from 'lucide-react'; +import { comms } from '../communication'; +import { PanelHeader } from './common/PanelHeader'; +import { CyberPanel } from './common/CyberPanel'; + +interface PortData { + cartSize: string; + enabled?: boolean; + confirm?: boolean; + rid: string; + rid_trust?: boolean; + rid_new?: boolean; + sid: string; + sid0?: string; + sid_trust?: boolean; + qty: string; + qty_trust?: boolean; + qty_rq?: boolean; + vname: string; + vname_trust?: boolean; + vlot: string; + vlot_trust?: boolean; + mfgdate: string; + mfgdate_trust?: boolean; + partno: string; + partno_trust?: boolean; + reelSize: string; + rid2?: string; + sid2?: string; + qty2?: string; + vname2?: string; + vlot2?: string; + mfgdate2?: string; + partno2?: string; + batch?: string; + qtymax?: string; + barcodeCount?: number; + regexCount?: number; +} + +interface VisionData { + left: PortData; + center: PortData; + right: PortData; +} + +const fieldLabels = ['RID', 'SID', 'QTY', 'VNAME', 'VLOT', 'MFG', 'PART', 'SIZE', 'BATCH']; + +export const VisionDataPanel: React.FC = () => { + const [data, setData] = useState(null); + + useEffect(() => { + const unsubscribe = comms.subscribe((msg: any) => { + if (msg?.type === 'VISION_DATA_UPDATE' && msg.data) { + setData(msg.data); + } + }); + + return () => { + unsubscribe(); + }; + }, []); + + const getTrustColor = (trust?: boolean) => { + if (trust === undefined) return 'text-slate-400'; + return trust ? 'text-lime-400' : 'text-slate-400'; + }; + + const getCompareColor = (val1: string, val2?: string) => { + if (!val2 || val2 === '') return ''; + return val1 !== val2 ? 'text-red-400' : ''; + }; + + const renderPortColumn = (port: PortData | undefined, title: string, isCenter: boolean = false) => { + if (!port) return null; + + const values = [ + { val: port.rid, val2: port.rid2, trust: port.rid_trust }, + { val: port.sid, val2: port.sid2, trust: port.sid_trust }, + { val: port.qty, val2: port.qty2, trust: port.qty_trust, rq: port.qty_rq }, + { val: port.vname, val2: port.vname2, trust: port.vname_trust }, + { val: port.vlot, val2: port.vlot2, trust: port.vlot_trust }, + { val: port.mfgdate, val2: port.mfgdate2, trust: port.mfgdate_trust }, + { val: port.partno, val2: port.partno2, trust: port.partno_trust }, + { val: port.reelSize === 'None' ? '--' : port.reelSize, val2: undefined, trust: undefined }, + { val: port.batch || '-', val2: undefined, trust: undefined }, + ]; + + const headerBg = port.enabled === false ? 'bg-orange-600/80' : (port.confirm ? 'bg-lime-600/80' : 'bg-slate-700/80'); + const headerText = port.enabled === false ? 'DISABLE' : port.cartSize; + + return ( +
+ {/* Header */} +
+ {headerText} + ({title}) +
+ {/* Values */} +
+ {values.map((item, idx) => ( +
+ {fieldLabels[idx]} + + {item.rq ? `RQ:${item.val}` : item.val || '-'} + + {isCenter && item.trust !== undefined && ( + item.trust ? + : + + )} +
+ ))} + {/* Center-specific fields */} + {isCenter && ( + <> +
+ MAX + {port.qtymax || '-'} +
+
+ BCD + {port.barcodeCount ?? 0} +
+
+ REGEX + {port.regexCount ?? 0} +
+ + )} +
+
+ ); + }; + + return ( + + + +
+ {!data ? ( +
+ Waiting for data... +
+ ) : ( +
+ {renderPortColumn(data.left, 'LEFT')} + {renderPortColumn(data.center, 'CENTER', true)} + {renderPortColumn(data.right, 'RIGHT')} +
+ )} +
+
+ ); +}; diff --git a/FrontEnd/components/layout/Footer.tsx b/FrontEnd/components/layout/Footer.tsx index fdca001..37b1cac 100644 --- a/FrontEnd/components/layout/Footer.tsx +++ b/FrontEnd/components/layout/Footer.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { RobotTarget } from '../../types'; import { comms } from '../../communication'; @@ -10,6 +10,13 @@ interface HWItem { status: number; } +// ์˜ค๋ฅ˜ ์šฐ์„ ์ˆœ์œ„: motion(1) > i/o(2) > emergency(3) > general(4) +interface HardwareError { + type: 'motion' | 'io' | 'emergency' | 'general'; + priority: number; + message: string; +} + interface FooterProps { isHostConnected: boolean; robotTarget: RobotTarget; @@ -45,31 +52,72 @@ export const Footer: React.FC = ({ isHostConnected, robotTarget }) }; }, []); + // ํ•˜๋“œ์›จ์–ด ์˜ค๋ฅ˜ ๊ฐ์ง€ ๋ฐ ์šฐ์„ ์ˆœ์œ„ ๊ธฐ๋ฐ˜ ํ‘œ์‹œ + // ์šฐ์„ ์ˆœ์œ„: motion(1) > i/o(2) > emergency(3) > general(4) + const hardwareError = useMemo((): HardwareError | null => { + const errors: HardwareError[] = []; + + for (const hw of hwStatus) { + if (hw.status === 3) { // OFF ์ƒํƒœ (์˜ค๋ฅ˜) + if (hw.name === 'MOT') { + errors.push({ type: 'motion', priority: 1, message: 'MOTION HARDWARE ERROR' }); + } else if (hw.name === 'DIO' || hw.name === 'I/O') { + errors.push({ type: 'io', priority: 2, message: 'I/O HARDWARE ERROR' }); + } else if (hw.name === 'EMG') { + errors.push({ type: 'emergency', priority: 3, message: 'EMERGENCY ERROR' }); + } + // ๋‹ค๋ฅธ ํ•˜๋“œ์›จ์–ด๋Š” ์ผ๋ฐ˜ ์˜ค๋ฅ˜๋กœ ํ‘œ์‹œํ•˜์ง€ ์•Š์Œ (BCD, VIS, PRT ๋“ฑ์€ ์ผ๋ฐ˜์ ์ธ ์—ฐ๊ฒฐ ์ƒํƒœ) + } + } + + // ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๊ฐ€์žฅ ๋†’์€(์ˆซ์ž๊ฐ€ ๋‚ฎ์€) ์˜ค๋ฅ˜ ๋ฐ˜ํ™˜ + if (errors.length === 0) return null; + return errors.sort((a, b) => a.priority - b.priority)[0]; + }, [hwStatus]); + return ( -
-
- {/* H/W ์ƒํƒœ ํ‘œ์‹œ (์œˆํผ HWState์™€ ๋™์ผ) */} - {hwStatus.map((hw) => { - const colors = getStatusColor(hw.status); - return ( -
-
- {hw.name} - {hw.title} + <> + {/* ํ•˜๋“œ์›จ์–ด ์˜ค๋ฅ˜ ํ‘œ์‹œ ๋ฐฐ๋„ˆ - ํ™”๋ฉด ์ค‘์•™ ์ƒ๋‹จ์— ํฌ๊ฒŒ ํ‘œ์‹œ */} + {hardwareError && ( +
+
+
+
โš 
+
+ {hardwareError.message} +
+
+ CHECK HARDWARE CONNECTION +
- ); - })} - {/* HOST ์—ฐ๊ฒฐ ์ƒํƒœ */} -
-
- HOST +
-
-
- POS.X: {robotTarget.x.toFixed(3)} - POS.Y: {robotTarget.y.toFixed(3)} - POS.Z: {robotTarget.z.toFixed(3)} -
-
+ )} + +
+
+ {/* H/W ์ƒํƒœ ํ‘œ์‹œ (์œˆํผ HWState์™€ ๋™์ผ) */} + {hwStatus.map((hw) => { + const colors = getStatusColor(hw.status); + return ( +
+
+ {hw.name} +
+ ); + })} + {/* HOST ์—ฐ๊ฒฐ ์ƒํƒœ */} +
+
+ HOST +
+
+
+ POS.X: {robotTarget.x.toFixed(3)} + POS.Y: {robotTarget.y.toFixed(3)} + POS.Z: {robotTarget.z.toFixed(3)} +
+
+ ); }; diff --git a/FrontEnd/pages/HomePage.tsx b/FrontEnd/pages/HomePage.tsx index 7593840..56532da 100644 --- a/FrontEnd/pages/HomePage.tsx +++ b/FrontEnd/pages/HomePage.tsx @@ -8,6 +8,7 @@ import { MotionPanel } from '../components/MotionPanel'; import { CyberPanel } from '../components/common/CyberPanel'; import { ModelInfoPanel } from '../components/ModelInfoPanel'; import { ProcessedDataPanel } from '../components/ProcessedDataPanel'; +import { VisionDataPanel } from '../components/VisionDataPanel'; import { SystemStatusPanel } from '../components/SystemStatusPanel'; import { EventLogPanel } from '../components/EventLogPanel'; import { SystemState, Recipe, IOPoint, LogEntry, RobotTarget, ConfigItem } from '../types'; @@ -121,8 +122,13 @@ export const HomePage: React.FC = ({ - {/* Bottom Docked - Processed Data Panel */} -
+ {/* Left - Vision Data Panel (Recognized Data) - ์„ธ๋กœ ๊ณต๊ฐ„ ์ตœ๋Œ€ ์‚ฌ์šฉ */} +
+ +
+ + {/* Bottom Docked - Processed Data Panel (์šฐ์ธก ์‚ฌ์ด๋“œ๋ฐ” ํ”ผํ•จ) */} +
diff --git a/Handler/Project/Dialog/fWebView.cs b/Handler/Project/Dialog/fWebView.cs index 817c3b7..b18d83e 100644 --- a/Handler/Project/Dialog/fWebView.cs +++ b/Handler/Project/Dialog/fWebView.cs @@ -277,6 +277,7 @@ namespace Project.Dialog { _hwUpdateCounter = 0; BroadcastHWStatus(); + BroadcastVisionData(); } } @@ -311,6 +312,37 @@ namespace Project.Dialog } } + // Vision Data ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ (listView21๊ณผ ๋™์ผํ•œ ๋ฐ์ดํ„ฐ) + private void BroadcastVisionData() + { + try + { + var bridge = new WebUI.MachineBridge(this); + string visionDataJson = bridge.GetVisionData(); + + var payload = new + { + type = "VISION_DATA_UPDATE", + data = JsonConvert.DeserializeObject(visionDataJson) + }; + + string json = JsonConvert.SerializeObject(payload); + + // WebView2๋กœ ์ „์†ก + if (webView != null && webView.CoreWebView2 != null) + { + webView.CoreWebView2.PostWebMessageAsJson(json); + } + + // WebSocket์œผ๋กœ ์ „์†ก + _wsServer?.Broadcast(json); + } + catch (Exception ex) + { + Console.WriteLine($"[fWebView] Vision data broadcast error: {ex.Message}"); + } + } + private List GetChangedIOs() { var list = new List(); diff --git a/Handler/Project/WebUI/MachineBridge.cs b/Handler/Project/WebUI/MachineBridge.cs index bf291e7..3a95435 100644 --- a/Handler/Project/WebUI/MachineBridge.cs +++ b/Handler/Project/WebUI/MachineBridge.cs @@ -1367,7 +1367,7 @@ namespace Project.WebUI var exename = AR.UTIL.MakePath("LogView.exe"); if (System.IO.File.Exists(exename) == false) { - var response = new { success = false, message = "Log viewer file not found\nPlease contact support" }; + var response = new { success = false, message = $"Log viewer file not found\n{exename}\nPlease contact support" }; return JsonConvert.SerializeObject(response); } @@ -2221,7 +2221,15 @@ namespace Project.WebUI hwList.Add(new { name = "PLC", title = "SET", status = 0 }); } - // 9. Motion + // 9. DIO (Digital I/O) + hwList.Add(new + { + name = "DIO", + title = PUB.dio?.IsInit == true ? "ON" : "OFF", + status = PUB.dio?.IsInit == true ? 1 : 3 + }); + + // 10. Motion hwList.Add(new { name = "MOT", @@ -2238,6 +2246,100 @@ namespace Project.WebUI } } + /// + /// ์‹ค์‹œ๊ฐ„ ์ธ์‹ ๋ฐ์ดํ„ฐ ์กฐํšŒ (listView21๊ณผ ๋™์ผํ•œ ๋ฐ์ดํ„ฐ) + /// Left/Center/Right ํฌํŠธ์˜ VisionData ์ •๋ณด + /// + public string GetVisionData() + { + try + { + var visdataL = PUB.Result.ItemDataL?.VisionData; + var visdataC = PUB.Result.ItemDataC?.VisionData; + var visdataR = PUB.Result.ItemDataR?.VisionData; + + var result = new + { + left = new + { + cartSize = DIO.getCartSize(0).ToString(), + enabled = PUB.flag.get(eVarBool.FG_ENABLE_LEFT), + rid = visdataL?.RID ?? "", + sid = visdataL?.SID ?? "", + qty = visdataL?.QTY ?? "", + vname = visdataL?.VNAME ?? "", + vlot = visdataL?.VLOT ?? "", + mfgdate = visdataL?.MFGDATE ?? "", + partno = visdataL?.PARTNO ?? "", + reelSize = visdataL?.ReelSize.ToString() ?? "--", + batch = visdataL?.BATCH ?? "", + rid2 = visdataL?.RID2 ?? "", + sid2 = visdataL?.SID2 ?? "", + qty2 = visdataL?.QTY2 ?? "", + vname2 = visdataL?.VNAME2 ?? "", + vlot2 = visdataL?.VLOT2 ?? "", + mfgdate2 = visdataL?.MFGDATE2 ?? "", + partno2 = visdataL?.PARTNO2 ?? "" + }, + center = new + { + cartSize = DIO.getCartSize(1).ToString(), + confirm = visdataC?.Confirm ?? false, + rid = visdataC?.RID ?? "", + rid_trust = visdataC?.RID_Trust ?? false, + rid_new = visdataC?.RIDNew ?? false, + sid = visdataC?.SID ?? "", + sid0 = visdataC?.SID0 ?? "", + sid_trust = visdataC?.SID_Trust ?? false, + qty = visdataC?.QTY ?? "", + qty_trust = visdataC?.QTY_Trust ?? false, + qty_rq = visdataC?.QTYRQ ?? false, + vname = visdataC?.VNAME ?? "", + vname_trust = visdataC?.VNAME_Trust ?? false, + vlot = visdataC?.VLOT ?? "", + vlot_trust = visdataC?.VLOT_Trust ?? false, + mfgdate = visdataC?.MFGDATE ?? "", + mfgdate_trust = visdataC?.MFGDATE_Trust ?? false, + partno = visdataC?.PARTNO ?? "", + partno_trust = visdataC?.PARTNO_Trust ?? false, + reelSize = visdataC?.ReelSize.ToString() ?? "--", + batch = visdataC?.BATCH ?? "", + qtymax = visdataC?.QTYMAX ?? "", + barcodeCount = visdataC?.barcodelist?.Count ?? 0, + regexCount = PUB.Result.BCDPattern?.Count ?? 0 + }, + right = new + { + cartSize = DIO.getCartSize(2).ToString(), + enabled = PUB.flag.get(eVarBool.FG_ENABLE_RIGHT), + rid = visdataR?.RID ?? "", + sid = visdataR?.SID ?? "", + qty = visdataR?.QTY ?? "", + vname = visdataR?.VNAME ?? "", + vlot = visdataR?.VLOT ?? "", + mfgdate = visdataR?.MFGDATE ?? "", + partno = visdataR?.PARTNO ?? "", + reelSize = visdataR?.ReelSize.ToString() ?? "--", + batch = visdataR?.BATCH ?? "", + rid2 = visdataR?.RID2 ?? "", + sid2 = visdataR?.SID2 ?? "", + qty2 = visdataR?.QTY2 ?? "", + vname2 = visdataR?.VNAME2 ?? "", + vlot2 = visdataR?.VLOT2 ?? "", + mfgdate2 = visdataR?.MFGDATE2 ?? "", + partno2 = visdataR?.PARTNO2 ?? "" + } + }; + + return JsonConvert.SerializeObject(result); + } + catch (Exception ex) + { + Console.WriteLine($"[ERROR] Failed to get vision data: {ex.Message}"); + return JsonConvert.SerializeObject(new { left = new { }, center = new { }, right = new { } }); + } + } + /// /// ์ธํ„ฐ๋ฝ ๋ชฉ๋ก ์กฐํšŒ (์‹ค์‹œ๊ฐ„ ๊ฐ’ ํฌํ•จ) /// diff --git a/Handler/Project/fMain.cs b/Handler/Project/fMain.cs index 5b8bfa1..b0c8ee3 100644 --- a/Handler/Project/fMain.cs +++ b/Handler/Project/fMain.cs @@ -1651,7 +1651,7 @@ namespace Project var exename = UTIL.MakePath("LogView.exe"); if (System.IO.File.Exists(exename) == false) { - UTIL.MsgE("Log viewer file not found\nPlease contact support (T8567)"); + UTIL.MsgE($"Log viewer file not found\n{exename}\nPlease contact support (T8567)"); return; }