From 58eb0dbdf783f8151bcc8bdadb00f800a383ed6d Mon Sep 17 00:00:00 2001 From: Kalana Ratnayake Date: Wed, 2 Jul 2025 18:24:42 +1000 Subject: [PATCH 01/13] added docker --- .github/workflows/build.yml | 0 README.md | 24 +++++++++++++++++------ backend/.dockerignore | 9 +++++++++ backend/Dockerfile | 21 ++++++++++++++++++++ compose.yaml | 39 +++++++++++++++++++++++++++++++++++++ frontend/.dockerignore | 11 +++++++++++ frontend/Dockerfile | 32 ++++++++++++++++++++++++++++++ 7 files changed, 130 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/build.yml create mode 100644 backend/.dockerignore create mode 100644 backend/Dockerfile create mode 100644 compose.yaml create mode 100644 frontend/.dockerignore create mode 100644 frontend/Dockerfile diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md index 1314d08..3e18b37 100644 --- a/README.md +++ b/README.md @@ -9,22 +9,32 @@ This app allows: - REST API for historical data access -## 🚀 How to Run - -### 1. Prerequisites +## Prerequisites - Node.js ≥ 18.x - MongoDB (running locally or via Atlas) - ROS2 Capabilities2 system publishing to `/events` via [foxglove-bridge](https://github.com/foxglove/ros-foxglove-bridge) -### 2. Clone and Setup +## Clone and Setup ```bash git clone https://github.com/CollaborativeRoboticsLab/capabilities2-ui.git cd capabilities2-ui ``` -### 3. Backend +## Docker based Deployment + +Make sure you have docker installed. Then, + +```sh +docker compose up +``` + +## Pure Deployment + +### Backend + +If you want to modify or start the backend seperately, run following commands ```bash cd backend @@ -52,7 +62,9 @@ node server.js ``` -### 4. Frontend +### Frontend + +If you want to modify or start the backend seperately, run following commands ```bash cd ../frontend diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..af69b10 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,9 @@ +node_modules +npm-debug.log +Dockerfile +.dockerignore +.git +.gitignore +.env +*.md +*.log diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..6d07cc3 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,21 @@ +# Use a lightweight Node.js base image +FROM node:18-alpine + +# Set working directory +WORKDIR /app + +# Copy package files and install dependencies +COPY package.json package-lock.json ./ +RUN npm ci + +# Copy backend source code +COPY . . + +# Set environment variables (override in production) +ENV PORT=5000 + +# Expose backend port +EXPOSE 5000 + +# Start server +CMD ["node", "server.js"] \ No newline at end of file diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..d4235ca --- /dev/null +++ b/compose.yaml @@ -0,0 +1,39 @@ +version: '3.8' + +services: + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + container_name: react_frontend + ports: + - "3000:80" + restart: unless-stopped + depends_on: + - backend + + backend: + build: + context: ./backend + dockerfile: Dockerfile + container_name: node_backend + ports: + - "5000:5000" + environment: + - PORT=5000 + - MONGO_URI=mongodb://mongo:27017/capabilities2 + restart: unless-stopped + depends_on: + - mongo + + mongo: + image: mongo:6 + container_name: mongo_db + ports: + - "27017:27017" + volumes: + - mongo_data:/data/db + restart: unless-stopped + +volumes: + mongo_data: diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..2203164 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,11 @@ +node_modules +build +dist +npm-debug.log +Dockerfile +.dockerignore +.git +.gitignore +.env +*.md +*.log \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..8f8c10e --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,32 @@ +# Step 1: Build the React app +FROM node:18-alpine AS builder + +# Set working directory +WORKDIR /app + +# Copy only package files first for caching +COPY package.json package-lock.json ./ + +# Install dependencies +RUN npm ci + +# Copy all other source files +COPY . . + +# Build the app +RUN npm run build + +# Step 2: Serve the build using Nginx +FROM nginx:alpine + +# Remove default nginx static assets +RUN rm -rf /usr/share/nginx/html/* + +# Copy built React app to Nginx static folder +COPY --from=builder /app/build /usr/share/nginx/html + +# Expose default HTTP port +EXPOSE 80 + +# Run Nginx in the foreground +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file From 88a562da2aebe32c9f54060dd70e90cc341954fe Mon Sep 17 00:00:00 2001 From: Kalana Ratnayake Date: Fri, 4 Jul 2025 02:09:33 +1000 Subject: [PATCH 02/13] added ros-bridge --- .github/workflows/build.yml | 0 ...ilitiesEventDecoder.js => EventDecoder.js} | 4 +- backend/decoders/index.js | 2 +- backend/foxgloveClient.js | 4 +- compose.yaml | 37 ++++---- frontend/Dockerfile | 6 +- frontend/nginx.conf | 16 ++++ frontend/public/index.html | 2 +- frontend/src/pages/CurrentSessionPage.js | 85 ++++++++++++++----- ros2-client/Dockerfile | 66 ++++++++++++++ ros2-client/ros2-client/CMakeLists.txt | 14 +++ ros2-client/ros2-client/LICENSE | 17 ++++ .../ros2-client/launch/foxglove.launch.py | 61 +++++++++++++ ros2-client/ros2-client/package.xml | 20 +++++ ros2-client/workspace_entrypoint.sh | 17 ++++ 15 files changed, 306 insertions(+), 45 deletions(-) delete mode 100644 .github/workflows/build.yml rename backend/decoders/{capabilitiesEventDecoder.js => EventDecoder.js} (94%) create mode 100644 frontend/nginx.conf create mode 100644 ros2-client/Dockerfile create mode 100644 ros2-client/ros2-client/CMakeLists.txt create mode 100644 ros2-client/ros2-client/LICENSE create mode 100644 ros2-client/ros2-client/launch/foxglove.launch.py create mode 100644 ros2-client/ros2-client/package.xml create mode 100644 ros2-client/workspace_entrypoint.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index e69de29..0000000 diff --git a/backend/decoders/capabilitiesEventDecoder.js b/backend/decoders/EventDecoder.js similarity index 94% rename from backend/decoders/capabilitiesEventDecoder.js rename to backend/decoders/EventDecoder.js index 317a886..963b0f9 100644 --- a/backend/decoders/capabilitiesEventDecoder.js +++ b/backend/decoders/EventDecoder.js @@ -19,7 +19,7 @@ const TYPE_ENUM_MAP = { 5: "RUNNER_EVENT", }; -function decodeCapabilityEvent(data) { +function decodeEvent(data) { const reader = new CdrReader(data); try { const header = { @@ -69,4 +69,4 @@ function decodeCapabilityEvent(data) { } } -module.exports = decodeCapabilityEvent; +module.exports = decodeEvent; diff --git a/backend/decoders/index.js b/backend/decoders/index.js index dec0ece..a374f9c 100644 --- a/backend/decoders/index.js +++ b/backend/decoders/index.js @@ -1,5 +1,5 @@ module.exports = { - "/events": require("./capabilitiesEventDecoder"), + "/events": require("./EventDecoder"), // future topics can be added like: // "/sensor_data": require("./sensorDataDecoder"), diff --git a/backend/foxgloveClient.js b/backend/foxgloveClient.js index bde33bb..b248560 100644 --- a/backend/foxgloveClient.js +++ b/backend/foxgloveClient.js @@ -26,10 +26,10 @@ async function startFoxgloveClient(sessionId) { console.log(`[FoxgloveClient] Starting client for session: ${sessionId}`); const tryConnect = () => { - console.log("[FoxgloveClient] Attempting to connect to ws://0.0.0.0:8765..."); + console.log("[FoxgloveClient] Attempting to connect to ws://localhost:8765..."); const client = new FoxgloveClient({ - ws: new WebSocket("ws://0.0.0.0:8765", [FoxgloveClient.SUPPORTED_SUBPROTOCOL]), + ws: new WebSocket(process.env.ROSBRIDGE_URL || "ws://localhost:8765", [FoxgloveClient.SUPPORTED_SUBPROTOCOL]), }); const deserializers = new Map(); diff --git a/compose.yaml b/compose.yaml index d4235ca..2e2383a 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,39 +1,46 @@ -version: '3.8' - services: frontend: build: context: ./frontend dockerfile: Dockerfile - container_name: react_frontend - ports: - - "3000:80" + container_name: event_frontend + network_mode: host restart: unless-stopped - depends_on: - - backend backend: build: context: ./backend dockerfile: Dockerfile - container_name: node_backend - ports: - - "5000:5000" + container_name: event_backend environment: - PORT=5000 - - MONGO_URI=mongodb://mongo:27017/capabilities2 + - MONGO_URI=mongodb://localhost:27017/event_logger + - ROSBRIDGE_URL=ws://localhost:8765 + network_mode: host restart: unless-stopped - depends_on: - - mongo mongo: image: mongo:6 container_name: mongo_db - ports: - - "27017:27017" + network_mode: host + restart: unless-stopped volumes: - mongo_data:/data/db + + rosbridge: + build: + context: ./ros2-client + dockerfile: Dockerfile + container_name: foxglove_bridge + network_mode: host restart: unless-stopped + environment: + - RMW_IMPLEMENTATION=rmw_cyclonedds_cpp + - ROS_DOMAIN_ID=0 + - port=8765 + - address=0.0.0.0 + - topic_whitelist=['/events'] + - tls=false volumes: mongo_data: diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 8f8c10e..6730969 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -25,8 +25,12 @@ RUN rm -rf /usr/share/nginx/html/* # Copy built React app to Nginx static folder COPY --from=builder /app/build /usr/share/nginx/html +# Use custom config +RUN rm /etc/nginx/conf.d/default.conf +COPY nginx.conf /etc/nginx/conf.d/default.conf + # Expose default HTTP port -EXPOSE 80 +EXPOSE 3000 # Run Nginx in the foreground CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..31b3579 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,16 @@ +# frontend/nginx.conf + +server { + listen 3000; # <-- Make Nginx listen on port 3000 + + server_name localhost; + + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri /index.html; + } + + error_page 404 /index.html; +} diff --git a/frontend/public/index.html b/frontend/public/index.html index cb3c5e8..8dad2b9 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -24,7 +24,7 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - Capabilities2 UI + Event Logger UI diff --git a/frontend/src/pages/CurrentSessionPage.js b/frontend/src/pages/CurrentSessionPage.js index 5c60748..e5e266e 100644 --- a/frontend/src/pages/CurrentSessionPage.js +++ b/frontend/src/pages/CurrentSessionPage.js @@ -12,7 +12,6 @@ function CurrentSessionPage() { const [graphNodes, setGraphNodes] = useState([]); const [graphEdges, setGraphEdges] = useState([]); - // Load saved session on mount useEffect(() => { const saved = localStorage.getItem("liveSession"); if (saved) { @@ -21,7 +20,6 @@ function CurrentSessionPage() { } }, []); - // Load events for resumed session useEffect(() => { if (!session || session.isNewSession) return; @@ -31,7 +29,6 @@ function CurrentSessionPage() { .catch((err) => console.error("Failed to load events:", err)); }, [session]); - // Live session WebSocket useEffect(() => { if (!session || !session.isNewSession) return; @@ -44,32 +41,74 @@ function CurrentSessionPage() { if (message.type === "GRAPH_UPDATE") { const { graph } = message; - if (!graph) { - console.warn("GRAPH_UPDATE received with no graph data:", message); + if (!graph || !graph.nodes || !graph.edges) { + console.warn("GRAPH_UPDATE received with incomplete graph data:", message); return; } - // Convert nodeId → string ID format + const eventLog = graph.eventLog || []; const nodeMap = new Map(); - const convertedNodes = graph.nodes.map(n => { - const id = `${n.capability}:${n.provider}`; - nodeMap.set(n.nodeId, id); - return { + const edgeMap = new Map(); + + if (eventLog.length === 0) { + const idleNodes = graph.nodes.map((n) => ({ ...n, - id, + id: n.nodeId, state: "idle", - }; - }); - - const convertedEdges = graph.edges.map(e => ({ - ...e, - source: nodeMap.get(e.sourceNodeID), - target: nodeMap.get(e.targetNodeID), - activated: 0, - })); - - setGraphNodes(convertedNodes); - setGraphEdges(convertedEdges); + })); + const baseEdges = graph.edges.map((e) => ({ + ...e, + source: e.sourceNodeID, + target: e.targetNodeID, + activated: 0, + })); + + setGraphNodes(idleNodes); + setGraphEdges(baseEdges); + return; + } + + for (const entry of eventLog) { + // Handle node state + if (entry.nodeId !== null) { + let node = nodeMap.get(entry.nodeId); + if (!node) { + const orig = graph.nodes.find((n) => n.nodeId === entry.nodeId); + if (orig) { + node = { + ...orig, + id: orig.nodeId, + state: entry.nodeState || "idle", + }; + nodeMap.set(entry.nodeId, node); + } + } else if (entry.nodeState) { + node.state = entry.nodeState; + } + } + + // Handle edge state + if (entry.edgeId !== null) { + let edge = edgeMap.get(entry.edgeId); + if (!edge) { + const orig = graph.edges.find((e) => e.edgeId === entry.edgeId); + if (orig) { + edge = { + ...orig, + source: orig.sourceNodeID, + target: orig.targetNodeID, + activated: entry.edgeState ? 1 : 0, + }; + edgeMap.set(entry.edgeId, edge); + } + } else if (entry.edgeState) { + edge.activated += 1; + } + } + } + + setGraphNodes([...nodeMap.values()]); + setGraphEdges([...edgeMap.values()]); } else { setEvents((prev) => [message, ...prev]); } diff --git a/ros2-client/Dockerfile b/ros2-client/Dockerfile new file mode 100644 index 0000000..ec76ac7 --- /dev/null +++ b/ros2-client/Dockerfile @@ -0,0 +1,66 @@ +# check=skip=JSONArgsRecommended +#--------------------------------------------------------------------------------------------------------------------------- +#---- Stage 1: Build +#--------------------------------------------------------------------------------------------------------------------------- + +FROM ros:humble-ros-base-jammy as base + +ENV WORKSPACE_ROOT=/foxglove + +#--------------------------------------------------- +# Install dependencies +#--------------------------------------------------- +RUN apt-get update + +RUN apt-get install -y --no-install-recommends python3-pip \ + python3-colcon-common-extensions \ + ros-$ROS_DISTRO-foxglove-bridge \ + ros-$ROS_DISTRO-rmw-cyclonedds-cpp + + +#--------------------------------------------------- +# Copy your ROS 2 package and build workspace +#--------------------------------------------------- +WORKDIR ${WORKSPACE_ROOT}/src + +COPY ros2-client/ ./ +RUN git clone https://github.com/CollaborativeRoboticsLab/event_logger.git + +RUN rosdep install --from-paths ${WORKSPACE_ROOT}/src -y --ignore-src + +WORKDIR ${WORKSPACE_ROOT} + +RUN . /opt/ros/humble/setup.sh && colcon build + +#--------------------------------------------------- +# Clean up workspace +#--------------------------------------------------- +RUN rm -rf ${WORKSPACE_ROOT}/src +RUN rm -rf ${WORKSPACE_ROOT}/log +RUN rm -rf ${WORKSPACE_ROOT}/build + +RUN apt-get clean + +RUN rm -rf /var/lib/apt/lists/* +RUN rm -rf /tmp/* + +#--------------------------------------------------------------------------------------------------------------------------- +#---- Stage 2: Runtime +#--------------------------------------------------------------------------------------------------------------------------- +FROM ros:humble-ros-base-jammy as final + +ENV WORKSPACE_ROOT=/foxglove +ENV RMW_IMPLEMENTATION=rmw_cyclonedds_cpp + +# Copy built workspace +COPY --from=base / / + +# Optional entrypoint script if needed +COPY /workspace_entrypoint.sh /workspace_entrypoint.sh +RUN chmod +x /workspace_entrypoint.sh + +ENTRYPOINT ["/workspace_entrypoint.sh"] +WORKDIR ${WORKSPACE_ROOT} + +# CMD executes your launch file with env-configurable parameters +CMD ros2 launch ros2-client foxglove.launch.py diff --git a/ros2-client/ros2-client/CMakeLists.txt b/ros2-client/ros2-client/CMakeLists.txt new file mode 100644 index 0000000..a957004 --- /dev/null +++ b/ros2-client/ros2-client/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.8) +project(ros2-client) + +if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-Wall -Wextra -Wpedantic) +endif() + +find_package(ament_cmake REQUIRED) + +install(DIRECTORY launch + DESTINATION share/${PROJECT_NAME} +) + +ament_package() diff --git a/ros2-client/ros2-client/LICENSE b/ros2-client/ros2-client/LICENSE new file mode 100644 index 0000000..30e8e2e --- /dev/null +++ b/ros2-client/ros2-client/LICENSE @@ -0,0 +1,17 @@ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/ros2-client/ros2-client/launch/foxglove.launch.py b/ros2-client/ros2-client/launch/foxglove.launch.py new file mode 100644 index 0000000..5b54cbd --- /dev/null +++ b/ros2-client/ros2-client/launch/foxglove.launch.py @@ -0,0 +1,61 @@ +from launch import LaunchDescription +from launch_ros.actions import Node +from launch.substitutions import LaunchConfiguration +from launch.actions import DeclareLaunchArgument +from launch.substitutions import EnvironmentVariable + +def generate_launch_description(): + # Declare launch arguments + launch_args = [ + DeclareLaunchArgument('port', default_value=EnvironmentVariable('port', default_value='8765')), + DeclareLaunchArgument('address', default_value=EnvironmentVariable('address', default_value='127.0.0.1')), + DeclareLaunchArgument('tls', default_value=EnvironmentVariable('tls', default_value='false')), + DeclareLaunchArgument('certfile', default_value=''), + DeclareLaunchArgument('keyfile', default_value=''), + DeclareLaunchArgument('topic_whitelist', default_value="['/events']"), + # DeclareLaunchArgument('topic_whitelist', default_value="['.*']"), # Uncomment this line to whitelist all topics + DeclareLaunchArgument('param_whitelist', default_value="['.*']"), + DeclareLaunchArgument('service_whitelist', default_value="['.*']"), + DeclareLaunchArgument('client_topic_whitelist', default_value="['.*']"), + DeclareLaunchArgument('min_qos_depth', default_value='1'), + DeclareLaunchArgument('max_qos_depth', default_value='10'), + DeclareLaunchArgument('num_threads', default_value='0'), + DeclareLaunchArgument('send_buffer_limit', default_value='10000000'), + DeclareLaunchArgument('use_sim_time', default_value='false'), + DeclareLaunchArgument('capabilities', default_value="[clientPublish,parameters,parametersSubscribe,services,connectionGraph,assets]"), + DeclareLaunchArgument('include_hidden', default_value='false'), + DeclareLaunchArgument( + 'asset_uri_allowlist', + default_value=r"['^package://(?:[-\\w]+/)*[-\\w]+\\.(?:dae|fbx|glb|gltf|jpeg|jpg|mtl|obj|png|stl|tif|tiff|urdf|webp|xacro)$']" + ), + DeclareLaunchArgument('ignore_unresponsive_param_nodes', default_value='true'), + ] + + # Define the Node + foxglove_bridge_node = Node( + package='foxglove_bridge', + executable='foxglove_bridge', + parameters=[{ + 'port': LaunchConfiguration('port'), + 'address': LaunchConfiguration('address'), + 'tls': LaunchConfiguration('tls'), + 'certfile': LaunchConfiguration('certfile'), + 'keyfile': LaunchConfiguration('keyfile'), + 'topic_whitelist': LaunchConfiguration('topic_whitelist'), + 'param_whitelist': LaunchConfiguration('param_whitelist'), + 'service_whitelist': LaunchConfiguration('service_whitelist'), + 'client_topic_whitelist': LaunchConfiguration('client_topic_whitelist'), + 'min_qos_depth': LaunchConfiguration('min_qos_depth'), + 'max_qos_depth': LaunchConfiguration('max_qos_depth'), + 'num_threads': LaunchConfiguration('num_threads'), + 'send_buffer_limit': LaunchConfiguration('send_buffer_limit'), + 'use_sim_time': LaunchConfiguration('use_sim_time'), + 'capabilities': LaunchConfiguration('capabilities'), + 'include_hidden': LaunchConfiguration('include_hidden'), + 'asset_uri_allowlist': LaunchConfiguration('asset_uri_allowlist'), + 'ignore_unresponsive_param_nodes': LaunchConfiguration('ignore_unresponsive_param_nodes'), + }], + output='screen' + ) + + return LaunchDescription(launch_args + [foxglove_bridge_node]) \ No newline at end of file diff --git a/ros2-client/ros2-client/package.xml b/ros2-client/ros2-client/package.xml new file mode 100644 index 0000000..7f2ff54 --- /dev/null +++ b/ros2-client/ros2-client/package.xml @@ -0,0 +1,20 @@ + + + + ros2-client + 0.0.0 + TODO: Package description + kalana + MIT + + ament_cmake + + ament_lint_auto + ament_lint_common + + foxglove_bridge + + + ament_cmake + + \ No newline at end of file diff --git a/ros2-client/workspace_entrypoint.sh b/ros2-client/workspace_entrypoint.sh new file mode 100644 index 0000000..65dc3ee --- /dev/null +++ b/ros2-client/workspace_entrypoint.sh @@ -0,0 +1,17 @@ +#!/bin/bash +set -e + +if [ -f "/opt/ros/$ROS_DISTRO/setup.bash" ]; then + echo "sourcing /opt/ros/$ROS_DISTRO/setup.bash" + source /opt/ros/$ROS_DISTRO/setup.bash +else + echo "notfound /opt/ros/$ROS_DISTRO/setup.bash" + + echo "sourcing /opt/ros/$ROS_DISTRO/install/setup.bash" + source /opt/ros/$ROS_DISTRO/install/setup.bash +fi + +echo "sourcing $WORKSPACE_ROOT/install/setup.bash" +source "$WORKSPACE_ROOT/install/setup.bash" + +exec "$@" \ No newline at end of file From 46d2dd262e1a5bef1c4ed5718beb82015d4433d7 Mon Sep 17 00:00:00 2001 From: Kalana Ratnayake Date: Fri, 4 Jul 2025 02:13:00 +1000 Subject: [PATCH 03/13] name change --- README.md | 8 ++++---- backend/server.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3e18b37..d3f213d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Capabilities2 UI +# Event Logger UI A Node.js + React-based web application that connects to [`foxglove-rosbridge`](https://github.com/foxglove/ros-foxglove-bridge) and visualizes `/events` messages published in ROS2 using the CDR encoding format. @@ -18,8 +18,8 @@ This app allows: ## Clone and Setup ```bash -git clone https://github.com/CollaborativeRoboticsLab/capabilities2-ui.git -cd capabilities2-ui +git clone https://github.com/CollaborativeRoboticsLab/event_logger_ui.git +cd event_logger_ui ``` ## Docker based Deployment @@ -41,7 +41,7 @@ cd backend npm install # Setup MongoDB URI -echo "MONGO_URI=mongodb://localhost:27017/capabilities2" > .env +echo "MONGO_URI=mongodb://localhost:27017/event_logger" > .env # Start the backend (REST API + WebSocket + Foxglove client) node server.js diff --git a/backend/server.js b/backend/server.js index f18b517..eb2bc42 100644 --- a/backend/server.js +++ b/backend/server.js @@ -22,7 +22,7 @@ app.use("/api/sessions", sessionRoutes); app.use("/api/events", eventRoutes); app.use("/api/graphs", graphRoutes); -mongoose.connect(process.env.MONGO_URI || "mongodb://localhost:27017/capabilities2", { +mongoose.connect(process.env.MONGO_URI || "mongodb://localhost:27017/event_logger", { useNewUrlParser: true, useUnifiedTopology: true, }); From 0d608a6658d3e94c06d12911045f9636d61ec59e Mon Sep 17 00:00:00 2001 From: Kalana Ratnayake Date: Fri, 4 Jul 2025 11:34:42 +1000 Subject: [PATCH 04/13] updated decoders --- backend/decoders/EventDecoder.js | 25 ++++++++++++------------- backend/foxgloveClient.js | 2 +- backend/models/Event.js | 2 -- backend/utils/graphManage.js | 10 +++++----- 4 files changed, 18 insertions(+), 21 deletions(-) diff --git a/backend/decoders/EventDecoder.js b/backend/decoders/EventDecoder.js index 963b0f9..824ae1d 100644 --- a/backend/decoders/EventDecoder.js +++ b/backend/decoders/EventDecoder.js @@ -32,17 +32,10 @@ function decodeEvent(data) { const origin_node = reader.string(); - const source = { - capability: reader.string(), - provider: reader.string(), - parameters: reader.string(), - }; - - const target = { - capability: reader.string(), - provider: reader.string(), - parameters: reader.string(), - }; + const source_capability = reader.string(); + const source_provider = reader.string(); + const target_capability = reader.string(); + const target_provider = reader.string(); const thread_id = reader.int8(); @@ -55,8 +48,14 @@ function decodeEvent(data) { return { header, origin_node, - source, - target, + source :{ + capability: source_capability, + provider: source_provider, + }, + target: { + capability: target_capability, + provider: target_provider, + }, thread_id, event: EVENT_ENUM_MAP[eventNum] || "UNDEFINED", type: TYPE_ENUM_MAP[typeNum] || "INFO", diff --git a/backend/foxgloveClient.js b/backend/foxgloveClient.js index b248560..f9ca213 100644 --- a/backend/foxgloveClient.js +++ b/backend/foxgloveClient.js @@ -84,7 +84,7 @@ async function startFoxgloveClient(sessionId) { }); client.on("error", (err) => { - console.error(`[FoxgloveClient] ❌ WebSocket error: ${err.message}`); + console.error(`[FoxgloveClient] ❌ WebSocket error for session ${sessionId}: ${err.message}`); }); client.on("close", () => { diff --git a/backend/models/Event.js b/backend/models/Event.js index 6bff30d..577ca59 100644 --- a/backend/models/Event.js +++ b/backend/models/Event.js @@ -17,13 +17,11 @@ const eventSchema = new mongoose.Schema({ source: { capability: String, provider: String, - parameters: String, }, target: { capability: String, provider: String, - parameters: String, }, thread_id: Number, diff --git a/backend/utils/graphManage.js b/backend/utils/graphManage.js index 68e0005..d6e5375 100644 --- a/backend/utils/graphManage.js +++ b/backend/utils/graphManage.js @@ -301,28 +301,28 @@ async function updateGraphWithRunnerEvent(sessionId, event) { if (event.type === "RUNNER_EVENT") { if (!sourceCapability || !sourceProvider || !targetCapability || !targetProvider) { - console.warn(`[GraphUpdater] Invalid event data for session ${sessionKey}:`, event); + console.warn(`[GraphUpdater] Invalid event data for session ${sessionId}:`, event); return; } if (graph.nodes.some(node => node.capability === sourceCapability && node.provider === sourceProvider)) { sourceNodeID = graph.nodes.find(node => node.capability === sourceCapability && node.provider === sourceProvider).nodeId; } else { - console.warn(`[GraphUpdater] Source node not found in graph for session ${sessionKey}`); + console.warn(`[GraphUpdater] Source node not found in graph for session ${sessionId}`); return; } if (graph.nodes.some(node => node.capability === targetCapability && node.provider === targetProvider)) { targetNodeID = graph.nodes.find(node => node.capability === targetCapability && node.provider === targetProvider).nodeId; } else { - console.warn(`[GraphUpdater] Target node not found in graph for session ${sessionKey}`); + console.warn(`[GraphUpdater] Target node not found in graph for session ${sessionId}`); return; } if (sourceNodeID !== null || targetNodeID !== null) { edgeID = graph.edges.find(edge => edge.sourceNodeID === sourceNodeID && edge.targetNodeID === targetNodeID)?.edgeId; if (edgeID === undefined) { - console.warn(`[GraphUpdater] Edge not found in graph for session ${sessionKey}:`, + console.warn(`[GraphUpdater] Edge not found in graph for session ${sessionId}:`, `sourceNodeID=${sourceNodeID}, targetNodeID=${targetNodeID}`); return; } @@ -350,7 +350,7 @@ async function updateGraphWithRunnerEvent(sessionId, event) { edgeActivated = true; } else { // Handle other events or default case - console.warn(`[GraphUpdater] Unknown event type for session ${sessionKey}:`, event.event); + console.warn(`[GraphUpdater] Unknown event type for session ${sessionId}:`, event.event); return; } From 4bef85e27cfb4a871c81ac6d4b3d33f0c1ce655c Mon Sep 17 00:00:00 2001 From: Kalana Ratnayake Date: Fri, 4 Jul 2025 16:44:21 +1000 Subject: [PATCH 05/13] fix to event_logger_msg loading --- ros2-client/Dockerfile | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ros2-client/Dockerfile b/ros2-client/Dockerfile index ec76ac7..ebe2f28 100644 --- a/ros2-client/Dockerfile +++ b/ros2-client/Dockerfile @@ -13,6 +13,7 @@ ENV WORKSPACE_ROOT=/foxglove RUN apt-get update RUN apt-get install -y --no-install-recommends python3-pip \ + git \ python3-colcon-common-extensions \ ros-$ROS_DISTRO-foxglove-bridge \ ros-$ROS_DISTRO-rmw-cyclonedds-cpp @@ -23,13 +24,14 @@ RUN apt-get install -y --no-install-recommends python3-pip \ #--------------------------------------------------- WORKDIR ${WORKSPACE_ROOT}/src -COPY ros2-client/ ./ -RUN git clone https://github.com/CollaborativeRoboticsLab/event_logger.git +COPY ros2-client/ ros2-client/ -RUN rosdep install --from-paths ${WORKSPACE_ROOT}/src -y --ignore-src +RUN git clone https://github.com/CollaborativeRoboticsLab/event_logger.git WORKDIR ${WORKSPACE_ROOT} +RUN rosdep install --from-paths src -y --ignore-src + RUN . /opt/ros/humble/setup.sh && colcon build #--------------------------------------------------- From 96948e69b846859ef2ea285447133786fa48012e Mon Sep 17 00:00:00 2001 From: Kalana Ratnayake Date: Thu, 10 Jul 2025 15:02:44 +1000 Subject: [PATCH 06/13] work in progress --- backend/decoders/EventDecoder.js | 25 ++++++++-------- backend/foxgloveClient.js | 2 +- backend/models/Event.js | 2 ++ backend/utils/graphQueueManager.js | 29 +++++++++++++++++-- frontend/src/pages/PastSessionsPage.js | 1 + .../ros2-client/launch/foxglove.launch.py | 3 +- 6 files changed, 46 insertions(+), 16 deletions(-) diff --git a/backend/decoders/EventDecoder.js b/backend/decoders/EventDecoder.js index 824ae1d..9b5a111 100644 --- a/backend/decoders/EventDecoder.js +++ b/backend/decoders/EventDecoder.js @@ -32,10 +32,17 @@ function decodeEvent(data) { const origin_node = reader.string(); - const source_capability = reader.string(); - const source_provider = reader.string(); - const target_capability = reader.string(); - const target_provider = reader.string(); + const source = { + capability: reader.string(), + provider: reader.string(), + parameters: reader.string(), + } + + const target = { + capability: reader.string(), + provider: reader.string(), + parameters: reader.string(), + } const thread_id = reader.int8(); @@ -48,14 +55,8 @@ function decodeEvent(data) { return { header, origin_node, - source :{ - capability: source_capability, - provider: source_provider, - }, - target: { - capability: target_capability, - provider: target_provider, - }, + source, + target, thread_id, event: EVENT_ENUM_MAP[eventNum] || "UNDEFINED", type: TYPE_ENUM_MAP[typeNum] || "INFO", diff --git a/backend/foxgloveClient.js b/backend/foxgloveClient.js index f9ca213..d02c11c 100644 --- a/backend/foxgloveClient.js +++ b/backend/foxgloveClient.js @@ -75,7 +75,7 @@ async function startFoxgloveClient(sessionId) { // Queue graph-related processing instead of direct function calls if (["RUNNER_DEFINE", "RUNNER_EVENT"].includes(event.type)) { - graphQueueManager.addToQueue(event, sessionId); + graphQueueManager.process(event, sessionId); } }) .catch((err) => diff --git a/backend/models/Event.js b/backend/models/Event.js index 577ca59..6bff30d 100644 --- a/backend/models/Event.js +++ b/backend/models/Event.js @@ -17,11 +17,13 @@ const eventSchema = new mongoose.Schema({ source: { capability: String, provider: String, + parameters: String, }, target: { capability: String, provider: String, + parameters: String, }, thread_id: Number, diff --git a/backend/utils/graphQueueManager.js b/backend/utils/graphQueueManager.js index fa0b552..42197ab 100644 --- a/backend/utils/graphQueueManager.js +++ b/backend/utils/graphQueueManager.js @@ -5,9 +5,10 @@ const { updateGraphNumbers, getActiveGraphKey, } = require("./graphManage") -const runnerQueues = new Map(); // sessionId => { define: [], event: [] } +const sessionQueues = new Map(); // sessionId => { define: [], event: [] } let broadcastGraphFn = null; + /** Sets the function to broadcast graph updates. * @param {Function} fn - The function to call when broadcasting graph updates. * This function is used to set a callback that will be invoked @@ -18,6 +19,30 @@ function setGraphBroadcast(fn) { broadcastGraphFn = fn; } +async function process(event, sessionId) { + if (!sessionId) { + console.warn("[QueueProcessor] No session ID provided, cannot process event."); + return; + } + + if (!sessionQueues.has(sessionId)) { + sessionQueues.set(sessionId, { + queue: [], + process_state: "IDLE" // IDLE, DEFINE, EVENT + }); + } + + const queues = sessionQueues.get(sessionId); + + if (event.type === "RUNNER_DEFINE") { + queues.define.push(event); + } else if (event.type === "RUNNER_EVENT") { + queues.event.push(event); + } + + await processQueues(sessionId); +} + async function addToQueue(event, sessionId) { const keyTest = getActiveGraphKey(sessionId); @@ -101,6 +126,6 @@ async function processQueues(sessionId) { } module.exports = { - addToQueue, + process, setGraphBroadcast, }; \ No newline at end of file diff --git a/frontend/src/pages/PastSessionsPage.js b/frontend/src/pages/PastSessionsPage.js index 2068e8b..e9f93d0 100644 --- a/frontend/src/pages/PastSessionsPage.js +++ b/frontend/src/pages/PastSessionsPage.js @@ -35,6 +35,7 @@ function PastSessionsPage() { axios .get(`http://localhost:5000/api/graphs/${selectedSession._id}/count`) .then((res) => { + console.log("Graph count response:", res.data); // Add this const count = res.data.count; setGraphs(Array(count).fill(null)); // Placeholder setGraphIndex(0); diff --git a/ros2-client/ros2-client/launch/foxglove.launch.py b/ros2-client/ros2-client/launch/foxglove.launch.py index 5b54cbd..a01e667 100644 --- a/ros2-client/ros2-client/launch/foxglove.launch.py +++ b/ros2-client/ros2-client/launch/foxglove.launch.py @@ -55,7 +55,8 @@ def generate_launch_description(): 'asset_uri_allowlist': LaunchConfiguration('asset_uri_allowlist'), 'ignore_unresponsive_param_nodes': LaunchConfiguration('ignore_unresponsive_param_nodes'), }], - output='screen' + output='screen', + arguments=['--ros-args', '--log-level', 'info'] ) return LaunchDescription(launch_args + [foxglove_bridge_node]) \ No newline at end of file From 73b0d10e56740a295afa9eb24456f5e3c72f2bda Mon Sep 17 00:00:00 2001 From: Kalana Ratnayake Date: Mon, 14 Jul 2025 19:18:02 +1000 Subject: [PATCH 07/13] minor change --- backend/utils/graphManage.js | 2 - backend/utils/graphQueueManager.js | 136 ++++++++++++++--------------- 2 files changed, 67 insertions(+), 71 deletions(-) diff --git a/backend/utils/graphManage.js b/backend/utils/graphManage.js index d6e5375..c95b13a 100644 --- a/backend/utils/graphManage.js +++ b/backend/utils/graphManage.js @@ -382,6 +382,4 @@ module.exports = { createGraphForSession, finalizeGraphForSession, updateGraphWithRunnerEvent, - updateGraphNumbers, - getActiveGraphKey, }; \ No newline at end of file diff --git a/backend/utils/graphQueueManager.js b/backend/utils/graphQueueManager.js index 42197ab..daa0920 100644 --- a/backend/utils/graphQueueManager.js +++ b/backend/utils/graphQueueManager.js @@ -1,14 +1,11 @@ const { createGraphForSession, finalizeGraphForSession, - updateGraphWithRunnerEvent, - updateGraphNumbers, - getActiveGraphKey, } = require("./graphManage") + updateGraphWithRunnerEvent,} = require("./graphManage") const sessionQueues = new Map(); // sessionId => { define: [], event: [] } let broadcastGraphFn = null; - /** Sets the function to broadcast graph updates. * @param {Function} fn - The function to call when broadcasting graph updates. * This function is used to set a callback that will be invoked @@ -20,109 +17,110 @@ function setGraphBroadcast(fn) { } async function process(event, sessionId) { + // Validate the sessionId and event. if (!sessionId) { console.warn("[QueueProcessor] No session ID provided, cannot process event."); return; } + // Initialize the session queue if it doesn't exist. if (!sessionQueues.has(sessionId)) { sessionQueues.set(sessionId, { queue: [], + define: [], + processing: false, process_state: "IDLE" // IDLE, DEFINE, EVENT }); } - const queues = sessionQueues.get(sessionId); + // Check if the event is valid. + if (!event || typeof event.type !== "string") return; + + // Retrieve the queue for the given session ID. + const queue = sessionQueues.get(sessionId); - if (event.type === "RUNNER_DEFINE") { - queues.define.push(event); - } else if (event.type === "RUNNER_EVENT") { - queues.event.push(event); + // Push the event into the queue based on if it is a RUNNER_DEFINE or RUNNER_EVENT. + if (event.type === "RUNNER_DEFINE" || event.type === "RUNNER_EVENT") { + queue.queue.push(event); } await processQueues(sessionId); } -async function addToQueue(event, sessionId) { - const keyTest = getActiveGraphKey(sessionId); +async function processQueues(sessionId) { + // Retrieve the queues for the given session ID. + const queue = sessionQueues.get(sessionId); - if (!keyTest) { - console.warn(`[QueueProcessor] No active graph key for session ${sessionId} so creating a new one.`); - await updateGraphNumbers(sessionId); - } + // If the queue is already processing, return early to avoid re-entrancy issues. + if (!queue || queue.processing) return; - const key = getActiveGraphKey(sessionId); + // Set the queue to processing state. + queue.processing = true; - if (!runnerQueues.has(key)) { - runnerQueues.set(key, { - define: [], - event: [], - processing: false - }); - } + for (let index = 0; index < queue.queue.length; index++) { + const element = array[index]; - if (!event || typeof event.type !== "string") return; + try { + if (queue.process_state === "IDLE" && element.type === "RUNNER_DEFINE") { + queue.process_state = "DEFINE"; - const queues = runnerQueues.get(key); + // If we are coming from IDLE state and get RUNNER_DEFINE, push the event into define queue + queue.define.push(element); - if (event.type === "RUNNER_DEFINE") { - queues.define.push(event); - } else if (event.type === "RUNNER_EVENT") { - queues.event.push(event); - } + } else if (queue.process_state === "DEFINE" && element.type === "RUNNER_DEFINE") { + queue.process_state = "DEFINE"; - await processQueues(sessionId); -} + // If we are in DEFINE and still getting RUNNER_DEFINE, push the event into define queue + queue.define.push(element); -async function processQueues(sessionId) { - const key = getActiveGraphKey(sessionId); + } else if (queue.process_state === "DEFINE" && element.type === "RUNNER_EVENT") { + queue.process_state = "EVENT"; - const queues = runnerQueues.get(key); - if (!queues || queues.processing) return; + // If we are in DEFINE state and RUNNER_EVENT are starting, create the graph with define queue + const newGraph = await createGraphForSession(sessionId, queue.define); - queues.processing = true; + if (broadcastGraphFn && newGraph) { + broadcastGraphFn({ type: "GRAPH_UPDATE", graph: newGraph }); + } - try { - if (queues.define.length > 0 && queues.event.length === 1) { - const newGraph = await createGraphForSession(sessionId, queues.define); + // Empty the define queue after creating the graph + queue.define.length = 0; - if (broadcastGraphFn && newGraph) { - broadcastGraphFn({ type: "GRAPH_UPDATE", graph: newGraph }); - } + // update the graph with event + const updatedGraph = await updateGraphWithRunnerEvent(sessionId, element); - queues.define = []; - } + if (broadcastGraphFn && updatedGraph) { + broadcastGraphFn({ type: "GRAPH_UPDATE", graph: updatedGraph }); + } - while (queues.event.length > 0) { - const evt = queues.event.shift(); + } else if (queue.process_state === "EVENT" && element.type === "RUNNER_EVENT") { + queue.process_state = "EVENT"; - const updatedGraph = await updateGraphWithRunnerEvent(sessionId, evt); + // If we are in EVENT state and getting RUNNER_EVENT, update the graph with the event // update the graph with event + const updatedGraph = await updateGraphWithRunnerEvent(sessionId, element); - if (broadcastGraphFn && updatedGraph) { - broadcastGraphFn({ type: "GRAPH_UPDATE", graph: updatedGraph }); - } - } + if (broadcastGraphFn && updatedGraph) { + broadcastGraphFn({ type: "GRAPH_UPDATE", graph: updatedGraph }); + } - if (queues.define.length === 1 && queues.event.length === 0) { - await finalizeGraphForSession(sessionId); + } else if (queue.process_state === "EVENT" && element.type === "RUNNER_DEFINE") { + queue.process_state = "DEFINE"; - const newKey = getActiveGraphKey(sessionId); - - runnerQueues.set(newKey, { - define: [], - event: [], - processing: false - }); - - const newQueues = runnerQueues.get(newKey); - - newQueues.define = queues.define.slice(0, 1); + // If we are in EVENT state and getting RUNNER_DEFINE, finalize the graph for the session + await finalizeGraphForSession(sessionId); + + // Add the RUNNER_DEFINE to the define queue + queue.define.push(element); + } + } catch (err) { + console.error(`[QueueProcessor] ❌ Error processing element: ${err.message}`); + queue.processing = false; + return; } - } catch (err) { - console.error(`[QueueProcessor] ❌ Error: ${err.message}`); - } finally { - queues.processing = false; } + + queue.queue.splice(0, queue.queue.length); // Clear the queue after processing + queue.processing = false; } module.exports = { From abce424cce92276ca19182af712be9e8bab82e95 Mon Sep 17 00:00:00 2001 From: Kalana Ratnayake Date: Mon, 28 Jul 2025 16:21:55 +1000 Subject: [PATCH 08/13] minor fixes --- backend/utils/graphManage.js | 53 +++++++++++------------------- backend/utils/graphQueueManager.js | 6 ++-- 2 files changed, 23 insertions(+), 36 deletions(-) diff --git a/backend/utils/graphManage.js b/backend/utils/graphManage.js index c95b13a..249a161 100644 --- a/backend/utils/graphManage.js +++ b/backend/utils/graphManage.js @@ -4,18 +4,24 @@ const Event = require("../models/Event"); const activeGraphs = new Map(); // still exportable const graphNumbers = new Map(); // sessionId => graphId -/** Sets the active graph number for a given session ID. - * @param {string} sessionId - The ID of the session for which to set the active graph key. - * @param {number} graphNo - The graph number to set as active for the session. - * This function updates the activeGraphKeys map to associate the session ID with the specified graph number. - */ -async function updateGraphNumbers(sessionId) { - const graphCount = await Graph.countDocuments({ session: sessionId }); - graphNumbers.set(sessionId, graphCount + 1); - - console.log(`[GraphManager] Updated graph number for session ${sessionId}: ${getGraphNumbers(sessionId)}`); +function incrementGraphNumbers(sessionId) { + if (!graphNumbers.has(sessionId)) { + graphNumbers.set(sessionId, 1); + console.log(`[GraphManager] Initialized graph number for session ${sessionId}: 1`); + } else { + graphNumbers.set(sessionId, graphNumbers.get(sessionId) + 1); + console.log(`[GraphManager] Incremented graph number for session ${sessionId}: ${getGraphNumbers(sessionId)}`); + } } +/** Retrieves the graph number for a given session ID. + * @param {string} sessionId - The ID of the session for which to get the + * active graph number. + * @returns {number|null} The active graph number for the session, or null if no + * active graph exists. + * This function checks the graphNumbers map for the session ID and returns the associated graph number. + * If no graph number is found, it returns null. + */ function getGraphNumbers(sessionId) { return graphNumbers.get(sessionId) || null; } @@ -60,25 +66,6 @@ function getActiveGraph(sessionId) { } } -/** Sets the active graph for a given session ID. - * This function updates the activeGraphs map to associate the session ID with the specified graph. - * - * @param {string} sessionId - The ID of the session for which to set the active graph. - * @param {Graph} graph - The graph to set as active for the session. - * @returns {void} - * - * This function retrieves the active graph key for the session and sets the graph in the activeGraphs map. - * If no active graph key is found, it logs a warning. - */ -function setActiveGraph(sessionId, graph) { - const key = getActiveGraphKey(sessionId); - if (!key) { - console.warn(`[GraphManager] No active graph key for session ${sessionId}`); - return; - } - activeGraphs.set(key, graph); -} - /** Generates a graph structure from a list of events. This function processes the events of * type "RUNNER_DEFINE" to create nodes and edges. It initializes nodes for unique capabilities * and providers, creates edges between source and target nodes, and logs events. @@ -211,8 +198,10 @@ async function createGraphForSession(sessionId, events) { const { nodes, edges, eventLog } = generateGraph(events); - const graphKey = getActiveGraphKey(sessionId); + incrementGraphNumbers(sessionId); + const graphNumber = getGraphNumbers(sessionId); + const graphKey = getActiveGraphKey(sessionId); console.log("Graph data for graph:", graphKey); console.log("Nodes:", nodes); @@ -230,7 +219,7 @@ async function createGraphForSession(sessionId, events) { await graph.save(); - setActiveGraph(sessionId, graph); + activeGraphs.set(graphKey, graph); return graph; } @@ -258,8 +247,6 @@ async function finalizeGraphForSession(sessionId) { activeGraphs.delete(key); console.log(`[GraphFinalizer] Finalized graph ${graph.graphId}`); - - updateGraphNumbers(sessionId); } /** Updates the graph with a runner event. diff --git a/backend/utils/graphQueueManager.js b/backend/utils/graphQueueManager.js index daa0920..1d2decc 100644 --- a/backend/utils/graphQueueManager.js +++ b/backend/utils/graphQueueManager.js @@ -57,9 +57,10 @@ async function processQueues(sessionId) { // Set the queue to processing state. queue.processing = true; - for (let index = 0; index < queue.queue.length; index++) { - const element = array[index]; + const eventsToProcess = [...queue.queue]; // clone current queue + queue.queue.length = 0; // clear early to avoid mid-loop appends + for (const element of eventsToProcess) { try { if (queue.process_state === "IDLE" && element.type === "RUNNER_DEFINE") { queue.process_state = "DEFINE"; @@ -119,7 +120,6 @@ async function processQueues(sessionId) { } } - queue.queue.splice(0, queue.queue.length); // Clear the queue after processing queue.processing = false; } From e1dbfc320512f6d0bc925ea6b19fb644fe5e25cf Mon Sep 17 00:00:00 2001 From: Kalana Ratnayake Date: Tue, 29 Jul 2025 14:54:16 +1000 Subject: [PATCH 09/13] minor fixes --- backend/models/Graph.js | 2 + backend/routes/events.js | 28 ++++++------- backend/routes/graphs.js | 10 ++--- compose.yaml | 3 ++ .../src/components/GraphTimelinePlayer.js | 39 +++++++++++++------ 5 files changed, 47 insertions(+), 35 deletions(-) diff --git a/backend/models/Graph.js b/backend/models/Graph.js index 4914e24..13c3aeb 100644 --- a/backend/models/Graph.js +++ b/backend/models/Graph.js @@ -35,4 +35,6 @@ const graphSchema = new mongoose.Schema({ completedAt: Date, }, { timestamps: true }); +graphSchema.index({ session: 1, graphNo: 1 }); + module.exports = mongoose.model("Graph", graphSchema); diff --git a/backend/routes/events.js b/backend/routes/events.js index c32feb5..6b2f205 100644 --- a/backend/routes/events.js +++ b/backend/routes/events.js @@ -2,25 +2,19 @@ const express = require('express'); const router = express.Router(); const Event = require('../models/Event'); -// Save new event -router.post('/', async (req, res) => { - try { - const newEvent = new Event(req.body); - const saved = await newEvent.save(); - res.status(201).json(saved); - } catch (err) { - res.status(400).json({ error: err.message }); - } -}); - -// Get all events +// Get filtered events router.get('/', async (req, res) => { - try { - const events = await Event.find().sort({ createdAt: -1 }); - res.json(events); - } catch (err) { - res.status(500).json({ error: err.message }); + try { + const filter = {}; + + if (req.query.session) { + filter.session = req.query.session; } + + const events = await Event.find(filter).sort({ createdAt: -1 }); + res.json(events); + } catch (err) { + res.status(500).json({ error: err.message }); } }); module.exports = router; \ No newline at end of file diff --git a/backend/routes/graphs.js b/backend/routes/graphs.js index 241debf..3c6f32f 100644 --- a/backend/routes/graphs.js +++ b/backend/routes/graphs.js @@ -17,7 +17,7 @@ router.post("/:sessionId/create", async (req, res) => { router.get("/:sessionId", async (req, res) => { try { - const graphs = await Graph.find({ session: req.params.sessionId }).sort({ graphNumber: 1 }); + const graphs = await Graph.find({ session: req.params.sessionId }).sort({ graphNo: 1 }); res.json(graphs); } catch (err) { res.status(500).json({ error: err.message }); @@ -37,20 +37,18 @@ router.get("/:sessionId/count", async (req, res) => { router.get("/:sessionId/:index", async (req, res) => { const { sessionId, index } = req.params; try { - const graph = await Graph.findOne({ session: sessionId }) + const graph = await Graph.find({ session: sessionId }) .sort({ graphNo: 1 }) .skip(Number(index)) .limit(1); - if (!graph) return res.status(404).json({ message: "Graph not found" }); + if (!graph || graph.length === 0) return res.status(404).json({ message: "Graph not found" }); - res.json(graph); + res.json(graph[0]); } catch (err) { res.status(500).json({ error: err.message }); } }); - - module.exports = router; diff --git a/compose.yaml b/compose.yaml index 2e2383a..db1fb83 100644 --- a/compose.yaml +++ b/compose.yaml @@ -26,6 +26,7 @@ services: restart: unless-stopped volumes: - mongo_data:/data/db + attach: false rosbridge: build: @@ -41,6 +42,8 @@ services: - address=0.0.0.0 - topic_whitelist=['/events'] - tls=false + attach: false + volumes: mongo_data: diff --git a/frontend/src/components/GraphTimelinePlayer.js b/frontend/src/components/GraphTimelinePlayer.js index 06acfa2..f18bb8c 100644 --- a/frontend/src/components/GraphTimelinePlayer.js +++ b/frontend/src/components/GraphTimelinePlayer.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useMemo } from "react"; import GraphCanvas from "./GraphCanvas"; import "./GraphTimelinePlayer.css"; @@ -11,6 +11,18 @@ function GraphTimelinePlayer({ graph }) { setStep(0); }, [graph]); + const nodeLookup = useMemo(() => { + const map = new Map(); + graph?.nodes?.forEach((n) => map.set(n.nodeId, n)); + return map; + }, [graph]); + + const edgeLookup = useMemo(() => { + const map = new Map(); + graph?.edges?.forEach((e) => map.set(e.edgeId, e)); + return map; + }, [graph]); + useEffect(() => { if (!graph) return; @@ -19,7 +31,6 @@ function GraphTimelinePlayer({ graph }) { const eventLog = graph.eventLog || []; - // If no timeline, just show idle nodes and basic edges if (eventLog.length === 0) { const idleNodes = graph.nodes.map((n) => ({ ...n, @@ -42,11 +53,11 @@ function GraphTimelinePlayer({ graph }) { for (let i = 0; i <= step && i < eventLog.length; i++) { const entry = eventLog[i]; - // Handle Node State + // Node if (entry.nodeId !== null) { let node = nodeMap.get(entry.nodeId); if (!node) { - const orig = graph.nodes.find((n) => n.nodeId === entry.nodeId); + const orig = nodeLookup.get(entry.nodeId); if (orig) { node = { ...orig, @@ -60,11 +71,11 @@ function GraphTimelinePlayer({ graph }) { } } - // Handle Edge State + // Edge if (entry.edgeId !== null) { let edge = edgeMap.get(entry.edgeId); if (!edge) { - const orig = graph.edges.find((e) => e.edgeId === entry.edgeId); + const orig = edgeLookup.get(entry.edgeId); if (orig) { edge = { ...orig, @@ -74,28 +85,32 @@ function GraphTimelinePlayer({ graph }) { }; edgeMap.set(entry.edgeId, edge); } - } else if (entry.edgeState) { - edge.activated += 1; + } else { + edge.activated = entry.edgeState ? 1 : 0; } } } setCurrentNodes([...nodeMap.values()]); setCurrentEdges([...edgeMap.values()]); - }, [graph, step]); + }, [graph, step, nodeLookup, edgeLookup]); if (!graph) return

No graph loaded.

; + const totalSteps = graph.eventLog?.length || 0; + return (
- + - Step {graph.eventLog.length ? step + 1 : 0} / {graph.eventLog.length} + Step {totalSteps ? step + 1 : 0} / {totalSteps}