From 73d2cc8c739ede59b297a244bfc552f584205df3 Mon Sep 17 00:00:00 2001 From: Gomathi9520 Date: Mon, 2 Jun 2025 14:37:09 +0530 Subject: [PATCH] Implement AGV, Conveyor, and Robotic Arm simulations with IIOT integration --- app/src/modules/IIOTTemp/AgvSimulation.tsx | 146 +++++++++++ app/src/modules/IIOTTemp/ArmAnimator.tsx | 246 ++++++++++++++++++ app/src/modules/IIOTTemp/CanvasComponent.tsx | 152 +++++++++++ .../modules/IIOTTemp/ConveyorSimulation.tsx | 152 +++++++++++ app/src/modules/IIOTTemp/IIotIkSolver.tsx | 94 +++++++ .../modules/IIOTTemp/MachineSimulation.tsx | 52 ++++ app/src/modules/IIOTTemp/MqttListener.tsx | 82 ++++++ .../modules/IIOTTemp/RealTimeSimulation.tsx | 38 +++ .../modules/IIOTTemp/RoboticArmSimulation.tsx | 109 ++++++++ app/src/modules/scene/scene.tsx | 44 ++++ app/src/store/iiot/iiotmqttConnectionStore.ts | 49 ++++ 11 files changed, 1164 insertions(+) create mode 100644 app/src/modules/IIOTTemp/AgvSimulation.tsx create mode 100644 app/src/modules/IIOTTemp/ArmAnimator.tsx create mode 100644 app/src/modules/IIOTTemp/CanvasComponent.tsx create mode 100644 app/src/modules/IIOTTemp/ConveyorSimulation.tsx create mode 100644 app/src/modules/IIOTTemp/IIotIkSolver.tsx create mode 100644 app/src/modules/IIOTTemp/MachineSimulation.tsx create mode 100644 app/src/modules/IIOTTemp/MqttListener.tsx create mode 100644 app/src/modules/IIOTTemp/RealTimeSimulation.tsx create mode 100644 app/src/modules/IIOTTemp/RoboticArmSimulation.tsx create mode 100644 app/src/store/iiot/iiotmqttConnectionStore.ts diff --git a/app/src/modules/IIOTTemp/AgvSimulation.tsx b/app/src/modules/IIOTTemp/AgvSimulation.tsx new file mode 100644 index 0000000..b9fed17 --- /dev/null +++ b/app/src/modules/IIOTTemp/AgvSimulation.tsx @@ -0,0 +1,146 @@ +import { useFrame, useThree } from '@react-three/fiber'; +import React, { useEffect, useRef, useState } from 'react'; +import * as THREE from 'three'; +import useModuleStore from '../../store/useModuleStore'; +import { MaterialModel } from '../simulation/materials/instances/material/materialModel'; + +type AgvSimulationProps = { + data: any; + assetName: string; + assetUUID: string; +}; + +function AgvSimulation({ data, assetName, assetUUID }: AgvSimulationProps) { + const { scene } = useThree(); + const { activeModule } = useModuleStore(); + const materialRef = useRef(null); + const startPosRef = useRef(null); + const endPosRef = useRef(null); + const quaternionRef = useRef(null); + const offsetRef = useRef(null); // store material offset + const startTimeRef = useRef(null); + const agvRef = useRef(null); + + const duration = 5000; // 5 seconds + + const lastPercentageRef = useRef(0); + const targetPercentageRef = useRef(0); + const interpolationStartTimeRef = useRef(performance.now()); + const [isRunning, setIsRunning] = useState(false); + + + useEffect(() => { + if (activeModule !== 'visualization') return; + const AgvObject = scene.getObjectByProperty('uuid', assetUUID); + if (!AgvObject) return; + agvRef.current = AgvObject; + + AgvObject.updateWorldMatrix(true, false); + + const worldPosition = new THREE.Vector3(); + AgvObject.getWorldPosition(worldPosition); + + const worldQuaternion = new THREE.Quaternion(); + AgvObject.getWorldQuaternion(worldQuaternion); + + const offsetStart = new THREE.Vector3(0, 0.9, 0).applyQuaternion(worldQuaternion); // offset above AGV + offsetRef.current = offsetStart; + + const materialPosition = worldPosition.clone().add(offsetStart); + + // Set start and end positions + const startPosition = new THREE.Vector3(5.24076846034801, 0, -46.23525167395107); + const endPosition = new THREE.Vector3(10.24076846034801, 0, -50.23525167395107); + startPosRef.current = startPosition; + endPosRef.current = endPosition; + quaternionRef.current = worldQuaternion; + + startTimeRef.current = performance.now(); + + // Place material initially + if (materialRef.current) { + materialRef.current.position.copy(materialPosition); + materialRef.current.quaternion.copy(worldQuaternion); + } + }, [activeModule, assetUUID, scene]); + + useFrame(() => { + if (data.assetName !== assetName || data.event !== 'move') return; + if (!agvRef.current || !startPosRef.current || !endPosRef.current || !startTimeRef.current || !isRunning) + return; + const now = performance.now(); + const elapsed = now - interpolationStartTimeRef.current; + const duration = 2000; + const t = Math.min(elapsed / duration, 1); + const interpolatedPercentage = + lastPercentageRef.current + + (targetPercentageRef.current - lastPercentageRef.current) * t; + const progress = Math.min(interpolatedPercentage / 100, 1); + const currentPos = new THREE.Vector3().lerpVectors(startPosRef.current, endPosRef.current, progress); + // if (elapsed > duration) { + // agvRef.current.position.copy(endPosRef.current); + // if (materialRef.current && offsetRef.current) { + // materialRef.current.position.copy(endPosRef.current.clone().add(offsetRef.current)); + // } + // return; + // } + // const currentPos = new THREE.Vector3().lerpVectors(startPosRef.current, endPosRef.current, t); + agvRef.current.position.copy(currentPos); + if (materialRef.current && offsetRef.current) { + materialRef.current.position.copy(currentPos.clone().add(offsetRef.current)); + } + console.log(` ${assetName} is running at ${data.percentage}%`); + }); + + useEffect(() => { + if (data.assetName !== assetName) return; + if (data.state === 'running' && data.percentage !== undefined) { + + // Reset to start if restarting + if (data.percentage === 0) { + lastPercentageRef.current = 0; + targetPercentageRef.current = 0; + + if (startPosRef.current && materialRef.current && quaternionRef.current) { + materialRef.current.position.copy(startPosRef.current); + materialRef.current.quaternion.copy(quaternionRef.current); + } + } else { + lastPercentageRef.current = targetPercentageRef.current; + } + targetPercentageRef.current = data.percentage; + interpolationStartTimeRef.current = performance.now(); + setIsRunning(true); + } else { + + setIsRunning(false); + } + }, [data, assetName]); + const [isVisible, setIsVisible] = useState(false); + useEffect(() => { + if (data.assetName !== assetName) return; + if (data.state === 'running' && typeof data.percentage === 'number') { + const shouldBeVisible = data.percentage <= 100; + // Only update visibility if it changes + setIsVisible(prev => { + if (prev !== shouldBeVisible) { + return shouldBeVisible; + } + return prev; + }); + } else { + setIsVisible(false); + } + }, [data, assetName]); + + return ( + + ); +} + +export default AgvSimulation; diff --git a/app/src/modules/IIOTTemp/ArmAnimator.tsx b/app/src/modules/IIOTTemp/ArmAnimator.tsx new file mode 100644 index 0000000..3a1a38c --- /dev/null +++ b/app/src/modules/IIOTTemp/ArmAnimator.tsx @@ -0,0 +1,246 @@ +import { useFrame, useThree } from '@react-three/fiber'; +import React, { useEffect, useRef, useState } from 'react' +import * as THREE from "three"; +import { useAnimationPlaySpeed } from '../../store/usePlayButtonStore'; + +type PointWithDegree = { + position: [number, number, number]; + degree: number; +}; + +function ArmAnimator({ armBot, ikSolver, setIkSolver, targetBone, restPosition, path }: any) { + const { scene } = useThree(); + const progressRef = useRef(0); + const curveRef = useRef(null); + const totalDistanceRef = useRef(0); + const startTimeRef = useRef(null); + const segmentDistancesRef = useRef([]); + const [currentPath, setCurrentPath] = useState<[number, number, number][]>([]); + const [circlePoints, setCirclePoints] = useState<[number, number, number][]>([]); + const [circlePointsWithDegrees, setCirclePointsWithDegrees] = useState([]); + const [customCurvePoints, setCustomCurvePoints] = useState(null); + let curveHeight = 1.75 + const CIRCLE_RADIUS = 1.6 + const { speed } = useAnimationPlaySpeed(); + let duration = 5000 + + useEffect(() => { + + console.log('path: ', path); + setCurrentPath(path); + }, [path]); + + // Handle circle points based on armBot position + useEffect(() => { + const points = generateRingPoints(CIRCLE_RADIUS, 64) + setCirclePoints(points); + }, [armBot.position]); + + //Generate Circle Points + function generateRingPoints(radius: any, segments: any) { + const points: [number, number, number][] = []; + for (let i = 0; i < segments; i++) { + // Calculate angle for current segment + const angle = (i / segments) * Math.PI * 2; + // Calculate x and z coordinates (y remains the same for a flat ring) + const x = Math.cos(angle) * radius; + const z = Math.sin(angle) * radius; + points.push([x, 1.5, z]); + } + return points; + } + + //Generate CirclePoints with Angle + function generateRingPointsWithDegrees(radius: number, segments: number, initialRotation: [number, number, number]) { + const points: { position: [number, number, number]; degree: number }[] = []; + for (let i = 0; i < segments; i++) { + const angleRadians = (i / segments) * Math.PI * 2; + const x = Math.cos(angleRadians) * radius; + const z = Math.sin(angleRadians) * radius; + const degree = (angleRadians * 180) / Math.PI; // Convert radians to degrees + points.push({ + position: [x, 1.5, z], + degree, + }); + } + return points; + } + // Handle circle points based on armBot position + useEffect(() => { + const points = generateRingPointsWithDegrees(CIRCLE_RADIUS, 64, armBot.rotation); + setCirclePointsWithDegrees(points) + }, [armBot.rotation]); + + // Function for find nearest Circlepoints Index + const findNearestIndex = (nearestPoint: [number, number, number], points: [number, number, number][], epsilon = 1e-6) => { + for (let i = 0; i < points.length; i++) { + const [x, y, z] = points[i]; + if ( + Math.abs(x - nearestPoint[0]) < epsilon && + Math.abs(y - nearestPoint[1]) < epsilon && + Math.abs(z - nearestPoint[2]) < epsilon + ) { + return i; // Found the matching index + } + } + return -1; // Not found + }; + + //function to find nearest Circlepoints + const findNearest = (target: [number, number, number]) => { + return circlePoints.reduce((nearest, point) => { + const distance = Math.hypot(target[0] - point[0], target[1] - point[1], target[2] - point[2]); + const nearestDistance = Math.hypot(target[0] - nearest[0], target[1] - nearest[1], target[2] - nearest[2]); + return distance < nearestDistance ? point : nearest; + }, circlePoints[0]); + }; + + // Helper function to collect points and check forbidden degrees + const collectArcPoints = (startIdx: number, endIdx: number, clockwise: boolean) => { + const totalSegments = 64; + const arcPoints: [number, number, number][] = []; + let i = startIdx; + + while (i !== (endIdx + (clockwise ? 1 : -1) + totalSegments) % totalSegments) { + const { degree, position } = circlePointsWithDegrees[i]; + // Skip over + arcPoints.push(position); + i = (i + (clockwise ? 1 : -1) + totalSegments) % totalSegments; + } + return arcPoints; + }; + + //Range to restrict angle + const hasForbiddenDegrees = (arc: [number, number, number][]) => { + return arc.some(p => { + const idx = findNearestIndex(p, circlePoints); + const degree = circlePointsWithDegrees[idx]?.degree || 0; + return degree >= 271 && degree <= 300; // Forbidden range: 271° to 300° + }); + }; + + // Handle nearest points and final path (including arc points) + useEffect(() => { + if (circlePoints.length > 0 && currentPath.length > 0) { + + const start = currentPath[0]; + const end = currentPath[currentPath.length - 1]; + + const raisedStart = [start[0], start[1] + 0.5, start[2]] as [number, number, number]; + + const raisedEnd = [end[0], end[1] + 0.5, end[2]] as [number, number, number]; + + + const nearestToStart = findNearest(raisedStart); + const nearestToEnd = findNearest(raisedEnd); + + const indexOfNearestStart = findNearestIndex(nearestToStart, circlePoints); + const indexOfNearestEnd = findNearestIndex(nearestToEnd, circlePoints); + + const totalSegments = 64; + const clockwiseDistance = (indexOfNearestEnd - indexOfNearestStart + totalSegments) % totalSegments; + const counterClockwiseDistance = (indexOfNearestStart - indexOfNearestEnd + totalSegments) % totalSegments; + + // Try both directions + const arcClockwise = collectArcPoints(indexOfNearestStart, indexOfNearestEnd, true); + const arcCounterClockwise = collectArcPoints(indexOfNearestStart, indexOfNearestEnd, false); + + const clockwiseForbidden = hasForbiddenDegrees(arcClockwise); + const counterClockwiseForbidden = hasForbiddenDegrees(arcCounterClockwise); + + let arcPoints: [number, number, number][] = []; + + if (!clockwiseForbidden && (clockwiseDistance <= counterClockwiseDistance || counterClockwiseForbidden)) { + arcPoints = arcClockwise; + } else { + arcPoints = arcCounterClockwise; + } + + const pathVectors = [ + new THREE.Vector3(start[0], start[1], start[2]), + new THREE.Vector3(start[0], curveHeight, start[2]), + new THREE.Vector3(nearestToStart[0], curveHeight, nearestToStart[2]), + ...arcPoints.map(point => new THREE.Vector3(point[0], curveHeight, point[2])), + new THREE.Vector3(nearestToEnd[0], curveHeight, nearestToEnd[2]), + new THREE.Vector3(end[0], curveHeight, end[2]), + new THREE.Vector3(end[0], end[1], end[2]) + ]; + + const pathSegments: [THREE.Vector3, THREE.Vector3][] = []; + for (let i = 0; i < pathVectors.length - 1; i++) { + pathSegments.push([pathVectors[i], pathVectors[i + 1]]); + } + + const segmentDistances = pathSegments.map(([p1, p2]) => p1.distanceTo(p2)); + segmentDistancesRef.current = segmentDistances; + const totalDistance = segmentDistances.reduce((sum, d) => sum + d, 0); + totalDistanceRef.current = totalDistance; + + + setCustomCurvePoints(pathVectors); + } + }, [circlePoints, currentPath]); + + // Frame update for animation + useFrame((state, delta) => { + if (!ikSolver || !customCurvePoints || customCurvePoints.length === 0) return; + + const bone = ikSolver.mesh.skeleton.bones.find((b: any) => b.name === targetBone); + if (!bone) return; + + const distances = segmentDistancesRef.current; + const totalDistance = totalDistanceRef.current; + + // Initialize start time + if (startTimeRef.current === null) { + startTimeRef.current = state.clock.elapsedTime; + } + + const elapsed = (state.clock.elapsedTime - startTimeRef.current) * 1000; // ms + const clampedProgress = Math.min(elapsed / duration, 1); // progress [0,1] + const coveredDistance = clampedProgress * totalDistance; + + // Traverse segments to find current position + let index = 0; + let accumulatedDistance = 0; + + while (index < distances.length && coveredDistance > accumulatedDistance + distances[index]) { + accumulatedDistance += distances[index]; + index++; + } + + if (index < distances.length) { + const startPoint = customCurvePoints[index]; + const endPoint = customCurvePoints[index + 1]; + const segmentDistance = distances[index]; + const t = (coveredDistance - accumulatedDistance) / segmentDistance; + + if (startPoint && endPoint) { + const position = startPoint.clone().lerp(endPoint, t); + bone.position.copy(position); + } + } + + ikSolver.update(); + + // Reset at the end + if (clampedProgress >= 1) { + setCurrentPath([]); + setCustomCurvePoints([]); + curveRef.current = null; + progressRef.current = 0; + startTimeRef.current = null; + } + if (currentPath.length === 0 && bone) { + bone.position.copy(bone.position); + ikSolver.update(); + } + }); + + + return ( + <> + ) +} + +export default ArmAnimator diff --git a/app/src/modules/IIOTTemp/CanvasComponent.tsx b/app/src/modules/IIOTTemp/CanvasComponent.tsx new file mode 100644 index 0000000..527251f --- /dev/null +++ b/app/src/modules/IIOTTemp/CanvasComponent.tsx @@ -0,0 +1,152 @@ +import React, { useMemo, useEffect, useState } from 'react'; +import { Canvas } from '@react-three/fiber'; +import { Environment, OrbitControls } from '@react-three/drei'; +import * as THREE from 'three'; +import * as CONSTANTS from '../../types/world/worldConstants'; +import { useTileDistance, useToggleView } from '../../store/builder/store'; +import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; +import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'; +import { Vector3, Euler } from 'three'; +import background from "../../assets/textures/hdr/mudroadpuresky2k.hdr"; + +export default function CanvasComponent() { + const { toggleView } = useToggleView(); + const { gridValue, planeValue } = useTileDistance(); + const url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`; + + const assets = [ + { + modelName: 'conveyor', + modelUuid: '187988af-3d50-46e7-b9d2-211465b84fc0', + modelfileID: '7dc04e36882e4debbc1a8e3d', + position: [-10.293992719387735, 0, 3.039958827038974], + rotation: { x: Math.PI, y: -1.564219187623611, z: Math.PI }, + }, + { + modelName: 'robotic arm 4 axis', + modelUuid: '6c666c95-7d7c-47b0-9d2c-c26a95eaba90', + modelfileID: '52e6681fbb743a890d96c914', + position: [-7.226814829418026, 0, 1.7900395767061474], + rotation: { x: 0, y: 0, z: 0 }, + }, + { + modelName: 'vertical machining center 5 axis', + modelUuid: 'c05dc5df-64ff-4597-ba64-153573e80a9b', + modelfileID: '29dee78715ad5b853f5c346d', + position: [-7.049029908111559, 0, -0.7406233295906898], + rotation: { x: 0, y: 0, z: 0 }, + }, + { + modelName: 'conveyor', + modelUuid: 'd56bb1aa-219f-45d1-8204-6f5a73ee0ab2', + modelfileID: '7dc04e36882e4debbc1a8e3d', + position: [-3.8644953037273306, 0, 3.0765845401995224], + rotation: { x: Math.PI, y: -1.564219187623611, z: Math.PI }, + }, + { + modelName: 'agv', + modelUuid: 'bf59f396-58ff-4775-80a3-2a037eb4bf49', + modelfileID: 'a1ee92554935007b10b3eb05', + position: [2, 0, 5.5], + rotation: { x: 0, y: 0, z: 0 }, + }, + ]; + + const [loadedAssets, setLoadedAssets] = useState([]); + + useEffect(() => { + const loader = new GLTFLoader(); + const dracoLoader = new DRACOLoader(); + dracoLoader.setDecoderPath('https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/libs/draco/gltf/'); + loader.setDRACOLoader(dracoLoader); + + const loadAllAssets = async () => { + const promises = assets.map((asset) => { + const url = `${url_Backend_dwinzo}/api/v2/AssetFile/${asset.modelfileID}`; + return new Promise((resolve, reject) => { + loader.load( + url, + (gltf) => { + gltf.scene.position.set(...asset.position as [number, number, number]); + gltf.scene.rotation.set( + asset.rotation.x, + asset.rotation.y, + asset.rotation.z + ); + resolve({ + ...asset, + scene: gltf.scene, + }); + }, + undefined, + reject + ); + }); + }); + + const results = await Promise.allSettled(promises); + const successfulAssets = results + .filter((r): r is PromiseFulfilledResult => r.status === 'fulfilled') + .map((r) => r.value); + + setLoadedAssets(successfulAssets); + }; + + loadAllAssets(); + }, []); + + const gridHelper = useMemo(() => { + return new THREE.GridHelper( + gridValue.size, + gridValue.divisions, + CONSTANTS.gridConfig.primaryColor, + CONSTANTS.gridConfig.secondaryColor + ); + }, [gridValue.size, gridValue.divisions]); + + return ( + e.preventDefault()} + camera={{ position: [10, 10, 10], fov: 50 }} + > + + + + {/* Grid */} + + + + + + + + + + + {/* Loaded Models */} + {loadedAssets.map((asset, index) => ( + + ))} + + ); +} diff --git a/app/src/modules/IIOTTemp/ConveyorSimulation.tsx b/app/src/modules/IIOTTemp/ConveyorSimulation.tsx new file mode 100644 index 0000000..cf2d7f0 --- /dev/null +++ b/app/src/modules/IIOTTemp/ConveyorSimulation.tsx @@ -0,0 +1,152 @@ +import React, { useEffect, useRef, useState } from 'react'; +import * as THREE from 'three'; +import useMqttConnectionStore from '../../store/iiot/iiotmqttConnectionStore'; +import { useFrame, useThree } from '@react-three/fiber'; +import useModuleStore from '../../store/useModuleStore'; +import { MaterialModel } from '../simulation/materials/instances/material/materialModel'; + +type ConveyorSimulationProps = { + data: any; + assetName: string; + assetUUID: string; +}; + +function ConveyorSimulation({ data, assetName, assetUUID }: ConveyorSimulationProps) { + const { connect } = useMqttConnectionStore(); + const { scene } = useThree(); + const { activeModule } = useModuleStore(); + const materialRef = useRef(null); + const [isRunning, setIsRunning] = useState(false); + + const startPosRef = useRef(null); + const endPosRef = useRef(null); + const quaternionRef = useRef(null); + const startTimeRef = useRef(null); + + const lastPercentageRef = useRef(0); + const targetPercentageRef = useRef(0); + const interpolationStartTimeRef = useRef(performance.now()); + + // Initialize conveyor positions + useEffect(() => { + if (activeModule !== 'visualization') return; + connect(); + + const conveyorObject = scene.getObjectByProperty('uuid', assetUUID); + if (!conveyorObject) return; + + conveyorObject.updateWorldMatrix(true, false); + + const worldPosition = new THREE.Vector3(); + conveyorObject.getWorldPosition(worldPosition); + + const worldQuaternion = new THREE.Quaternion(); + conveyorObject.getWorldQuaternion(worldQuaternion); + + const offsetStart = new THREE.Vector3(0, 0.9, 2.3).applyQuaternion(worldQuaternion); + const offsetEnd = new THREE.Vector3(0, 0.9, -2.3).applyQuaternion(worldQuaternion); + + const startPosition = worldPosition.clone().add(offsetStart); + const endPosition = worldPosition.clone().add(offsetEnd); + + startPosRef.current = startPosition; + endPosRef.current = endPosition; + quaternionRef.current = worldQuaternion; + startTimeRef.current = performance.now(); + + if (materialRef.current) { + materialRef.current.position.copy(startPosition); + materialRef.current.quaternion.copy(worldQuaternion); + } + }, [activeModule, assetUUID]); + + // Animate movement + useFrame(() => { + if (data.assetName !== assetName || data.event !== 'move') return; + if ( + !startPosRef.current || + !endPosRef.current || + !materialRef.current || + !quaternionRef.current || + !isRunning + ) return; + + const now = performance.now(); + const elapsed = now - interpolationStartTimeRef.current; + const duration = 2000; // interpolate every 2s + const t = Math.min(elapsed / duration, 1); + + const interpolatedPercentage = + lastPercentageRef.current + + (targetPercentageRef.current - lastPercentageRef.current) * t; + + const progress = Math.min(interpolatedPercentage / 100, 1); + const currentPos = new THREE.Vector3().lerpVectors(startPosRef.current, endPosRef.current, progress); + + materialRef.current.position.copy(currentPos); + materialRef.current.quaternion.copy(quaternionRef.current); + console.log(` ${assetName} is running at ${data.percentage}%`); + }); + + // Update movement state + useEffect(() => { + // + if (data.assetName !== assetName) return; + + if (data.state === 'running' && data.percentage !== undefined) { + + // Reset to start if restarting + if (data.percentage === 0) { + lastPercentageRef.current = 0; + targetPercentageRef.current = 0; + + if (startPosRef.current && materialRef.current && quaternionRef.current) { + materialRef.current.position.copy(startPosRef.current); + materialRef.current.quaternion.copy(quaternionRef.current); + } + } else { + lastPercentageRef.current = targetPercentageRef.current; + } + + targetPercentageRef.current = data.percentage; + interpolationStartTimeRef.current = performance.now(); + setIsRunning(true); + } else { + + setIsRunning(false); + } + }, [data, assetName]); + + const [isVisible, setIsVisible] = useState(false); + useEffect(() => { + if (data.assetName !== assetName) return; + + if (data.state === 'running' && typeof data.percentage === 'number') { + const shouldBeVisible = data.percentage <= 100; + // Only update visibility if it changes + setIsVisible(prev => { + if (prev !== shouldBeVisible) { + return shouldBeVisible; + } + return prev; + }); + } else { + setIsVisible(false); + } + }, [data, assetName]); + + return ( + <> + + + + ); + +} + +export default ConveyorSimulation; diff --git a/app/src/modules/IIOTTemp/IIotIkSolver.tsx b/app/src/modules/IIOTTemp/IIotIkSolver.tsx new file mode 100644 index 0000000..7ba6cd2 --- /dev/null +++ b/app/src/modules/IIOTTemp/IIotIkSolver.tsx @@ -0,0 +1,94 @@ +import { useLoader, useThree } from '@react-three/fiber'; +import React, { useEffect, useMemo, useState } from 'react' +import { CCDIKSolver, CCDIKHelper, } from "three/examples/jsm/animation/CCDIKSolver"; +import * as THREE from "three"; +import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader"; +import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader"; +import { clone } from "three/examples/jsm/utils/SkeletonUtils"; + +type IKInstanceProps = { + modelUrl: string; + ikSolver: any; + setIkSolver: any + armBot: any; + groupRef: any; +}; + +function IIotIkSolver({ modelUrl, setIkSolver, ikSolver, armBot, groupRef }: IKInstanceProps) { + const { scene } = useThree() + const gltf = useLoader(GLTFLoader, modelUrl, (loader) => { + const draco = new DRACOLoader(); + draco.setDecoderPath("https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/libs/draco/"); + loader.setDRACOLoader(draco); + }); + const cloned = useMemo(() => clone(gltf?.scene), [gltf]); + const targetBoneName = "Target"; + const skinnedMeshName = "link_0"; + const [selectedArm, setSelectedArm] = useState(); + + useEffect(() => { + + if (!gltf) return; + const OOI: any = {}; + cloned.traverse((n: any) => { + if (n.name === targetBoneName) OOI.Target_Bone = n; + if (n.name === skinnedMeshName) OOI.Skinned_Mesh = n; + }); + if (!OOI.Target_Bone || !OOI.Skinned_Mesh) return; + const iks = [ + { + target: 7, + effector: 6, + links: [ + { + index: 5, + enabled: true, + rotationMin: new THREE.Vector3(-Math.PI / 2, 0, 0), + rotationMax: new THREE.Vector3(Math.PI / 2, 0, 0), + }, + { + index: 4, + enabled: true, + rotationMin: new THREE.Vector3(-Math.PI / 2, 0, 0), + rotationMax: new THREE.Vector3(0, 0, 0), + }, + { + index: 3, + enabled: true, + rotationMin: new THREE.Vector3(0, 0, 0), + rotationMax: new THREE.Vector3(2, 0, 0), + }, + { index: 1, enabled: true, limitation: new THREE.Vector3(0, 1, 0) }, + { index: 0, enabled: false, limitation: new THREE.Vector3(0, 0, 0) }, + ], + }, + ]; + + const solver = new CCDIKSolver(OOI.Skinned_Mesh, iks); + setIkSolver(solver); + + const helper = new CCDIKHelper(OOI.Skinned_Mesh, iks, 0.05) + + setSelectedArm(OOI.Target_Bone); + + scene.add(helper); + + }, [cloned, gltf, setIkSolver]); + + return ( + <> + {armBot.position && { + setSelectedArm(groupRef.current?.getObjectByName(targetBoneName)) + }}> + + } + + ) +} + +export default IIotIkSolver diff --git a/app/src/modules/IIOTTemp/MachineSimulation.tsx b/app/src/modules/IIOTTemp/MachineSimulation.tsx new file mode 100644 index 0000000..ad5d9a9 --- /dev/null +++ b/app/src/modules/IIOTTemp/MachineSimulation.tsx @@ -0,0 +1,52 @@ +import { useFrame, useThree } from '@react-three/fiber'; +import React, { useEffect, useRef, useState } from 'react'; +import useModuleStore from '../../store/useModuleStore'; + +type MachineSimulationProps = { + data: any; + assetName: string; + assetUUID: string; +}; + +function MachineSimulation({ data, assetName, assetUUID }: MachineSimulationProps) { + const { scene } = useThree(); + const { activeModule } = useModuleStore(); + + const percentageRef = useRef(0); + const [isRunning, setIsRunning] = useState(false); + + // Setup: Track machine if needed (optional) + useEffect(() => { + if (activeModule !== 'visualization') return; + scene.getObjectByProperty('uuid', assetUUID); // Optional: could store ref if needed + }, [activeModule, assetUUID, scene]); + + // Update internal state when data changes + useEffect(() => { + if (activeModule !== 'visualization') return; + if (data.state === 'running' && data.percentage !== undefined) { + + percentageRef.current = data.percentage; + setIsRunning(true); + } else { + + setIsRunning(false); + } + }, [data, assetName]); + + // Frame loop (logic-only, no visuals) + useFrame(() => { + if (data.assetName !== assetName || data.event !== 'move') return; + + if (!isRunning) return; + + const percentage = percentageRef.current; + + // 🟢 Use percentage in logic if needed + console.log(` ${assetName} is running at ${data.percentage}%`); + }); + + return null; +} + +export default MachineSimulation; diff --git a/app/src/modules/IIOTTemp/MqttListener.tsx b/app/src/modules/IIOTTemp/MqttListener.tsx new file mode 100644 index 0000000..8db00b7 --- /dev/null +++ b/app/src/modules/IIOTTemp/MqttListener.tsx @@ -0,0 +1,82 @@ +import React, { useEffect, useState } from 'react'; +import useMqttConnectionStore from '../../store/iiot/iiotmqttConnectionStore'; +import { IClientSubscribeOptions } from 'mqtt'; + + +interface MqttListenerProps { + setData: (data: any) => void; +} + +const MqttListener: React.FC = ({ setData }) => { + const { client } = useMqttConnectionStore(); + const [message, setMessage] = useState(''); + + useEffect(() => { + if (!client) return; + + const topic = 'conveyor0001/0001/status'; + const topic2 = 'conveyor0002/0001/status'; + const topic3 = 'cnc0001/0001/status' + const topic4 = 'agv0001/0001/status' + + client.subscribe(topic, {} as IClientSubscribeOptions, (err) => { + if (err) { + console.error('Subscription error:', err); + } + }); + client.subscribe(topic2, {} as IClientSubscribeOptions, (err) => { + if (err) { + console.error('Subscription error:', err); + } + }); + client.subscribe(topic3, {} as IClientSubscribeOptions, (err) => { + if (err) { + console.error('Subscription error:', err); + } + }); + client.subscribe(topic4, {} as IClientSubscribeOptions, (err) => { + if (err) { + console.error('Subscription error:', err); + } + }); + + const handleMessage = (receivedTopic: string, payload: Buffer) => { + if (receivedTopic === topic) { + const msgStr = payload.toString(); + setData(JSON.parse(msgStr)) + // setMessage(msgStr); + } + if (receivedTopic === topic2) { + const msgStr = payload.toString(); + setData(JSON.parse(msgStr)) + // setMessage(msgStr); + } + if (receivedTopic === topic3) { + const msgStr = payload.toString(); + setData(JSON.parse(msgStr)) + + // setMessage(msgStr); + } + if (receivedTopic === topic4) { + const msgStr = payload.toString(); + setData(JSON.parse(msgStr)) + + // setMessage(msgStr); + } + }; + client.on('message', handleMessage); + + return () => { + client.off('message', handleMessage); + client.unsubscribe(topic); + }; + }, [client]); + + return ( + <> + + + ); +}; + +export default MqttListener; diff --git a/app/src/modules/IIOTTemp/RealTimeSimulation.tsx b/app/src/modules/IIOTTemp/RealTimeSimulation.tsx new file mode 100644 index 0000000..160bb92 --- /dev/null +++ b/app/src/modules/IIOTTemp/RealTimeSimulation.tsx @@ -0,0 +1,38 @@ + +//singleConveyor +import { useEffect, useState } from 'react'; +import MqttListener from './MqttListener'; +import ConveyorSimulation from './ConveyorSimulation'; +import MachineSimulation from './MachineSimulation'; +import AgvSimulation from './AgvSimulation'; +import RoboticArmSimulation from './RoboticArmSimulation'; + +type MqttData = { + state?: string; + remainingTime?: number; + percentage?: number; + assetName?: string; + event?: string; +}; + +function RealTimeSimulation() { + const [data, setData] = useState({}); + + useEffect(() => { + }, [data]) + + return ( + <> + + + + + + + + + ); +} + +export default RealTimeSimulation; + diff --git a/app/src/modules/IIOTTemp/RoboticArmSimulation.tsx b/app/src/modules/IIOTTemp/RoboticArmSimulation.tsx new file mode 100644 index 0000000..f7fe22a --- /dev/null +++ b/app/src/modules/IIOTTemp/RoboticArmSimulation.tsx @@ -0,0 +1,109 @@ +import React, { useEffect, useRef, useState } from "react"; +import useModuleStore from "../../store/useModuleStore"; +import { useThree } from "@react-three/fiber"; +import IIotIkSolver from "./IIotIkSolver"; +import armModel from "../../assets/gltf-glb/rigged/ik_arm_1.glb"; +import * as THREE from "three"; +import ArmAnimator from "./ArmAnimator"; +import { useFloorItems } from "../../store/builder/store"; + +type ArmBotSimulationProps = { + data: any; + assetName: string; + assetUUID: string; +}; + +function RoboticArmSimulation({ + data, + assetName, + assetUUID, +}: ArmBotSimulationProps) { + const [ikSolver, setIkSolver] = useState(null); + const { scene } = useThree(); + const { activeModule } = useModuleStore(); + const groupRef = useRef(null); + const [armBot, setArmBot] = useState(null); + const targetBone = "Target"; + const restPosition = new THREE.Vector3(0, 1.75, -1.6); + const [path, setPath] = useState<[number, number, number][]>([]); + + useEffect(() => { + if (activeModule !== "visualization") return; + if (assetUUID) { + const targetMesh = scene?.getObjectByProperty("uuid", assetUUID); + if (targetMesh) { + targetMesh.visible = activeModule !== "visualization" + } + } + const RoboticArmObject = scene.getObjectByProperty("uuid", assetUUID); + if (!RoboticArmObject) return; + const updatedArmBot = { + modelUuid: assetUUID, + modelName: assetName, + position: [RoboticArmObject.position.x, RoboticArmObject.position.y, RoboticArmObject.position.z,], + rotation: [RoboticArmObject.rotation.x, RoboticArmObject.rotation.y, RoboticArmObject.rotation.z,], + }; + + + setArmBot(updatedArmBot); + }, [scene, assetUUID, activeModule]); + useEffect(() => { + if (!ikSolver) return; + + const targetBones = ikSolver?.mesh.skeleton.bones.find((b: any) => b.name === targetBone); + if (!targetBones) return; + + const startPoint = new THREE.Vector3(-0.050142171738890684, 1.9, -1.999371341850562); + const endPoint = new THREE.Vector3(1.9999942666761656, 1.9, 0.004788868599774787); + + const curve1 = createCurveBetweenTwoPoints(targetBones.position.clone(), restPosition.clone()); + const curve2 = createCurveBetweenTwoPoints(restPosition.clone(), startPoint.clone()); + const curve3 = createCurveBetweenTwoPoints(startPoint.clone(), endPoint.clone()); + const curve4 = createCurveBetweenTwoPoints(endPoint.clone(), restPosition.clone()); + + const curveList = [curve1, curve2, curve3, curve4]; + + let allPoints: [number, number, number][] = []; + + curveList.forEach((curve, index) => { + setTimeout(() => { + if (curve) { + setPath(curve.points.map(point => [point.x, point.y, point.z])); + } + }, index * 5000); // 2 seconds delay between each curve + }); + }, [ikSolver]); + + + + function createCurveBetweenTwoPoints(p1: any, p2: any) { + const mid = new THREE.Vector3().addVectors(p1, p2).multiplyScalar(0.5); + const points = [p1, mid, p2]; + return new THREE.CatmullRomCurve3(points); + } + return ( + <> + {activeModule === "visualization" && armBot && ( + <> + + + + )} + + ); +} + +export default RoboticArmSimulation; diff --git a/app/src/modules/scene/scene.tsx b/app/src/modules/scene/scene.tsx index d8d3447..3d5df6a 100644 --- a/app/src/modules/scene/scene.tsx +++ b/app/src/modules/scene/scene.tsx @@ -7,6 +7,7 @@ import Visualization from "../visualization/visualization"; import Setup from "./setup/setup"; import Simulation from "../simulation/simulation"; import Collaboration from "../collaboration/collaboration"; +import RealTimeSimulation from "../IIOTTemp/RealTimeSimulation"; export default function Scene() { const map = useMemo(() => [ @@ -33,8 +34,51 @@ export default function Scene() { + + ); } + + + + + +// export default function Scene() { +// const map = useMemo(() => [ +// { name: "forward", keys: ["ArrowUp", "w", "W"] }, +// { name: "backward", keys: ["ArrowDown", "s", "S"] }, +// { name: "left", keys: ["ArrowLeft", "a", "A"] }, +// { name: "right", keys: ["ArrowRight", "d", "D"] }, +// ], []); +// const { activeModule, setActiveModule } = useModuleStore(); + +// return ( +// +// {activeModule !== "visualization" && +// { +// e.preventDefault(); +// }} +// > +// + +// + +// + +// + + + +// +// } +// {activeModule === "visualization" && } + +// +// ); +// } \ No newline at end of file diff --git a/app/src/store/iiot/iiotmqttConnectionStore.ts b/app/src/store/iiot/iiotmqttConnectionStore.ts new file mode 100644 index 0000000..91f5522 --- /dev/null +++ b/app/src/store/iiot/iiotmqttConnectionStore.ts @@ -0,0 +1,49 @@ +import { create } from 'zustand'; +import mqtt, { MqttClient } from 'mqtt'; + +interface MqttConnectionState { + client: MqttClient | null; + connected: boolean; + connect: () => void; + disconnect: () => void; +} + +const useMqttConnectionStore = create((set, get) => { + let client: MqttClient; + + return { + client: null, + connected: false, + + connect: () => { + if (get().connected) return; + client = mqtt.connect('ws://185.100.212.76:23457'); // Replace with your broker URL + + + client.on('connect', () => { + console.log('Connected to MQTT'); + set({ connected: true, client }); + }); + + client.on('error', (err) => { + console.error('MQTT Error:', err); + }); + + client.on('close', () => { + console.warn('MQTT connection closed'); + set({ connected: false, client: null }); + }); + }, + + disconnect: () => { + const currentClient = get().client; + if (currentClient && currentClient.connected) { + currentClient.end(); + set({ connected: false, client: null }); + console.log('MQTT disconnected'); + } + }, + }; +}); + +export default useMqttConnectionStore;