diff --git a/package-lock.json b/package-lock.json index 34c4380..fe0f097 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "@types/react-dom": "^19.1.9", "react": "^19.1.1", "react-dom": "^19.1.1", - "react-scripts": "5.0.1", + "react-scripts": "^5.0.1", "roslib": "^1.3.0", "typescript": "^4.9.5", "web-vitals": "^2.1.4" @@ -45,19 +45,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -73,30 +60,30 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", - "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", - "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.3", - "@babel/parser": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.3", - "@babel/types": "^7.28.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -121,9 +108,9 @@ } }, "node_modules/@babel/eslint-parser": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.28.0.tgz", - "integrity": "sha512-N4ntErOlKvcbTt01rr5wj3y55xnIdx1ymrfIr8C2WnM1Y9glFgWaGDEULJIazOX3XM9NRzhfJ6zZnQ1sBNWU+w==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.28.4.tgz", + "integrity": "sha512-Aa+yDiH87980jR6zvRfFuCR1+dLb00vBydhTL+zI992Rz/wQhSvuxjmOOuJOgO3XmakO6RykRGD2S1mq1AtgHA==", "license": "MIT", "dependencies": { "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", @@ -443,25 +430,25 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", - "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2" + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", - "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.2" + "@babel/types": "^7.28.4" }, "bin": { "parser": "bin/babel-parser.js" @@ -1012,9 +999,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.0.tgz", - "integrity": "sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.4.tgz", + "integrity": "sha512-1yxmvN0MJHOhPVmAsmoW5liWwoILobu/d/ShymZmj867bAdxGbehIrew1DuLpw2Ukv+qDSSPQdYW1dLNE7t11A==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1059,9 +1046,9 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.3.tgz", - "integrity": "sha512-DoEWC5SuxuARF2KdKmGUq3ghfPMO6ZzR12Dnp5gubwbeWJo4dbNWXJPVlwvh4Zlq6Z7YVvL8VFxeSOJgjsx4Sg==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", + "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", @@ -1069,7 +1056,7 @@ "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/traverse": "^7.28.4" }, "engines": { "node": ">=6.9.0" @@ -1455,16 +1442,16 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.0.tgz", - "integrity": "sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz", + "integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==", "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.0", "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/traverse": "^7.28.0" + "@babel/traverse": "^7.28.4" }, "engines": { "node": ">=6.9.0" @@ -1664,9 +1651,9 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.3.tgz", - "integrity": "sha512-K3/M/a4+ESb5LEldjQb+XSrpY0nF+ZBFlTCbSnKaYAMfD8v33O6PMs4uYnOk19HlcsI8WMu3McdFPTiQHF/1/A==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz", + "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -2066,17 +2053,17 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", - "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.3", + "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2", + "@babel/types": "^7.28.4", "debug": "^4.3.1" }, "engines": { @@ -2084,9 +2071,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", - "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -2389,9 +2376,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" @@ -2518,9 +2505,9 @@ } }, "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", - "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "license": "MIT", "engines": { "node": ">=12" @@ -2530,9 +2517,9 @@ } }, "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "license": "MIT", "engines": { "node": ">=12" @@ -2559,9 +2546,9 @@ } }, "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -2919,6 +2906,16 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -2945,9 +2942,9 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", - "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -3879,9 +3876,9 @@ } }, "node_modules/@types/semver": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", - "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", "license": "MIT" }, "node_modules/@types/send": { @@ -5240,6 +5237,15 @@ "node": "^4.5.0 || >= 5.9" } }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.8.tgz", + "integrity": "sha512-be0PUaPsQX/gPWWgFsdD+GFzaoig5PXaUC1xLkQiYdDnANU8sMnHoQd8JhbJQuvTWrWLyeFN9Imb5Qtfvr4RrQ==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -5385,9 +5391,9 @@ "license": "BSD-2-Clause" }, "node_modules/browserslist": { - "version": "4.25.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz", - "integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==", + "version": "4.26.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", + "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", "funding": [ { "type": "opencollective", @@ -5404,9 +5410,10 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001737", - "electron-to-chromium": "^1.5.211", - "node-releases": "^2.0.19", + "baseline-browser-mapping": "^2.8.3", + "caniuse-lite": "^1.0.30001741", + "electron-to-chromium": "^1.5.218", + "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, "bin": { @@ -5552,9 +5559,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001737", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001737.tgz", - "integrity": "sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw==", + "version": "1.0.30001745", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001745.tgz", + "integrity": "sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==", "funding": [ { "type": "opencollective", @@ -6536,9 +6543,9 @@ } }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -6940,9 +6947,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.211", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.211.tgz", - "integrity": "sha512-IGBvimJkotaLzFnwIVgW9/UD/AOJ2tByUmeOrtqBfACSbAw5b1G0XpvdaieKyc7ULmbwXVx+4e4Be8pOPBrYkw==", + "version": "1.5.225", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.225.tgz", + "integrity": "sha512-Oiv6+nGcMg0xuSUeYumk+eE0pDk0PuQ6Gnz16pErrPtlitaFZxq95hUtvGRg90kcJ/AdsM+AW5VkVUl3fGk+SQ==", "license": "ISC" }, "node_modules/emittery": { @@ -7080,9 +7087,9 @@ } }, "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -10933,9 +10940,9 @@ } }, "node_modules/jest-watch-typeahead/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -10948,9 +10955,9 @@ } }, "node_modules/jest-watch-typeahead/node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", - "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "license": "MIT", "engines": { "node": ">=12" @@ -11748,9 +11755,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.21", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz", + "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", "license": "MIT" }, "node_modules/normalize-path": { @@ -11808,9 +11815,9 @@ } }, "node_modules/nwsapi": { - "version": "2.2.21", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.21.tgz", - "integrity": "sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==", + "version": "2.2.22", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.22.tgz", + "integrity": "sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==", "license": "MIT" }, "node_modules/object-assign": { @@ -12824,9 +12831,19 @@ } }, "node_modules/postcss-js": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", - "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { "camelcase-css": "^2.0.1" @@ -12834,10 +12851,6 @@ "engines": { "node": "^12 || ^14 || >= 16" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, "peerDependencies": { "postcss": "^8.4.21" } @@ -14262,9 +14275,9 @@ "license": "MIT" }, "node_modules/regenerate-unicode-properties": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", - "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", "license": "MIT", "dependencies": { "regenerate": "^1.4.2" @@ -14306,17 +14319,17 @@ } }, "node_modules/regexpu-core": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", - "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", "license": "MIT", "dependencies": { "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.2.0", + "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", - "regjsparser": "^0.12.0", + "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" + "unicode-match-property-value-ecmascript": "^2.2.1" }, "engines": { "node": ">=4" @@ -14329,29 +14342,17 @@ "license": "MIT" }, "node_modules/regjsparser": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", - "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", + "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", "license": "BSD-2-Clause", "dependencies": { - "jsesc": "~3.0.2" + "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, - "node_modules/regjsparser/node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", @@ -16310,13 +16311,13 @@ } }, "node_modules/terser": { - "version": "5.43.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz", - "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", + "version": "5.44.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", + "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.14.0", + "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -16744,18 +16745,18 @@ } }, "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", - "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", "license": "MIT", "engines": { "node": ">=4" diff --git a/package.json b/package.json index 6ef2212..8898b5b 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "@types/react-dom": "^19.1.9", "react": "^19.1.1", "react-dom": "^19.1.1", - "react-scripts": "5.0.1", + "react-scripts": "^5.0.1", "roslib": "^1.3.0", "typescript": "^4.9.5", "web-vitals": "^2.1.4" @@ -22,7 +22,10 @@ "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", - "eject": "react-scripts eject" + "eject": "react-scripts eject", + "bag:record": "wsl bash ./scripts/record-bag.sh", + "bag:play": "wsl bash ./scripts/play-bag.sh", + "bag:list": "wsl bash -c \"mkdir -p bags && ls -lh bags/\"" }, "eslintConfig": { "extends": [ diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..331d1d3 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,108 @@ +# ROS Bag Helper Scripts + +Quick commands to record and play back ROS2 bags for testing the simulation GUI. + +## Quick Start + +### 1. Record a test bag from the GUI + +```powershell +# In PowerShell (Windows) - default 15 seconds, saves to bags/pool_test_01 +npm run bag:record +``` + +This will: +- Prompt you to start the GUI simulation first +- Record `/imu/data`, `/dvl/odom`, and `/depth/pose` for 15 seconds +- Save to `bags/pool_test_01/` + +**Custom bag name and duration:** +```powershell +# Record to a custom bag name for 30 seconds +wsl bash ./scripts/record-bag.sh my_test_bag 30 +``` + +### 2. Play back a bag + +```powershell +# In PowerShell (Windows) - plays bags/pool_test_01 in loop +npm run bag:play +``` + +This will: +- Play `bags/pool_test_01/` in loop mode at normal speed +- Press Ctrl+C to stop + +**Custom bag name and playback rate:** +```powershell +# Play custom bag at 2x speed +wsl bash ./scripts/play-bag.sh my_test_bag 2.0 +``` + +### 3. List available bags + +```powershell +npm run bag:list +``` + +## Manual Usage (from WSL) + +If you prefer to run the scripts directly in WSL: + +```bash +# Record +./scripts/record-bag.sh [bag_name] [duration_seconds] + +# Play +./scripts/play-bag.sh [bag_name] [rate] + +# Examples +./scripts/record-bag.sh pool_test_02 20 +./scripts/play-bag.sh pool_test_02 1.5 +``` + +## Workflow for Testing Bag Mode + +1. **Record a bag:** + ```powershell + npm start # Start GUI + # Set Data Source: Synthetic Simulation, click Start + npm run bag:record # In another terminal + ``` + +2. **Play it back:** + ```powershell + npm run bag:play # Plays in loop + # In GUI: switch to "Bag Playback (listen only)" + # Click "Seed from ROS topics" + ``` + +3. **Verify seeding works:** + - Topics should show Hz values (not "lost!") + - Seed button should be enabled + - After clicking "Seed from ROS topics", you should see: + - Green status line: "Seeded orientation from /imu/data, depth from /depth/pose, velocity from /dvl/odom — HH:MM:SS" + +## Troubleshooting + +**"Bag not found" error:** +- Check `npm run bag:list` to see available bags +- Make sure you recorded a bag first with `npm run bag:record` + +**"No topics" when recording:** +- Ensure rosbridge is running in WSL: `ros2 launch rosbridge_server rosbridge_websocket_launch.xml` +- Start the GUI and begin synthetic simulation +- Verify topics with: `wsl ros2 topic list` + +**Bag playback but GUI shows "lost!" banners:** +- Verify topics are publishing: `wsl ros2 topic hz /imu/data` +- Check rosbridge is connected (green banner in GUI) +- Ensure bag contains the right topics: `wsl ros2 bag info bags/pool_test_01` + +## Notes + +- All scripts run inside WSL2 (where ROS2 is installed) +- The npm commands are wrappers that invoke WSL automatically +- Bags are stored in the `bags/` directory at the repo root +- Recording uses a 15-second default duration (configurable) +- Playback runs in loop mode by default diff --git a/scripts/play-bag.sh b/scripts/play-bag.sh new file mode 100644 index 0000000..749a823 --- /dev/null +++ b/scripts/play-bag.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# Helper script to play back a recorded bag +# Usage: ./scripts/play-bag.sh [bag_name] [rate] + +# Source ROS2 setup +source /opt/ros/humble/setup.bash + +BAG_NAME="${1:-pool_test_01}" +RATE="${2:-1.0}" +BAG_DIR="$(cd "$(dirname "$0")/.." && pwd)/bags/${BAG_NAME}" + +echo "=========================================" +echo "Playing ROS2 bag" +echo "Bag: ${BAG_NAME}" +echo "Path: ${BAG_DIR}" +echo "Rate: ${RATE}x" +echo "=========================================" +echo "" + +# Check if bag exists +if [ ! -f "${BAG_DIR}/metadata.yaml" ]; then + echo "ERROR: Bag not found at ${BAG_DIR}" + echo "" + echo "Available bags:" + ls -1 "$(dirname "$0")/../bags" 2>/dev/null || echo " (none)" + echo "" + echo "To record a new bag:" + echo " npm run bag:record -- ${BAG_NAME}" + exit 1 +fi + +# Show bag info +ros2 bag info "$BAG_DIR" +echo "" +echo "Playing in loop mode. Press Ctrl+C to stop." +echo "" + +# Play bag in loop mode +ros2 bag play "$BAG_DIR" -l -r "$RATE" diff --git a/scripts/record-bag.sh b/scripts/record-bag.sh new file mode 100644 index 0000000..7ea3301 --- /dev/null +++ b/scripts/record-bag.sh @@ -0,0 +1,90 @@ +#!/bin/bash +# Helper script to record a test bag from the GUI's synthetic simulation +# Usage: ./scripts/record-bag.sh [bag_name] [duration_seconds] + +# Source ROS2 setup +source /opt/ros/humble/setup.bash + +BAG_NAME="${1:-pool_test_01}" +DURATION="${2:-15}" +BAG_DIR="$(cd "$(dirname "$0")/.." && pwd)/bags/${BAG_NAME}" + +echo "=========================================" +echo "Recording ROS2 bag for ${DURATION} seconds" +echo "Bag: ${BAG_NAME}" +echo "Path: ${BAG_DIR}" +echo "=========================================" +echo "" +echo "Topics to record:" +echo " - /imu/data (sensor_msgs/Imu)" +echo " - /dvl/odom (nav_msgs/Odometry)" +echo " - /depth/pose (geometry_msgs/PoseWithCovarianceStamped)" +echo "" +echo "Before recording:" +echo " 1. Start the GUI (npm start)" +echo " 2. Set Data Source: Synthetic Simulation" +echo " 3. Click 'Start Simulation'" +echo " 4. Wait a few seconds for topics to stabilize" +echo "" +read -p "Press Enter when simulation is running, or Ctrl+C to cancel..." + +# Remove existing bag if present +if [ -d "$BAG_DIR" ]; then + echo "Removing existing bag at $BAG_DIR" + rm -rf "$BAG_DIR" +fi + +echo "" +echo "Recording for ${DURATION} seconds..." +echo "Press Ctrl+C to stop recording." +echo "" + +# Start recording in background and capture its PID +ros2 bag record \ + /imu/data \ + /dvl/odom \ + /depth/pose \ + -o "$BAG_DIR" & + +RECORD_PID=$! + +# Function to kill the recording process +cleanup() { + echo "" + echo "Stopping recording..." + kill $RECORD_PID 2>/dev/null + wait $RECORD_PID 2>/dev/null + echo "Recording stopped." +} + +# Set up trap to catch Ctrl+C +trap cleanup SIGINT SIGTERM + +# Wait for the specified duration +sleep ${DURATION} + +# Stop recording +cleanup + +echo "" +echo "=========================================" +echo "Recording complete!" +echo "=========================================" +echo "" + +# Show bag info +if [ -f "${BAG_DIR}/metadata.yaml" ]; then + ros2 bag info "$BAG_DIR" + echo "" + echo "To play this bag:" + echo " npm run bag:play -- ${BAG_NAME}" + echo "" + echo "Or manually:" + echo " ros2 bag play ${BAG_DIR} -l" +else + echo "ERROR: No metadata.yaml found. Recording may have failed." + echo "Make sure:" + echo " - rosbridge is running (ros2 launch rosbridge_server rosbridge_websocket_launch.xml)" + echo " - GUI simulation is publishing topics" + echo " - You can see topics with: ros2 topic list" +fi diff --git a/src/components/RosContext.tsx b/src/components/RosContext.tsx index 323902f..7b44cb0 100644 --- a/src/components/RosContext.tsx +++ b/src/components/RosContext.tsx @@ -37,8 +37,44 @@ export function RosProvider({ children }: RosProviderProps) { }); + /** + * resolve the ROS bridge websocket URL, made it ***Dynamic*** :) + * priority (highest to lowest): + * - URL query params: ?ros_host=HOST&ros_port=PORT or ?ros_ws=ws://host:port + * - localStorage: ros.host, ros.port, ros.ws + * - ENV (build-time): REACT_APP_ROSBRIDGE_HOST, REACT_APP_ROSBRIDGE_PORT + * - if page host is not localhost/127.0.0.1, use window.location.hostname + * - fallback default: localhost:9090 (works with WSL2) + */ + function getRosBridgeUrl(): string { + try { + const params = new URLSearchParams(window.location.search); + const qpWs = params.get('ros_ws'); + if (qpWs) return qpWs; + + const lsWs = localStorage.getItem('ros.ws'); + if (lsWs) return lsWs; + + const qpHost = params.get('ros_host') ?? localStorage.getItem('ros.host') ?? process.env.REACT_APP_ROSBRIDGE_HOST ?? ''; + const qpPort = params.get('ros_port') ?? localStorage.getItem('ros.port') ?? process.env.REACT_APP_ROSBRIDGE_PORT ?? ''; + + const pageHost = window.location.hostname; + // default to localhost (works with WSL2), or use page hostname if deployed remotely + const defaultHost = (pageHost && pageHost !== 'localhost' && pageHost !== '127.0.0.1') ? pageHost : 'localhost'; + const host = qpHost || defaultHost; + const port = qpPort || '9090'; + const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'; + return `${protocol}://${host}:${port}`; + } catch { + // safe fallback + return 'ws://localhost:9090'; + } + } + function connect_to_ros() { - rosRef.current.connect('ws://localhost:9090'); + const url = getRosBridgeUrl(); + console.log(`[ROS] connecting to ${url}`); + rosRef.current.connect(url); } // on start up diff --git a/src/components/SimulationControl.css b/src/components/SimulationControl.css new file mode 100644 index 0000000..e2a8c4c --- /dev/null +++ b/src/components/SimulationControl.css @@ -0,0 +1,241 @@ +/* Simulation Control - layout, header, collapsed content, and status borders */ +.simulation-wrapper { + width: 100%; + display: flex; + justify-content: flex-start; + margin-bottom: 16px; + transition: margin-bottom 0.3s ease; +} + +.simulation-wrapper:not(.expanded) { + width: 100%; + height: fit-content; + margin-bottom: 2px; +} + +.simulation-control { + /* removed debug border */ + border-radius: 8px; + margin-bottom: 0; + background-color: #ffffff; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + overflow: hidden; + transition: all 0.3s ease; + width: 100%; + min-height: fit-content; + border: 2px solid transparent; +} + +/* Status-based borders */ +.simulation-control.stopped { + border-color: #e53935; /* match bad card red */ +} + +.simulation-control.running { + border-color: #4caf50; /* match success green */ +} + +.simulation-control:not(.expanded) { + margin-bottom: 0; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + width: 100%; + min-width: 260px; + height: fit-content; + min-height: auto; +} + +.simulation-header { + cursor: pointer; + background-color: #f8fafc; + padding: 0; + transition: background-color 0.2s ease, border-radius 0.3s ease; + user-select: none; +} + +.simulation-header:hover { + background-color: #f1f5f9; +} + +.simulation-control:not(.expanded) .simulation-header { + border-radius: 7px; +} + +.header-content { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + transition: padding 0.3s ease; + white-space: nowrap; +} + +.header-content h4 { + margin: 0; + font-size: 0.95rem; + font-weight: 600; + color: #334155; + flex-shrink: 0; + line-height: 1.2; +} + +.header-status { + display: flex; + align-items: center; + gap: 12px; + flex-shrink: 0; +} + +.status-badge { + padding: 3px 8px; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.025em; + transition: all 0.3s ease; +} + +.status-badge.running { + background-color: #dcfce7; + color: #166534; +} + +.status-badge.stopped { + background-color: #fef2f2; + color: #991b1b; +} + +.expand-chevron { + font-size: 0.8rem; + color: #64748b; + transition: transform 0.3s ease; + transform: rotate(-90deg); +} + +.expand-chevron.expanded { + transform: rotate(0deg); +} + +.simulation-content { + overflow: hidden; + transition: max-height 0.3s ease-out, padding 0.3s ease-out, opacity 0.2s ease-out; + max-height: 0; + padding: 0 16px; + opacity: 0; +} + +.simulation-content.expanded { + max-height: 200px; + padding: 16px; + border-top: 1px solid #e2e8f0; + opacity: 1; +} + +.simulation-content.collapsed { + max-height: 0 !important; + padding: 0 16px !important; + margin: 0 !important; + border-top: 0 !important; +} + +.simulation-grid { + display: flex; + flex-direction: column; + gap: 12px; +} + +.control-group { + display: flex; + flex-direction: column; + gap: 6px; +} + +.control-group label { + font-size: 0.875rem; + font-weight: 500; + color: #374151; +} + +.scenario-select { + padding: 8px 12px; + border: 1px solid #d1d5db; + border-radius: 6px; + background-color: #ffffff; + color: #374151; + font-size: 0.875rem; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.scenario-select:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +.scenario-select:disabled { + background-color: #f3f4f6; + color: #9ca3af; + cursor: not-allowed; +} + +.sim-toggle-btn { + padding: 10px 16px; + border: none; + border-radius: 6px; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + align-self: flex-start; +} + +.sim-toggle-btn.start { + background-color: #10b981; + color: white; +} + +.sim-toggle-btn.start:hover { + background-color: #059669; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(16, 185, 129, 0.3); +} + +.sim-toggle-btn.stop { + background-color: #ef4444; + color: white; +} + +.sim-toggle-btn.stop:hover { + background-color: #dc2626; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(239, 68, 68, 0.3); +} + +.simulation-info { + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid #f1f5f9; +} + +.simulation-info span { + color: #64748b; + font-size: 0.8rem; + font-style: italic; +} + +@media (min-width: 640px) { + .simulation-grid { + flex-direction: row; + align-items: end; + gap: 16px; + } + + .control-group { + flex: 1; + } + + .sim-toggle-btn { + align-self: end; + white-space: nowrap; + } +} diff --git a/src/components/SimulationControl.tsx b/src/components/SimulationControl.tsx new file mode 100644 index 0000000..9d95b24 --- /dev/null +++ b/src/components/SimulationControl.tsx @@ -0,0 +1,467 @@ +/** + * SimulationControl + * - Collapsible panel to publish fake sensor data + * - IMU: ~20Hz, DVL: ~10Hz, Depth: ~16Hz (realistic rates) + * - Provides scenarios (idle, dive, surface, forward, circle, wobble) + * - Status-aware header and ROS bridge topic publishers + */ +import React, { useState, useRef, useEffect } from 'react'; +import { useRos } from './RosContext'; +import { useTopic } from '../hooks/useTopic'; +import * as ROSLIB from 'roslib'; +import './SimulationControl.css'; + +interface SimulationControlProps { + connected: boolean; +} + +const SimulationControl: React.FC = ({ connected }) => { + // Simulation state + const [isSimulating, setIsSimulating] = useState(false); + const [simulationScenario, setSimulationScenario] = useState(() => { + try { + const v = localStorage.getItem('sim.scenario'); + return v ?? 'idle'; + } catch { return 'idle'; } + }); + const [isExpanded, setIsExpanded] = useState(() => { + try { + const v = localStorage.getItem('sim.expanded'); + return v ? v === '1' : false; + } catch { return false; } + }); + // Use drift-compensated timer for IMU to hit ~20Hz reliably in browsers + const imuTimerRef = useRef(null); + const dvlTimerRef = useRef(null); + const depthTimerRef = useRef(null); + const stateTimerRef = useRef(null); + + const { ros } = useRos(); + // Data source selection + const [dataSource, setDataSource] = useState<'synthetic' | 'bag'>(() => { + try { return (localStorage.getItem('sim.source') as any) || 'synthetic'; } catch { return 'synthetic'; } + }); + // Subscribe to topics for seeding + const [imuMsg] = useTopic('/imu/data', 'sensor_msgs/Imu'); + const [depthMsg] = useTopic('/depth/pose', 'geometry_msgs/PoseWithCovarianceStamped'); + const [dvlMsg] = useTopic('/dvl/odom', 'nav_msgs/Odometry'); + // Seeding feedback state + const [seededAt, setSeededAt] = useState(null); + const [seedSummary, setSeedSummary] = useState(''); + + // Publishers for fake data + const imuPublisher = useRef(null); + const depthPublisher = useRef(null); + const dvlPublisher = useRef(null); + + // Simulation state variables + const simStateRef = useRef({ + time: 0, + depth: -2.0, // Starting depth (negative is underwater) + orientation: { roll: 0, pitch: 0, yaw: 0 }, + velocity: { x: 0, y: 0, z: 0 }, + position: { x: 0, y: 0, z: -2.0 } + }); + + // Initialize publishers when connected + useEffect(() => { + if (connected && ros && dataSource === 'synthetic') { + imuPublisher.current = new ROSLIB.Topic({ + ros: ros, + name: '/imu/data', + messageType: 'sensor_msgs/Imu' + }); + + depthPublisher.current = new ROSLIB.Topic({ + ros: ros, + name: '/depth/pose', + messageType: 'geometry_msgs/PoseWithCovarianceStamped' + }); + + dvlPublisher.current = new ROSLIB.Topic({ + ros: ros, + name: '/dvl/odom', + messageType: 'nav_msgs/Odometry' + }); + } + if (connected && ros && dataSource === 'bag') { + if (imuPublisher.current) { imuPublisher.current.unadvertise(); imuPublisher.current = null; } + if (depthPublisher.current) { depthPublisher.current.unadvertise(); depthPublisher.current = null; } + if (dvlPublisher.current) { dvlPublisher.current.unadvertise(); dvlPublisher.current = null; } + } + }, [connected, ros, dataSource]); + + // Helper function to convert euler angles to quaternion + const eulerToQuaternion = (roll: number, pitch: number, yaw: number) => { + const cy = Math.cos(yaw * 0.5); + const sy = Math.sin(yaw * 0.5); + const cp = Math.cos(pitch * 0.5); + const sp = Math.sin(pitch * 0.5); + const cr = Math.cos(roll * 0.5); + const sr = Math.sin(roll * 0.5); + + return { + w: cr * cp * cy + sr * sp * sy, + x: sr * cp * cy - cr * sp * sy, + y: cr * sp * cy + sr * cp * sy, + z: cr * cp * sy - sr * sp * cy + }; + }; + + // Quaternion -> Euler (for seeding from IMU) + const quaternionToEuler = (x: number, y: number, z: number, w: number) => { + const sinr_cosp = 2 * (w * x + y * z); + const cosr_cosp = 1 - 2 * (x * x + y * y); + const roll = Math.atan2(sinr_cosp, cosr_cosp); + + const sinp = 2 * (w * y - z * x); + const pitch = Math.abs(sinp) >= 1 ? Math.sign(sinp) * Math.PI / 2 : Math.asin(sinp); + + const siny_cosp = 2 * (w * z + x * y); + const cosy_cosp = 1 - 2 * (y * y + z * z); + const yaw = Math.atan2(siny_cosp, cosy_cosp); + return { roll, pitch, yaw }; + }; + + // Seed initial sim state from latest ROS topics + const seedFromRosTopics = () => { + const s = simStateRef.current; + let seededParts: string[] = []; + + const q = (imuMsg as any)?.orientation; + if (q && typeof q.x === 'number' && typeof q.y === 'number' && typeof q.z === 'number' && typeof q.w === 'number') { + const e = quaternionToEuler(q.x, q.y, q.z, q.w); + s.orientation = { roll: e.roll, pitch: e.pitch, yaw: e.yaw }; + seededParts.push('orientation from /imu/data'); + } + + const p = (depthMsg as any)?.pose?.pose?.position; + if (p && typeof p.z === 'number') { + s.position.z = p.z; + s.depth = -p.z; // assume z-up + seededParts.push('depth from /depth/pose'); + } + + const v = (dvlMsg as any)?.twist?.twist?.linear; + if (v && (typeof v.x === 'number' || typeof v.y === 'number' || typeof v.z === 'number')) { + s.velocity = { x: v.x ?? 0, y: v.y ?? 0, z: v.z ?? 0 }; + seededParts.push('velocity from /dvl/odom'); + } + + if (seededParts.length === 0) { + setSeedSummary('No recent messages available to seed. Make sure rosbag2 is playing and topics are active.'); + setSeededAt(Date.now()); + return; + } + + setSeedSummary(`Seeded ${seededParts.join(', ')}`); + setSeededAt(Date.now()); + // Force a tiny state change to ensure any UI bound to sim state can reflect updates + // (simStateRef updates alone do not trigger a re-render) + // We reuse seedSummary/seededAt states above for this purpose. + // Optional: console for debugging + // eslint-disable-next-line no-console + console.log('[Simulation] Seeded from ROS topics:', { orientation: q, positionZ: p?.z, velocity: v }); + }; + + // Simulation scenarios - update state based on scenario + const updateSimulationState = (dt: number) => { + const state = simStateRef.current; + state.time += dt; + + // Apply scenario-specific motions + switch (simulationScenario) { + case 'dive': + state.velocity.z = -0.5; // Diving down + state.position.z += state.velocity.z * dt; + state.depth = -state.position.z; + break; + + case 'surface': + state.velocity.z = 0.3; // Rising up + state.position.z += state.velocity.z * dt; + state.depth = -state.position.z; + if (state.depth < 0) state.depth = 0; // Can't go above surface + break; + + case 'forward': + state.velocity.x = 1.0; // Moving forward + state.position.x += state.velocity.x * dt; + break; + + case 'circle': + const radius = 5.0; + const angularVel = 0.1; + state.position.x = radius * Math.cos(state.time * angularVel); + state.position.y = radius * Math.sin(state.time * angularVel); + state.velocity.x = -radius * angularVel * Math.sin(state.time * angularVel); + state.velocity.y = radius * angularVel * Math.cos(state.time * angularVel); + state.orientation.yaw = state.time * angularVel; + break; + + case 'wobble': + // Simulate rough waters or instability + state.orientation.roll = 0.1 * Math.sin(state.time * 2); + state.orientation.pitch = 0.05 * Math.cos(state.time * 3); + break; + + default: // idle + state.velocity.x = 0; + state.velocity.y = 0; + state.velocity.z = 0; + } + }; + + // Publish IMU data (~20Hz) + const publishIMU = () => { + const state = simStateRef.current; + const noise = () => (Math.random() - 0.5) * 0.01; + + if (imuPublisher.current) { + const quat = eulerToQuaternion( + state.orientation.roll + noise(), + state.orientation.pitch + noise(), + state.orientation.yaw + noise() + ); + + const imuMsg = new ROSLIB.Message({ + header: { + stamp: { sec: Math.floor(Date.now() / 1000), nanosec: 0 }, + frame_id: 'imu_link' + }, + orientation: quat, + linear_acceleration: { + x: state.velocity.x * 0.1 + noise(), + y: state.velocity.y * 0.1 + noise(), + z: 9.81 + state.velocity.z * 0.1 + noise() + }, + angular_velocity: { + x: (state.orientation.roll - (simStateRef.current.orientation.roll || 0)) / 0.05 + noise(), + y: (state.orientation.pitch - (simStateRef.current.orientation.pitch || 0)) / 0.05 + noise(), + z: (state.orientation.yaw - (simStateRef.current.orientation.yaw || 0)) / 0.05 + noise() + } + }); + imuPublisher.current.publish(imuMsg); + } + }; + + // Publish Depth data (~16Hz) + const publishDepth = () => { + const state = simStateRef.current; + const noise = () => (Math.random() - 0.5) * 0.01; + + if (depthPublisher.current) { + const depthMsg = new ROSLIB.Message({ + header: { + stamp: { sec: Math.floor(Date.now() / 1000), nanosec: 0 }, + frame_id: 'depth_link' + }, + pose: { + pose: { + position: { + x: state.position.x, + y: state.position.y, + z: state.depth + noise() + }, + orientation: { x: 0, y: 0, z: 0, w: 1 } + } + } + }); + depthPublisher.current.publish(depthMsg); + } + }; + + // Publish DVL data (~10Hz) + const publishDVL = () => { + const state = simStateRef.current; + const noise = () => (Math.random() - 0.5) * 0.01; + + if (dvlPublisher.current) { + const dvlMsg = new ROSLIB.Message({ + header: { + stamp: { sec: Math.floor(Date.now() / 1000), nanosec: 0 }, + frame_id: 'dvl_link' + }, + twist: { + twist: { + linear: { + x: state.velocity.x + noise(), + y: state.velocity.y + noise(), + z: state.velocity.z + noise() + }, + angular: { x: 0, y: 0, z: 0 } + } + } + }); + dvlPublisher.current.publish(dvlMsg); + } + }; + + // Start/stop simulation + const toggleSimulation = () => { + if (dataSource === 'bag') return; // don't publish in bag mode + if (isSimulating) { + // Stop all sensor publishers and state updater + if (stateTimerRef.current) { + clearInterval(stateTimerRef.current); + stateTimerRef.current = null; + } + if (imuTimerRef.current !== null) { + window.clearTimeout(imuTimerRef.current); + imuTimerRef.current = null; + } + if (dvlTimerRef.current) { + clearInterval(dvlTimerRef.current); + dvlTimerRef.current = null; + } + if (depthTimerRef.current) { + clearInterval(depthTimerRef.current); + depthTimerRef.current = null; + } + setIsSimulating(false); + } else { + // Start state updater at high frequency + stateTimerRef.current = setInterval(() => updateSimulationState(0.02), 20); // 50Hz state updates + + // Start IMU with drift-compensated timer (~20Hz) + const imuPeriod = 50; // ms + const startIMULoop = () => { + let next = performance.now() + imuPeriod; + const tick = () => { + publishIMU(); + const now = performance.now(); + // schedule next run compensating for drift + next += imuPeriod; + const delay = Math.max(0, next - now); + imuTimerRef.current = window.setTimeout(tick, delay) as unknown as number; + }; + imuTimerRef.current = window.setTimeout(tick, imuPeriod) as unknown as number; + }; + startIMULoop(); + // Start remaining publishers at their respective rates + dvlTimerRef.current = setInterval(publishDVL, 100); // ~10Hz + depthTimerRef.current = setInterval(publishDepth, 62.5); // ~16Hz + setIsSimulating(true); + } + }; + + // Persist scenario and expanded state + useEffect(() => { + try { localStorage.setItem('sim.scenario', simulationScenario); } catch {} + }, [simulationScenario]); + + useEffect(() => { + try { localStorage.setItem('sim.expanded', isExpanded ? '1' : '0'); } catch {} + }, [isExpanded]); + + // Persist data source and stop timers when switching to bag + useEffect(() => { + try { localStorage.setItem('sim.source', dataSource); } catch {} + if (dataSource === 'bag' && isSimulating) { + if (stateTimerRef.current) { clearInterval(stateTimerRef.current); stateTimerRef.current = null; } + if (imuTimerRef.current !== null) { window.clearTimeout(imuTimerRef.current); imuTimerRef.current = null; } + if (dvlTimerRef.current) { clearInterval(dvlTimerRef.current); dvlTimerRef.current = null; } + if (depthTimerRef.current) { clearInterval(depthTimerRef.current); depthTimerRef.current = null; } + setIsSimulating(false); + } + }, [dataSource]); + + // Cleanup on unmount + useEffect(() => { + return () => { + if (stateTimerRef.current) clearInterval(stateTimerRef.current); + if (imuTimerRef.current !== null) window.clearTimeout(imuTimerRef.current); + if (dvlTimerRef.current) clearInterval(dvlTimerRef.current); + if (depthTimerRef.current) clearInterval(depthTimerRef.current); + }; + }, []); + + if (!connected) return null; + + return ( +
+
+
setIsExpanded(!isExpanded)}> +
+

Sensor Data Simulation

+
+ + {isSimulating ? 'Running' : 'Stopped'} + + + ▼ + +
+
+
+ +
+
+
+ + +
+
+ + +
+ + {dataSource === 'synthetic' ? ( + + ) : ( + + )} +
+ +
+ {dataSource === 'synthetic' ? ( + Publishing to /imu/data (~20Hz), /dvl/odom (~10Hz), /depth/pose (~16Hz) + ) : ( + Listening to /imu/data, /dvl/odom, /depth/pose (run: ros2 bag play ...) + )} +
+ {seededAt && ( +
+ {seedSummary} — {new Date(seededAt).toLocaleTimeString()} +
+ )} +
+
+
+ ); +}; + +export default SimulationControl; diff --git a/src/components/preflight.css b/src/components/preflight.css index 75ea440..69a26ac 100644 --- a/src/components/preflight.css +++ b/src/components/preflight.css @@ -11,23 +11,61 @@ max-width: 1200px; margin-left: auto; margin-right: auto; + gap: 0; } -/* Top row containing bad and good cards */ -.preflight-card::before { - content: ""; - display: flex; - flex: 1; +/* ROS Connection Status Styling */ +.ros-connection-status { + padding: 12px 16px; + border-radius: 8px; + margin-bottom: 16px; + text-align: center; + transition: all 0.3s ease; +} + +.ros-connection-status.connected { + background-color: #e8f5e8; + border: 2px solid #4caf50; + color: #2e7d32; +} + +.ros-connection-status.disconnected { + background-color: #ffebee; + border: 2px solid #f44336; + color: #c62828; +} + +.ros-connection-status h3 { + margin: 0 0 8px 0; + font-size: 1.2em; +} + +.ros-connection-status p { + margin: 0; + font-size: 0.9em; +} + +/* Simulation container */ +.simulation-container { margin-bottom: 16px; + transition: margin-bottom 0.3s ease; + min-height: fit-content; +} + +/* Compact spacing when simulation panel is collapsed */ +.simulation-container:has(.simulation-control:not(.expanded)) { + margin-bottom: 2px; } -/* Bad and good card container (flex row) */ -.preflight-card > div:not(.preflight-logs-card) { + + +/* Topic status row (bad/good columns) */ +.preflight-top-row { display: flex; flex-direction: row; gap: 16px; margin-bottom: 16px; - height: 200px; /* Fixed height for top cards */ + align-items: stretch; } /* Bad card - for error messages */ @@ -67,7 +105,7 @@ overflow-y: auto; } -/* Style for good card items - UPDATED to prevent resizing */ +/* Style for good card items (prevent layout shift) */ .good-card-li { color: #2e7d32; font-weight: bold; @@ -77,7 +115,7 @@ border-radius: 4px; list-style-type: none; - /* NEW: Fixed width properties to prevent layout shifts */ + /* Fixed width properties to prevent layout shifts */ width: calc(100% - 24px); box-sizing: border-box; display: flex; @@ -85,7 +123,7 @@ font-family: 'Courier New', monospace; } -/* NEW: Create a container for the changing Hz value */ +/* Reserved area for the Hz value */ .good-card-li::after { content: ""; display: inline-block; @@ -93,7 +131,7 @@ text-align: right; } -/* Logs card - UPDATED: increased height */ +/* Logs card */ .preflight-logs-card { display: flex; flex-direction: row; @@ -101,22 +139,22 @@ border-radius: 8px; padding: 12px; background-color: #f3e5f5; - height: 300px; /* CHANGED: Increased from 200px to 300px */ - overflow-y: hidden; /* CHANGED: from auto to hidden */ + height: 300px; + overflow-y: hidden; } -/* NEW: Completely redesigned IMU log section */ +/* IMU log section */ .imu-log { flex: 1; padding: 8px; border-right: 1px dashed #b39ddb; display: grid; - grid-template-columns: 1fr 1fr; /* Create 2 columns */ + grid-template-columns: 1fr 1fr; grid-column-gap: 10px; align-content: start; } -/* NEW: Add header to IMU section */ +/* IMU header */ .imu-log::before { content: "IMU Data"; grid-column: 1 / -1; @@ -126,7 +164,7 @@ border-bottom: 1px solid #b39ddb; } -/* UPDATED: Log lists within IMU section */ +/* IMU list container */ .imu-log .log-list { list-style-type: none; padding: 0; @@ -134,7 +172,7 @@ display: contents; /* Make list part of grid */ } -/* UPDATED: Log items within IMU section */ +/* IMU list items */ .imu-log .log-list-item { font-family: monospace; padding: 4px 0; @@ -145,7 +183,7 @@ font-size: 0.9em; } -/* NEW: Visual grouping for IMU data types */ +/* Visual grouping for IMU data types */ .imu-log .log-list-item:nth-child(-n+4) { background-color: rgba(179, 157, 219, 0.1); /* Quaternion background */ } @@ -158,7 +196,7 @@ background-color: rgba(100, 181, 246, 0.1); /* Angular twist background */ } -/* Updated Depth log section to match IMU and DVL styles */ +/* Depth log section */ .depth-log { flex: 1; padding: 8px; @@ -166,8 +204,6 @@ display: flex; flex-direction: column; } - -/* Add header to Depth section similar to others */ .depth-log::before { content: "Depth Sensor"; font-weight: bold; @@ -176,36 +212,28 @@ border-bottom: 1px solid #b39ddb; } -/* Style the depth value container */ -.depth-log { +/* Depth value presentation (container, label, and units) */ +.depth-log::before { display: flex; flex-direction: column; } - -/* Create a styled container for the depth value */ .depth-log::after { content: attr(data-unit); font-size: 0.9em; color: #7e57c2; margin-top: 4px; } - -/* Style for the actual depth value */ .depth-log { position: relative; display: flex; flex-direction: column; } - -/* Create proper labeling for depth value */ .depth-log::after { content: "joe handsome"; font-size: 0.8em; color: #7e57c2; margin-top: 4px; } - -/* Make depth value stand out */ .depth-log > div { background-color: rgba(179, 157, 219, 0.2); border-radius: 8px; @@ -218,8 +246,6 @@ font-family: 'Courier New', monospace; position: relative; } - -/* Add depth label */ .depth-log > div::before { content: "Z Position:"; position: absolute; @@ -229,13 +255,9 @@ font-weight: normal; color: #5e35b1; } - -/* Add decimal precision formatting for depth */ .depth-log > div { position: relative; } - -/* Format the depth value to show proper decimals */ .depth-log > div::after { content: " m"; font-size: 20px; @@ -276,7 +298,7 @@ align-self: center; margin: auto; } -/* DVL log section - CSS only solution */ +/* DVL log section */ .dvl-log { flex: 1; padding: 8px; diff --git a/src/components/preflight.tsx b/src/components/preflight.tsx index 93f395d..5133ff3 100644 --- a/src/components/preflight.tsx +++ b/src/components/preflight.tsx @@ -1,9 +1,11 @@ -import React, { useEffect, useRef } from 'react' +import React from 'react' import './preflight.css' import { useTopic } from '../hooks/useTopic'; +import { useRos } from './RosContext'; import { PoseWithCovarianceStamped } from '../ros_msg_types/geometry_msgs' import { Odometry } from '../ros_msg_types/nav_msgs'; import { Imu } from '../ros_msg_types/sensor_msgs'; +import SimulationControl from './SimulationControl'; /* * @@ -21,6 +23,7 @@ function create_dead_topic_list_item(topic_hz: number, msg: string) { return (
  • {msg}
  • ); } +/** Helper: render a good item if the topic is publishing (hz > 0). */ function create_alive_topic_list_item(topic_hz: number, msg: string) { const topic_is_dead = topic_hz === 0 if (topic_is_dead) { return } @@ -29,6 +32,9 @@ function create_alive_topic_list_item(topic_hz: number, msg: string) { } function Preflight() { + // Get ROS connection status + const { connected } = useRos(); + // TODO add camera topics and display what the cameras see (either before or after AI) const [imu_msg, imu_hz, _imu_topic] = useTopic('/imu/data', 'sensor_msgs/Imu'); const [depth_msg, depth_hz, _depth_topic] = useTopic('/depth/pose', 'geometry_msgs/PoseWithCovarianceStamped'); @@ -37,19 +43,37 @@ function Preflight() { return ( <>
    -
    - {create_dead_topic_list_item(imu_hz, "imu lost!")} - {create_dead_topic_list_item(dvl_hz, "dvl lost!")} - {create_dead_topic_list_item(depth_hz, "depth lost!")} + {/* ROS connection status */} +
    +

    ROS Bridge Status: {connected ? '🟢 Connected' : '🔴 Disconnected'}

    + {connected ? ( +

    ✅ Successfully connected to ROS bridge server (ws://localhost:9090)

    + ) : ( +

    ❌ Unable to connect to ROS bridge server. Make sure it's running.

    + )}
    -
    - {create_alive_topic_list_item(imu_hz, `Imu Hz: ${imu_hz}`)} - {create_alive_topic_list_item(dvl_hz, `dvl Hz: ${dvl_hz}`)} - {create_alive_topic_list_item(depth_hz, `Depth Hz: ${depth_hz}`)} + {/* Simulation Control */} +
    +
    -
    +
    +
    + {!connected &&
  • ROS Bridge disconnected!
  • } + {create_dead_topic_list_item(imu_hz, "imu lost!")} + {create_dead_topic_list_item(dvl_hz, "dvl lost!")} + {create_dead_topic_list_item(depth_hz, "depth lost!")} +
    + +
    + {create_alive_topic_list_item(imu_hz, `Imu Hz: ${imu_hz}`)} + {create_alive_topic_list_item(dvl_hz, `dvl Hz: ${dvl_hz}`)} + {create_alive_topic_list_item(depth_hz, `Depth Hz: ${depth_hz}`)} +
    +
    + +
    • {`quat x: ${imu_msg?.orientation.x}`}
    • diff --git a/src/hooks/useTopic.ts b/src/hooks/useTopic.ts index 62368b4..6f58711 100644 --- a/src/hooks/useTopic.ts +++ b/src/hooks/useTopic.ts @@ -30,10 +30,14 @@ export function useTopic(topicName: string, messageT intervalRef.current = setInterval(() => { - const topic_is_dead = timesRef.current.length === 0 || Date.now() - timesRef.current[timesRef.current.length - 1] > TOPIC_TIMEOUT_SECONDS * 1000 + const topic_is_dead = + timesRef.current.length === 0 || + Date.now() - timesRef.current[timesRef.current.length - 1] > TOPIC_TIMEOUT_SECONDS * 1000; if (topic_is_dead) { - timesRef.current = [] - setHz(0) + timesRef.current = []; + setHz(0); + // Clear last message so dependent UI knows the topic is inactive + setMessage(null as any); } }, TOPIC_TIMEOUT_SECONDS * 1000);