From 4d76bf41e7918462ce097042a365a65244fccc40 Mon Sep 17 00:00:00 2001 From: Andrew <112200249+andytgarcia@users.noreply.github.com> Date: Tue, 20 Jan 2026 19:04:55 -0800 Subject: [PATCH 01/10] added README (#2) --- README.md | 119 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..555432c --- /dev/null +++ b/README.md @@ -0,0 +1,119 @@ +# Neptune +Yonder Deep 2026 + +## To Get Started: +READ DOCS/docker.md + +## Branch Management + +### Main Branch +- `main` is the primary working branch and should always contain stable, tested code +- All code in `main` must pass automated tests and peer review +- Do not force push to `main`, create your own branch before working on your issue. + +### Development Branches +- Create feature branches for individual or subteam work: `feature/sensor-integration`, `feature/navigation-system` +- Use descriptive branch names: `andytgarcia/pressure-sensor-calibration`, `yonderdeep/motor-controller-pid` +- Branch naming convention: `yourgithubusername/brief-descriptive-name` +- Delete branches after merging to keep the repository clean + +### Merge Process +1. Create a pull request from your branch to `main` +2. Ensure all tests pass (or write your own) +3. Request review from at least one team member +4. Address review comments +5. Merge only after approval + +## Code Style Guidelines + +### Indentation +- Nested blocks should be indented one level deeper + +### Line Lengths +- Make lines entirely readable on screen +- Break long lines logically at operators, commas, or before opening parentheses +- Comments should also respect the line length limit + +### Brace Style + +```cpp +if (condition) { + // code here +} +else { + // alternative code +} + +void functionName() { + // function body +} +``` + +### Naming Conventions + +#### Variables +- Use `camelCase` for local variables: `sensorData`, `motorSpeed` +- Use `UPPER_SNAKE_CASE` for constants: `MAX_DEPTH`, `SENSOR_TIMEOUT_MS` +- Use descriptive names that convey purpose: `currentDepth` instead of `d` + +#### Functions +- Use `camelCase` for function names: `calculateBuoyancy()`, `updateMotorController()` +- Use verb-noun pairs for clarity: `readPressureSensor()`, `setThrottle()` + +#### Classes +- Use `PascalCase` for class names: `NavigationSystem`, `PressureSensor` +- Use nouns or noun phrases: `MotorController`, `DataLogger` + +#### Files +- Use `snake_case` for filenames: `motor_controller.cpp`, `pressure_sensor.h` +- Header files use `.h` or `.hpp` extension +- Implementation files use `.cpp` or `.c` extension + +## Documentation Standards + +### Header Files +Use '#pragma once' for all .h and .hpp files + +```cpp + +#pragma once + +class ClassName + +``` + +### Class Headers +Document every class with its purpose and usage: + +```cpp +/** + * @class PressureSensor + * @brief Interface for MS5837 pressure/temperature sensor + * + * Provides depth and temperature readings using I2C communication. + * Handles calibration and conversion to engineering units. + */ +class PressureSensor { + // class definition +}; +``` + +### Function Headers +Document all public functions and complex private functions: + + +### Inline Comments +- Use inline comments for complex logic or non-obvious decisions +- Explain the "why," not the "what" +- Keep comments up-to-date with code changes + + +### First Time Contributors +1. Read this entire README +2. Review the existing codebase to understand project structure +3. Set up your development environment +4. Start with a small issue labeled "good first issue" +5. Ask questions in Discord + + +For questions or suggestions about these guidelines, please open an issue or contact the project maintainer. \ No newline at end of file From 3ebd2fcab4c250016f005deb2c349e619a1ff2b5 Mon Sep 17 00:00:00 2001 From: Suhaan Khurana <114335244+SuhaanCoding@users.noreply.github.com> Date: Fri, 13 Feb 2026 17:43:27 -0800 Subject: [PATCH 02/10] ci: add lint CI workflow and pre-commit config (#8) * ci: add lint CI workflow and pre-commit config Add GitHub Actions workflow that runs pre-commit hooks on all pushes and PRs to main/master. Includes pre-commit-config with clang-format, ESLint, Prettier, and yamllint. Legacy auv/ directory is excluded. * ci: fix pre-commit hooks and auto-fix existing lint issues Fix ESLint/Prettier hooks to strip repo-root prefix from file paths before cd-ing into web_app/frontend_gui. Exclude tsconfig.json from check-json (it uses JSONC with comments). Auto-fix trailing whitespace, missing final newlines, and prettier formatting across the codebase. * ci: use local node_modules binaries instead of npx npx downloads fresh eslint/prettier versions in CI instead of using the packages installed by npm ci. Use ./node_modules/.bin/ directly. * ci: add missing eslint/prettier devDependencies to package.json The lint CI workflow fails because eslint, prettier, and all eslint plugins were missing from package.json. npm ci in CI never installed them, causing pre-commit hooks to fail with "No such file or directory". --- .devcontainer/devcontainer.json | 2 +- .github/workflows/lint.yml | 31 + .pre-commit-config.yaml | 58 + .yamllint.yml | 15 + Docker/Dockerfile | 7 +- Docs/Docker.md | 5 +- Docs/Running.md | 2 - README.md | 6 +- auv_cpp/.clang-format | 2 +- auv_cpp/.editorconfig | 2 +- auv_cpp/.gitignore | 2 +- auv_cpp/CMakeLists.txt | 2 +- auv_cpp/tests/CMakeLists.txt | 5 + auv_cpp/tests/core/test_config | Bin 0 -> 241120 bytes src/.gitignore | 2 +- src/CMakeLists.txt | 2 +- web_app/__init__.py | 4 +- web_app/backend.py | 2 +- web_app/build.sh | 2 +- web_app/build_win.bat | 4 +- web_app/data/gui_layouts.json | 2 +- web_app/frontend_gui/eslint.config.js | 34 +- web_app/frontend_gui/package-lock.json | 3338 ++++++++++++++++- web_app/frontend_gui/package.json | 7 + web_app/frontend_gui/public/fonts.css | 8 +- web_app/frontend_gui/public/leaflet.css | 858 ++--- web_app/frontend_gui/public/main.css | 205 +- web_app/frontend_gui/src/App.tsx | 212 +- web_app/frontend_gui/src/Grid.tsx | 106 +- web_app/frontend_gui/src/cssModules.d.ts | 6 +- web_app/frontend_gui/src/index.tsx | 20 +- web_app/frontend_gui/src/inputs/Forms.tsx | 333 +- .../src/inputs/SocketHealth.module.css | 25 +- .../frontend_gui/src/inputs/SocketHealth.tsx | 66 +- .../src/inputs/TaskManager.module.css | 32 +- .../frontend_gui/src/inputs/TaskManager.tsx | 192 +- .../frontend_gui/src/inputs/forms.module.css | 40 +- web_app/frontend_gui/src/outputs/map.tsx | 48 +- web_app/frontend_gui/src/outputs/sim.tsx | 73 +- .../src/outputs/status.module.css | 41 +- web_app/frontend_gui/src/outputs/status.tsx | 125 +- .../frontend_gui/src/outputs/video.module.css | 12 +- web_app/frontend_gui/src/outputs/video.tsx | 24 +- web_app/frontend_gui/src/types.ts | 84 +- web_app/frontend_gui/src/utils/icons.tsx | 95 +- web_app/frontend_gui/vite.config.js | 14 +- web_app/radio_setup/README.md | 30 +- web_app/radio_setup/radio.m | 10 +- web_app/radio_setup/radio.py | 2 +- web_app/startup_win.bat | 2 +- web_app/tests/__init__.py | 1 + 51 files changed, 4673 insertions(+), 1527 deletions(-) create mode 100644 .github/workflows/lint.yml create mode 100644 .pre-commit-config.yaml create mode 100644 .yamllint.yml create mode 100644 auv_cpp/tests/CMakeLists.txt create mode 100755 auv_cpp/tests/core/test_config create mode 100644 web_app/tests/__init__.py diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d14c3ea..37e97b5 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,4 +3,4 @@ "build": { "dockerfile": "../Docker/Dockerfile" } -} \ No newline at end of file +} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..851a235 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,31 @@ +name: Lint + +on: + push: + pull_request: + branches: [main, master] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install frontend dependencies + working-directory: web_app/frontend_gui + run: npm ci + + - name: Install clang-format + run: | + sudo apt-get update + sudo apt-get install -y clang-format-18 + + - uses: pre-commit/action@v3.0.1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a997106 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,58 @@ +# Pre-commit hooks for Neptune repository (C++ and Frontend only) +# Install: pip install pre-commit && pre-commit install +# Run manually: pre-commit run --all-files + +exclude: ^auv/ + +repos: + # General file checks + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + args: [--unsafe] + - id: check-json + exclude: tsconfig\.json$ + - id: check-added-large-files + args: ["--maxkb=1000"] + - id: check-merge-conflict + + # C++ clangformat + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v18.1.8 + hooks: + - id: clang-format + files: ^auv_cpp/.*\.(cpp|hpp|h)$ + args: [--style=file] + + # TypeScript/JavaScript -ESLint + Prettier + - repo: local + hooks: + - id: eslint + name: ESLint + entry: bash -c 'stripped=(); for f in "$@"; do stripped+=("${f#web_app/frontend_gui/}"); done; cd web_app/frontend_gui && ./node_modules/.bin/eslint --fix "${stripped[@]}"' -- + language: system + files: ^web_app/frontend_gui/src/.*\.(ts|tsx|js|jsx)$ + pass_filenames: true + + - id: prettier + name: Prettier + entry: bash -c 'stripped=(); for f in "$@"; do stripped+=("${f#web_app/frontend_gui/}"); done; cd web_app/frontend_gui && ./node_modules/.bin/prettier --write "${stripped[@]}"' -- + language: system + files: ^web_app/frontend_gui/.*\.(ts|tsx|js|jsx|css|json)$ + exclude: tsconfig\.json$ + pass_filenames: true + + # YAML validation + - repo: https://github.com/adrienverge/yamllint + rev: v1.35.1 + hooks: + - id: yamllint + args: [-c, .yamllint.yml] + files: \.(yaml|yml)$ + +ci: + autofix_commit_msg: "style: auto-fix by pre-commit hooks" + autoupdate_commit_msg: "chore: update pre-commit hooks" diff --git a/.yamllint.yml b/.yamllint.yml new file mode 100644 index 0000000..8ae80bb --- /dev/null +++ b/.yamllint.yml @@ -0,0 +1,15 @@ +--- +extends: default + +rules: + line-length: + max: 200 + level: warning + truthy: + check-keys: false + comments: + min-spaces-from-content: 1 + document-start: disable + indentation: + spaces: 2 + indent-sequences: true diff --git a/Docker/Dockerfile b/Docker/Dockerfile index 9b0851e..9a3b491 100644 --- a/Docker/Dockerfile +++ b/Docker/Dockerfile @@ -1,11 +1,8 @@ FROM debian:bookworm -RUN apt-get update -y +RUN apt-get update -y -# install cmake and ninja +# install cmake and ninja RUN apt-get install -y cmake build-essential RUN apt-get install -y ninja-build RUN apt-get install -y git - - - diff --git a/Docs/Docker.md b/Docs/Docker.md index 610bba9..95c33ce 100644 --- a/Docs/Docker.md +++ b/Docs/Docker.md @@ -4,11 +4,10 @@ Docker allows us to containerize our app to ensure the environment is the same a # Launching the project in the container -* Clone the repository and open it in vscode -* Make sure the [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension by microsoft is installed +* Clone the repository and open it in vscode +* Make sure the [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension by microsoft is installed * From you command palete (ctrl(cmd on mac) + shift + p) run the `Dev containers rebuild and reopen in container` command. * The first time, this may take awhile to build You can tell the container is open if the bar at the top of vscode shows `Neptune [Dev Container: neptune]` Later, you can relaunch directly in the contaier by choosing the [dev container] option when you select your workspace - diff --git a/Docs/Running.md b/Docs/Running.md index 7569061..c0ede66 100644 --- a/Docs/Running.md +++ b/Docs/Running.md @@ -14,5 +14,3 @@ build the project, then run it cmake --build neptune ./neptune ``` - - diff --git a/README.md b/README.md index 555432c..6158ff5 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ READ DOCS/docker.md ```cpp if (condition) { // code here -} +} else { // alternative code } @@ -89,7 +89,7 @@ Document every class with its purpose and usage: /** * @class PressureSensor * @brief Interface for MS5837 pressure/temperature sensor - * + * * Provides depth and temperature readings using I2C communication. * Handles calibration and conversion to engineering units. */ @@ -116,4 +116,4 @@ Document all public functions and complex private functions: 5. Ask questions in Discord -For questions or suggestions about these guidelines, please open an issue or contact the project maintainer. \ No newline at end of file +For questions or suggestions about these guidelines, please open an issue or contact the project maintainer. diff --git a/auv_cpp/.clang-format b/auv_cpp/.clang-format index 3d8f02d..1bcfcff 100644 --- a/auv_cpp/.clang-format +++ b/auv_cpp/.clang-format @@ -5,4 +5,4 @@ PointerAlignment: Left AllowShortFunctionsOnASingleLine: Inline AllowShortIfStatementsOnASingleLine: Never BreakBeforeBraces: Attach -SortIncludes: true \ No newline at end of file +SortIncludes: true diff --git a/auv_cpp/.editorconfig b/auv_cpp/.editorconfig index e925a43..19ba592 100644 --- a/auv_cpp/.editorconfig +++ b/auv_cpp/.editorconfig @@ -9,4 +9,4 @@ trim_trailing_whitespace = true insert_final_newline = true [*.{yml,yaml}] -indent_size = 2 \ No newline at end of file +indent_size = 2 diff --git a/auv_cpp/.gitignore b/auv_cpp/.gitignore index 50df32b..8df5e2f 100644 --- a/auv_cpp/.gitignore +++ b/auv_cpp/.gitignore @@ -26,4 +26,4 @@ logs/ *.log # OS -.DS_Store \ No newline at end of file +.DS_Store diff --git a/auv_cpp/CMakeLists.txt b/auv_cpp/CMakeLists.txt index 4a2ad3d..05f0668 100644 --- a/auv_cpp/CMakeLists.txt +++ b/auv_cpp/CMakeLists.txt @@ -26,4 +26,4 @@ add_subdirectory(src/core) if(BUILD_TESTS) enable_testing() add_subdirectory(tests) -endif() \ No newline at end of file +endif() diff --git a/auv_cpp/tests/CMakeLists.txt b/auv_cpp/tests/CMakeLists.txt new file mode 100644 index 0000000..db81165 --- /dev/null +++ b/auv_cpp/tests/CMakeLists.txt @@ -0,0 +1,5 @@ +# ============================================================================= +# tests/CMakeLists.txt - Test suite +# ============================================================================= + +add_subdirectory(core) diff --git a/auv_cpp/tests/core/test_config b/auv_cpp/tests/core/test_config new file mode 100755 index 0000000000000000000000000000000000000000..70c8e37992f334177ad40bbf9a3b610fe2d9eb7d GIT binary patch literal 241120 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 F`#B+nd-(tW literal 0 HcmV?d00001 diff --git a/src/.gitignore b/src/.gitignore index 07ed706..a007fea 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -1 +1 @@ -build/* \ No newline at end of file +build/* diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bb8f81c..5b58b99 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -3,4 +3,4 @@ cmake_minimum_required(VERSION 3.25) project(neptune CXX) -add_executable(neptune main.cpp) \ No newline at end of file +add_executable(neptune main.cpp) diff --git a/web_app/__init__.py b/web_app/__init__.py index 187e97a..50ad4a3 100644 --- a/web_app/__init__.py +++ b/web_app/__init__.py @@ -15,7 +15,7 @@ from pathlib import Path from time import sleep -from backend import socket_thread +from backend import socket_thread from pydantic_yaml import parse_yaml_file_as from ruamel.yaml import YAML @@ -73,7 +73,7 @@ def log(message: str): def start_backend(): """ If the backend websocket thread is not alive, start it. - If it is alive, do nothing. + If it is alive, do nothing. """ if backend_thread.is_alive() == False: backend_thread.start() diff --git a/web_app/backend.py b/web_app/backend.py index 66d4734..8360b19 100644 --- a/web_app/backend.py +++ b/web_app/backend.py @@ -59,7 +59,7 @@ def socket_handler( ) queue_to_frontend.put(json.dumps(asdict(ping_result))) active_pings.remove(ping) - + if time() - last_ping > ping_interval: active_pings.append( Ping( diff --git a/web_app/build.sh b/web_app/build.sh index efae4d7..6454f5d 100644 --- a/web_app/build.sh +++ b/web_app/build.sh @@ -10,7 +10,7 @@ python3 -m pip install --upgrade pip python3 -m pip install -r requirements.txt # Run npm script to build react-vite app & generate static bundle -cd frontend_gui +cd frontend_gui npm install npm run build diff --git a/web_app/build_win.bat b/web_app/build_win.bat index 2faa4c2..65032b6 100644 --- a/web_app/build_win.bat +++ b/web_app/build_win.bat @@ -5,12 +5,12 @@ python -m pip install --upgrade pip python -m pip install -r requirements.txt :: Run npm script to build react-vite app & generate static bundle -cd frontend_gui +cd frontend_gui call npm install call npm run build :: Return to original directory cd .. -:: At the end of this script, you will no longer be in the virtual environment. +:: At the end of this script, you will no longer be in the virtual environment. :: To get back in, run '.\.venv\Scripts\activate' from the command line diff --git a/web_app/data/gui_layouts.json b/web_app/data/gui_layouts.json index 2b26e04..87f2439 100644 --- a/web_app/data/gui_layouts.json +++ b/web_app/data/gui_layouts.json @@ -1 +1 @@ -{"lg":[{"w":8,"h":6,"x":0,"y":0,"i":"0/.0","minW":2,"minH":2,"moved":false,"static":false},{"w":7,"h":6,"x":8,"y":12,"i":"1/.1","minW":2,"minH":2,"moved":false,"static":false},{"w":3,"h":3,"x":4,"y":10,"i":"2/.2","minW":2,"minH":2,"moved":false,"static":false},{"w":3,"h":3,"x":0,"y":13,"i":"3/.3","minW":2,"minH":2,"moved":false,"static":false},{"w":4,"h":3,"x":11,"y":6,"i":"4/.4","minW":2,"minH":2,"moved":false,"static":false},{"w":4,"h":3,"x":7,"y":6,"i":"5/.5","minW":2,"minH":2,"moved":false,"static":false},{"w":4,"h":3,"x":7,"y":9,"i":"6/.6","minW":2,"minH":2,"moved":false,"static":false},{"w":4,"h":3,"x":0,"y":10,"i":"7/.7","minW":2,"minH":2,"moved":false,"static":false},{"w":5,"h":11,"x":15,"y":0,"i":"8/.8","minW":2,"minH":2,"moved":false,"static":false},{"w":4,"h":2,"x":11,"y":9,"i":"9/.9","moved":false,"static":false},{"w":7,"h":4,"x":0,"y":6,"i":"10/.a","moved":false,"static":false},{"w":7,"h":6,"x":8,"y":0,"i":"11/.b","moved":false,"static":false}],"md":[{"w":8,"h":6,"x":0,"y":0,"i":"0/.0","minW":2,"minH":2,"moved":false,"static":false},{"w":7,"h":6,"x":7,"y":6,"i":"1/.1","minW":2,"minH":2,"moved":false,"static":false},{"w":3,"h":3,"x":0,"y":9,"i":"2/.2","minW":2,"minH":2,"moved":false,"static":false},{"w":3,"h":3,"x":0,"y":6,"i":"3/.3","minW":2,"minH":2,"moved":false,"static":false},{"w":4,"h":5,"x":10,"y":12,"i":"4/.4","minW":2,"minH":2,"moved":false,"static":false},{"w":4,"h":4,"x":7,"y":17,"i":"5/.5","minW":2,"minH":2,"moved":false,"static":false},{"w":4,"h":3,"x":10,"y":0,"i":"6/.6","minW":2,"minH":2,"moved":false,"static":false},{"w":4,"h":3,"x":8,"y":3,"i":"7/.7","minW":2,"minH":2,"moved":false,"static":false},{"w":5,"h":11,"x":9,"y":21,"i":"8/.8","minW":2,"minH":2,"moved":false,"static":false}]} \ No newline at end of file +{"lg":[{"w":8,"h":6,"x":0,"y":0,"i":"0/.0","minW":2,"minH":2,"moved":false,"static":false},{"w":7,"h":6,"x":8,"y":12,"i":"1/.1","minW":2,"minH":2,"moved":false,"static":false},{"w":3,"h":3,"x":4,"y":10,"i":"2/.2","minW":2,"minH":2,"moved":false,"static":false},{"w":3,"h":3,"x":0,"y":13,"i":"3/.3","minW":2,"minH":2,"moved":false,"static":false},{"w":4,"h":3,"x":11,"y":6,"i":"4/.4","minW":2,"minH":2,"moved":false,"static":false},{"w":4,"h":3,"x":7,"y":6,"i":"5/.5","minW":2,"minH":2,"moved":false,"static":false},{"w":4,"h":3,"x":7,"y":9,"i":"6/.6","minW":2,"minH":2,"moved":false,"static":false},{"w":4,"h":3,"x":0,"y":10,"i":"7/.7","minW":2,"minH":2,"moved":false,"static":false},{"w":5,"h":11,"x":15,"y":0,"i":"8/.8","minW":2,"minH":2,"moved":false,"static":false},{"w":4,"h":2,"x":11,"y":9,"i":"9/.9","moved":false,"static":false},{"w":7,"h":4,"x":0,"y":6,"i":"10/.a","moved":false,"static":false},{"w":7,"h":6,"x":8,"y":0,"i":"11/.b","moved":false,"static":false}],"md":[{"w":8,"h":6,"x":0,"y":0,"i":"0/.0","minW":2,"minH":2,"moved":false,"static":false},{"w":7,"h":6,"x":7,"y":6,"i":"1/.1","minW":2,"minH":2,"moved":false,"static":false},{"w":3,"h":3,"x":0,"y":9,"i":"2/.2","minW":2,"minH":2,"moved":false,"static":false},{"w":3,"h":3,"x":0,"y":6,"i":"3/.3","minW":2,"minH":2,"moved":false,"static":false},{"w":4,"h":5,"x":10,"y":12,"i":"4/.4","minW":2,"minH":2,"moved":false,"static":false},{"w":4,"h":4,"x":7,"y":17,"i":"5/.5","minW":2,"minH":2,"moved":false,"static":false},{"w":4,"h":3,"x":10,"y":0,"i":"6/.6","minW":2,"minH":2,"moved":false,"static":false},{"w":4,"h":3,"x":8,"y":3,"i":"7/.7","minW":2,"minH":2,"moved":false,"static":false},{"w":5,"h":11,"x":9,"y":21,"i":"8/.8","minW":2,"minH":2,"moved":false,"static":false}]} diff --git a/web_app/frontend_gui/eslint.config.js b/web_app/frontend_gui/eslint.config.js index 238d2e4..2cf9713 100644 --- a/web_app/frontend_gui/eslint.config.js +++ b/web_app/frontend_gui/eslint.config.js @@ -1,38 +1,38 @@ -import js from '@eslint/js' -import globals from 'globals' -import react from 'eslint-plugin-react' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' +import js from "@eslint/js"; +import globals from "globals"; +import react from "eslint-plugin-react"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; export default [ - { ignores: ['dist'] }, + { ignores: ["dist"] }, { - files: ['**/*.{js,jsx}'], + files: ["**/*.{js,jsx}"], languageOptions: { ecmaVersion: 2020, globals: globals.browser, parserOptions: { - ecmaVersion: 'latest', + ecmaVersion: "latest", ecmaFeatures: { jsx: true }, - sourceType: 'module', + sourceType: "module", }, }, - settings: { react: { version: '18.3' } }, + settings: { react: { version: "18.3" } }, plugins: { react, - 'react-hooks': reactHooks, - 'react-refresh': reactRefresh, + "react-hooks": reactHooks, + "react-refresh": reactRefresh, }, rules: { ...js.configs.recommended.rules, ...react.configs.recommended.rules, - ...react.configs['jsx-runtime'].rules, + ...react.configs["jsx-runtime"].rules, ...reactHooks.configs.recommended.rules, - 'react/jsx-no-target-blank': 'off', - 'react-refresh/only-export-components': [ - 'warn', + "react/jsx-no-target-blank": "off", + "react-refresh/only-export-components": [ + "warn", { allowConstantExport: true }, ], }, }, -] +]; diff --git a/web_app/frontend_gui/package-lock.json b/web_app/frontend_gui/package-lock.json index 7dd5b5f..975ef3e 100644 --- a/web_app/frontend_gui/package-lock.json +++ b/web_app/frontend_gui/package-lock.json @@ -18,12 +18,19 @@ "three": "^0.174.0" }, "devDependencies": { + "@eslint/js": "^9.39.2", "@types/leaflet": "^1.9.17", "@types/react": "^19.1.6", "@types/react-dom": "^19.1.2", "@types/react-grid-layout": "^1.3.5", "@types/three": "^0.175.0", "@vitejs/plugin-react": "^4.3.4", + "eslint": "^9.39.2", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.26", + "globals": "^15.15.0", + "prettier": "^3.8.1", "typescript": "^5.8.3", "vite": "^6.2.6" } @@ -297,6 +304,15 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/types": { "version": "7.27.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", @@ -731,6 +747,215 @@ "node": ">=18" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", @@ -1239,6 +1464,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/leaflet": { "version": "1.9.17", "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.17.tgz", @@ -1352,11 +1584,11 @@ "license": "BSD-3-Clause" }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "devOptional": true, "license": "MIT", - "optional": true, "bin": { "acorn": "bin/acorn" }, @@ -1364,6 +1596,227 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -1390,6 +1843,17 @@ "integrity": "sha512-2E8U2h/3ZltRwxfVkT1c2DqWUjgIGFRg8cD1qQDJ0m7YOJCpxqI87s+vaaEzvIxRw4MJOUVJ3OZ6K904UNX+iw==", "license": "MIT" }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/browserslist": { "version": "4.24.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", @@ -1446,47 +1910,220 @@ "ieee754": "^1.2.1" } }, - "node_modules/caniuse-lite": { - "version": "1.0.30001713", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001713.tgz", - "integrity": "sha512-wCIWIg+A4Xr7NfhTuHdX+/FKh3+Op3LBbSp2N5Pfx6T/LhdQy3GTyoTg48BReaW/MyMNZAkTadsBtai3ldWK0Q==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001713", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001713.tgz", + "integrity": "sha512-wCIWIg+A4Xr7NfhTuHdX+/FKh3+Op3LBbSp2N5Pfx6T/LhdQy3GTyoTg48BReaW/MyMNZAkTadsBtai3ldWK0Q==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "license": "MIT" }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -1504,12 +2141,260 @@ } } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.136", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.136.tgz", "integrity": "sha512-kL4+wUTD7RSA5FHx5YwWtjDnEEkIIikFgWHR4P6fqjw1PPLlqYkxeOb++wAauAssat0YClCy8Y3C5SxgSkjibQ==", "license": "ISC" }, + "node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz", + "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.1", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/esbuild": { "version": "0.25.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", @@ -1559,106 +2444,1132 @@ "node": ">=6" } }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/fast-equals": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz", - "integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==", - "license": "MIT" - }, - "node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, "peerDependencies": { - "picomatch": "^3 || ^4" + "jiti": "*" }, "peerDependenciesMeta": { - "picomatch": { + "jiti": { "optional": true } } }, - "node_modules/fflate": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", "dev": true, - "license": "MIT" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-equals": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz", + "integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", + "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, "license": "MIT", "engines": { - "node": ">=6.9.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-tsconfig": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", - "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, "license": "MIT", "dependencies": { - "resolve-pkg-maps": "^1.0.0" + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, "engines": { - "node": ">=4" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } }, "node_modules/its-fine": { "version": "2.0.0", @@ -1678,6 +3589,19 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -1690,6 +3614,27 @@ "node": ">=6" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -1702,6 +3647,32 @@ "node": ">=6" } }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -1717,6 +3688,43 @@ "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", "license": "BSD-2-Clause" }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -1738,62 +3746,298 @@ "yallist": "^3.0.2" } }, - "node_modules/meshoptimizer": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz", - "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==", + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/meshoptimizer": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz", + "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "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==", + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, - "license": "MIT" - }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" + "dependencies": { + "callsites": "^3.0.0" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": ">=6" } }, - "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==", - "license": "MIT" + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1856,6 +4100,16 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", @@ -1895,6 +4149,32 @@ "url": "https://opencollective.com/preact" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -1906,6 +4186,16 @@ "react-is": "^16.13.1" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/react": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", @@ -2098,80 +4388,355 @@ } } }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "license": "MIT" }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resize-observer-polyfill": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", "license": "MIT" }, - "node_modules/resolve-pkg-maps": { + "node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rollup": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz", + "integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.7" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.40.0", + "@rollup/rollup-android-arm64": "4.40.0", + "@rollup/rollup-darwin-arm64": "4.40.0", + "@rollup/rollup-darwin-x64": "4.40.0", + "@rollup/rollup-freebsd-arm64": "4.40.0", + "@rollup/rollup-freebsd-x64": "4.40.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.40.0", + "@rollup/rollup-linux-arm-musleabihf": "4.40.0", + "@rollup/rollup-linux-arm64-gnu": "4.40.0", + "@rollup/rollup-linux-arm64-musl": "4.40.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.40.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.40.0", + "@rollup/rollup-linux-riscv64-gnu": "4.40.0", + "@rollup/rollup-linux-riscv64-musl": "4.40.0", + "@rollup/rollup-linux-s390x-gnu": "4.40.0", + "@rollup/rollup-linux-x64-gnu": "4.40.0", + "@rollup/rollup-linux-x64-musl": "4.40.0", + "@rollup/rollup-win32-arm64-msvc": "4.40.0", + "@rollup/rollup-win32-ia32-msvc": "4.40.0", + "@rollup/rollup-win32-x64-msvc": "4.40.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/scheduler": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", + "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/rollup": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz", - "integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==", - "devOptional": true, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.7" - }, - "bin": { - "rollup": "dist/bin/rollup" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": ">= 0.4" }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.40.0", - "@rollup/rollup-android-arm64": "4.40.0", - "@rollup/rollup-darwin-arm64": "4.40.0", - "@rollup/rollup-darwin-x64": "4.40.0", - "@rollup/rollup-freebsd-arm64": "4.40.0", - "@rollup/rollup-freebsd-x64": "4.40.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.40.0", - "@rollup/rollup-linux-arm-musleabihf": "4.40.0", - "@rollup/rollup-linux-arm64-gnu": "4.40.0", - "@rollup/rollup-linux-arm64-musl": "4.40.0", - "@rollup/rollup-linux-loongarch64-gnu": "4.40.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.40.0", - "@rollup/rollup-linux-riscv64-gnu": "4.40.0", - "@rollup/rollup-linux-riscv64-musl": "4.40.0", - "@rollup/rollup-linux-s390x-gnu": "4.40.0", - "@rollup/rollup-linux-x64-gnu": "4.40.0", - "@rollup/rollup-linux-x64-musl": "4.40.0", - "@rollup/rollup-win32-arm64-msvc": "4.40.0", - "@rollup/rollup-win32-ia32-msvc": "4.40.0", - "@rollup/rollup-win32-x64-msvc": "4.40.0", - "fsevents": "~2.3.2" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/scheduler": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", - "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/sisteransi": { @@ -2190,6 +4755,157 @@ "node": ">=0.10.0" } }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/suspend-react": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz", @@ -2241,6 +4957,97 @@ "fsevents": "~2.3.3" } }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -2255,6 +5062,25 @@ "node": ">=14.17" } }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", @@ -2305,6 +5131,16 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/use-sync-external-store": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", @@ -2396,12 +5232,140 @@ "license": "MIT", "optional": true }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "license": "ISC" }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zustand": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz", diff --git a/web_app/frontend_gui/package.json b/web_app/frontend_gui/package.json index 2928cdb..80efc76 100644 --- a/web_app/frontend_gui/package.json +++ b/web_app/frontend_gui/package.json @@ -20,12 +20,19 @@ "three": "^0.174.0" }, "devDependencies": { + "@eslint/js": "^9.39.2", "@types/leaflet": "^1.9.17", "@types/react": "^19.1.6", "@types/react-dom": "^19.1.2", "@types/react-grid-layout": "^1.3.5", "@types/three": "^0.175.0", "@vitejs/plugin-react": "^4.3.4", + "eslint": "^9.39.2", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.26", + "globals": "^15.15.0", + "prettier": "^3.8.1", "typescript": "^5.8.3", "vite": "^6.2.6" } diff --git a/web_app/frontend_gui/public/fonts.css b/web_app/frontend_gui/public/fonts.css index 39b2122..3342640 100644 --- a/web_app/frontend_gui/public/fonts.css +++ b/web_app/frontend_gui/public/fonts.css @@ -1,6 +1,6 @@ @font-face { - font-family: "Open Sans"; - font-weight: 400; - font-style: normal; - src: url('fonts/OpenSans.ttf') format('truetype'); + font-family: "Open Sans"; + font-weight: 400; + font-style: normal; + src: url("fonts/OpenSans.ttf") format("truetype"); } diff --git a/web_app/frontend_gui/public/leaflet.css b/web_app/frontend_gui/public/leaflet.css index 9ade8dc..2dd1c58 100644 --- a/web_app/frontend_gui/public/leaflet.css +++ b/web_app/frontend_gui/public/leaflet.css @@ -10,233 +10,248 @@ .leaflet-zoom-box, .leaflet-image-layer, .leaflet-layer { - position: absolute; - left: 0; - top: 0; - } + position: absolute; + left: 0; + top: 0; +} .leaflet-container { - overflow: hidden; - } + overflow: hidden; +} .leaflet-tile, .leaflet-marker-icon, .leaflet-marker-shadow { - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - -webkit-user-drag: none; - } + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + -webkit-user-drag: none; +} /* Prevents IE11 from highlighting tiles in blue */ .leaflet-tile::selection { - background: transparent; + background: transparent; } /* Safari renders non-retina tile on retina better with this, but Chrome is worse */ .leaflet-safari .leaflet-tile { - image-rendering: -webkit-optimize-contrast; - } + image-rendering: -webkit-optimize-contrast; +} /* hack that prevents hw layers "stretching" when loading new tiles */ .leaflet-safari .leaflet-tile-container { - width: 1600px; - height: 1600px; - -webkit-transform-origin: 0 0; - } + width: 1600px; + height: 1600px; + -webkit-transform-origin: 0 0; +} .leaflet-marker-icon, .leaflet-marker-shadow { - display: block; - } + display: block; +} /* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */ /* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */ .leaflet-container .leaflet-overlay-pane svg { - max-width: none !important; - max-height: none !important; - } + max-width: none !important; + max-height: none !important; +} .leaflet-container .leaflet-marker-pane img, .leaflet-container .leaflet-shadow-pane img, .leaflet-container .leaflet-tile-pane img, .leaflet-container img.leaflet-image-layer, .leaflet-container .leaflet-tile { - max-width: none !important; - max-height: none !important; - width: auto; - padding: 0; - } + max-width: none !important; + max-height: none !important; + width: auto; + padding: 0; +} .leaflet-container img.leaflet-tile { - /* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */ - mix-blend-mode: plus-lighter; + /* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */ + mix-blend-mode: plus-lighter; } .leaflet-container.leaflet-touch-zoom { - -ms-touch-action: pan-x pan-y; - touch-action: pan-x pan-y; - } + -ms-touch-action: pan-x pan-y; + touch-action: pan-x pan-y; +} .leaflet-container.leaflet-touch-drag { - -ms-touch-action: pinch-zoom; - /* Fallback for FF which doesn't support pinch-zoom */ - touch-action: none; - touch-action: pinch-zoom; + -ms-touch-action: pinch-zoom; + /* Fallback for FF which doesn't support pinch-zoom */ + touch-action: none; + touch-action: pinch-zoom; } .leaflet-container.leaflet-touch-drag.leaflet-touch-zoom { - -ms-touch-action: none; - touch-action: none; + -ms-touch-action: none; + touch-action: none; } .leaflet-container { - -webkit-tap-highlight-color: transparent; + -webkit-tap-highlight-color: transparent; } .leaflet-container a { - -webkit-tap-highlight-color: rgba(51, 181, 229, 0.4); + -webkit-tap-highlight-color: rgba(51, 181, 229, 0.4); } .leaflet-tile { - filter: inherit; - visibility: hidden; - } + filter: inherit; + visibility: hidden; +} .leaflet-tile-loaded { - visibility: inherit; - } + visibility: inherit; +} .leaflet-zoom-box { - width: 0; - height: 0; - -moz-box-sizing: border-box; - box-sizing: border-box; - z-index: 800; - } + width: 0; + height: 0; + -moz-box-sizing: border-box; + box-sizing: border-box; + z-index: 800; +} /* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */ .leaflet-overlay-pane svg { - -moz-user-select: none; - } + -moz-user-select: none; +} -.leaflet-pane { z-index: 400; } +.leaflet-pane { + z-index: 400; +} -.leaflet-tile-pane { z-index: 200; } -.leaflet-overlay-pane { z-index: 400; } -.leaflet-shadow-pane { z-index: 500; } -.leaflet-marker-pane { z-index: 600; } -.leaflet-tooltip-pane { z-index: 650; } -.leaflet-popup-pane { z-index: 700; } +.leaflet-tile-pane { + z-index: 200; +} +.leaflet-overlay-pane { + z-index: 400; +} +.leaflet-shadow-pane { + z-index: 500; +} +.leaflet-marker-pane { + z-index: 600; +} +.leaflet-tooltip-pane { + z-index: 650; +} +.leaflet-popup-pane { + z-index: 700; +} -.leaflet-map-pane canvas { z-index: 100; } -.leaflet-map-pane svg { z-index: 200; } +.leaflet-map-pane canvas { + z-index: 100; +} +.leaflet-map-pane svg { + z-index: 200; +} .leaflet-vml-shape { - width: 1px; - height: 1px; - } + width: 1px; + height: 1px; +} .lvml { - behavior: url(#default#VML); - display: inline-block; - position: absolute; - } - + behavior: url(#default#VML); + display: inline-block; + position: absolute; +} /* control positioning */ .leaflet-control { - position: relative; - z-index: 800; - pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ - pointer-events: auto; - } + position: relative; + z-index: 800; + pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ + pointer-events: auto; +} .leaflet-top, .leaflet-bottom { - position: absolute; - z-index: 1000; - pointer-events: none; - } + position: absolute; + z-index: 1000; + pointer-events: none; +} .leaflet-top { - top: 0; - } + top: 0; +} .leaflet-right { - right: 0; - } + right: 0; +} .leaflet-bottom { - bottom: 0; - } + bottom: 0; +} .leaflet-left { - left: 0; - } + left: 0; +} .leaflet-control { - float: left; - clear: both; - } + float: left; + clear: both; +} .leaflet-right .leaflet-control { - float: right; - } + float: right; +} .leaflet-top .leaflet-control { - margin-top: 10px; - } + margin-top: 10px; +} .leaflet-bottom .leaflet-control { - margin-bottom: 10px; - } + margin-bottom: 10px; +} .leaflet-left .leaflet-control { - margin-left: 10px; - } + margin-left: 10px; +} .leaflet-right .leaflet-control { - margin-right: 10px; - } - + margin-right: 10px; +} /* zoom and fade animations */ .leaflet-fade-anim .leaflet-popup { - opacity: 0; - -webkit-transition: opacity 0.2s linear; - -moz-transition: opacity 0.2s linear; - transition: opacity 0.2s linear; - } + opacity: 0; + -webkit-transition: opacity 0.2s linear; + -moz-transition: opacity 0.2s linear; + transition: opacity 0.2s linear; +} .leaflet-fade-anim .leaflet-map-pane .leaflet-popup { - opacity: 1; - } + opacity: 1; +} .leaflet-zoom-animated { - -webkit-transform-origin: 0 0; - -ms-transform-origin: 0 0; - transform-origin: 0 0; - } + -webkit-transform-origin: 0 0; + -ms-transform-origin: 0 0; + transform-origin: 0 0; +} svg.leaflet-zoom-animated { - will-change: transform; + will-change: transform; } .leaflet-zoom-anim .leaflet-zoom-animated { - -webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1); - -moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1); - transition: transform 0.25s cubic-bezier(0,0,0.25,1); - } + -webkit-transition: -webkit-transform 0.25s cubic-bezier(0, 0, 0.25, 1); + -moz-transition: -moz-transform 0.25s cubic-bezier(0, 0, 0.25, 1); + transition: transform 0.25s cubic-bezier(0, 0, 0.25, 1); +} .leaflet-zoom-anim .leaflet-tile, .leaflet-pan-anim .leaflet-tile { - -webkit-transition: none; - -moz-transition: none; - transition: none; - } + -webkit-transition: none; + -moz-transition: none; + transition: none; +} .leaflet-zoom-anim .leaflet-zoom-hide { - visibility: hidden; - } - + visibility: hidden; +} /* cursors */ .leaflet-interactive { - cursor: pointer; - } + cursor: pointer; +} .leaflet-grab { - cursor: -webkit-grab; - cursor: -moz-grab; - cursor: grab; - } + cursor: -webkit-grab; + cursor: -moz-grab; + cursor: grab; +} .leaflet-crosshair, .leaflet-crosshair .leaflet-interactive { - cursor: crosshair; - } + cursor: crosshair; +} .leaflet-popup-pane, .leaflet-control { - cursor: auto; - } + cursor: auto; +} .leaflet-dragging .leaflet-grab, .leaflet-dragging .leaflet-grab .leaflet-interactive, .leaflet-dragging .leaflet-marker-draggable { - cursor: move; - cursor: -webkit-grabbing; - cursor: -moz-grabbing; - cursor: grabbing; - } + cursor: move; + cursor: -webkit-grabbing; + cursor: -moz-grabbing; + cursor: grabbing; +} /* marker & overlays interactivity */ .leaflet-marker-icon, @@ -244,418 +259,419 @@ svg.leaflet-zoom-animated { .leaflet-image-layer, .leaflet-pane > svg path, .leaflet-tile-container { - pointer-events: none; - } + pointer-events: none; +} .leaflet-marker-icon.leaflet-interactive, .leaflet-image-layer.leaflet-interactive, .leaflet-pane > svg path.leaflet-interactive, svg.leaflet-image-layer.leaflet-interactive path { - pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ - pointer-events: auto; - } + pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ + pointer-events: auto; +} /* visual tweaks */ .leaflet-container { - background: #ddd; - outline-offset: 1px; - } + background: #ddd; + outline-offset: 1px; +} .leaflet-container a { - color: #0078A8; - } + color: #0078a8; +} .leaflet-zoom-box { - border: 2px dotted #38f; - background: rgba(255,255,255,0.5); - } - + border: 2px dotted #38f; + background: rgba(255, 255, 255, 0.5); +} /* general typography */ .leaflet-container { - font-family: "Helvetica Neue", Arial, Helvetica, sans-serif; - font-size: 12px; - font-size: 0.75rem; - line-height: 1.5; - } - + font-family: "Helvetica Neue", Arial, Helvetica, sans-serif; + font-size: 12px; + font-size: 0.75rem; + line-height: 1.5; +} /* general toolbar styles */ .leaflet-bar { - box-shadow: 0 1px 5px rgba(0,0,0,0.65); - border-radius: 4px; - } + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.65); + border-radius: 4px; +} .leaflet-bar a { - background-color: #fff; - border-bottom: 1px solid #ccc; - width: 26px; - height: 26px; - line-height: 26px; - display: block; - text-align: center; - text-decoration: none; - color: black; - } + background-color: #fff; + border-bottom: 1px solid #ccc; + width: 26px; + height: 26px; + line-height: 26px; + display: block; + text-align: center; + text-decoration: none; + color: black; +} .leaflet-bar a, .leaflet-control-layers-toggle { - background-position: 50% 50%; - background-repeat: no-repeat; - display: block; - } + background-position: 50% 50%; + background-repeat: no-repeat; + display: block; +} .leaflet-bar a:hover, .leaflet-bar a:focus { - background-color: #f4f4f4; - } + background-color: #f4f4f4; +} .leaflet-bar a:first-child { - border-top-left-radius: 4px; - border-top-right-radius: 4px; - } + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} .leaflet-bar a:last-child { - border-bottom-left-radius: 4px; - border-bottom-right-radius: 4px; - border-bottom: none; - } + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + border-bottom: none; +} .leaflet-bar a.leaflet-disabled { - cursor: default; - background-color: #f4f4f4; - color: #bbb; - } + cursor: default; + background-color: #f4f4f4; + color: #bbb; +} .leaflet-touch .leaflet-bar a { - width: 30px; - height: 30px; - line-height: 30px; - } + width: 30px; + height: 30px; + line-height: 30px; +} .leaflet-touch .leaflet-bar a:first-child { - border-top-left-radius: 2px; - border-top-right-radius: 2px; - } + border-top-left-radius: 2px; + border-top-right-radius: 2px; +} .leaflet-touch .leaflet-bar a:last-child { - border-bottom-left-radius: 2px; - border-bottom-right-radius: 2px; - } + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; +} /* zoom control */ .leaflet-control-zoom-in, .leaflet-control-zoom-out { - font: bold 18px 'Lucida Console', Monaco, monospace; - text-indent: 1px; - } - -.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out { - font-size: 22px; - } + font: + bold 18px "Lucida Console", + Monaco, + monospace; + text-indent: 1px; +} +.leaflet-touch .leaflet-control-zoom-in, +.leaflet-touch .leaflet-control-zoom-out { + font-size: 22px; +} /* layers control */ .leaflet-control-layers { - box-shadow: 0 1px 5px rgba(0,0,0,0.4); - background: #fff; - border-radius: 5px; - } + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.4); + background: #fff; + border-radius: 5px; +} .leaflet-control-layers-toggle { - background-image: url(images/layers.png); - width: 36px; - height: 36px; - } + background-image: url(images/layers.png); + width: 36px; + height: 36px; +} .leaflet-retina .leaflet-control-layers-toggle { - background-image: url(images/layers-2x.png); - background-size: 26px 26px; - } + background-image: url(images/layers-2x.png); + background-size: 26px 26px; +} .leaflet-touch .leaflet-control-layers-toggle { - width: 44px; - height: 44px; - } + width: 44px; + height: 44px; +} .leaflet-control-layers .leaflet-control-layers-list, .leaflet-control-layers-expanded .leaflet-control-layers-toggle { - display: none; - } + display: none; +} .leaflet-control-layers-expanded .leaflet-control-layers-list { - display: block; - position: relative; - } + display: block; + position: relative; +} .leaflet-control-layers-expanded { - padding: 6px 10px 6px 6px; - color: #333; - background: #fff; - } + padding: 6px 10px 6px 6px; + color: #333; + background: #fff; +} .leaflet-control-layers-scrollbar { - overflow-y: scroll; - overflow-x: hidden; - padding-right: 5px; - } + overflow-y: scroll; + overflow-x: hidden; + padding-right: 5px; +} .leaflet-control-layers-selector { - margin-top: 2px; - position: relative; - top: 1px; - } + margin-top: 2px; + position: relative; + top: 1px; +} .leaflet-control-layers label { - display: block; - font-size: 13px; - font-size: 1.08333em; - } + display: block; + font-size: 13px; + font-size: 1.08333em; +} .leaflet-control-layers-separator { - height: 0; - border-top: 1px solid #ddd; - margin: 5px -10px 5px -6px; - } + height: 0; + border-top: 1px solid #ddd; + margin: 5px -10px 5px -6px; +} /* Default icon URLs */ -.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */ - background-image: url(images/marker-icon.png); - } - +.leaflet-default-icon-path { + /* used only in path-guessing heuristic, see L.Icon.Default */ + background-image: url(images/marker-icon.png); +} /* attribution and scale controls */ .leaflet-container .leaflet-control-attribution { - background: #fff; - background: rgba(255, 255, 255, 0.8); - margin: 0; - } + background: #fff; + background: rgba(255, 255, 255, 0.8); + margin: 0; +} .leaflet-control-attribution, .leaflet-control-scale-line { - padding: 0 5px; - color: #333; - line-height: 1.4; - } + padding: 0 5px; + color: #333; + line-height: 1.4; +} .leaflet-control-attribution a { - text-decoration: none; - } + text-decoration: none; +} .leaflet-control-attribution a:hover, .leaflet-control-attribution a:focus { - text-decoration: underline; - } + text-decoration: underline; +} .leaflet-attribution-flag { - display: inline !important; - vertical-align: baseline !important; - width: 1em; - height: 0.6669em; - } + display: inline !important; + vertical-align: baseline !important; + width: 1em; + height: 0.6669em; +} .leaflet-left .leaflet-control-scale { - margin-left: 5px; - } + margin-left: 5px; +} .leaflet-bottom .leaflet-control-scale { - margin-bottom: 5px; - } + margin-bottom: 5px; +} .leaflet-control-scale-line { - border: 2px solid #777; - border-top: none; - line-height: 1.1; - padding: 2px 5px 1px; - white-space: nowrap; - -moz-box-sizing: border-box; - box-sizing: border-box; - background: rgba(255, 255, 255, 0.8); - text-shadow: 1px 1px #fff; - } + border: 2px solid #777; + border-top: none; + line-height: 1.1; + padding: 2px 5px 1px; + white-space: nowrap; + -moz-box-sizing: border-box; + box-sizing: border-box; + background: rgba(255, 255, 255, 0.8); + text-shadow: 1px 1px #fff; +} .leaflet-control-scale-line:not(:first-child) { - border-top: 2px solid #777; - border-bottom: none; - margin-top: -2px; - } + border-top: 2px solid #777; + border-bottom: none; + margin-top: -2px; +} .leaflet-control-scale-line:not(:first-child):not(:last-child) { - border-bottom: 2px solid #777; - } + border-bottom: 2px solid #777; +} .leaflet-touch .leaflet-control-attribution, .leaflet-touch .leaflet-control-layers, .leaflet-touch .leaflet-bar { - box-shadow: none; - } + box-shadow: none; +} .leaflet-touch .leaflet-control-layers, .leaflet-touch .leaflet-bar { - border: 2px solid rgba(0,0,0,0.2); - background-clip: padding-box; - } - + border: 2px solid rgba(0, 0, 0, 0.2); + background-clip: padding-box; +} /* popup */ .leaflet-popup { - position: absolute; - text-align: center; - margin-bottom: 20px; - } + position: absolute; + text-align: center; + margin-bottom: 20px; +} .leaflet-popup-content-wrapper { - padding: 1px; - text-align: left; - border-radius: 12px; - } + padding: 1px; + text-align: left; + border-radius: 12px; +} .leaflet-popup-content { - margin: 13px 24px 13px 20px; - line-height: 1.3; - font-size: 13px; - font-size: 1.08333em; - min-height: 1px; - } + margin: 13px 24px 13px 20px; + line-height: 1.3; + font-size: 13px; + font-size: 1.08333em; + min-height: 1px; +} .leaflet-popup-content p { - margin: 17px 0; - margin: 1.3em 0; - } + margin: 17px 0; + margin: 1.3em 0; +} .leaflet-popup-tip-container { - width: 40px; - height: 20px; - position: absolute; - left: 50%; - margin-top: -1px; - margin-left: -20px; - overflow: hidden; - pointer-events: none; - } + width: 40px; + height: 20px; + position: absolute; + left: 50%; + margin-top: -1px; + margin-left: -20px; + overflow: hidden; + pointer-events: none; +} .leaflet-popup-tip { - width: 17px; - height: 17px; - padding: 1px; - - margin: -10px auto 0; - pointer-events: auto; - - -webkit-transform: rotate(45deg); - -moz-transform: rotate(45deg); - -ms-transform: rotate(45deg); - transform: rotate(45deg); - } + width: 17px; + height: 17px; + padding: 1px; + + margin: -10px auto 0; + pointer-events: auto; + + -webkit-transform: rotate(45deg); + -moz-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); +} .leaflet-popup-content-wrapper, .leaflet-popup-tip { - background: white; - color: #333; - box-shadow: 0 3px 14px rgba(0,0,0,0.4); - } + background: white; + color: #333; + box-shadow: 0 3px 14px rgba(0, 0, 0, 0.4); +} .leaflet-container a.leaflet-popup-close-button { - position: absolute; - top: 0; - right: 0; - border: none; - text-align: center; - width: 24px; - height: 24px; - font: 16px/24px Tahoma, Verdana, sans-serif; - color: #757575; - text-decoration: none; - background: transparent; - } + position: absolute; + top: 0; + right: 0; + border: none; + text-align: center; + width: 24px; + height: 24px; + font: + 16px/24px Tahoma, + Verdana, + sans-serif; + color: #757575; + text-decoration: none; + background: transparent; +} .leaflet-container a.leaflet-popup-close-button:hover, .leaflet-container a.leaflet-popup-close-button:focus { - color: #585858; - } + color: #585858; +} .leaflet-popup-scrolled { - overflow: auto; - } + overflow: auto; +} .leaflet-oldie .leaflet-popup-content-wrapper { - -ms-zoom: 1; - } + -ms-zoom: 1; +} .leaflet-oldie .leaflet-popup-tip { - width: 24px; - margin: 0 auto; + width: 24px; + margin: 0 auto; - -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)"; - filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678); - } + -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)"; + filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678); +} .leaflet-oldie .leaflet-control-zoom, .leaflet-oldie .leaflet-control-layers, .leaflet-oldie .leaflet-popup-content-wrapper, .leaflet-oldie .leaflet-popup-tip { - border: 1px solid #999; - } - + border: 1px solid #999; +} /* div icon */ .leaflet-div-icon { - background: #fff; - border: 1px solid #666; - } - + background: #fff; + border: 1px solid #666; +} /* Tooltip */ /* Base styles for the element that has a tooltip */ .leaflet-tooltip { - position: absolute; - padding: 6px; - background-color: #fff; - border: 1px solid #fff; - border-radius: 3px; - color: #222; - white-space: nowrap; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - pointer-events: none; - box-shadow: 0 1px 3px rgba(0,0,0,0.4); - } + position: absolute; + padding: 6px; + background-color: #fff; + border: 1px solid #fff; + border-radius: 3px; + color: #222; + white-space: nowrap; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + pointer-events: none; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); +} .leaflet-tooltip.leaflet-interactive { - cursor: pointer; - pointer-events: auto; - } + cursor: pointer; + pointer-events: auto; +} .leaflet-tooltip-top:before, .leaflet-tooltip-bottom:before, .leaflet-tooltip-left:before, .leaflet-tooltip-right:before { - position: absolute; - pointer-events: none; - border: 6px solid transparent; - background: transparent; - content: ""; - } + position: absolute; + pointer-events: none; + border: 6px solid transparent; + background: transparent; + content: ""; +} /* Directions */ .leaflet-tooltip-bottom { - margin-top: 6px; + margin-top: 6px; } .leaflet-tooltip-top { - margin-top: -6px; + margin-top: -6px; } .leaflet-tooltip-bottom:before, .leaflet-tooltip-top:before { - left: 50%; - margin-left: -6px; - } + left: 50%; + margin-left: -6px; +} .leaflet-tooltip-top:before { - bottom: 0; - margin-bottom: -12px; - border-top-color: #fff; - } + bottom: 0; + margin-bottom: -12px; + border-top-color: #fff; +} .leaflet-tooltip-bottom:before { - top: 0; - margin-top: -12px; - margin-left: -6px; - border-bottom-color: #fff; - } + top: 0; + margin-top: -12px; + margin-left: -6px; + border-bottom-color: #fff; +} .leaflet-tooltip-left { - margin-left: -6px; + margin-left: -6px; } .leaflet-tooltip-right { - margin-left: 6px; + margin-left: 6px; } .leaflet-tooltip-left:before, .leaflet-tooltip-right:before { - top: 50%; - margin-top: -6px; - } + top: 50%; + margin-top: -6px; +} .leaflet-tooltip-left:before { - right: 0; - margin-right: -12px; - border-left-color: #fff; - } + right: 0; + margin-right: -12px; + border-left-color: #fff; +} .leaflet-tooltip-right:before { - left: 0; - margin-left: -12px; - border-right-color: #fff; - } + left: 0; + margin-left: -12px; + border-right-color: #fff; +} /* Printing */ @media print { - /* Prevent printers from removing background-images of controls. */ - .leaflet-control { - -webkit-print-color-adjust: exact; - print-color-adjust: exact; - } - } + /* Prevent printers from removing background-images of controls. */ + .leaflet-control { + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + } +} diff --git a/web_app/frontend_gui/public/main.css b/web_app/frontend_gui/public/main.css index 3c1ed0d..d57014b 100644 --- a/web_app/frontend_gui/public/main.css +++ b/web_app/frontend_gui/public/main.css @@ -1,109 +1,111 @@ -html, body { - font-family: "Open Sans", system-ui; - padding: 0; - margin: 0; - height: 100vh; - background: #CEF3FF; - font-size: 1.2; +html, +body { + font-family: "Open Sans", system-ui; + padding: 0; + margin: 0; + height: 100vh; + background: #cef3ff; + font-size: 1.2; } :root { - /* Generated with https://www.joshwcomeau.com/shadow-palette/*/ - --shadow-color: 195deg 39% 56%; /* Same as #CEF3FF */ - --shadow-elevation-low: - 0px 0.3px 0.3px hsl(var(--shadow-color) / 0.52), - 0px 0.5px 0.5px -1.9px hsl(var(--shadow-color) / 0.4), - 0px 1.3px 1.2px -3.7px hsl(var(--shadow-color) / 0.29); - --shadow-elevation-medium: - 0px 0.3px 0.3px hsl(var(--shadow-color) / 0.43), - 0px 0.6px 0.6px -0.9px hsl(var(--shadow-color) / 0.38), - 0px 1.3px 1.2px -1.9px hsl(var(--shadow-color) / 0.32), - 0px 3.1px 2.9px -2.8px hsl(var(--shadow-color) / 0.26), - 0px 6.3px 5.9px -3.7px hsl(var(--shadow-color) / 0.2); - --shadow-elevation-high: - 0px 0.2px 0.2px hsl(var(--shadow-color) / 0.32), - 0px 0.8px 0.8px -0.5px hsl(var(--shadow-color) / 0.3), - 0px 1.4px 1.4px -1px hsl(var(--shadow-color) / 0.28), - 0px 2.5px 2.4px -1.5px hsl(var(--shadow-color) / 0.25), - -0.1px 4.3px 4.2px -2px hsl(var(--shadow-color) / 0.23), - -0.1px 7.1px 6.9px -2.5px hsl(var(--shadow-color) / 0.21), - -0.2px 11.3px 11px -3px hsl(var(--shadow-color) / 0.19), - -0.3px 17.1px 16.7px -3.5px hsl(var(--shadow-color) / 0.16); + /* Generated with https://www.joshwcomeau.com/shadow-palette/*/ + --shadow-color: 195deg 39% 56%; /* Same as #CEF3FF */ + --shadow-elevation-low: + 0px 0.3px 0.3px hsl(var(--shadow-color) / 0.52), + 0px 0.5px 0.5px -1.9px hsl(var(--shadow-color) / 0.4), + 0px 1.3px 1.2px -3.7px hsl(var(--shadow-color) / 0.29); + --shadow-elevation-medium: + 0px 0.3px 0.3px hsl(var(--shadow-color) / 0.43), + 0px 0.6px 0.6px -0.9px hsl(var(--shadow-color) / 0.38), + 0px 1.3px 1.2px -1.9px hsl(var(--shadow-color) / 0.32), + 0px 3.1px 2.9px -2.8px hsl(var(--shadow-color) / 0.26), + 0px 6.3px 5.9px -3.7px hsl(var(--shadow-color) / 0.2); + --shadow-elevation-high: + 0px 0.2px 0.2px hsl(var(--shadow-color) / 0.32), + 0px 0.8px 0.8px -0.5px hsl(var(--shadow-color) / 0.3), + 0px 1.4px 1.4px -1px hsl(var(--shadow-color) / 0.28), + 0px 2.5px 2.4px -1.5px hsl(var(--shadow-color) / 0.25), + -0.1px 4.3px 4.2px -2px hsl(var(--shadow-color) / 0.23), + -0.1px 7.1px 6.9px -2.5px hsl(var(--shadow-color) / 0.21), + -0.2px 11.3px 11px -3px hsl(var(--shadow-color) / 0.19), + -0.3px 17.1px 16.7px -3.5px hsl(var(--shadow-color) / 0.16); } * { - box-sizing: border-box; + box-sizing: border-box; } h1 { - margin: -0.25rem auto 0.25rem auto; - width: fit-content; - background-color: white; - padding: 0.75rem; - border-radius: 1rem; - box-shadow: var(--shadow-elevation-medium); + margin: -0.25rem auto 0.25rem auto; + width: fit-content; + background-color: white; + padding: 0.75rem; + border-radius: 1rem; + box-shadow: var(--shadow-elevation-medium); } h2 { - margin: 0 0 0.5rem; - line-height: 1; - text-align: center; + margin: 0 0 0.5rem; + line-height: 1; + text-align: center; } h3 { - margin: 0; + margin: 0; } input, select { - font-family: inherit; + font-family: inherit; } ul { - margin-top: 0; - list-style: none; - padding-left: 0; + margin-top: 0; + list-style: none; + padding-left: 0; } -ul>li { - margin-bottom: 0.75rem; +ul > li { + margin-bottom: 0.75rem; } -pre { /* For the gui console output*/ - text-wrap: wrap; +pre { + /* For the gui console output*/ + text-wrap: wrap; } button { - cursor: pointer; + cursor: pointer; } .parent-container { - padding: 1rem; + padding: 1rem; } .top-right-buttons { - position: absolute; - top: 1rem; - right: 1.5rem; - display: flex; + position: absolute; + top: 1rem; + right: 1.5rem; + display: flex; } .top-right-buttons > button { - margin: 0.25rem; - border-radius: 0.75rem; - border: none; - padding: 0.5rem 0.75rem; - display: flex; - align-items: center; - justify-content: center; - gap: 0.2rem; - background: white; - box-shadow: var(--shadow-elevation-medium); - font-size: 0.9rem; + margin: 0.25rem; + border-radius: 0.75rem; + border: none; + padding: 0.5rem 0.75rem; + display: flex; + align-items: center; + justify-content: center; + gap: 0.2rem; + background: white; + box-shadow: var(--shadow-elevation-medium); + font-size: 0.9rem; } .top-right-buttons > button:hover { - transform: translateY(-0.1rem); + transform: translateY(-0.1rem); } /* This section has `react-grid-layout` package class names that are not found @@ -111,76 +113,77 @@ button { * future, it is due to react-grid-layout changing their class names. Change * them to match, or if that is impossible simply track the grid state manually * and apply custom class names. */ -.react-grid-item { /* Assigned to grid items by react-grid-layout*/ - padding: 1rem; - background-color: white; - border-radius: 1.5rem; - box-shadow: var(--shadow-elevation-high); +.react-grid-item { + /* Assigned to grid items by react-grid-layout*/ + padding: 1rem; + background-color: white; + border-radius: 1.5rem; + box-shadow: var(--shadow-elevation-high); } .react-grid-layout button { - border: none; - border-radius: 0.75rem; - margin: 0.5rem; - padding: 0.75rem 1rem; - background: white; - filter: brightness(95%); - font-weight: bold; + border: none; + border-radius: 0.75rem; + margin: 0.5rem; + padding: 0.75rem 1rem; + background: white; + filter: brightness(95%); + font-weight: bold; } .react-grid-layout button:hover { - filter: brightness(85%); + filter: brightness(85%); } /* For cursor when hovering in drag mode*/ .react-grid-item.react-draggable { - cursor: grab; + cursor: grab; } /* For cursor when actively dragging*/ .react-grid-item.react-draggable-dragging { - cursor: grabbing; + cursor: grabbing; } /* END of weird section with react-grid-layout classes*/ .map-handle { - position: absolute; - top: 0; - left: 1.5rem; - right: 1.5rem; - height: 2rem; - cursor: grab; + position: absolute; + top: 0; + left: 1.5rem; + right: 1.5rem; + height: 2rem; + cursor: grab; } .map-holder { - flex-grow: 1; - display: flex; - flex-direction: column; + flex-grow: 1; + display: flex; + flex-direction: column; } .map { - flex-grow: 1; - border-radius: 1rem; + flex-grow: 1; + border-radius: 1rem; } .status-section { - display: flex; - flex-direction: column; - align-items: flex-start; - padding-inline: 1rem; + display: flex; + flex-direction: column; + align-items: flex-start; + padding-inline: 1rem; } .status-item { - width: 20ch; + width: 20ch; } .testing-section { - flex-grow: 1; - display: flex; - justify-content: space-evenly; - align-items: flex-start; + flex-grow: 1; + display: flex; + justify-content: space-evenly; + align-items: flex-start; } .testing-section > div { - padding-inline: 1rem; + padding-inline: 1rem; } diff --git a/web_app/frontend_gui/src/App.tsx b/web_app/frontend_gui/src/App.tsx index 972d874..ff4ba9e 100644 --- a/web_app/frontend_gui/src/App.tsx +++ b/web_app/frontend_gui/src/App.tsx @@ -2,125 +2,129 @@ import React, { useEffect, useState, useRef } from "react"; import { Layouts } from "react-grid-layout"; import { - ParametersForm, - MotorTestForm, - HeadingTestForm, - StartMission, + ParametersForm, + MotorTestForm, + HeadingTestForm, + StartMission, } from "./inputs/Forms"; -import { SocketHealth } from "./inputs/SocketHealth" -import { TaskManager} from "./inputs/TaskManager"; +import { SocketHealth } from "./inputs/SocketHealth"; +import { TaskManager } from "./inputs/TaskManager"; import { StatusItem, StatusMessages } from "./outputs/status"; import { Simulation } from "./outputs/sim"; -import { Map } from "./outputs/map" -import { VideoStream } from "./outputs/video" -import { - Command, - isCommand, - State, - isState, - Data, - isData -} from "./types" +import { Map } from "./outputs/map"; +import { VideoStream } from "./outputs/video"; +import { Command, isCommand, State, isState, Data, isData } from "./types"; import { Grid } from "./Grid"; import { GridSvg } from "./utils/icons"; const tryJson = (raw: string): any => { - try { - return JSON.parse(raw) - } - catch { - return undefined - } -} + try { + return JSON.parse(raw); + } catch { + return undefined; + } +}; export default function App() { - // These are the only state variables that need full scope - const [imuData, setImuData] = useState([ - { title: 'Magnetometer', value: '', id: 1 }, - { title: 'Accelerometer', value: '', id: 2 }, - { title: 'Gyroscope', value: '', id: 3 } - ]); - const [insData, setInsData] = useState([ - { title: 'Heading', value: '', id: 1 }, - { title: 'Roll', value: '', id: 2 }, - { title: 'Pitch', value: '', id: 3 } - ]); - const [websocket, setWebsocket]: any = useState(); - const [statusMessages, setStatusMessages]: any = useState([]); - const [attitude, setAttitude] = useState([0,0,0]); + // These are the only state variables that need full scope + const [imuData, setImuData] = useState([ + { title: "Magnetometer", value: "", id: 1 }, + { title: "Accelerometer", value: "", id: 2 }, + { title: "Gyroscope", value: "", id: 3 }, + ]); + const [insData, setInsData] = useState([ + { title: "Heading", value: "", id: 1 }, + { title: "Roll", value: "", id: 2 }, + { title: "Pitch", value: "", id: 3 }, + ]); + const [websocket, setWebsocket]: any = useState(); + const [statusMessages, setStatusMessages]: any = useState([]); + const [attitude, setAttitude] = useState([0, 0, 0]); - const handleSocketData = (event: MessageEvent) => { - console.log("Socket data arrived: " + event.data); - const data: Command | Data = tryJson(event.data) - if (isData(data) && data.type == "state" && isState(data.content)) { - setAttitude(data.content.attitude) - } else { - setStatusMessages((prev: any) => [...prev, event.data]); - } + const handleSocketData = (event: MessageEvent) => { + console.log("Socket data arrived: " + event.data); + const data: Command | Data = tryJson(event.data); + if (isData(data) && data.type == "state" && isState(data.content)) { + setAttitude(data.content.attitude); + } else { + setStatusMessages((prev: any) => [...prev, event.data]); } + }; - // Register socket handler for server-sent data - const connection: any = useRef(null); - useEffect(() => { - const socket: WebSocket = new WebSocket("ws://localhost:6543/api/websocket"); + // Register socket handler for server-sent data + const connection: any = useRef(null); + useEffect(() => { + const socket: WebSocket = new WebSocket( + "ws://localhost:6543/api/websocket", + ); - socket.addEventListener("open", () => setWebsocket(socket)); - socket.addEventListener("message", handleSocketData); + socket.addEventListener("open", () => setWebsocket(socket)); + socket.addEventListener("message", handleSocketData); - connection.current = socket; + connection.current = socket; - return () => connection.current.close(); - }, [useState]); + return () => connection.current.close(); + }, [useState]); - // Grid Layout Section - const [gridEnabled, setGridEnabled] = useState(false); - const [layouts, setLayouts] = useState(); - // On load try to get saved layout - useEffect(() => { - const fetchLayout = async () => { - const response = await fetch("http://" + window.location.host + "/api/layouts"); - const initialLayout: Layouts = await response.json(); - setLayouts(initialLayout); - } - fetchLayout(); - }, []); - // Save layouts at server - const saveLayout = () => { - fetch("http://" + window.location.host + "/api/layouts", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(layouts) - }); - } + // Grid Layout Section + const [gridEnabled, setGridEnabled] = useState(false); + const [layouts, setLayouts] = useState(); + // On load try to get saved layout + useEffect(() => { + const fetchLayout = async () => { + const response = await fetch( + "http://" + window.location.host + "/api/layouts", + ); + const initialLayout: Layouts = await response.json(); + setLayouts(initialLayout); + }; + fetchLayout(); + }, []); + // Save layouts at server + const saveLayout = () => { + fetch("http://" + window.location.host + "/api/layouts", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(layouts), + }); + }; - return ( -
-

Yonder Deep Nautilus Dashboard

-
- - -
- - - - - - - - - - - - - - -
- ); + return ( +
+

Yonder Deep Nautilus Dashboard

+
+ + +
+ + + + + + + + + + + + + + +
+ ); } diff --git a/web_app/frontend_gui/src/Grid.tsx b/web_app/frontend_gui/src/Grid.tsx index 84f5847..5564a16 100644 --- a/web_app/frontend_gui/src/Grid.tsx +++ b/web_app/frontend_gui/src/Grid.tsx @@ -1,61 +1,59 @@ -import React from 'react'; -import { Responsive, WidthProvider, Layout, Layouts } from 'react-grid-layout'; +import React from "react"; +import { Responsive, WidthProvider, Layout, Layouts } from "react-grid-layout"; // Import resizable styles -import 'react-grid-layout/css/styles.css'; -import 'react-resizable/css/styles.css'; +import "react-grid-layout/css/styles.css"; +import "react-resizable/css/styles.css"; const ResponsiveGridLayout = WidthProvider(Responsive); export const Grid = ({ - children, - enabled, - layouts, - setLayouts, -}:{ - children: any, // TODO: Figure out typing for react children of children - enabled: boolean, - layouts: Layouts | undefined, - setLayouts: React.Dispatch> - }) => { - - return ( - { - setLayouts(allLayouts); - }} - draggableHandle=".drag-handle" - isDraggable={enabled} - isResizable={enabled} - // The breakpoints & cols chosen for 50px increments - breakpoints={{ lg: 1200, md: 700, sm:500, xs:200, xxs:0 }} - cols={{ lg: 20, md: 14, sm: 10, xs: 4, xxs: 2 }} - rowHeight={50} - resizeHandles={['s', 'w', 'e', 'n', 'sw', 'nw', 'se', 'ne']} - > - {React.Children.map(children, (child, index) => { - if (child.props.coordinates) { - /** The only one w/ coordinates as props is the map - * The map needs to have a defined separate handle to allow - * for movement dragging to conflict with map dragging - * The extra class "map-flex-fix" is for the map to expand correctly - **/ - return ( -
- {child} -
- ) - } - return ( -
- {child} -
- ) - })} - {children} -
- ); + children, + enabled, + layouts, + setLayouts, +}: { + children: any; // TODO: Figure out typing for react children of children + enabled: boolean; + layouts: Layouts | undefined; + setLayouts: React.Dispatch>; +}) => { + return ( + { + setLayouts(allLayouts); + }} + draggableHandle=".drag-handle" + isDraggable={enabled} + isResizable={enabled} + // The breakpoints & cols chosen for 50px increments + breakpoints={{ lg: 1200, md: 700, sm: 500, xs: 200, xxs: 0 }} + cols={{ lg: 20, md: 14, sm: 10, xs: 4, xxs: 2 }} + rowHeight={50} + resizeHandles={["s", "w", "e", "n", "sw", "nw", "se", "ne"]} + > + {React.Children.map(children, (child, index) => { + if (child.props.coordinates) { + /** The only one w/ coordinates as props is the map + * The map needs to have a defined separate handle to allow + * for movement dragging to conflict with map dragging + * The extra class "map-flex-fix" is for the map to expand correctly + **/ + return ( +
+ {child} +
+ ); + } + return ( +
+ {child} +
+ ); + })} + {children} +
+ ); }; - diff --git a/web_app/frontend_gui/src/cssModules.d.ts b/web_app/frontend_gui/src/cssModules.d.ts index 8b00847..f2d12bb 100644 --- a/web_app/frontend_gui/src/cssModules.d.ts +++ b/web_app/frontend_gui/src/cssModules.d.ts @@ -1,4 +1,4 @@ -declare module '*.module.css' { - const classes: { [key: string]: string }; - export default classes; +declare module "*.module.css" { + const classes: { [key: string]: string }; + export default classes; } diff --git a/web_app/frontend_gui/src/index.tsx b/web_app/frontend_gui/src/index.tsx index ec92126..679f993 100644 --- a/web_app/frontend_gui/src/index.tsx +++ b/web_app/frontend_gui/src/index.tsx @@ -1,14 +1,14 @@ -import { scan } from "react-scan" -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' +import { scan } from "react-scan"; +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; import App from "./App.js"; scan({ - enabled: true, - }); + enabled: true, +}); -createRoot(document.getElementById('root')!).render( - - - , -) +createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/web_app/frontend_gui/src/inputs/Forms.tsx b/web_app/frontend_gui/src/inputs/Forms.tsx index 795ef19..9a362a9 100644 --- a/web_app/frontend_gui/src/inputs/Forms.tsx +++ b/web_app/frontend_gui/src/inputs/Forms.tsx @@ -1,184 +1,191 @@ -import React, { useState, useEffect, FC } from 'react'; -import styles from "./forms.module.css" +import React, { useState, useEffect, FC } from "react"; +import styles from "./forms.module.css"; export const ParametersForm = ({ websocket }: any) => { - const [pidAxis, setPidAxis] = useState(''); - const [constantP, setConstantP] = useState(''); - const [constantI, setConstantI] = useState(''); - const [constantD, setConstantD] = useState(''); - const makePidRequest = () => { - if (!pidAxis || !constantP || !constantI || !constantD) { return; } - const pidConstants = { - axis: '' + pidAxis, - p: '' + constantP, - i: '' + constantI, - d: '' + constantD - }; - const request = { - command: "pid", - content: pidConstants - }; - websocket.send(JSON.stringify(request)); + const [pidAxis, setPidAxis] = useState(""); + const [constantP, setConstantP] = useState(""); + const [constantI, setConstantI] = useState(""); + const [constantD, setConstantD] = useState(""); + const makePidRequest = () => { + if (!pidAxis || !constantP || !constantI || !constantD) { + return; } + const pidConstants = { + axis: "" + pidAxis, + p: "" + constantP, + i: "" + constantI, + d: "" + constantD, + }; + const request = { + command: "pid", + content: pidConstants, + }; + websocket.send(JSON.stringify(request)); + }; - return ( -
-

Set PID Constants

-
-
- - setConstantP(e.target.value)} - /> - setConstantI(e.target.value)} - /> - setConstantD(e.target.value)} - /> -
- -
+ return ( +
+

Set PID Constants

+
+
+ + setConstantP(e.target.value)} + /> + setConstantI(e.target.value)} + /> + setConstantD(e.target.value)} + />
- ) -} + +
+
+ ); +}; export const MotorTestForm = ({ websocket }: any) => { - const [motor1, setMotor1] = useState("0"); - const [motor2, setMotor2] = useState("0"); - const [motor3, setMotor3] = useState("0"); - const [motor4, setMotor4] = useState("0"); + const [motor1, setMotor1] = useState("0"); + const [motor2, setMotor2] = useState("0"); + const [motor3, setMotor3] = useState("0"); + const [motor4, setMotor4] = useState("0"); - const makeMotorRequest = (arr: number[] | undefined = undefined) => { - console.log("Making motor request.") - let motorTest = [] - try { - if (arr) motorTest = arr; - else motorTest = [ - parseFloat(motor1), - parseFloat(motor2), - parseFloat(motor3), - parseFloat(motor4), - ]; - } catch { - return - } - const request = { - command: "motor", - content: motorTest - }; - websocket.send(JSON.stringify(request)); + const makeMotorRequest = (arr: number[] | undefined = undefined) => { + console.log("Making motor request."); + let motorTest = []; + try { + if (arr) motorTest = arr; + else + motorTest = [ + parseFloat(motor1), + parseFloat(motor2), + parseFloat(motor3), + parseFloat(motor4), + ]; + } catch { + return; } + const request = { + command: "motor", + content: motorTest, + }; + websocket.send(JSON.stringify(request)); + }; - const zeroOut = () => { - setMotor1("0"); - setMotor2("0"); - setMotor3("0"); - setMotor4("0"); - makeMotorRequest([0,0,0,0]); - } + const zeroOut = () => { + setMotor1("0"); + setMotor2("0"); + setMotor3("0"); + setMotor4("0"); + makeMotorRequest([0, 0, 0, 0]); + }; - const [keymap, setKeymap] = useState(false); + const [keymap, setKeymap] = useState(false); - return ( -
-

Motor Testing

-
-
- setMotor1(e.target.value)} - /> - setMotor2(e.target.value)} - /> - setMotor3(e.target.value)} - /> - setMotor4(e.target.value)} - /> -
-
- - - -
-
+ return ( +
+

Motor Testing

+
+
+ setMotor1(e.target.value)} + /> + setMotor2(e.target.value)} + /> + setMotor3(e.target.value)} + /> + setMotor4(e.target.value)} + /> +
+
+ + +
- ) -} +
+
+ ); +}; export const HeadingTestForm = ({ websocket }: any) => { - const [targetHeading, setTargetHeading] = useState(''); - const headingRequest = () => { - console.log("Making heading request.") - const request = { - command: "control", - content: targetHeading - }; - websocket.send(JSON.stringify(request)); - } + const [targetHeading, setTargetHeading] = useState(""); + const headingRequest = () => { + console.log("Making heading request."); + const request = { + command: "control", + content: targetHeading, + }; + websocket.send(JSON.stringify(request)); + }; - return ( -
-

Heading Test

-
- (setTargetHeading(e.target.value))} - /> - -
-
- ) -} + return ( +
+

Heading Test

+
+ setTargetHeading(e.target.value)} + /> + +
+
+ ); +}; export const StartMission = ({ websocket }: any) => { - const [targetPoint, setTargetPoint] = useState(''); - const missionRequest = () => { - websocket.send(JSON.stringify({ - command: "mission", - content: targetPoint - })) - } + const [targetPoint, setTargetPoint] = useState(""); + const missionRequest = () => { + websocket.send( + JSON.stringify({ + command: "mission", + content: targetPoint, + }), + ); + }; - return ( + return (
-

Start Mission

-
- (setTargetPoint(e.target.value))} - /> - -
+

Start Mission

+
+ setTargetPoint(e.target.value)} + /> + +
- ) -} + ); +}; diff --git a/web_app/frontend_gui/src/inputs/SocketHealth.module.css b/web_app/frontend_gui/src/inputs/SocketHealth.module.css index 6e81f3c..ddf0bdf 100644 --- a/web_app/frontend_gui/src/inputs/SocketHealth.module.css +++ b/web_app/frontend_gui/src/inputs/SocketHealth.module.css @@ -1,22 +1,23 @@ -.socketForm, .socketButtons { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; +.socketForm, +.socketButtons { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; } .socketButtons { - place-items: center; - justify-content: space-around; + place-items: center; + justify-content: space-around; } .socketButtons > button { - margin-left: 0; - padding: 0.5rem; - height: 2.5rem; - width: 2.5rem; + margin-left: 0; + padding: 0.5rem; + height: 2.5rem; + width: 2.5rem; } .socketForm > p { - margin: 0.25rem; + margin: 0.25rem; } diff --git a/web_app/frontend_gui/src/inputs/SocketHealth.tsx b/web_app/frontend_gui/src/inputs/SocketHealth.tsx index 96fe219..49af170 100644 --- a/web_app/frontend_gui/src/inputs/SocketHealth.tsx +++ b/web_app/frontend_gui/src/inputs/SocketHealth.tsx @@ -3,37 +3,39 @@ import styles from "./SocketHealth.module.css"; import { RestartSvg, PingSvg } from "../utils/icons"; export const SocketHealth = ({ websocket }: any) => { - const [status, setStatus] = useState<"open"|"closed"|"unknown">("unknown"); - const restart = () => { - const request = { - command: "websocket", - content: "restart", - }; - websocket.send(JSON.stringify(request)); - } - const ping = () => { - setStatus("unknown"); - const request = { - command: "websocket", - content: "ping", - }; - websocket.send(JSON.stringify(request)); - } + const [status, setStatus] = useState<"open" | "closed" | "unknown">( + "unknown", + ); + const restart = () => { + const request = { + command: "websocket", + content: "restart", + }; + websocket.send(JSON.stringify(request)); + }; + const ping = () => { + setStatus("unknown"); + const request = { + command: "websocket", + content: "ping", + }; + websocket.send(JSON.stringify(request)); + }; - return ( -
-

Websocket Health

-
-

Status: {status}

-
- - -
-
+ return ( +
+

Websocket Health

+
+

Status: {status}

+
+ +
- ) -} +
+
+ ); +}; diff --git a/web_app/frontend_gui/src/inputs/TaskManager.module.css b/web_app/frontend_gui/src/inputs/TaskManager.module.css index fece705..4627fa9 100644 --- a/web_app/frontend_gui/src/inputs/TaskManager.module.css +++ b/web_app/frontend_gui/src/inputs/TaskManager.module.css @@ -1,27 +1,27 @@ .taskOuter { - padding-inline: 0.5rem; - width: 100%; - display: grid; - grid-template-columns: 1fr 1fr; - gap: 1rem; + padding-inline: 0.5rem; + width: 100%; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; } .taskInner { - width: 100%; - padding-inline: 0.5rem; - display: flex; - flex-direction: row; - justify-content: space-between; + width: 100%; + padding-inline: 0.5rem; + display: flex; + flex-direction: row; + justify-content: space-between; } .taskMiddle { - display: flex; - flex-direction: column; + display: flex; + flex-direction: column; } button.taskButton { - margin: 0.5rem auto; - display: flex; - place-items: center; - gap: 0.5rem; + margin: 0.5rem auto; + display: flex; + place-items: center; + gap: 0.5rem; } diff --git a/web_app/frontend_gui/src/inputs/TaskManager.tsx b/web_app/frontend_gui/src/inputs/TaskManager.tsx index 315628f..2eb5f25 100644 --- a/web_app/frontend_gui/src/inputs/TaskManager.tsx +++ b/web_app/frontend_gui/src/inputs/TaskManager.tsx @@ -1,112 +1,112 @@ -import { useState, useEffect, useCallback, FC } from "react" -import { Tasks, isData } from "../types" -import { RestartSvg } from "../utils/icons" -import styles from "./TaskManager.module.css" +import { useState, useEffect, useCallback, FC } from "react"; +import { Tasks, isData } from "../types"; +import { RestartSvg } from "../utils/icons"; +import styles from "./TaskManager.module.css"; const tryJson = (raw: any): any => { - try { - return JSON.parse(raw) - } - catch { - return undefined - } -} + try { + return JSON.parse(raw); + } catch { + return undefined; + } +}; const useForceUpdate = () => { - const [, setValue] = useState(0); - return useCallback(() => setValue(value => value + 1), []); - } + const [, setValue] = useState(0); + return useCallback(() => setValue((value) => value + 1), []); +}; // Map from task status to displayed text on button const displayMap = { - "unknown": "Unknown", - "active": "Disable", - "inactive": "Enable", -} as const + unknown: "Unknown", + active: "Disable", + inactive: "Enable", +} as const; // Map from current task value to possible action*/ const actionMap = { - "unknown": "info", - "active": "disable", - "inactive": "enable" -} as const + unknown: "info", + active: "disable", + inactive: "enable", +} as const; export const TaskManager: FC<{ websocket: WebSocket }> = ({ websocket }) => { - const forceUpdate = useForceUpdate(); - const [tasks, setTasks] = useState({ - "Localization": "unknown", - "Perception": "unknown", - "Control": "unknown", - "Navigation": "unknown", - }); + const forceUpdate = useForceUpdate(); + const [tasks, setTasks] = useState({ + Localization: "unknown", + Perception: "unknown", + Control: "unknown", + Navigation: "unknown", + }); - useEffect(() => { - if (websocket) - websocket.addEventListener("message", (raw: MessageEvent) => { - const msg = tryJson(raw.data); - if (msg && isData(msg) && msg.type == "tasks") { - const content = msg.content as Tasks; - const newTasks = Object.assign(tasks, content); - setTasks(newTasks); - forceUpdate(); - } - }); - }, [websocket]); - - const manageTask = (taskName: keyof Tasks | undefined) => { - let subcommand: "info" | "enable" | "disable"; - if (taskName) - subcommand = actionMap[tasks[taskName]]; - else - subcommand = "info" - const request = { - command: "tasks", - content: { - sub: subcommand, - task: taskName, - }, + useEffect(() => { + if (websocket) + websocket.addEventListener("message", (raw: MessageEvent) => { + const msg = tryJson(raw.data); + if (msg && isData(msg) && msg.type == "tasks") { + const content = msg.content as Tasks; + const newTasks = Object.assign(tasks, content); + setTasks(newTasks); + forceUpdate(); } - websocket.send(JSON.stringify(request)); - } + }); + }, [websocket]); + + const manageTask = (taskName: keyof Tasks | undefined) => { + let subcommand: "info" | "enable" | "disable"; + if (taskName) subcommand = actionMap[tasks[taskName]]; + else subcommand = "info"; + const request = { + command: "tasks", + content: { + sub: subcommand, + task: taskName, + }, + }; + websocket.send(JSON.stringify(request)); + }; - return ( -
-

Task Manager

-
-
-
-

Localization

- -
-
-

Perception

- -
-
-
-
-

Control

- -
-
-

Navigation

- -
-
-
-
- -
+ return ( +
+

Task Manager

+
+
+
+

Localization

+ +
+
+

Perception

+ +
+
+
+
+

Control

+ +
+
+

Navigation

+ +
- ) -} +
+
+ +
+
+ ); +}; diff --git a/web_app/frontend_gui/src/inputs/forms.module.css b/web_app/frontend_gui/src/inputs/forms.module.css index 54c5bc2..4a6d9b7 100644 --- a/web_app/frontend_gui/src/inputs/forms.module.css +++ b/web_app/frontend_gui/src/inputs/forms.module.css @@ -1,32 +1,34 @@ .formBody { - margin: 1rem 0 0; - display: flex; - flex-direction: column; - align-items: center; + margin: 1rem 0 0; + display: flex; + flex-direction: column; + align-items: center; } .formBody select, input { - margin-bottom: 0.75rem; - border-radius: 0.5rem; - border: 1px solid lightgray; - padding: 0.3rem; - background: #f2f2f2; - text-align: center; + margin-bottom: 0.75rem; + border-radius: 0.5rem; + border: 1px solid lightgray; + padding: 0.3rem; + background: #f2f2f2; + text-align: center; } -.motorForm, .parameterForm { - width: 100%; - display: flex; - flex-direction: row; - justify-content: space-around; - align-items: flex-start; +.motorForm, +.parameterForm { + width: 100%; + display: flex; + flex-direction: row; + justify-content: space-around; + align-items: flex-start; } -.motorForm input, .parameterForm input { - width: 5ch; +.motorForm input, +.parameterForm input { + width: 5ch; } .parameterForm { - justify-content: space-between; + justify-content: space-between; } diff --git a/web_app/frontend_gui/src/outputs/map.tsx b/web_app/frontend_gui/src/outputs/map.tsx index f03ed43..a8499fe 100644 --- a/web_app/frontend_gui/src/outputs/map.tsx +++ b/web_app/frontend_gui/src/outputs/map.tsx @@ -1,26 +1,26 @@ -import { useEffect, useRef } from "react" -import * as L from "leaflet" -import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet" -import "../../public/leaflet.css" +import { useEffect, useRef } from "react"; +import * as L from "leaflet"; +import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet"; +import "../../public/leaflet.css"; -export const Map = ({ coordinates } : { coordinates: number[][]}) => { - const center = L.latLng(32.865249, -117.243515); - const zoom = 13 +export const Map = ({ coordinates }: { coordinates: number[][] }) => { + const center = L.latLng(32.865249, -117.243515); + const zoom = 13; - return ( - <> -

Map

- - - - Origin - - - - ) -} + return ( + <> +

+ Map +

+ + + + Origin + + + + ); +}; diff --git a/web_app/frontend_gui/src/outputs/sim.tsx b/web_app/frontend_gui/src/outputs/sim.tsx index 6159062..83da2d8 100644 --- a/web_app/frontend_gui/src/outputs/sim.tsx +++ b/web_app/frontend_gui/src/outputs/sim.tsx @@ -1,45 +1,44 @@ import { useRef, useState, useEffect } from "react"; -import { - Canvas, - useFrame -} from "@react-three/fiber" -import * as THREE from "three" +import { Canvas, useFrame } from "@react-three/fiber"; +import * as THREE from "three"; THREE.Object3D.DEFAULT_UP.set(0, 0, 1); -const Mesh = ({euler}: {euler: number[]}) => { - const eulerAngles = new THREE.Euler() - let meshRef = useRef(null!); +const Mesh = ({ euler }: { euler: number[] }) => { + const eulerAngles = new THREE.Euler(); + let meshRef = useRef(null!); - useEffect(() => { - eulerAngles.set(euler[0], euler[1], euler[2], "ZYX"); - console.log("Euler Angles: " + JSON.stringify(euler)); - }, [euler]) + useEffect(() => { + eulerAngles.set(euler[0], euler[1], euler[2], "ZYX"); + console.log("Euler Angles: " + JSON.stringify(euler)); + }, [euler]); - useFrame(() => { - const [z, y, x] = eulerAngles.toArray(); - meshRef.current.rotation.set(x, y, z); - }) + useFrame(() => { + const [z, y, x] = eulerAngles.toArray(); + meshRef.current.rotation.set(x, y, z); + }); - return ( - - - - - ) -} + return ( + + + + + ); +}; -export const Simulation = ({euler}: {euler: number[]}) => { - - return ( - <> -

Visualization

- - - - - - - - ) -} +export const Simulation = ({ euler }: { euler: number[] }) => { + return ( + <> +

Visualization

+ + + + + + + + ); +}; diff --git a/web_app/frontend_gui/src/outputs/status.module.css b/web_app/frontend_gui/src/outputs/status.module.css index 9216379..61ee515 100644 --- a/web_app/frontend_gui/src/outputs/status.module.css +++ b/web_app/frontend_gui/src/outputs/status.module.css @@ -1,39 +1,38 @@ .StatusMessages { - height: 100%; - width: 100%; - border-radius: 1rem; + height: 100%; + width: 100%; + border-radius: 1rem; - display: flex; - flex-direction: column; + display: flex; + flex-direction: column; } .StatusMessages > h2 { - flex-grow: 0; - margin: 0 0 1rem; - text-align: center; + flex-grow: 0; + margin: 0 0 1rem; + text-align: center; } .StatusMessages > ul { - flex-grow: 1; - border-radius: 1rem; - margin: 0; - padding: 1rem; - background-color: black; - color: white; - overflow-y: scroll; + flex-grow: 1; + border-radius: 1rem; + margin: 0; + padding: 1rem; + background-color: black; + color: white; + overflow-y: scroll; } .StatusMessages li { - border-top: 1px solid grey; + border-top: 1px solid grey; } .StatusMessage pre { - margin: 0.5rem; + margin: 0.5rem; } .StatusMessages > button { - flex-grow: 0; - margin: 1rem auto 0; - width: fit-content; + flex-grow: 0; + margin: 1rem auto 0; + width: fit-content; } - diff --git a/web_app/frontend_gui/src/outputs/status.tsx b/web_app/frontend_gui/src/outputs/status.tsx index 9bf8b62..fb2e128 100644 --- a/web_app/frontend_gui/src/outputs/status.tsx +++ b/web_app/frontend_gui/src/outputs/status.tsx @@ -1,75 +1,78 @@ -import React, { useRef, useEffect, useState} from "react" +import React, { useRef, useEffect, useState } from "react"; -import styles from "./status.module.css" +import styles from "./status.module.css"; interface dataPart { - id: number, - title: string, - value: string + id: number; + title: string; + value: string; } -export const StatusItem = ({ statusType, statusData }:any) => { - const statusListItems = statusData.map((dataPart: dataPart) => -
  • {dataPart.title}: {dataPart.value}
  • - ) +export const StatusItem = ({ statusType, statusData }: any) => { + const statusListItems = statusData.map((dataPart: dataPart) => ( +
  • + {dataPart.title}: {dataPart.value} +
  • + )); - return ( -
    -

    {statusType}

    -
      {statusListItems}
    -
    - ) -} + return ( +
    +

    {statusType}

    +
      {statusListItems}
    +
    + ); +}; export const StatusMessages = ({ - statusMessages, - setStatusMessages + statusMessages, + setStatusMessages, }: { - statusMessages: string[], - setStatusMessages: React.Dispatch> - }) => { - const listRef = useRef(undefined!); - const [doScroll, setDoScroll] = useState(); + statusMessages: string[]; + setStatusMessages: React.Dispatch>; +}) => { + const listRef = useRef(undefined!); + const [doScroll, setDoScroll] = useState(); - const clearMessages = () => { - setStatusMessages([]); - }; + const clearMessages = () => { + setStatusMessages([]); + }; - const checkScroll = () => { - const list = listRef.current; - setDoScroll(list.scrollHeight - list.clientHeight <= list.scrollTop); - }; + const checkScroll = () => { + const list = listRef.current; + setDoScroll(list.scrollHeight - list.clientHeight <= list.scrollTop); + }; - useEffect(() => { // Check for scroll decoupled from react render loop - listRef.current.addEventListener("scroll", checkScroll); - }); + useEffect(() => { + // Check for scroll decoupled from react render loop + listRef.current.addEventListener("scroll", checkScroll); + }); - useEffect(() => { // This bit is for autoscroll (50px as a buffer to bottom of scroll container) - const list = listRef.current; - if(doScroll) { - list.scrollTop = list.scrollHeight - list.clientHeight; - } - checkScroll(); - }, [statusMessages]); + useEffect(() => { + // This bit is for autoscroll (50px as a buffer to bottom of scroll container) + const list = listRef.current; + if (doScroll) { + list.scrollTop = list.scrollHeight - list.clientHeight; + } + checkScroll(); + }, [statusMessages]); - return ( -
    -

    Status Messages

    -
      - {statusMessages?.map((message: any, index: any) => { - try { - const newMessage: any = JSON.parse(message); - message = JSON.stringify(newMessage, null, 2); - } - catch {} - return ( -
    • -
      {message}
      -
    • - ) - })} -
    - -
    - ) -} + return ( +
    +

    Status Messages

    +
      + {statusMessages?.map((message: any, index: any) => { + try { + const newMessage: any = JSON.parse(message); + message = JSON.stringify(newMessage, null, 2); + } catch {} + return ( +
    • +
      {message}
      +
    • + ); + })} +
    + +
    + ); +}; diff --git a/web_app/frontend_gui/src/outputs/video.module.css b/web_app/frontend_gui/src/outputs/video.module.css index fc89a6b..b16e771 100644 --- a/web_app/frontend_gui/src/outputs/video.module.css +++ b/web_app/frontend_gui/src/outputs/video.module.css @@ -1,11 +1,11 @@ .videoContainer { - display: flex; - flex-direction: column; + display: flex; + flex-direction: column; } .video { - margin: auto; - border-radius: 1rem; - display: block; - height: 100%; + margin: auto; + border-radius: 1rem; + display: block; + height: 100%; } diff --git a/web_app/frontend_gui/src/outputs/video.tsx b/web_app/frontend_gui/src/outputs/video.tsx index 2ce8fff..57ad76e 100644 --- a/web_app/frontend_gui/src/outputs/video.tsx +++ b/web_app/frontend_gui/src/outputs/video.tsx @@ -1,14 +1,14 @@ -import styles from "./video.module.css" +import styles from "./video.module.css"; export const VideoStream = () => { - return ( -
    -

    Video Stream

    - Video stream not enabled or not available -
    - ) -} + return ( +
    +

    Video Stream

    + Video stream not enabled or not available +
    + ); +}; diff --git a/web_app/frontend_gui/src/types.ts b/web_app/frontend_gui/src/types.ts index 0693fb8..105cd61 100644 --- a/web_app/frontend_gui/src/types.ts +++ b/web_app/frontend_gui/src/types.ts @@ -1,61 +1,63 @@ export interface Command { - command: string - content: any - ack?: boolean + command: string; + content: any; + ack?: boolean; } export const isCommand = (obj: any): obj is Command => { - return ( - typeof obj === 'object' && - obj !== null && - typeof obj.command === 'string' && - typeof obj.content === 'number' - ) -} + return ( + typeof obj === "object" && + obj !== null && + typeof obj.command === "string" && + typeof obj.content === "number" + ); +}; export interface State { - position: number[] - velocity: number[] - attitude: number[] - angular_velocity: number[] + position: number[]; + velocity: number[]; + attitude: number[]; + angular_velocity: number[]; } const isNumberArray = (value: any): value is number[] => { - return Array.isArray(value) && value.every((item) => typeof item === "number"); -} + return ( + Array.isArray(value) && value.every((item) => typeof item === "number") + ); +}; export const isState = (obj: any): obj is State => { - return ( - typeof obj === 'object' && - obj !== null && - isNumberArray(obj.position) && - isNumberArray(obj.velocity) && - isNumberArray(obj.attitude) && - isNumberArray(obj.angular_velocity) //&& - ) -} + return ( + typeof obj === "object" && + obj !== null && + isNumberArray(obj.position) && + isNumberArray(obj.velocity) && + isNumberArray(obj.attitude) && + isNumberArray(obj.angular_velocity) //&& + ); +}; export type Status = "unknown" | "active" | "inactive"; export interface Tasks { - "Localization": Status, - "Perception": Status, - "Control": Status, - "Navigation": Status, + Localization: Status; + Perception: Status; + Control: Status; + Navigation: Status; } export interface Data { - source: string - type: string - content: Tasks | State | string -} + source: string; + type: string; + content: Tasks | State | string; +} export const isData = (obj: any): obj is Data => { - return ( - typeof obj === 'object' && - obj !== null && - typeof obj.source === 'string' && - typeof obj.type === 'string' && - (typeof obj.content === 'object' || typeof obj.content === 'string') - ) -} + return ( + typeof obj === "object" && + obj !== null && + typeof obj.source === "string" && + typeof obj.type === "string" && + (typeof obj.content === "object" || typeof obj.content === "string") + ); +}; diff --git a/web_app/frontend_gui/src/utils/icons.tsx b/web_app/frontend_gui/src/utils/icons.tsx index ad8bcc9..b1e47ab 100644 --- a/web_app/frontend_gui/src/utils/icons.tsx +++ b/web_app/frontend_gui/src/utils/icons.tsx @@ -1,41 +1,76 @@ -import { CSSProperties } from "react" +import { CSSProperties } from "react"; // This is a function so that the svg color can be reactive -export const GridSvg = ({ color="#1f1f1f" }: { color?: CSSProperties["color"]})=> { - - return ( - - { + return ( + + - - ) -} + 400h160v-160H600v160Zm-400 0h160v-160H200v160Zm400-400Zm0 240Zm-240 0Zm0-240Z" + /> + + ); +}; export const RestartSvg = () => { - return ( - - - - ) -} + return ( + + + + ); +}; export const CommandKeySvg = () => { - return ( - - - - ) -} + return ( + + + + ); +}; export const PingSvg = () => ( - - - -) + + + +); export const SaveSvg = () => ( - - - -) + + + +); diff --git a/web_app/frontend_gui/vite.config.js b/web_app/frontend_gui/vite.config.js index c1e38f2..275c9b7 100644 --- a/web_app/frontend_gui/vite.config.js +++ b/web_app/frontend_gui/vite.config.js @@ -1,10 +1,10 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; // https://vite.dev/config/ export default defineConfig({ - build: { - sourcemap: true, - }, - plugins: [react()], -}) + build: { + sourcemap: true, + }, + plugins: [react()], +}); diff --git a/web_app/radio_setup/README.md b/web_app/radio_setup/README.md index 62fa8d1..9b510c5 100644 --- a/web_app/radio_setup/README.md +++ b/web_app/radio_setup/README.md @@ -1,26 +1,26 @@ -# PPP Connection Instructions -This directory is for creating the ppp (point to point protocol) connection over the USB radio between the base station and the AUV. We will be using the [pppd (point-to-point protocol Daemon)](https://linux.die.net/man/8/pppd). +# PPP Connection Instructions +This directory is for creating the ppp (point to point protocol) connection over the USB radio between the base station and the AUV. We will be using the [pppd (point-to-point protocol Daemon)](https://linux.die.net/man/8/pppd). -Currently, we support setting up a ppp connection only for Windows (WSL) and MacOS. +Currently, we support setting up a ppp connection only for Windows (WSL) and MacOS. -## Windows (WSL) -- Connect your radio to a serial port. -- Use [usbipd](https://learn.microsoft.com/en-us/windows/wsl/connect-usb) to attach the radio to WSL so that you can access it in WSL. +## Windows (WSL) +- Connect your radio to a serial port. +- Use [usbipd](https://learn.microsoft.com/en-us/windows/wsl/connect-usb) to attach the radio to WSL so that you can access it in WSL. - You can check if the radio is detected in WSL by running `lsusb` - First, on the AUV-side (Raspberry Pi) start a pppd connection with `sudo pppd [path to radio] 57600 lock local noauth 192.168.100.11:192.168.100.10` - - `[path to radio]` is often `/dev/ttyUSB0` - - `57600` = baud rate - - `lock` = Ensures exclusive access to the device that uses this connection + - `[path to radio]` is often `/dev/ttyUSB0` + - `57600` = baud rate + - `lock` = Ensures exclusive access to the device that uses this connection - `local` = non-modem - - `noauth` = don't require authentication + - `noauth` = don't require authentication - `IP1 : IP2` = local IP : remote IP -- Then, on the base station (WSL), connect to the pi with `sudo pppd [path to radio] 57600 lock local noauth nodetach 192.168.100.10:192.168.100.11` +- Then, on the base station (WSL), connect to the pi with `sudo pppd [path to radio] 57600 lock local noauth nodetach 192.168.100.10:192.168.100.11` - `nodetach` = don't run the command in the background (i.e. it's fine if the terminal gets locked on this process) -- You will know that the connection is successful, if the devices and IPs appear in your WSL terminal +- You will know that the connection is successful, if the devices and IPs appear in your WSL terminal -Some useful commands: -- `lsusb` = see what serial devices are detected by WSL -- `ps aux | grep pppd` = checks if a pppd process is running +Some useful commands: +- `lsusb` = see what serial devices are detected by WSL +- `ps aux | grep pppd` = checks if a pppd process is running ## MacOS #### Conceptually: diff --git a/web_app/radio_setup/radio.m b/web_app/radio_setup/radio.m index d44061f..5589db1 100644 --- a/web_app/radio_setup/radio.m +++ b/web_app/radio_setup/radio.m @@ -25,7 +25,7 @@ &kCFTypeDictionaryValueCallBacks); CFDictionarySetValue(matchingUSBDict, CFSTR(kIOPropertyMatchKey), subDict); - + //NSLog(@"Matching Dict:"); //NSLog(@"%@", matchingUSBDict); @@ -34,7 +34,7 @@ matchedService = IOServiceGetMatchingService(kIOMainPortDefault, matchingUSBDict); //NSLog(@"Service: %u", matchedService); - + // Service path, not really that useful for our purposes io_string_t servicePath; IORegistryEntryGetPath(matchedService, kIOServicePlane, servicePath); @@ -54,7 +54,7 @@ size_t devPathLength; Boolean gotString = false; - /* This commented part is from the Apple docs but always comes up null + /* This commented part is from the Apple docs but always comes up null CFTypeRef deviceNameAsCFString; deviceNameAsCFString = (CFStringRef) IORegistryEntrySearchCFProperty ( matchedService, @@ -75,7 +75,7 @@ deviceFilePath + strlen(deviceFilePath), MAXPATHLEN - strlen(deviceFilePath), kCFStringEncodingASCII); - + if (gotString) { //NSLog(@"Device file path: %s", deviceFilePath); char* finalResult = malloc(strlen(deviceFilePath)); @@ -111,7 +111,7 @@ int main() { NSString *radioPath = [NSString stringWithUTF8String:result]; free(result); - + if (radioPath && ![radioPath isEqual: @"Radio device not found"]) { NSLog(@"Calling pppd for device at %@", radioPath); NSTask *task = [[NSTask alloc] init]; diff --git a/web_app/radio_setup/radio.py b/web_app/radio_setup/radio.py index 9fd3c45..4a5dd1f 100644 --- a/web_app/radio_setup/radio.py +++ b/web_app/radio_setup/radio.py @@ -26,4 +26,4 @@ print("Named path: " ) # Launch pppd - subprocess("pppd", "call", "usbRadio") \ No newline at end of file + subprocess("pppd", "call", "usbRadio") diff --git a/web_app/startup_win.bat b/web_app/startup_win.bat index 94396a3..69f3994 100644 --- a/web_app/startup_win.bat +++ b/web_app/startup_win.bat @@ -1,2 +1,2 @@ echo "Starting basestation web app..." -python app.py \ No newline at end of file +python app.py diff --git a/web_app/tests/__init__.py b/web_app/tests/__init__.py new file mode 100644 index 0000000..a8a65ec --- /dev/null +++ b/web_app/tests/__init__.py @@ -0,0 +1 @@ +# Backend tests From d9bdb0646b51ded5f63c3ceafd8f8af72683bfcc Mon Sep 17 00:00:00 2001 From: WilyHyperion <123993989+WilyHyperion@users.noreply.github.com> Date: Wed, 22 Apr 2026 18:40:16 -0700 Subject: [PATCH 03/10] Noah/motor test (#11) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * motor.hpp - created motor.hpp off of motor.py - set up pigpio in CMakeLists.txt - main.cpp is for testing motor.hpp * motor.hpp is a header file. motor.cpp doesn't exist. motor.hpp doesn't do stuff. * added some comments * added some comments * edits - moved files to src/drivers - edited gpioServo - it won't run with add_subdirectory(src/core) so I made it a comment * Revert "edits" This reverts commit cbc291e1222085288e56645a4d654706539eb37a. * Revert "Revert "edits"" This reverts commit 193e9ba305caa0b82730aee03bf0caecb5b3419f. * test motor without pi * spacing Now if you do BUILD_SIMULATION it'll run the little test code. If we want like an actual simulation we should do that. They just had some test code in motor.py that now works here. * made it fit the style better * comment * Added support for C language in cmake list and added lsm6dsox driver as a static library. platform_i2c.cpp so lis3md and lsm6dsox which are in c can communicate with pigpio. * test.cpp to test the motor also some imu stuff * test motor * for testing * Change code to use lgpio instead of pigpio Hopefully test should work now. * remove build * fixed pin * continued work on imu so it fits with lgpio better * cut out work on imu so it's just the motor code. It should still function the same. * added pool test code. It's untested. Pin numbers haven't been added yet. Also assuming gpio chip number is 0. * updated pins in pool_test * Replace lgTxServo with sysfs hardware PWM to fix motor stuttering * Replace sysfs PWM with RT-thread software PWM for Pi 5 sysfs hardware PWM (/sys/class/pwm/pwmchip0/) didn't work on Pi 5. Switch to hand-rolled 50Hz PWM running in a dedicated SCHED_FIFO thread that toggles GPIO via lgGpioWrite + clock_nanosleep. RT hardening to minimize jitter: - Pin PWM thread to CPU core 3 (pthread_setaffinity_np) - SCHED_FIFO priority 99 (max RT) - mlockall + 16KB stack prefault to avoid page faults - CMake defaults to Release build - add_compile_definitions(_GNU_SOURCE) for CPU_SET / pthread_setaffinity_np main.cpp and pool_test.cpp updated for new Motor API: constructor takes (pin, gpiochip handle), init() starts the RT thread, cleanup() stops it and frees the pin. * Busy-wait for PWM pulse width to eliminate ESC chatter The previous RT-thread approach still let clock_nanosleep wakeup jitter (~10-50us even at SCHED_FIFO 99) land on the LOW edge, since pulseEnd was computed from a fixed nextPeriod base. That wobbled pulse width enough for the T200 ESC to repeatedly lose sync. Fix: capture the real timestamp immediately after writing HIGH, compute pulseEnd from that, and busy-wait (clock_gettime loop, vDSO on aarch64) instead of clock_nanosleep. Pulse width is now pw +/- ~1us, well inside ESC tolerance. Period boundary still uses clock_nanosleep but its jitter no longer affects pulse width. Costs ~7.5% of core 3 CPU during each 1500us pulse, which is free since the thread is pinned there at priority 99. * Widen reverse test to 1450us to escape ESC dead zone 1475us sits on the edge of the T200 ESC's reverse deadband (~25us around 1500us neutral) and often reads as stopped. Forward at 1525us was working because the forward deadband happened to be just inside that margin; reverse needs more offset to cross reliably. * Arm 4 ESCs before user input in pool test; update prompt text pool_test.cpp was jumping straight from motor.init() into the speed input loop, so the first command hit unarmed ESCs and they ignored it. Add a 7-second 1500us neutral hold after init, mirroring main.cpp. Also update the speed-range prompt to reflect the ~25us ESC deadband we hit during single-motor testing: forward starts at ~1530, reverse at ~1470. Previous 1525/1475 text sat on the edge of the deadband. Add explicit and includes for std::this_thread:: sleep_for (they were coming in transitively via motor.hpp). * Simplify motor.hpp: drop non-essential RT hardening, clean up time math Remove CPU core pinning (pthread_setaffinity_np), stack prefault, and _GNU_SOURCE — these were belt-and-suspenders that don't measurably affect the ESC chatter fix. The essential pieces remain: SCHED_FIFO 99, mlockall, and the busy-wait anchored to the real HIGH timestamp. Replace timespec arithmetic (addNs helper, two-field comparisons) with int64 nanosecond math via nowNs()/sleepUntilNs() helpers. Drop explicit memory_order_relaxed annotations in favor of default ordering — the performance difference at 50Hz is unmeasurable. Public API is unchanged; main.cpp and pool_test.cpp need no updates. * Revert "Simplify motor.hpp: drop non-essential RT hardening, clean up time math" This reverts commit 691158bc4494c572a922d058c7d28f9218ea5885. * Unfinished working example * Work on motor class * Motor api * Fix git ignore * Remove uneeded build artifacts and gitignore them --------- Co-authored-by: IBfettuccine Co-authored-by: SuhaanCoding --- .gitignore | 6 +-- Docker/Dockerfile | 10 ++++- auv_cpp/api/motor_controller.hpp | 6 +++ src/.gitignore | 4 ++ src/CMakeLists.txt | 74 +++++++++++++++++++++++++++++++- src/drivers/motors/motor.cpp | 20 +++++++++ src/drivers/motors/motor.hpp | 32 ++++++++++++++ src/drivers/motors/noah_test.cpp | 25 +++++++++++ src/drivers/motors/sim_motor.cpp | 70 ++++++++++++++++++++++++++++++ src/drivers/motors/sim_motor.hpp | 30 +++++++++++++ src/drivers/motors/thruster.cpp | 57 ++++++++++++++++++++++++ src/drivers/motors/thruster.hpp | 40 +++++++++++++++++ src/tests/thruster.cc | 17 ++++++++ 13 files changed, 383 insertions(+), 8 deletions(-) create mode 100644 auv_cpp/api/motor_controller.hpp create mode 100644 src/drivers/motors/motor.cpp create mode 100644 src/drivers/motors/motor.hpp create mode 100644 src/drivers/motors/noah_test.cpp create mode 100644 src/drivers/motors/sim_motor.cpp create mode 100644 src/drivers/motors/sim_motor.hpp create mode 100644 src/drivers/motors/thruster.cpp create mode 100644 src/drivers/motors/thruster.hpp create mode 100644 src/tests/thruster.cc diff --git a/.gitignore b/.gitignore index 1d46c1d..631b342 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ **/.DS_Store **/node_modules **/.venv -<<<<<<< HEAD **/.vscode **/__pycache__ @@ -9,7 +8,4 @@ auv/data/local/* !auv/data/local/.gitkeep web_app/data/local/* !web_app/data/local/.gitkeep -======= -*.vscode -__pycache__ ->>>>>>> main +build \ No newline at end of file diff --git a/Docker/Dockerfile b/Docker/Dockerfile index 9a3b491..4b754a4 100644 --- a/Docker/Dockerfile +++ b/Docker/Dockerfile @@ -1,8 +1,14 @@ -FROM debian:bookworm +FROM ubuntu:latest RUN apt-get update -y # install cmake and ninja RUN apt-get install -y cmake build-essential RUN apt-get install -y ninja-build -RUN apt-get install -y git +RUN apt-get install -y git gdb curl + +#Get project dependencies +RUN apt-get install -y gpiod libgpiod-dev liblgpio-dev +WORKDIR /usr/local/include +RUN curl -LO https://github.com/yhirose/cpp-httplib/raw/refs/tags/latest/httplib.h > httplib.h + diff --git a/auv_cpp/api/motor_controller.hpp b/auv_cpp/api/motor_controller.hpp new file mode 100644 index 0000000..96e8d43 --- /dev/null +++ b/auv_cpp/api/motor_controller.hpp @@ -0,0 +1,6 @@ +//object that manages and abstracts all interactions with the motor array +// for the auv + +#pragma once + +#include diff --git a/src/.gitignore b/src/.gitignore index a007fea..a00eb85 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -1 +1,5 @@ build/* +CMakeFiles/* +CMakeCache.txt +.cache +.gazebo \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5b58b99..2acfd15 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,6 +1,78 @@ cmake_minimum_required(VERSION 3.25) -project(neptune CXX) +project(neptune C CXX) +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release) +endif() + +#required for pthread_setaffinity_np / CPU_SET (GNU extensions) +add_compile_definitions(_GNU_SOURCE) add_executable(neptune main.cpp) +include(FetchContent) +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip +) +# For Windows: Prevent overriding the parent project's compiler/linker settings +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +FetchContent_MakeAvailable(googletest) +#BUILD_SIMULATION option +#this isn't a physics engine its to mimic motor.py to simulate the motor +option(BUILD_SIMULATION "Build in simulation mode" OFF) + +#find lgpio library(only if not in simulation mode) +if(NOT BUILD_SIMULATION) + find_library(LGPIO_LIB lgpio) + find_path(LGPIO_INCLUDE_DIR lgpio.h) + + if(NOT LGPIO_LIB OR NOT LGPIO_INCLUDE_DIR) + message(FATAL_ERROR "lgpio library not found") + endif() + + set(LGPIO_LINK_LIBS ${LGPIO_LIB}) + message(STATUS "Found lgpio library: ${LGPIO_LIB}") + message(STATUS "Found lgpio include directory: ${LGPIO_INCLUDE_DIR}") + + #neptune links lgpio and has lgpio include directory if not in simulation + target_link_libraries(neptune ${LGPIO_LINK_LIBS}) + target_include_directories(neptune PRIVATE ${LGPIO_INCLUDE_DIR}) +else() + #lgpio library isn't required in simulation mode + #not sure how simulations will work yet if we do any + message(STATUS "Building in simulation mode. lgpio library not used.") + set(LGPIO_LINK_LIBS "") +endif() + +#create motor library (just header file) +add_library(motor INTERFACE) +target_include_directories(motor INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/drivers) + +#add lgpio include directory to motor target +if(NOT BUILD_SIMULATION) + target_include_directories(motor INTERFACE ${LGPIO_INCLUDE_DIR}) + target_link_libraries(motor INTERFACE ${LGPIO_LINK_LIBS}) +endif() + +#need pthreads for RT thread +find_package(Threads REQUIRED) + +enable_testing() + +add_executable( + thruster_test + tests/thruster.cc +) + +target_link_libraries( + thruster_test + GTest::gtest_main +) + +include(GoogleTest) +gtest_discover_tests(thruster_test) + + +#to use the motor library elsewhere +#target_link_libraries(your_target motor) diff --git a/src/drivers/motors/motor.cpp b/src/drivers/motors/motor.cpp new file mode 100644 index 0000000..a9e25db --- /dev/null +++ b/src/drivers/motors/motor.cpp @@ -0,0 +1,20 @@ +#include "motor.hpp" +#include + + +//All of these should probably not be here or throw execptions but I need to put them here for the linker +int Motor::setSpeed(float speed) +{ + std::cout << "Dont call me" << std::endl; + return 0; +} + +int Motor::setFrequency(float cycle) +{ + std::cout << "Dont call me" << std::endl; + return 0; +} + +void Motor::armMotor() +{ +} diff --git a/src/drivers/motors/motor.hpp b/src/drivers/motors/motor.hpp new file mode 100644 index 0000000..378e9b3 --- /dev/null +++ b/src/drivers/motors/motor.hpp @@ -0,0 +1,32 @@ +#pragma once +/** + * @brief The abstract class that represents a motor on the robot + * + */ +class Motor +{ +public: + /** + * @brief sets the motor to run at speed until this or another method is called + * @param speed A value between -1.0 and 1.0, with -1.0 being max reverse thrust, 0.0 being still, and 1.0 being max forward thrust + * @return The return code. Negative indicates errors, otherwise the meaning depends on the impelmenting class + */ + virtual int setSpeed(float speed); + /** + * @brief Sets the frequency of the motor pwm, ie how quick cycles are sent to the ESC + * @param cycle The value, in hz, for the new cycle + * @return The return code. Negative indicates errors, otherwise the meaning depends on the impelmenting class + */ + virtual int setFrequency(float cycle); + + /** + * @brief arms the motor. Likely a blocking method for a few seconds + */ + virtual void armMotor(); + float speed; + float cycle; + protected: + Motor(int speed,int cycle) : speed(speed), cycle(cycle){}; + protected: + Motor(){}; +}; \ No newline at end of file diff --git a/src/drivers/motors/noah_test.cpp b/src/drivers/motors/noah_test.cpp new file mode 100644 index 0000000..b4c7386 --- /dev/null +++ b/src/drivers/motors/noah_test.cpp @@ -0,0 +1,25 @@ +#include "motor.hpp" +#include "sim_motor.hpp" +#include +#include +#include +int main(){ + srand(time(0)); + Motor* m = new SimulatedMotor(SimulatedMotor::BackRight); + Motor* m1 = new SimulatedMotor(SimulatedMotor::FrontRight); + Motor* m2 = new SimulatedMotor(SimulatedMotor::FrontLeft); + Motor* m3 = new SimulatedMotor(SimulatedMotor::BackLeft); + m->armMotor(); + m1->armMotor(); + m2->armMotor(); + m3->armMotor(); + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_real_distribution dis(-1.0, 1.0); + while (true) + { + if(rand() % 2) m->setSpeed(dis(gen)); + if(rand() % 2) m1->setSpeed(dis(gen)); + sleep(3); + } +} \ No newline at end of file diff --git a/src/drivers/motors/sim_motor.cpp b/src/drivers/motors/sim_motor.cpp new file mode 100644 index 0000000..efc4a0a --- /dev/null +++ b/src/drivers/motors/sim_motor.cpp @@ -0,0 +1,70 @@ +#include "sim_motor.hpp" +#include +httplib::Client* SimulatedMotor::sim_connection = nullptr; +std::string SimulatedMotor::GetMotorPath(){ + switch (this->ml) + { + case SimulatedMotor::FrontLeft: + return "/motor/top_left/set"; + break; + case SimulatedMotor::FrontRight: + return "/motor/top_right/set"; + break; + case SimulatedMotor::BackLeft: + return "/motor/bottom_left/set"; + break; + case SimulatedMotor::BackRight: + return "/motor/bottom_right/set"; + break; + default: + //Error? + break; + } + return ""; +} + +SimulatedMotor::SimulatedMotor(SimulatedMotor::MotorLocation ml, int speed, float cycle) : Motor(speed, cycle) +{ + if (SimulatedMotor::sim_connection == 0) + { + SimulatedMotor::sim_connection = new httplib::Client(SIM_ADDR); + httplib::Result out = SimulatedMotor::sim_connection->Get("/heartbeat"); + if(out == nullptr){ + throw std::runtime_error("No simulation connection found"); + } + std::cout << "Status code from sim:" << out->status << std::endl; + } + this->ml = ml; +} + +int SimulatedMotor::setSpeed(float speed) +{ + if(!this->armed){ + throw std::logic_error("Tried to set the speed of an unarmed motor"); + } + this->speed = speed; + // In the long run we should add a json lib + std::ostringstream oss; + oss << "{\"speed\":" << speed << "}"; + std::string s = oss.str(); + auto out = SimulatedMotor::sim_connection->Post(this->GetMotorPath().c_str(), s.c_str(),"application/json"); + if(out == nullptr){ + std::cout << "Error sending sim info" << std::endl; + } + else { + std::cout << out->status << std::endl; + } + + return 0; +} + +int SimulatedMotor::setFrequency(float cycle) +{ + // idk if this will be called at all + return 0; +} + +void SimulatedMotor::armMotor() +{ + this->armed = true; +} \ No newline at end of file diff --git a/src/drivers/motors/sim_motor.hpp b/src/drivers/motors/sim_motor.hpp new file mode 100644 index 0000000..ba987b4 --- /dev/null +++ b/src/drivers/motors/sim_motor.hpp @@ -0,0 +1,30 @@ +#pragma once +#include "motor.hpp" +#include +#include + +class SimulatedMotor : public Motor +{ + + public: + //At some point we should prob use this datatype everywhere, for now its here + enum MotorLocation { + FrontRight, + FrontLeft, + BackRight, + BackLeft + }; + MotorLocation ml; + bool armed; + SimulatedMotor(SimulatedMotor::MotorLocation ml, int speed = 0.0, float cycle = 50); + int setSpeed(float speed) override; + + int setFrequency(float cycle) override; + + void armMotor() override; + private: + + std::string GetMotorPath(); + const std::string SIM_ADDR = "http://localhost:6767"; + static httplib::Client* sim_connection; +}; \ No newline at end of file diff --git a/src/drivers/motors/thruster.cpp b/src/drivers/motors/thruster.cpp new file mode 100644 index 0000000..cf0d923 --- /dev/null +++ b/src/drivers/motors/thruster.cpp @@ -0,0 +1,57 @@ +#include "thruster.hpp" +#include +#include +#include +#include + + +Thruster::Thruster(int pin, int handle, int speed /*= 0*/, int cycle /*= 50*/) : pin(pin) , handle(handle), Motor(speed, cycle) { + if(lgGpioClaimOutput(handle, 0, pin, 0) !=LG_OKAY ){ + throw new std::runtime_error("Failed to claim a pin for a motor"); + } +} + +Thruster::~Thruster(){ + if(lgGpioFree(handle, pin) <0) { + std::cerr << "Error code freeing pin " << pin << std::endl; + } +} +float Thruster::getDutyCycle() +{ + // Convert to nanoseconds + float cycle_width = 1 / (this->cycle * (1e-6)) ; + float goal_interval = ((MAX_PWM_DIST * this->speed) + STOP_PWM); + return (goal_interval / cycle_width) * 100; +} + +int Thruster::sendMotorPWM() +{ + if(!this->armed){ + throw new std::logic_error("Tried to affect motor before arming. Always ensure motors are armed before activating them"); + } + return lgTxPwm(this->handle, this->pin, this->cycle, getDutyCycle(), 0, 0); + +} + +int Thruster::setSpeed(float newSpeed) { + this->speed = newSpeed; + if(newSpeed < -1.0 || newSpeed > 1.0){ + //TODO clamp here instead ? + throw new std::logic_error("Motor Speed out of range"); + } + return sendMotorPWM(); +} + +int Thruster::setFrequency(float cycle) +{ + this->cycle = cycle; + return sendMotorPWM(); +} +void Thruster::armMotor(){ + this->speed = 0.0; + this->armed = true; + if(this->sendMotorPWM() < 0){ + //Maybe error here? + } + lguSleep(ARM_SECONDS); +} diff --git a/src/drivers/motors/thruster.hpp b/src/drivers/motors/thruster.hpp new file mode 100644 index 0000000..2efc90d --- /dev/null +++ b/src/drivers/motors/thruster.hpp @@ -0,0 +1,40 @@ +#pragma once +#include "motor.hpp" +#include +/** + * @brief Represents one of the blue robotics thrusters on the robot + */ +class Thruster : public Motor +{ +public: + int pin; + int handle; + bool armed; + + Thruster(int pin, int handle, int speed = 0, int cycle = 50); + ~Thruster(); + int setSpeed(float speed) override; + + int setFrequency(float cycle) override; + + void armMotor() override; + /** + * @brief gets the duty cycle to send to the motor in order to move the motor at the current speed + */ + float getDutyCycle(); + protected: + Thruster() {}; +private: + const int STOP_PWM = 1500; + const int MAX_PWM_DIST = 400; + const int ARM_SECONDS = 3; //I dont think this actually has to be 7 seconds, like 3 works but better safe than sorry + + + /** + * @brief updates the pwm values being sent. Must be called after any updates to class variables that will affect this value + * @return the return value of the lgTxPwm call + */ + int sendMotorPWM(); + + +}; \ No newline at end of file diff --git a/src/tests/thruster.cc b/src/tests/thruster.cc new file mode 100644 index 0000000..ec4e921 --- /dev/null +++ b/src/tests/thruster.cc @@ -0,0 +1,17 @@ +#include +#include "thruster.hpp" +#include +TEST(Thruster, DutyCycle){ + Thruster* thruster = new Thruster(); + thruster->armed = true; + thruster->cycle = 50; + thruster->speed = 0.0; + EXPECT_EQ(7.5, thruster->getDutyCycle()); + +} + +TEST(Thruster, UnarmedFailure){ + Thruster* thruster = new Thruster(0,0); + thruster->armed = false; + ASSERT_THROW(thruster->setSpeed(50, std::logic_error())); +} From ea9f5a5d0ff19031b7520a37026a1d3f5f4ac014 Mon Sep 17 00:00:00 2001 From: Andrew <112200249+andytgarcia@users.noreply.github.com> Date: Wed, 29 Apr 2026 19:24:54 -0700 Subject: [PATCH 04/10] Andytgarcia/new comms (#12) * added old server with updated motor func * ran linter, just pruning endlines * comment hot fix imma bypass da rules --- .gitignore | 2 +- Docker/Dockerfile | 5 +- auv_cpp/api/motor_controller.hpp | 4 +- motor_client.py | 190 +++++++++++++++ src/.gitignore | 2 +- src/CMakeLists.txt | 41 +++- src/comms/tcp_server.hpp | 123 ++++++++++ src/drivers/motors/motor.hpp | 2 +- src/drivers/motors/noah_test.cpp | 6 +- src/drivers/motors/sim_motor.cpp | 4 +- src/drivers/motors/sim_motor.hpp | 6 +- src/drivers/motors/thruster.cpp | 2 +- src/drivers/motors/thruster.hpp | 4 +- src/drivers/server/motor_server.cpp | 363 ++++++++++++++++++++++++++++ src/tests/thruster.cc | 2 +- 15 files changed, 734 insertions(+), 22 deletions(-) create mode 100644 motor_client.py create mode 100644 src/comms/tcp_server.hpp create mode 100644 src/drivers/server/motor_server.cpp diff --git a/.gitignore b/.gitignore index 631b342..ad41229 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,4 @@ auv/data/local/* !auv/data/local/.gitkeep web_app/data/local/* !web_app/data/local/.gitkeep -build \ No newline at end of file +build diff --git a/Docker/Dockerfile b/Docker/Dockerfile index 4b754a4..5fd71c1 100644 --- a/Docker/Dockerfile +++ b/Docker/Dockerfile @@ -7,8 +7,7 @@ RUN apt-get install -y cmake build-essential RUN apt-get install -y ninja-build RUN apt-get install -y git gdb curl -#Get project dependencies -RUN apt-get install -y gpiod libgpiod-dev liblgpio-dev +#Get project dependencies +RUN apt-get install -y gpiod libgpiod-dev liblgpio-dev WORKDIR /usr/local/include RUN curl -LO https://github.com/yhirose/cpp-httplib/raw/refs/tags/latest/httplib.h > httplib.h - diff --git a/auv_cpp/api/motor_controller.hpp b/auv_cpp/api/motor_controller.hpp index 96e8d43..098bee4 100644 --- a/auv_cpp/api/motor_controller.hpp +++ b/auv_cpp/api/motor_controller.hpp @@ -1,5 +1,5 @@ -//object that manages and abstracts all interactions with the motor array -// for the auv +// object that manages and abstracts all interactions with the motor array +// for the auv #pragma once diff --git a/motor_client.py b/motor_client.py new file mode 100644 index 0000000..5425d6b --- /dev/null +++ b/motor_client.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +""" +motor_client.py — Base station client for motor_server + +Connects to the Pi's motor_server over TCP (WiFi HaLow, Ethernet, USB-gadget, +or localhost) and provides an interactive command line to control motors. + +For HolyBro Telemetry Radio: + ensure that radios are connected to Pi and Base Station + on Pi: sudo pppd /dev/ttyUSB0 57600 lock local noauth 192.168.100.11:192.168.100.10 + on base: sudo pppd /dev/ttyUSB0 57600 lock local noauth nodetach 192.168.100.10:192.168.100.11 + +Usage: + python3 motor_client.py # localhost:9000 (both sides local) + python3 motor_client.py 192.168.100.11 # Pi via static Ethernet/HaLow + python3 motor_client.py 192.168.100.11 9000 # explicit port + +Commands (once connected): + motors Set forward, turn, front, back [-1.0 to 1.0] + pwm Set one motor by raw PWM (1100–1900 µs) + stop Zero all motors + status Query current normalized speed for all motors + quit Shut down the server and exit + help Show this message + Ctrl+C or exit Disconnect and exit client only +""" + +import json +import socket +import sys +import readline # noqa: F401 — gives line editing / history for free + + +DEFAULT_HOST = "localhost" +DEFAULT_PORT = 9000 + + +def send_cmd(sock: socket.socket, obj: dict) -> dict | None: + """Send a JSON command and return the parsed response.""" + try: + msg = json.dumps(obj) + "\n" + sock.sendall(msg.encode()) + # Read until newline + buf = b"" + while not buf.endswith(b"\n"): + chunk = sock.recv(4096) + if not chunk: + return None + buf += chunk + return json.loads(buf.strip()) + except (OSError, json.JSONDecodeError) as e: + print(f"[ERR] {e}") + return None + + +def print_response(resp: dict | None): + if resp is None: + print("[ERR] No response (disconnected?)") + return + ok = resp.get("ok", False) + prefix = "[ OK]" if ok else "[ERR]" + + # Status response has speeds array (normalized -1.0 to 1.0) + if "speeds" in resp: + speeds = resp["speeds"] + labels = ["forward", "turn ", "front ", "back "] + print(f"{prefix} Motor speeds (normalized):") + for i, (label, speed) in enumerate(zip(labels, speeds)): + bar_len = int(abs(speed) * 20) + direction = "►" if speed >= 0 else "◄" + bar = direction * bar_len + print(f" [{i}] {label}: {speed:+.3f} {bar}") + else: + print(f"{prefix} {resp.get('msg', resp)}") + + +def print_help(): + print(""" +Commands: + motors Normalized values, -1.0 to 1.0 + fwd = forward/reverse thruster + turn = yaw thruster + front = front vertical thruster + back = back vertical thruster + + pwm Raw PWM for one motor (1100–1900 µs) + 1500 = neutral/stop + 1900 = full forward + 1100 = full reverse + + stop Stop all motors + status Show current normalized speed for all motors + quit Shut down the server then exit + help Show this message + exit / Ctrl+C Disconnect client (server keeps running) +""") + + +def main(): + host = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_HOST + port = int(sys.argv[2]) if len(sys.argv) > 2 else DEFAULT_PORT + + print(f"[CLIENT] Connecting to {host}:{port} ...") + try: + sock = socket.create_connection((host, port), timeout=5) + sock.settimeout(None) # blocking after connect + except OSError as e: + print(f"[ERR] Could not connect: {e}") + sys.exit(1) + + # Read the greeting + buf = b"" + while not buf.endswith(b"\n"): + buf += sock.recv(4096) + greeting = json.loads(buf.strip()) + print_response(greeting) + print_help() + + try: + while True: + try: + raw = input("motor> ").strip() + except EOFError: + break + + if not raw: + continue + + parts = raw.split() + cmd = parts[0].lower() + + if cmd in ("exit", "disconnect"): + print("[CLIENT] Disconnecting (server still running).") + break + + elif cmd == "help": + print_help() + + elif cmd == "stop": + resp = send_cmd(sock, {"command": "stop"}) + print_response(resp) + + elif cmd == "status": + resp = send_cmd(sock, {"command": "status"}) + print_response(resp) + + elif cmd == "quit": + resp = send_cmd(sock, {"command": "quit"}) + print_response(resp) + break + + elif cmd == "motors": + if len(parts) != 5: + print("[ERR] Usage: motors ") + continue + try: + vals = [float(p) for p in parts[1:]] + except ValueError: + print("[ERR] Values must be floats in [-1.0, 1.0]") + continue + if any(abs(v) > 1.0 for v in vals): + print("[WARN] Values outside [-1, 1] will be clamped by the server.") + resp = send_cmd(sock, {"command": "motors", "values": vals}) + print_response(resp) + + elif cmd == "pwm": + if len(parts) != 3: + print("[ERR] Usage: pwm ") + continue + try: + idx = int(parts[1]) + val = int(parts[2]) + except ValueError: + print("[ERR] Index and value must be integers.") + continue + resp = send_cmd(sock, {"command": "pwm", "motor": idx, "value": val}) + print_response(resp) + + else: + print(f"[ERR] Unknown command '{cmd}'. Type 'help' for options.") + + except KeyboardInterrupt: + print("\n[CLIENT] Interrupted.") + finally: + sock.close() + print("[CLIENT] Connection closed.") + + +if __name__ == "__main__": + main() diff --git a/src/.gitignore b/src/.gitignore index a00eb85..5dd7ff8 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -2,4 +2,4 @@ build/* CMakeFiles/* CMakeCache.txt .cache -.gazebo \ No newline at end of file +.gazebo diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2acfd15..74298b1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -18,6 +18,8 @@ FetchContent_Declare( # For Windows: Prevent overriding the parent project's compiler/linker settings set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) FetchContent_MakeAvailable(googletest) + + #BUILD_SIMULATION option #this isn't a physics engine its to mimic motor.py to simulate the motor option(BUILD_SIMULATION "Build in simulation mode" OFF) @@ -73,6 +75,41 @@ target_link_libraries( include(GoogleTest) gtest_discover_tests(thruster_test) +add_library(thruster STATIC + drivers/motors/thruster.cpp + drivers/motors/motor.cpp +) +target_include_directories(thruster PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/drivers +) +if(NOT BUILD_SIMULATION) + target_link_libraries(thruster PUBLIC ${LGPIO_LINK_LIBS}) + target_include_directories(thruster PUBLIC ${LGPIO_INCLUDE_DIR}) +endif() + +add_library(sim_motor STATIC + drivers/motors/sim_motor.cpp + drivers/motors/motor.cpp +) +target_include_directories(sim_motor PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/drivers +) -#to use the motor library elsewhere -#target_link_libraries(your_target motor) +add_executable(motor_server + drivers/server/motor_server.cpp +) + +target_include_directories(motor_server PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/comms + ${CMAKE_CURRENT_SOURCE_DIR}/drivers +) + +target_link_libraries(motor_server PRIVATE Threads::Threads) + +if(NOT BUILD_SIMULATION) + target_link_libraries(motor_server PRIVATE thruster) + target_include_directories(motor_server PRIVATE ${LGPIO_INCLUDE_DIR}) +else() + target_link_libraries(motor_server PRIVATE sim_motor) + target_compile_definitions(motor_server PRIVATE BUILD_SIMULATION) +endif() diff --git a/src/comms/tcp_server.hpp b/src/comms/tcp_server.hpp new file mode 100644 index 0000000..6b54345 --- /dev/null +++ b/src/comms/tcp_server.hpp @@ -0,0 +1,123 @@ +#pragma once + +/** + * @file tcp_server.hpp + * @brief Minimal single-client TCP server using POSIX sockets. + * + * Accepts one connection at a time. Each call to recv_line() blocks until a + * newline-terminated message arrives. The server automatically re-accepts after + * a client disconnects. + * + * No external dependencies — POSIX only. + * + * For HolyBro Telemetry Radio: + ensure that radios are connected to Pi and Base Station + on Pi: sudo pppd /dev/ttyUSB0 57600 lock local noauth 192.168.100.11:192.168.100.10 + on base: sudo pppd /dev/ttyUSB0 57600 lock local noauth nodetach 192.168.100.10:192.168.100.11 + + IPs should be ordered relative to the device you are running the command on + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +class TcpServer { +public: + explicit TcpServer(int port) : port_(port), server_fd_(-1), client_fd_(-1) {} + + ~TcpServer() { close_all(); } + + /** + * Bind and start listening. Call once before the main loop. + */ + void start() { + server_fd_ = socket(AF_INET, SOCK_STREAM, 0); + if (server_fd_ < 0) throw std::runtime_error("socket() failed: " + err()); + + // Allow rapid restart without "Address already in use" + int opt = 1; + setsockopt(server_fd_, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = INADDR_ANY; + addr.sin_port = htons(static_cast(port_)); + + if (bind(server_fd_, reinterpret_cast(&addr), sizeof(addr)) < 0) + throw std::runtime_error("bind() failed: " + err()); + + if (listen(server_fd_, 1) < 0) + throw std::runtime_error("listen() failed: " + err()); + + std::cout << "[TCP] Listening on port " << port_ << std::endl; + } + + /** + * Block until a client connects. Closes any existing client first. + */ + void accept_client() { + if (client_fd_ >= 0) { + close(client_fd_); + client_fd_ = -1; + } + sockaddr_in client_addr{}; + socklen_t len = sizeof(client_addr); + std::cout << "[TCP] Waiting for connection..." << std::endl; + client_fd_ = accept(server_fd_, reinterpret_cast(&client_addr), &len); + if (client_fd_ < 0) throw std::runtime_error("accept() failed: " + err()); + + char ip[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, &client_addr.sin_addr, ip, sizeof(ip)); + std::cout << "[TCP] Client connected from " << ip << std::endl; + } + + /** + * Read one newline-terminated line from the current client. + * Returns false if the client disconnected (caller should call accept_client again). + */ + bool recv_line(std::string& out) { + out.clear(); + char ch; + while (true) { + ssize_t n = recv(client_fd_, &ch, 1, 0); + if (n <= 0) { + std::cout << "[TCP] Client disconnected." << std::endl; + return false; + } + if (ch == '\n') return true; + if (ch != '\r') out += ch; + } + } + + /** + * Send a newline-terminated string to the current client. + * Returns false if the send failed. + */ + bool send_line(const std::string& msg) { + std::string out = msg + "\n"; + ssize_t n = send(client_fd_, out.c_str(), out.size(), MSG_NOSIGNAL); + return n == static_cast(out.size()); + } + + bool has_client() const { return client_fd_ >= 0; } + +private: + int port_; + int server_fd_; + int client_fd_; + + void close_all() { + if (client_fd_ >= 0) { close(client_fd_); client_fd_ = -1; } + if (server_fd_ >= 0) { close(server_fd_); server_fd_ = -1; } + } + + static std::string err() { return std::string(strerror(errno)); } +}; diff --git a/src/drivers/motors/motor.hpp b/src/drivers/motors/motor.hpp index 378e9b3..da8bcc0 100644 --- a/src/drivers/motors/motor.hpp +++ b/src/drivers/motors/motor.hpp @@ -29,4 +29,4 @@ class Motor Motor(int speed,int cycle) : speed(speed), cycle(cycle){}; protected: Motor(){}; -}; \ No newline at end of file +}; diff --git a/src/drivers/motors/noah_test.cpp b/src/drivers/motors/noah_test.cpp index b4c7386..34745f3 100644 --- a/src/drivers/motors/noah_test.cpp +++ b/src/drivers/motors/noah_test.cpp @@ -13,8 +13,8 @@ int main(){ m1->armMotor(); m2->armMotor(); m3->armMotor(); - std::random_device rd; - std::mt19937 gen(rd()); + std::random_device rd; + std::mt19937 gen(rd()); std::uniform_real_distribution dis(-1.0, 1.0); while (true) { @@ -22,4 +22,4 @@ int main(){ if(rand() % 2) m1->setSpeed(dis(gen)); sleep(3); } -} \ No newline at end of file +} diff --git a/src/drivers/motors/sim_motor.cpp b/src/drivers/motors/sim_motor.cpp index efc4a0a..64de212 100644 --- a/src/drivers/motors/sim_motor.cpp +++ b/src/drivers/motors/sim_motor.cpp @@ -37,7 +37,7 @@ SimulatedMotor::SimulatedMotor(SimulatedMotor::MotorLocation ml, int speed, floa this->ml = ml; } -int SimulatedMotor::setSpeed(float speed) +int SimulatedMotor::setSpeed(float speed) { if(!this->armed){ throw std::logic_error("Tried to set the speed of an unarmed motor"); @@ -67,4 +67,4 @@ int SimulatedMotor::setFrequency(float cycle) void SimulatedMotor::armMotor() { this->armed = true; -} \ No newline at end of file +} diff --git a/src/drivers/motors/sim_motor.hpp b/src/drivers/motors/sim_motor.hpp index ba987b4..24cdccf 100644 --- a/src/drivers/motors/sim_motor.hpp +++ b/src/drivers/motors/sim_motor.hpp @@ -5,7 +5,7 @@ class SimulatedMotor : public Motor { - + public: //At some point we should prob use this datatype everywhere, for now its here enum MotorLocation { @@ -25,6 +25,6 @@ class SimulatedMotor : public Motor private: std::string GetMotorPath(); - const std::string SIM_ADDR = "http://localhost:6767"; + const std::string SIM_ADDR = "http://localhost:6767"; static httplib::Client* sim_connection; -}; \ No newline at end of file +}; diff --git a/src/drivers/motors/thruster.cpp b/src/drivers/motors/thruster.cpp index cf0d923..753d66e 100644 --- a/src/drivers/motors/thruster.cpp +++ b/src/drivers/motors/thruster.cpp @@ -36,7 +36,7 @@ int Thruster::sendMotorPWM() int Thruster::setSpeed(float newSpeed) { this->speed = newSpeed; if(newSpeed < -1.0 || newSpeed > 1.0){ - //TODO clamp here instead ? + //TODO clamp here instead ? throw new std::logic_error("Motor Speed out of range"); } return sendMotorPWM(); diff --git a/src/drivers/motors/thruster.hpp b/src/drivers/motors/thruster.hpp index 2efc90d..6810a8e 100644 --- a/src/drivers/motors/thruster.hpp +++ b/src/drivers/motors/thruster.hpp @@ -12,7 +12,7 @@ class Thruster : public Motor bool armed; Thruster(int pin, int handle, int speed = 0, int cycle = 50); - ~Thruster(); + ~Thruster(); int setSpeed(float speed) override; int setFrequency(float cycle) override; @@ -37,4 +37,4 @@ class Thruster : public Motor int sendMotorPWM(); -}; \ No newline at end of file +}; diff --git a/src/drivers/server/motor_server.cpp b/src/drivers/server/motor_server.cpp new file mode 100644 index 0000000..facbe80 --- /dev/null +++ b/src/drivers/server/motor_server.cpp @@ -0,0 +1,363 @@ +/** + * @file motor_server.cpp + * @brief Remote motor control server over TCP. + * + * Uses the Thruster class (real hardware) or SimulatedMotor (BUILD_SIMULATION). + * + * Motor API: + * - Thruster(int pin, int handle) -- constructor claims pin; destructor frees it + * - motor.armMotor() -- blocks ~3 s, sets neutral + * - motor.setSpeed(float speed) -- -1.0 (full reverse) .. 0.0 (stop) .. 1.0 (full forward) + * + * ## Protocol (newline-terminated JSON) + * + * Set all four motors (normalized -1.0 to 1.0): + * {"command":"motors","values":[0.5, 0.0, -0.3, 0.2]} + * + * Set one motor by raw PWM microseconds (1100-1900): + * {"command":"pwm","motor":0,"value":1600} + * (converted internally to normalized speed before calling setSpeed) + * + * Stop all motors: + * {"command":"stop"} + * + * Query current normalized speeds: + * {"command":"status"} + * + * Graceful server shutdown: + * {"command":"quit"} + * + * ## Build & run on Pi + * cmake -B build && cmake --build build --target motor_server + * sudo ./build/motor_server [port] (default 9000) + * + * ## Simulate (routes motor commands to Gazebo sim over HTTP) + * cmake -B build -DBUILD_SIMULATION=ON + * ./build/motor_server + */ + +#include +#include +#include +#include +#include +#include +#include + +#ifndef BUILD_SIMULATION + #include + #include "../motors/thruster.hpp" +#else + #include "../motors/sim_motor.hpp" +#endif + +#include "../../comms/tcp_server.hpp" +// --------------------------------------------------------------------------- +// Constants -- mirrors Thruster internals for the "pwm" command converter +// --------------------------------------------------------------------------- +static constexpr float STOP_SPEED = 0.0f; +static constexpr int NEUTRAL_PWM = 1500; +static constexpr int MIN_PWM = 1100; +static constexpr int MAX_PWM = 1900; +static constexpr int MAX_PWM_DIST = 400; // microseconds from neutral to limit + +// --------------------------------------------------------------------------- +// Motor GPIO pin assignments (matches pool_test.cpp / motor_controller.py) +// --------------------------------------------------------------------------- +static constexpr int NUM_MOTORS = 4; +static constexpr int MOTOR_PINS[NUM_MOTORS] = { + 4, // forward (FORWARD_GPIO_PIN) + 11, // turn (TURN_GPIO_PIN) + 18, // front (FRONT_GPIO_PIN) + 24, // back (BACK_GPIO_PIN) +}; + +#ifdef BUILD_SIMULATION +// Map each motor slot to a SimulatedMotor::MotorLocation for the HTTP sim API. +// Mapping is approximate -- adjust once the sim model is finalised. +static constexpr SimulatedMotor::MotorLocation MOTOR_LOCATIONS[NUM_MOTORS] = { + SimulatedMotor::FrontLeft, // motor 0 - forward + SimulatedMotor::FrontRight, // motor 1 - turn + SimulatedMotor::BackLeft, // motor 2 - front + SimulatedMotor::BackRight, // motor 3 - back +}; +#endif + +// Current normalized speed for each motor (source of truth for "status" replies) +static std::array g_speeds = { + STOP_SPEED, STOP_SPEED, STOP_SPEED, STOP_SPEED +}; + +// --------------------------------------------------------------------------- +// Minimal JSON helpers (no external lib) +// --------------------------------------------------------------------------- +static std::string make_response(bool ok, const std::string& msg) { + std::ostringstream ss; + ss << "{\"ok\":" << (ok ? "true" : "false") + << ",\"msg\":\"" << msg << "\"}"; + return ss.str(); +} + +static std::string make_status(const std::array& speeds) { + std::ostringstream ss; + ss << "{\"ok\":true,\"speeds\":[" + << speeds[0] << "," << speeds[1] << "," << speeds[2] << "," << speeds[3] + << "]}"; + return ss.str(); +} + +static std::string parse_string(const std::string& json, const std::string& key) { + std::string search = "\"" + key + "\""; + size_t pos = json.find(search); + if (pos == std::string::npos) return ""; + pos = json.find(':', pos); + if (pos == std::string::npos) return ""; + pos = json.find('"', pos); + if (pos == std::string::npos) return ""; + size_t end = json.find('"', pos + 1); + if (end == std::string::npos) return ""; + return json.substr(pos + 1, end - pos - 1); +} + +static bool parse_int(const std::string& json, const std::string& key, int& out) { + std::string search = "\"" + key + "\""; + size_t pos = json.find(search); + if (pos == std::string::npos) return false; + pos = json.find(':', pos); + if (pos == std::string::npos) return false; + pos++; + while (pos < json.size() && (json[pos] == ' ' || json[pos] == '\t')) pos++; + try { + size_t consumed = 0; + out = std::stoi(json.substr(pos), &consumed); + return consumed > 0; + } catch (...) { return false; } +} + +static bool parse_float_array4(const std::string& json, const std::string& key, + std::array& out) { + std::string search = "\"" + key + "\""; + size_t pos = json.find(search); + if (pos == std::string::npos) return false; + pos = json.find('[', pos); + if (pos == std::string::npos) return false; + pos++; + for (int i = 0; i < 4; i++) { + while (pos < json.size() && + (json[pos] == ' ' || json[pos] == '\t' || json[pos] == ',')) + pos++; + if (pos >= json.size() || json[pos] == ']') return false; + try { + size_t consumed = 0; + out[i] = std::stof(json.substr(pos), &consumed); + pos += consumed; + } catch (...) { return false; } + } + return true; +} + +// Convert PWM microseconds (1100-1900) to normalized speed (-1.0 to 1.0). +// Mirrors Thruster::getDutyCycle() in reverse. +static float pwm_to_speed(int pwm) { + return static_cast(pwm - NEUTRAL_PWM) / static_cast(MAX_PWM_DIST); +} + +static void zero_motors(std::array& motors) { + for (int i = 0; i < NUM_MOTORS; i++) { + motors[i]->setSpeed(STOP_SPEED); + g_speeds[i] = STOP_SPEED; + } +} + +// --------------------------------------------------------------------------- +// Signal handling +// --------------------------------------------------------------------------- +static volatile bool g_running = true; +static void handle_signal(int) { g_running = false; } + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- +int main(int argc, char* argv[]) { + int port = 9000; + if (argc >= 2) { + try { port = std::stoi(argv[1]); } + catch (...) { + std::cerr << "Usage: " << argv[0] << " [port]\n"; + return 1; + } + } + + std::signal(SIGINT, handle_signal); + std::signal(SIGTERM, handle_signal); + std::signal(SIGPIPE, SIG_IGN); + + // ----------------------------------------------------------------------- + // Open lgpio chip (real hardware only) + // ----------------------------------------------------------------------- +#ifndef BUILD_SIMULATION + int handle = lgGpiochipOpen(0); + if (handle < 0) { + std::cerr << "[INIT] lgGpiochipOpen failed: " << handle << std::endl; + return 1; + } + std::cout << "[INIT] GPIO chip opened." << std::endl; +#else + std::cout << "[SIM] Simulation mode -- routing to Gazebo sim over HTTP." << std::endl; +#endif + + // ----------------------------------------------------------------------- + // Construct motors + // + // Thruster: constructor claims the GPIO pin; destructor frees it. + // SimulatedMotor: constructor connects to the HTTP sim server. + // Both throw on failure, so we catch here and bail cleanly. + // ----------------------------------------------------------------------- + std::array, NUM_MOTORS> motor_storage; + + std::cout << "[INIT] Constructing motors..." << std::endl; + for (int i = 0; i < NUM_MOTORS; i++) { + try { +#ifndef BUILD_SIMULATION + motor_storage[i] = std::make_unique(MOTOR_PINS[i], handle); +#else + motor_storage[i] = std::make_unique(MOTOR_LOCATIONS[i]); +#endif + } catch (const std::exception& e) { + std::cerr << "[INIT] Failed to construct motor " << i << ": " + << e.what() << std::endl; +#ifndef BUILD_SIMULATION + lgGpiochipClose(handle); +#endif + return 1; + } + } + + // Raw pointer array for convenience in helper functions + std::array motors = { + motor_storage[0].get(), motor_storage[1].get(), + motor_storage[2].get(), motor_storage[3].get() + }; + + // ----------------------------------------------------------------------- + // Arm ESCs -- armMotor() sets neutral and blocks for the required delay + // ----------------------------------------------------------------------- + std::cout << "[INIT] Arming ESCs..." << std::endl; + for (auto* m : motors) m->armMotor(); + std::cout << "[INIT] Motors ready." << std::endl; + + // ----------------------------------------------------------------------- + // TCP server + // ----------------------------------------------------------------------- + TcpServer server(port); + try { + server.start(); + } catch (const std::exception& e) { + std::cerr << "[TCP] " << e.what() << std::endl; +#ifndef BUILD_SIMULATION + lgGpiochipClose(handle); +#endif + return 1; + } + + // ----------------------------------------------------------------------- + // Main accept loop + // ----------------------------------------------------------------------- + while (g_running) { + try { + server.accept_client(); + } catch (const std::exception& e) { + std::cerr << "[TCP] accept failed: " << e.what() << std::endl; + break; + } + + server.send_line(make_response(true, "connected -- motor_server ready")); + + std::string line; + while (g_running && server.recv_line(line)) { + if (line.empty()) continue; + std::cout << "[CMD] " << line << std::endl; + + std::string cmd = parse_string(line, "command"); + + if (cmd == "stop") { + zero_motors(motors); + server.send_line(make_response(true, "all motors stopped")); + + } else if (cmd == "motors") { + std::array vals{}; + if (!parse_float_array4(line, "values", vals)) { + server.send_line(make_response(false, "bad values array")); + continue; + } + bool ok = true; + for (int i = 0; i < NUM_MOTORS; i++) { + // Clamp to [-1, 1] before passing to setSpeed + float speed = vals[i]; + if (speed > 1.0f) speed = 1.0f; + if (speed < -1.0f) speed = -1.0f; + + if (motors[i]->setSpeed(speed) < 0) { + server.send_line(make_response(false, + "motor " + std::to_string(i) + " setSpeed failed")); + ok = false; + break; + } + g_speeds[i] = speed; + } + if (ok) server.send_line(make_response(true, "motors updated")); + + } else if (cmd == "pwm") { + // Accept legacy PWM microseconds (1100-1900) and convert to speed + int motor_idx = -1, pwm_value = -1; + if (!parse_int(line, "motor", motor_idx) || + !parse_int(line, "value", pwm_value) || + motor_idx < 0 || motor_idx >= NUM_MOTORS || + pwm_value < MIN_PWM || pwm_value > MAX_PWM) { + server.send_line(make_response(false, + "bad args -- motor 0-3, value 1100-1900")); + continue; + } + float speed = pwm_to_speed(pwm_value); + if (motors[motor_idx]->setSpeed(speed) < 0) { + server.send_line(make_response(false, "setSpeed failed")); + continue; + } + g_speeds[motor_idx] = speed; + server.send_line(make_response(true, + "motor " + std::to_string(motor_idx) + + " -> " + std::to_string(pwm_value) + " us" + " (speed " + std::to_string(speed) + ")")); + + } else if (cmd == "status") { + server.send_line(make_status(g_speeds)); + + } else if (cmd == "quit") { + server.send_line(make_response(true, "shutting down")); + g_running = false; + break; + + } else { + server.send_line(make_response(false, + "unknown command: " + (cmd.empty() ? "(empty)" : cmd))); + } + } + + std::cout << "[SAFE] Client gone -- zeroing motors." << std::endl; + zero_motors(motors); + } + + // ----------------------------------------------------------------------- + // Cleanup + // Thruster destructors free GPIO pins automatically when motor_storage + // goes out of scope; no manual freePin() calls needed. + // ----------------------------------------------------------------------- + std::cout << "[SHUTDOWN] Stopping motors." << std::endl; + for (auto* m : motors) m->setSpeed(STOP_SPEED); + + // motor_storage destructors run here, freeing all pins +#ifndef BUILD_SIMULATION + lgGpiochipClose(handle); +#endif + return 0; +} diff --git a/src/tests/thruster.cc b/src/tests/thruster.cc index ec4e921..f4a195e 100644 --- a/src/tests/thruster.cc +++ b/src/tests/thruster.cc @@ -7,7 +7,7 @@ TEST(Thruster, DutyCycle){ thruster->cycle = 50; thruster->speed = 0.0; EXPECT_EQ(7.5, thruster->getDutyCycle()); - + } TEST(Thruster, UnarmedFailure){ From a9384c3d7f7a39a80b8fa46f966524755738f942 Mon Sep 17 00:00:00 2001 From: WilyHyperion <123993989+WilyHyperion@users.noreply.github.com> Date: Wed, 6 May 2026 18:58:10 -0700 Subject: [PATCH 05/10] Support for both simulated and hardware GPS drivers, based on the gps object (#13) * Working physical + sim gps * run formater * Run correct linter and add targets * change sim to share a common connection --------- Co-authored-by: Copilot --- .devcontainer/devcontainer.json | 10 + Docker/Dockerfile | 13 +- src/CMakeLists.txt | 57 +++- src/comms/tcp_server.hpp | 179 +++++----- src/drivers/CMakeLists.txt | 12 + src/drivers/common/sim.hpp | 8 + src/drivers/gps/gps.hpp | 16 + src/drivers/gps/hardware_gps.hpp | 82 +++++ src/drivers/gps/simulated_gps.hpp | 31 ++ src/drivers/motors/motor.cpp | 22 +- src/drivers/motors/motor.hpp | 54 +-- src/drivers/motors/noah_test.cpp | 39 +-- src/drivers/motors/sim_motor.cpp | 110 +++--- src/drivers/motors/sim_motor.hpp | 34 +- src/drivers/motors/thruster.cpp | 80 +++-- src/drivers/motors/thruster.hpp | 53 +-- src/drivers/server/motor_server.cpp | 508 +++++++++++++++------------- src/main.cpp | 21 +- src/sim_main.cpp | 10 + src/tests/thruster.cc | 17 +- src/types/CMakeLists.txt | 0 src/types/GPSCoordinate.hpp | 13 + 22 files changed, 818 insertions(+), 551 deletions(-) create mode 100644 src/drivers/CMakeLists.txt create mode 100644 src/drivers/common/sim.hpp create mode 100644 src/drivers/gps/gps.hpp create mode 100644 src/drivers/gps/hardware_gps.hpp create mode 100644 src/drivers/gps/simulated_gps.hpp create mode 100644 src/sim_main.cpp create mode 100644 src/types/CMakeLists.txt create mode 100644 src/types/GPSCoordinate.hpp diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 37e97b5..80273e7 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,5 +2,15 @@ "name": "neptune", "build": { "dockerfile": "../Docker/Dockerfile" + }, + + "runArgs": ["--privileged", "--add-host=host.docker.internal:host-gateway" ], + "customizations": { + "vscode": { + "extensions": [ + "ms-vscode.cpptools-extension-pack" + ] } +}, +"onCreateCommand": "pipx run pre-commit install" } diff --git a/Docker/Dockerfile b/Docker/Dockerfile index 5fd71c1..d42c19a 100644 --- a/Docker/Dockerfile +++ b/Docker/Dockerfile @@ -2,12 +2,19 @@ FROM ubuntu:latest RUN apt-get update -y -# install cmake and ninja +# install dev dependcies RUN apt-get install -y cmake build-essential RUN apt-get install -y ninja-build -RUN apt-get install -y git gdb curl +RUN apt-get update -y +RUN apt-get install -y git gdb curl clang-format #Get project dependencies -RUN apt-get install -y gpiod libgpiod-dev liblgpio-dev +RUN apt-get install -y gpiod libgpiod-dev liblgpio-dev libserial-dev gpsd libgps-dev +#dev dependencies +RUN apt-get install -y python3 python3-pip pipx +WORKDIR /workspace/Neptune +RUN pipx install pre-commit WORKDIR /usr/local/include +#install httplib and serial lib TODO - this is unstable, we should choose a specfic version to fix RUN curl -LO https://github.com/yhirose/cpp-httplib/raw/refs/tags/latest/httplib.h > httplib.h +RUN curl -LO https://github.com/CLIUtils/CLI11/releases/download/v2.6.2/CLI11.hpp > CLI11.hpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 74298b1..adc8c5f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -5,11 +5,13 @@ project(neptune C CXX) if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Release) endif() +find_package(Threads REQUIRED) #required for pthread_setaffinity_np / CPU_SET (GNU extensions) add_compile_definitions(_GNU_SOURCE) add_executable(neptune main.cpp) +add_executable(neptune_sim sim_main.cpp) include(FetchContent) FetchContent_Declare( googletest @@ -47,19 +49,29 @@ else() set(LGPIO_LINK_LIBS "") endif() -#create motor library (just header file) -add_library(motor INTERFACE) -target_include_directories(motor INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/drivers) -#add lgpio include directory to motor target -if(NOT BUILD_SIMULATION) - target_include_directories(motor INTERFACE ${LGPIO_INCLUDE_DIR}) - target_link_libraries(motor INTERFACE ${LGPIO_LINK_LIBS}) -endif() -#need pthreads for RT thread -find_package(Threads REQUIRED) +link_directories(drivers) +add_subdirectory(drivers) +add_subdirectory(types) + + +target_link_libraries(motors ${LGPIO_LINK_LIBS}) + +#main target +target_include_directories(motors PRIVATE ${LGPIO_INCLUDE_DIR}) + +target_link_libraries(neptune_sim motors) +target_link_libraries(neptune motors) + +target_link_libraries(neptune_sim gps_drivers) +target_link_libraries(neptune gps_drivers) + + +# Everything below here is not part of the main target + +#Testing enable_testing() add_executable( @@ -67,9 +79,9 @@ add_executable( tests/thruster.cc ) -target_link_libraries( - thruster_test +target_link_libraries(thruster_test GTest::gtest_main + motors ) include(GoogleTest) @@ -104,6 +116,7 @@ target_include_directories(motor_server PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/drivers ) + target_link_libraries(motor_server PRIVATE Threads::Threads) if(NOT BUILD_SIMULATION) @@ -113,3 +126,23 @@ else() target_link_libraries(motor_server PRIVATE sim_motor) target_compile_definitions(motor_server PRIVATE BUILD_SIMULATION) endif() + + +#linter +find_program(PIPX NAMES "pipx") +if(PIPX) + add_custom_target( + lint + COMMAND ${PIPX} run pre-commit run + + COMMENT "Runing precommit checks" + ) + add_custom_target( + lint_all + COMMAND ${PIPX} run pre-commit run --all-files + + COMMENT "Runing precommit checks" + ) +else() +message(WARNING "Failed to find pipx for precommit checks") +endif() diff --git a/src/comms/tcp_server.hpp b/src/comms/tcp_server.hpp index 6b54345..4b495ad 100644 --- a/src/comms/tcp_server.hpp +++ b/src/comms/tcp_server.hpp @@ -12,8 +12,9 @@ * * For HolyBro Telemetry Radio: ensure that radios are connected to Pi and Base Station - on Pi: sudo pppd /dev/ttyUSB0 57600 lock local noauth 192.168.100.11:192.168.100.10 - on base: sudo pppd /dev/ttyUSB0 57600 lock local noauth nodetach 192.168.100.10:192.168.100.11 + on Pi: sudo pppd /dev/ttyUSB0 57600 lock local noauth + 192.168.100.11:192.168.100.10 on base: sudo pppd /dev/ttyUSB0 57600 lock local + noauth nodetach 192.168.100.10:192.168.100.11 IPs should be ordered relative to the device you are running the command on */ @@ -31,93 +32,105 @@ class TcpServer { public: - explicit TcpServer(int port) : port_(port), server_fd_(-1), client_fd_(-1) {} - - ~TcpServer() { close_all(); } - - /** - * Bind and start listening. Call once before the main loop. - */ - void start() { - server_fd_ = socket(AF_INET, SOCK_STREAM, 0); - if (server_fd_ < 0) throw std::runtime_error("socket() failed: " + err()); - - // Allow rapid restart without "Address already in use" - int opt = 1; - setsockopt(server_fd_, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); - - sockaddr_in addr{}; - addr.sin_family = AF_INET; - addr.sin_addr.s_addr = INADDR_ANY; - addr.sin_port = htons(static_cast(port_)); - - if (bind(server_fd_, reinterpret_cast(&addr), sizeof(addr)) < 0) - throw std::runtime_error("bind() failed: " + err()); - - if (listen(server_fd_, 1) < 0) - throw std::runtime_error("listen() failed: " + err()); - - std::cout << "[TCP] Listening on port " << port_ << std::endl; + explicit TcpServer(int port) : port_(port), server_fd_(-1), client_fd_(-1) {} + + ~TcpServer() { close_all(); } + + /** + * Bind and start listening. Call once before the main loop. + */ + void start() { + server_fd_ = socket(AF_INET, SOCK_STREAM, 0); + if (server_fd_ < 0) + throw std::runtime_error("socket() failed: " + err()); + + // Allow rapid restart without "Address already in use" + int opt = 1; + setsockopt(server_fd_, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = INADDR_ANY; + addr.sin_port = htons(static_cast(port_)); + + if (bind(server_fd_, reinterpret_cast(&addr), sizeof(addr)) < 0) + throw std::runtime_error("bind() failed: " + err()); + + if (listen(server_fd_, 1) < 0) + throw std::runtime_error("listen() failed: " + err()); + + std::cout << "[TCP] Listening on port " << port_ << std::endl; + } + + /** + * Block until a client connects. Closes any existing client first. + */ + void accept_client() { + if (client_fd_ >= 0) { + close(client_fd_); + client_fd_ = -1; } - - /** - * Block until a client connects. Closes any existing client first. - */ - void accept_client() { - if (client_fd_ >= 0) { - close(client_fd_); - client_fd_ = -1; - } - sockaddr_in client_addr{}; - socklen_t len = sizeof(client_addr); - std::cout << "[TCP] Waiting for connection..." << std::endl; - client_fd_ = accept(server_fd_, reinterpret_cast(&client_addr), &len); - if (client_fd_ < 0) throw std::runtime_error("accept() failed: " + err()); - - char ip[INET_ADDRSTRLEN]; - inet_ntop(AF_INET, &client_addr.sin_addr, ip, sizeof(ip)); - std::cout << "[TCP] Client connected from " << ip << std::endl; - } - - /** - * Read one newline-terminated line from the current client. - * Returns false if the client disconnected (caller should call accept_client again). - */ - bool recv_line(std::string& out) { - out.clear(); - char ch; - while (true) { - ssize_t n = recv(client_fd_, &ch, 1, 0); - if (n <= 0) { - std::cout << "[TCP] Client disconnected." << std::endl; - return false; - } - if (ch == '\n') return true; - if (ch != '\r') out += ch; - } + sockaddr_in client_addr{}; + socklen_t len = sizeof(client_addr); + std::cout << "[TCP] Waiting for connection..." << std::endl; + client_fd_ = + accept(server_fd_, reinterpret_cast(&client_addr), &len); + if (client_fd_ < 0) + throw std::runtime_error("accept() failed: " + err()); + + char ip[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, &client_addr.sin_addr, ip, sizeof(ip)); + std::cout << "[TCP] Client connected from " << ip << std::endl; + } + + /** + * Read one newline-terminated line from the current client. + * Returns false if the client disconnected (caller should call accept_client + * again). + */ + bool recv_line(std::string &out) { + out.clear(); + char ch; + while (true) { + ssize_t n = recv(client_fd_, &ch, 1, 0); + if (n <= 0) { + std::cout << "[TCP] Client disconnected." << std::endl; + return false; + } + if (ch == '\n') + return true; + if (ch != '\r') + out += ch; } + } - /** - * Send a newline-terminated string to the current client. - * Returns false if the send failed. - */ - bool send_line(const std::string& msg) { - std::string out = msg + "\n"; - ssize_t n = send(client_fd_, out.c_str(), out.size(), MSG_NOSIGNAL); - return n == static_cast(out.size()); - } + /** + * Send a newline-terminated string to the current client. + * Returns false if the send failed. + */ + bool send_line(const std::string &msg) { + std::string out = msg + "\n"; + ssize_t n = send(client_fd_, out.c_str(), out.size(), MSG_NOSIGNAL); + return n == static_cast(out.size()); + } - bool has_client() const { return client_fd_ >= 0; } + bool has_client() const { return client_fd_ >= 0; } private: - int port_; - int server_fd_; - int client_fd_; - - void close_all() { - if (client_fd_ >= 0) { close(client_fd_); client_fd_ = -1; } - if (server_fd_ >= 0) { close(server_fd_); server_fd_ = -1; } + int port_; + int server_fd_; + int client_fd_; + + void close_all() { + if (client_fd_ >= 0) { + close(client_fd_); + client_fd_ = -1; + } + if (server_fd_ >= 0) { + close(server_fd_); + server_fd_ = -1; } + } - static std::string err() { return std::string(strerror(errno)); } + static std::string err() { return std::string(strerror(errno)); } }; diff --git a/src/drivers/CMakeLists.txt b/src/drivers/CMakeLists.txt new file mode 100644 index 0000000..a10c00a --- /dev/null +++ b/src/drivers/CMakeLists.txt @@ -0,0 +1,12 @@ + +add_library(motors motors/motor.cpp motors/sim_motor.cpp motors/thruster.cpp) + + +find_package(PkgConfig REQUIRED) +pkg_check_modules(GPSD REQUIRED libgps) +add_library(gps_drivers INTERFACE) +target_include_directories(gps_drivers INTERFACE ${GPSD_INCLUDE_DIRS}) + +target_include_directories(motors PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/motors) +target_include_directories(gps_drivers INTERFACE d ${CMAKE_CURRENT_SOURCE_DIR}/gps) +target_link_libraries(gps_drivers INTERFACE ${GPSD_LIBRARIES}) diff --git a/src/drivers/common/sim.hpp b/src/drivers/common/sim.hpp new file mode 100644 index 0000000..6c369c1 --- /dev/null +++ b/src/drivers/common/sim.hpp @@ -0,0 +1,8 @@ +#pragma once +#include + +#include + + +const std::string SIM_ADDR = "http://host.docker.internal:6767"; +inline httplib::Client *sim_connection = nullptr; diff --git a/src/drivers/gps/gps.hpp b/src/drivers/gps/gps.hpp new file mode 100644 index 0000000..d4adbf9 --- /dev/null +++ b/src/drivers/gps/gps.hpp @@ -0,0 +1,16 @@ +#pragma once +#include "../../types/GPSCoordinate.hpp" +#include +/** + */ +class GPS { +public: + virtual bool await_lock(int interval, int tries) { + std::cout << "Dont call this" << std::endl; + return false; + } + virtual GPSCoordinate *location() { + std::cout << "Dont call this" << std::endl; + return nullptr; + } +}; diff --git a/src/drivers/gps/hardware_gps.hpp b/src/drivers/gps/hardware_gps.hpp new file mode 100644 index 0000000..be98049 --- /dev/null +++ b/src/drivers/gps/hardware_gps.hpp @@ -0,0 +1,82 @@ +#include "gps.hpp" + +#include "../../types/GPSCoordinate.hpp" +#include +#include +#include +/** + * Note - its likely that only 1 gps can function at a time + */ +class HardwareGPS : public GPS { +public: + ~HardwareGPS() { delete this->gps_rec; } + GPSCoordinate *location() override { + while (true) { + struct gps_data_t *data; + + if (!(this->gps_rec->waiting(10000))) { + continue; + } + + if ((data = this->gps_rec->read()) == NULL) { + std::cerr << "Read error." << std::endl; + // TODO - what to do here? + return nullptr; + } else { + if (data->fix.mode >= MODE_2D) { + // Connection gotten + GPSCoordinate *out = new GPSCoordinate(); + out->latitude = data->fix.latitude; + out->longitude = data->fix.longitude; + + return out; + } else { + std::cout << "No gps fix" << std::endl; + } + } + } + } + HardwareGPS(std::string location, const char *gpsd_port = DEFAULT_GPSD_PORT) { + std::string out = "gpsd " + location; + int val = system(out.c_str()); + if (val != 0) { + throw new std::runtime_error("GPS failed to connect - gpsd didn't start"); + } + std::cout << "GPS loaded" << std::endl; + this->gps_rec = new gpsmm("localhost", gpsd_port); + } + bool await_lock(int interval, int tries) override { + if (this->gps_rec->stream(WATCH_ENABLE | WATCH_JSON) == NULL) { + std::cerr << "No GPSD running." << std::endl; + return 1; + } + int attempt = 0; + while (true) { + struct gps_data_t *data; + + if (!(this->gps_rec->waiting(interval * 10000))) { + continue; + } + + if ((data = this->gps_rec->read()) == NULL) { + std::cerr << "Read error." << std::endl; + // TODO - what to do here? + return false; + } else { + if (data->fix.mode >= MODE_2D) { + // Connection gotten + return true; + } else { + if (++attempt > tries) { + return false; + } + std::cout << "No gps fix" << std::endl; + } + } + } + return true; + } + +private: + gpsmm *gps_rec; +}; diff --git a/src/drivers/gps/simulated_gps.hpp b/src/drivers/gps/simulated_gps.hpp new file mode 100644 index 0000000..7464bfd --- /dev/null +++ b/src/drivers/gps/simulated_gps.hpp @@ -0,0 +1,31 @@ +#pragma once +#include "../../types/GPSCoordinate.hpp" +#include "gps.hpp" +#include +#include "../common/sim.hpp" + +class Simulated_GPS : public GPS { +public: + + Simulated_GPS() { + if (sim_connection == 0) { + sim_connection = new httplib::Client(SIM_ADDR); + httplib::Result out = sim_connection->Get("/heartbeat"); + if (out == nullptr) { + throw std::runtime_error("No simulation connection found"); + } + std::cout << "Status code from sim:" << out->status << std::endl; + } + } + bool await_lock(int interval, int tries) override { return true; } + GPSCoordinate *location() override { + auto out = sim_connection->Get("/gps"); + if (out == nullptr) { + std::cout << "Error getting GPS sim info" << std::endl; + return nullptr; + } + GPSCoordinate *s = new GPSCoordinate(); + sscanf(out->body.c_str(), "[%lf,%lf,", &s->latitude, &s->longitude); + return s; + } +}; diff --git a/src/drivers/motors/motor.cpp b/src/drivers/motors/motor.cpp index a9e25db..62222eb 100644 --- a/src/drivers/motors/motor.cpp +++ b/src/drivers/motors/motor.cpp @@ -1,20 +1,16 @@ #include "motor.hpp" #include - -//All of these should probably not be here or throw execptions but I need to put them here for the linker -int Motor::setSpeed(float speed) -{ - std::cout << "Dont call me" << std::endl; - return 0; +// All of these should probably not be here or throw execptions but I need to +// put them here for the linker +int Motor::setSpeed(float speed) { + std::cout << "Dont call me" << std::endl; + return 0; } -int Motor::setFrequency(float cycle) -{ - std::cout << "Dont call me" << std::endl; - return 0; +int Motor::setFrequency(float cycle) { + std::cout << "Dont call me" << std::endl; + return 0; } -void Motor::armMotor() -{ -} +void Motor::armMotor() {} diff --git a/src/drivers/motors/motor.hpp b/src/drivers/motors/motor.hpp index da8bcc0..a82f76c 100644 --- a/src/drivers/motors/motor.hpp +++ b/src/drivers/motors/motor.hpp @@ -3,30 +3,36 @@ * @brief The abstract class that represents a motor on the robot * */ -class Motor -{ +class Motor { public: - /** - * @brief sets the motor to run at speed until this or another method is called - * @param speed A value between -1.0 and 1.0, with -1.0 being max reverse thrust, 0.0 being still, and 1.0 being max forward thrust - * @return The return code. Negative indicates errors, otherwise the meaning depends on the impelmenting class - */ - virtual int setSpeed(float speed); - /** - * @brief Sets the frequency of the motor pwm, ie how quick cycles are sent to the ESC - * @param cycle The value, in hz, for the new cycle - * @return The return code. Negative indicates errors, otherwise the meaning depends on the impelmenting class - */ - virtual int setFrequency(float cycle); + /** + * @brief sets the motor to run at speed until this or another method is + * called + * @param speed A value between -1.0 and 1.0, with -1.0 being max reverse + * thrust, 0.0 being still, and 1.0 being max forward thrust + * @return The return code. Negative indicates errors, otherwise the meaning + * depends on the impelmenting class + */ + virtual int setSpeed(float speed); + /** + * @brief Sets the frequency of the motor pwm, ie how quick cycles are sent to + * the ESC + * @param cycle The value, in hz, for the new cycle + * @return The return code. Negative indicates errors, otherwise the meaning + * depends on the impelmenting class + */ + virtual int setFrequency(float cycle); - /** - * @brief arms the motor. Likely a blocking method for a few seconds - */ - virtual void armMotor(); - float speed; - float cycle; - protected: - Motor(int speed,int cycle) : speed(speed), cycle(cycle){}; - protected: - Motor(){}; + /** + * @brief arms the motor. Likely a blocking method for a few seconds + */ + virtual void armMotor(); + float speed; + float cycle; + +protected: + Motor(int speed, int cycle) : speed(speed), cycle(cycle) {}; + +protected: + Motor() {}; }; diff --git a/src/drivers/motors/noah_test.cpp b/src/drivers/motors/noah_test.cpp index 34745f3..ef223e6 100644 --- a/src/drivers/motors/noah_test.cpp +++ b/src/drivers/motors/noah_test.cpp @@ -3,23 +3,24 @@ #include #include #include -int main(){ - srand(time(0)); - Motor* m = new SimulatedMotor(SimulatedMotor::BackRight); - Motor* m1 = new SimulatedMotor(SimulatedMotor::FrontRight); - Motor* m2 = new SimulatedMotor(SimulatedMotor::FrontLeft); - Motor* m3 = new SimulatedMotor(SimulatedMotor::BackLeft); - m->armMotor(); - m1->armMotor(); - m2->armMotor(); - m3->armMotor(); - std::random_device rd; - std::mt19937 gen(rd()); - std::uniform_real_distribution dis(-1.0, 1.0); - while (true) - { - if(rand() % 2) m->setSpeed(dis(gen)); - if(rand() % 2) m1->setSpeed(dis(gen)); - sleep(3); - } +int main() { + srand(time(0)); + Motor *m = new SimulatedMotor(SimulatedMotor::BackRight); + Motor *m1 = new SimulatedMotor(SimulatedMotor::FrontRight); + Motor *m2 = new SimulatedMotor(SimulatedMotor::FrontLeft); + Motor *m3 = new SimulatedMotor(SimulatedMotor::BackLeft); + m->armMotor(); + m1->armMotor(); + m2->armMotor(); + m3->armMotor(); + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_real_distribution dis(-1.0, 1.0); + while (true) { + if (rand() % 2) + m->setSpeed(dis(gen)); + if (rand() % 2) + m1->setSpeed(dis(gen)); + sleep(3); + } } diff --git a/src/drivers/motors/sim_motor.cpp b/src/drivers/motors/sim_motor.cpp index 64de212..d79a32d 100644 --- a/src/drivers/motors/sim_motor.cpp +++ b/src/drivers/motors/sim_motor.cpp @@ -1,70 +1,64 @@ #include "sim_motor.hpp" #include -httplib::Client* SimulatedMotor::sim_connection = nullptr; -std::string SimulatedMotor::GetMotorPath(){ - switch (this->ml) - { - case SimulatedMotor::FrontLeft: - return "/motor/top_left/set"; - break; - case SimulatedMotor::FrontRight: - return "/motor/top_right/set"; - break; - case SimulatedMotor::BackLeft: - return "/motor/bottom_left/set"; - break; - case SimulatedMotor::BackRight: - return "/motor/bottom_right/set"; - break; - default: - //Error? - break; - } - return ""; +#include "../common/sim.hpp" +std::string SimulatedMotor::GetMotorPath() { + switch (this->ml) { + case SimulatedMotor::FrontLeft: + return "/motor/top_left/set"; + break; + case SimulatedMotor::FrontRight: + return "/motor/top_right/set"; + break; + case SimulatedMotor::BackLeft: + return "/motor/bottom_left/set"; + break; + case SimulatedMotor::BackRight: + return "/motor/bottom_right/set"; + break; + default: + // Error? + break; + } + return ""; } -SimulatedMotor::SimulatedMotor(SimulatedMotor::MotorLocation ml, int speed, float cycle) : Motor(speed, cycle) -{ - if (SimulatedMotor::sim_connection == 0) - { - SimulatedMotor::sim_connection = new httplib::Client(SIM_ADDR); - httplib::Result out = SimulatedMotor::sim_connection->Get("/heartbeat"); - if(out == nullptr){ - throw std::runtime_error("No simulation connection found"); - } - std::cout << "Status code from sim:" << out->status << std::endl; +SimulatedMotor::SimulatedMotor(SimulatedMotor::MotorLocation ml, int speed, + float cycle) + : Motor(speed, cycle) { + if (sim_connection == 0) { + sim_connection = new httplib::Client(SIM_ADDR); + httplib::Result out = sim_connection->Get("/heartbeat"); + if (out == nullptr) { + throw std::runtime_error("No simulation connection found"); } - this->ml = ml; + std::cout << "Status code from sim:" << out->status << std::endl; + } + this->ml = ml; } -int SimulatedMotor::setSpeed(float speed) -{ - if(!this->armed){ - throw std::logic_error("Tried to set the speed of an unarmed motor"); - } - this->speed = speed; - // In the long run we should add a json lib - std::ostringstream oss; - oss << "{\"speed\":" << speed << "}"; - std::string s = oss.str(); - auto out = SimulatedMotor::sim_connection->Post(this->GetMotorPath().c_str(), s.c_str(),"application/json"); - if(out == nullptr){ - std::cout << "Error sending sim info" << std::endl; - } - else { - std::cout << out->status << std::endl; - } +int SimulatedMotor::setSpeed(float speed) { + if (!this->armed) { + throw std::logic_error("Tried to set the speed of an unarmed motor"); + } + this->speed = speed; + // In the long run we should add a json lib + std::ostringstream oss; + oss << "{\"speed\":" << speed << "}"; + std::string s = oss.str(); + auto out = sim_connection->Post( + this->GetMotorPath().c_str(), s.c_str(), "application/json"); + if (out == nullptr) { + std::cout << "Error sending sim info" << std::endl; + } else { + std::cout << out->status << std::endl; + } - return 0; + return 0; } -int SimulatedMotor::setFrequency(float cycle) -{ - // idk if this will be called at all - return 0; +int SimulatedMotor::setFrequency(float cycle) { + // idk if this will be called at all + return 0; } -void SimulatedMotor::armMotor() -{ - this->armed = true; -} +void SimulatedMotor::armMotor() { this->armed = true; } diff --git a/src/drivers/motors/sim_motor.hpp b/src/drivers/motors/sim_motor.hpp index 24cdccf..dfd05e4 100644 --- a/src/drivers/motors/sim_motor.hpp +++ b/src/drivers/motors/sim_motor.hpp @@ -1,30 +1,22 @@ #pragma once #include "motor.hpp" -#include #include -class SimulatedMotor : public Motor -{ +class SimulatedMotor : public Motor { - public: - //At some point we should prob use this datatype everywhere, for now its here - enum MotorLocation { - FrontRight, - FrontLeft, - BackRight, - BackLeft - }; - MotorLocation ml; - bool armed; - SimulatedMotor(SimulatedMotor::MotorLocation ml, int speed = 0.0, float cycle = 50); - int setSpeed(float speed) override; +public: + // At some point we should prob use this datatype everywhere, for now its here + enum MotorLocation { FrontRight, FrontLeft, BackRight, BackLeft }; + MotorLocation ml; + bool armed; + SimulatedMotor(SimulatedMotor::MotorLocation ml, int speed = 0.0, + float cycle = 50); + int setSpeed(float speed) override; - int setFrequency(float cycle) override; + int setFrequency(float cycle) override; - void armMotor() override; - private: + void armMotor() override; - std::string GetMotorPath(); - const std::string SIM_ADDR = "http://localhost:6767"; - static httplib::Client* sim_connection; +private: + std::string GetMotorPath(); }; diff --git a/src/drivers/motors/thruster.cpp b/src/drivers/motors/thruster.cpp index 753d66e..1143043 100644 --- a/src/drivers/motors/thruster.cpp +++ b/src/drivers/motors/thruster.cpp @@ -1,57 +1,55 @@ #include "thruster.hpp" +#include #include -#include #include -#include - +#include -Thruster::Thruster(int pin, int handle, int speed /*= 0*/, int cycle /*= 50*/) : pin(pin) , handle(handle), Motor(speed, cycle) { - if(lgGpioClaimOutput(handle, 0, pin, 0) !=LG_OKAY ){ - throw new std::runtime_error("Failed to claim a pin for a motor"); - } +Thruster::Thruster(int pin, int handle, int speed /*= 0*/, int cycle /*= 50*/) + : pin(pin), handle(handle), Motor(speed, cycle) { + if (lgGpioClaimOutput(handle, 0, pin, 0) != LG_OKAY) { + throw new std::runtime_error("Failed to claim a pin for a motor"); + } } -Thruster::~Thruster(){ - if(lgGpioFree(handle, pin) <0) { - std::cerr << "Error code freeing pin " << pin << std::endl; - } +Thruster::~Thruster() { + if (lgGpioFree(handle, pin) < 0) { + std::cerr << "Error code freeing pin " << pin << std::endl; + } } -float Thruster::getDutyCycle() -{ - // Convert to nanoseconds - float cycle_width = 1 / (this->cycle * (1e-6)) ; - float goal_interval = ((MAX_PWM_DIST * this->speed) + STOP_PWM); - return (goal_interval / cycle_width) * 100; +float Thruster::getDutyCycle() { + // Convert to nanoseconds + float cycle_width = 1 / (this->cycle * (1e-6)); + float goal_interval = ((MAX_PWM_DIST * this->speed) + STOP_PWM); + return (goal_interval / cycle_width) * 100; } -int Thruster::sendMotorPWM() -{ - if(!this->armed){ - throw new std::logic_error("Tried to affect motor before arming. Always ensure motors are armed before activating them"); - } - return lgTxPwm(this->handle, this->pin, this->cycle, getDutyCycle(), 0, 0); - +int Thruster::sendMotorPWM() { + if (!this->armed) { + throw new std::logic_error( + "Tried to affect motor before arming. Always ensure motors are armed " + "before activating them"); + } + return lgTxPwm(this->handle, this->pin, this->cycle, getDutyCycle(), 0, 0); } int Thruster::setSpeed(float newSpeed) { - this->speed = newSpeed; - if(newSpeed < -1.0 || newSpeed > 1.0){ - //TODO clamp here instead ? - throw new std::logic_error("Motor Speed out of range"); - } - return sendMotorPWM(); + this->speed = newSpeed; + if (newSpeed < -1.0 || newSpeed > 1.0) { + // TODO clamp here instead ? + throw new std::logic_error("Motor Speed out of range"); + } + return sendMotorPWM(); } -int Thruster::setFrequency(float cycle) -{ - this->cycle = cycle; - return sendMotorPWM(); +int Thruster::setFrequency(float cycle) { + this->cycle = cycle; + return sendMotorPWM(); } -void Thruster::armMotor(){ - this->speed = 0.0; - this->armed = true; - if(this->sendMotorPWM() < 0){ - //Maybe error here? - } - lguSleep(ARM_SECONDS); +void Thruster::armMotor() { + this->speed = 0.0; + this->armed = true; + if (this->sendMotorPWM() < 0) { + // Maybe error here? + } + lguSleep(ARM_SECONDS); } diff --git a/src/drivers/motors/thruster.hpp b/src/drivers/motors/thruster.hpp index 6810a8e..1fa2f1f 100644 --- a/src/drivers/motors/thruster.hpp +++ b/src/drivers/motors/thruster.hpp @@ -4,37 +4,38 @@ /** * @brief Represents one of the blue robotics thrusters on the robot */ -class Thruster : public Motor -{ +class Thruster : public Motor { public: - int pin; - int handle; - bool armed; + int pin; + int handle; + bool armed; - Thruster(int pin, int handle, int speed = 0, int cycle = 50); - ~Thruster(); - int setSpeed(float speed) override; + Thruster(int pin, int handle, int speed = 0, int cycle = 50); + ~Thruster(); + int setSpeed(float speed) override; - int setFrequency(float cycle) override; - - void armMotor() override; - /** - * @brief gets the duty cycle to send to the motor in order to move the motor at the current speed - */ - float getDutyCycle(); - protected: - Thruster() {}; -private: - const int STOP_PWM = 1500; - const int MAX_PWM_DIST = 400; - const int ARM_SECONDS = 3; //I dont think this actually has to be 7 seconds, like 3 works but better safe than sorry + int setFrequency(float cycle) override; + void armMotor() override; + /** + * @brief gets the duty cycle to send to the motor in order to move the motor + * at the current speed + */ + float getDutyCycle(); - /** - * @brief updates the pwm values being sent. Must be called after any updates to class variables that will affect this value - * @return the return value of the lgTxPwm call - */ - int sendMotorPWM(); +protected: + explicit Thruster() {}; +private: + const int STOP_PWM = 1500; + const int MAX_PWM_DIST = 400; + const int ARM_SECONDS = 3; // I dont think this actually has to be 7 seconds, + // like 3 works but better safe than sorry + /** + * @brief updates the pwm values being sent. Must be called after any updates + * to class variables that will affect this value + * @return the return value of the lgTxPwm call + */ + int sendMotorPWM(); }; diff --git a/src/drivers/server/motor_server.cpp b/src/drivers/server/motor_server.cpp index facbe80..f1ad075 100644 --- a/src/drivers/server/motor_server.cpp +++ b/src/drivers/server/motor_server.cpp @@ -5,9 +5,11 @@ * Uses the Thruster class (real hardware) or SimulatedMotor (BUILD_SIMULATION). * * Motor API: - * - Thruster(int pin, int handle) -- constructor claims pin; destructor frees it + * - Thruster(int pin, int handle) -- constructor claims pin; destructor + * frees it * - motor.armMotor() -- blocks ~3 s, sets neutral - * - motor.setSpeed(float speed) -- -1.0 (full reverse) .. 0.0 (stop) .. 1.0 (full forward) + * - motor.setSpeed(float speed) -- -1.0 (full reverse) .. 0.0 (stop) + * .. 1.0 (full forward) * * ## Protocol (newline-terminated JSON) * @@ -36,137 +38,152 @@ * ./build/motor_server */ +#include #include #include -#include -#include -#include #include +#include #include +#include #ifndef BUILD_SIMULATION - #include - #include "../motors/thruster.hpp" +#include "../motors/thruster.hpp" +#include #else - #include "../motors/sim_motor.hpp" +#include "../motors/sim_motor.hpp" #endif #include "../../comms/tcp_server.hpp" // --------------------------------------------------------------------------- // Constants -- mirrors Thruster internals for the "pwm" command converter // --------------------------------------------------------------------------- -static constexpr float STOP_SPEED = 0.0f; -static constexpr int NEUTRAL_PWM = 1500; -static constexpr int MIN_PWM = 1100; -static constexpr int MAX_PWM = 1900; -static constexpr int MAX_PWM_DIST = 400; // microseconds from neutral to limit +static constexpr float STOP_SPEED = 0.0f; +static constexpr int NEUTRAL_PWM = 1500; +static constexpr int MIN_PWM = 1100; +static constexpr int MAX_PWM = 1900; +static constexpr int MAX_PWM_DIST = 400; // microseconds from neutral to limit // --------------------------------------------------------------------------- // Motor GPIO pin assignments (matches pool_test.cpp / motor_controller.py) // --------------------------------------------------------------------------- static constexpr int NUM_MOTORS = 4; static constexpr int MOTOR_PINS[NUM_MOTORS] = { - 4, // forward (FORWARD_GPIO_PIN) - 11, // turn (TURN_GPIO_PIN) - 18, // front (FRONT_GPIO_PIN) - 24, // back (BACK_GPIO_PIN) + 4, // forward (FORWARD_GPIO_PIN) + 11, // turn (TURN_GPIO_PIN) + 18, // front (FRONT_GPIO_PIN) + 24, // back (BACK_GPIO_PIN) }; #ifdef BUILD_SIMULATION // Map each motor slot to a SimulatedMotor::MotorLocation for the HTTP sim API. // Mapping is approximate -- adjust once the sim model is finalised. static constexpr SimulatedMotor::MotorLocation MOTOR_LOCATIONS[NUM_MOTORS] = { - SimulatedMotor::FrontLeft, // motor 0 - forward - SimulatedMotor::FrontRight, // motor 1 - turn - SimulatedMotor::BackLeft, // motor 2 - front - SimulatedMotor::BackRight, // motor 3 - back + SimulatedMotor::FrontLeft, // motor 0 - forward + SimulatedMotor::FrontRight, // motor 1 - turn + SimulatedMotor::BackLeft, // motor 2 - front + SimulatedMotor::BackRight, // motor 3 - back }; #endif -// Current normalized speed for each motor (source of truth for "status" replies) -static std::array g_speeds = { - STOP_SPEED, STOP_SPEED, STOP_SPEED, STOP_SPEED -}; +// Current normalized speed for each motor (source of truth for "status" +// replies) +static std::array g_speeds = {STOP_SPEED, STOP_SPEED, + STOP_SPEED, STOP_SPEED}; // --------------------------------------------------------------------------- // Minimal JSON helpers (no external lib) // --------------------------------------------------------------------------- -static std::string make_response(bool ok, const std::string& msg) { - std::ostringstream ss; - ss << "{\"ok\":" << (ok ? "true" : "false") - << ",\"msg\":\"" << msg << "\"}"; - return ss.str(); +static std::string make_response(bool ok, const std::string &msg) { + std::ostringstream ss; + ss << "{\"ok\":" << (ok ? "true" : "false") << ",\"msg\":\"" << msg << "\"}"; + return ss.str(); } -static std::string make_status(const std::array& speeds) { - std::ostringstream ss; - ss << "{\"ok\":true,\"speeds\":[" - << speeds[0] << "," << speeds[1] << "," << speeds[2] << "," << speeds[3] - << "]}"; - return ss.str(); +static std::string make_status(const std::array &speeds) { + std::ostringstream ss; + ss << "{\"ok\":true,\"speeds\":[" << speeds[0] << "," << speeds[1] << "," + << speeds[2] << "," << speeds[3] << "]}"; + return ss.str(); } -static std::string parse_string(const std::string& json, const std::string& key) { - std::string search = "\"" + key + "\""; - size_t pos = json.find(search); - if (pos == std::string::npos) return ""; - pos = json.find(':', pos); - if (pos == std::string::npos) return ""; - pos = json.find('"', pos); - if (pos == std::string::npos) return ""; - size_t end = json.find('"', pos + 1); - if (end == std::string::npos) return ""; - return json.substr(pos + 1, end - pos - 1); +static std::string parse_string(const std::string &json, + const std::string &key) { + std::string search = "\"" + key + "\""; + size_t pos = json.find(search); + if (pos == std::string::npos) + return ""; + pos = json.find(':', pos); + if (pos == std::string::npos) + return ""; + pos = json.find('"', pos); + if (pos == std::string::npos) + return ""; + size_t end = json.find('"', pos + 1); + if (end == std::string::npos) + return ""; + return json.substr(pos + 1, end - pos - 1); } -static bool parse_int(const std::string& json, const std::string& key, int& out) { - std::string search = "\"" + key + "\""; - size_t pos = json.find(search); - if (pos == std::string::npos) return false; - pos = json.find(':', pos); - if (pos == std::string::npos) return false; +static bool parse_int(const std::string &json, const std::string &key, + int &out) { + std::string search = "\"" + key + "\""; + size_t pos = json.find(search); + if (pos == std::string::npos) + return false; + pos = json.find(':', pos); + if (pos == std::string::npos) + return false; + pos++; + while (pos < json.size() && (json[pos] == ' ' || json[pos] == '\t')) pos++; - while (pos < json.size() && (json[pos] == ' ' || json[pos] == '\t')) pos++; - try { - size_t consumed = 0; - out = std::stoi(json.substr(pos), &consumed); - return consumed > 0; - } catch (...) { return false; } + try { + size_t consumed = 0; + out = std::stoi(json.substr(pos), &consumed); + return consumed > 0; + } catch (...) { + return false; + } } -static bool parse_float_array4(const std::string& json, const std::string& key, - std::array& out) { - std::string search = "\"" + key + "\""; - size_t pos = json.find(search); - if (pos == std::string::npos) return false; - pos = json.find('[', pos); - if (pos == std::string::npos) return false; - pos++; - for (int i = 0; i < 4; i++) { - while (pos < json.size() && - (json[pos] == ' ' || json[pos] == '\t' || json[pos] == ',')) - pos++; - if (pos >= json.size() || json[pos] == ']') return false; - try { - size_t consumed = 0; - out[i] = std::stof(json.substr(pos), &consumed); - pos += consumed; - } catch (...) { return false; } +static bool parse_float_array4(const std::string &json, const std::string &key, + std::array &out) { + std::string search = "\"" + key + "\""; + size_t pos = json.find(search); + if (pos == std::string::npos) + return false; + pos = json.find('[', pos); + if (pos == std::string::npos) + return false; + pos++; + for (int i = 0; i < 4; i++) { + while (pos < json.size() && + (json[pos] == ' ' || json[pos] == '\t' || json[pos] == ',')) + pos++; + if (pos >= json.size() || json[pos] == ']') + return false; + try { + size_t consumed = 0; + out[i] = std::stof(json.substr(pos), &consumed); + pos += consumed; + } catch (...) { + return false; } - return true; + } + return true; } // Convert PWM microseconds (1100-1900) to normalized speed (-1.0 to 1.0). // Mirrors Thruster::getDutyCycle() in reverse. static float pwm_to_speed(int pwm) { - return static_cast(pwm - NEUTRAL_PWM) / static_cast(MAX_PWM_DIST); + return static_cast(pwm - NEUTRAL_PWM) / + static_cast(MAX_PWM_DIST); } -static void zero_motors(std::array& motors) { - for (int i = 0; i < NUM_MOTORS; i++) { - motors[i]->setSpeed(STOP_SPEED); - g_speeds[i] = STOP_SPEED; - } +static void zero_motors(std::array &motors) { + for (int i = 0; i < NUM_MOTORS; i++) { + motors[i]->setSpeed(STOP_SPEED); + g_speeds[i] = STOP_SPEED; + } } // --------------------------------------------------------------------------- @@ -178,186 +195,195 @@ static void handle_signal(int) { g_running = false; } // --------------------------------------------------------------------------- // Main // --------------------------------------------------------------------------- -int main(int argc, char* argv[]) { - int port = 9000; - if (argc >= 2) { - try { port = std::stoi(argv[1]); } - catch (...) { - std::cerr << "Usage: " << argv[0] << " [port]\n"; - return 1; - } +int main(int argc, char *argv[]) { + int port = 9000; + if (argc >= 2) { + try { + port = std::stoi(argv[1]); + } catch (...) { + std::cerr << "Usage: " << argv[0] << " [port]\n"; + return 1; } + } - std::signal(SIGINT, handle_signal); - std::signal(SIGTERM, handle_signal); - std::signal(SIGPIPE, SIG_IGN); + std::signal(SIGINT, handle_signal); + std::signal(SIGTERM, handle_signal); + std::signal(SIGPIPE, SIG_IGN); - // ----------------------------------------------------------------------- - // Open lgpio chip (real hardware only) - // ----------------------------------------------------------------------- + // ----------------------------------------------------------------------- + // Open lgpio chip (real hardware only) + // ----------------------------------------------------------------------- #ifndef BUILD_SIMULATION - int handle = lgGpiochipOpen(0); - if (handle < 0) { - std::cerr << "[INIT] lgGpiochipOpen failed: " << handle << std::endl; - return 1; - } - std::cout << "[INIT] GPIO chip opened." << std::endl; + int handle = lgGpiochipOpen(0); + if (handle < 0) { + std::cerr << "[INIT] lgGpiochipOpen failed: " << handle << std::endl; + return 1; + } + std::cout << "[INIT] GPIO chip opened." << std::endl; #else - std::cout << "[SIM] Simulation mode -- routing to Gazebo sim over HTTP." << std::endl; + std::cout << "[SIM] Simulation mode -- routing to Gazebo sim over HTTP." + << std::endl; #endif - // ----------------------------------------------------------------------- - // Construct motors - // - // Thruster: constructor claims the GPIO pin; destructor frees it. - // SimulatedMotor: constructor connects to the HTTP sim server. - // Both throw on failure, so we catch here and bail cleanly. - // ----------------------------------------------------------------------- - std::array, NUM_MOTORS> motor_storage; - - std::cout << "[INIT] Constructing motors..." << std::endl; - for (int i = 0; i < NUM_MOTORS; i++) { - try { + // ----------------------------------------------------------------------- + // Construct motors + // + // Thruster: constructor claims the GPIO pin; destructor frees it. + // SimulatedMotor: constructor connects to the HTTP sim server. + // Both throw on failure, so we catch here and bail cleanly. + // ----------------------------------------------------------------------- + std::array, NUM_MOTORS> motor_storage; + + std::cout << "[INIT] Constructing motors..." << std::endl; + for (int i = 0; i < NUM_MOTORS; i++) { + try { #ifndef BUILD_SIMULATION - motor_storage[i] = std::make_unique(MOTOR_PINS[i], handle); + motor_storage[i] = std::make_unique(MOTOR_PINS[i], handle); #else - motor_storage[i] = std::make_unique(MOTOR_LOCATIONS[i]); + motor_storage[i] = std::make_unique(MOTOR_LOCATIONS[i]); #endif - } catch (const std::exception& e) { - std::cerr << "[INIT] Failed to construct motor " << i << ": " - << e.what() << std::endl; + } catch (const std::exception &e) { + std::cerr << "[INIT] Failed to construct motor " << i << ": " << e.what() + << std::endl; #ifndef BUILD_SIMULATION - lgGpiochipClose(handle); + lgGpiochipClose(handle); #endif - return 1; - } + return 1; } - - // Raw pointer array for convenience in helper functions - std::array motors = { - motor_storage[0].get(), motor_storage[1].get(), - motor_storage[2].get(), motor_storage[3].get() - }; - - // ----------------------------------------------------------------------- - // Arm ESCs -- armMotor() sets neutral and blocks for the required delay - // ----------------------------------------------------------------------- - std::cout << "[INIT] Arming ESCs..." << std::endl; - for (auto* m : motors) m->armMotor(); - std::cout << "[INIT] Motors ready." << std::endl; - - // ----------------------------------------------------------------------- - // TCP server - // ----------------------------------------------------------------------- - TcpServer server(port); - try { - server.start(); - } catch (const std::exception& e) { - std::cerr << "[TCP] " << e.what() << std::endl; + } + + // Raw pointer array for convenience in helper functions + std::array motors = { + motor_storage[0].get(), motor_storage[1].get(), motor_storage[2].get(), + motor_storage[3].get()}; + + // ----------------------------------------------------------------------- + // Arm ESCs -- armMotor() sets neutral and blocks for the required delay + // ----------------------------------------------------------------------- + std::cout << "[INIT] Arming ESCs..." << std::endl; + for (auto *m : motors) + m->armMotor(); + std::cout << "[INIT] Motors ready." << std::endl; + + // ----------------------------------------------------------------------- + // TCP server + // ----------------------------------------------------------------------- + TcpServer server(port); + try { + server.start(); + } catch (const std::exception &e) { + std::cerr << "[TCP] " << e.what() << std::endl; #ifndef BUILD_SIMULATION - lgGpiochipClose(handle); + lgGpiochipClose(handle); #endif - return 1; + return 1; + } + + // ----------------------------------------------------------------------- + // Main accept loop + // ----------------------------------------------------------------------- + while (g_running) { + try { + server.accept_client(); + } catch (const std::exception &e) { + std::cerr << "[TCP] accept failed: " << e.what() << std::endl; + break; } - // ----------------------------------------------------------------------- - // Main accept loop - // ----------------------------------------------------------------------- - while (g_running) { - try { - server.accept_client(); - } catch (const std::exception& e) { - std::cerr << "[TCP] accept failed: " << e.what() << std::endl; - break; - } + server.send_line(make_response(true, "connected -- motor_server ready")); - server.send_line(make_response(true, "connected -- motor_server ready")); - - std::string line; - while (g_running && server.recv_line(line)) { - if (line.empty()) continue; - std::cout << "[CMD] " << line << std::endl; - - std::string cmd = parse_string(line, "command"); - - if (cmd == "stop") { - zero_motors(motors); - server.send_line(make_response(true, "all motors stopped")); - - } else if (cmd == "motors") { - std::array vals{}; - if (!parse_float_array4(line, "values", vals)) { - server.send_line(make_response(false, "bad values array")); - continue; - } - bool ok = true; - for (int i = 0; i < NUM_MOTORS; i++) { - // Clamp to [-1, 1] before passing to setSpeed - float speed = vals[i]; - if (speed > 1.0f) speed = 1.0f; - if (speed < -1.0f) speed = -1.0f; - - if (motors[i]->setSpeed(speed) < 0) { - server.send_line(make_response(false, - "motor " + std::to_string(i) + " setSpeed failed")); - ok = false; - break; - } - g_speeds[i] = speed; - } - if (ok) server.send_line(make_response(true, "motors updated")); - - } else if (cmd == "pwm") { - // Accept legacy PWM microseconds (1100-1900) and convert to speed - int motor_idx = -1, pwm_value = -1; - if (!parse_int(line, "motor", motor_idx) || - !parse_int(line, "value", pwm_value) || - motor_idx < 0 || motor_idx >= NUM_MOTORS || - pwm_value < MIN_PWM || pwm_value > MAX_PWM) { - server.send_line(make_response(false, - "bad args -- motor 0-3, value 1100-1900")); - continue; - } - float speed = pwm_to_speed(pwm_value); - if (motors[motor_idx]->setSpeed(speed) < 0) { - server.send_line(make_response(false, "setSpeed failed")); - continue; - } - g_speeds[motor_idx] = speed; - server.send_line(make_response(true, - "motor " + std::to_string(motor_idx) + - " -> " + std::to_string(pwm_value) + " us" - " (speed " + std::to_string(speed) + ")")); - - } else if (cmd == "status") { - server.send_line(make_status(g_speeds)); - - } else if (cmd == "quit") { - server.send_line(make_response(true, "shutting down")); - g_running = false; - break; - - } else { - server.send_line(make_response(false, - "unknown command: " + (cmd.empty() ? "(empty)" : cmd))); - } - } + std::string line; + while (g_running && server.recv_line(line)) { + if (line.empty()) + continue; + std::cout << "[CMD] " << line << std::endl; + + std::string cmd = parse_string(line, "command"); - std::cout << "[SAFE] Client gone -- zeroing motors." << std::endl; + if (cmd == "stop") { zero_motors(motors); + server.send_line(make_response(true, "all motors stopped")); + + } else if (cmd == "motors") { + std::array vals{}; + if (!parse_float_array4(line, "values", vals)) { + server.send_line(make_response(false, "bad values array")); + continue; + } + bool ok = true; + for (int i = 0; i < NUM_MOTORS; i++) { + // Clamp to [-1, 1] before passing to setSpeed + float speed = vals[i]; + if (speed > 1.0f) + speed = 1.0f; + if (speed < -1.0f) + speed = -1.0f; + + if (motors[i]->setSpeed(speed) < 0) { + server.send_line(make_response(false, "motor " + std::to_string(i) + + " setSpeed failed")); + ok = false; + break; + } + g_speeds[i] = speed; + } + if (ok) + server.send_line(make_response(true, "motors updated")); + + } else if (cmd == "pwm") { + // Accept legacy PWM microseconds (1100-1900) and convert to speed + int motor_idx = -1, pwm_value = -1; + if (!parse_int(line, "motor", motor_idx) || + !parse_int(line, "value", pwm_value) || motor_idx < 0 || + motor_idx >= NUM_MOTORS || pwm_value < MIN_PWM || + pwm_value > MAX_PWM) { + server.send_line( + make_response(false, "bad args -- motor 0-3, value 1100-1900")); + continue; + } + float speed = pwm_to_speed(pwm_value); + if (motors[motor_idx]->setSpeed(speed) < 0) { + server.send_line(make_response(false, "setSpeed failed")); + continue; + } + g_speeds[motor_idx] = speed; + server.send_line( + make_response(true, "motor " + std::to_string(motor_idx) + " -> " + + std::to_string(pwm_value) + + " us" + " (speed " + + std::to_string(speed) + ")")); + + } else if (cmd == "status") { + server.send_line(make_status(g_speeds)); + + } else if (cmd == "quit") { + server.send_line(make_response(true, "shutting down")); + g_running = false; + break; + + } else { + server.send_line(make_response( + false, "unknown command: " + (cmd.empty() ? "(empty)" : cmd))); + } } - // ----------------------------------------------------------------------- - // Cleanup - // Thruster destructors free GPIO pins automatically when motor_storage - // goes out of scope; no manual freePin() calls needed. - // ----------------------------------------------------------------------- - std::cout << "[SHUTDOWN] Stopping motors." << std::endl; - for (auto* m : motors) m->setSpeed(STOP_SPEED); + std::cout << "[SAFE] Client gone -- zeroing motors." << std::endl; + zero_motors(motors); + } - // motor_storage destructors run here, freeing all pins + // ----------------------------------------------------------------------- + // Cleanup + // Thruster destructors free GPIO pins automatically when motor_storage + // goes out of scope; no manual freePin() calls needed. + // ----------------------------------------------------------------------- + std::cout << "[SHUTDOWN] Stopping motors." << std::endl; + for (auto *m : motors) + m->setSpeed(STOP_SPEED); + + // motor_storage destructors run here, freeing all pins #ifndef BUILD_SIMULATION - lgGpiochipClose(handle); + lgGpiochipClose(handle); #endif - return 0; + return 0; } diff --git a/src/main.cpp b/src/main.cpp index c68b0a3..09f7ed3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,7 +1,18 @@ // main.cpp -#include - -int main() { - std::cout << "Hello World" << std::endl; - return 0; +#include "CLI11.hpp" +#include "drivers/gps/hardware_gps.hpp" +int main(int argc, char **argv) { + CLI::App app; + std::string gps_port; + app.add_option("--gps", gps_port, + "The string path to the gps. Likely, /dev/ttyACM0") + ->required(); + CLI11_PARSE(app, argc, argv); + GPS *gps = new HardwareGPS(gps_port); + std::cout << "awaiting lock" << std::endl; + gps->await_lock(5, 200); + while (true) { + std::cout << *(gps->location()) << std::endl; + sleep(3); + } } diff --git a/src/sim_main.cpp b/src/sim_main.cpp new file mode 100644 index 0000000..6b574e5 --- /dev/null +++ b/src/sim_main.cpp @@ -0,0 +1,10 @@ +#include "drivers/gps/gps.hpp" +#include "drivers/gps/simulated_gps.hpp" +int main() { + GPS *g = new Simulated_GPS(); + + while (true) { + std::cout << *g->location() << std::endl; + sleep(1); + } +} diff --git a/src/tests/thruster.cc b/src/tests/thruster.cc index f4a195e..8ab10e6 100644 --- a/src/tests/thruster.cc +++ b/src/tests/thruster.cc @@ -1,17 +1,24 @@ #include -#include "thruster.hpp" +#include "../drivers/motors/thruster.hpp" #include +class ThrusterTest : public Thruster { +public: + // Use the protected constructor in the fixture's initializer list + ThrusterTest() : Thruster() {} + +}; + TEST(Thruster, DutyCycle){ - Thruster* thruster = new Thruster(); + ThrusterTest* thruster = new ThrusterTest(); thruster->armed = true; thruster->cycle = 50; thruster->speed = 0.0; - EXPECT_EQ(7.5, thruster->getDutyCycle()); + EXPECT_FLOAT_EQ(7.5f, thruster->getDutyCycle()); } TEST(Thruster, UnarmedFailure){ - Thruster* thruster = new Thruster(0,0); + Thruster* thruster = new ThrusterTest(); thruster->armed = false; - ASSERT_THROW(thruster->setSpeed(50, std::logic_error())); + ASSERT_ANY_THROW(thruster->setSpeed(0.0)); } diff --git a/src/types/CMakeLists.txt b/src/types/CMakeLists.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/types/GPSCoordinate.hpp b/src/types/GPSCoordinate.hpp new file mode 100644 index 0000000..724593c --- /dev/null +++ b/src/types/GPSCoordinate.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include + +struct GPSCoordinate { + double latitude; + double longitude; +}; + +inline std::ostream &operator<<(std::ostream &os, GPSCoordinate const &coord) { + return os << "GPSCoordinate{" << coord.latitude << ", " << coord.longitude + << "}"; +} From d76fafd31971f3d06344d4b507699ad54c6a001c Mon Sep 17 00:00:00 2001 From: WilyHyperion <123993989+WilyHyperion@users.noreply.github.com> Date: Tue, 12 May 2026 17:02:40 -0700 Subject: [PATCH 06/10] state machine basics (#14) --- src/CMakeLists.txt | 1 - src/drivers/CMakeLists.txt | 2 +- src/drivers/motors/noah_test.cpp | 26 --------- src/drivers/motors/sim_motor.cpp | 64 --------------------- src/drivers/motors/sim_motor.hpp | 74 +++++++++++++++++++++--- src/neptune.hpp | 59 ++++++++++++++++++++ src/sim_main.cpp | 26 +++++++-- src/states/CMakeLists.txt | 0 src/states/idle.hpp | 11 ++++ src/states/navigate.hpp | 96 ++++++++++++++++++++++++++++++++ src/states/state.hpp | 18 ++++++ src/types/GPSCoordinate.hpp | 78 ++++++++++++++++++++++++++ src/types/MotorLocation.hpp | 22 ++++++++ 13 files changed, 372 insertions(+), 105 deletions(-) delete mode 100644 src/drivers/motors/noah_test.cpp delete mode 100644 src/drivers/motors/sim_motor.cpp create mode 100644 src/neptune.hpp create mode 100644 src/states/CMakeLists.txt create mode 100644 src/states/idle.hpp create mode 100644 src/states/navigate.hpp create mode 100644 src/states/state.hpp create mode 100644 src/types/MotorLocation.hpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index adc8c5f..750dd11 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -100,7 +100,6 @@ if(NOT BUILD_SIMULATION) endif() add_library(sim_motor STATIC - drivers/motors/sim_motor.cpp drivers/motors/motor.cpp ) target_include_directories(sim_motor PUBLIC diff --git a/src/drivers/CMakeLists.txt b/src/drivers/CMakeLists.txt index a10c00a..e3e8489 100644 --- a/src/drivers/CMakeLists.txt +++ b/src/drivers/CMakeLists.txt @@ -1,5 +1,5 @@ -add_library(motors motors/motor.cpp motors/sim_motor.cpp motors/thruster.cpp) +add_library(motors motors/motor.cpp motors/thruster.cpp) find_package(PkgConfig REQUIRED) diff --git a/src/drivers/motors/noah_test.cpp b/src/drivers/motors/noah_test.cpp deleted file mode 100644 index ef223e6..0000000 --- a/src/drivers/motors/noah_test.cpp +++ /dev/null @@ -1,26 +0,0 @@ -#include "motor.hpp" -#include "sim_motor.hpp" -#include -#include -#include -int main() { - srand(time(0)); - Motor *m = new SimulatedMotor(SimulatedMotor::BackRight); - Motor *m1 = new SimulatedMotor(SimulatedMotor::FrontRight); - Motor *m2 = new SimulatedMotor(SimulatedMotor::FrontLeft); - Motor *m3 = new SimulatedMotor(SimulatedMotor::BackLeft); - m->armMotor(); - m1->armMotor(); - m2->armMotor(); - m3->armMotor(); - std::random_device rd; - std::mt19937 gen(rd()); - std::uniform_real_distribution dis(-1.0, 1.0); - while (true) { - if (rand() % 2) - m->setSpeed(dis(gen)); - if (rand() % 2) - m1->setSpeed(dis(gen)); - sleep(3); - } -} diff --git a/src/drivers/motors/sim_motor.cpp b/src/drivers/motors/sim_motor.cpp deleted file mode 100644 index d79a32d..0000000 --- a/src/drivers/motors/sim_motor.cpp +++ /dev/null @@ -1,64 +0,0 @@ -#include "sim_motor.hpp" -#include -#include "../common/sim.hpp" -std::string SimulatedMotor::GetMotorPath() { - switch (this->ml) { - case SimulatedMotor::FrontLeft: - return "/motor/top_left/set"; - break; - case SimulatedMotor::FrontRight: - return "/motor/top_right/set"; - break; - case SimulatedMotor::BackLeft: - return "/motor/bottom_left/set"; - break; - case SimulatedMotor::BackRight: - return "/motor/bottom_right/set"; - break; - default: - // Error? - break; - } - return ""; -} - -SimulatedMotor::SimulatedMotor(SimulatedMotor::MotorLocation ml, int speed, - float cycle) - : Motor(speed, cycle) { - if (sim_connection == 0) { - sim_connection = new httplib::Client(SIM_ADDR); - httplib::Result out = sim_connection->Get("/heartbeat"); - if (out == nullptr) { - throw std::runtime_error("No simulation connection found"); - } - std::cout << "Status code from sim:" << out->status << std::endl; - } - this->ml = ml; -} - -int SimulatedMotor::setSpeed(float speed) { - if (!this->armed) { - throw std::logic_error("Tried to set the speed of an unarmed motor"); - } - this->speed = speed; - // In the long run we should add a json lib - std::ostringstream oss; - oss << "{\"speed\":" << speed << "}"; - std::string s = oss.str(); - auto out = sim_connection->Post( - this->GetMotorPath().c_str(), s.c_str(), "application/json"); - if (out == nullptr) { - std::cout << "Error sending sim info" << std::endl; - } else { - std::cout << out->status << std::endl; - } - - return 0; -} - -int SimulatedMotor::setFrequency(float cycle) { - // idk if this will be called at all - return 0; -} - -void SimulatedMotor::armMotor() { this->armed = true; } diff --git a/src/drivers/motors/sim_motor.hpp b/src/drivers/motors/sim_motor.hpp index dfd05e4..5a4f829 100644 --- a/src/drivers/motors/sim_motor.hpp +++ b/src/drivers/motors/sim_motor.hpp @@ -1,22 +1,78 @@ #pragma once #include "motor.hpp" #include +#include "../../types/MotorLocation.hpp" -class SimulatedMotor : public Motor { +class SimulatedMotor : public Motor +{ public: - // At some point we should prob use this datatype everywhere, for now its here - enum MotorLocation { FrontRight, FrontLeft, BackRight, BackLeft }; MotorLocation ml; bool armed; - SimulatedMotor(SimulatedMotor::MotorLocation ml, int speed = 0.0, - float cycle = 50); - int setSpeed(float speed) override; - int setFrequency(float cycle) override; + SimulatedMotor(MotorLocation ml, int speed, + float cycle) : Motor(speed, cycle) + { + if (sim_connection == 0) + { + sim_connection = new httplib::Client(SIM_ADDR); + httplib::Result out = sim_connection->Get("/heartbeat"); + if (out == nullptr) + { + throw std::runtime_error("No simulation connection found"); + } + std::cout << "Status code from sim:" << out->status << std::endl; + } + this->ml = ml; + } + int setSpeed(float speed) override { + if (!this->armed) { + throw std::logic_error("Tried to set the speed of an unarmed motor"); + } + this->speed = speed; + // In the long run we should add a json lib + std::ostringstream oss; + oss << "{\"speed\":" << speed << "}"; + std::string s = oss.str(); + auto out = sim_connection->Post( + this->GetMotorPath().c_str(), s.c_str(), "application/json"); + if (out == nullptr) { + std::cout << "Error sending sim info" << std::endl; + } else { + std::cout << out->status << std::endl; + } + + return 0; +} +int setFrequency(float cycle) override { + // idk if this will be called at all + return 0; +} + +void armMotor()override { this->armed = true; } - void armMotor() override; private: - std::string GetMotorPath(); + std::string GetMotorPath() + { + switch (this->ml) + { + case FrontLeft: + return "/motor/top_left/set"; + break; + case FrontRight: + return "/motor/top_right/set"; + break; + case BackLeft: + return "/motor/bottom_left/set"; + break; + case BackRight: + return "/motor/bottom_right/set"; + break; + default: + // Error? + break; + } + return ""; + } }; diff --git a/src/neptune.hpp b/src/neptune.hpp new file mode 100644 index 0000000..ab0e225 --- /dev/null +++ b/src/neptune.hpp @@ -0,0 +1,59 @@ +#pragma once +#include "drivers/motors/motor.hpp" +#include "drivers/gps/gps.hpp" +#include +#include +#include "states/state.hpp" + +class Neptune { + public: + State* state; + inline static Neptune* instance = nullptr; + GPS* gps; + Motor* front_left; + Motor* front_right; + Motor* back_left; + Motor* back_right; + Motor* all_motors[4]; + /** + * Gets the heading, in radians of the robot. 0 indicates north, pi indicates south + * @return The heading + */ + double get_heading(){ + //TODO - Use IMU to calculate this + return 0; + } + std::array, 2> GetMotorVectorMatrice() { + std::array, 2> out = { + }; + for(int m = 0; m <= 3; m ++) { + out[0][m] = thrust_vector(static_cast(m)).latitude; + out[1][m] = thrust_vector(static_cast(m)).longitude; + } + return out; + } + /** + * Starts neptune. This method will functionally take over the thread it is called from + */ + void start(){ + Neptune::instance = this; + gps->await_lock(5, 500); + std::cout << "Arming motors" << std::endl; + front_left->armMotor(); + front_right->armMotor(); + back_left->armMotor(); + back_right->armMotor(); + + all_motors[0] = front_left; + all_motors[1] = front_right; + all_motors[2] = back_left; + all_motors[3] = back_right; + + while (true) + { + std::this_thread::sleep_for(this->state->tick()); + } + + } + +}; diff --git a/src/sim_main.cpp b/src/sim_main.cpp index 6b574e5..0fd5f18 100644 --- a/src/sim_main.cpp +++ b/src/sim_main.cpp @@ -1,10 +1,28 @@ #include "drivers/gps/gps.hpp" #include "drivers/gps/simulated_gps.hpp" +#include "drivers/motors/sim_motor.hpp" +#include "drivers/motors/motor.hpp" +#include "types/MotorLocation.hpp" +#include "neptune.hpp" +#include "states/navigate.hpp" int main() { GPS *g = new Simulated_GPS(); - while (true) { - std::cout << *g->location() << std::endl; - sleep(1); - } + Motor* front_left = new SimulatedMotor(MotorLocation::FrontLeft, 0 ,50); + Motor* front_right = new SimulatedMotor(MotorLocation::FrontRight, 0 ,50); + Motor* back_left = new SimulatedMotor(MotorLocation::BackLeft, 0, 50); + Motor* back_right = new SimulatedMotor(MotorLocation::BackRight, 0, 50); + Neptune* neptune = new Neptune(); + neptune->gps = g; + neptune->back_left = back_left; + neptune->back_right = back_right; + neptune->front_left = front_left; + neptune->front_right = front_right; + GPSCoordinate goal = GPSCoordinate{ + 0, + 10 + }; +neptune->state = new Navigate(goal); + neptune->start(); + } diff --git a/src/states/CMakeLists.txt b/src/states/CMakeLists.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/states/idle.hpp b/src/states/idle.hpp new file mode 100644 index 0000000..6ecca7f --- /dev/null +++ b/src/states/idle.hpp @@ -0,0 +1,11 @@ + +#pragma once +#include "state.hpp" +class Idle : public State { + public: + + std::chrono::milliseconds tick() override { + std::cout << "Idling" << std::endl; + return std::chrono::milliseconds(5000); + } +}; diff --git a/src/states/navigate.hpp b/src/states/navigate.hpp new file mode 100644 index 0000000..066ec3c --- /dev/null +++ b/src/states/navigate.hpp @@ -0,0 +1,96 @@ +#pragma once +#include "state.hpp" +#include "../../neptune.hpp" +#include +#include +#include +#include +/**Tries to solve with no negative inputs, so motors don't fire backwards + * This is vibe coded slop. In the future, we should add a linear algebra lib instead of using this + */ +std::optional> solveNonNegative(const std::array, 2>& mat) +{ + constexpr int VARS = 4; + constexpr double EPS = 1e-9; + + // Try every pair of "basic" variables; the remaining two are fixed at 0. + for (int p = 0; p < VARS; ++p) { + for (int q = p + 1; q < VARS; ++q) { + + // Extract the 2x2 sub-system for columns p and q. + const double a00 = mat[0][p], a01 = mat[0][q], b0 = mat[0][4]; + const double a10 = mat[1][p], a11 = mat[1][q], b1 = mat[1][4]; + + const double det = a00 * a11 - a01 * a10; + if (std::abs(det) < EPS) + continue; // columns are linearly dependent — skip + + // Cramer's Rule + const double xp = (b0 * a11 - b1 * a01) / det; + const double xq = (a00 * b1 - a10 * b0) / det; + + if (xp < -EPS || xq < -EPS) + continue; // a basic variable went negative — skip + + // Valid non-negative basic feasible solution found. + std::array solution{}; // nosn-basics default to 0 + solution[p] = std::max(0.0, xp); // clamp -0 artefacts + solution[q] = std::max(0.0, xq); + return solution; + } + } + + return std::nullopt; // all 6 bases checked, none feasible +} + + + +class Navigate : public State { + public: + bool finished; + GPSCoordinate goal; + Navigate(GPSCoordinate goal) : goal(goal){ + + } + + + std::chrono::milliseconds tick() override { + double current_heading = Neptune::instance->get_heading(); + GPSCoordinate* loc = Neptune::instance->gps->location(); + GPSCoordinate goal_dif = goal - *loc; + double mag = pow( goal_dif.latitude * goal_dif.latitude + goal_dif.longitude * goal_dif.longitude, 0.5); + std::cout << "Dist:" << mag << std::endl; + if(mag < 0.1){ + //super close, get a random new cord + goal.latitude = ((rand() % 30) - 15); + goal.longitude = ((rand() % 30) - 15); + } + goal_dif.latitude /= mag; + goal_dif.longitude /= mag; + /** + * We know we want to translate along goal_dif + * We can form goal_dif by combining the vectors of each motor's thrust vectors + * Thus, our problem is to form a linear combination of the goal vector through the 4 thrusters, and take one solution + * ex an array + * m1x m2x m3x m4x gx + * and for y + */ + std::array, 2> mat = Neptune::instance->GetMotorVectorMatrice(); + mat[0][4] = goal_dif.latitude; + mat[1][4] = goal_dif.longitude; + std::optional> out = solveNonNegative(mat); + std::cout << goal << *loc << goal_dif << std::endl; + if(out.has_value()){ + for(int i = 0; i < out.value().size(); i ++) { + std::cout << i << ":" << out.value()[i] << std::endl; + //TODO normalize to some constant range based on desired activation + Neptune::instance->all_motors[i]->setSpeed(out.value()[i]); + } + } + else { + std::cout << "Failed to solve" << std::endl; + } + delete loc; + return std::chrono::milliseconds(1000); + } +}; diff --git a/src/states/state.hpp b/src/states/state.hpp new file mode 100644 index 0000000..795b956 --- /dev/null +++ b/src/states/state.hpp @@ -0,0 +1,18 @@ +#pragma once +#include "../neptune.hpp" +#include +//Represents a state the usv can be in +class State{ + public: + State(){ + + } + + /** + * @brief The driving logic for this tick + * @returns The time, in ms, to wait before ticking again + */ + virtual std::chrono::milliseconds tick(){ + return std::chrono::milliseconds(5000); + } +}; diff --git a/src/types/GPSCoordinate.hpp b/src/types/GPSCoordinate.hpp index 724593c..5ca6c4d 100644 --- a/src/types/GPSCoordinate.hpp +++ b/src/types/GPSCoordinate.hpp @@ -7,6 +7,84 @@ struct GPSCoordinate { double longitude; }; +// Equality operators +inline bool operator==(GPSCoordinate const& lhs, GPSCoordinate const& rhs) { + return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude; +} + +inline bool operator!=(GPSCoordinate const& lhs, GPSCoordinate const& rhs) { + return !(lhs == rhs); +} + +// Comparison operators (lexicographical) +inline bool operator<(GPSCoordinate const& lhs, GPSCoordinate const& rhs) { + if (lhs.latitude != rhs.latitude) return lhs.latitude < rhs.latitude; + return lhs.longitude < rhs.longitude; +} + +inline bool operator>(GPSCoordinate const& lhs, GPSCoordinate const& rhs) { + return rhs < lhs; +} + +inline bool operator<=(GPSCoordinate const& lhs, GPSCoordinate const& rhs) { + return !(rhs < lhs); +} + +inline bool operator>=(GPSCoordinate const& lhs, GPSCoordinate const& rhs) { + return !(lhs < rhs); +} + +// Arithmetic operators +inline GPSCoordinate operator+(GPSCoordinate const& lhs, GPSCoordinate const& rhs) { + return {lhs.latitude + rhs.latitude, lhs.longitude + rhs.longitude}; +} + +inline GPSCoordinate operator-(GPSCoordinate const& lhs, GPSCoordinate const& rhs) { + return {lhs.latitude - rhs.latitude, lhs.longitude - rhs.longitude}; +} + +inline GPSCoordinate operator*(GPSCoordinate const& coord, double scalar) { + return {coord.latitude * scalar, coord.longitude * scalar}; +} + +inline GPSCoordinate operator*(double scalar, GPSCoordinate const& coord) { + return coord * scalar; +} + +inline GPSCoordinate operator/(GPSCoordinate const& coord, double scalar) { + return {coord.latitude / scalar, coord.longitude / scalar}; +} + +// Unary minus +inline GPSCoordinate operator-(GPSCoordinate const& coord) { + return {-coord.latitude, -coord.longitude}; +} + +// Compound assignment +inline GPSCoordinate& operator+=(GPSCoordinate& lhs, GPSCoordinate const& rhs) { + lhs.latitude += rhs.latitude; + lhs.longitude += rhs.longitude; + return lhs; +} + +inline GPSCoordinate& operator-=(GPSCoordinate& lhs, GPSCoordinate const& rhs) { + lhs.latitude -= rhs.latitude; + lhs.longitude -= rhs.longitude; + return lhs; +} + +inline GPSCoordinate& operator*=(GPSCoordinate& coord, double scalar) { + coord.latitude *= scalar; + coord.longitude *= scalar; + return coord; +} + +inline GPSCoordinate& operator/=(GPSCoordinate& coord, double scalar) { + coord.latitude /= scalar; + coord.longitude /= scalar; + return coord; +} + inline std::ostream &operator<<(std::ostream &os, GPSCoordinate const &coord) { return os << "GPSCoordinate{" << coord.latitude << ", " << coord.longitude << "}"; diff --git a/src/types/MotorLocation.hpp b/src/types/MotorLocation.hpp new file mode 100644 index 0000000..84d971d --- /dev/null +++ b/src/types/MotorLocation.hpp @@ -0,0 +1,22 @@ +#pragma once +#include "GPSCoordinate.hpp" +#include +enum MotorLocation { FrontRight, FrontLeft, BackRight, BackLeft }; + +constexpr double SQRT2 = 1.41421356237309504880; +//TODO make sure these stay up to date, and maybe consider a seperate point class that isn't a GPScoord +GPSCoordinate thrust_vector(MotorLocation m){ + switch(m){ + case FrontLeft: + return {0.5 * SQRT2, 0.5 * SQRT2}; + case FrontRight: + return {-0.5* SQRT2, 0.5* SQRT2}; + case BackRight: + return {-0.5* SQRT2, -0.5* SQRT2}; + case BackLeft: + return {0.5* SQRT2, -0.5* SQRT2}; + } + +} +// 0 - lat +// 1 - long From ce1bf9a135b4d6e87cabfba3601257e2f2b60136 Mon Sep 17 00:00:00 2001 From: WilyHyperion Date: Tue, 19 May 2026 22:47:31 +0000 Subject: [PATCH 07/10] add comments --- src/drivers/gps/gps.hpp | 8 ++- src/drivers/gps/hardware_gps.hpp | 32 ++++++++--- src/drivers/gps/simulated_gps.hpp | 8 +-- src/main.cpp | 2 +- src/states/navigate.hpp | 96 ------------------------------- src/types/MotorLocation.hpp | 2 +- 6 files changed, 36 insertions(+), 112 deletions(-) delete mode 100644 src/states/navigate.hpp diff --git a/src/drivers/gps/gps.hpp b/src/drivers/gps/gps.hpp index d4adbf9..3558fa7 100644 --- a/src/drivers/gps/gps.hpp +++ b/src/drivers/gps/gps.hpp @@ -2,15 +2,17 @@ #include "../../types/GPSCoordinate.hpp" #include /** + * An abstract class that represents a gps that can gather cordinate location */ class GPS { public: + virtual bool await_lock(int interval, int tries) { std::cout << "Dont call this" << std::endl; - return false; + throw std::logic_error("Tried to use a gps parent class"); } - virtual GPSCoordinate *location() { + virtual GPSCoordinate location() { std::cout << "Dont call this" << std::endl; - return nullptr; + throw std::logic_error("Tried to use a gps parent class"); } }; diff --git a/src/drivers/gps/hardware_gps.hpp b/src/drivers/gps/hardware_gps.hpp index be98049..4f3bfc0 100644 --- a/src/drivers/gps/hardware_gps.hpp +++ b/src/drivers/gps/hardware_gps.hpp @@ -1,16 +1,28 @@ +#pragma once #include "gps.hpp" #include "../../types/GPSCoordinate.hpp" #include #include #include +#include + /** * Note - its likely that only 1 gps can function at a time + * Reprsents a GPS attached by usb to the pi. Uses the gpsd daemon to gather information */ class HardwareGPS : public GPS { public: ~HardwareGPS() { delete this->gps_rec; } - GPSCoordinate *location() override { + + + /** + * Get the current GPS location once a valid fix is obtained. + * No alt is gotten. + * Maybe block for awhile + * @return GPSCoordinate containing latitude and longitude. + */ + GPSCoordinate location() override { while (true) { struct gps_data_t *data; @@ -21,17 +33,17 @@ class HardwareGPS : public GPS { if ((data = this->gps_rec->read()) == NULL) { std::cerr << "Read error." << std::endl; // TODO - what to do here? - return nullptr; + throw std::runtime_error("Failed to read gps"); } else { if (data->fix.mode >= MODE_2D) { // Connection gotten - GPSCoordinate *out = new GPSCoordinate(); - out->latitude = data->fix.latitude; - out->longitude = data->fix.longitude; - + GPSCoordinate out = GPSCoordinate(); + out.latitude = data->fix.latitude; + out.longitude = data->fix.longitude; return out; } else { - std::cout << "No gps fix" << std::endl; + //TODO - order + std::cout << "No gps fix yet" << std::endl; } } } @@ -45,6 +57,12 @@ class HardwareGPS : public GPS { std::cout << "GPS loaded" << std::endl; this->gps_rec = new gpsmm("localhost", gpsd_port); } + /** + * Awaits a gps lock. Note that a lock could be lost if the gps is blocked again, in which case the next location call will re-obtain the lock + * @param interval Wait time between tries + * @param tries Number of times to try before returning false + * @returns the fix status + */ bool await_lock(int interval, int tries) override { if (this->gps_rec->stream(WATCH_ENABLE | WATCH_JSON) == NULL) { std::cerr << "No GPSD running." << std::endl; diff --git a/src/drivers/gps/simulated_gps.hpp b/src/drivers/gps/simulated_gps.hpp index 7464bfd..102f874 100644 --- a/src/drivers/gps/simulated_gps.hpp +++ b/src/drivers/gps/simulated_gps.hpp @@ -18,14 +18,14 @@ class Simulated_GPS : public GPS { } } bool await_lock(int interval, int tries) override { return true; } - GPSCoordinate *location() override { + GPSCoordinate location() override { auto out = sim_connection->Get("/gps"); if (out == nullptr) { std::cout << "Error getting GPS sim info" << std::endl; - return nullptr; + //throw? } - GPSCoordinate *s = new GPSCoordinate(); - sscanf(out->body.c_str(), "[%lf,%lf,", &s->latitude, &s->longitude); + GPSCoordinate s = GPSCoordinate(); + sscanf(out->body.c_str(), "[%lf,%lf,", &s.latitude, &s.longitude); return s; } }; diff --git a/src/main.cpp b/src/main.cpp index 09f7ed3..83640d8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -12,7 +12,7 @@ int main(int argc, char **argv) { std::cout << "awaiting lock" << std::endl; gps->await_lock(5, 200); while (true) { - std::cout << *(gps->location()) << std::endl; + std::cout << (gps->location()) << std::endl; sleep(3); } } diff --git a/src/states/navigate.hpp b/src/states/navigate.hpp deleted file mode 100644 index 066ec3c..0000000 --- a/src/states/navigate.hpp +++ /dev/null @@ -1,96 +0,0 @@ -#pragma once -#include "state.hpp" -#include "../../neptune.hpp" -#include -#include -#include -#include -/**Tries to solve with no negative inputs, so motors don't fire backwards - * This is vibe coded slop. In the future, we should add a linear algebra lib instead of using this - */ -std::optional> solveNonNegative(const std::array, 2>& mat) -{ - constexpr int VARS = 4; - constexpr double EPS = 1e-9; - - // Try every pair of "basic" variables; the remaining two are fixed at 0. - for (int p = 0; p < VARS; ++p) { - for (int q = p + 1; q < VARS; ++q) { - - // Extract the 2x2 sub-system for columns p and q. - const double a00 = mat[0][p], a01 = mat[0][q], b0 = mat[0][4]; - const double a10 = mat[1][p], a11 = mat[1][q], b1 = mat[1][4]; - - const double det = a00 * a11 - a01 * a10; - if (std::abs(det) < EPS) - continue; // columns are linearly dependent — skip - - // Cramer's Rule - const double xp = (b0 * a11 - b1 * a01) / det; - const double xq = (a00 * b1 - a10 * b0) / det; - - if (xp < -EPS || xq < -EPS) - continue; // a basic variable went negative — skip - - // Valid non-negative basic feasible solution found. - std::array solution{}; // nosn-basics default to 0 - solution[p] = std::max(0.0, xp); // clamp -0 artefacts - solution[q] = std::max(0.0, xq); - return solution; - } - } - - return std::nullopt; // all 6 bases checked, none feasible -} - - - -class Navigate : public State { - public: - bool finished; - GPSCoordinate goal; - Navigate(GPSCoordinate goal) : goal(goal){ - - } - - - std::chrono::milliseconds tick() override { - double current_heading = Neptune::instance->get_heading(); - GPSCoordinate* loc = Neptune::instance->gps->location(); - GPSCoordinate goal_dif = goal - *loc; - double mag = pow( goal_dif.latitude * goal_dif.latitude + goal_dif.longitude * goal_dif.longitude, 0.5); - std::cout << "Dist:" << mag << std::endl; - if(mag < 0.1){ - //super close, get a random new cord - goal.latitude = ((rand() % 30) - 15); - goal.longitude = ((rand() % 30) - 15); - } - goal_dif.latitude /= mag; - goal_dif.longitude /= mag; - /** - * We know we want to translate along goal_dif - * We can form goal_dif by combining the vectors of each motor's thrust vectors - * Thus, our problem is to form a linear combination of the goal vector through the 4 thrusters, and take one solution - * ex an array - * m1x m2x m3x m4x gx - * and for y - */ - std::array, 2> mat = Neptune::instance->GetMotorVectorMatrice(); - mat[0][4] = goal_dif.latitude; - mat[1][4] = goal_dif.longitude; - std::optional> out = solveNonNegative(mat); - std::cout << goal << *loc << goal_dif << std::endl; - if(out.has_value()){ - for(int i = 0; i < out.value().size(); i ++) { - std::cout << i << ":" << out.value()[i] << std::endl; - //TODO normalize to some constant range based on desired activation - Neptune::instance->all_motors[i]->setSpeed(out.value()[i]); - } - } - else { - std::cout << "Failed to solve" << std::endl; - } - delete loc; - return std::chrono::milliseconds(1000); - } -}; diff --git a/src/types/MotorLocation.hpp b/src/types/MotorLocation.hpp index 84d971d..4767321 100644 --- a/src/types/MotorLocation.hpp +++ b/src/types/MotorLocation.hpp @@ -16,7 +16,7 @@ GPSCoordinate thrust_vector(MotorLocation m){ case BackLeft: return {0.5* SQRT2, -0.5* SQRT2}; } - + throw std::logic_error("Invalid type"); } // 0 - lat // 1 - long From 62c0f90652a7aedd1fd12d85b7670696d1bbac51 Mon Sep 17 00:00:00 2001 From: WilyHyperion Date: Tue, 19 May 2026 22:49:37 +0000 Subject: [PATCH 08/10] re add deleted file --- src/states/navigate.hpp | 96 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 src/states/navigate.hpp diff --git a/src/states/navigate.hpp b/src/states/navigate.hpp new file mode 100644 index 0000000..066ec3c --- /dev/null +++ b/src/states/navigate.hpp @@ -0,0 +1,96 @@ +#pragma once +#include "state.hpp" +#include "../../neptune.hpp" +#include +#include +#include +#include +/**Tries to solve with no negative inputs, so motors don't fire backwards + * This is vibe coded slop. In the future, we should add a linear algebra lib instead of using this + */ +std::optional> solveNonNegative(const std::array, 2>& mat) +{ + constexpr int VARS = 4; + constexpr double EPS = 1e-9; + + // Try every pair of "basic" variables; the remaining two are fixed at 0. + for (int p = 0; p < VARS; ++p) { + for (int q = p + 1; q < VARS; ++q) { + + // Extract the 2x2 sub-system for columns p and q. + const double a00 = mat[0][p], a01 = mat[0][q], b0 = mat[0][4]; + const double a10 = mat[1][p], a11 = mat[1][q], b1 = mat[1][4]; + + const double det = a00 * a11 - a01 * a10; + if (std::abs(det) < EPS) + continue; // columns are linearly dependent — skip + + // Cramer's Rule + const double xp = (b0 * a11 - b1 * a01) / det; + const double xq = (a00 * b1 - a10 * b0) / det; + + if (xp < -EPS || xq < -EPS) + continue; // a basic variable went negative — skip + + // Valid non-negative basic feasible solution found. + std::array solution{}; // nosn-basics default to 0 + solution[p] = std::max(0.0, xp); // clamp -0 artefacts + solution[q] = std::max(0.0, xq); + return solution; + } + } + + return std::nullopt; // all 6 bases checked, none feasible +} + + + +class Navigate : public State { + public: + bool finished; + GPSCoordinate goal; + Navigate(GPSCoordinate goal) : goal(goal){ + + } + + + std::chrono::milliseconds tick() override { + double current_heading = Neptune::instance->get_heading(); + GPSCoordinate* loc = Neptune::instance->gps->location(); + GPSCoordinate goal_dif = goal - *loc; + double mag = pow( goal_dif.latitude * goal_dif.latitude + goal_dif.longitude * goal_dif.longitude, 0.5); + std::cout << "Dist:" << mag << std::endl; + if(mag < 0.1){ + //super close, get a random new cord + goal.latitude = ((rand() % 30) - 15); + goal.longitude = ((rand() % 30) - 15); + } + goal_dif.latitude /= mag; + goal_dif.longitude /= mag; + /** + * We know we want to translate along goal_dif + * We can form goal_dif by combining the vectors of each motor's thrust vectors + * Thus, our problem is to form a linear combination of the goal vector through the 4 thrusters, and take one solution + * ex an array + * m1x m2x m3x m4x gx + * and for y + */ + std::array, 2> mat = Neptune::instance->GetMotorVectorMatrice(); + mat[0][4] = goal_dif.latitude; + mat[1][4] = goal_dif.longitude; + std::optional> out = solveNonNegative(mat); + std::cout << goal << *loc << goal_dif << std::endl; + if(out.has_value()){ + for(int i = 0; i < out.value().size(); i ++) { + std::cout << i << ":" << out.value()[i] << std::endl; + //TODO normalize to some constant range based on desired activation + Neptune::instance->all_motors[i]->setSpeed(out.value()[i]); + } + } + else { + std::cout << "Failed to solve" << std::endl; + } + delete loc; + return std::chrono::milliseconds(1000); + } +}; From ac305095329174ca1966123fe09c949aff0e76df Mon Sep 17 00:00:00 2001 From: WilyHyperion Date: Thu, 28 May 2026 02:43:19 +0000 Subject: [PATCH 09/10] run formater --- Docker/Dockerfile | 3 +- src/CMakeLists.txt | 8 +- src/drivers/CMakeLists.txt | 5 + src/drivers/gps/hardware_gps.hpp | 1 - src/drivers/imu/compass.hpp | 22 +++ src/drivers/motors/sim_motor.hpp | 2 - src/main.cpp | 40 +++++- src/neptune.hpp | 238 ++++++++++++++++++++++++++++--- src/sim_main.cpp | 28 ++-- src/states/menu.hpp | 75 ++++++++++ src/states/navigate.hpp | 92 ++---------- src/types/GPSCoordinate.hpp | 6 +- src/types/MotorLocation.hpp | 17 ++- 13 files changed, 400 insertions(+), 137 deletions(-) create mode 100644 src/drivers/imu/compass.hpp create mode 100644 src/states/menu.hpp diff --git a/Docker/Dockerfile b/Docker/Dockerfile index d42c19a..53ba434 100644 --- a/Docker/Dockerfile +++ b/Docker/Dockerfile @@ -9,7 +9,8 @@ RUN apt-get update -y RUN apt-get install -y git gdb curl clang-format #Get project dependencies -RUN apt-get install -y gpiod libgpiod-dev liblgpio-dev libserial-dev gpsd libgps-dev +RUN apt-get install -y gpiod libgpiod-dev liblgpio-dev libserial-dev gpsd libgps-dev libeigen3-dev + #dev dependencies RUN apt-get install -y python3 python3-pip pipx WORKDIR /workspace/Neptune diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 750dd11..257b6f1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -51,17 +51,21 @@ endif() +# Eigen(linear algebra lib) +find_package(Eigen3 REQUIRED) + +target_include_directories(neptune PRIVATE ${EIGEN3_INCLUDE_DIRS}) +target_include_directories(neptune_sim PRIVATE ${EIGEN3_INCLUDE_DIRS}) link_directories(drivers) add_subdirectory(drivers) add_subdirectory(types) - +add_subdirectory(states) target_link_libraries(motors ${LGPIO_LINK_LIBS}) #main target target_include_directories(motors PRIVATE ${LGPIO_INCLUDE_DIR}) - target_link_libraries(neptune_sim motors) target_link_libraries(neptune motors) diff --git a/src/drivers/CMakeLists.txt b/src/drivers/CMakeLists.txt index e3e8489..5c4a757 100644 --- a/src/drivers/CMakeLists.txt +++ b/src/drivers/CMakeLists.txt @@ -10,3 +10,8 @@ target_include_directories(gps_drivers INTERFACE ${GPSD_INCLUDE_DIRS}) target_include_directories(motors PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/motors) target_include_directories(gps_drivers INTERFACE d ${CMAKE_CURRENT_SOURCE_DIR}/gps) target_link_libraries(gps_drivers INTERFACE ${GPSD_LIBRARIES}) + + + +add_library(imu INTERFACE) +target_include_directories(imu INTERFACE d ${CMAKE_CURRENT_SOURCE_DIR}/imu) diff --git a/src/drivers/gps/hardware_gps.hpp b/src/drivers/gps/hardware_gps.hpp index 4f3bfc0..c9b1599 100644 --- a/src/drivers/gps/hardware_gps.hpp +++ b/src/drivers/gps/hardware_gps.hpp @@ -1,6 +1,5 @@ #pragma once #include "gps.hpp" - #include "../../types/GPSCoordinate.hpp" #include #include diff --git a/src/drivers/imu/compass.hpp b/src/drivers/imu/compass.hpp new file mode 100644 index 0000000..39c8988 --- /dev/null +++ b/src/drivers/imu/compass.hpp @@ -0,0 +1,22 @@ +#pragma once +#include + +#include + +using namespace Eigen; +/* +TODO: this is a placeholder until the commit w/ imu gets made + +*/ +class Compass +{ +public: + /** + * Filler sim version, should convert to abstract once we are done with the IMU + * Returns the true north vector in the context of the world + */ + std::array get() + { + return {0,0,0}; + } +}; diff --git a/src/drivers/motors/sim_motor.hpp b/src/drivers/motors/sim_motor.hpp index 5a4f829..9108b15 100644 --- a/src/drivers/motors/sim_motor.hpp +++ b/src/drivers/motors/sim_motor.hpp @@ -38,8 +38,6 @@ class SimulatedMotor : public Motor this->GetMotorPath().c_str(), s.c_str(), "application/json"); if (out == nullptr) { std::cout << "Error sending sim info" << std::endl; - } else { - std::cout << out->status << std::endl; } return 0; diff --git a/src/main.cpp b/src/main.cpp index 83640d8..ff75627 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,18 +1,46 @@ // main.cpp #include "CLI11.hpp" #include "drivers/gps/hardware_gps.hpp" -int main(int argc, char **argv) { +#include "drivers/gps/gps.hpp" +#include "drivers/motors/motor.hpp" +#include "neptune.hpp" +#include "states/navigate.hpp" +#include "drivers/motors/thruster.hpp" +#include "states/menu.hpp" +#include "lgpio.h" +#include +#include +#include +int main(int argc, char **argv) +{ CLI::App app; std::string gps_port; app.add_option("--gps", gps_port, "The string path to the gps. Likely, /dev/ttyACM0") ->required(); + + std::vector pins; + app.add_option("--motors", pins, "The 4 pins for motors (space delimited) in the form: front_left front_right back_left back_right"); CLI11_PARSE(app, argc, argv); GPS *gps = new HardwareGPS(gps_port); - std::cout << "awaiting lock" << std::endl; - gps->await_lock(5, 200); - while (true) { - std::cout << (gps->location()) << std::endl; - sleep(3); + //Hardcoded since I dont think this will ever change on the pi + + if(pins.size() != 4){ + std::cout << "Wrong number of input pins for motors:" << pins.size() <<"\n"; + return 1; } + int handle = lgGpiochipOpen(0); + Motor *front_left = new Thruster(pins[0], handle); + Motor *front_right = new Thruster(pins[1], handle); + Motor *back_left = new Thruster(pins[2], handle); + Motor *back_right = new Thruster(pins[3], handle); + Neptune *neptune = new Neptune(); + neptune->gps = gps; + neptune->back_left = back_left; + neptune->back_right = back_right; + neptune->front_left = front_left; + neptune->front_right = front_right; + neptune->compass = new Compass(); + neptune->state = new Menu(); + neptune->start(); } diff --git a/src/neptune.hpp b/src/neptune.hpp index ab0e225..bd839e3 100644 --- a/src/neptune.hpp +++ b/src/neptune.hpp @@ -1,41 +1,179 @@ #pragma once #include "drivers/motors/motor.hpp" #include "drivers/gps/gps.hpp" + +#include "drivers/imu/compass.hpp" #include #include +#include +#include +#include +#include +#include +#include "types/MotorLocation.hpp" +#include +#include + #include "states/state.hpp" -class Neptune { - public: - State* state; - inline static Neptune* instance = nullptr; - GPS* gps; - Motor* front_left; - Motor* front_right; - Motor* back_left; - Motor* back_right; - Motor* all_motors[4]; +class Neptune +{ +public: + State *state; + inline static Neptune *instance = nullptr; + GPS *gps; + Motor *front_left; + Motor *front_right; + Motor *back_left; + Motor *back_right; + Motor *all_motors[4]; + Compass *compass; + /** + * Prints all of the sensor data to std out in a nice format + */ + void dispData() + { + // GPS + try + { + auto loc = gps->location(); + std::cout << loc << "\n"; + } + catch (...) + { + std::cout << "GPS Location: \n"; + } + // Compass + try + { + auto c = compass->get(); + std::cout << "Compass: [" << c[0] << ", " << c[1] << ", " << c[2] << "] Heading(rad): " << get_heading() << "\n"; + } + catch (...) + { + std::cout << "Compass: \n"; + } + // Motors + for (int i = 0; i < 4; ++i) + { + Motor *m = all_motors[i]; + if (m) + { + double s = 0; + try + { + s = m->speed; + } + catch (...) + { + } + std::cout << "Motor[" << i << "] speed: " << s << "\n"; + } + else + { + std::cout << "Motor[" << i << "]: \n"; + } + } + // State + if (state) + { + try + { + std::cout << "State tick(ms): " << typeid(state).name() << "\n"; + } + catch (...) + { + std::cout << "State: \n"; + } + } + else + { + std::cout << "State: \n"; + } + } + // Helper to power motors for forward movement + void forwards(double max = 0.75) + { + front_left->setSpeed(max); + front_right->setSpeed(max); + back_left->setSpeed(max); + back_right->setSpeed(max); + } + // Helper to power motors for backwards movement + void backwards(double max = 0.75) + { + front_left->setSpeed(-max); + front_right->setSpeed(-max); + back_left->setSpeed(-max); + back_right->setSpeed(-max); + } + + // Helper to halt all motors. Note this stops the motor, and does not account for drifting + void stop() + { + for (Motor *m : all_motors) + { + m->setSpeed(0); + } + } + /** * Gets the heading, in radians of the robot. 0 indicates north, pi indicates south + * Heading is a **counterclockwise** rotation about the upward axsis * @return The heading */ - double get_heading(){ - //TODO - Use IMU to calculate this - return 0; + double get_heading() + { + auto c = compass->get(); // north in the format: [nx, ny, nz] of the compass + // We know true north in ENU is [0,0,1], so we need to measure our north directions difference from that + // Ignore z(upwards) cause if we get rotated too far upward it impacts nav we're fucked anyway. Thus our axis of rotation is +z (0,0,1) + + // Flip x/y here because atan will give from x axsis but we want from +y + double out = atan2(c[1], c[0]); + + return out; } - std::array, 2> GetMotorVectorMatrice() { - std::array, 2> out = { - }; - for(int m = 0; m <= 3; m ++) { - out[0][m] = thrust_vector(static_cast(m)).latitude; - out[1][m] = thrust_vector(static_cast(m)).longitude; + const double MAX_MOTOR_ACTIVATION = 0.75; // TODO move to some config or smth + /** + * Activates the motors to travel towards the target location. + * Uses MAX_MOTOR_ACTIVATION to scale the output + * TODO change tolerance, 5.0 is crazy high for gps coords + * @return if the location is reached + */ + bool travel(GPSCoordinate goal, double dist_tolerance = 1.0, double rot_tolerance = (0.174533 / 2)) // 5 deg + { + Vector2d dif = goal - gps->location(); + if (dif.norm() < dist_tolerance) + { + stop(); + return true; } - return out; - } + // Find a way to move along dif with our motors + + double heading = get_heading(); + double goal_heading = atan2(dif[1], dif[0]); + double angle_error = atan2( + sin(goal_heading - heading), + cos(goal_heading - heading)); + + std::cout << "Heading: " << heading << "goal" << goal_heading << "dif: " << angle_error << std::endl; + if (angle_error < rot_tolerance) + { + // move forward + forwards(); + } + else + { + // rotate self + rotate((angle_error < 0), 0.3); + } + return false; + } /** * Starts neptune. This method will functionally take over the thread it is called from */ - void start(){ + void start() + { Neptune::instance = this; gps->await_lock(5, 500); std::cout << "Arming motors" << std::endl; @@ -53,7 +191,61 @@ class Neptune { { std::this_thread::sleep_for(this->state->tick()); } - + } + // Rotates + void rotate(bool clockwise, double max = 0.75) + { + if (clockwise) + { + // rights on backwards, lefts on forward + front_left->setSpeed(max); + front_right->setSpeed(-max); + back_left->setSpeed(max); + back_right->setSpeed(-max); + } + else + { + // inverse + front_left->setSpeed(-max); + front_right->setSpeed(max); + back_left->setSpeed(-max); + back_right->setSpeed(max); + } } +private: + // Helper to build a matrix of the motors thrust vectors, rotate by the heading + Matrix motor_mat() + { + Matrix out; + std::cout << "heading" << get_heading() << std::endl; + Eigen::Rotation2D rot(get_heading()); + for (int m = static_cast(MotorLocation::FrontRight); m <= static_cast(MotorLocation::BackLeft); m++) + { + out.col(m) = rot * thrust_vector(static_cast(m)); // rotate by heading + } + return out; + } + // Helper to activate motors based on a vector + bool set_motors(Vector4d v) + { + for (int m = static_cast(MotorLocation::FrontRight); m <= static_cast(MotorLocation::BackLeft); m++) + { + all_motors[m]->setSpeed(v[m]); + } + return true; // TODO error checking? + } + // Wraps an angle to [0, 2pi] + double normalizeToPositiveAngle(double angle) + { + while (angle < 0) + { + angle += M_PI * 2; + } + while (angle > 2 * M_PI) + { + angle -= M_PI * 2; + } + return angle; + } }; diff --git a/src/sim_main.cpp b/src/sim_main.cpp index 0fd5f18..36d32a6 100644 --- a/src/sim_main.cpp +++ b/src/sim_main.cpp @@ -5,24 +5,30 @@ #include "types/MotorLocation.hpp" #include "neptune.hpp" #include "states/navigate.hpp" -int main() { +#include "states/menu.hpp" +#include +#include +#include + +int main() + +{ + sim_connection = new httplib::Client(SIM_ADDR); GPS *g = new Simulated_GPS(); - Motor* front_left = new SimulatedMotor(MotorLocation::FrontLeft, 0 ,50); - Motor* front_right = new SimulatedMotor(MotorLocation::FrontRight, 0 ,50); - Motor* back_left = new SimulatedMotor(MotorLocation::BackLeft, 0, 50); - Motor* back_right = new SimulatedMotor(MotorLocation::BackRight, 0, 50); - Neptune* neptune = new Neptune(); + Motor *front_left = new SimulatedMotor(MotorLocation::FrontLeft, 0, 50); + Motor *front_right = new SimulatedMotor(MotorLocation::FrontRight, 0, 50); + Motor *back_left = new SimulatedMotor(MotorLocation::BackLeft, 0, 50); + Motor *back_right = new SimulatedMotor(MotorLocation::BackRight, 0, 50); + Neptune *neptune = new Neptune(); neptune->gps = g; neptune->back_left = back_left; neptune->back_right = back_right; neptune->front_left = front_left; neptune->front_right = front_right; - GPSCoordinate goal = GPSCoordinate{ - 0, - 10 - }; -neptune->state = new Navigate(goal); + neptune->compass = new Compass(); + neptune->state = new Menu(); neptune->start(); + return 0; } diff --git a/src/states/menu.hpp b/src/states/menu.hpp new file mode 100644 index 0000000..62b929a --- /dev/null +++ b/src/states/menu.hpp @@ -0,0 +1,75 @@ +#pragma once +#include "state.hpp" +#include "../../neptune.hpp" +#include +#include +#include +#include + +/** + * Rep + */ +class Menu : public State +{ +public: + bool finished; + int wait = 500; + double speed = 0.5; + Menu() + { + } + + std::chrono::milliseconds tick() override + { + Neptune::instance->stop(); + std::cout << "Current duration(ms): " << wait << "Current Speed:" << speed << "\nOptions:\n[0] Rotate Clockwise \n[1] Rotate Counter-Clockwise \n[2] Move Forwards \n[3] Move Backwards \n[4]Set Duration \n[5]Change Speed \n[6]Read Sensors" << std::endl; + std::cout << ":"; + int out; + while (!(std::cin >> out) && (0 <= out) && (out <= 6)) + { + std::cin.clear(); + std::cin.ignore(std::numeric_limits::max(), '\n'); + std::cout << "Invalid input. Please enter a number: "; + } + std::cout << "running:" << out << std::endl; + switch (out) + { + case 0: + Neptune::instance->rotate(true, speed); + return std::chrono::milliseconds(wait); + case 1: + Neptune::instance->rotate(true, speed); + return std::chrono::milliseconds(wait); + case 2: + Neptune::instance->forwards(speed); + return std::chrono::milliseconds(wait); + case 3: + Neptune::instance->backwards(speed); + return std::chrono::milliseconds(wait); + case 4: + int wait; + while (!(std::cin >> wait)) + { + std::cin.clear(); + std::cin.ignore(std::numeric_limits::max(), '\n'); + std::cout << "Invalid input. Please enter a number: "; + } + this->wait = wait; + return std::chrono::milliseconds(wait); + case 5: + double speed; + while (!(std::cin >> speed) && (0.01 <= speed) && (speed <= 1)) + { + std::cin.clear(); + std::cin.ignore(std::numeric_limits::max(), '\n'); + std::cout << "Invalid input. Please enter a number: "; + } + this->speed = speed; + return std::chrono::milliseconds(wait); + case 6: + Neptune::instance->dispData(); + return std::chrono::milliseconds(wait); + } + return std::chrono::milliseconds(wait); + } +}; diff --git a/src/states/navigate.hpp b/src/states/navigate.hpp index 066ec3c..206eb23 100644 --- a/src/states/navigate.hpp +++ b/src/states/navigate.hpp @@ -5,92 +5,20 @@ #include #include #include -/**Tries to solve with no negative inputs, so motors don't fire backwards - * This is vibe coded slop. In the future, we should add a linear algebra lib instead of using this - */ -std::optional> solveNonNegative(const std::array, 2>& mat) -{ - constexpr int VARS = 4; - constexpr double EPS = 1e-9; - - // Try every pair of "basic" variables; the remaining two are fixed at 0. - for (int p = 0; p < VARS; ++p) { - for (int q = p + 1; q < VARS; ++q) { - - // Extract the 2x2 sub-system for columns p and q. - const double a00 = mat[0][p], a01 = mat[0][q], b0 = mat[0][4]; - const double a10 = mat[1][p], a11 = mat[1][q], b1 = mat[1][4]; - - const double det = a00 * a11 - a01 * a10; - if (std::abs(det) < EPS) - continue; // columns are linearly dependent — skip - - // Cramer's Rule - const double xp = (b0 * a11 - b1 * a01) / det; - const double xq = (a00 * b1 - a10 * b0) / det; - - if (xp < -EPS || xq < -EPS) - continue; // a basic variable went negative — skip - - // Valid non-negative basic feasible solution found. - std::array solution{}; // nosn-basics default to 0 - solution[p] = std::max(0.0, xp); // clamp -0 artefacts - solution[q] = std::max(0.0, xq); - return solution; - } - } - - return std::nullopt; // all 6 bases checked, none feasible -} - - -class Navigate : public State { - public: +class Navigate : public State +{ +public: bool finished; GPSCoordinate goal; - Navigate(GPSCoordinate goal) : goal(goal){ - + Navigate(GPSCoordinate goal) : goal(goal) + { } - - std::chrono::milliseconds tick() override { - double current_heading = Neptune::instance->get_heading(); - GPSCoordinate* loc = Neptune::instance->gps->location(); - GPSCoordinate goal_dif = goal - *loc; - double mag = pow( goal_dif.latitude * goal_dif.latitude + goal_dif.longitude * goal_dif.longitude, 0.5); - std::cout << "Dist:" << mag << std::endl; - if(mag < 0.1){ - //super close, get a random new cord - goal.latitude = ((rand() % 30) - 15); - goal.longitude = ((rand() % 30) - 15); - } - goal_dif.latitude /= mag; - goal_dif.longitude /= mag; - /** - * We know we want to translate along goal_dif - * We can form goal_dif by combining the vectors of each motor's thrust vectors - * Thus, our problem is to form a linear combination of the goal vector through the 4 thrusters, and take one solution - * ex an array - * m1x m2x m3x m4x gx - * and for y - */ - std::array, 2> mat = Neptune::instance->GetMotorVectorMatrice(); - mat[0][4] = goal_dif.latitude; - mat[1][4] = goal_dif.longitude; - std::optional> out = solveNonNegative(mat); - std::cout << goal << *loc << goal_dif << std::endl; - if(out.has_value()){ - for(int i = 0; i < out.value().size(); i ++) { - std::cout << i << ":" << out.value()[i] << std::endl; - //TODO normalize to some constant range based on desired activation - Neptune::instance->all_motors[i]->setSpeed(out.value()[i]); - } - } - else { - std::cout << "Failed to solve" << std::endl; - } - delete loc; - return std::chrono::milliseconds(1000); + std::chrono::milliseconds tick() override + { + //std::cout << Neptune::instance->get_heading() << std::endl; + Neptune::instance->travel(goal); + return std::chrono::milliseconds(50); } }; diff --git a/src/types/GPSCoordinate.hpp b/src/types/GPSCoordinate.hpp index 5ca6c4d..5a58ada 100644 --- a/src/types/GPSCoordinate.hpp +++ b/src/types/GPSCoordinate.hpp @@ -1,7 +1,9 @@ #pragma once #include +#include +using namespace Eigen; struct GPSCoordinate { double latitude; double longitude; @@ -39,8 +41,8 @@ inline GPSCoordinate operator+(GPSCoordinate const& lhs, GPSCoordinate const& rh return {lhs.latitude + rhs.latitude, lhs.longitude + rhs.longitude}; } -inline GPSCoordinate operator-(GPSCoordinate const& lhs, GPSCoordinate const& rhs) { - return {lhs.latitude - rhs.latitude, lhs.longitude - rhs.longitude}; +inline Vector2d operator-(GPSCoordinate const& lhs, GPSCoordinate const& rhs) { + return Vector2d{lhs.latitude - rhs.latitude, lhs.longitude - rhs.longitude}; } inline GPSCoordinate operator*(GPSCoordinate const& coord, double scalar) { diff --git a/src/types/MotorLocation.hpp b/src/types/MotorLocation.hpp index 4767321..c2879c3 100644 --- a/src/types/MotorLocation.hpp +++ b/src/types/MotorLocation.hpp @@ -1,22 +1,25 @@ #pragma once #include "GPSCoordinate.hpp" #include + +#include +using namespace Eigen; + +//IMPORTANT: If the robot is placed facing north, the front motors should be facing north enum MotorLocation { FrontRight, FrontLeft, BackRight, BackLeft }; constexpr double SQRT2 = 1.41421356237309504880; -//TODO make sure these stay up to date, and maybe consider a seperate point class that isn't a GPScoord -GPSCoordinate thrust_vector(MotorLocation m){ +//TODO make sure these stay up to date +Vector2d thrust_vector(MotorLocation m){ switch(m){ case FrontLeft: return {0.5 * SQRT2, 0.5 * SQRT2}; case FrontRight: - return {-0.5* SQRT2, 0.5* SQRT2}; + return {0.5* SQRT2, -0.5* SQRT2}; case BackRight: - return {-0.5* SQRT2, -0.5* SQRT2}; + return {-0.5* SQRT2, 0.5* SQRT2}; case BackLeft: - return {0.5* SQRT2, -0.5* SQRT2}; + return {-0.5* SQRT2, -0.5* SQRT2}; } throw std::logic_error("Invalid type"); } -// 0 - lat -// 1 - long From d010f7ebe9ac9c73c3e20295e16933b32f4b1037 Mon Sep 17 00:00:00 2001 From: WilyHyperion Date: Thu, 28 May 2026 03:30:30 +0000 Subject: [PATCH 10/10] fix motors not stopping on ctrl c and wrong turn dirc --- src/main.cpp | 6 ++++++ src/neptune.hpp | 2 +- src/states/menu.hpp | 11 ++++++----- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index ff75627..95363c4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -11,6 +11,11 @@ #include #include #include +#include +void handle_quit(int signum){ + Neptune::instance->stop(); + exit(0); +} int main(int argc, char **argv) { CLI::App app; @@ -42,5 +47,6 @@ int main(int argc, char **argv) neptune->front_right = front_right; neptune->compass = new Compass(); neptune->state = new Menu(); + std::signal(SIGINT,handle_quit); neptune->start(); } diff --git a/src/neptune.hpp b/src/neptune.hpp index bd839e3..3da81d4 100644 --- a/src/neptune.hpp +++ b/src/neptune.hpp @@ -175,7 +175,7 @@ class Neptune void start() { Neptune::instance = this; - gps->await_lock(5, 500); + //gps->await_lock(5, 500); std::cout << "Arming motors" << std::endl; front_left->armMotor(); front_right->armMotor(); diff --git a/src/states/menu.hpp b/src/states/menu.hpp index 62b929a..531e81c 100644 --- a/src/states/menu.hpp +++ b/src/states/menu.hpp @@ -31,14 +31,13 @@ class Menu : public State std::cin.ignore(std::numeric_limits::max(), '\n'); std::cout << "Invalid input. Please enter a number: "; } - std::cout << "running:" << out << std::endl; switch (out) { case 0: Neptune::instance->rotate(true, speed); return std::chrono::milliseconds(wait); case 1: - Neptune::instance->rotate(true, speed); + Neptune::instance->rotate(false, speed); return std::chrono::milliseconds(wait); case 2: Neptune::instance->forwards(speed); @@ -48,6 +47,7 @@ class Menu : public State return std::chrono::milliseconds(wait); case 4: int wait; + std::cout << "New duration(ms):"; while (!(std::cin >> wait)) { std::cin.clear(); @@ -55,9 +55,10 @@ class Menu : public State std::cout << "Invalid input. Please enter a number: "; } this->wait = wait; - return std::chrono::milliseconds(wait); + return std::chrono::milliseconds(10); case 5: double speed; + std::cout << "New speed(0.01 to 1):"; while (!(std::cin >> speed) && (0.01 <= speed) && (speed <= 1)) { std::cin.clear(); @@ -65,10 +66,10 @@ class Menu : public State std::cout << "Invalid input. Please enter a number: "; } this->speed = speed; - return std::chrono::milliseconds(wait); + return std::chrono::milliseconds(10); case 6: Neptune::instance->dispData(); - return std::chrono::milliseconds(wait); + return std::chrono::milliseconds(10); } return std::chrono::milliseconds(wait); }