From 89babb2a795314794bb5508f5017cd973954f221 Mon Sep 17 00:00:00 2001 From: Amal Nanavati Date: Tue, 21 May 2024 10:37:49 -0700 Subject: [PATCH 1/5] Add pre-commit hooks --- .github/workflows/pre-commit.yaml | 32 +++++++++++++++++ .pre-commit-config.yaml | 59 +++++++++++++++++++++++++++++++ README.md | 6 ++++ 3 files changed, 97 insertions(+) create mode 100644 .github/workflows/pre-commit.yaml create mode 100644 .pre-commit-config.yaml diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml new file mode 100644 index 00000000..a45b680e --- /dev/null +++ b/.github/workflows/pre-commit.yaml @@ -0,0 +1,32 @@ +name: pre-commit + +on: + push: + branches: + - main + - master + pull_request: + workflow_dispatch: + +concurrency: + group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" + cancel-in-progress: true + +env: + PYTHON_VERSION: "3.10" + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + ## Run pre-commit and try to apply fixes + - name: Run pre-commit + uses: pre-commit/action@v3.0.1 + - name: Apply fixes from pre-commit + uses: pre-commit-ci/lite-action@v1.0.2 + if: always() diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..c2963743 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,59 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: check-added-large-files + - id: check-case-conflict + - id: check-executables-have-shebangs + - id: check-merge-conflict + - id: check-shebang-scripts-are-executable + - id: check-symlinks + - id: check-xml + - id: check-yaml + - id: debug-statements + - id: destroyed-symlinks + - id: detect-private-key + - id: end-of-file-fixer + - id: mixed-line-ending + - id: requirements-txt-fixer + - id: trailing-whitespace + + # isort auto-sorts Python imports + - repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + args: ["--profile", "black"] + + # Black formats Python code + - repo: https://github.com/psf/black + rev: 23.1.0 + hooks: + - id: black + + # Beautysh formats Bash scripts + - repo: https://github.com/lovesegfault/beautysh + rev: v6.2.1 + hooks: + - id: beautysh + + # Mdformat formats Markdown files + - repo: https://github.com/executablebooks/mdformat + rev: 0.7.16 + hooks: + - id: mdformat + + # Codespell checks the code for common misspellings + - repo: https://github.com/codespell-project/codespell + rev: v2.2.4 + hooks: + - id: codespell + + # Prettier formats JS(X), TS(X), HTML, CSS, etc. files + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v4.0.0-alpha.8 + hooks: + - id: prettier diff --git a/README.md b/README.md index 30e1cac9..c821462f 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,12 @@ The action modes can be selected in the dropdown in the top-left corner of the i - **Press-Release**: Stretch will move while you are pressing and holding the button and will stop when you release. - **Click-Click**: Stretch will start moving when you click and will stop when you click again. You can also stop Stretch by moving the cursor outside the button you clicked. +# Contributing +- This repository uses pre-commit hooks to enforce consistent formatting and style. + - Install pre-commit: `python3 -m pip install pre-commit` + - Install the hooks locally: `cd` to the top-level of this repository and run `pre-commit install`. + - Moving forward, pre-commit hooks will run before you create any commit. + # Troubleshooting TODO From 7b3a9e4cac548dcf70caae1965413760c9415f81 Mon Sep 17 00:00:00 2001 From: Amal Nanavati Date: Tue, 21 May 2024 10:38:12 -0700 Subject: [PATCH 2/5] Add pull request template --- .github/pull_request_template.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..905c6585 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,18 @@ +# Description + +\[TODO: describe, in-detail, what issue this PR addresses and how it addresses it. Link to relevant Github Issues.\] + +# Testing procedure + +\[TODO: describe, in-detail, how you tested this. The procedure must be detailed enough for the reviewer(s) to recreate it.\] + +# Before opening a pull request + +From the top-level of this repository, run: + +- \[ \] `pre-commit run --all-files` +- \[ \] `pylint --recursive=y --rcfile=.pylintrc .`. All warnings but `fixme` must be addressed. + +# To merge + +- \[ \] `Squash & Merge` \ No newline at end of file From eb7c20764ed00b254431dc4dd97f4735b3f33eda Mon Sep 17 00:00:00 2001 From: Amal Nanavati Date: Tue, 21 May 2024 11:02:21 -0700 Subject: [PATCH 3/5] Format pre-commit and pull request templates --- .github/pull_request_template.md | 2 +- .pre-commit-config.yaml | 4 ++++ .prettierignore | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .prettierignore diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 905c6585..acf22a8c 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -15,4 +15,4 @@ From the top-level of this repository, run: # To merge -- \[ \] `Squash & Merge` \ No newline at end of file +- \[ \] `Squash & Merge` diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c2963743..47018106 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -51,6 +51,10 @@ repos: rev: v2.2.4 hooks: - id: codespell + exclude: > + (?x)^( + .*\.svg + )$ # Prettier formats JS(X), TS(X), HTML, CSS, etc. files - repo: https://github.com/pre-commit/mirrors-prettier diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..e3604eec --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +# Prettier should not reformat any markdown files +*.md From 11366b46dc3135d25a07fa2b9782b961df6cda65 Mon Sep 17 00:00:00 2001 From: Amal Nanavati Date: Tue, 21 May 2024 11:11:21 -0700 Subject: [PATCH 4/5] Ran pre-commit hooks on all files --- .gitignore | 2 +- LICENSE.md | 2 +- README.md | 65 +- WEBRTC_PROJECT_LICENSE.md | 4 +- config/configure_video_streams_params.yaml | 145 +- documentation/tutorial.md | 52 +- launch/gripper_camera.launch.py | 66 +- launch/multi_camera.launch.py | 251 ++- launch/navigation_camera.launch.py | 68 +- launch/web_interface.launch.py | 431 +++-- launch_interface.sh | 2 +- maps/map.yaml | 6 +- nodes/configure_video_streams.py | 278 ++-- nodes/gripper_camera.py | 96 +- nodes/navigation_camera.py | 97 +- nodes/old_navigation_camera.py | 93 +- scripts/crop_map.py | 12 +- server.js | 151 +- src/pages/operator/README.md | 50 +- src/pages/operator/css/Alert.css | 2 +- src/pages/operator/css/BatteryGuage.css | 56 +- src/pages/operator/css/ButtonGrid.css | 67 +- src/pages/operator/css/ButtonPad.css | 130 +- src/pages/operator/css/CameraView.css | 326 ++-- src/pages/operator/css/CustomizeButton.css | 20 +- src/pages/operator/css/DropZone.css | 72 +- src/pages/operator/css/LayoutArea.css | 48 +- src/pages/operator/css/Map.css | 211 +-- src/pages/operator/css/MobileOperator.css | 300 ++-- src/pages/operator/css/MovementRecorder.css | 74 +- src/pages/operator/css/Operator.css | 240 +-- src/pages/operator/css/Panel.css | 92 +- src/pages/operator/css/PoseLibrary.css | 32 +- src/pages/operator/css/PredictiveDisplay.css | 20 +- src/pages/operator/css/RadioGroup.css | 83 +- src/pages/operator/css/RunStopButton.css | 27 +- src/pages/operator/css/Sidebar.css | 209 ++- src/pages/operator/css/SimpleCameraView.css | 157 +- src/pages/operator/css/SpeedControl.css | 20 +- src/pages/operator/css/TabGroup.css | 94 +- src/pages/operator/css/Tooltip.css | 138 +- src/pages/operator/css/VirtualJoystick.css | 44 +- src/pages/operator/css/basic_components.css | 340 ++-- src/pages/operator/css/index.css | 170 +- src/pages/operator/html/index.html | 30 +- src/pages/operator/icons/Arm_In.svg | 10 +- src/pages/operator/icons/Arm_Out.svg | 4 +- src/pages/operator/icons/Pitch_Down.svg | 2 +- src/pages/operator/icons/Pitch_Up.svg | 2 +- src/pages/operator/icons/Yaw_Left.svg | 2 +- src/pages/operator/icons/Yaw_Right.svg | 2 +- src/pages/operator/tsx/MobileOperator.tsx | 553 ++++--- src/pages/operator/tsx/Operator.tsx | 522 +++--- src/pages/operator/tsx/README.md | 46 +- .../tsx/basic_components/AccordionSelect.tsx | 90 +- .../operator/tsx/basic_components/Alert.tsx | 42 +- .../basic_components/CheckToggleButton.tsx | 57 +- .../tsx/basic_components/Dropdown.tsx | 121 +- .../tsx/basic_components/PopupModal.tsx | 148 +- .../tsx/basic_components/RadioGroup.tsx | 141 +- .../tsx/basic_components/TabGroup.tsx | 101 +- src/pages/operator/tsx/create_component.md | 9 +- src/pages/operator/tsx/customization_logic.md | 113 +- .../tsx/default_layouts/SIMPLE_LAYOUT.tsx | 189 +-- .../BatteryVoltageFunctionProvider.tsx | 92 +- .../ButtonFunctionProvider.tsx | 749 +++++---- .../function_providers/FunctionProvider.tsx | 142 +- .../MapFunctionProvider.tsx | 65 +- .../MovementRecorderFunctionProvider.tsx | 185 ++- .../PredictiveDisplayFunctionProvider.tsx | 144 +- .../RunStopFunctionProvider.tsx | 64 +- .../UnderMapFunctionProvider.tsx | 288 ++-- .../UnderVideoFunctionProvider.tsx | 182 ++- src/pages/operator/tsx/index.tsx | 442 +++--- .../tsx/layout_components/ButtonGrid.tsx | 191 ++- .../tsx/layout_components/ButtonPad.tsx | 465 +++--- .../tsx/layout_components/CameraView.tsx | 1198 +++++++------- .../tsx/layout_components/ComponentList.tsx | 106 +- .../CustomizableComponent.tsx | 123 +- .../tsx/layout_components/DropZone.tsx | 422 ++--- .../operator/tsx/layout_components/Map.tsx | 714 +++++---- .../layout_components/MovementRecorder.tsx | 384 +++-- .../operator/tsx/layout_components/Panel.tsx | 417 ++--- .../layout_components/PredictiveDisplay.tsx | 346 ++-- .../layout_components/SimpleCameraView.tsx | 317 ++-- .../tsx/layout_components/VirtualJoystick.tsx | 204 +-- .../tsx/static_components/BatteryGauge.tsx | 26 +- .../operator/tsx/static_components/Canvas.tsx | 94 +- .../tsx/static_components/CustomizeButton.tsx | 41 +- .../tsx/static_components/LayoutArea.tsx | 100 +- .../tsx/static_components/OccupancyGrid.tsx | 652 ++++---- .../tsx/static_components/RunStop.tsx | 38 +- .../tsx/static_components/Sidebar.tsx | 919 +++++------ .../tsx/static_components/SpeedControl.tsx | 68 +- .../operator/tsx/static_components/Swipe.tsx | 71 +- .../tsx/static_components/Tooltip.tsx | 26 +- .../FirebaseStorageHandler.tsx | 458 +++--- .../storage_handler/LocalStorageHandler.tsx | 299 ++-- .../operator/tsx/storage_handler/README.md | 11 +- .../tsx/storage_handler/StorageHandler.tsx | 287 ++-- .../operator/tsx/utils/aruco_markers_dict.tsx | 342 ++-- .../tsx/utils/component_definitions.tsx | 210 +-- .../operator/tsx/utils/layout_helpers.tsx | 276 ++-- src/pages/operator/tsx/utils/svg.tsx | 671 ++++---- src/pages/robot/css/index.css | 10 +- src/pages/robot/html/index.html | 14 +- src/pages/robot/tsx/index.tsx | 396 ++--- src/pages/robot/tsx/robot.tsx | 1402 +++++++++-------- src/pages/robot/tsx/videostreams.tsx | 173 +- src/shared/commands.tsx | 84 +- src/shared/remoterobot.tsx | 627 ++++---- src/shared/util.tsx | 516 +++--- src/shared/webrtcconnections.tsx | 617 ++++---- start_robot_browser.js | 90 +- stop_interface.sh | 3 + webpack.config.js | 168 +- 116 files changed, 12395 insertions(+), 10561 deletions(-) mode change 100755 => 100644 maps/map.yaml diff --git a/.gitignore b/.gitignore index 61f1b644..e7a79a3a 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,4 @@ mkcert* dist/* -outcomes/* \ No newline at end of file +outcomes/* diff --git a/LICENSE.md b/LICENSE.md index 8bd47ce5..8ea4a526 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,4 @@ -From https://github.com/hello-robot/stretch_web_interface/blob/master/LICENSE.md +From https://github.com/hello-robot/stretch_web_interface/blob/master/LICENSE.md The following license applies to the contents of this directory created by Hello Robot Inc. (the "Contents"), but does not cover materials from other sources. This software is intended for use with the Stretch RE1 mobile manipulator, which is a robot produced and sold by Hello Robot Inc. diff --git a/README.md b/README.md index c821462f..7f9cc91a 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ # Overview + This interface enables a user to remotely teleoperate a Stretch robot through a web browser. This website can be set up to teleoperate the robot remotely from anywhere in the world with an internet connection, or simply eyes-off teleop from the next room on a local network. The codebase is built on ROS2, WebRTC, Nav2, and TypeScript. # Setup -The interface is compatible with the Stretch RE1, RE2 and SE3. It currently only supports Ubuntu 22.04 and ROS2 Humble. Upgrade your operating system if necessary ([instructions]()) and create a the Stretch ROS2 Humble workspace ([instructions]()). This will install all package dependencies and install the web teleop interface. + +The interface is compatible with the Stretch RE1, RE2 and SE3. It currently only supports Ubuntu 22.04 and ROS2 Humble. Upgrade your operating system if necessary ([instructions](<>)) and create a the Stretch ROS2 Humble workspace ([instructions](<>)). This will install all package dependencies and install the web teleop interface. ## Installing Beta Teleop Cameras @@ -13,6 +15,7 @@ REx_camera_set_symlink.py --list ``` You should see an output similar to: + ``` For use with S T R E T C H (R) from Hello Robot Inc. --------------------------------------------------------------------- @@ -34,16 +37,18 @@ Note, it is important to make sure the cameras are not plugged in at the same ti REx_camera_set_symlink.py --port --symlink ``` -Replace `` with the 0th element in the ports list for the `USB CAMERA` ouputted by `REx_camera_set_symlink.py --list` command. In the example above, that would be `/dev/video6`. Replace `` with `hello-navigation-camera` or `hello-gripper-camera` for the navigation and gripper camera respectively. For example, if we were setting up the navigation camera the command would look similar to: +Replace `` with the 0th element in the ports list for the `USB CAMERA` outputted by `REx_camera_set_symlink.py --list` command. In the example above, that would be `/dev/video6`. Replace `` with `hello-navigation-camera` or `hello-gripper-camera` for the navigation and gripper camera respectively. For example, if we were setting up the navigation camera the command would look similar to: ``` REx_camera_set_symlink.py --port /dev/video6 --symlink hello-navigation-camera ``` Repeat this process for both cameras, then run: + ``` ll /dev/hello-* ``` + and verify the symlinks are setup correctly. # Launching the Interface @@ -55,25 +60,29 @@ colcon_cd stretch_web_teleop ``` Next, launch the interface: + ``` ./launch_interface ``` If you'd like to launch the interface with a map run: + ``` ./launch_interface -m maps/.yaml ``` In the terminal, you will see output similar to: + ``` Visit the URL(s) below to see the web interface: https://localhost/operator https://192.168.1.14/operator ``` -Look for a URL like `https:///operator`. Visit this URL in a web browser on your personal laptop or desktop to see the web interface. Ensure your personal computer is connected to the same network as Stretch. You might see a warning that says "Your connection is not private". If you do, click `Advanced` and `Proceed`. +Look for a URL like `https:///operator`. Visit this URL in a web browser on your personal laptop or desktop to see the web interface. Ensure your personal computer is connected to the same network as Stretch. You might see a warning that says "Your connection is not private". If you do, click `Advanced` and `Proceed`. Once you're done with the interface, close the browser and run: + ``` ./stop_interface.sh ``` @@ -84,7 +93,7 @@ Once you're done with the interface, close the browser and run: **WARNING: This is prototype code and there are security issues. Deploy this code at your own risk.** -We recommend setting up the interface for remote use using [ngrok](https://ngrok.com/docs/what-is-ngrok/). First, create an account with `ngrok` and follow the Linux installation instructions in the `Setup & Installation` tab in your ngrok account dashboard. +We recommend setting up the interface for remote use using [ngrok](https://ngrok.com/docs/what-is-ngrok/). First, create an account with `ngrok` and follow the Linux installation instructions in the `Setup & Installation` tab in your ngrok account dashboard. Navigate to the `Domains` tab and click `Create Domain`. ngrok will automatically generate a domain name for your free account. You will see a domain similar to `deciding-hornet-purely.ngrok-free.app`. Follow the interface launch instructions and then start the ngrok tunnel by running the following command (replace `` with your account's domain and `user:password` with a secure username and password): @@ -92,15 +101,18 @@ Navigate to the `Domains` tab and click `Create Domain`. ngrok will automaticall ngrok http --basic-auth="user:password" --domain= 443 ``` -In your browser, open `https:///operator` to see the interface. You will then be prompted to enter the appropiate username and password. Note, anyone in the world with internet access can open this link. +In your browser, open `https:///operator` to see the interface. You will then be prompted to enter the appropriate username and password. Note, anyone in the world with internet access can open this link. + +## Storing Ngrok Tunnel Configuration -## Storing Ngrok Tunnel Configuration To store this configuration, open the ngrok config file: + ``` ngrok config edit ``` -Add the following configuration to the file. Make sure to update ``, ``, and `admin:password` with the appropiate values. +Add the following configuration to the file. Make sure to update ``, ``, and `admin:password` with the appropriate values. + ``` authtoken: version: 2 @@ -109,13 +121,13 @@ tunnels: proto: http domain: addr: 443 - basic_auth: + basic_auth: - "admin:password" host_header: rewrite inspect: true ``` -Now run `ngrok start stretch-web-teleop` to start the tunnel and navigate to `https:///operator`. You will then be prompted to enter the appropiate username and password. +Now run `ngrok start stretch-web-teleop` to start the tunnel and navigate to `https:///operator`. You will then be prompted to enter the appropriate username and password. # Usage Guide @@ -126,37 +138,48 @@ The web interface currently has a variety of control modes, displays and customi There are three panels. The `Camera Views` panel contains the wide angle and gripper camera views. The second panel has three tabs: (1) `Base`, (2) `Wrist & Gripper`, and (3) `Arm & Lift`. Each of these tabs contains a button pad for controlling the respective joints. The `Safety` panel contains the run stop and battery gauge. The header contains a drop down for three action modes, the speed controls (`Slowest`, `Slow`, `Medium`, `Fast`, and `Fastest`) and a button to enable the customization mode. ## Wide-Angle Camera View + The wide angle camera is attached to the robot's head which can pan and tilt. There are four buttons bordering the camera feed the will pan and tilt the camera. +

### Quick Look -There are three built-in quick look options: `Look Ahead`, `Look at Base` and `Look at Gripper`. + +There are three built-in quick look options: `Look Ahead`, `Look at Base` and `Look at Gripper`. +

### Follow Gripper + The `follow gripper` button will automatically pan/tilt the head to focus on the gripper as the arm is moved. This is can be useful when trying to pick something up. +

### Predictive Display -The 'predictive display' mode will overlay a trajectory over the video stream that Stretch will follow. Stretch's speed and heading will depend on the length and curve of the trajectory. Stretch will move faster the longer the trajectory is and slower the shorter the trajectory is. The trajectory will turn red when you click and the robot is moving. The robot will rotate in place when you click on the base and will move backwards when you click behind the base. In the `press-release` and `click-click` [action modes](#action-modes) you can move the cursor to update the trajectory while the robot is moving. Additionally, you can scale the speed by selecting one of the speed controls. + +The 'predictive display' mode will overlay a trajectory over the video stream that Stretch will follow. Stretch's speed and heading will depend on the length and curve of the trajectory. Stretch will move faster the longer the trajectory is and slower the shorter the trajectory is. The trajectory will turn red when you click and the robot is moving. The robot will rotate in place when you click on the base and will move backwards when you click behind the base. In the `press-release` and `click-click` [action modes](#action-modes) you can move the cursor to update the trajectory while the robot is moving. Additionally, you can scale the speed by selecting one of the speed controls. +

## Gripper Camera + There are two quick actions for the gripper camera view: (1) `center wrist` and (2) `stow wrist`. Center wrist will turn the wrist out and align it with the arm. Stow wrist will rotate the wrist to the stow position. +

## Button Pads -Each button pad controls a different set of joints on the robot. When you click a button the robot will move and the button will highlight blue while the robot is moving. The button will turn red when the respective joint is at its limit. + +Each button pad controls a different set of joints on the robot. When you click a button the robot will move and the button will highlight blue while the robot is moving. The button will turn red when the respective joint is at its limit. @@ -174,6 +197,7 @@ Each button pad controls a different set of joints on the robot. When you click
## Action Modes + The action modes can be selected in the dropdown in the top-left corner of the interface. The action modes provides varying degrees of discrete and continuous control. - **Step Actions**: When you click, Stretch will move a fixed distance based on the selected speed. @@ -181,30 +205,31 @@ The action modes can be selected in the dropdown in the top-left corner of the i - **Click-Click**: Stretch will start moving when you click and will stop when you click again. You can also stop Stretch by moving the cursor outside the button you clicked. # Contributing + - This repository uses pre-commit hooks to enforce consistent formatting and style. - - Install pre-commit: `python3 -m pip install pre-commit` - - Install the hooks locally: `cd` to the top-level of this repository and run `pre-commit install`. - - Moving forward, pre-commit hooks will run before you create any commit. + - Install pre-commit: `python3 -m pip install pre-commit` + - Install the hooks locally: `cd` to the top-level of this repository and run `pre-commit install`. + - Moving forward, pre-commit hooks will run before you create any commit. # Troubleshooting TODO - # Licenses -The following license applies to the contents of this directory written by Vinitha Ranganeni, Noah Ponto, authors associated with the University of Washington, and authors associated with Hello Robot Inc. (the "Contents"). This software is intended for use with Stretch ® mobile manipulators produced and sold by Hello Robot ®. + +The following license applies to the contents of this directory written by Vinitha Ranganeni, Noah Ponto, authors associated with the University of Washington, and authors associated with Hello Robot Inc. (the "Contents"). This software is intended for use with Stretch ® mobile manipulators produced and sold by Hello Robot ®. Copyright 2023 Vinitha Ranganeni, Noah Ponto, the University of Washington, and Hello Robot Inc. The Contents are licensed under the Apache License, Version 2.0 (the "License"). You may not use the Contents except in compliance with the License. You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 +http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, the Contents are distributed under the License are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -============================================================ +\============================================================ -Some of the contents of this directory derive from the following repositories: +Some of the contents of this directory derive from the following repositories: https://github.com/hello-robot/stretch_web_interface diff --git a/WEBRTC_PROJECT_LICENSE.md b/WEBRTC_PROJECT_LICENSE.md index c22e7bfa..0243cb40 100644 --- a/WEBRTC_PROJECT_LICENSE.md +++ b/WEBRTC_PROJECT_LICENSE.md @@ -2,9 +2,9 @@ From https://github.com/hello-robot/stretch_web_interface/blob/master/WEBRTC_PRO The following license covers the original code from which some of the web interface code was derived (e.g., operator_acquire_av.js, robot_acquire_av.js). The original code was released in the following repository, which contains WebRTC example code. -https://github.com/webrtc/samples +https://github.com/webrtc/samples -====================================== +\====================================== Copyright (c) 2014, The WebRTC project authors. All rights reserved. diff --git a/config/configure_video_streams_params.yaml b/config/configure_video_streams_params.yaml index 655ee328..6cc3e050 100644 --- a/config/configure_video_streams_params.yaml +++ b/config/configure_video_streams_params.yaml @@ -1,92 +1,61 @@ { - # Images published to /navigation_camera/image_raw - # Image size is 768 x 1024 - "overhead": { - "fixed": { - # The mask contain params to make a circular mask over the image - # Pass "mask": null to not create mask - "mask": { - "width": 768, - "height": 768, - "radius": null, - "center": null # Pass "center": {"x": __, "y": __} to define center + # Images published to /navigation_camera/image_raw + # Image size is 768 x 1024 + "overhead": { "fixed": { + # The mask contain params to make a circular mask over the image + # Pass "mask": null to not create mask + "mask": { + "width": 768, + "height": 768, + "radius": null, + "center": null, # Pass "center": {"x": __, "y": __} to define center }, - # The crop contain params to crop the image - # x_max - x_min = mask_width and y_max - y_min = mask_height - # Pass "crop": null to not crop image - "crop": { - "x_min": 0, - "x_max": 768, - "y_min": 128, - "y_max": 896 - }, - # Number of degress to rotate video stream clockwise - # Pass "rotate": null to not rotate - "rotate": null, - }, - "wide_angle_cam": { - # The mask contain params to make a circular mask over the image - # Pass "mask": null to not create mask - "mask": null, - # The crop contain params to crop the image - # x_max - x_min = mask_width and y_max - y_min = mask_height - # Pass "crop": null to not crop image - "crop": null, - # Number of degress to rotate video stream clockwise - # Pass "rotate": null to not rotate - # "rotate": null, - "rotate": 'ROTATE_90_COUNTERCLOCKWISE' - } - }, - "realsense": { - "default": { - "mask": null, - "crop": null, - "rotate": 'ROTATE_90_CLOCKWISE' - } - }, - # Images published to /gripper_camera/image_raw - # Image size is 768 x 1024 - "gripper": { - "default": { - # The mask contain params to make a circular mask over the image - # Pass "mask": null to not create mask - "mask": { - "width": 768, - "height": 768, - "radius": null, - "center": null - }, - # The crop contain params to crop the image - # x_max - x_min = mask_width and y_max - y_min = mask_height - # Pass "crop": null to not crop image - "crop": { - "x_min": 0, - "x_max": 768, - "y_min": 128, - "y_max": 896 - }, - "rotate": null - }, - "d405": { - # The mask contain params to make a circular mask over the image - # Pass "mask": null to not create mask - "mask": { - "width": 270, - "height": 270, - "radius": null, - "center": null - }, - # The crop contain params to crop the image - # x_max - x_min = mask_width and y_max - y_min = mask_height - # Pass "crop": null to not crop image - "crop": { - "y_min": 125, - "y_max": 395, - "x_min": 0, - "x_max": 270 - }, - "rotate": null - }, + # The crop contain params to crop the image + # x_max - x_min = mask_width and y_max - y_min = mask_height + # Pass "crop": null to not crop image + "crop": { "x_min": 0, "x_max": 768, "y_min": 128, "y_max": 896 }, + # Number of degrees to rotate video stream clockwise + # Pass "rotate": null to not rotate + "rotate": null, + }, "wide_angle_cam": { + # The mask contain params to make a circular mask over the image + # Pass "mask": null to not create mask + "mask": null, + # The crop contain params to crop the image + # x_max - x_min = mask_width and y_max - y_min = mask_height + # Pass "crop": null to not crop image + "crop": null, + # Number of degrees to rotate video stream clockwise + # Pass "rotate": null to not rotate + # "rotate": null, + "rotate": "ROTATE_90_COUNTERCLOCKWISE", + } }, + "realsense": + { + "default": + { "mask": null, "crop": null, "rotate": "ROTATE_90_CLOCKWISE" }, }, + # Images published to /gripper_camera/image_raw + # Image size is 768 x 1024 + "gripper": { "default": { + # The mask contain params to make a circular mask over the image + # Pass "mask": null to not create mask + "mask": + { "width": 768, "height": 768, "radius": null, "center": null }, + # The crop contain params to crop the image + # x_max - x_min = mask_width and y_max - y_min = mask_height + # Pass "crop": null to not crop image + "crop": { "x_min": 0, "x_max": 768, "y_min": 128, "y_max": 896 }, + "rotate": null, + }, "d405": { + # The mask contain params to make a circular mask over the image + # Pass "mask": null to not create mask + "mask": + { "width": 270, "height": 270, "radius": null, "center": null }, + # The crop contain params to crop the image + # x_max - x_min = mask_width and y_max - y_min = mask_height + # Pass "crop": null to not crop image + "crop": { "y_min": 125, "y_max": 395, "x_min": 0, "x_max": 270 }, + "rotate": null, + } }, } diff --git a/documentation/tutorial.md b/documentation/tutorial.md index edcd4074..26710e97 100644 --- a/documentation/tutorial.md +++ b/documentation/tutorial.md @@ -1,69 +1,82 @@ -# Interace Usage Tutorial +# Interface Usage Tutorial + The web interface currently has a variety of control modes, displays and customization options. This tutorial will go through how to use the various components of the interface. ## Overview of Layout + We have a variety of layouts that you can load. We will start by exploring one of the layouts and how to can customize it to your liking. There are two tabs: `Navigation` and `Manipulation`. The `Navigation` tab has a overhead video stream from a fish eye camera and a video stream from a Realsense camera. The `Manipulation` tab has the same video streams as the `Navigation` tab and an additional video stream from a fish eye camera mounted on the gripper. The overhead fish eye video stream in the `Manipulation` tab is cropped and rotated to focus on the arm and gripper. -| Navigation Tab | Manipulation Tab | -|----------------------------------------- | -------------------------------------------- | +| Navigation Tab | Manipulation Tab | +| ---------------------------------------- | -------------------------------------------- | | ![Navigation Tab](assets/navigation.png) | ![Manipulation Tab](assets/manipulation.png) | ## Button Pads -All the video streams have `button pads` overlaid on the video streams. These buttons control different joints on the robot. Each video stream has its own distinct button pad. To modify the button pads, click the `Customize` button select one of the `button pads` on a video stream and click the trash icon. Then select a new `button pad` that you want to add from the `button pad` drop down in the customize menu. Then select the video stream you want to place it on. +All the video streams have `button pads` overlaid on the video streams. These buttons control different joints on the robot. Each video stream has its own distinct button pad. To modify the button pads, click the `Customize` button select one of the `button pads` on a video stream and click the trash icon. Then select a new `button pad` that you want to add from the `button pad` drop down in the customize menu. Then select the video stream you want to place it on.

-You can also move button pads outside of the video stream by selecting one of the regions to the side of the stream. +You can also move button pads outside of the video stream by selecting one of the regions to the side of the stream.

## Action Modes + There are three different `action modes`: Step Actions, Press-Release and Click-Click. You can select one of 5 discrete speeds: `Slowest`, `Slow`, `Medium`, `Fast`, and `Fastest`. When you click a button, it will turn red to indicate that Stretch is moving. ### Step Actions + When you click, Stretch will move a fixed distance based on the selected speed. +

### Press-Release + Stretch will move while you are pressing and holding the button and will stop when you release. +

### Click-Click + Stretch will start moving when you click and will stop when you click again. You can also stop Stretch by moving the cursor outside the button you clicked. +

-## Predictive Display -The overhead fish eye video stream has an additional mode called `predictive display`. This mode will overlay a trajectory over the video stream that Stretch will follow. Stretch's speed and heading will depend on the length and curve of the trajectory. Stretch will move faster when the trajectory the longer the trajectory is. The trajectory will turn red when the robot is moving. The robot will rotate in place when you click on the base and will move backwards when you click behind the base. In the `press-release` and `click-click` modes you can move the cursor to update the trajectory while the robot is moving. Additionally, you can scale the speed by selecting one of the speed controls. The video below demonstrates using `predictive display` in the `press-release` mode, however, you can use any of the action modes for `predictive display`. +## Predictive Display + +The overhead fish eye video stream has an additional mode called `predictive display`. This mode will overlay a trajectory over the video stream that Stretch will follow. Stretch's speed and heading will depend on the length and curve of the trajectory. Stretch will move faster when the trajectory the longer the trajectory is. The trajectory will turn red when the robot is moving. The robot will rotate in place when you click on the base and will move backwards when you click behind the base. In the `press-release` and `click-click` modes you can move the cursor to update the trajectory while the robot is moving. Additionally, you can scale the speed by selecting one of the speed controls. The video below demonstrates using `predictive display` in the `press-release` mode, however, you can use any of the action modes for `predictive display`.

## Collision and Joint Limits -| The button turns orange when the respectivate joint is in collision | The button turns red when the respectivate joint is a its limit | -|-------------------------------------------------------------------- | --------------------------------------------------------------- | + +| The button turns orange when the respectivate joint is in collision | The button turns red when the respectivate joint is a its limit | +| ------------------------------------------------------------------- | --------------------------------------------------------------- | | ![Collision](assets/collision.png) | ![Joint Limit](assets/limit.png) | ## Changing Camera Views + You can change the camera direction for both the overhead fish eye and realsense cameras: +
    -
  • Switch to Drive View: The overhead fisheye camera will focus on the Stretch's. This is benefitial when driving around.
  • -
  • Switch to Gripper View: The overhead fisheye camera will focus on the Stretch's arm and gripper. This is benefitial when trying to pick something up.
  • -
  • Look at Base: The realsense camera will move to look at Stretch's base. This is benefitial when driving around.
  • -
  • Look at Gripper: The realsense camera will move to look at Stretch's arm. This is benefitial when trying to pick something up.
  • +
  • Switch to Drive View: The overhead fisheye camera will focus on the Stretch's. This is beneficial when driving around.
  • +
  • Switch to Gripper View: The overhead fisheye camera will focus on the Stretch's arm and gripper. This is beneficial when trying to pick something up.
  • +
  • Look at Base: The realsense camera will move to look at Stretch's base. This is beneficial when driving around.
  • +
  • Look at Gripper: The realsense camera will move to look at Stretch's arm. This is beneficial when trying to pick something up.

@@ -71,6 +84,7 @@ You can change the camera direction for both the overhead fish eye and realsense

## Follow Gripper + The `follow gripper` button will automatically move the realsense to focus on the gripper as the arm is moved. This is can be useful when trying to pick something up.

@@ -78,6 +92,7 @@ The `follow gripper` button will automatically move the realsense to focus on th

## Depth Sensing + The `depth sensing` button will highlight points that are in the Stretch's reach in blue. This can be useful when trying to pick something up.

@@ -85,6 +100,7 @@ The `depth sensing` button will highlight points that are in the Stretch's reach

## Pan/Tilt Realsense Camera + You can pan and tilt the realsense camera by clicking the buttons bordering the realsense video stream.

@@ -92,6 +108,7 @@ You can pan and tilt the realsense camera by clicking the buttons bordering the

## Button Grid and Joystick + You can add a `button grid` and `joystick` from the customize menu similar to the way you would add a button pad. The `button grid` is similar to the `button pad` but is separated by the different sets of joints you can control. The `joystick` is similar to how you would drive the robot if you were controlling it with a remote controller's joystick.

@@ -99,9 +116,11 @@ You can add a `button grid` and `joystick` from the customize menu similar to th

## Adding/Deleting Panels and Tabs -A `panel` contains a set of `tabs`; each `tab` contains a `layout` that you can define by adding different components such as camera views, button pads, etc. + +A `panel` contains a set of `tabs`; each `tab` contains a `layout` that you can define by adding different components such as camera views, button pads, etc. ### Panels + You can add and delete panels. When you add a new `panel`, you must enter a name for a `tab` in that `panel`.

@@ -109,6 +128,7 @@ You can add and delete panels. When you add a new `panel`, you must enter a name

### Tabs + You can add and delete tabs. Click the tab with the plus icon to add a `tab`. You will then be prompted to name the `tab` and can add components in that tab. To delete the `tab`, select it and click the trash icon.

@@ -116,6 +136,7 @@ You can add and delete tabs. Click the tab with the plus icon to add a `tab`. Yo

## Voice Commands + You can control Stretch with voice commands. Click the info icon to see the available commands. Click the microphone icon to turn on the microphone. After you say a command, the command will display next to microphone icon. Click the microphone icon to turn off the microphone.

@@ -123,4 +144,5 @@ You can control Stretch with voice commands. Click the info icon to see the avai

## Load/Save Layouts -We have pre-defined layouts that you can load. You can also save your layout and load it later. \ No newline at end of file + +We have pre-defined layouts that you can load. You can also save your layout and load it later. diff --git a/launch/gripper_camera.launch.py b/launch/gripper_camera.launch.py index 8db7a84a..a8f986cf 100644 --- a/launch/gripper_camera.launch.py +++ b/launch/gripper_camera.launch.py @@ -1,37 +1,41 @@ +from launch_ros.actions import Node + import launch from launch import LaunchDescription from launch.actions import DeclareLaunchArgument from launch.substitutions import LaunchConfiguration -from launch_ros.actions import Node + def generate_launch_description(): - return LaunchDescription([ - Node( - package='usb_cam', - executable='usb_cam_node_exe', - name='gripper_camera', - output='screen', - parameters=[ - {'video_device': '/dev/hello-gripper-camera'}, - {'image_width': 1024}, - {'image_height': 768}, - {'framerate': 30.0}, - {'pixel_format': 'yuyv'}, - {'brightness': -40}, - {'contrast': 40}, - {'saturation': 60}, - {'hue': 0}, - {'sharpness': 100}, - {'autoexposure': True}, - {'exposure': 150}, - {'auto_white_balance': False}, - {'white_balance': 4250}, - {'gain': 80}, - {'camera_frame_id': 'gripper_camera'}, - {'camera_name': 'gripper_camera'}, - {'io_method': 'mmap'} - ], - remappings=[('/image_raw', '/gripper_camera/image_raw')] - ), - # You can add more nodes or configurations here if needed. - ]) \ No newline at end of file + return LaunchDescription( + [ + Node( + package="usb_cam", + executable="usb_cam_node_exe", + name="gripper_camera", + output="screen", + parameters=[ + {"video_device": "/dev/hello-gripper-camera"}, + {"image_width": 1024}, + {"image_height": 768}, + {"framerate": 30.0}, + {"pixel_format": "yuyv"}, + {"brightness": -40}, + {"contrast": 40}, + {"saturation": 60}, + {"hue": 0}, + {"sharpness": 100}, + {"autoexposure": True}, + {"exposure": 150}, + {"auto_white_balance": False}, + {"white_balance": 4250}, + {"gain": 80}, + {"camera_frame_id": "gripper_camera"}, + {"camera_name": "gripper_camera"}, + {"io_method": "mmap"}, + ], + remappings=[("/image_raw", "/gripper_camera/image_raw")], + ), + # You can add more nodes or configurations here if needed. + ] + ) diff --git a/launch/multi_camera.launch.py b/launch/multi_camera.launch.py index 37992c26..a3f75ebd 100644 --- a/launch/multi_camera.launch.py +++ b/launch/multi_camera.launch.py @@ -1,88 +1,191 @@ import os + from ament_index_python.packages import get_package_share_directory +from launch_ros.actions import Node, SetRemap + from launch import LaunchDescription -from launch.actions import IncludeLaunchDescription, GroupAction +from launch.actions import DeclareLaunchArgument, GroupAction, IncludeLaunchDescription from launch.launch_description_sources import PythonLaunchDescriptionSource -from launch.actions import DeclareLaunchArgument -from launch_ros.actions import SetRemap, Node -json_path = os.path.join(get_package_share_directory('stretch_core'), 'config', 'HighAccuracyPreset.json') +json_path = os.path.join( + get_package_share_directory("stretch_core"), "config", "HighAccuracyPreset.json" +) + +configurable_parameters = [ + {"name": "camera_namespace1", "default": "", "description": "namespace for camera"}, + {"name": "camera_name1", "default": "camera", "description": "camera unique name"}, + {"name": "device_type1", "default": "d435", "description": "camera unique name"}, + { + "name": "json_file_path1", + "default": json_path, + "description": "allows advanced configuration", + }, + { + "name": "depth_module.depth_profile1", + "default": "424x240x15", + "description": "depth module profile", + }, + { + "name": "depth_module.infra_profile1", + "default": "424x240x15", + "description": "depth module infrared profile", + }, + {"name": "enable_depth1", "default": "true", "description": "enable depth stream"}, + { + "name": "rgb_camera.color_profile1", + "default": "424x240x15", + "description": "color image width", + }, + {"name": "enable_color1", "default": "true", "description": "enable color stream"}, + { + "name": "enable_infra11", + "default": "true", + "description": "enable infra1 stream", + }, + { + "name": "enable_infra21", + "default": "false", + "description": "enable infra2 stream", + }, + {"name": "infra_rgb1", "default": "false", "description": "enable infra2 stream"}, + { + "name": "enable_confidence1", + "default": "false", + "description": "enable depth stream", + }, + {"name": "gyro_fps1", "default": "200", "description": "''"}, + {"name": "accel_fps1", "default": "100", "description": "''"}, + {"name": "enable_gyro1", "default": "true", "description": "''"}, + {"name": "enable_accel1", "default": "true", "description": "''"}, + {"name": "pointcloud.enable1", "default": "true", "description": ""}, + { + "name": "pointcloud.stream_filter1", + "default": "2", + "description": "texture stream for pointcloud", + }, + { + "name": "pointcloud.stream_index_filter1", + "default": "0", + "description": "texture stream index for pointcloud", + }, + {"name": "enable_sync1", "default": "true", "description": "''"}, + {"name": "align_depth.enable1", "default": "true", "description": "''"}, + {"name": "initial_reset1", "default": "true", "description": "''"}, + {"name": "allow_no_texture_points1", "default": "true", "description": "''"}, + {"name": "camera_namespace2", "default": "", "description": "namespace for camera"}, + { + "name": "camera_name2", + "default": "gripper_camera", + "description": "camera unique name", + }, + {"name": "device_type2", "default": "d405", "description": "camera unique name"}, + { + "name": "json_file_path2", + "default": json_path, + "description": "allows advanced configuration", + }, + { + "name": "depth_module.depth_profile2", + "default": "480x270x15", + "description": "depth module profile", + }, + { + "name": "depth_module.enable_auto_exposure2", + "default": "true", + "description": "enable/disable auto exposure for depth image", + }, + {"name": "enable_depth2", "default": "true", "description": "enable depth stream"}, + { + "name": "depth_module.color_profile2", + "default": "480x270x15", + "description": "color image width", + }, + {"name": "enable_color2", "default": "true", "description": "enable color stream"}, + { + "name": "enable_infra12", + "default": "false", + "description": "enable infra1 stream", + }, + { + "name": "enable_infra22", + "default": "false", + "description": "enable infra2 stream", + }, + {"name": "infra_rgb2", "default": "false", "description": "enable infra2 stream"}, + { + "name": "enable_confidence2", + "default": "false", + "description": "enable depth stream", + }, + {"name": "gyro_fps2", "default": "200", "description": "''"}, + {"name": "accel_fps2", "default": "100", "description": "''"}, + {"name": "enable_gyro2", "default": "true", "description": "''"}, + {"name": "enable_accel2", "default": "true", "description": "''"}, + {"name": "pointcloud.enable2", "default": "true", "description": ""}, + { + "name": "pointcloud.stream_filter2", + "default": "2", + "description": "texture stream for pointcloud", + }, + { + "name": "pointcloud.stream_index_filter2", + "default": "0", + "description": "texture stream index for pointcloud", + }, + {"name": "enable_sync2", "default": "true", "description": "''"}, + {"name": "align_depth.enable2", "default": "true", "description": "''"}, + {"name": "initial_reset2", "default": "false", "description": "''"}, + {"name": "allow_no_texture_points2", "default": "true", "description": "''"}, +] -configurable_parameters = [{'name': 'camera_namespace1', 'default': '', 'description': 'namespace for camera'}, - {'name': 'camera_name1', 'default': 'camera', 'description': 'camera unique name'}, - {'name': 'device_type1', 'default': 'd435', 'description': 'camera unique name'}, - {'name': 'json_file_path1', 'default': json_path, 'description': 'allows advanced configuration'}, - {'name': 'depth_module.depth_profile1', 'default': '424x240x15', 'description': 'depth module profile'}, - {'name': 'depth_module.infra_profile1', 'default': '424x240x15', 'description': 'depth module infrared profile'}, - {'name': 'enable_depth1', 'default': 'true', 'description': 'enable depth stream'}, - {'name': 'rgb_camera.color_profile1', 'default': '424x240x15', 'description': 'color image width'}, - {'name': 'enable_color1', 'default': 'true', 'description': 'enable color stream'}, - {'name': 'enable_infra11', 'default': 'true', 'description': 'enable infra1 stream'}, - {'name': 'enable_infra21', 'default': 'false', 'description': 'enable infra2 stream'}, - {'name': 'infra_rgb1', 'default': 'false', 'description': 'enable infra2 stream'}, - {'name': 'enable_confidence1', 'default': 'false', 'description': 'enable depth stream'}, - {'name': 'gyro_fps1', 'default': '200', 'description': "''"}, - {'name': 'accel_fps1', 'default': '100', 'description': "''"}, - {'name': 'enable_gyro1', 'default': 'true', 'description': "''"}, - {'name': 'enable_accel1', 'default': 'true', 'description': "''"}, - {'name': 'pointcloud.enable1', 'default': 'true', 'description': ''}, - {'name': 'pointcloud.stream_filter1', 'default': '2', 'description': 'texture stream for pointcloud'}, - {'name': 'pointcloud.stream_index_filter1','default': '0', 'description': 'texture stream index for pointcloud'}, - {'name': 'enable_sync1', 'default': 'true', 'description': "''"}, - {'name': 'align_depth.enable1', 'default': 'true', 'description': "''"}, - {'name': 'initial_reset1', 'default': 'true', 'description': "''"}, - {'name': 'allow_no_texture_points1', 'default': 'true', 'description': "''"}, - {'name': 'camera_namespace2', 'default': '', 'description': 'namespace for camera'}, - {'name': 'camera_name2', 'default': 'gripper_camera', 'description': 'camera unique name'}, - {'name': 'device_type2', 'default': 'd405', 'description': 'camera unique name'}, - {'name': 'json_file_path2', 'default': json_path, 'description': 'allows advanced configuration'}, - {'name': 'depth_module.depth_profile2', 'default': '480x270x15', 'description': 'depth module profile'}, - {'name': 'depth_module.enable_auto_exposure2', 'default': 'true', 'description': 'enable/disable auto exposure for depth image'}, - {'name': 'enable_depth2', 'default': 'true', 'description': 'enable depth stream'}, - {'name': 'depth_module.color_profile2', 'default': '480x270x15', 'description': 'color image width'}, - {'name': 'enable_color2', 'default': 'true', 'description': 'enable color stream'}, - {'name': 'enable_infra12', 'default': 'false', 'description': 'enable infra1 stream'}, - {'name': 'enable_infra22', 'default': 'false', 'description': 'enable infra2 stream'}, - {'name': 'infra_rgb2', 'default': 'false', 'description': 'enable infra2 stream'}, - {'name': 'enable_confidence2', 'default': 'false', 'description': 'enable depth stream'}, - {'name': 'gyro_fps2', 'default': '200', 'description': "''"}, - {'name': 'accel_fps2', 'default': '100', 'description': "''"}, - {'name': 'enable_gyro2', 'default': 'true', 'description': "''"}, - {'name': 'enable_accel2', 'default': 'true', 'description': "''"}, - {'name': 'pointcloud.enable2', 'default': 'true', 'description': ''}, - {'name': 'pointcloud.stream_filter2', 'default': '2', 'description': 'texture stream for pointcloud'}, - {'name': 'pointcloud.stream_index_filter2','default': '0', 'description': 'texture stream index for pointcloud'}, - {'name': 'enable_sync2', 'default': 'true', 'description': "''"}, - {'name': 'align_depth.enable2', 'default': 'true', 'description': "''"}, - {'name': 'initial_reset2', 'default': 'false', 'description': "''"}, - {'name': 'allow_no_texture_points2', 'default': 'true', 'description': "''"}, - ] def declare_configurable_parameters(parameters): - return [DeclareLaunchArgument(param['name'], default_value=param['default'], description=param['description']) for param in parameters] + return [ + DeclareLaunchArgument( + param["name"], + default_value=param["default"], + description=param["description"], + ) + for param in parameters + ] + def set_configurable_parameters(parameters): - return dict([(param['name'], LaunchConfiguration(param['name'])) for param in parameters]) + return dict( + [(param["name"], LaunchConfiguration(param["name"])) for param in parameters] + ) + def generate_launch_description(): - realsense_launch = GroupAction( - actions=[ - SetRemap(src='/gripper_camera/color/image_rect_raw', dst='/gripper_camera/image_raw'), - IncludeLaunchDescription( - PythonLaunchDescriptionSource([os.path.join( - get_package_share_directory('realsense2_camera'), 'launch'), - '/rs_multi_camera_launch.py']) - ) - ] - ) + realsense_launch = GroupAction( + actions=[ + SetRemap( + src="/gripper_camera/color/image_rect_raw", + dst="/gripper_camera/image_raw", + ), + IncludeLaunchDescription( + PythonLaunchDescriptionSource( + [ + os.path.join( + get_package_share_directory("realsense2_camera"), "launch" + ), + "/rs_multi_camera_launch.py", + ] + ) + ), + ] + ) - d435i_accel_correction = Node( - package='stretch_core', - executable='d435i_accel_correction', - output='screen', - ) + d435i_accel_correction = Node( + package="stretch_core", + executable="d435i_accel_correction", + output="screen", + ) - return LaunchDescription(declare_configurable_parameters(configurable_parameters) + [ - realsense_launch, - d435i_accel_correction, - ]) + return LaunchDescription( + declare_configurable_parameters(configurable_parameters) + + [ + realsense_launch, + d435i_accel_correction, + ] + ) diff --git a/launch/navigation_camera.launch.py b/launch/navigation_camera.launch.py index 665a5b0f..acc84f8c 100644 --- a/launch/navigation_camera.launch.py +++ b/launch/navigation_camera.launch.py @@ -1,38 +1,42 @@ +from launch_ros.actions import Node + import launch from launch import LaunchDescription from launch.actions import DeclareLaunchArgument from launch.substitutions import LaunchConfiguration -from launch_ros.actions import Node + def generate_launch_description(): - return LaunchDescription([ - Node( - package='usb_cam', - executable='usb_cam_node_exe', - name='navigation_camera', - output='screen', - parameters=[ - {'video_device': '/dev/hello-navigation-camera'}, - {'image_width': 800}, - {'image_height': 600}, - {'framerate': 15.0}, - {'pixel_format': 'mjpeg2rgb'}, - {'brightness': 10}, - {'contrast': 30}, - {'hue': 0}, - {'saturation': 80}, - {'sharpness': 3}, - {'gamma': 80}, - {'exposure_auto': True}, - #{'exposure': 150}, - {'white_balance_temperature_auto': True}, - #{'white_balance': 4250}, - {'gain': 10}, - {'camera_frame_id': 'navigation_camera'}, - {'camera_name': 'navigation_camera'}, - {'io_method': 'mmap'} - ], - remappings=[('/image_raw', '/navigation_camera/image_raw')] - ), - # You can add more nodes or configurations here if needed. - ]) + return LaunchDescription( + [ + Node( + package="usb_cam", + executable="usb_cam_node_exe", + name="navigation_camera", + output="screen", + parameters=[ + {"video_device": "/dev/hello-navigation-camera"}, + {"image_width": 800}, + {"image_height": 600}, + {"framerate": 15.0}, + {"pixel_format": "mjpeg2rgb"}, + {"brightness": 10}, + {"contrast": 30}, + {"hue": 0}, + {"saturation": 80}, + {"sharpness": 3}, + {"gamma": 80}, + {"exposure_auto": True}, + # {'exposure': 150}, + {"white_balance_temperature_auto": True}, + # {'white_balance': 4250}, + {"gain": 10}, + {"camera_frame_id": "navigation_camera"}, + {"camera_name": "navigation_camera"}, + {"io_method": "mmap"}, + ], + remappings=[("/image_raw", "/navigation_camera/image_raw")], + ), + # You can add more nodes or configurations here if needed. + ] + ) diff --git a/launch/web_interface.launch.py b/launch/web_interface.launch.py index 1c784324..e75be401 100644 --- a/launch/web_interface.launch.py +++ b/launch/web_interface.launch.py @@ -1,25 +1,41 @@ -import os import fnmatch -import stretch_body.robot_params +import os +import stretch_body.robot_params from ament_index_python import get_package_share_directory -from launch import LaunchDescription -from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription, GroupAction, ExecuteProcess -from launch.conditions import IfCondition, UnlessCondition, LaunchConfigurationNotEquals -from launch.launch_description_sources import PythonLaunchDescriptionSource, FrontendLaunchDescriptionSource -from launch.substitutions import LaunchConfiguration, PathJoinSubstitution, FindExecutable, OrSubstitution, AndSubstitution, NotSubstitution -from launch.substitutions import ThisLaunchFileDir -from launch_ros.actions import Node from ament_index_python.packages import get_package_share_path +from launch_ros.actions import Node + +from launch import LaunchDescription +from launch.actions import ( + DeclareLaunchArgument, + ExecuteProcess, + GroupAction, + IncludeLaunchDescription, +) +from launch.conditions import IfCondition, LaunchConfigurationNotEquals, UnlessCondition +from launch.launch_description_sources import ( + FrontendLaunchDescriptionSource, + PythonLaunchDescriptionSource, +) +from launch.substitutions import ( + AndSubstitution, + FindExecutable, + LaunchConfiguration, + NotSubstitution, + OrSubstitution, + PathJoinSubstitution, + ThisLaunchFileDir, +) def symlinks_to_has_beta_teleop_kit(): usb_device_seen = { - 'hello-navigation-camera': False, - 'hello-gripper-camera': False, + "hello-navigation-camera": False, + "hello-gripper-camera": False, } - listOfFiles = os.listdir('/dev') + listOfFiles = os.listdir("/dev") pattern = "hello*" for entry in listOfFiles: if fnmatch.fnmatch(entry, pattern): @@ -30,10 +46,10 @@ def symlinks_to_has_beta_teleop_kit(): def symlinks_to_has_nav_head_cam(): usb_device_seen = { - 'hello-nav-head-camera': False, + "hello-nav-head-camera": False, } - listOfFiles = os.listdir('/dev') + listOfFiles = os.listdir("/dev") pattern = "hello*" for entry in listOfFiles: if fnmatch.fnmatch(entry, pattern): @@ -58,95 +74,211 @@ def map_configuration_to_drivers(model, tool, has_beta_teleop_kit, has_nav_head_ add_head_nav_driver (True or False) """ # Stretch RE1 - if model == "RE1V0" and tool == "tool_stretch_gripper" and has_beta_teleop_kit == False and has_nav_head_cam == False: - return 'd435-only', False, False, False - elif model == "RE1V0" and tool == "tool_stretch_gripper" and has_beta_teleop_kit == True and has_nav_head_cam == False: - return 'd435-only', True, True, False - elif model == "RE1V0" and tool == "tool_stretch_dex_wrist" and has_beta_teleop_kit == False and has_nav_head_cam == False: - return 'd435-only', False, False, False - elif model == "RE1V0" and tool == "tool_stretch_dex_wrist" and has_beta_teleop_kit == True and has_nav_head_cam == False: - return 'd435-only', True, True, False + if ( + model == "RE1V0" + and tool == "tool_stretch_gripper" + and has_beta_teleop_kit == False + and has_nav_head_cam == False + ): + return "d435-only", False, False, False + elif ( + model == "RE1V0" + and tool == "tool_stretch_gripper" + and has_beta_teleop_kit == True + and has_nav_head_cam == False + ): + return "d435-only", True, True, False + elif ( + model == "RE1V0" + and tool == "tool_stretch_dex_wrist" + and has_beta_teleop_kit == False + and has_nav_head_cam == False + ): + return "d435-only", False, False, False + elif ( + model == "RE1V0" + and tool == "tool_stretch_dex_wrist" + and has_beta_teleop_kit == True + and has_nav_head_cam == False + ): + return "d435-only", True, True, False # Stretch 2 - elif model == "RE2V0" and tool == "tool_stretch_gripper" and has_beta_teleop_kit == False and has_nav_head_cam == False: - return 'd435-only', False, False, False - elif model == "RE2V0" and tool == "tool_stretch_gripper" and has_beta_teleop_kit == True and has_nav_head_cam == False: - return 'd435-only', True, True, False - elif model == "RE2V0" and tool == "tool_stretch_dex_wrist" and has_beta_teleop_kit == False and has_nav_head_cam == False: - return 'd435-only', False, False, False - elif model == "RE2V0" and tool == "tool_stretch_dex_wrist" and has_beta_teleop_kit == True and has_nav_head_cam == False: - return 'd435-only', True, True, False + elif ( + model == "RE2V0" + and tool == "tool_stretch_gripper" + and has_beta_teleop_kit == False + and has_nav_head_cam == False + ): + return "d435-only", False, False, False + elif ( + model == "RE2V0" + and tool == "tool_stretch_gripper" + and has_beta_teleop_kit == True + and has_nav_head_cam == False + ): + return "d435-only", True, True, False + elif ( + model == "RE2V0" + and tool == "tool_stretch_dex_wrist" + and has_beta_teleop_kit == False + and has_nav_head_cam == False + ): + return "d435-only", False, False, False + elif ( + model == "RE2V0" + and tool == "tool_stretch_dex_wrist" + and has_beta_teleop_kit == True + and has_nav_head_cam == False + ): + return "d435-only", True, True, False # Stretch 2+ (upgraded Stretch 2) - elif model == "RE2V0" and tool == "eoa_wrist_dw3_tool_sg3" and has_beta_teleop_kit == False and has_nav_head_cam == True: - return 'both' , False, False, True - elif model == "RE2V0" and tool == "eoa_wrist_dw3_tool_nil" and has_beta_teleop_kit == False and has_nav_head_cam == True: - return 'both' , False, False, True + elif ( + model == "RE2V0" + and tool == "eoa_wrist_dw3_tool_sg3" + and has_beta_teleop_kit == False + and has_nav_head_cam == True + ): + return "both", False, False, True + elif ( + model == "RE2V0" + and tool == "eoa_wrist_dw3_tool_nil" + and has_beta_teleop_kit == False + and has_nav_head_cam == True + ): + return "both", False, False, True # Stretch 3 - elif model == "SE3" and tool == "eoa_wrist_dw3_tool_sg3" and has_beta_teleop_kit == False and has_nav_head_cam == True: - return 'both' , False, False, True - elif model == "SE3" and tool == "eoa_wrist_dw3_tool_nil" and has_beta_teleop_kit == False and has_nav_head_cam == True: - return 'both' , False, False, True - elif model == "SE3" and tool == "eoa_wrist_dw3_tool_tablet_12in" and has_beta_teleop_kit == False and has_nav_head_cam == True: - return 'both' , False, False, True + elif ( + model == "SE3" + and tool == "eoa_wrist_dw3_tool_sg3" + and has_beta_teleop_kit == False + and has_nav_head_cam == True + ): + return "both", False, False, True + elif ( + model == "SE3" + and tool == "eoa_wrist_dw3_tool_nil" + and has_beta_teleop_kit == False + and has_nav_head_cam == True + ): + return "both", False, False, True + elif ( + model == "SE3" + and tool == "eoa_wrist_dw3_tool_tablet_12in" + and has_beta_teleop_kit == False + and has_nav_head_cam == True + ): + return "both", False, False, True + + raise ValueError( + f"cannot find valid configuration for model={model}, tool={tool}, has_beta_teleop_kit={has_beta_teleop_kit}, has_nav_head_cam={has_nav_head_cam}" + ) - raise ValueError(f'cannot find valid configuration for model={model}, tool={tool}, has_beta_teleop_kit={has_beta_teleop_kit}, has_nav_head_cam={has_nav_head_cam}') def generate_launch_description(): - teleop_interface_package = str(get_package_share_path('stretch_web_teleop')) - core_package = str(get_package_share_path('stretch_core')) - rosbridge_package = str(get_package_share_path('rosbridge_server')) - stretch_core_path = str(get_package_share_directory('stretch_core')) - stretch_navigation_path = str(get_package_share_directory('stretch_nav2')) - navigation_bringup_path = str(get_package_share_directory('nav2_bringup')) + teleop_interface_package = str(get_package_share_path("stretch_web_teleop")) + core_package = str(get_package_share_path("stretch_core")) + rosbridge_package = str(get_package_share_path("rosbridge_server")) + stretch_core_path = str(get_package_share_directory("stretch_core")) + stretch_navigation_path = str(get_package_share_directory("stretch_nav2")) + navigation_bringup_path = str(get_package_share_directory("nav2_bringup")) _, robot_params = stretch_body.robot_params.RobotParams().get_params() - stretch_serial_no = robot_params['robot']['serial_no'] - stretch_model = robot_params['robot']['model_name'] - stretch_tool = robot_params['robot']['tool'] + stretch_serial_no = robot_params["robot"]["serial_no"] + stretch_model = robot_params["robot"]["model_name"] + stretch_tool = robot_params["robot"]["tool"] stretch_has_beta_teleop_kit = symlinks_to_has_beta_teleop_kit() stretch_has_nav_head_cam = symlinks_to_has_nav_head_cam() - drivers_realsense, driver_gripper_cam, driver_navigation_cam, driver_nav_head_cam = map_configuration_to_drivers( - stretch_model, stretch_tool, stretch_has_beta_teleop_kit, stretch_has_nav_head_cam) + ( + drivers_realsense, + driver_gripper_cam, + driver_navigation_cam, + driver_nav_head_cam, + ) = map_configuration_to_drivers( + stretch_model, + stretch_tool, + stretch_has_beta_teleop_kit, + stretch_has_nav_head_cam, + ) # Declare launch arguments - params_file = DeclareLaunchArgument('params', default_value=[ - PathJoinSubstitution([teleop_interface_package, 'config', 'configure_video_streams_params.yaml'])]) - map_yaml = DeclareLaunchArgument('map_yaml', description='filepath to previously captured map', default_value='') - certfile_arg = DeclareLaunchArgument('certfile', default_value=stretch_serial_no + '+6.pem') - keyfile_arg = DeclareLaunchArgument('keyfile', default_value=stretch_serial_no + '+6-key.pem') + params_file = DeclareLaunchArgument( + "params", + default_value=[ + PathJoinSubstitution( + [ + teleop_interface_package, + "config", + "configure_video_streams_params.yaml", + ] + ) + ], + ) + map_yaml = DeclareLaunchArgument( + "map_yaml", description="filepath to previously captured map", default_value="" + ) + certfile_arg = DeclareLaunchArgument( + "certfile", default_value=stretch_serial_no + "+6.pem" + ) + keyfile_arg = DeclareLaunchArgument( + "keyfile", default_value=stretch_serial_no + "+6-key.pem" + ) nav2_params_file_param = DeclareLaunchArgument( - 'nav2_params_file', - default_value=os.path.join(stretch_navigation_path, 'config', 'nav2_params.yaml'), - description='Full path to the ROS2 parameters file to use for all launched nodes') - dict_file_path = os.path.join(core_package, 'config', 'stretch_marker_dict.yaml') - depthimage_to_laserscan_config = os.path.join(core_package, 'config', 'depthimage_to_laser_scan_params.yaml') + "nav2_params_file", + default_value=os.path.join( + stretch_navigation_path, "config", "nav2_params.yaml" + ), + description="Full path to the ROS2 parameters file to use for all launched nodes", + ) + dict_file_path = os.path.join(core_package, "config", "stretch_marker_dict.yaml") + depthimage_to_laserscan_config = os.path.join( + core_package, "config", "depthimage_to_laser_scan_params.yaml" + ) # Start collecting nodes to launch - ld = LaunchDescription([ - map_yaml, - nav2_params_file_param, - params_file, - certfile_arg, - keyfile_arg, - ]) - - if drivers_realsense == 'd435-only': + ld = LaunchDescription( + [ + map_yaml, + nav2_params_file_param, + params_file, + certfile_arg, + keyfile_arg, + ] + ) + + if drivers_realsense == "d435-only": # Launch only D435i if there is no D405 ld.add_action( GroupAction( actions=[ IncludeLaunchDescription( - PythonLaunchDescriptionSource(PathJoinSubstitution([core_package, 'launch', 'd435i_low_resolution.launch.py'])) + PythonLaunchDescriptionSource( + PathJoinSubstitution( + [ + core_package, + "launch", + "d435i_low_resolution.launch.py", + ] + ) + ) ) ] ) ) - elif drivers_realsense == 'both': + elif drivers_realsense == "both": # Launch both D435i and D405 ld.add_action( GroupAction( actions=[ IncludeLaunchDescription( - PythonLaunchDescriptionSource(PathJoinSubstitution([teleop_interface_package, 'launch', 'multi_camera.launch.py'])) + PythonLaunchDescriptionSource( + PathJoinSubstitution( + [ + teleop_interface_package, + "launch", + "multi_camera.launch.py", + ] + ) + ) ) ] ) @@ -158,7 +290,15 @@ def generate_launch_description(): GroupAction( actions=[ IncludeLaunchDescription( - PythonLaunchDescriptionSource(PathJoinSubstitution([core_package, 'launch', 'beta_navigation_camera.launch.py'])) + PythonLaunchDescriptionSource( + PathJoinSubstitution( + [ + core_package, + "launch", + "beta_navigation_camera.launch.py", + ] + ) + ) ) ] ) @@ -170,7 +310,11 @@ def generate_launch_description(): GroupAction( actions=[ IncludeLaunchDescription( - PythonLaunchDescriptionSource(PathJoinSubstitution([core_package, 'launch', 'navigation_camera.launch.py'])) + PythonLaunchDescriptionSource( + PathJoinSubstitution( + [core_package, "launch", "navigation_camera.launch.py"] + ) + ) ) ] ) @@ -182,7 +326,15 @@ def generate_launch_description(): GroupAction( actions=[ IncludeLaunchDescription( - PythonLaunchDescriptionSource(PathJoinSubstitution([core_package, 'launch', 'beta_gripper_camera.launch.py'])) + PythonLaunchDescriptionSource( + PathJoinSubstitution( + [ + core_package, + "launch", + "beta_gripper_camera.launch.py", + ] + ) + ) ) ] ) @@ -193,83 +345,122 @@ def generate_launch_description(): # Publish blank image if no navigation camera exists ld.add_action( Node( - package='image_publisher', - executable='image_publisher_node', - name='navigation_camera_node', - output='screen', - parameters=[{'publish_rate': 15.0}], - remappings=[('image_raw', '/navigation_camera/image_raw')], - arguments=[PathJoinSubstitution([teleop_interface_package, 'nodes', 'blank_image.png'])], + package="image_publisher", + executable="image_publisher_node", + name="navigation_camera_node", + output="screen", + parameters=[{"publish_rate": 15.0}], + remappings=[("image_raw", "/navigation_camera/image_raw")], + arguments=[ + PathJoinSubstitution( + [teleop_interface_package, "nodes", "blank_image.png"] + ) + ], ) ) - if drivers_realsense == 'd435-only' and driver_gripper_cam == False: + if drivers_realsense == "d435-only" and driver_gripper_cam == False: # Blank Gripper Camera Node # Publish blank image if there is no gripper camera exists ld.add_action( Node( - package='image_publisher', - executable='image_publisher_node', - name='gripper_camera_node', - output='screen', - parameters=[{'publish_rate': 15.0}], - remappings=[('image_raw', '/gripper_camera/color/image_rect_raw')], - arguments=[PathJoinSubstitution([teleop_interface_package, 'nodes', 'blank_image.png'])], + package="image_publisher", + executable="image_publisher_node", + name="gripper_camera_node", + output="screen", + parameters=[{"publish_rate": 15.0}], + remappings=[("image_raw", "/gripper_camera/color/image_rect_raw")], + arguments=[ + PathJoinSubstitution( + [teleop_interface_package, "nodes", "blank_image.png"] + ) + ], ) ) tf2_web_republisher_node = Node( - package='tf2_web_republisher_py', - executable='tf2_web_republisher', - name='tf2_web_republisher_node' + package="tf2_web_republisher_py", + executable="tf2_web_republisher", + name="tf2_web_republisher_node", ) ld.add_action(tf2_web_republisher_node) # Stretch Driver stretch_driver_launch = IncludeLaunchDescription( - PythonLaunchDescriptionSource(PathJoinSubstitution([core_package, 'launch', 'stretch_driver.launch.py'])), - launch_arguments={'broadcast_odom_tf': 'True'}.items()) + PythonLaunchDescriptionSource( + PathJoinSubstitution([core_package, "launch", "stretch_driver.launch.py"]) + ), + launch_arguments={"broadcast_odom_tf": "True"}.items(), + ) ld.add_action(stretch_driver_launch) # Rosbridge Websocket rosbridge_launch = IncludeLaunchDescription( - FrontendLaunchDescriptionSource(PathJoinSubstitution([rosbridge_package, 'launch', 'rosbridge_websocket_launch.xml'])), + FrontendLaunchDescriptionSource( + PathJoinSubstitution( + [rosbridge_package, "launch", "rosbridge_websocket_launch.xml"] + ) + ), launch_arguments={ - 'port': '9090', - 'address': 'localhost', - 'ssl': 'true', - 'certfile': PathJoinSubstitution([teleop_interface_package, 'certificates', LaunchConfiguration('certfile')]), - 'keyfile': PathJoinSubstitution([teleop_interface_package, 'certificates', LaunchConfiguration('keyfile')]), - 'authenticate': 'false' - }.items() + "port": "9090", + "address": "localhost", + "ssl": "true", + "certfile": PathJoinSubstitution( + [ + teleop_interface_package, + "certificates", + LaunchConfiguration("certfile"), + ] + ), + "keyfile": PathJoinSubstitution( + [ + teleop_interface_package, + "certificates", + LaunchConfiguration("keyfile"), + ] + ), + "authenticate": "false", + }.items(), ) ld.add_action(rosbridge_launch) # Configure Video Streams configure_video_streams_node = Node( - package='stretch_web_teleop', - executable='configure_video_streams.py', - output='screen', - arguments=[LaunchConfiguration('params'), str(stretch_has_beta_teleop_kit)], - parameters=[{'has_beta_teleop_kit': stretch_has_beta_teleop_kit}] + package="stretch_web_teleop", + executable="configure_video_streams.py", + output="screen", + arguments=[LaunchConfiguration("params"), str(stretch_has_beta_teleop_kit)], + parameters=[{"has_beta_teleop_kit": stretch_has_beta_teleop_kit}], ) ld.add_action(configure_video_streams_node) rplidar_launch = IncludeLaunchDescription( - PythonLaunchDescriptionSource([stretch_core_path, '/launch/rplidar.launch.py'])) + PythonLaunchDescriptionSource([stretch_core_path, "/launch/rplidar.launch.py"]) + ) ld.add_action(rplidar_launch) navigation_bringup_launch = GroupAction( - condition=LaunchConfigurationNotEquals('map_yaml', ''), + condition=LaunchConfigurationNotEquals("map_yaml", ""), actions=[ IncludeLaunchDescription( - PythonLaunchDescriptionSource([stretch_navigation_path, '/launch/bringup_launch.py']), - launch_arguments={'use_sim_time': 'false', - 'autostart': 'true', - 'map': PathJoinSubstitution([teleop_interface_package, 'maps', LaunchConfiguration('map_yaml')]), - 'params_file': LaunchConfiguration('nav2_params_file'), - 'use_rviz': 'false'}.items()) - ] + PythonLaunchDescriptionSource( + [stretch_navigation_path, "/launch/bringup_launch.py"] + ), + launch_arguments={ + "use_sim_time": "false", + "autostart": "true", + "map": PathJoinSubstitution( + [ + teleop_interface_package, + "maps", + LaunchConfiguration("map_yaml"), + ] + ), + "params_file": LaunchConfiguration("nav2_params_file"), + "use_rviz": "false", + }.items(), + ) + ], ) ld.add_action(navigation_bringup_launch) @@ -281,7 +472,7 @@ def generate_launch_description(): " service call ", "/reinitialize_global_localization ", "std_srvs/srv/Empty ", - "\"{}\"", + '"{}"', ] ], shell=True, diff --git a/launch_interface.sh b/launch_interface.sh index a9e9733a..25c35524 100755 --- a/launch_interface.sh +++ b/launch_interface.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Usage: ./launch_interface -m $HELLO_FLEET_PATH/maps/.yaml +# Usage: ./launch_interface.sh -m $HELLO_FLEET_PATH/maps/.yaml MAP_ARG="" if getopts ":m:" opt && [[ $opt == "m" && -f $OPTARG ]]; then echo "Setting map..." diff --git a/maps/map.yaml b/maps/map.yaml old mode 100755 new mode 100644 index 565dd759..7694d8b3 --- a/maps/map.yaml +++ b/maps/map.yaml @@ -3,7 +3,7 @@ image: map.pgm negate: 0 occupied_thresh: 0.65 origin: -- -6.1499999999999915 -- -14.75 -- 0.0 + - -6.1499999999999915 + - -14.75 + - 0.0 resolution: 0.05 diff --git a/nodes/configure_video_streams.py b/nodes/configure_video_streams.py index 2b2206e5..d6b1ecb7 100755 --- a/nodes/configure_video_streams.py +++ b/nodes/configure_video_streams.py @@ -1,44 +1,53 @@ #!/usr/bin/env python3 -import rclpy -from rclpy.node import Node -from rclpy.duration import Duration -from rclpy.time import Time +import math +import sys + +import cv2 import message_filters import numpy as np -import cv2 -import yaml -import sys -import ros2_numpy -import tf2_ros import pcl import PyKDL -import math -# from tf2_sensor_msgs.tf2_sensor_msgs import do_transform_cloud -from sensor_msgs.msg import Image, CameraInfo, CompressedImage, PointCloud2, CameraInfo, JointState -from sensor_msgs_py.point_cloud2 import read_points, create_cloud +import rclpy +import ros2_numpy +import tf2_ros +import yaml from cv_bridge import CvBridge -from visualization_msgs.msg import MarkerArray -from rclpy.qos import ReliabilityPolicy, QoSProfile +from rclpy.duration import Duration from rclpy.executors import MultiThreadedExecutor +from rclpy.node import Node +from rclpy.qos import QoSProfile, ReliabilityPolicy +from rclpy.time import Time + +# from tf2_sensor_msgs.tf2_sensor_msgs import do_transform_cloud +from sensor_msgs.msg import CameraInfo, CompressedImage, Image, JointState, PointCloud2 +from sensor_msgs_py.point_cloud2 import create_cloud, read_points from std_srvs.srv import SetBool +from visualization_msgs.msg import MarkerArray + class ConfigureVideoStreams(Node): def __init__(self, params_file, has_beta_teleop_kit): - super().__init__('configure_video_streams') - - with open(params_file, 'r') as params: + super().__init__("configure_video_streams") + + with open(params_file, "r") as params: self.image_params = yaml.safe_load(params) self.declare_parameter("has_beta_teleop_kit", rclpy.Parameter.Type.BOOL) self.tf_buffer = tf2_ros.Buffer(cache_time=Duration(seconds=12)) self.tf2_listener = tf2_ros.TransformListener(self.tf_buffer, self) - + # Loaded params for each video stream - self.overhead_params = self.image_params["overhead"] if "overhead" in self.image_params else None - self.realsense_params = self.image_params["realsense"] if "realsense" in self.image_params else None - self.gripper_params = self.image_params["gripper"] if "gripper" in self.image_params else None + self.overhead_params = ( + self.image_params["overhead"] if "overhead" in self.image_params else None + ) + self.realsense_params = ( + self.image_params["realsense"] if "realsense" in self.image_params else None + ) + self.gripper_params = ( + self.image_params["gripper"] if "gripper" in self.image_params else None + ) # Stores all the images created using the loaded params self.overhead_images = {} @@ -51,32 +60,65 @@ def __init__(self, params_file, has_beta_teleop_kit): self.cv_bridge = CvBridge() # Compressed Image publishers - self.publisher_realsense_cmp = self.create_publisher(CompressedImage, '/camera/color/image_raw/rotated/compressed', 15) - self.publisher_overhead_cmp = self.create_publisher(CompressedImage, '/navigation_camera/image_raw/rotated/compressed', 15) - self.publisher_gripper_cmp = self.create_publisher(CompressedImage, '/gripper_camera/image_raw/cropped/compressed', 15) + self.publisher_realsense_cmp = self.create_publisher( + CompressedImage, "/camera/color/image_raw/rotated/compressed", 15 + ) + self.publisher_overhead_cmp = self.create_publisher( + CompressedImage, "/navigation_camera/image_raw/rotated/compressed", 15 + ) + self.publisher_gripper_cmp = self.create_publisher( + CompressedImage, "/gripper_camera/image_raw/cropped/compressed", 15 + ) # Subscribers - self.camera_rgb_subscriber = message_filters.Subscriber(self, Image, "/camera/color/image_raw") - self.overhead_camera_rgb_subscriber = self.create_subscription(Image, "/navigation_camera/image_raw", self.navigation_camera_cb, QoSProfile(depth=1, reliability=ReliabilityPolicy.RELIABLE)) - self.gripper_camera_rgb_subscriber = self.create_subscription(Image, "/gripper_camera/image_raw", self.gripper_camera_cb, 1) - self.joint_state_subscription = self.create_subscription(JointState, "/stretch/joint_states", self.joint_state_cb, 1) - self.point_cloud_subscriber = message_filters.Subscriber(self, PointCloud2, "/camera/depth/color/points") - self.camera_info_subscriber = self.create_subscription(CameraInfo, "/camera/aligned_depth_to_color/camera_info", self.camera_info_cb, 1) - self.camera_synchronizer = message_filters.ApproximateTimeSynchronizer([ - self.camera_rgb_subscriber, - self.point_cloud_subscriber, - ], 1, 1, allow_headerless=True) + self.camera_rgb_subscriber = message_filters.Subscriber( + self, Image, "/camera/color/image_raw" + ) + self.overhead_camera_rgb_subscriber = self.create_subscription( + Image, + "/navigation_camera/image_raw", + self.navigation_camera_cb, + QoSProfile(depth=1, reliability=ReliabilityPolicy.RELIABLE), + ) + self.gripper_camera_rgb_subscriber = self.create_subscription( + Image, "/gripper_camera/image_raw", self.gripper_camera_cb, 1 + ) + self.joint_state_subscription = self.create_subscription( + JointState, "/stretch/joint_states", self.joint_state_cb, 1 + ) + self.point_cloud_subscriber = message_filters.Subscriber( + self, PointCloud2, "/camera/depth/color/points" + ) + self.camera_info_subscriber = self.create_subscription( + CameraInfo, + "/camera/aligned_depth_to_color/camera_info", + self.camera_info_cb, + 1, + ) + self.camera_synchronizer = message_filters.ApproximateTimeSynchronizer( + [ + self.camera_rgb_subscriber, + self.point_cloud_subscriber, + ], + 1, + 1, + allow_headerless=True, + ) self.camera_synchronizer.registerCallback(self.realsense_cb) # Default image perspectives # self.get_logger().info(f"wide_angle_cam={wide_angle_cam} d405={d405}") - self.overhead_camera_perspective = "fixed" if has_beta_teleop_kit else "wide_angle_cam" + self.overhead_camera_perspective = ( + "fixed" if has_beta_teleop_kit else "wide_angle_cam" + ) self.realsense_camera_perspective = "default" self.gripper_camera_perspective = "default" if has_beta_teleop_kit else "d405" self.get_logger().info(self.gripper_camera_perspective) # Service for enabling the depth AR overlay on the realsense stream - self.depth_ar_service = self.create_service(SetBool, 'depth_ar', self.depth_ar_callback) + self.depth_ar_service = self.create_service( + SetBool, "depth_ar", self.depth_ar_callback + ) self.depth_ar = False self.pcl_cloud_filtered = None @@ -84,11 +126,19 @@ def __init__(self, params_file, has_beta_teleop_kit): # https://github.com/ros/geometry2/blob/noetic-devel/tf2_sensor_msgs/src/tf2_sensor_msgs/tf2_sensor_msgs.py#L44 def transform_to_kdl(self, t): - return PyKDL.Frame(PyKDL.Rotation.Quaternion(t.transform.rotation.x, t.transform.rotation.y, - t.transform.rotation.z, t.transform.rotation.w), - PyKDL.Vector(t.transform.translation.x, - t.transform.translation.y, - t.transform.translation.z)) + return PyKDL.Frame( + PyKDL.Rotation.Quaternion( + t.transform.rotation.x, + t.transform.rotation.y, + t.transform.rotation.z, + t.transform.rotation.w, + ), + PyKDL.Vector( + t.transform.translation.x, + t.transform.translation.y, + t.transform.translation.z, + ), + ) # https://github.com/ros/geometry2/blob/noetic-devel/tf2_sensor_msgs/src/tf2_sensor_msgs/tf2_sensor_msgs.py#L52 def do_transform_cloud(self, cloud, transform): @@ -99,9 +149,9 @@ def do_transform_cloud(self, cloud, transform): p_out = t_kdl * PyKDL.Vector(p_in[0], p_in[1], p_in[2]) points_out.append([p_out[0], p_out[1], p_out[2]]) return np.array(points_out) - + def camera_info_cb(self, msg): - self.P = np.array(msg.p).reshape(3,4) + self.P = np.array(msg.p).reshape(3, 4) # self.camera_info_subscriber.destroy() def pc_callback(self, msg, img): @@ -115,33 +165,41 @@ def pc_callback(self, msg, img): # Get transform try: transform = self.tf_buffer.lookup_transform( - 'base_link', - 'camera_color_optical_frame', + "base_link", + "camera_color_optical_frame", Time(), - timeout=Duration(seconds=0.1) + timeout=Duration(seconds=0.1), ) except: - self.get_logger().warn("Could not find the transform between frames {} and {}".format('base_link', 'camera_color_optical_frame')) + self.get_logger().warn( + "Could not find the transform between frames {} and {}".format( + "base_link", "camera_color_optical_frame" + ) + ) + return img + + if not self.pcl_cloud_filtered: + return img + if self.pcl_cloud_filtered.to_array().size == 0: return img - - if not self.pcl_cloud_filtered: return img - if self.pcl_cloud_filtered.to_array().size == 0: return img # Transform points cloud to base link and points that are in robot's reach pc_in_base_link = self.do_transform_cloud(self.pcl_cloud_filtered, transform) # pc_in_base_link = ros2_numpy.point_cloud2.pointcloud2_to_xyz_array(pc_in_base_link_msg) - dist = np.sqrt(np.power(pc_in_base_link[:,0], 2) + np.power(pc_in_base_link[:,1], 2)) + dist = np.sqrt( + np.power(pc_in_base_link[:, 0], 2) + np.power(pc_in_base_link[:, 1], 2) + ) filtered_indices = np.where((dist > 0.25) & (dist < 1))[0] # Get filtered points in camera frame # pc_in_camera = ros2_numpy.point_cloud2.pointcloud2_to_xyz_array(msg) # N x 3 pts_in_range = self.pcl_cloud_filtered.to_array()[filtered_indices, :] - pts_in_range = np.hstack((pts_in_range, np.ones((pts_in_range.shape[0],1)))) + pts_in_range = np.hstack((pts_in_range, np.ones((pts_in_range.shape[0], 1)))) - # Get pixel coordinates - coords = np.matmul(self.P, np.transpose(pts_in_range)) # 3 x N - x_idx = np.absolute((coords[0,:]/coords[2,:]).astype(int)) - y_idx = np.absolute((coords[1,:]/coords[2,:]).astype(int)) + # Get pixel coordinates + coords = np.matmul(self.P, np.transpose(pts_in_range)) # 3 x N + x_idx = np.absolute((coords[0, :] / coords[2, :]).astype(int)) + y_idx = np.absolute((coords[1, :] / coords[2, :]).astype(int)) # negative_indices = np.where((x_idx < 0) or (y_idx < 0)) # Change color of pixels in robot's reach @@ -149,7 +207,7 @@ def pc_callback(self, msg, img): img[x_idx, img.shape[1] - 1 - y_idx, :] = [255, 191, 0, 50] # img[y_idx, x_idx, 3] = 0 - return img + return img def depth_ar_callback(self, req, res): self.depth_ar = req.data @@ -157,10 +215,14 @@ def depth_ar_callback(self, req, res): return res def crop_image(self, image, params): - if params["x_min"] is None: raise ValueError("Crop x_min is not defined!") - if params["x_max"] is None: raise ValueError("Crop x_max is not defined!") - if params["y_min"] is None: raise ValueError("Crop y_min is not defined!") - if params["y_max"] is None: raise ValueError("Crop y_max is not defined!") + if params["x_min"] is None: + raise ValueError("Crop x_min is not defined!") + if params["x_max"] is None: + raise ValueError("Crop x_max is not defined!") + if params["y_min"] is None: + raise ValueError("Crop y_min is not defined!") + if params["y_max"] is None: + raise ValueError("Crop y_max is not defined!") x_min = params["x_min"] x_max = params["x_max"] @@ -171,24 +233,30 @@ def crop_image(self, image, params): # https://stackoverflow.com/questions/44865023/how-can-i-create-a-circular-mask-for-a-numpy-array def create_circular_mask(self, h, w, center=None, radius=None): - if center is None: # use the middle of the image - center = (int(w/2), int(h/2)) - if radius is None: # use the smallest distance between the center and image walls - radius = min(center[0], center[1], w-center[0], h-center[1]) + if center is None: # use the middle of the image + center = (int(w / 2), int(h / 2)) + if ( + radius is None + ): # use the smallest distance between the center and image walls + radius = min(center[0], center[1], w - center[0], h - center[1]) Y, X = np.ogrid[:h, :w] - dist_from_center = np.sqrt((X - center[0])**2 + (Y-center[1])**2) + dist_from_center = np.sqrt((X - center[0]) ** 2 + (Y - center[1]) ** 2) mask = dist_from_center <= radius return mask def mask_image(self, image, params): - if params["width"] is None: raise ValueError("Mask width is not defined!") - if params["height"] is None: raise ValueError("Mask height is not defined!") + if params["width"] is None: + raise ValueError("Mask width is not defined!") + if params["height"] is None: + raise ValueError("Mask height is not defined!") w = params["width"] h = params["height"] - center = (params["center"]["x"], params["center"]["y"]) if params["center"] else None + center = ( + (params["center"]["x"], params["center"]["y"]) if params["center"] else None + ) radius = params["radius"] mask = self.create_circular_mask(h, w, center, radius) @@ -197,28 +265,30 @@ def mask_image(self, image, params): return img def rotate_image(self, image, value): - if value == 'ROTATE_90_CLOCKWISE': + if value == "ROTATE_90_CLOCKWISE": return cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE) elif value == "ROTATE_180": return cv2.rotate(image, cv2.ROTATE_180) elif value == "ROTATE_90_COUNTERCLOCKWISE": return cv2.rotate(image, cv2.ROTATE_90_COUNTERCLOCKWISE) else: - raise ValueError("Invalid rotate image value: options are ROTATE_90_CLOCKWISE, ROTATE_180, or ROTATE_90_COUNTERCLOCKWISE") + raise ValueError( + "Invalid rotate image value: options are ROTATE_90_CLOCKWISE, ROTATE_180, or ROTATE_90_COUNTERCLOCKWISE" + ) def configure_images(self, rgb_image, params): # if rgb_image.shape[-1] == 2: # rgb_image = cv2.cvtColor(rgb_image, cv2.COLOR_YUV2RGB_YVYU) # else: - + rgb_image = cv2.cvtColor(rgb_image, cv2.COLOR_BGR2RGB) if params: - if params['crop']: - rgb_image = self.crop_image(rgb_image, params['crop']) - if params['mask']: - rgb_image = self.mask_image(rgb_image, params['mask']) - if params['rotate']: - rgb_image = self.rotate_image(rgb_image, params['rotate']) + if params["crop"]: + rgb_image = self.crop_image(rgb_image, params["crop"]) + if params["mask"]: + rgb_image = self.mask_image(rgb_image, params["mask"]) + if params["rotate"]: + rgb_image = self.rotate_image(rgb_image, params["rotate"]) return rgb_image def realsense_cb(self, ros_image, pc): @@ -226,43 +296,59 @@ def realsense_cb(self, ros_image, pc): for image_config_name in self.realsense_params: img = self.configure_images(image, self.realsense_params[image_config_name]) img = cv2.cvtColor(img, cv2.COLOR_RGB2RGBA) - if self.depth_ar: img = self.pc_callback(pc, img) + if self.depth_ar: + img = self.pc_callback(pc, img) # if self.aruco_markers: img = self.aruco_markers_callback(marker_msg, img) self.realsense_images[image_config_name] = img - - self.realsense_rgb_image = self.realsense_images[self.realsense_camera_perspective] - self.publish_compressed_msg(self.realsense_rgb_image, self.publisher_realsense_cmp) + + self.realsense_rgb_image = self.realsense_images[ + self.realsense_camera_perspective + ] + self.publish_compressed_msg( + self.realsense_rgb_image, self.publisher_realsense_cmp + ) def gripper_camera_cb(self, ros_image): - image = self.cv_bridge.imgmsg_to_cv2(ros_image, 'rgb8') - img = self.rotate_image_around_center(image, -1*self.roll_value) - self.gripper_camera_rgb_image = self.configure_images(img, self.gripper_params[self.gripper_camera_perspective]) - self.publish_compressed_msg(self.gripper_camera_rgb_image, self.publisher_gripper_cmp) + image = self.cv_bridge.imgmsg_to_cv2(ros_image, "rgb8") + img = self.rotate_image_around_center(image, -1 * self.roll_value) + self.gripper_camera_rgb_image = self.configure_images( + img, self.gripper_params[self.gripper_camera_perspective] + ) + self.publish_compressed_msg( + self.gripper_camera_rgb_image, self.publisher_gripper_cmp + ) def navigation_camera_cb(self, ros_image): - image = self.cv_bridge.imgmsg_to_cv2(ros_image, 'rgb8') - self.overhead_camera_rgb_image = self.configure_images(image, self.overhead_params[self.overhead_camera_perspective]) - self.publish_compressed_msg(self.overhead_camera_rgb_image, self.publisher_overhead_cmp) + image = self.cv_bridge.imgmsg_to_cv2(ros_image, "rgb8") + self.overhead_camera_rgb_image = self.configure_images( + image, self.overhead_params[self.overhead_camera_perspective] + ) + self.publish_compressed_msg( + self.overhead_camera_rgb_image, self.publisher_overhead_cmp + ) def rotate_image_around_center(self, image, angle): image_center = tuple(np.array(image.shape[1::-1]) / 2) rot_mat = cv2.getRotationMatrix2D(image_center, math.degrees(angle), 1.0) - result = cv2.warpAffine(image, rot_mat, image.shape[1::-1], flags=cv2.INTER_LINEAR) + result = cv2.warpAffine( + image, rot_mat, image.shape[1::-1], flags=cv2.INTER_LINEAR + ) return result def joint_state_cb(self, joint_state): - if 'joint_wrist_roll' in joint_state.name: - roll_index = joint_state.name.index('joint_wrist_roll') + if "joint_wrist_roll" in joint_state.name: + roll_index = joint_state.name.index("joint_wrist_roll") self.roll_value = joint_state.position[roll_index] - + def publish_compressed_msg(self, image, publisher): msg = CompressedImage() msg.header.stamp = self.get_clock().now().to_msg() msg.format = "jpeg" - msg.data = np.array(cv2.imencode('.jpg', image)[1]).tobytes() + msg.data = np.array(cv2.imencode(".jpg", image)[1]).tobytes() publisher.publish(msg) -if __name__ == '__main__': + +if __name__ == "__main__": rclpy.init() print(sys.argv) node = ConfigureVideoStreams(sys.argv[1], sys.argv[2] == "True") diff --git a/nodes/gripper_camera.py b/nodes/gripper_camera.py index 9c7d4091..d2b19815 100755 --- a/nodes/gripper_camera.py +++ b/nodes/gripper_camera.py @@ -3,39 +3,53 @@ # my_image_publisher/my_image_publisher/publisher_node.py +import os +import threading + import cv2 -from sensor_msgs.msg import Image -from cv_bridge import CvBridge import rclpy -from rclpy.node import Node -import threading +from cv_bridge import CvBridge from rclpy.executors import MultiThreadedExecutor -import os +from rclpy.node import Node +from sensor_msgs.msg import Image -UVC_COLOR_SIZE = [1024, 768] # [3840,2880] [1920, 1080] [1280, 720] [1280, 800] [640, 480] +UVC_COLOR_SIZE = [ + 1024, + 768, +] # [3840,2880] [1920, 1080] [1280, 720] [1280, 800] [640, 480] UVC_FPS = 100 -UVC_VIDEO_INDEX = '/dev/hello-gripper-camera' -UVC_VIDEO_FORMAT = 'MJPG' # MJPG YUYV - -UVC_BRIGHTNESS = -40 # brightness 0x00980900 (int) : min=-64 max=64 step=1 default=0 value=0 -UVC_CONTRAST = 40 # contrast 0x00980901 (int) : min=0 max=64 step=1 default=32 value=32 -UVC_SATURATION = 60 # saturation 0x00980902 (int) : min=0 max=128 step=1 default=90 value=90 -UVC_HUE = 0 # hue 0x00980903 (int) : min=-40 max=40 step=1 default=0 value=0 -UVC_GAMMA = 80 # gamma 0x00980910 (int) : min=72 max=500 step=1 default=100 value=100 -UVC_GAIN = 80 # gain 0x00980913 (int) : min=0 max=100 step=1 default=0 value=0 -UVC_WB_TEMP = 4250 # white_balance_temperature 0x0098091a (int) : min=2800 max=6500 step=1 default=4600 value=4600 flags=inactive -UVC_SHARPNESS = 100 # sharpness 0x0098091b (int) : min=0 max=6 step=1 default=3 value=3 -UVC_BACKLIT_COMP = 1 # backlight_compensation 0x0098091c (int) : min=0 max=2 step=1 default=1 value=1 -UVC_EXPOSURE_TIME = 157 # exposure_time_absolute 0x009a0902 (int) : min=1 max=5000 step=1 default=157 value=157 flags=inactive +UVC_VIDEO_INDEX = "/dev/hello-gripper-camera" +UVC_VIDEO_FORMAT = "MJPG" # MJPG YUYV + +UVC_BRIGHTNESS = ( + -40 +) # brightness 0x00980900 (int) : min=-64 max=64 step=1 default=0 value=0 +UVC_CONTRAST = ( + 40 # contrast 0x00980901 (int) : min=0 max=64 step=1 default=32 value=32 +) +UVC_SATURATION = ( + 60 # saturation 0x00980902 (int) : min=0 max=128 step=1 default=90 value=90 +) +UVC_HUE = 0 # hue 0x00980903 (int) : min=-40 max=40 step=1 default=0 value=0 +UVC_GAMMA = ( + 80 # gamma 0x00980910 (int) : min=72 max=500 step=1 default=100 value=100 +) +UVC_GAIN = 80 # gain 0x00980913 (int) : min=0 max=100 step=1 default=0 value=0 +UVC_WB_TEMP = 4250 # white_balance_temperature 0x0098091a (int) : min=2800 max=6500 step=1 default=4600 value=4600 flags=inactive +UVC_SHARPNESS = ( + 100 # sharpness 0x0098091b (int) : min=0 max=6 step=1 default=3 value=3 +) +UVC_BACKLIT_COMP = 1 # backlight_compensation 0x0098091c (int) : min=0 max=2 step=1 default=1 value=1 +UVC_EXPOSURE_TIME = 157 # exposure_time_absolute 0x009a0902 (int) : min=1 max=5000 step=1 default=157 value=157 flags=inactive # More UVC Video capture properties here: # https://docs.opencv.org/3.4/d4/d15/group__videoio__flags__base.html -# +# # Arducam wiki info site -# https://docs.arducam.com/UVC-Camera/Appilcation-Note/OpenCV-Python-GStreamer-on-linux/ -# -# Setting Video formates using v4l2 +# https://docs.arducam.com/UVC-Camera/Appilcation-Note/OpenCV-Python-GStreamer-on-linux/ +# +# Setting Video formats using v4l2 # http://trac.gateworks.com/wiki/linux/v4l2 # # @@ -49,16 +63,17 @@ def setup_uvc_camera(device_index, size, fps): return cap - -UVC_SETTINGS = {'brightness':UVC_BRIGHTNESS, - 'contrast':UVC_CONTRAST, - 'hue':UVC_HUE, - 'gamma':UVC_GAMMA, - 'gain':UVC_GAIN, - 'white_balance_temperature':UVC_WB_TEMP, - 'sharpness':UVC_SHARPNESS, - 'backlight_compensation':UVC_BACKLIT_COMP, - 'exposure_time_absolute':UVC_EXPOSURE_TIME} +UVC_SETTINGS = { + "brightness": UVC_BRIGHTNESS, + "contrast": UVC_CONTRAST, + "hue": UVC_HUE, + "gamma": UVC_GAMMA, + "gain": UVC_GAIN, + "white_balance_temperature": UVC_WB_TEMP, + "sharpness": UVC_SHARPNESS, + "backlight_compensation": UVC_BACKLIT_COMP, + "exposure_time_absolute": UVC_EXPOSURE_TIME, +} # Set video format and size cmd = f"v4l2-ctl --device {UVC_VIDEO_INDEX} --set-fmt-video=width={UVC_COLOR_SIZE[0]},height={UVC_COLOR_SIZE[1]}" @@ -69,10 +84,13 @@ def setup_uvc_camera(device_index, size, fps): cmd = f"v4l2-ctl --device {UVC_VIDEO_INDEX} --set-ctrl={k}={UVC_SETTINGS[k]}" os.system(cmd) + class GripperImagePublisherNode(Node): def __init__(self): - super().__init__('image_publisher_node') - self.publisher = self.create_publisher(Image, '/gripper_camera/color/image_rect_raw', 15) + super().__init__("image_publisher_node") + self.publisher = self.create_publisher( + Image, "/gripper_camera/color/image_rect_raw", 15 + ) timer_period = 0.001 # seconds self.timer = self.create_timer(timer_period, self.timer_callback) self.timer2 = self.create_timer(timer_period, self.timer_callback2) @@ -81,7 +99,7 @@ def __init__(self): self.image_msg = None # self.uvc_camera_thread = threading.Thread(target=self.stream_camera) # self.uvc_camera_thread.start() - + # def stream_camera(self): # print("Starting Image Stream") # while True: @@ -104,13 +122,12 @@ def timer_callback2(self): try: ret, image_uvc = self.uvc_camera.read() # Convert the OpenCV image to a ROS Image message - self.image_msg = self.cv_bridge.cv2_to_imgmsg(image_uvc, encoding='bgr8') + self.image_msg = self.cv_bridge.cv2_to_imgmsg(image_uvc, encoding="bgr8") # print("Updated") except Exception as e: print(f"Error UVC Cam: {e}") - def main(args=None): rclpy.init(args=args) @@ -126,5 +143,6 @@ def main(args=None): image_publisher_node.destroy_node() rclpy.shutdown() -if __name__ == '__main__': + +if __name__ == "__main__": main() diff --git a/nodes/navigation_camera.py b/nodes/navigation_camera.py index 2db441c7..ca06d4cb 100755 --- a/nodes/navigation_camera.py +++ b/nodes/navigation_camera.py @@ -3,39 +3,53 @@ # my_image_publisher/my_image_publisher/publisher_node.py +import os +import threading + import cv2 -from sensor_msgs.msg import Image -from cv_bridge import CvBridge import rclpy -from rclpy.node import Node -import threading +from cv_bridge import CvBridge from rclpy.executors import MultiThreadedExecutor -import os +from rclpy.node import Node +from sensor_msgs.msg import Image -UVC_COLOR_SIZE = [1280, 800] # [3840,2880] [1920, 1080] [1280, 720] [1280, 800] [640, 480] +UVC_COLOR_SIZE = [ + 1280, + 800, +] # [3840,2880] [1920, 1080] [1280, 720] [1280, 800] [640, 480] UVC_FPS = 100 -UVC_VIDEO_INDEX = '/dev/hello-nav-head-camera' -UVC_VIDEO_FORMAT = 'MJPG' # MJPG YUYV - -UVC_BRIGHTNESS = 10 # brightness 0x00980900 (int) : min=-64 max=64 step=1 default=0 value=0 -UVC_CONTRAST = 30 # contrast 0x00980901 (int) : min=0 max=64 step=1 default=32 value=32 -UVC_SATURATION = 80 # saturation 0x00980902 (int) : min=0 max=128 step=1 default=90 value=90 -UVC_HUE = 0 # hue 0x00980903 (int) : min=-40 max=40 step=1 default=0 value=0 -UVC_GAMMA = 80 # gamma 0x00980910 (int) : min=72 max=500 step=1 default=100 value=100 -UVC_GAIN = 10 # gain 0x00980913 (int) : min=0 max=100 step=1 default=0 value=0 -UVC_WB_TEMP = 4600 # white_balance_temperature 0x0098091a (int) : min=2800 max=6500 step=1 default=4600 value=4600 flags=inactive -UVC_SHARPNESS = 3 # sharpness 0x0098091b (int) : min=0 max=6 step=1 default=3 value=3 -UVC_BACKLIT_COMP = 1 # backlight_compensation 0x0098091c (int) : min=0 max=2 step=1 default=1 value=1 -UVC_EXPOSURE_TIME = 157 # exposure_time_absolute 0x009a0902 (int) : min=1 max=5000 step=1 default=157 value=157 flags=inactive +UVC_VIDEO_INDEX = "/dev/hello-nav-head-camera" +UVC_VIDEO_FORMAT = "MJPG" # MJPG YUYV + +UVC_BRIGHTNESS = ( + 10 # brightness 0x00980900 (int) : min=-64 max=64 step=1 default=0 value=0 +) +UVC_CONTRAST = ( + 30 # contrast 0x00980901 (int) : min=0 max=64 step=1 default=32 value=32 +) +UVC_SATURATION = ( + 80 # saturation 0x00980902 (int) : min=0 max=128 step=1 default=90 value=90 +) +UVC_HUE = 0 # hue 0x00980903 (int) : min=-40 max=40 step=1 default=0 value=0 +UVC_GAMMA = ( + 80 # gamma 0x00980910 (int) : min=72 max=500 step=1 default=100 value=100 +) +UVC_GAIN = 10 # gain 0x00980913 (int) : min=0 max=100 step=1 default=0 value=0 +UVC_WB_TEMP = 4600 # white_balance_temperature 0x0098091a (int) : min=2800 max=6500 step=1 default=4600 value=4600 flags=inactive +UVC_SHARPNESS = ( + 3 # sharpness 0x0098091b (int) : min=0 max=6 step=1 default=3 value=3 +) +UVC_BACKLIT_COMP = 1 # backlight_compensation 0x0098091c (int) : min=0 max=2 step=1 default=1 value=1 +UVC_EXPOSURE_TIME = 157 # exposure_time_absolute 0x009a0902 (int) : min=1 max=5000 step=1 default=157 value=157 flags=inactive # More UVC Video capture properties here: # https://docs.opencv.org/3.4/d4/d15/group__videoio__flags__base.html -# +# # Arducam wiki info site -# https://docs.arducam.com/UVC-Camera/Appilcation-Note/OpenCV-Python-GStreamer-on-linux/ -# -# Setting Video formates using v4l2 +# https://docs.arducam.com/UVC-Camera/Appilcation-Note/OpenCV-Python-GStreamer-on-linux/ +# +# Setting Video formats using v4l2 # http://trac.gateworks.com/wiki/linux/v4l2 # # @@ -49,16 +63,17 @@ def setup_uvc_camera(device_index, size, fps): return cap - -UVC_SETTINGS = {'brightness':UVC_BRIGHTNESS, - 'contrast':UVC_CONTRAST, - 'hue':UVC_HUE, - 'gamma':UVC_GAMMA, - 'gain':UVC_GAIN, - 'white_balance_temperature':UVC_WB_TEMP, - 'sharpness':UVC_SHARPNESS, - 'backlight_compensation':UVC_BACKLIT_COMP, - 'exposure_time_absolute':UVC_EXPOSURE_TIME} +UVC_SETTINGS = { + "brightness": UVC_BRIGHTNESS, + "contrast": UVC_CONTRAST, + "hue": UVC_HUE, + "gamma": UVC_GAMMA, + "gain": UVC_GAIN, + "white_balance_temperature": UVC_WB_TEMP, + "sharpness": UVC_SHARPNESS, + "backlight_compensation": UVC_BACKLIT_COMP, + "exposure_time_absolute": UVC_EXPOSURE_TIME, +} # Set video format and size cmd = f"v4l2-ctl --device {UVC_VIDEO_INDEX} --set-fmt-video=width={UVC_COLOR_SIZE[0]},height={UVC_COLOR_SIZE[1]}" @@ -70,12 +85,12 @@ def setup_uvc_camera(device_index, size, fps): os.system(cmd) - - class ImagePublisherNode(Node): def __init__(self): - super().__init__('image_publisher_node') - self.publisher = self.create_publisher(Image, '/navigation_camera/image_raw', 15) + super().__init__("image_publisher_node") + self.publisher = self.create_publisher( + Image, "/navigation_camera/image_raw", 15 + ) timer_period = 0.001 # seconds self.timer = self.create_timer(timer_period, self.timer_callback) self.timer2 = self.create_timer(timer_period, self.timer_callback2) @@ -84,7 +99,7 @@ def __init__(self): self.image_msg = None # self.uvc_camera_thread = threading.Thread(target=self.stream_camera) # self.uvc_camera_thread.start() - + # def stream_camera(self): # print("Starting Image Stream") # while True: @@ -107,13 +122,12 @@ def timer_callback2(self): try: ret, image_uvc = self.uvc_camera.read() # Convert the OpenCV image to a ROS Image message - self.image_msg = self.cv_bridge.cv2_to_imgmsg(image_uvc, encoding='bgr8') + self.image_msg = self.cv_bridge.cv2_to_imgmsg(image_uvc, encoding="bgr8") # print("Updated") except Exception as e: print(f"Error UVC Cam: {e}") - def main(args=None): rclpy.init(args=args) @@ -129,5 +143,6 @@ def main(args=None): image_publisher_node.destroy_node() rclpy.shutdown() -if __name__ == '__main__': + +if __name__ == "__main__": main() diff --git a/nodes/old_navigation_camera.py b/nodes/old_navigation_camera.py index 37c5427d..5b33d89e 100755 --- a/nodes/old_navigation_camera.py +++ b/nodes/old_navigation_camera.py @@ -1,38 +1,53 @@ #! /usr/bin/env python3 +import os +import threading + import cv2 -from sensor_msgs.msg import Image -from cv_bridge import CvBridge import rclpy -from rclpy.node import Node -import threading +from cv_bridge import CvBridge from rclpy.executors import MultiThreadedExecutor -import os +from rclpy.node import Node +from sensor_msgs.msg import Image -UVC_COLOR_SIZE = [1024, 768] # [3840,2880] [1920, 1080] [1280, 720] [1280, 800] [640, 480] +UVC_COLOR_SIZE = [ + 1024, + 768, +] # [3840,2880] [1920, 1080] [1280, 720] [1280, 800] [640, 480] UVC_FPS = 100 -UVC_VIDEO_INDEX = '/dev/hello-navigation-camera' -UVC_VIDEO_FORMAT = 'MJPG' # MJPG YUYV +UVC_VIDEO_INDEX = "/dev/hello-navigation-camera" +UVC_VIDEO_FORMAT = "MJPG" # MJPG YUYV + +UVC_BRIGHTNESS = ( + 10 # brightness 0x00980900 (int) : min=-64 max=64 step=1 default=0 value=0 +) +UVC_CONTRAST = ( + 30 # contrast 0x00980901 (int) : min=0 max=64 step=1 default=32 value=32 +) +UVC_SATURATION = ( + 80 # saturation 0x00980902 (int) : min=0 max=128 step=1 default=90 value=90 +) +UVC_HUE = 0 # hue 0x00980903 (int) : min=-40 max=40 step=1 default=0 value=0 +UVC_GAMMA = ( + 80 # gamma 0x00980910 (int) : min=72 max=500 step=1 default=100 value=100 +) +UVC_GAIN = 10 # gain 0x00980913 (int) : min=0 max=100 step=1 default=0 value=0 +UVC_WB_TEMP = 4600 # white_balance_temperature 0x0098091a (int) : min=2800 max=6500 step=1 default=4600 value=4600 flags=inactive +UVC_SHARPNESS = ( + 3 # sharpness 0x0098091b (int) : min=0 max=6 step=1 default=3 value=3 +) +UVC_BACKLIT_COMP = 1 # backlight_compensation 0x0098091c (int) : min=0 max=2 step=1 default=1 value=1 +UVC_EXPOSURE_TIME = 150 # exposure_time_absolute 0x009a0902 (int) : min=1 max=5000 step=1 default=157 value=157 flags=inactive -UVC_BRIGHTNESS = 10 # brightness 0x00980900 (int) : min=-64 max=64 step=1 default=0 value=0 -UVC_CONTRAST = 30 # contrast 0x00980901 (int) : min=0 max=64 step=1 default=32 value=32 -UVC_SATURATION = 80 # saturation 0x00980902 (int) : min=0 max=128 step=1 default=90 value=90 -UVC_HUE = 0 # hue 0x00980903 (int) : min=-40 max=40 step=1 default=0 value=0 -UVC_GAMMA = 80 # gamma 0x00980910 (int) : min=72 max=500 step=1 default=100 value=100 -UVC_GAIN = 10 # gain 0x00980913 (int) : min=0 max=100 step=1 default=0 value=0 -UVC_WB_TEMP = 4600 # white_balance_temperature 0x0098091a (int) : min=2800 max=6500 step=1 default=4600 value=4600 flags=inactive -UVC_SHARPNESS = 3 # sharpness 0x0098091b (int) : min=0 max=6 step=1 default=3 value=3 -UVC_BACKLIT_COMP = 1 # backlight_compensation 0x0098091c (int) : min=0 max=2 step=1 default=1 value=1 -UVC_EXPOSURE_TIME = 150 # exposure_time_absolute 0x009a0902 (int) : min=1 max=5000 step=1 default=157 value=157 flags=inactive # More UVC Video capture properties here: # https://docs.opencv.org/3.4/d4/d15/group__videoio__flags__base.html -# +# # Arducam wiki info site -# https://docs.arducam.com/UVC-Camera/Appilcation-Note/OpenCV-Python-GStreamer-on-linux/ -# -# Setting Video formates using v4l2 +# https://docs.arducam.com/UVC-Camera/Appilcation-Note/OpenCV-Python-GStreamer-on-linux/ +# +# Setting Video formats using v4l2 # http://trac.gateworks.com/wiki/linux/v4l2 def setup_uvc_camera(device_index, size, fps): cap = cv2.VideoCapture(device_index) @@ -41,15 +56,18 @@ def setup_uvc_camera(device_index, size, fps): cap.set(cv2.CAP_PROP_FPS, fps) return cap -UVC_SETTINGS = {'brightness':UVC_BRIGHTNESS, - 'contrast':UVC_CONTRAST, - 'hue':UVC_HUE, - 'gamma':UVC_GAMMA, - 'gain':UVC_GAIN, - 'white_balance_temperature':UVC_WB_TEMP, - 'sharpness':UVC_SHARPNESS, - 'backlight_compensation':UVC_BACKLIT_COMP, - 'exposure_time_absolute':UVC_EXPOSURE_TIME} + +UVC_SETTINGS = { + "brightness": UVC_BRIGHTNESS, + "contrast": UVC_CONTRAST, + "hue": UVC_HUE, + "gamma": UVC_GAMMA, + "gain": UVC_GAIN, + "white_balance_temperature": UVC_WB_TEMP, + "sharpness": UVC_SHARPNESS, + "backlight_compensation": UVC_BACKLIT_COMP, + "exposure_time_absolute": UVC_EXPOSURE_TIME, +} # Set video format and size cmd = f"v4l2-ctl --device {UVC_VIDEO_INDEX} --set-fmt-video=width={UVC_COLOR_SIZE[0]},height={UVC_COLOR_SIZE[1]}" @@ -60,10 +78,13 @@ def setup_uvc_camera(device_index, size, fps): cmd = f"v4l2-ctl --device {UVC_VIDEO_INDEX} --set-ctrl={k}={UVC_SETTINGS[k]}" os.system(cmd) + class ImagePublisherNode(Node): def __init__(self): - super().__init__('image_publisher_node') - self.publisher = self.create_publisher(Image, '/navigation_camera/image_raw', 15) + super().__init__("image_publisher_node") + self.publisher = self.create_publisher( + Image, "/navigation_camera/image_raw", 15 + ) timer_period = 0.001 # seconds self.timer = self.create_timer(timer_period, self.timer_callback) self.timer2 = self.create_timer(timer_period, self.timer_callback2) @@ -80,10 +101,11 @@ def timer_callback2(self): try: ret, image_uvc = self.uvc_camera.read() # Convert the OpenCV image to a ROS Image message - self.image_msg = self.cv_bridge.cv2_to_imgmsg(image_uvc, encoding='bgr8') + self.image_msg = self.cv_bridge.cv2_to_imgmsg(image_uvc, encoding="bgr8") except Exception as e: print(f"Error UVC Cam: {e}") + def main(args=None): rclpy.init(args=args) @@ -99,5 +121,6 @@ def main(args=None): image_publisher_node.destroy_node() rclpy.shutdown() -if __name__ == '__main__': + +if __name__ == "__main__": main() diff --git a/scripts/crop_map.py b/scripts/crop_map.py index 113cb498..fd2f26e1 100644 --- a/scripts/crop_map.py +++ b/scripts/crop_map.py @@ -4,11 +4,13 @@ from __future__ import print_function +import math import sys + +import numpy as np import yaml from PIL import Image -import math -import numpy as np + def find_bounds(map_image): x_min = map_image.size[0] @@ -26,8 +28,9 @@ def find_bounds(map_image): y_end = max(y, y_end) return x_min, x_end, y_min, y_end + def computed_cropped_origin(map_image, bounds, resolution, origin): - """ Compute the image for the cropped map when map_image is cropped by bounds and had origin before. """ + """Compute the image for the cropped map when map_image is cropped by bounds and had origin before.""" ox = origin[0] oy = origin[1] oth = origin[2] @@ -44,6 +47,7 @@ def computed_cropped_origin(map_image, bounds, resolution, origin): return [new_ox, new_oy, oth] + if __name__ == "__main__": if len(sys.argv) < 2: print("Usage: %s map.yaml [cropped.yaml]" % sys.argv[0], file=sys.stderr) @@ -74,7 +78,7 @@ def computed_cropped_origin(map_image, bounds, resolution, origin): cropped_image = map_image.crop((bounds[0], bounds[2], bounds[1] + 1, bounds[3] + 1)) np_cropped_image = np.array(cropped_image) - np_cropped_image[np.where(np_cropped_image==206)] = 0 + np_cropped_image[np.where(np_cropped_image == 206)] = 0 cropped_image = Image.fromarray(np_cropped_image) cropped_image.save(crop_image) diff --git a/server.js b/server.js index 12e1df26..f825c677 100644 --- a/server.js +++ b/server.js @@ -1,9 +1,9 @@ -var fs = require('fs'); -require('dotenv').config(); +var fs = require("fs"); +require("dotenv").config(); var options = { - key: fs.readFileSync(`certificates/${process.env.keyfile}`), - cert: fs.readFileSync(`certificates/${process.env.certfile}`) + key: fs.readFileSync(`certificates/${process.env.keyfile}`), + cert: fs.readFileSync(`certificates/${process.env.certfile}`), }; // const http = require('http'); @@ -18,91 +18,96 @@ var options = { // console.log('listening on *:' + port); // }); -var pm2 = require('pm2'); -const socket = require('socket.io'); -var express = require('express') +var pm2 = require("pm2"); +const socket = require("socket.io"); +var express = require("express"); var app = express(); -app.all('*', ensureSecure); // at top of routing calls +app.all("*", ensureSecure); // at top of routing calls function ensureSecure(req, res, next) { - if (!req.secure) { - // handle port numbers if you need non defaults - console.log('redirecting insecure request') - return res.redirect('https://' + req.hostname + req.url); - // res.redirect(`https://${req.hostname}${process.env.NGROK_URL}`); - } + if (!req.secure) { + // handle port numbers if you need non defaults + console.log("redirecting insecure request"); + return res.redirect("https://" + req.hostname + req.url); + // res.redirect(`https://${req.hostname}${process.env.NGROK_URL}`); + } - return next(); -}; + return next(); +} -var server = require('http').Server(app); -var secure_server = require('https').Server(options, app); +var server = require("http").Server(app); +var secure_server = require("https").Server(options, app); const io = socket(secure_server, { - allowEIO3: true + allowEIO3: true, }); -app.enable('trust proxy') -app.set('port', 443); +app.enable("trust proxy"); +app.set("port", 443); server.listen(80); secure_server.listen(443); -var path = require('path') -app.use('/', express.static(path.join(__dirname, 'dist'))); +var path = require("path"); +app.use("/", express.static(path.join(__dirname, "dist"))); -app.listen(process.env.port) +app.listen(process.env.port); io.on("connect_error", (err) => { - console.log(`connect_error due to ${err.message}`); + console.log(`connect_error due to ${err.message}`); }); -io.on('connection', function (socket) { - console.log('new socket.io connection'); - // console.log('socket.handshake = '); - // console.log(socket.handshake); +io.on("connection", function (socket) { + console.log("new socket.io connection"); + // console.log('socket.handshake = '); + // console.log(socket.handshake); - socket.on('join', function (room) { - console.log('Received request to join room ' + room); - // A room can have atmost two clients - if (!io.sockets.adapter.rooms.get(room) || io.sockets.adapter.rooms.get(room).size < 2) { - socket.join(room); - socket.emit('join', room, socket.id); - } else { - console.log('room full') - socket.emit('full', room) - } - }); + socket.on("join", function (room) { + console.log("Received request to join room " + room); + // A room can have atmost two clients + if ( + !io.sockets.adapter.rooms.get(room) || + io.sockets.adapter.rooms.get(room).size < 2 + ) { + socket.join(room); + socket.emit("join", room, socket.id); + } else { + console.log("room full"); + socket.emit("full", room); + } + }); - socket.on('add operator to robot room', (callback) => { - // The robot room is only available if another operator is not connected to it - if (io.sockets.adapter.rooms.get('robot')) { - if (io.sockets.adapter.rooms.get('robot').size < 2) { - socket.join('robot'); - socket.in('robot').emit('joined', 'robot'); - callback( {'success': true }) - } else { - console.log('could not connect because robot room is full') - callback( {'success': false }) - } - } else { - console.log('could not connect because robot is not available') - callback( {'success': false }) - } - }) + socket.on("add operator to robot room", (callback) => { + // The robot room is only available if another operator is not connected to it + if (io.sockets.adapter.rooms.get("robot")) { + if (io.sockets.adapter.rooms.get("robot").size < 2) { + socket.join("robot"); + socket.in("robot").emit("joined", "robot"); + callback({ success: true }); + } else { + console.log("could not connect because robot room is full"); + callback({ success: false }); + } + } else { + console.log("could not connect because robot is not available"); + callback({ success: false }); + } + }); - socket.on('signalling', function (message) { - if (io.sockets.adapter.rooms.get('robot')) { - socket.to('robot').emit('signalling', message); - } else { - console.log('robot_operator_room is none, so there is nobody to send the WebRTC message to'); - } - }); + socket.on("signalling", function (message) { + if (io.sockets.adapter.rooms.get("robot")) { + socket.to("robot").emit("signalling", message); + } else { + console.log( + "robot_operator_room is none, so there is nobody to send the WebRTC message to", + ); + } + }); - socket.on('bye', (role) => { - console.log(role, socket.rooms) - if (socket.rooms.has('robot')) { - socket.to('robot').emit('bye'); - console.log('Attempting to have the ' + role + ' leave the robot room.'); - console.log(''); - socket.leave('robot'); - } - }) -}); \ No newline at end of file + socket.on("bye", (role) => { + console.log(role, socket.rooms); + if (socket.rooms.has("robot")) { + socket.to("robot").emit("bye"); + console.log("Attempting to have the " + role + " leave the robot room."); + console.log(""); + socket.leave("robot"); + } + }); +}); diff --git a/src/pages/operator/README.md b/src/pages/operator/README.md index e1d3c6dc..6834bf91 100644 --- a/src/pages/operator/README.md +++ b/src/pages/operator/README.md @@ -14,37 +14,39 @@ Below is a diagram showing the hierarchy between components in the operator page ![operator page component hierarchy](../../../documentation/assets/operator/component_hiearchy.png) -* Header -: fixed at the top of the screen, this displays controls that the user always has access to - * Action Mode +- Header + : fixed at the top of the screen, this displays controls that the user always has access to + + - Action Mode : dropdown on the left of the header to switch between step-actions, press-release, and click-click action modes - * Speed Control + - Speed Control : Set of buttons labeled from "slowest" to "fastest" which control the scaling of the robots speed for all joints and all controls - * Customize button + - Customize button : button on the right side of the header which enters/leaves customization mode -* Voice Controls -: component below the header and above the layout, allows the user to activate a microphone with voice controls of the interface +- Voice Controls + : component below the header and above the layout, allows the user to activate a microphone with voice controls of the interface + +- Sidebar + : the vertical menu on the right side of the page which is only displayed while in customize mode. The top of the sidebar shows the currently selected element, or "none" if nothing is selected. -* Sidebar -: the vertical menu on the right side of the page which is only displayed while in customize mode. The top of the sidebar shows the currently selected element, or "none" if nothing is selected. - * Component Provider + - Component Provider : visible when no elements are selected from the layout, this is the upper area in the Sidebar which allows users to choose an element to add into the layout - * Display voice controls + - Display voice controls : option to display or hide the voice controls -* Layout -: the area where the user has control over the elements inside. The user can add, remove, or modify elements in the layout - * Panel +- Layout + : the area where the user has control over the elements inside. The user can add, remove, or modify elements in the layout + + - Panel : a component which contains one or more tabs - * Tab + - Tab : a component which contains one or more controls or camera views - * Camera view - : a video element showing a live stream from one of the robot's cameras - * Predictive display - : a special control which can only be placed over the overhead camera view. It draws a curve from the base to the cursor showing the path the robot will take if the user clicks at a location on the camera view. - * Joystick - : a virtual joystick which can be used to control the base - * Button Pad - : a set of buttons which can be placed independently or over a camera view - \ No newline at end of file + - Camera view + : a video element showing a live stream from one of the robot's cameras + - Predictive display + : a special control which can only be placed over the overhead camera view. It draws a curve from the base to the cursor showing the path the robot will take if the user clicks at a location on the camera view. + - Joystick + : a virtual joystick which can be used to control the base + - Button Pad + : a set of buttons which can be placed independently or over a camera view diff --git a/src/pages/operator/css/Alert.css b/src/pages/operator/css/Alert.css index a8bb1514..1ec72aa2 100644 --- a/src/pages/operator/css/Alert.css +++ b/src/pages/operator/css/Alert.css @@ -62,4 +62,4 @@ } .hide { display: none; -} \ No newline at end of file +} diff --git a/src/pages/operator/css/BatteryGuage.css b/src/pages/operator/css/BatteryGuage.css index 36e4a56b..623dff3d 100644 --- a/src/pages/operator/css/BatteryGuage.css +++ b/src/pages/operator/css/BatteryGuage.css @@ -1,53 +1,59 @@ .bar { - width: 100%; - height: 15px; - background-color: #436fb7; - display: flex; - margin-top: 3px; + width: 100%; + height: 15px; + background-color: #436fb7; + display: flex; + margin-top: 3px; } .barsContainer { - width: 50px; - height: 100px; - display: flex; - flex-direction: column-reverse; - align-items: center; + width: 50px; + height: 100px; + display: flex; + flex-direction: column-reverse; + align-items: center; } .batteryGauge { - height: 100px; - /* position: absolute; */ - width: 100%; - padding: 0px 10px 10px 10px; + height: 100px; + /* position: absolute; */ + width: 100%; + padding: 0px 10px 10px 10px; } .green { - filter: invert(17%) sepia(15%) saturate(6070%) hue-rotate(89deg) brightness(75%) contrast(108%); + filter: invert(17%) sepia(15%) saturate(6070%) hue-rotate(89deg) + brightness(75%) contrast(108%); } .yellow-green { - filter: brightness(70%) saturate(100%) invert(0%) sepia(28%) saturate(4155%) hue-rotate(56deg) contrast(119%); + filter: brightness(70%) saturate(100%) invert(0%) sepia(28%) saturate(4155%) + hue-rotate(56deg) contrast(119%); } .yellow { - filter: brightness(90%) saturate(100%) invert(0%) sepia(28%) saturate(4155%) hue-rotate(6deg) contrast(119%); + filter: brightness(90%) saturate(100%) invert(0%) sepia(28%) saturate(4155%) + hue-rotate(6deg) contrast(119%); } .orange { - filter: brightness(70%) saturate(100%) invert(10%) sepia(28%) saturate(4155%) hue-rotate(6deg) contrast(119%); + filter: brightness(70%) saturate(100%) invert(10%) sepia(28%) saturate(4155%) + hue-rotate(6deg) contrast(119%); } .orange-red { - filter: brightness(50%) saturate(100%) invert(10%) sepia(28%) saturate(4155%) hue-rotate(6deg) contrast(119%); + filter: brightness(50%) saturate(100%) invert(10%) sepia(28%) saturate(4155%) + hue-rotate(6deg) contrast(119%); } .red { - filter: brightness(30%) saturate(100%) invert(10%) sepia(28%) saturate(4155%) hue-rotate(346deg) contrast(119%); + filter: brightness(30%) saturate(100%) invert(10%) sepia(28%) saturate(4155%) + hue-rotate(346deg) contrast(119%); } .batteryGaugeContainer { - align-items: center; - display: flex; - flex-direction: column; - max-height: 90%; -} \ No newline at end of file + align-items: center; + display: flex; + flex-direction: column; + max-height: 90%; +} diff --git a/src/pages/operator/css/ButtonGrid.css b/src/pages/operator/css/ButtonGrid.css index ca86bb17..97742c87 100644 --- a/src/pages/operator/css/ButtonGrid.css +++ b/src/pages/operator/css/ButtonGrid.css @@ -1,59 +1,58 @@ .button-grid { - flex: 2; - display: grid; - grid-template-columns: repeat(6, 1fr); - grid-template-rows: repeat(4, auto 1fr); - grid-template-areas: - "header0 header0 header0 header0 header0 header0" - ". b0 b1 b2 b3 ." - "header1 header1 header1 header1 header1 header1" - ". b4 b5 b6 b7 ." - "header2 header2 header2 header2 header2 header2" - "b8 b9 b10 b11 b12 b13" - "header3 header3 header3 header3 header3 header3" - ". . b14 b15 . ." - ; - - padding: 0.4rem; - position: relative; - z-index: 1; - border-radius: var(--radius); + flex: 2; + display: grid; + grid-template-columns: repeat(6, 1fr); + grid-template-rows: repeat(4, auto 1fr); + grid-template-areas: + "header0 header0 header0 header0 header0 header0" + ". b0 b1 b2 b3 ." + "header1 header1 header1 header1 header1 header1" + ". b4 b5 b6 b7 ." + "header2 header2 header2 header2 header2 header2" + "b8 b9 b10 b11 b12 b13" + "header3 header3 header3 header3 header3 header3" + ". . b14 b15 . ."; + + padding: 0.4rem; + position: relative; + z-index: 1; + border-radius: var(--radius); } .button-grid::after { - border-radius: var(--radius); + border-radius: var(--radius); } .button-grid p { - text-align: center; - margin: 1.5rem 0 0.5rem 0; + text-align: center; + margin: 1.5rem 0 0.5rem 0; } .button-grid p:first-of-type { - text-align: center; - margin-top: 0.5rem; + text-align: center; + margin-top: 0.5rem; } .button-grid button { - width: 90%; - height: 4rem; - justify-self: center; - align-self: center; + width: 90%; + height: 4rem; + justify-self: center; + align-self: center; } .button-grid button.active { - background-color: var(--btn-turquoise); + background-color: var(--btn-turquoise); } .button-grid button.collision { - background-color: orange; + background-color: orange; } .button-grid button.limit { - background-color: red; + background-color: red; } .button-grid-bkg-color { - grid-column: 1 / 7; - height: 5rem; -} \ No newline at end of file + grid-column: 1 / 7; + height: 5rem; +} diff --git a/src/pages/operator/css/ButtonPad.css b/src/pages/operator/css/ButtonPad.css index e315f6d2..3829a6f0 100644 --- a/src/pages/operator/css/ButtonPad.css +++ b/src/pages/operator/css/ButtonPad.css @@ -1,113 +1,117 @@ /* Stand-alone button pad *****************************************************/ .button-pads { - /* Shared style */ - stroke-linecap: round; - stroke-linejoin: round; - cursor: pointer; - - /* Style for standalone version (not overlay */ - fill-opacity: 100%; - stroke-width: 6px; - stroke: var(--background-color); - fill: hsl(0,0%,31%); - flex: 1 1 0; - padding: 0.4rem; - max-height: 100%; - /* max-width: fit-content; */ - touch-action: none; - user-select: none; - -webkit-tap-highlight-color:rgba(0,0,0,0); + /* Shared style */ + stroke-linecap: round; + stroke-linejoin: round; + cursor: pointer; + + /* Style for standalone version (not overlay */ + fill-opacity: 100%; + stroke-width: 6px; + stroke: var(--background-color); + fill: hsl(0, 0%, 31%); + flex: 1 1 0; + padding: 0.4rem; + max-height: 100%; + /* max-width: fit-content; */ + touch-action: none; + user-select: none; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } .button-pads svg { - height: 100%; - width: 100%; + height: 100%; + width: 100%; } /* Overlay ********************************************************************/ .button-pads.overlay { - stroke-width: 3px; - fill: hsl(200, 50%, 60%); - fill-opacity: 0; - stroke: hsl(200, 0%, 60%); - - width: 100%; - height: 100%; - padding: 0; - margin: 0; + stroke-width: 3px; + fill: hsl(200, 50%, 60%); + fill-opacity: 0; + stroke: hsl(200, 0%, 60%); + + width: 100%; + height: 100%; + padding: 0; + margin: 0; } /* Settings for the buttons on the button pad *********************************/ /*When hovering over a child element of an svg*/ .button-pads path:hover { - fill-opacity: 30%; + fill-opacity: 30%; } @media (hover: none) { - .button-pads path.inactive:hover+image { filter: none !important; }; + .button-pads path.inactive:hover + image { + filter: none !important; + } } .button-pads image { - pointer-events: none; + pointer-events: none; } -.button-pads path.inactive:hover+image { - filter: brightness(50%) sepia(100%) saturate(10000%) hue-rotate(194deg); +.button-pads path.inactive:hover + image { + filter: brightness(50%) sepia(100%) saturate(10000%) hue-rotate(194deg); } .button-pads path.active { - fill-opacity: 60%; - fill: var(--btn-turquoise); + fill-opacity: 60%; + fill: var(--btn-turquoise); } .button-pads path.collision { - fill: orange; - fill-opacity: 40%; + fill: orange; + fill-opacity: 40%; } .button-pads path.limit { - fill: red; - fill-opacity: 40%; + fill: red; + fill-opacity: 40%; } /* Disable hover actions */ .button-pads.customizing path { - pointer-events: none; + pointer-events: none; } .button-pads.selected { - opacity: 100%; - stroke: var(--selected-color); - opacity: 100%; - border: 1px solid var(--selected-color); - stroke-width: 5px; + opacity: 100%; + stroke: var(--selected-color); + opacity: 100%; + border: 1px solid var(--selected-color); + stroke-width: 5px; } .button-pad { - font-size: x-large; - flex: 1 1 0; - display: grid; - text-align: center; - max-height: 100%; - justify-items: center; + font-size: x-large; + flex: 1 1 0; + display: grid; + text-align: center; + max-height: 100%; + justify-items: center; } -@media(max-width:1300px) { - .title { - font-size: smaller; - margin: 0.5rem; - } +@media (max-width: 1300px) { + .title { + font-size: smaller; + margin: 0.5rem; + } } -@media screen and (orientation:portrait) { - .button-pad { - width: 100%; - } +@media screen and (orientation: portrait) { + .button-pad { + width: 100%; + } } -@media(max-width:500px) { - .button-pads { filter: drop-shadow(6px 7px 2px rgb(0 0 0 / 0.4)); }; -} \ No newline at end of file +@media (max-width: 500px) { + .button-pads { + filter: drop-shadow(6px 7px 2px rgb(0 0 0 / 0.4)); + } +} diff --git a/src/pages/operator/css/CameraView.css b/src/pages/operator/css/CameraView.css index 2cf72b8a..1bd3e9ab 100644 --- a/src/pages/operator/css/CameraView.css +++ b/src/pages/operator/css/CameraView.css @@ -1,127 +1,128 @@ .video-container { - position: relative; - flex: 1 1 0; - display: grid; - justify-items: center; - padding: 0.4rem; - height: 100%; - grid-template-columns: auto; - grid-template-rows: 1fr auto minmax(9rem, 1fr); - object-fit: cover; - justify-content: center; - /* align-items: center; + position: relative; + flex: 1 1 0; + display: grid; + justify-items: center; + padding: 0.4rem; + height: 100%; + grid-template-columns: auto; + grid-template-rows: 1fr auto minmax(9rem, 1fr); + object-fit: cover; + justify-content: center; + /* align-items: center; flex-direction: column; */ } /* Overlays the button pad on top of the camera view*/ .video-overlay-container { - z-index: 1; - position: absolute; - width: 100%; - height: 100%; - box-sizing: border-box; - top: 0; - left: 0; + z-index: 1; + position: absolute; + width: 100%; + height: 100%; + box-sizing: border-box; + top: 0; + left: 0; } -.video-overlay-container.realsense, .video-overlay-container.overhead { - z-index: 2; - position: absolute; - width: calc(100% - 102px); - height: calc(100% - 102px); - box-sizing: border-box; - top: 0; - left: 0; - left: 51px; - top: 51px; - display: flex; - align-items: center; +.video-overlay-container.realsense, +.video-overlay-container.overhead { + z-index: 2; + position: absolute; + width: calc(100% - 102px); + height: calc(100% - 102px); + box-sizing: border-box; + top: 0; + left: 0; + left: 51px; + top: 51px; + display: flex; + align-items: center; } .video-overlay-container.overhead.predictiveDisplay { - z-index: 2; - position: absolute; - width: 100%; - height: 100%; - box-sizing: border-box; - top: 0; - left: 0; - display: flex; - align-items: center; + z-index: 2; + position: absolute; + width: 100%; + height: 100%; + box-sizing: border-box; + top: 0; + left: 0; + display: flex; + align-items: center; } .video-overlay-container::after { - grid-row: 2/2; - grid-column: 1/1; + grid-row: 2/2; + grid-column: 1/1; } .video-area { - position: relative; - grid-row: 2/2; - width: 100%; - max-height: 100%; - max-width: fit-content; /* 100% */ - object-fit: cover; - display: inline-block; + position: relative; + grid-row: 2/2; + width: 100%; + max-height: 100%; + max-width: fit-content; /* 100% */ + object-fit: cover; + display: inline-block; } .video-canvas { - display: inline-block; - width: 100%; - height: auto; - object-fit: cover; + display: inline-block; + width: 100%; + height: auto; + object-fit: cover; } .video-canvas.constrainedHeight { - display: inline-block; - /* width: auto; */ - height: 100%; - object-fit: cover; + display: inline-block; + /* width: auto; */ + height: 100%; + object-fit: cover; } /* Don't display selected highlight on video canvas element */ .video-canvas.customizing::after { - content: none; + content: none; } .video-canvas.customizing { - filter: brightness(0.6); + filter: brightness(0.6); } .video-canvas.customizing.selected { - filter: none; + filter: none; } /* Under video buttons ********************************************************/ .under-video-area { - grid-row: 3; - grid-column: 1; - /* background: blue; */ - /* display: grid; */ - /* flex-wrap: wrap; */ - align-content: flex-start; - padding-top: 0.5rem; - width: 100%; - object-fit: cover; - /* justify-content: center; */ - align-items: center; - overflow-y: auto; - overflow-x: hidden; + grid-row: 3; + grid-column: 1; + /* background: blue; */ + /* display: grid; */ + /* flex-wrap: wrap; */ + align-content: flex-start; + padding-top: 0.5rem; + width: 100%; + object-fit: cover; + /* justify-content: center; */ + align-items: center; + overflow-y: auto; + overflow-x: hidden; } .under-video-area button { - /* height: 30%; */ - margin: 0.2rem 0rem 0.2rem 0rem; - /* flex: 1 0 auto; */ - align-items: center; - width: 99%; + /* height: 30%; */ + margin: 0.2rem 0rem 0.2rem 0rem; + /* flex: 1 0 auto; */ + align-items: center; + width: 99%; } /* Realsense pan-tilt controls ************************************************/ .realsense-pan-tilt-grid { - width: 100%; - /* max-width: fit-content; + width: 100%; + /* max-width: fit-content; height: auto; grid-row: 2/2; grid-column: 1/1; @@ -134,127 +135,124 @@ } .realsense-pan-tilt-grid.constrainedHeight { - height: 100%; - /* width: auto; */ - /* grid-template-columns: var(--pan-tilt-button-size) min-content var(--pan-tilt-button-size); + height: 100%; + /* width: auto; */ + /* grid-template-columns: var(--pan-tilt-button-size) min-content var(--pan-tilt-button-size); grid-template-rows: var(--pan-tilt-button-size) fit-content var(--pan-tilt-button-size); */ - justify-content: center; - object-fit: cover; - z-index: 2; - position: absolute; - display: block; + justify-content: center; + object-fit: cover; + z-index: 2; + position: absolute; + display: block; } .realsense-pan-tilt-grid button { - padding: 0px; + padding: 0px; } .realsense-pan-tilt-grid button .material-icons { - margin: 0; - font-size: xxx-large; - background-color: whitesmoke; - opacity: 40%; - border-radius: 60px; + margin: 0; + font-size: xxx-large; + background-color: whitesmoke; + opacity: 40%; + border-radius: 60px; } .realsense-pan-tilt-grid .up { - border-radius: var(--btn-brdr-radius) var(--btn-brdr-radius) 0 0; - z-index: 2; - width: 100%; - position: absolute; - background: transparent; - box-shadow: none; + border-radius: var(--btn-brdr-radius) var(--btn-brdr-radius) 0 0; + z-index: 2; + width: 100%; + position: absolute; + background: transparent; + box-shadow: none; } .realsense-pan-tilt-grid .down { - border-radius: 0 0 var(--btn-brdr-radius) var(--btn-brdr-radius); - z-index: 2; - width: 100%; - bottom: 0; - left: 0; - position: absolute; - background: transparent; - box-shadow: none; + border-radius: 0 0 var(--btn-brdr-radius) var(--btn-brdr-radius); + z-index: 2; + width: 100%; + bottom: 0; + left: 0; + position: absolute; + background: transparent; + box-shadow: none; } .realsense-pan-tilt-grid .left { - border-radius: var(--btn-brdr-radius) 0 0 var(--btn-brdr-radius); - z-index: 2; - height: 100%; - position: absolute; - background: transparent; - box-shadow: none; + border-radius: var(--btn-brdr-radius) 0 0 var(--btn-brdr-radius); + z-index: 2; + height: 100%; + position: absolute; + background: transparent; + box-shadow: none; } .realsense-pan-tilt-grid .right { - border-radius: 0 var(--btn-brdr-radius) var(--btn-brdr-radius) 0; - z-index: 2; - position: absolute; - right: 0; - height: 100%; - background: transparent; - box-shadow: none; + border-radius: 0 var(--btn-brdr-radius) var(--btn-brdr-radius) 0; + z-index: 2; + position: absolute; + right: 0; + height: 100%; + background: transparent; + box-shadow: none; } - - /* Context menu popup *********************************************************/ .video-context-menu { - list-style-type: none; - position: absolute; - background-color: var(--background-color); - /* border: var(--btn-brdr); */ - margin: 0; - padding: 0; - white-space: nowrap; - - --padding: 1rem; - - padding-top: var(--padding); - grid-row: 2/2; - z-index: 4; + list-style-type: none; + position: absolute; + background-color: var(--background-color); + /* border: var(--btn-brdr); */ + margin: 0; + padding: 0; + white-space: nowrap; + + --padding: 1rem; + + padding-top: var(--padding); + grid-row: 2/2; + z-index: 4; } - .video-context-menu li { - padding: var(--padding); - cursor: pointer; - background-color: inherit; + padding: var(--padding); + cursor: pointer; + background-color: inherit; } .video-context-menu li:hover { - filter: brightness(90%); + filter: brightness(90%); } .video-context-menu::before { - content: attr(aria-label); - font-weight: bold; - padding: var(--padding); + content: attr(aria-label); + font-weight: bold; + padding: var(--padding); } .title { - font-size: x-large; - align-self: flex-end; - text-align: center; - margin: 10px; + font-size: x-large; + align-self: flex-end; + text-align: center; + margin: 10px; } -@media screen and (orientation:portrait) and (max-device-width: 900px) { - .video-container { - grid-template-rows: 1fr auto minmax(3rem, 1fr); - } +@media screen and (orientation: portrait) and (max-device-width: 900px) { + .video-container { + grid-template-rows: 1fr auto minmax(3rem, 1fr); + } } -@media screen and (orientation:landscape) and (max-device-width: 900px) { - .video-container { - grid-template-rows: auto; - } - - .realsense-pan-tilt-grid button .material-icons { - font-size: larger; - } - - .title { - font-size: smaller; - } -} \ No newline at end of file +@media screen and (orientation: landscape) and (max-device-width: 900px) { + .video-container { + grid-template-rows: auto; + } + + .realsense-pan-tilt-grid button .material-icons { + font-size: larger; + } + + .title { + font-size: smaller; + } +} diff --git a/src/pages/operator/css/CustomizeButton.css b/src/pages/operator/css/CustomizeButton.css index 6b3804f1..3b83b732 100644 --- a/src/pages/operator/css/CustomizeButton.css +++ b/src/pages/operator/css/CustomizeButton.css @@ -1,16 +1,16 @@ #customize-button { - display: flex; - align-items: center; - justify-content: center; - width: var(--header-btn-width); + display: flex; + align-items: center; + justify-content: center; + width: var(--header-btn-width); } #customize-button span { - margin-right: 2rem; + margin-right: 2rem; } -@media(max-width:900px) { - #customize-button { - width: var(--btn-header-width-med); - } -} \ No newline at end of file +@media (max-width: 900px) { + #customize-button { + width: var(--btn-header-width-med); + } +} diff --git a/src/pages/operator/css/DropZone.css b/src/pages/operator/css/DropZone.css index a1b5fc01..30dbcbee 100644 --- a/src/pages/operator/css/DropZone.css +++ b/src/pages/operator/css/DropZone.css @@ -1,60 +1,60 @@ .drop-zone { - background-color: hsl(0, 0%, 90%); - border: 0.2rem dashed gray; - border-radius: 10px; - opacity: 100%; - flex: 0 0 2rem; - margin: 1rem 0.5rem; - cursor: pointer; - text-align: center; - transition-property: opacity, flex, width; - transition-duration: 0.4s; - transition-timing-function: ease-out; + background-color: hsl(0, 0%, 90%); + border: 0.2rem dashed gray; + border-radius: 10px; + opacity: 100%; + flex: 0 0 2rem; + margin: 1rem 0.5rem; + cursor: pointer; + text-align: center; + transition-property: opacity, flex, width; + transition-duration: 0.4s; + transition-timing-function: ease-out; } .drop-zone.standard { - height: 90%; + height: 90%; } /* Drop zone in layout or tab content*/ .drop-zone.standard, .drop-zone.overlay { - display: flex; - align-items: center; - justify-content: center; - width: -webkit-fill-available; + display: flex; + align-items: center; + justify-content: center; + width: -webkit-fill-available; } /* Dropzone is a tab in a tabs component header */ .drop-zone.material-icons.tab { - margin: 0; - width: 5rem; - padding-top: 10px; + margin: 0; + width: 5rem; + padding-top: 10px; - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; - /* border-bottom: none; */ + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + /* border-bottom: none; */ } /* Dropzone is an overlay in a video stream*/ .drop-zone.overlay { - margin: 0; - width: 100%; - height: 100%; - opacity: 60%; - flex-grow: 1; + margin: 0; + width: 100%; + height: 100%; + opacity: 60%; + flex-grow: 1; } .drop-zone[hidden] { - flex: 0; - width: 0; - opacity: 0; - margin: 0; - padding: 0; - border: none; - visibility: hidden; + flex: 0; + width: 0; + opacity: 0; + margin: 0; + padding: 0; + border: none; + visibility: hidden; } .drop-zone:hover { - background-color: hsl(0, 0%, 50%); -} \ No newline at end of file + background-color: hsl(0, 0%, 50%); +} diff --git a/src/pages/operator/css/LayoutArea.css b/src/pages/operator/css/LayoutArea.css index 66b11a5d..6f68b847 100644 --- a/src/pages/operator/css/LayoutArea.css +++ b/src/pages/operator/css/LayoutArea.css @@ -1,38 +1,38 @@ #layout-area { - /* padding: var(--screen-padding); + /* padding: var(--screen-padding); flex: 1 1 0; */ - padding: var(--screen-padding); - flex-direction: column; - height: auto; + padding: var(--screen-padding); + flex-direction: column; + height: auto; } /* Remove margin for elements at edges of the layout area */ -#layout-area > *:first-of-type{ - margin-left: 0; +#layout-area > *:first-of-type { + margin-left: 0; } -#layout-area > *:last-of-type{ - margin-right: 0; +#layout-area > *:last-of-type { + margin-right: 0; } #layout-area, .tabs-content { - display: flex; - justify-content: space-evenly; - align-items: center; + display: flex; + justify-content: space-evenly; + align-items: center; } -@media screen and (orientation:portrait) and (max-device-width: 900px) { - #layout-area, - .tabs-content { - flex-direction: column; - padding: 1rem; - grid-gap: 0.3rem; - } +@media screen and (orientation: portrait) and (max-device-width: 900px) { + #layout-area, + .tabs-content { + flex-direction: column; + padding: 1rem; + grid-gap: 0.3rem; + } } -@media screen and (orientation:landscape) and (max-device-width: 900px) { - #layout-area, - .tabs-content { - overflow: scroll; - } -} \ No newline at end of file +@media screen and (orientation: landscape) and (max-device-width: 900px) { + #layout-area, + .tabs-content { + overflow: scroll; + } +} diff --git a/src/pages/operator/css/Map.css b/src/pages/operator/css/Map.css index 7ad9ac63..dbf816d5 100644 --- a/src/pages/operator/css/Map.css +++ b/src/pages/operator/css/Map.css @@ -1,152 +1,153 @@ -.map-container, .mobile-map-container { - position: relative; - /* flex: 1 1 0; */ - display: grid; - justify-items: center; - padding: 0.4rem; - max-height: 100%; - grid-template-columns: auto; - /* grid-template-rows: 1fr auto minmax(9rem, 1fr); */ - object-fit: cover; - justify-content: center; - /* align-items: center; +.map-container, +.mobile-map-container { + position: relative; + /* flex: 1 1 0; */ + display: grid; + justify-items: center; + padding: 0.4rem; + max-height: 100%; + grid-template-columns: auto; + /* grid-template-rows: 1fr auto minmax(9rem, 1fr); */ + object-fit: cover; + justify-content: center; + /* align-items: center; flex-direction: column; */ - z-index: 1; + z-index: 1; } .map-container { - flex: 1 1 0; - /* display: flex; + flex: 1 1 0; + /* display: flex; flex-direction: column; */ - /* overflow: hidden; */ + /* overflow: hidden; */ } .map-title { - font-size: x-large; - text-align: center; - margin: 10px; + font-size: x-large; + text-align: center; + margin: 10px; } .map { - align-self: center; - width: auto; - height: 100%; - z-index: 1; + align-self: center; + width: auto; + height: 100%; + z-index: 1; } .map.constrainedHeight { - width: auto; - height: 100%; + width: auto; + height: 100%; } .mapCanvas { - max-width: 100%; - max-height: 100%; + max-width: 100%; + max-height: 100%; } .dropdown em { - color: #6a6a6a; - margin-right: 1rem; + color: #6a6a6a; + margin-right: 1rem; } .mobile { - padding: var(--btn-padding); - /* font-size: 30px; */ - flex: auto; + padding: var(--btn-padding); + /* font-size: 30px; */ + flex: auto; } .mobile-map-save-btn { - width: 54%; - border-radius: 13px; - border: 5px solid whitesmoke; - padding: 10px; - margin: 0.5rem; - vertical-align: middle; - display: flex; - justify-content: space-evenly; - align-items: center; + width: 54%; + border-radius: 13px; + border: 5px solid whitesmoke; + padding: 10px; + margin: 0.5rem; + vertical-align: middle; + display: flex; + justify-content: space-evenly; + align-items: center; } .map-save-btn { - width: 100%; - border-radius: 13px; - padding: 8px; - vertical-align: middle; - display: flex; - justify-content: center; - align-items: center; - margin-bottom: 0.5rem; - text-align: center; + width: 100%; + border-radius: 13px; + padding: 8px; + vertical-align: middle; + display: flex; + justify-content: center; + align-items: center; + margin-bottom: 0.5rem; + text-align: center; } .mobile-map-play-btn { - width: 32%; - border-radius: 13px; - border: 5px solid whitesmoke; - padding: 10px; - margin: 0.5rem; - text-align: center; - background: #06c7e1; - vertical-align: middle; - display: flex; - justify-content: space-evenly; - align-items: center; + width: 32%; + border-radius: 13px; + border: 5px solid whitesmoke; + padding: 10px; + margin: 0.5rem; + text-align: center; + background: #06c7e1; + vertical-align: middle; + display: flex; + justify-content: space-evenly; + align-items: center; } .map-play-btn { - width: 100%; - border-radius: 13px; - padding: 8px; - text-align: center; - background: var(--selected-color); - vertical-align: middle; - display: flex; - align-items: center; - margin-top: 0.5rem; - justify-content: center; - margin-bottom: 0.5rem; + width: 100%; + border-radius: 13px; + padding: 8px; + text-align: center; + background: var(--selected-color); + vertical-align: middle; + display: flex; + align-items: center; + margin-top: 0.5rem; + justify-content: center; + margin-bottom: 0.5rem; } .mobile-map-cancel-btn { - width: 32%; - border-radius: 13px; - border: 5px solid whitesmoke; - padding: 10px; - margin: 0.5rem; - text-align: center; - background: #cd0b0b; - vertical-align: middle; - display: flex; - justify-content: space-evenly; - align-items: center; - color: white; + width: 32%; + border-radius: 13px; + border: 5px solid whitesmoke; + padding: 10px; + margin: 0.5rem; + text-align: center; + background: #cd0b0b; + vertical-align: middle; + display: flex; + justify-content: space-evenly; + align-items: center; + color: white; } .map-cancel-btn { - width: 100%; - border-radius: 13px; - padding: 8px; - text-align: center; - vertical-align: middle; - display: flex; - align-items: center; - margin-top: 0.5rem; - justify-content: center; - margin-bottom: 0.5rem; - background: #cd0b0b; - display: flex; - color: white; + width: 100%; + border-radius: 13px; + padding: 8px; + text-align: center; + vertical-align: middle; + display: flex; + align-items: center; + margin-top: 0.5rem; + justify-content: center; + margin-bottom: 0.5rem; + background: #cd0b0b; + display: flex; + color: white; } .map-fn-btns { - display: grid; - font-size: 18px; - margin-top: 1rem; - margin-bottom: 0.5rem; - width: 100%; + display: grid; + font-size: 18px; + margin-top: 1rem; + margin-bottom: 0.5rem; + width: 100%; } .map-fn-btns-mobile { - display: flex; - /* font-size: 45px; */ -} \ No newline at end of file + display: flex; + /* font-size: 45px; */ +} diff --git a/src/pages/operator/css/MobileOperator.css b/src/pages/operator/css/MobileOperator.css index b4a085e4..0cd8efe8 100644 --- a/src/pages/operator/css/MobileOperator.css +++ b/src/pages/operator/css/MobileOperator.css @@ -1,214 +1,224 @@ #mobile-operator { - height: 100%; - touch-action: none; - /* display: grid; */ - grid-template-rows: auto auto 1fr; - grid-template-columns: 1fr auto; - background-color: #7f7f7f; + height: 100%; + touch-action: none; + /* display: grid; */ + grid-template-rows: auto auto 1fr; + grid-template-columns: 1fr auto; + background-color: #7f7f7f; } .switch-camera { - z-index: 5; - position: absolute; - border-radius: 0px; - opacity: 0.5; - font-size: 2rem; - padding: 0; - float: right; - text-align: right; - margin-left: 85%; - color: black; + z-index: 5; + position: absolute; + border-radius: 0px; + opacity: 0.5; + font-size: 2rem; + padding: 0; + float: right; + text-align: right; + margin-left: 85%; + color: black; } .switch-camera .material-icons { - color: black; + color: black; } .record { - z-index: 5; - position: absolute; - border-radius: 0px; - opacity: 0.65; - /* font-size: 50px; */ - text-align: right; - display: flex; - padding: 0.5rem; - align-items: center; - color: black; + z-index: 5; + position: absolute; + border-radius: 0px; + opacity: 0.65; + /* font-size: 50px; */ + text-align: right; + display: flex; + padding: 0.5rem; + align-items: center; + color: black; } .depth-sensing { - float: right; - z-index: 5; - position: absolute; - border-radius: 0px; - opacity: 0.5; - /* font-size: 50px; */ - text-align: right; - display: flex; - align-items: center; - /* width: 20rem; */ - /* margin-top: 85%; */ - /* margin-left: 68%; */ - background: whitesmoke; + float: right; + z-index: 5; + position: absolute; + border-radius: 0px; + opacity: 0.5; + /* font-size: 50px; */ + text-align: right; + display: flex; + align-items: center; + /* width: 20rem; */ + /* margin-top: 85%; */ + /* margin-left: 68%; */ + background: whitesmoke; } .pill { - border-radius: 1.5rem; + border-radius: 1.5rem; } .active-color { - color:white; - background-color: #084298; + color: white; + background-color: #084298; } #mobile-operator-body { - display: flex; - flex-direction: column; - height: 100%; - /* flex: 1 1 0; + display: flex; + flex-direction: column; + height: 100%; + /* flex: 1 1 0; grid-column: 1/1; */ - /* grid-row: 3; */ + /* grid-row: 3; */ } .mobile-alert { - position: absolute; - width: 100%; - z-index: 6; + position: absolute; + width: 100%; + z-index: 6; } /** https://stackoverflow.com/a/40989121 **/ .loader { - position: absolute; - top: calc(50% - 5em); - left: calc(50% - 5em); - width: 11em; - height: 11em; - border: 1.1em solid rgba(0, 0, 0, 0.2); - border-left: 1.1em solid #000000; - border-radius: 50%; - animation: load 1s infinite linear; + position: absolute; + top: calc(50% - 5em); + left: calc(50% - 5em); + width: 11em; + height: 11em; + border: 1.1em solid rgba(0, 0, 0, 0.2); + border-left: 1.1em solid #000000; + border-radius: 50%; + animation: load 1s infinite linear; } .loading-text { - position: absolute; - top: calc(50% - 1em); - left: calc(50% - 2em); - font-size: large; - z-index: 4; + position: absolute; + top: calc(50% - 1em); + left: calc(50% - 2em); + font-size: large; + z-index: 4; } .control-modes { - touch-action: none; - font-size: 3.5rem; - justify-content: center; - display: flex; - padding-top: 20px; - gap: 15px; - padding-bottom: 50px; - -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + touch-action: none; + font-size: 3.5rem; + justify-content: center; + display: flex; + padding-top: 20px; + gap: 15px; + padding-bottom: 50px; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } @keyframes load { - 0% { - transform: rotate(0deg); - } + 0% { + transform: rotate(0deg); + } - 100% { - transform: rotate(360deg); - } + 100% { + transform: rotate(360deg); + } } -@media(max-width:900px) { - #operator-header .dropdown { - width: var(--header-btn-width-med); - } +@media (max-width: 900px) { + #operator-header .dropdown { + width: var(--header-btn-width-med); + } - #operator-header>*, - #operator-header button { - height: 3rem; - } + #operator-header > *, + #operator-header button { + height: 3rem; + } - .operator-voice, - .operator-pose-library, - .operator-pose-recorder, - .operator-aruco-markers { - width: 30rem; - height: 5rem; - } + .operator-voice, + .operator-pose-library, + .operator-pose-recorder, + .operator-aruco-markers { + width: 30rem; + height: 5rem; + } } .slider { - height: 15px; - touch-action: none; - pointer-events: all; + height: 15px; + touch-action: none; + pointer-events: all; } -.slider-container { - display: flex; - align-items: baseline; - justify-content: space-around; - padding-top: 16px; +.slider-container { + display: flex; + align-items: baseline; + justify-content: space-around; + padding-top: 16px; } .slider { - -webkit-appearance: none; - width: 70%; - /* height: 30px; */ - border-radius: 19px; - background: #d3d3d3; - outline: none; - opacity: 0.7; - -webkit-transition: .2s; - transition: opacity .2s; - } - + -webkit-appearance: none; + width: 70%; + /* height: 30px; */ + border-radius: 19px; + background: #d3d3d3; + outline: none; + opacity: 0.7; + -webkit-transition: 0.2s; + transition: opacity 0.2s; +} + .slider::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: 25px; - height: 25px; - border-radius: 50%; - background: #4CAF50; - cursor: pointer; + -webkit-appearance: none; + appearance: none; + width: 25px; + height: 25px; + border-radius: 50%; + background: #4caf50; + cursor: pointer; } .label { - font-size: 20px; - padding-bottom: 10px; + font-size: 20px; + padding-bottom: 10px; } -.map, .controls { - display: contents; +.map, +.controls { + display: contents; } -.map.hideMap, .controls.hideControls { - display: none; +.map.hideMap, +.controls.hideControls { + display: none; } .record-circle { - width: 20px; - height: 20px; - background-color: #bd1919; - border-radius: 50%; - margin: 0.5rem; + width: 20px; + height: 20px; + background-color: #bd1919; + border-radius: 50%; + margin: 0.5rem; } /** https://codepen.io/vram1980/pen/oNvWdO */ .recording { - border: 3px solid #bd1919; - -webkit-border-radius: 30px; - height: 30px; - width: 30px; - position: absolute; - left: 11px; - top: 11px; - -webkit-animation: pulsate 1s ease-out; - -webkit-animation-iteration-count: infinite; - opacity: 0.0; + border: 3px solid #bd1919; + -webkit-border-radius: 30px; + height: 30px; + width: 30px; + position: absolute; + left: 11px; + top: 11px; + -webkit-animation: pulsate 1s ease-out; + -webkit-animation-iteration-count: infinite; + opacity: 0; } @-webkit-keyframes pulsate { - 0% {-webkit-transform: scale(0.1, 0.1); opacity: 0.0;} - 50% {opacity: 1.0;} - 100% {-webkit-transform: scale(1.2, 1.2); opacity: 0.0;} -} \ No newline at end of file + 0% { + -webkit-transform: scale(0.1, 0.1); + opacity: 0; + } + 50% { + opacity: 1; + } + 100% { + -webkit-transform: scale(1.2, 1.2); + opacity: 0; + } +} diff --git a/src/pages/operator/css/MovementRecorder.css b/src/pages/operator/css/MovementRecorder.css index 728167d5..fa813a00 100644 --- a/src/pages/operator/css/MovementRecorder.css +++ b/src/pages/operator/css/MovementRecorder.css @@ -1,54 +1,54 @@ #movement-recorder-container { - display: flex; - gap: 15px; - align-items: center; - justify-content: center; + display: flex; + gap: 15px; + align-items: center; + justify-content: center; } .play-btn { - background-color: var(--selected-color); - display: flex; + background-color: var(--selected-color); + display: flex; } .save-btn { - background-color: var(--btn-turquoise); - display: flex; + background-color: var(--btn-turquoise); + display: flex; } -.delete-btn { - background-color: var(--btn-red); - display: flex; +.delete-btn { + background-color: var(--btn-red); + display: flex; } -@media(max-width:1300px) { - #movement-recorder-container { - font-size: smaller; - } +@media (max-width: 1300px) { + #movement-recorder-container { + font-size: smaller; + } } .mobile-movement-save-btn { - border-radius: 13px; - border: 5px solid whitesmoke; - font-size: 25px; - padding: 10px; - margin: 1rem; - text-align: center; - background: #06c7e1; - vertical-align: middle; - display: flex; - justify-content: center; + border-radius: 13px; + border: 5px solid whitesmoke; + font-size: 25px; + padding: 10px; + margin: 1rem; + text-align: center; + background: #06c7e1; + vertical-align: middle; + display: flex; + justify-content: center; } .mobile-movement-play-btn { - /* width: 97%; */ - border-radius: 13px; - border: 5px solid whitesmoke; - font-size: 20px; - padding: 10px; - margin: 0.5rem; - text-align: center; - background: #06c7e1; - vertical-align: middle; - display: flex; - justify-content: center; -} \ No newline at end of file + /* width: 97%; */ + border-radius: 13px; + border: 5px solid whitesmoke; + font-size: 20px; + padding: 10px; + margin: 0.5rem; + text-align: center; + background: #06c7e1; + vertical-align: middle; + display: flex; + justify-content: center; +} diff --git a/src/pages/operator/css/Operator.css b/src/pages/operator/css/Operator.css index 3b7e11f2..b0cdb988 100644 --- a/src/pages/operator/css/Operator.css +++ b/src/pages/operator/css/Operator.css @@ -1,195 +1,195 @@ #operator { - height: 100%; - display: grid; - grid-template-rows: auto auto 1fr; - grid-template-columns: 1fr auto; + height: 100%; + display: grid; + grid-template-rows: auto auto 1fr; + grid-template-columns: 1fr auto; - --screen-padding: 1rem; + --screen-padding: 1rem; } #operator-header { - background-color: var(--gray-bg); - width: 100%; - box-sizing: border-box; - padding: var(--screen-padding); - display: flex; - align-items: center; - justify-content: space-between; - grid-row: 1; - grid-column: 1/3; - --header-btn-width: 11rem; - --header-btn-width-med: 7rem; + background-color: var(--gray-bg); + width: 100%; + box-sizing: border-box; + padding: var(--screen-padding); + display: flex; + align-items: center; + justify-content: space-between; + grid-row: 1; + grid-column: 1/3; + --header-btn-width: 11rem; + --header-btn-width-med: 7rem; } .operator-collision-alerts { - width: 100%; - /* padding: 0.5rem 1rem 0 1rem; */ - position: absolute; + width: 100%; + /* padding: 0.5rem 1rem 0 1rem; */ + position: absolute; } .operator-alert { - font-size: 1.5rem; + font-size: 1.5rem; } .operator-collision-alerts .operator-alert { - animation: fade-out 1s; + animation: fade-out 1s; } .operator-collision-alerts .operator-alert.fadeOut { - display: none; - animation: fade-out 2s; - opacity: 0; + display: none; + animation: fade-out 2s; + opacity: 0; } .operator-collision-alerts .operator-alert.fadeIn { - display: block; - animation: fade-in 0.5s; + display: block; + animation: fade-in 0.5s; } @keyframes fade-in { - from { - opacity: 0; - } + from { + opacity: 0; + } - to { - opacity: 1; - } + to { + opacity: 1; + } } @keyframes fade-out { - from { - opacity: 1; - } - - to { - opacity: 0; - } + from { + opacity: 1; + } + + to { + opacity: 0; + } } -/* Make all the components in the header fill the avaliable height */ -#operator-header>*, +/* Make all the components in the header fill the available height */ +#operator-header > *, #operator-header button { - height: 100%; + height: 100%; } #operator-header .dropdown { - width: var(--header-btn-width); + width: var(--header-btn-width); } .operator-voice, .operator-pose-library, .operator-pose-recorder, .operator-aruco-markers { - background-color: whitesmoke; - box-shadow: var(--shadow); - height: 6rem; - width: 40rem; - display: inline-grid; - align-items: center; - justify-content: center; - /* justify-self: center; */ - align-self: center; - border-radius: 200px; - /* margin-top: 10px; */ - /* grid-row: 2/2; + background-color: whitesmoke; + box-shadow: var(--shadow); + height: 6rem; + width: 40rem; + display: inline-grid; + align-items: center; + justify-content: center; + /* justify-self: center; */ + align-self: center; + border-radius: 200px; + /* margin-top: 10px; */ + /* grid-row: 2/2; grid-column: 1/1; */ - transition: all 0.2s ease-out; - font-size: large; + transition: all 0.2s ease-out; + font-size: large; } .operator-pose-library.hideLabels, .operator-pose-recorder.hideLabels { - width: 32rem; + width: 32rem; } .operator-aruco-markers.hideLabels { - width: 43rem; + width: 43rem; } .operator-aruco-markers { - width: 66rem; + width: 66rem; } .operator-voice[hidden], .operator-pose-library[hidden], .operator-pose-recorder[hidden], .operator-aruco-markers[hidden] { - display: none; + display: none; } #operator-global-controls { - display: flex; - flex-wrap: wrap; - row-gap: 10px; - column-gap: 10px; - justify-content: center; + display: flex; + flex-wrap: wrap; + row-gap: 10px; + column-gap: 10px; + justify-content: center; } #operator-body { - display: flex; - justify-content: center; - flex-flow: row; - flex: 1 1 0; - grid-column: 1/1; - grid-row: 3; + display: flex; + justify-content: center; + flex-flow: row; + flex: 1 1 0; + grid-column: 1/1; + grid-row: 3; } /** https://stackoverflow.com/a/40989121 **/ .loader { - position: absolute; - top: calc(50% - 5em); - left: calc(50% - 5em); - width: 11em; - height: 11em; - border: 1.1em solid rgba(0, 0, 0, 0.2); - border-left: 1.1em solid #000000; - border-radius: 50%; - animation: load 1s infinite linear; - background-color: white; - z-index: 4; + position: absolute; + top: calc(50% - 5em); + left: calc(50% - 5em); + width: 11em; + height: 11em; + border: 1.1em solid rgba(0, 0, 0, 0.2); + border-left: 1.1em solid #000000; + border-radius: 50%; + animation: load 1s infinite linear; + background-color: white; + z-index: 4; } .loading-text { - position: absolute; - top: calc(50% - 1em); - left: calc(50% - 2em); - font-size: large; - z-index: 4; + position: absolute; + top: calc(50% - 1em); + left: calc(50% - 2em); + font-size: large; + z-index: 4; } .reconnecting-text { - position: absolute; - top: calc(50% - 1em); - left: calc(50% - 3em); - font-size: large; - z-index: 5; + position: absolute; + top: calc(50% - 1em); + left: calc(50% - 3em); + font-size: large; + z-index: 5; } @keyframes load { - 0% { - transform: rotate(0deg); - } - - 100% { - transform: rotate(360deg); - } -} - -@media(max-width:900px) { - #operator-header .dropdown { - width: var(--header-btn-width-med); - } - - #operator-header>*, - #operator-header button { - height: 3rem; - } - - .operator-voice, - .operator-pose-library, - .operator-pose-recorder, - .operator-aruco-markers { - width: 30rem; - height: 5rem; - } -} \ No newline at end of file + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +@media (max-width: 900px) { + #operator-header .dropdown { + width: var(--header-btn-width-med); + } + + #operator-header > *, + #operator-header button { + height: 3rem; + } + + .operator-voice, + .operator-pose-library, + .operator-pose-recorder, + .operator-aruco-markers { + width: 30rem; + height: 5rem; + } +} diff --git a/src/pages/operator/css/Panel.css b/src/pages/operator/css/Panel.css index 7550651b..531af9c4 100644 --- a/src/pages/operator/css/Panel.css +++ b/src/pages/operator/css/Panel.css @@ -1,83 +1,83 @@ .tabs-component { - height: 100%; - display: flex; - flex-direction: column; - flex: 1 1 0; - /* margin: 0 0.5rem; */ - margin-top: 0.5rem; /* new */ - width: 100%; /* new */ - box-shadow: var(--shadow); - position: relative; + height: 100%; + display: flex; + flex-direction: column; + flex: 1 1 0; + /* margin: 0 0.5rem; */ + margin-top: 0.5rem; /* new */ + width: 100%; /* new */ + box-shadow: var(--shadow); + position: relative; - --radius: .3rem; - border-radius: var(--radius) var(--radius) 0px 0px; + --radius: 0.3rem; + border-radius: var(--radius) var(--radius) 0px 0px; } .tabs-component.customizing.selected::after { - background-color: var(--selected-color); + background-color: var(--selected-color); } .tabs-content { - border: 3px solid var(--btn-blue); - flex: 1 1 0; - background-color: var(--background-color); + border: 3px solid var(--btn-blue); + flex: 1 1 0; + background-color: var(--background-color); } /* Header *********************************************************************/ .tabs-header { - display: flex; - flex-wrap: wrap; - overflow: hidden; - border-radius: var(--radius) var(--radius) 0px 0px; + display: flex; + flex-wrap: wrap; + overflow: hidden; + border-radius: var(--radius) var(--radius) 0px 0px; } /* Apply to all the tabs in the header */ -.tabs-header>* { - flex: 1 0 auto; - background-color: var(--tab-inactive); - border-radius: var(--radius) var(--radius) 0px 0px; +.tabs-header > * { + flex: 1 0 auto; + background-color: var(--tab-inactive); + border-radius: var(--radius) var(--radius) 0px 0px; } /* Format tabs with icons */ -.tabs-header>.material-icons { - vertical-align: bottom; - padding-top: 3px; - padding-bottom: 3px; +.tabs-header > .material-icons { + vertical-align: bottom; + padding-top: 3px; + padding-bottom: 3px; } .tab-button { - border: none; - font-size: 1.5rem; - z-index: 0; + border: none; + font-size: 1.5rem; + z-index: 0; } .tab-button.active { - background-color: var(--btn-blue); - color: var(--font-white); - /* z-index: 2; */ - box-shadow: 0px 2px 2px 2px rgb(112 112 112 / 47%); + background-color: var(--btn-blue); + color: var(--font-white); + /* z-index: 2; */ + box-shadow: 0px 2px 2px 2px rgb(112 112 112 / 47%); } .tab-button.selected { - box-shadow: 0 0 0.2rem 0.2rem var(--selected-color); + box-shadow: 0 0 0.2rem 0.2rem var(--selected-color); } .add-tab:hover { - background-color: var(--btn-turquoise); - color: white; + background-color: var(--btn-turquoise); + color: white; } /* Popup for adding a new tab *************************************************/ -@media(max-width:900px) { - .tab-button { - font-size: medium; - } +@media (max-width: 900px) { + .tab-button { + font-size: medium; + } } -@media screen and (orientation:portrait) and (max-device-width: 900px) { - .tabs-component { - width: 100%; - } -} \ No newline at end of file +@media screen and (orientation: portrait) and (max-device-width: 900px) { + .tabs-component { + width: 100%; + } +} diff --git a/src/pages/operator/css/PoseLibrary.css b/src/pages/operator/css/PoseLibrary.css index 685dcad8..7c9251d4 100644 --- a/src/pages/operator/css/PoseLibrary.css +++ b/src/pages/operator/css/PoseLibrary.css @@ -1,27 +1,27 @@ #pose-library-container { - display: flex; - gap: 15px; - align-items: center; - justify-content: center; + display: flex; + gap: 15px; + align-items: center; + justify-content: center; } .play-btn { - background-color: var(--selected-color); - display: flex; + background-color: var(--selected-color); + display: flex; } .save-btn { - background-color: var(--btn-turquoise); - display: flex; + background-color: var(--btn-turquoise); + display: flex; } -.delete-btn { - background-color: var(--btn-red); - display: flex; +.delete-btn { + background-color: var(--btn-red); + display: flex; } -@media(max-width:1300px) { - #pose-library-container { - font-size: smaller; - } -} \ No newline at end of file +@media (max-width: 1300px) { + #pose-library-container { + font-size: smaller; + } +} diff --git a/src/pages/operator/css/PredictiveDisplay.css b/src/pages/operator/css/PredictiveDisplay.css index 54c745fc..1fbd5735 100644 --- a/src/pages/operator/css/PredictiveDisplay.css +++ b/src/pages/operator/css/PredictiveDisplay.css @@ -1,21 +1,21 @@ .predictive-display { - width: 100%; - height: 100%; - stroke: gray; - cursor: crosshair; - fill: none; + width: 100%; + height: 100%; + stroke: gray; + cursor: crosshair; + fill: none; } .predictive-display.customizing { - cursor: default; + cursor: default; } .predictive-display path { - stroke-width: 8; - stroke: var(--path-blue); - stroke-linecap: round; + stroke-width: 8; + stroke: var(--path-blue); + stroke-linecap: round; } .predictive-display.moving path { - stroke: red; + stroke: red; } diff --git a/src/pages/operator/css/RadioGroup.css b/src/pages/operator/css/RadioGroup.css index a1febe66..14425df5 100644 --- a/src/pages/operator/css/RadioGroup.css +++ b/src/pages/operator/css/RadioGroup.css @@ -1,70 +1,69 @@ .radio-btn-mobile { - font-size: 20px; - border-radius: 13px; - background-color: whitesmoke; - margin: 0.5rem; - border: 3px solid whitesmoke; - filter: drop-shadow(6px 7px 2px rgb(0 0 0 / 0.4)); - + font-size: 20px; + border-radius: 13px; + background-color: whitesmoke; + margin: 0.5rem; + border: 3px solid whitesmoke; + filter: drop-shadow(6px 7px 2px rgb(0 0 0 / 0.4)); } .radio-btn { - font-size: 20px; - border-radius: 4px; - background-color: whitesmoke; - padding: 5px; - margin: 0.5rem; - filter: drop-shadow(4px 2px 2px rgb(0 0 0 / 0.4)); + font-size: 20px; + border-radius: 4px; + background-color: whitesmoke; + padding: 5px; + margin: 0.5rem; + filter: drop-shadow(4px 2px 2px rgb(0 0 0 / 0.4)); } .radio { - height: 1rem; - width: 1rem; - margin-right: 2rem; + height: 1rem; + width: 1rem; + margin-right: 2rem; } .radio-mobile { - height: 1.25rem; - width: 1.25rem; - margin-right: 1.5rem; + height: 1.25rem; + width: 1.25rem; + margin-right: 1.5rem; } .radio-group { - overflow-y: auto; - width: 100%; - max-height: 18vh; + overflow-y: auto; + width: 100%; + max-height: 18vh; } .radio-group-mobile { - overflow-y: scroll; + overflow-y: scroll; } .modify { - float: right; - font-size: 25px; + float: right; + font-size: 25px; } .radio-icon { - padding-right: 2rem; + padding-right: 2rem; } .add-btn { - width: 97%; - border-radius: 13px; - border: 5px solid whitesmoke; - font-size: 45px; - padding: 20px; - margin: 1rem; - text-align: center; + width: 97%; + border-radius: 13px; + border: 5px solid whitesmoke; + font-size: 45px; + padding: 20px; + margin: 1rem; + text-align: center; } .start-btn { - width: 97%; - border-radius: 13px; - border: 5px solid whitesmoke; - font-size: 45px; - padding: 20px; - margin: 1rem; - text-align: center; - background: #06c7e1; -} \ No newline at end of file + width: 97%; + border-radius: 13px; + border: 5px solid whitesmoke; + font-size: 45px; + padding: 20px; + margin: 1rem; + text-align: center; + background: #06c7e1; +} diff --git a/src/pages/operator/css/RunStopButton.css b/src/pages/operator/css/RunStopButton.css index 5cd2f58f..38ffd19f 100644 --- a/src/pages/operator/css/RunStopButton.css +++ b/src/pages/operator/css/RunStopButton.css @@ -1,22 +1,23 @@ .run-stop-button { - max-width: 110px; - width: 100%; + max-width: 110px; + width: 100%; } .enabled { - animation: blinker 1s linear infinite; - filter: brightness(30%) saturate(100%) invert(10%) sepia(28%) saturate(4155%) hue-rotate(0deg) contrast(119%); + animation: blinker 1s linear infinite; + filter: brightness(30%) saturate(100%) invert(10%) sepia(28%) saturate(4155%) + hue-rotate(0deg) contrast(119%); } -@keyframes blinker { - 50% { - opacity: 0; - } +@keyframes blinker { + 50% { + opacity: 0; + } } .runStopContainer { - display: flex; - flex-direction: column; - align-items: center; - text-align: center; -} \ No newline at end of file + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} diff --git a/src/pages/operator/css/Sidebar.css b/src/pages/operator/css/Sidebar.css index 35901ef5..cf3437fc 100644 --- a/src/pages/operator/css/Sidebar.css +++ b/src/pages/operator/css/Sidebar.css @@ -1,199 +1,196 @@ #sidebar { - width: 20rem; - grid-row: 2/4; - grid-column: 2; - height: 100%; - background-color: var(--gray-bg); - display: grid; - grid-template-rows: 5rem auto 7rem; - transition: width 0.5s ease-out; - /* Darken the gray background color from index.css */ - --header-bg: color-mix(in srgb, var(--gray-bg) 90%, black); - white-space: nowrap; + width: 20rem; + grid-row: 2/4; + grid-column: 2; + height: 100%; + background-color: var(--gray-bg); + display: grid; + grid-template-rows: 5rem auto 7rem; + transition: width 0.5s ease-out; + /* Darken the gray background color from index.css */ + --header-bg: color-mix(in srgb, var(--gray-bg) 90%, black); + white-space: nowrap; } #sidebar[hidden] { - width: 0; + width: 0; } - #sidebar p { - margin-top: 0; - margin-bottom: 1rem; + margin-top: 0; + margin-bottom: 1rem; } - #sidebar-header { - background-color: var(--header-bg); - padding: 1rem; - display: flex; - align-items: center; - font-size: 18px; + background-color: var(--header-bg); + padding: 1rem; + display: flex; + align-items: center; + font-size: 18px; } #sidebar-body { - padding: 1rem; - box-shadow: inset 0 0 3px 0px var(--shadow-color); + padding: 1rem; + box-shadow: inset 0 0 3px 0px var(--shadow-color); - display: flex; - flex-direction: column; - justify-content: space-between; + display: flex; + flex-direction: column; + justify-content: space-between; } /* Footer *********************************************************************/ #sidebar-footer { - background-color: var(--header-bg); + background-color: var(--header-bg); } #delete-button { - font-size: 3rem; - padding: 1rem; - /* Center it */ - display: block; - width: 90%; - margin: 0.8rem auto; + font-size: 3rem; + padding: 1rem; + /* Center it */ + display: block; + width: 90%; + margin: 0.8rem auto; } /* Options ********************************************************************/ -#sidebar-options>button { - height: 5rem; - width: 100%; +#sidebar-options > button { + height: 5rem; + width: 100%; } .toggle-button-div { - padding-bottom: 10px; + padding-bottom: 10px; } .toggle-button { - display: inline-block; - margin-right: 1rem; - width: 7rem; - height: 5rem; - border-radius: var(--btn-brdr-radius); + display: inline-block; + margin-right: 1rem; + width: 7rem; + height: 5rem; + border-radius: var(--btn-brdr-radius); } .toggle-button.on { - background-color: var(--btn-lightgreen); + background-color: var(--btn-lightgreen); } /* Component Provider *********************************************************/ #sidebar-component-provider { - margin-bottom: 2rem; - flex: 1; - display: grid; - grid-template-rows: auto 1fr; + margin-bottom: 2rem; + flex: 1; + display: grid; + grid-template-rows: auto 1fr; } #components-set { - overflow-y: scroll; - height: 100%; + overflow-y: scroll; + height: 100%; } .provider-tab { - border-radius: var(--btn-brdr-radius); - box-shadow: var(--shadow); - margin-right: 0.5rem; + border-radius: var(--btn-brdr-radius); + box-shadow: var(--shadow); + margin-right: 0.5rem; } #sidebar-component-provider button { - width: 100%; - text-align: left; - padding: var(--btn-padding); - display: flex; - align-items: center; - height: 3rem; - box-shadow: none; + width: 100%; + text-align: left; + padding: var(--btn-padding); + display: flex; + align-items: center; + height: 3rem; + box-shadow: none; } .provider-tab .active { - background-color: var(--selected-color); + background-color: var(--selected-color); } .provider-tab button span { - width: 2rem; - transition: transform 0.2s linear; + width: 2rem; + transition: transform 0.2s linear; } .provider-tab button.expanded { - filter: brightness(95%); - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; + filter: brightness(95%); + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; } .provider-tab button.expanded span { - transform: scaleY(-1); + transform: scaleY(-1); } .provider-tab button.id-button { - padding-left: 3rem; + padding-left: 3rem; } .id-button { - transition: height 0.2s ease-out; - border-radius: 0; + transition: height 0.2s ease-out; + border-radius: 0; } .id-button:last-of-type { - border-bottom-left-radius: var(--btn-brdr-radius); - border-bottom-right-radius: var(--btn-brdr-radius); + border-bottom-left-radius: var(--btn-brdr-radius); + border-bottom-right-radius: var(--btn-brdr-radius); } /* Global settings area *******************************************************/ -#global-settings>button { - display: block; +#global-settings > button { + display: block; } #global-settings button { - height: 3rem; -} + height: 3rem; +} -#global-settings>* { - width: 100%; +#global-settings > * { + width: 100%; } -#global-settings>*, -.provider-tab -{ - margin-bottom: 0.4rem; +#global-settings > *, +.provider-tab { + margin-bottom: 0.4rem; } #load-layout-modal .dropdown p { - margin: 0; + margin: 0; } #load-layout-modal .dropdown em { - color: #6a6a6a; - margin-right: 1rem; + color: #6a6a6a; + margin-right: 1rem; } #load-layout-modal .dropdown { - width: 90%; + width: 90%; } .select-selected { - background-color: dodgerblue; + background-color: dodgerblue; } .global-label { - display: inline-flex; - inline-size: 164px; - text-wrap: wrap; -} - -@media(max-width:900px) { - #sidebar { - grid-template-rows: auto; - } - - #delete-button { - font-size: x-large; - padding: 0.1rem; - } - - #sidebar-body { - display: block; - overflow: scroll; - } -} \ No newline at end of file + display: inline-flex; + inline-size: 164px; + text-wrap: wrap; +} + +@media (max-width: 900px) { + #sidebar { + grid-template-rows: auto; + } + + #delete-button { + font-size: x-large; + padding: 0.1rem; + } + + #sidebar-body { + display: block; + overflow: scroll; + } +} diff --git a/src/pages/operator/css/SimpleCameraView.css b/src/pages/operator/css/SimpleCameraView.css index f8c23e29..951a748a 100644 --- a/src/pages/operator/css/SimpleCameraView.css +++ b/src/pages/operator/css/SimpleCameraView.css @@ -1,46 +1,46 @@ .simple-video-container { - position: relative; - /* flex: 1 1 0; */ - /* display: grid; */ - justify-items: center; - /* padding: 0.4rem; */ - /* height: 100%; */ - /* grid-template-columns: auto; */ - /* grid-template-rows: 1fr auto minmax(9rem, 1fr); */ - object-fit: cover; - justify-content: center; - /* align-items: center; + position: relative; + /* flex: 1 1 0; */ + /* display: grid; */ + justify-items: center; + /* padding: 0.4rem; */ + /* height: 100%; */ + /* grid-template-columns: auto; */ + /* grid-template-rows: 1fr auto minmax(9rem, 1fr); */ + object-fit: cover; + justify-content: center; + /* align-items: center; flex-direction: column; */ - text-align: -webkit-center + text-align: -webkit-center; } .simple-video-area { - position: relative; - grid-row: 2/2; - width: 100%; - max-height: 100%; - max-width: 100%; - object-fit: cover; - display: inline-block; - text-align: center; + position: relative; + grid-row: 2/2; + width: 100%; + max-height: 100%; + max-width: 100%; + object-fit: cover; + display: inline-block; + text-align: center; } .simple-video-canvas { - display: inline-block; - width: 100%; - height: auto; - object-fit: cover; + display: inline-block; + width: 100%; + height: auto; + object-fit: cover; } .simple-video-canvas.constrainedHeight { - display: inline-block; - /* width: auto; + display: inline-block; + /* width: auto; height: 100%; */ - object-fit: cover; + object-fit: cover; } .simple-realsense { - /* margin-top: -75%; */ + /* margin-top: -75%; */ } /* @media screen and (max-height: 800px) { @@ -49,89 +49,88 @@ } } */ .icon { - border-radius: 60px; - background-color: aliceblue; - font-size: 100px; - opacity: 0.5; - color: black; + border-radius: 60px; + background-color: aliceblue; + font-size: 100px; + opacity: 0.5; + color: black; } .simple-overlay { - font-size: 50px; - position: absolute; - z-index: 4; - /* margin-top: 75%; */ - background-color: transparent; - touch-action: none; - box-shadow: none; + font-size: 50px; + position: absolute; + z-index: 4; + /* margin-top: 75%; */ + background-color: transparent; + touch-action: none; + box-shadow: none; } .btn-left { - width: 20%; - height: 100%; + width: 20%; + height: 100%; } .btn-right { - width: 20%; - height: 100%; - margin-left: 80%; + width: 20%; + height: 100%; + margin-left: 80%; } .btn-up { - width: 100%; + width: 100%; } .btn-down { - width: 100%; - display: flex; - justify-content: center; + width: 100%; + display: flex; + justify-content: center; } /* Context menu popup *********************************************************/ .video-context-menu { - list-style-type: none; - position: absolute; - background-color: var(--background-color); - /* border: var(--btn-brdr); */ - margin: 0; - padding: 0; - white-space: nowrap; + list-style-type: none; + position: absolute; + background-color: var(--background-color); + /* border: var(--btn-brdr); */ + margin: 0; + padding: 0; + white-space: nowrap; - --padding: 1rem; + --padding: 1rem; - padding-top: var(--padding); - grid-row: 2/2; + padding-top: var(--padding); + grid-row: 2/2; } - .video-context-menu li { - padding: var(--padding); - cursor: pointer; - background-color: inherit; + padding: var(--padding); + cursor: pointer; + background-color: inherit; } .video-context-menu li:hover { - filter: brightness(90%); + filter: brightness(90%); } .video-context-menu::before { - content: attr(aria-label); - font-weight: bold; - padding: var(--padding); + content: attr(aria-label); + font-weight: bold; + padding: var(--padding); } .title { - font-size: x-large; - align-self: flex-end; - text-align: center; + font-size: x-large; + align-self: flex-end; + text-align: center; } -@media screen and (orientation:landscape) and (max-device-width: 900px) { - .realsense-pan-tilt-grid button .material-icons { - font-size: larger; - } - - .title { - font-size: smaller; - } -} \ No newline at end of file +@media screen and (orientation: landscape) and (max-device-width: 900px) { + .realsense-pan-tilt-grid button .material-icons { + font-size: larger; + } + + .title { + font-size: smaller; + } +} diff --git a/src/pages/operator/css/SpeedControl.css b/src/pages/operator/css/SpeedControl.css index c108e219..289f945e 100644 --- a/src/pages/operator/css/SpeedControl.css +++ b/src/pages/operator/css/SpeedControl.css @@ -1,19 +1,19 @@ #velocity-control-container button:first-of-type { - border-radius: var(--btn-brdr-radius) 0 0 var(--btn-brdr-radius); + border-radius: var(--btn-brdr-radius) 0 0 var(--btn-brdr-radius); } #velocity-control-container button:last-of-type { - border-radius: 0 var(--btn-brdr-radius) var(--btn-brdr-radius) 0; + border-radius: 0 var(--btn-brdr-radius) var(--btn-brdr-radius) 0; } #velocity-control-container button { - border-radius: 0; - /* Make buttons wider than default */ - width: 8rem; + border-radius: 0; + /* Make buttons wider than default */ + width: 8rem; } -@media(max-width:1000px) { - #velocity-control-container button { - width: auto; - } -} \ No newline at end of file +@media (max-width: 1000px) { + #velocity-control-container button { + width: auto; + } +} diff --git a/src/pages/operator/css/TabGroup.css b/src/pages/operator/css/TabGroup.css index f7c46822..7dcd07eb 100644 --- a/src/pages/operator/css/TabGroup.css +++ b/src/pages/operator/css/TabGroup.css @@ -1,75 +1,75 @@ .tab { - font-size: 20px; - /* padding-top: 10px; */ - cursor: pointer; - outline: 0; - display: flex; - border-bottom: 5px solid black; - justify-content: space-evenly; - background-color: transparent; - -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + font-size: 20px; + /* padding-top: 10px; */ + cursor: pointer; + outline: 0; + display: flex; + border-bottom: 5px solid black; + justify-content: space-evenly; + background-color: transparent; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } .pill-tab { - /* font-size: 45px; */ - cursor: pointer; - outline: 0; - display: flex; - justify-content: space-between; - background-color: transparent; - -webkit-tap-highlight-color: rgba(0, 0, 0, 0); - width: 80%; - height: auto; - background: #052a33; - margin: 1rem auto 1rem; - color: #E8F0F2; - border-radius: 10rem; - list-style: none; + /* font-size: 45px; */ + cursor: pointer; + outline: 0; + display: flex; + justify-content: space-between; + background-color: transparent; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + width: 80%; + height: auto; + background: #052a33; + margin: 1rem auto 1rem; + color: #e8f0f2; + border-radius: 10rem; + list-style: none; } .tab-btn { - background-color: transparent; - border-bottom: none; - color: white; - font-weight: 500; + background-color: transparent; + border-bottom: none; + color: white; + font-weight: 500; } li.pill-tab-btn { - background-color: transparent; - border-bottom: none; - padding: 0.5rem; - padding-left: 2rem; - padding-right: 2rem; + background-color: transparent; + border-bottom: none; + padding: 0.5rem; + padding-left: 2rem; + padding-right: 2rem; } li.pill-tab-btn:first-child { - border-bottom-left-radius: 5rem; - border-top-left-radius: 5rem; + border-bottom-left-radius: 5rem; + border-top-left-radius: 5rem; } li.pill-tab-btn:last-child { - border-bottom-right-radius: 5rem; - border-top-right-radius: 5rem; + border-bottom-right-radius: 5rem; + border-top-right-radius: 5rem; } .tab-btn.active { - border-bottom: 5px solid #06c7e1; + border-bottom: 5px solid #06c7e1; } .pill-tab-btn.active { - background: #39A2DB; + background: #39a2db; } .tab-content { - display: flex; - flex-direction: column; - height: 100%; - width: 100%; - flex: 1 1 0; - /* padding-bottom: 10px; */ - justify-content: space-between; + display: flex; + flex-direction: column; + height: 100%; + width: 100%; + flex: 1 1 0; + /* padding-bottom: 10px; */ + justify-content: space-between; } .tab-group { - display: contents; -} \ No newline at end of file + display: contents; +} diff --git a/src/pages/operator/css/Tooltip.css b/src/pages/operator/css/Tooltip.css index 05102dde..0c5323e5 100644 --- a/src/pages/operator/css/Tooltip.css +++ b/src/pages/operator/css/Tooltip.css @@ -1,107 +1,105 @@ .tooltip-trigger .tooltip-top::after { - content: " "; - position: absolute; - top: 100%; - left: 50%; - margin-left: -5px; - border-width: 5px; - border-style: solid; - border-color: black transparent transparent transparent; + content: " "; + position: absolute; + top: 100%; + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: black transparent transparent transparent; } .tooltip-trigger .tooltip-bottom::after { - content: " "; - position: absolute; - bottom: 100%; - left: 50%; - margin-left: -5px; - border-width: 5px; - border-style: solid; - border-color: transparent transparent black transparent; + content: " "; + position: absolute; + bottom: 100%; + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: transparent transparent black transparent; } .tooltip-trigger .tooltip { - display: none; - opacity: 0; + display: none; + opacity: 0; } .tooltip-trigger:hover .tooltip { - display: block; - opacity: 1; - animation: fade-in 0.5s; + display: block; + opacity: 1; + animation: fade-in 0.5s; } @keyframes fade-in { - from { - opacity: 0; - } + from { + opacity: 0; + } - to { - opacity: 1; - } + to { + opacity: 1; + } } .tooltip-trigger { - position: relative; - display: inline; + position: relative; + display: inline; } .tooltip-trigger .tooltip { - width: fit-content; - min-width: 100%; - left: 50%; - transform: translateX(-50%); - background-color: black; - color: #fff; - text-align: center; - border-radius: 6px; - padding: 5px; - opacity: 0; - transition: opacity 1s; - position: absolute; - z-index: 1; + width: fit-content; + min-width: 100%; + left: 50%; + transform: translateX(-50%); + background-color: black; + color: #fff; + text-align: center; + border-radius: 6px; + padding: 5px; + opacity: 0; + transition: opacity 1s; + position: absolute; + z-index: 1; } .tooltip-right { - top: -5px; - left: 105%; + top: -5px; + left: 105%; } - .tooltip-left { - top: -5px; - right: 105%; + top: -5px; + right: 105%; } - + .tooltip-top { - bottom:105%; - left:0%; + bottom: 105%; + left: 0%; } .tooltip-bottom { - top:105%; - left:0%; + top: 105%; + left: 0%; } .tooltip-trigger .tooltip-right::after { - content: " "; - position: absolute; - top: 50%; - right: 100%; - margin-top: -5px; - border-width: 5px; - border-style: solid; - border-color: transparent black transparent transparent; + content: " "; + position: absolute; + top: 50%; + right: 100%; + margin-top: -5px; + border-width: 5px; + border-style: solid; + border-color: transparent black transparent transparent; } - .tooltip-trigger .tooltip-left::after { - content: " "; - position: absolute; - top: 50%; - left: 100%; - margin-top: -5px; - border-width: 5px; - border-style: solid; - border-color: transparent transparent transparent black; -} \ No newline at end of file + content: " "; + position: absolute; + top: 50%; + left: 100%; + margin-top: -5px; + border-width: 5px; + border-style: solid; + border-color: transparent transparent transparent black; +} diff --git a/src/pages/operator/css/VirtualJoystick.css b/src/pages/operator/css/VirtualJoystick.css index 606d80a6..c1b3ffb4 100644 --- a/src/pages/operator/css/VirtualJoystick.css +++ b/src/pages/operator/css/VirtualJoystick.css @@ -1,44 +1,44 @@ .virtual-joystick { - flex: 1; - border-radius: var(--btn-brdr-radius); - /* border: 2px solid var(--btn-blue); */ - margin: 0.4rem; - position: relative; - z-index: 1; - max-height: 100%; - - display:grid; - justify-items: center; + flex: 1; + border-radius: var(--btn-brdr-radius); + /* border: 2px solid var(--btn-blue); */ + margin: 0.4rem; + position: relative; + z-index: 1; + max-height: 100%; + + display: grid; + justify-items: center; } .virtual-joystick svg { - max-height: 100%; - max-width: fit-content; + max-height: 100%; + max-width: fit-content; } .virtual-joystick:not(.customizing) { - cursor: crosshair; + cursor: crosshair; } .virtual-joystick::after { - border-radius: var(--btn-brdr-radius); + border-radius: var(--btn-brdr-radius); } .virtual-joystick path { - fill: none; - stroke-width: 1; - stroke: lightgray; + fill: none; + stroke-width: 1; + stroke: lightgray; } .virtual-joystick .joystick { - fill: darkgray; + fill: darkgray; } .virtual-joystick .outer-circle { - /* fill: lightgray; */ - fill: #312e2e; + /* fill: lightgray; */ + fill: #312e2e; } .virtual-joystick.active .joystick { - fill: var(--btn-turquoise); -} \ No newline at end of file + fill: var(--btn-turquoise); +} diff --git a/src/pages/operator/css/basic_components.css b/src/pages/operator/css/basic_components.css index d1aa3334..733e970f 100644 --- a/src/pages/operator/css/basic_components.css +++ b/src/pages/operator/css/basic_components.css @@ -1,324 +1,326 @@ /* Popup modal ****************************************************************/ .popup-modal { - position: fixed; - border-radius: var(--btn-brdr-radius); - background-color: var(--background-color); - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - padding: 1rem; - z-index: 10; - height: 50%; - width: 50%; - display: flex; - flex-direction: column; - /* align-items: center; */ - justify-content: space-between; - opacity: 100%; - touch-action: manipulation + position: fixed; + border-radius: var(--btn-brdr-radius); + background-color: var(--background-color); + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + padding: 1rem; + z-index: 10; + height: 50%; + width: 50%; + display: flex; + flex-direction: column; + /* align-items: center; */ + justify-content: space-between; + opacity: 100%; + touch-action: manipulation; } .popup-modal.small { - height: 15rem; - width: 20rem; - top: 10%; + height: 15rem; + width: 20rem; + top: 10%; } .popup-modal.medium { - height: 50rem; - width: 47rem; - top: -10% - /* height: 35%; */ + height: 50rem; + width: 47rem; + top: -10%; + /* height: 35%; */ } .popup-modal.large { - height: 50%; + height: 50%; } .mobile { - transform: translate(-50%, 50%); - display: flex; - justify-content: space-around; - /* font-size: 50px !important; */ + transform: translate(-50%, 50%); + display: flex; + justify-content: space-around; + /* font-size: 50px !important; */ } .voice-commands-popup-modal { - position: fixed; - border-radius: var(--btn-brdr-radius); - background-color: var(--background-color); - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - padding: 2rem; - z-index: 4; - height: 45rem; - width: 30rem; - display: flex; - flex-direction: column; - /* align-items: center; */ - justify-content: space-between; - opacity: 100%; + position: fixed; + border-radius: var(--btn-brdr-radius); + background-color: var(--background-color); + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + padding: 2rem; + z-index: 4; + height: 45rem; + width: 30rem; + display: flex; + flex-direction: column; + /* align-items: center; */ + justify-content: space-between; + opacity: 100%; } .voice-commands-popup-modal #close-modal { - padding-left: 80%; + padding-left: 80%; } .voice-commands-popup-modal #commands { - padding-right: 20%; + padding-right: 20%; } .popup-modal-bottom-buttons { - width: 100%; + width: 100%; } .popup-modal-bottom-buttons button { - width: 45%; - padding: .5em; - color: black; + width: 45%; + padding: 0.5em; + color: black; } /* Dark background behind popup */ -#popup-background, .loader-background { - position: fixed; - top: 0; - left: 0; - height: 100%; - width: 100%; - background-color: black; - opacity: 60%; - z-index: 3; +#popup-background, +.loader-background { + position: fixed; + top: 0; + left: 0; + height: 100%; + width: 100%; + background-color: black; + opacity: 60%; + z-index: 3; } .popup-modal label { - font-size: 30px; + font-size: 30px; } .popup-modal input { - width: 100%; - padding: 0.5em; + width: 100%; + padding: 0.5em; } /* Dropdown *******************************************************************/ .dropdown { - position: relative; + position: relative; } .dropdown-button { - display: flex; - align-items: center; - justify-content: space-between; - padding-top: 1rem; - padding-bottom: 1rem; - width: 100%; - position: relative; - color: black; + display: flex; + align-items: center; + justify-content: space-between; + padding-top: 1rem; + padding-bottom: 1rem; + width: 100%; + position: relative; + color: black; } .dropdown-button.expanded.bottom { - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; - color: black; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + color: black; } .dropdown-button.expanded.top { - border-top-left-radius: 0; - border-top-right-radius: 0; - margin: 0 !important; - color: black; + border-top-left-radius: 0; + border-top-right-radius: 0; + margin: 0 !important; + color: black; } /* Flip the dropdown arrow when active */ .dropdown-button span { - transition: transform 0.2s linear; + transition: transform 0.2s linear; } .dropdown-button.expanded span { - transform: scaleY(-1); + transform: scaleY(-1); } .dropdown-popup { - position: absolute; - min-width: 100%; - z-index: 3; - box-shadow: var(--shadow); - border-radius: 0 0 var(--btn-brdr-radius) var(--btn-brdr-radius); + position: absolute; + min-width: 100%; + z-index: 3; + box-shadow: var(--shadow); + border-radius: 0 0 var(--btn-brdr-radius) var(--btn-brdr-radius); } .dropdown-popup.top { - top: auto; - bottom: 100%; - box-shadow: var(--shadow-bottom); + top: auto; + bottom: 100%; + box-shadow: var(--shadow-bottom); } .dropdown-option { - padding-top: 1rem; - padding-bottom: 1rem; - cursor: pointer; - width: 100%; - display: block; - border-radius: 0; - text-align: left; - margin: 0 !important; - color: black; + padding-top: 1rem; + padding-bottom: 1rem; + cursor: pointer; + width: 100%; + display: block; + border-radius: 0; + text-align: left; + margin: 0 !important; + color: black; } .dropdown-option.active { - filter: brightness(80%); + filter: brightness(80%); } .dropdown-popup.top .dropdown-option:first-of-type { - border-radius: var(--btn-brdr-radius) var(--btn-brdr-radius) 0 0; + border-radius: var(--btn-brdr-radius) var(--btn-brdr-radius) 0 0; } .dropdown-popup.top .dropdown-option:last-of-type { - box-shadow: none; + box-shadow: none; } .dropdown-popup.bottom .dropdown-option:last-of-type { - border-radius: 0 0 var(--btn-brdr-radius) var(--btn-brdr-radius); + border-radius: 0 0 var(--btn-brdr-radius) var(--btn-brdr-radius); } /* CheckToggleButton **********************************************************/ .check-toggle-button { - display: flex; - justify-content: center; - color: black; + display: flex; + justify-content: center; + color: black; } .check-toggle-button span.material-icons { - margin-right: 1rem; + margin-right: 1rem; } -.check-toggle-button, .check-toggle-button-mobile { - border-radius: 13px; - display: flex; - justify-content: center; - align-items: center; - text-align: center; - background: transparent; +.check-toggle-button, +.check-toggle-button-mobile { + border-radius: 13px; + display: flex; + justify-content: center; + align-items: center; + text-align: center; + background: transparent; } .check-toggle-button-mobile { - border: 5px solid whitesmoke; - /* margin: 1rem; */ + border: 5px solid whitesmoke; + /* margin: 1rem; */ } .pose-name { - /* padding-top: 5%; */ - align-self: normal; + /* padding-top: 5%; */ + align-self: normal; } .pose-name label { - padding-right: 2.5rem; + padding-right: 2.5rem; } .pose-name input { - width: 65%; + width: 65%; } .mobile-pose-name input { - width: 100%; + width: 100%; } ul.checkbox { - margin: 0; - padding: 0; - margin-left: 1rem; - list-style: none; + margin: 0; + padding: 0; + margin-left: 1rem; + list-style: none; } ul.checkbox li { - border: 0.5rem transparent solid; + border: 0.5rem transparent solid; } hr { - width: 100% + width: 100%; } button .material-icons { - margin: 0 !important; + margin: 0 !important; } -input[type='checkbox'] { - width: 1.5rem; - height: 1.5rem; - background: white; - border-radius: 5px; - border: 2px solid #555; - margin-right: 1rem; +input[type="checkbox"] { + width: 1.5rem; + height: 1.5rem; + background: white; + border-radius: 5px; + border: 2px solid #555; + margin-right: 1rem; } @media screen and (max-device-width: 700px) { - input[type='checkbox'] { - margin-right: 3rem; - transform: scale(2); - } + input[type="checkbox"] { + margin-right: 3rem; + transform: scale(2); + } - .popup-modal label { - font-size: 50px; - } + .popup-modal label { + font-size: 50px; + } } /* AccordionSelect **********************************************************/ /* Style the accordion section */ .accordion_section { - display: flex; - flex-direction: column; + display: flex; + flex-direction: column; } - + /* Style the buttons that are used to open and close the accordion panel */ .accordion { - /* background-color: #eee; */ - /* color: #444; */ - cursor: pointer; - /* padding: 18px; */ - display: flex; - align-items: center; - border: none; - outline: none; - transition: background-color 0.6s ease; - justify-content: space-between; -} - + /* background-color: #eee; */ + /* color: #444; */ + cursor: pointer; + /* padding: 18px; */ + display: flex; + align-items: center; + border: none; + outline: none; + transition: background-color 0.6s ease; + justify-content: space-between; +} + /* Add a background color to the button if it is clicked on (add the .active class with JS), and when you move the mouse over it (hover) */ .accordion:hover, .active { - background-color: #ccc; + background-color: #ccc; } - + /* Style the accordion content title */ .accordion_title { - font-family: "Open Sans", sans-serif; - font-weight: 600; - font-size: 14px; + font-family: "Open Sans", sans-serif; + font-weight: 600; + font-size: 14px; } - + /* Style the accordion content panel. Note: hidden by default */ .accordion_content { - background-color: var(--btn-gray); - overflow: hidden; - transition: max-height 0.6s ease; + background-color: var(--btn-gray); + overflow: hidden; + transition: max-height 0.6s ease; } - /* Flip the dropdown arrow when active */ +/* Flip the dropdown arrow when active */ .accordion span { - transition: transform 0.2s linear; + transition: transform 0.2s linear; } .accordion.active span { - transform: scaleY(-1); + transform: scaleY(-1); } .accordion-item { - padding: 0.75rem 0rem 0.75rem 0rem; - text-align: center; + padding: 0.75rem 0rem 0.75rem 0rem; + text-align: center; } .accordion-item:hover { - background-color: #ccc; + background-color: #ccc; } diff --git a/src/pages/operator/css/index.css b/src/pages/operator/css/index.css index 90978358..c5097051 100644 --- a/src/pages/operator/css/index.css +++ b/src/pages/operator/css/index.css @@ -1,158 +1,158 @@ :root { - --btn-brdr-radius: 0.4rem; - --btn-padding: .5em 1em; - --btn-padding-med: 0.4rem 0.5rem; + --btn-brdr-radius: 0.4rem; + --btn-padding: 0.5em 1em; + --btn-padding-med: 0.4rem 0.5rem; - --selected-color: hsl(33, 95%, 63%); + --selected-color: hsl(33, 95%, 63%); - --btn-gray: #f0f0f0; - --btn-blue: hsl(200deg 83.23% 22.29%); - --btn-green: rgb(129, 218, 129); - --btn-red: hsl(0, 70%, 70%); - --btn-turquoise: hsl(171,32%,46%); + --btn-gray: #f0f0f0; + --btn-blue: hsl(200deg 83.23% 22.29%); + --btn-green: rgb(129, 218, 129); + --btn-red: hsl(0, 70%, 70%); + --btn-turquoise: hsl(171, 32%, 46%); - --shadow-color: rgba(112, 112, 112, 0.196); + --shadow-color: rgba(112, 112, 112, 0.196); - --background-color: hsl(0, 0%, 98%); - --gray-bg: hsl(0, 0%, 88%); + --background-color: hsl(0, 0%, 98%); + --gray-bg: hsl(0, 0%, 88%); - --shadow: 3px 3px 2px var(--shadow-color); + --shadow: 3px 3px 2px var(--shadow-color); - --tab-inactive: hsl(200, 20%, 85%); + --tab-inactive: hsl(200, 20%, 85%); - --font-white: hsl(0, 0%, 100%); + --font-white: hsl(0, 0%, 100%); - --path-blue: hsl(170, 37%, 53%); + --path-blue: hsl(170, 37%, 53%); - --shadow-bottom: 3px 0px 0px var(--shadow-color) + --shadow-bottom: 3px 0px 0px var(--shadow-color); } * { - box-sizing: border-box; - min-width: 0; - min-height: 0; - font: inherit; + box-sizing: border-box; + min-width: 0; + min-height: 0; + font: inherit; } body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - background-color: var(--background-color); + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", + "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + background-color: var(--background-color); } html { - -webkit-touch-callout: none; - -webkit-tap-highlight-color: transparent; - -moz-user-select: -moz-none; - -webkit-user-select: none; - -ms-user-select: none; - user-select: none; + -webkit-touch-callout: none; + -webkit-tap-highlight-color: transparent; + -moz-user-select: -moz-none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; } html, body, #root { - height: 100%; - margin: 0; - overflow: hidden; - user-select: none; + height: 100%; + margin: 0; + overflow: hidden; + user-select: none; } -@media screen and (orientation:landscape) and (max-device-width: 900px) { - html, - body, - #root { - overflow: scroll; - } +@media screen and (orientation: landscape) and (max-device-width: 900px) { + html, + body, + #root { + overflow: scroll; + } } /* In customization mode add hidden shadow after each element, to be shown when the element is selected */ .customizing::after { - content: ""; - position: absolute; + content: ""; + position: absolute; - z-index: -1; - width: 100%; - height: 100%; - opacity: 0; - left: 0; - top: 0; + z-index: -1; + width: 100%; + height: 100%; + opacity: 0; + left: 0; + top: 0; - box-shadow: 0 0 1rem 0.5rem var(--selected-color); - transition: opacity 0.25s ease-in-out; + box-shadow: 0 0 1rem 0.5rem var(--selected-color); + transition: opacity 0.25s ease-in-out; } .customizing.selected::after { - opacity: 1; + opacity: 1; } /* Button colors **************************************************************/ button { - background-color: var(--btn-gray); - border: none; - border-radius: var(--btn-brdr-radius); - padding: var(--btn-padding); - box-shadow: var(--shadow); - cursor: pointer; + background-color: var(--btn-gray); + border: none; + border-radius: var(--btn-brdr-radius); + padding: var(--btn-padding); + box-shadow: var(--shadow); + cursor: pointer; } button:hover { - filter: brightness(90%); - box-shadow: none; + filter: brightness(90%); + box-shadow: none; } button:disabled { - background-color: lightgray; - pointer-events: none; - box-shadow: none; + background-color: lightgray; + pointer-events: none; + box-shadow: none; } button .material-icons { - margin: -0.25rem; + margin: -0.25rem; } .btn-green { - background-color: var(--btn-green); + background-color: var(--btn-green); } .btn-red { - background-color: var(--btn-red); + background-color: var(--btn-red); } .btn-blue { - background-color: var(--btn-blue); + background-color: var(--btn-blue); } .btn-turquoise { - background-color: var(--btn-turquoise); + background-color: var(--btn-turquoise); } .font-white { - color: var(--font-white) + color: var(--font-white); } .btn-yellow { - background-color: var(--selected-color); + background-color: var(--selected-color); } -@media(max-width:1300px) { - button { - padding: var(--btn-padding-med); - /* font-size: 90px; */ - } +@media (max-width: 1300px) { + button { + padding: var(--btn-padding-med); + /* font-size: 90px; */ + } - .material-icons { - font-size: larger; - } + .material-icons { + font-size: larger; + } - .under-video-area button { - /* font-size: 30px; */ - padding: var(--btn-padding); - gap: 10px; - } + .under-video-area button { + /* font-size: 30px; */ + padding: var(--btn-padding); + gap: 10px; + } } diff --git a/src/pages/operator/html/index.html b/src/pages/operator/html/index.html index b2b15e11..0716ea62 100644 --- a/src/pages/operator/html/index.html +++ b/src/pages/operator/html/index.html @@ -1,17 +1,23 @@ - + - - + + Stretch Web Interface - - + + +
-
-
-

Loading...

-
+
+
+

Loading...

+
- - \ No newline at end of file + + diff --git a/src/pages/operator/icons/Arm_In.svg b/src/pages/operator/icons/Arm_In.svg index 97314413..0dcc0c04 100644 --- a/src/pages/operator/icons/Arm_In.svg +++ b/src/pages/operator/icons/Arm_In.svg @@ -75,7 +75,7 @@ .st0{clip-path:url(#SVGID_00000041990836480947321200000008459015841381596083_);} .st1{fill:#808080;} .st2{fill:#FFFFFF;filter:url(#Adobe_OpacityMaskFilter);} - + .st3{mask:url(#path-4-inside-1_401_634_00000047759094063494162890000005635916377974113452_);fill:#808080;stroke:#808080;stroke-width:2;} - - + + - + diff --git a/src/pages/operator/icons/Arm_Out.svg b/src/pages/operator/icons/Arm_Out.svg index 9fb4b195..2f1d3471 100644 --- a/src/pages/operator/icons/Arm_Out.svg +++ b/src/pages/operator/icons/Arm_Out.svg @@ -59,7 +59,7 @@ .st0{clip-path:url(#SVGID_00000041990836480947321200000008459015841381596083_);} .st1{fill:#808080;} .st2{fill:#FFFFFF;filter:url(#Adobe_OpacityMaskFilter);} - + .st3{mask:url(#path-4-inside-1_401_634_00000047759094063494162890000005635916377974113452_);fill:#808080;stroke:#808080;stroke-width:2;} - + - \ No newline at end of file + diff --git a/src/pages/operator/icons/Pitch_Up.svg b/src/pages/operator/icons/Pitch_Up.svg index 2170f031..587ba2a5 100644 --- a/src/pages/operator/icons/Pitch_Up.svg +++ b/src/pages/operator/icons/Pitch_Up.svg @@ -11,4 +11,4 @@ - \ No newline at end of file + diff --git a/src/pages/operator/icons/Yaw_Left.svg b/src/pages/operator/icons/Yaw_Left.svg index 69bfcbbd..26f9e650 100644 --- a/src/pages/operator/icons/Yaw_Left.svg +++ b/src/pages/operator/icons/Yaw_Left.svg @@ -49,7 +49,7 @@ transform="translate(-6.8682,-5.74)"> - + - + , - storageHandler: StorageHandler + remoteStreams: Map; + storageHandler: StorageHandler; }) => { - const [buttonCollision, setButtonCollision] = React.useState([]); - const [moveBaseState, setMoveBaseState] = React.useState() - const [cameraID, setCameraID] = React.useState(CameraViewId.realsense) - const [velocityScale, setVelocityScale] = React.useState(0.8); - const [hideMap, setHideMap] = React.useState(true) - const [hideControls, setHideControls] = React.useState(false) - const [activeMainGroupTab, setActiveMainGroupTab] = React.useState(0) - const [activeControlTab, setActiveControlTab] = React.useState(0) - const [isRecording, setIsRecording] = React.useState(); - const [depthSensing, setDepthSensing] = React.useState(false); - const [showAlert, setShowAlert] = React.useState(true); - - React.useEffect(()=>{ - setTimeout(function() { - setShowAlert(false) - }, 5000); - }, []) + const [buttonCollision, setButtonCollision] = React.useState< + ButtonPadButton[] + >([]); + const [moveBaseState, setMoveBaseState] = React.useState(); + const [cameraID, setCameraID] = React.useState( + CameraViewId.realsense, + ); + const [velocityScale, setVelocityScale] = React.useState(0.8); + const [hideMap, setHideMap] = React.useState(true); + const [hideControls, setHideControls] = React.useState(false); + const [activeMainGroupTab, setActiveMainGroupTab] = React.useState(0); + const [activeControlTab, setActiveControlTab] = React.useState(0); + const [isRecording, setIsRecording] = React.useState(); + const [depthSensing, setDepthSensing] = React.useState(false); + const [showAlert, setShowAlert] = React.useState(true); - FunctionProvider.actionMode = ActionMode.PressAndHold; + React.useEffect(() => { + setTimeout(function () { + setShowAlert(false); + }, 5000); + }, []); - // Just used as a flag to force the operator to rerender when the button state map - // has been updated - const [buttonStateMapRerender, setButtonStateMapRerender] = React.useState(false); - const buttonStateMap = React.useRef(); - function operatorCallback(bsm: ButtonStateMap) { - let collisionButtons: ButtonPadButton[] = [] - bsm.forEach((state, button) => { - if (state == ButtonState.Collision) collisionButtons.push(button) - }) - setButtonCollision(collisionButtons) - if (bsm !== buttonStateMap.current) { - buttonStateMap.current = bsm; - setButtonStateMapRerender(!buttonStateMapRerender); - } + FunctionProvider.actionMode = ActionMode.PressAndHold; + + // Just used as a flag to force the operator to rerender when the button state map + // has been updated + const [buttonStateMapRerender, setButtonStateMapRerender] = + React.useState(false); + const buttonStateMap = React.useRef(); + function operatorCallback(bsm: ButtonStateMap) { + let collisionButtons: ButtonPadButton[] = []; + bsm.forEach((state, button) => { + if (state == ButtonState.Collision) collisionButtons.push(button); + }); + setButtonCollision(collisionButtons); + if (bsm !== buttonStateMap.current) { + buttonStateMap.current = bsm; + setButtonStateMapRerender(!buttonStateMapRerender); } - buttonFunctionProvider.setOperatorCallback(operatorCallback); + } + buttonFunctionProvider.setOperatorCallback(operatorCallback); - function moveBaseStateCallback(state: MoveBaseState) { - setMoveBaseState(state) + function moveBaseStateCallback(state: MoveBaseState) { + setMoveBaseState(state); + } + underMapFunctionProvider.setOperatorCallback(moveBaseStateCallback); + let moveBaseAlertTimeout: NodeJS.Timeout; + React.useEffect(() => { + if (moveBaseState && moveBaseState.alertType != "info") { + if (moveBaseAlertTimeout) clearTimeout(moveBaseAlertTimeout); + moveBaseAlertTimeout = setTimeout(() => { + setMoveBaseState(undefined); + }, 5000); } - underMapFunctionProvider.setOperatorCallback(moveBaseStateCallback); - let moveBaseAlertTimeout: NodeJS.Timeout; - React.useEffect(() => { - if (moveBaseState && moveBaseState.alertType != "info") { - if (moveBaseAlertTimeout) clearTimeout(moveBaseAlertTimeout) - moveBaseAlertTimeout = setTimeout(() => { - setMoveBaseState(undefined) - }, 5000) - } - }, [moveBaseState]) + }, [moveBaseState]); - let remoteStreams = props.remoteStreams; + let remoteStreams = props.remoteStreams; - /** State passed from the operator and shared by all components */ - const sharedState: SharedState = { - customizing: false, - onSelect: () => {}, - remoteStreams: remoteStreams, - selectedPath: "deselected", - dropZoneState: { - onDrop: () => {}, - selectedDefinition: undefined - }, - buttonStateMap: buttonStateMap.current, - hideLabels: false, - hasBetaTeleopKit: hasBetaTeleopKit - } + /** State passed from the operator and shared by all components */ + const sharedState: SharedState = { + customizing: false, + onSelect: () => {}, + remoteStreams: remoteStreams, + selectedPath: "deselected", + dropZoneState: { + onDrop: () => {}, + selectedDefinition: undefined, + }, + buttonStateMap: buttonStateMap.current, + hideLabels: false, + hasBetaTeleopKit: hasBetaTeleopKit, + }; - function updateScreens() { - if (hideMap) { - setHideMap(false) - setHideControls(true) - } else { - setHideControls(false) - setHideMap(true) - } + function updateScreens() { + if (hideMap) { + setHideMap(false); + setHideControls(true); + } else { + setHideControls(false); + setHideMap(true); } - const swipeHandlers = Swipe({ - onSwipedLeft: () => updateScreens(), - onSwipedRight: () => updateScreens() - }); + } + const swipeHandlers = Swipe({ + onSwipedLeft: () => updateScreens(), + onSwipedRight: () => updateScreens(), + }); - const driveMode = (show: boolean) => { return ( - show - ? - {/* { + return show ? ( + + {/* */} - - - : <> - )} + + + ) : ( + <> + ); + }; - const armMode = (show: boolean) => { return ( - show - ? - - - : <> - )} - - const gripperMode = (show: boolean) => { return ( - show - ? - - - : <> - )} + const armMode = (show: boolean) => { + return show ? ( + + + + ) : ( + <> + ); + }; - const ControlModes = () => { - return ( - <> -
- Slow - { setVelocityScale(FunctionProvider.velocityScale) }} onChange={ (event) => { FunctionProvider.velocityScale = Number(event.target.value) }}/> - Fast -
- setActiveControlTab(index)} - pill={true} - key={'controls-group'} - /> - - ) - } + const gripperMode = (show: boolean) => { + return show ? ( + + + + ) : ( + <> + ); + }; - const controlModes = (show: boolean) => { return ( show ? : <>)} - const recordingList = (show: boolean) => { return ( )} + const ControlModes = () => { + return ( + <> +
+ Slow + { + setVelocityScale(FunctionProvider.velocityScale); + }} + onChange={(event) => { + FunctionProvider.velocityScale = Number(event.target.value); + }} + /> + Fast +
+ setActiveControlTab(index)} + pill={true} + key={"controls-group"} + /> + + ); + }; + const controlModes = (show: boolean) => { + return show ? : <>; + }; + const recordingList = (show: boolean) => { return ( -
e.preventDefault()}> -
- {showAlert ? -
- - Beta feature, use at your own risk - -
- : - <> - } -
-
- - -
- {cameraID == CameraViewId.realsense && -
- { - setDepthSensing(!depthSensing) - underVideoFunctionProvider.provideFunctions(UnderVideoButton.DepthSensing).onCheck!(!depthSensing) - }} - label="Depth Sensing" - /> -
- } - - {/*
*/} -
- -
- setActiveMainGroupTab(index)} - pill={false} - key={'main-group'} - /> -
- {/*
*/} -
-
- -
- -
+ + ); + }; + + return ( +
e.preventDefault()}> +
+ {showAlert ? ( +
+ + Beta feature, use at your own risk + +
+ ) : ( + <> + )} +
+
+ + +
+ {cameraID == CameraViewId.realsense && ( +
+ { + setDepthSensing(!depthSensing); + underVideoFunctionProvider.provideFunctions( + UnderVideoButton.DepthSensing, + ).onCheck!(!depthSensing); + }} + label="Depth Sensing" + />
+ )} + + {/*
*/} +
+ +
+ setActiveMainGroupTab(index)} + pill={false} + key={"main-group"} + /> +
+ {/*
*/} +
+
+ +
+
- ) -} \ No newline at end of file +
+
+ ); +}; diff --git a/src/pages/operator/tsx/Operator.tsx b/src/pages/operator/tsx/Operator.tsx index 4942be93..0d42327e 100644 --- a/src/pages/operator/tsx/Operator.tsx +++ b/src/pages/operator/tsx/Operator.tsx @@ -1,269 +1,327 @@ import React from "react"; -import { SpeedControl } from "./static_components/SpeedControl" +import { SpeedControl } from "./static_components/SpeedControl"; import { LayoutArea } from "./static_components/LayoutArea"; import { CustomizeButton } from "./static_components/CustomizeButton"; import { GlobalOptionsProps, Sidebar } from "./static_components/Sidebar"; import { SharedState } from "./layout_components/CustomizableComponent"; -import { ActionMode, ComponentDefinition, LayoutDefinition } from "./utils/component_definitions"; +import { + ActionMode, + ComponentDefinition, + LayoutDefinition, +} from "./utils/component_definitions"; import { className, MoveBaseState, RemoteStream, RobotPose } from "shared/util"; -import { buttonFunctionProvider, underMapFunctionProvider, hasBetaTeleopKit } from "."; -import { ButtonPadButton, ButtonState, ButtonStateMap } from "./function_providers/ButtonFunctionProvider"; +import { + buttonFunctionProvider, + underMapFunctionProvider, + hasBetaTeleopKit, +} from "."; +import { + ButtonPadButton, + ButtonState, + ButtonStateMap, +} from "./function_providers/ButtonFunctionProvider"; import { Dropdown } from "./basic_components/Dropdown"; -import { DEFAULT_LAYOUTS, DefaultLayoutName, StorageHandler } from "./storage_handler/StorageHandler"; +import { + DEFAULT_LAYOUTS, + DefaultLayoutName, + StorageHandler, +} from "./storage_handler/StorageHandler"; import { FunctionProvider } from "./function_providers/FunctionProvider"; -import { addToLayout, moveInLayout, removeFromLayout } from "./utils/layout_helpers"; +import { + addToLayout, + moveInLayout, + removeFromLayout, +} from "./utils/layout_helpers"; import { MovementRecorder } from "./layout_components/MovementRecorder"; import { Alert } from "./basic_components/Alert"; -import "operator/css/Operator.css" +import "operator/css/Operator.css"; /** Operator interface webpage */ export const Operator = (props: { - remoteStreams: Map, - layout: LayoutDefinition, - storageHandler: StorageHandler + remoteStreams: Map; + layout: LayoutDefinition; + storageHandler: StorageHandler; }) => { - const [customizing, setCustomizing] = React.useState(false); - const [selectedPath, setSelectedPath] = React.useState(undefined); - const [selectedDefinition, setSelectedDef] = React.useState(undefined); - const [velocityScale, setVelocityScale] = React.useState(FunctionProvider.velocityScale); - const [buttonCollision, setButtonCollision] = React.useState([]); - const [moveBaseState, setMoveBaseState] = React.useState() - - const layout = React.useRef(props.layout); + const [customizing, setCustomizing] = React.useState(false); + const [selectedPath, setSelectedPath] = React.useState( + undefined, + ); + const [selectedDefinition, setSelectedDef] = React.useState< + ComponentDefinition | undefined + >(undefined); + const [velocityScale, setVelocityScale] = React.useState( + FunctionProvider.velocityScale, + ); + const [buttonCollision, setButtonCollision] = React.useState< + ButtonPadButton[] + >([]); + const [moveBaseState, setMoveBaseState] = React.useState(); - // Just used as a flag to force the operator to rerender when the button state map - // has been updated - const [buttonStateMapRerender, setButtonStateMapRerender] = React.useState(false); - const buttonStateMap = React.useRef(); - function operatorCallback(bsm: ButtonStateMap) { - let collisionButtons: ButtonPadButton[] = [] - bsm.forEach((state, button) => { - if (state == ButtonState.Collision) collisionButtons.push(button) - }) - setButtonCollision(collisionButtons) - buttonStateMap.current = bsm; - setButtonStateMapRerender(!buttonStateMapRerender); - } - buttonFunctionProvider.setOperatorCallback(operatorCallback); + const layout = React.useRef(props.layout); + + // Just used as a flag to force the operator to rerender when the button state map + // has been updated + const [buttonStateMapRerender, setButtonStateMapRerender] = + React.useState(false); + const buttonStateMap = React.useRef(); + function operatorCallback(bsm: ButtonStateMap) { + let collisionButtons: ButtonPadButton[] = []; + bsm.forEach((state, button) => { + if (state == ButtonState.Collision) collisionButtons.push(button); + }); + setButtonCollision(collisionButtons); + buttonStateMap.current = bsm; + setButtonStateMapRerender(!buttonStateMapRerender); + } + buttonFunctionProvider.setOperatorCallback(operatorCallback); - function moveBaseStateCallback(state: MoveBaseState) { - setMoveBaseState(state) + function moveBaseStateCallback(state: MoveBaseState) { + setMoveBaseState(state); + } + underMapFunctionProvider.setOperatorCallback(moveBaseStateCallback); + let moveBaseAlertTimeout: NodeJS.Timeout; + React.useEffect(() => { + if (moveBaseState && moveBaseState.alert_type != "info") { + if (moveBaseAlertTimeout) clearTimeout(moveBaseAlertTimeout); + moveBaseAlertTimeout = setTimeout(() => { + setMoveBaseState(undefined); + }, 5000); } - underMapFunctionProvider.setOperatorCallback(moveBaseStateCallback); - let moveBaseAlertTimeout: NodeJS.Timeout; - React.useEffect(() => { - if (moveBaseState && moveBaseState.alert_type != "info") { - if (moveBaseAlertTimeout) clearTimeout(moveBaseAlertTimeout) - moveBaseAlertTimeout = setTimeout(() => { - setMoveBaseState(undefined) - }, 5000) - } - }, [moveBaseState]) + }, [moveBaseState]); - let remoteStreams = props.remoteStreams; + let remoteStreams = props.remoteStreams; - /** Rerenders the operator */ - function updateLayout() { - console.log('update layout'); - setButtonStateMapRerender(!buttonStateMapRerender); - } + /** Rerenders the operator */ + function updateLayout() { + console.log("update layout"); + setButtonStateMapRerender(!buttonStateMapRerender); + } - /** - * Updates the action mode in the layout (visually) and in the fuction - * provider (functionally). - */ - function setActionMode(actionMode: ActionMode) { - layout.current.actionMode = actionMode; - FunctionProvider.actionMode = actionMode; - props.storageHandler.saveCurrentLayout(layout.current); - updateLayout(); - } + /** + * Updates the action mode in the layout (visually) and in the function + * provider (functionally). + */ + function setActionMode(actionMode: ActionMode) { + layout.current.actionMode = actionMode; + FunctionProvider.actionMode = actionMode; + props.storageHandler.saveCurrentLayout(layout.current); + updateLayout(); + } - /** - * Sets the movement recorder component to display or hidden. - * - * @param displayMovementRecorder if the movement recorder component at the - * top of the operator body should be displayed - */ - function setDisplayMovementRecorder(displayMovementRecorder: boolean) { - layout.current.displayMovementRecorder = displayMovementRecorder; - updateLayout(); - } + /** + * Sets the movement recorder component to display or hidden. + * + * @param displayMovementRecorder if the movement recorder component at the + * top of the operator body should be displayed + */ + function setDisplayMovementRecorder(displayMovementRecorder: boolean) { + layout.current.displayMovementRecorder = displayMovementRecorder; + updateLayout(); + } - /** - * Sets the display labels property to display or hidden. - * - * @param displayLabels if the button text labels should be displayed - */ - function setDisplayLabels(displayLabels: boolean) { - layout.current.displayLabels = displayLabels; - updateLayout(); - } + /** + * Sets the display labels property to display or hidden. + * + * @param displayLabels if the button text labels should be displayed + */ + function setDisplayLabels(displayLabels: boolean) { + layout.current.displayLabels = displayLabels; + updateLayout(); + } - /** - * Callback when the user clicks on a drop zone, moves the active component - * into the drop zone - * @param path path to the clicked drop zone - */ - function handleDrop(path: string) { - console.log("handleDrop", path); - if (!selectedDefinition) throw Error('Active definition undefined on drop event') - let newPath: string = path; - if (!selectedPath) { - // New element not already in the layout - newPath = addToLayout(selectedDefinition, path, layout.current); - } else { - newPath = moveInLayout(selectedPath, path, layout.current); - } - setSelectedPath(newPath); - console.log('new active path', newPath) - updateLayout(); + /** + * Callback when the user clicks on a drop zone, moves the active component + * into the drop zone + * @param path path to the clicked drop zone + */ + function handleDrop(path: string) { + console.log("handleDrop", path); + if (!selectedDefinition) + throw Error("Active definition undefined on drop event"); + let newPath: string = path; + if (!selectedPath) { + // New element not already in the layout + newPath = addToLayout(selectedDefinition, path, layout.current); + } else { + newPath = moveInLayout(selectedPath, path, layout.current); } + setSelectedPath(newPath); + console.log("new active path", newPath); + updateLayout(); + } - /** - * Callback when a component is selected during customization - * @param path path to the selected component - * @param def definition of the selected component - */ - function handleSelect(def: ComponentDefinition, path?: string) { - console.log('selected', path); - if (!customizing) return; - - // If reselected the same component at the same path, or the same component - // without a path from the sidebar, then unactivate it - const pathsMatch = selectedPath && selectedPath == path; - const defsMatch = !selectedPath && def.type === selectedDefinition?.type && def.id === selectedDefinition?.id; - if (pathsMatch || defsMatch) { - setSelectedDef(undefined); - setSelectedPath(undefined); - return; - } + /** + * Callback when a component is selected during customization + * @param path path to the selected component + * @param def definition of the selected component + */ + function handleSelect(def: ComponentDefinition, path?: string) { + console.log("selected", path); + if (!customizing) return; - // Activate the selected component - setSelectedDef(def); - setSelectedPath(path); + // If reselected the same component at the same path, or the same component + // without a path from the sidebar, then unactivate it + const pathsMatch = selectedPath && selectedPath == path; + const defsMatch = + !selectedPath && + def.type === selectedDefinition?.type && + def.id === selectedDefinition?.id; + if (pathsMatch || defsMatch) { + setSelectedDef(undefined); + setSelectedPath(undefined); + return; } - /** Callback when the delete button in the sidebar is clicked */ - function handleDelete() { - if (!selectedPath) throw Error('handleDelete called when selectedPath is undefined'); - removeFromLayout(selectedPath, layout.current); - updateLayout(); - setSelectedPath(undefined); - setSelectedDef(undefined); - } + // Activate the selected component + setSelectedDef(def); + setSelectedPath(path); + } - /** - * Callback when the customization button is clicked. - */ - const handleToggleCustomize = () => { - if (customizing) { - console.log('saving layout'); - props.storageHandler.saveCurrentLayout(layout.current); - } - setCustomizing(!customizing); - setSelectedDef(undefined); - setSelectedPath(undefined); - } + /** Callback when the delete button in the sidebar is clicked */ + function handleDelete() { + if (!selectedPath) + throw Error("handleDelete called when selectedPath is undefined"); + removeFromLayout(selectedPath, layout.current); + updateLayout(); + setSelectedPath(undefined); + setSelectedDef(undefined); + } - /** Un-select current component when click inside of header */ - function handleClickHeader() { - setSelectedDef(undefined); - setSelectedPath(undefined); + /** + * Callback when the customization button is clicked. + */ + const handleToggleCustomize = () => { + if (customizing) { + console.log("saving layout"); + props.storageHandler.saveCurrentLayout(layout.current); } + setCustomizing(!customizing); + setSelectedDef(undefined); + setSelectedPath(undefined); + }; - /** State passed from the operator and shared by all components */ - const sharedState: SharedState = { - customizing: customizing, - onSelect: handleSelect, - remoteStreams: remoteStreams, - selectedPath: selectedPath, - dropZoneState: { - onDrop: handleDrop, - selectedDefinition: selectedDefinition - }, - buttonStateMap: buttonStateMap.current, - hideLabels: !layout.current.displayLabels, - hasBetaTeleopKit: hasBetaTeleopKit - } + /** Un-select current component when click inside of header */ + function handleClickHeader() { + setSelectedDef(undefined); + setSelectedPath(undefined); + } - /** Properties for the global options area of the sidebar */ - const globalOptionsProps: GlobalOptionsProps = { - displayMovementRecorder: layout.current.displayMovementRecorder, - displayLabels: layout.current.displayLabels, - setDisplayMovementRecorder: setDisplayMovementRecorder, - setDisplayLabels: setDisplayLabels, - defaultLayouts: Object.keys(DEFAULT_LAYOUTS), - customLayouts: props.storageHandler.getCustomLayoutNames(), - loadLayout: (layoutName: string, dflt: boolean) => { - layout.current = dflt ? - props.storageHandler.loadDefaultLayout(layoutName as DefaultLayoutName) : - props.storageHandler.loadCustomLayout(layoutName); - updateLayout(); - }, - saveLayout: (layoutName: string) => { props.storageHandler.saveCustomLayout(layout.current, layoutName); } - } + /** State passed from the operator and shared by all components */ + const sharedState: SharedState = { + customizing: customizing, + onSelect: handleSelect, + remoteStreams: remoteStreams, + selectedPath: selectedPath, + dropZoneState: { + onDrop: handleDrop, + selectedDefinition: selectedDefinition, + }, + buttonStateMap: buttonStateMap.current, + hideLabels: !layout.current.displayLabels, + hasBetaTeleopKit: hasBetaTeleopKit, + }; - const actionModes = Object.values(ActionMode); + /** Properties for the global options area of the sidebar */ + const globalOptionsProps: GlobalOptionsProps = { + displayMovementRecorder: layout.current.displayMovementRecorder, + displayLabels: layout.current.displayLabels, + setDisplayMovementRecorder: setDisplayMovementRecorder, + setDisplayLabels: setDisplayLabels, + defaultLayouts: Object.keys(DEFAULT_LAYOUTS), + customLayouts: props.storageHandler.getCustomLayoutNames(), + loadLayout: (layoutName: string, dflt: boolean) => { + layout.current = dflt + ? props.storageHandler.loadDefaultLayout( + layoutName as DefaultLayoutName, + ) + : props.storageHandler.loadCustomLayout(layoutName); + updateLayout(); + }, + saveLayout: (layoutName: string) => { + props.storageHandler.saveCustomLayout(layout.current, layoutName); + }, + }; - return ( -
-
- {/* Action mode button */} - setActionMode(actionModes[idx])} - selectedIndex={actionModes.indexOf(layout.current.actionMode)} - possibleOptions={actionModes} - showActive - placement="bottom" - /> - { setVelocityScale(newScale); FunctionProvider.velocityScale = newScale; }} - /> - -
- { -
-
0, fadeOut: buttonCollision.length == 0 })}> - - - {buttonCollision.length > 0 ? buttonCollision.join(', ') + " in collision!" : ""} - - -
-
- } - {moveBaseState && -
-
- -
-
- } -
- -
-
- -
-
+ ); +}; diff --git a/src/pages/operator/tsx/README.md b/src/pages/operator/tsx/README.md index 1a3b27b9..2a869b25 100644 --- a/src/pages/operator/tsx/README.md +++ b/src/pages/operator/tsx/README.md @@ -7,26 +7,28 @@ For more info on [how the customization logic works](./customization_logic.md). ## Directory outline A list of directories and their contents: -* `basic_components/` - * simple components used in multiple places throughout the code (such as specific buttons or popups) -* `default_layouts/` - * Javascript object definitions for the different default layouts avaliable for the user to load - * The default layouts provide a safe state of the interface that can always be reloaded -* `function_providers/` - * subclasses of the `FunctionProvider` class which are used during render to determine the functionality of controls (such as button pads or predictive display) -* `layout_components/` - * components which can be dynamically added, changed, or removed by the user - * includes things like camera views, button pads, panels, and tabs -* `static_components/` - * components which are always visible, such as the action mode button, sidebar, speed control, or customization button -* `utils/` - * typescript files which contain logic seperated from the React components -* `Operator.tsx/` - * Highest level React component for the entire operator page -* `index.tsx` - * Logic for connecting with the robot browser using WebRTC - * Initializes state for the application - * Renders `Operator` + +- `basic_components/` + - simple components used in multiple places throughout the code (such as specific buttons or popups) +- `default_layouts/` + - Javascript object definitions for the different default layouts available for the user to load + - The default layouts provide a safe state of the interface that can always be reloaded +- `function_providers/` + - subclasses of the `FunctionProvider` class which are used during render to determine the functionality of controls (such as button pads or predictive display) +- `layout_components/` + - components which can be dynamically added, changed, or removed by the user + - includes things like camera views, button pads, panels, and tabs +- `static_components/` + - components which are always visible, such as the action mode button, sidebar, speed control, or customization button +- `utils/` + - typescript files which contain logic separated from the React components +- `Operator.tsx/` + - Highest level React component for the entire operator page +- `index.tsx` + - Logic for connecting with the robot browser using WebRTC + - Initializes state for the application + - Renders `Operator` + # Render Logic Flow This diagram shows the flow of logic between classes and components while the Operator Page renders. @@ -40,7 +42,7 @@ This diagram shows the flow of logic between classes and components while the Op : creates a `sharedState` object with relevant information for all components in the layout, then passes the `layout` and `sharedState` to the `LayoutArea`. **`LayoutArea`** -: corresponds to "Layout" in the Component Hiearchy. This renders the individual components in the layout, with `DropZone`'s in between so that components can be moved in customize mode. +: corresponds to "Layout" in the Component Hierarchy. This renders the individual components in the layout, with `DropZone`'s in between so that components can be moved in customize mode. **`CustomizableComponent`** : a single component in the layout, the customizable component renders a different subcomponent based on the `type` in the `ComponentDefinition`. @@ -49,4 +51,4 @@ This diagram shows the flow of logic between classes and components while the Op : takes the action mode and speed control from `ActionMode` and `SpeedControl` respectively. Returns a set of functions for how different controls should behave, for example `onClick` and `onRelease` for a button on a `ButtonPad`. **`ButtonPad` and other controls** -: when `ButtonPad` or another control renders, it gets the set of functions from the `FunctionProvider`. \ No newline at end of file +: when `ButtonPad` or another control renders, it gets the set of functions from the `FunctionProvider`. diff --git a/src/pages/operator/tsx/basic_components/AccordionSelect.tsx b/src/pages/operator/tsx/basic_components/AccordionSelect.tsx index 3e64d6f2..cd28582c 100644 --- a/src/pages/operator/tsx/basic_components/AccordionSelect.tsx +++ b/src/pages/operator/tsx/basic_components/AccordionSelect.tsx @@ -1,54 +1,54 @@ - import React, { useRef, useState } from "react"; -import "operator/css/basic_components.css" +import "operator/css/basic_components.css"; import { className } from "shared/util"; export const AccordionSelect = (props: { - title: string, - possibleOptions: T[], - onChange: (selectedIndex: number) => void, + title: string; + possibleOptions: T[]; + onChange: (selectedIndex: number) => void; }) => { - const [active, setActiveState] = useState(false); - const [height, setHeightState] = useState("0px"); - const [rotate, setRotateState] = useState("accordion_icon"); - const content = useRef(null); + const [active, setActiveState] = useState(false); + const [height, setHeightState] = useState("0px"); + const [rotate, setRotateState] = useState("accordion_icon"); + const content = useRef(null); - function toggleAccordion() { - setActiveState(active ? false : true); - setHeightState( - active ? "0px" : `${content.current.scrollHeight}px` - ); - setRotateState( - active ? "accordion_icon" : "accordion_icon rotate" - ); - } - - function mapFunc(option: T, idx: number) { - return ( -
{ props.onChange(idx); toggleAccordion()}}> - {option} -
- ) - } + function toggleAccordion() { + setActiveState(active ? false : true); + setHeightState(active ? "0px" : `${content.current.scrollHeight}px`); + setRotateState(active ? "accordion_icon" : "accordion_icon rotate"); + } + function mapFunc(option: T, idx: number) { return ( -
- -
-
- {props.possibleOptions.map(mapFunc)} -
-
-
+
{ + props.onChange(idx); + toggleAccordion(); + }} + > + {option} +
); -} \ No newline at end of file + } + + return ( +
+ +
+
{props.possibleOptions.map(mapFunc)}
+
+
+ ); +}; diff --git a/src/pages/operator/tsx/basic_components/Alert.tsx b/src/pages/operator/tsx/basic_components/Alert.tsx index 2235eb5d..87c14961 100644 --- a/src/pages/operator/tsx/basic_components/Alert.tsx +++ b/src/pages/operator/tsx/basic_components/Alert.tsx @@ -1,30 +1,30 @@ import React, { useState, ReactElement, JSXElementConstructor } from "react"; import { MouseEvent } from "react"; import { className } from "shared/util"; -import "operator/css/Alert.css" +import "operator/css/Alert.css"; // https://blog.logrocket.com/create-custom-react-alert-message/ -export const Alert = (props: { - children?: ReactElement>, - type: string, - message?: string +export const Alert = (props: { + children?: ReactElement>; + type: string; + message?: string; }) => { - const [isShow, setIsShow] = useState(true); + const [isShow, setIsShow] = useState(true); - function renderElAlert() { - return React.cloneElement(props.children!); - }; + function renderElAlert() { + return React.cloneElement(props.children!); + } - React.useEffect(() => { - setIsShow(true) - }, [props]) + React.useEffect(() => { + setIsShow(true); + }, [props]); - return ( -
- setIsShow(false) }> - × - - {props.children ? renderElAlert() : props.message} -
- ); -} \ No newline at end of file + return ( +
+ setIsShow(false)}> + × + + {props.children ? renderElAlert() : props.message} +
+ ); +}; diff --git a/src/pages/operator/tsx/basic_components/CheckToggleButton.tsx b/src/pages/operator/tsx/basic_components/CheckToggleButton.tsx index 71434981..c2397e6e 100644 --- a/src/pages/operator/tsx/basic_components/CheckToggleButton.tsx +++ b/src/pages/operator/tsx/basic_components/CheckToggleButton.tsx @@ -1,5 +1,5 @@ import { className } from "shared/util"; -import "operator/css/basic_components.css" +import "operator/css/basic_components.css"; import { isMobile } from "react-device-detect"; import React from "react"; @@ -7,35 +7,38 @@ import React from "react"; * Properties for {@link CheckToggleButton} */ type CheckToggleButtonProps = { - /** Toggled on if true, toggled off if false. */ - checked: boolean, - /** - * Function when button is clicked, this should probably toggle the state - * of `checked` - */ - onClick: () => void, - /** - * Text to display on the button to the right of the checkbox. - */ - label: string, -} + /** Toggled on if true, toggled off if false. */ + checked: boolean; + /** + * Function when button is clicked, this should probably toggle the state + * of `checked` + */ + onClick: () => void; + /** + * Text to display on the button to the right of the checkbox. + */ + label: string; +}; /** - * A button with a check box on the left side to indicate if the button is + * A button with a check box on the left side to indicate if the button is * toggled on or off. - * + * * @param props {@link CheckToggleButtonProps} */ export const CheckToggleButton = (props: CheckToggleButtonProps) => { - const { checked } = props; - const icon = checked ? "check_box" : "check_box_outline_blank"; - return ( - - ) -} \ No newline at end of file + const { checked } = props; + const icon = checked ? "check_box" : "check_box_outline_blank"; + return ( + + ); +}; diff --git a/src/pages/operator/tsx/basic_components/Dropdown.tsx b/src/pages/operator/tsx/basic_components/Dropdown.tsx index 5cb3dc3b..e674d71f 100644 --- a/src/pages/operator/tsx/basic_components/Dropdown.tsx +++ b/src/pages/operator/tsx/basic_components/Dropdown.tsx @@ -1,64 +1,77 @@ import React from "react"; import { className } from "shared/util"; -import "operator/css/basic_components.css" +import "operator/css/basic_components.css"; export const Dropdown = (props: { - onChange: (selectedIndex: number) => void, - possibleOptions: T[], - selectedIndex?: number, - placeholderText?: string, - showActive?: boolean, - placement: string + onChange: (selectedIndex: number) => void; + possibleOptions: T[]; + selectedIndex?: number; + placeholderText?: string; + showActive?: boolean; + placement: string; }) => { - const [showDropdown, setShowDropdown] = React.useState(false); - const [placement, setPlacement] = React.useState(props.placement) - const inputRef = React.useRef(null); - if (props.selectedIndex === undefined && !props.placeholderText) - throw Error("both selectedOption and placeholderText undefined"); + const [showDropdown, setShowDropdown] = React.useState(false); + const [placement, setPlacement] = React.useState(props.placement); + const inputRef = React.useRef(null); + if (props.selectedIndex === undefined && !props.placeholderText) + throw Error("both selectedOption and placeholderText undefined"); - - // Handler to close dropdown when click outside - React.useEffect(() => { - const handler = (e: any) => { - if (inputRef.current && !inputRef.current.contains(e.target)) { - setShowDropdown(false); - } - }; - if (showDropdown) { - window.addEventListener("click", handler); - return () => { - window.removeEventListener("click", handler); - }; - } - }); - - function mapFunc(option: T, idx: number) { - const active = idx === props.selectedIndex; - if (active && !props.showActive) return null; - return ( - - ) + // Handler to close dropdown when click outside + React.useEffect(() => { + const handler = (e: any) => { + if (inputRef.current && !inputRef.current.contains(e.target)) { + setShowDropdown(false); + } + }; + if (showDropdown) { + window.addEventListener("click", handler); + return () => { + window.removeEventListener("click", handler); + }; } + }); + function mapFunc(option: T, idx: number) { + const active = idx === props.selectedIndex; + if (active && !props.showActive) return null; return ( -
- - - -
+ ); -} \ No newline at end of file + } + + return ( +
+ + +
+ ); +}; diff --git a/src/pages/operator/tsx/basic_components/PopupModal.tsx b/src/pages/operator/tsx/basic_components/PopupModal.tsx index 14d6dfa4..eaadc323 100644 --- a/src/pages/operator/tsx/basic_components/PopupModal.tsx +++ b/src/pages/operator/tsx/basic_components/PopupModal.tsx @@ -1,87 +1,91 @@ import React from "react"; -import "operator/css/basic_components.css" -import { className } from "shared/util" - +import "operator/css/basic_components.css"; +import { className } from "shared/util"; /** Properties for {@link PopupModal} */ export type PopupModalProps = { - /** If the popup should be shown */ - show: boolean, - /** - * Callback to set if the popup is shown, used to close the popup on - * cancel, accept, or click outside of window. - */ - setShow: (show: boolean) => void, - /** Callback when the user clicks accept. */ - onAccept: () => void, - /** Optional Callback when user cancels */ - onCancel?: () => void - /** Optional HTML id. */ - id?: string, - /** Text to display on text button, defaults to "Accept" if undefined. */ - acceptButtonText?: string, - /** Accept button disabled if true, enabled if false or undefined. */ - acceptDisabled?: boolean, - /** Modal size */ - size?: "small" | "medium" | "large" - mobile?: boolean -} + /** If the popup should be shown */ + show: boolean; + /** + * Callback to set if the popup is shown, used to close the popup on + * cancel, accept, or click outside of window. + */ + setShow: (show: boolean) => void; + /** Callback when the user clicks accept. */ + onAccept: () => void; + /** Optional Callback when user cancels */ + onCancel?: () => void; + /** Optional HTML id. */ + id?: string; + /** Text to display on text button, defaults to "Accept" if undefined. */ + acceptButtonText?: string; + /** Accept button disabled if true, enabled if false or undefined. */ + acceptDisabled?: boolean; + /** Modal size */ + size?: "small" | "medium" | "large"; + mobile?: boolean; +}; /** - * Generic component for a popup modal which covers the full screen with a + * Generic component for a popup modal which covers the full screen with a * darkened background. * @param props see {@link PopupModalProps} */ -export const PopupModal: React.FunctionComponent> = (props) => { - /** Call `onAccept` and hide the popup. */ - function handleClickAccept() { - props.onAccept(); - props.setShow(false); - } - /** Handle user keyboard input. */ - function handleKeyDown(e: React.KeyboardEvent) { - if (e.key == "Enter") { - handleClickAccept(); - } else if (e.key == "Escape") { - props.setShow(false); - } +export const PopupModal: React.FunctionComponent< + React.PropsWithChildren +> = (props) => { + /** Call `onAccept` and hide the popup. */ + function handleClickAccept() { + props.onAccept(); + props.setShow(false); + } + /** Handle user keyboard input. */ + function handleKeyDown(e: React.KeyboardEvent) { + if (e.key == "Enter") { + handleClickAccept(); + } else if (e.key == "Escape") { + props.setShow(false); } - const size = props.size - const mobile = props.mobile - const element = props.show ? ( - - {/* + {/* */} -
- {props.children} -
- - -
-
- {/*
*/} - -
- ) : null; +
+ {props.children} +
+ + +
+
+ {/* */} + + + ) : null; - return element; -} \ No newline at end of file + return element; +}; diff --git a/src/pages/operator/tsx/basic_components/RadioGroup.tsx b/src/pages/operator/tsx/basic_components/RadioGroup.tsx index 70c9b4f0..14731217 100644 --- a/src/pages/operator/tsx/basic_components/RadioGroup.tsx +++ b/src/pages/operator/tsx/basic_components/RadioGroup.tsx @@ -1,74 +1,83 @@ -import React from 'react'; -import { className } from 'shared/util'; -import 'operator/css/RadioGroup.css' -import { isMobile } from 'react-device-detect'; +import React from "react"; +import { className } from "shared/util"; +import "operator/css/RadioGroup.css"; +import { isMobile } from "react-device-detect"; export const RadioButton = (props: { - label: string, - selected: boolean, - onClick: () => void, - functs: RadioFunctions + label: string; + selected: boolean; + onClick: () => void; + functs: RadioFunctions; }) => { - return ( -
- -
- {props.functs.Edit && - mode_edit_outline - } - {props.functs.Delete && - props.functs.Delete!(props.label)}> - delete_outline - - } -
-
- ) -} + return ( +
+ +
+ {props.functs.Edit && ( + mode_edit_outline + )} + {props.functs.Delete && ( + props.functs.Delete!(props.label)} + > + delete_outline + + )} +
+
+ ); +}; export interface RadioFunctions { - GetLabels: () => string[] - SelectedLabel: (label: string) => void - // Add?: () => void - Edit?: (label: string) => void, - Delete?: (label: string) => void, - // Start?: (label: string) => void, - // Cancel?: () => void + GetLabels: () => string[]; + SelectedLabel: (label: string) => void; + // Add?: () => void + Edit?: (label: string) => void; + Delete?: (label: string) => void; + // Start?: (label: string) => void, + // Cancel?: () => void } -export const RadioGroup = (props: { - functs: RadioFunctions - }) => { - const [selected, setSelected] = React.useState() +export const RadioGroup = (props: { functs: RadioFunctions }) => { + const [selected, setSelected] = React.useState(); - return ( -
e.preventDefault()}> - {props.functs.GetLabels().map((label, index) => ( - { - if (selected === label) { - setSelected(''); - props.functs.SelectedLabel('') - } else { - setSelected(label); props.functs.SelectedLabel(label) - } - }} - functs={props.functs} - /> - ))} - {/* {props.functs.Add && + return ( +
e.preventDefault()} + > + {props.functs.GetLabels().map((label, index) => ( + { + if (selected === label) { + setSelected(""); + props.functs.SelectedLabel(""); + } else { + setSelected(label); + props.functs.SelectedLabel(label); + } + }} + functs={props.functs} + /> + ))} + {/* {props.functs.Add && add @@ -78,6 +87,6 @@ export const RadioGroup = (props: { play_arrow } */} -
- ) -} +
+ ); +}; diff --git a/src/pages/operator/tsx/basic_components/TabGroup.tsx b/src/pages/operator/tsx/basic_components/TabGroup.tsx index e3d508bb..c5af3bbf 100644 --- a/src/pages/operator/tsx/basic_components/TabGroup.tsx +++ b/src/pages/operator/tsx/basic_components/TabGroup.tsx @@ -1,51 +1,64 @@ -import React, { JSXElementConstructor, ReactElement } from 'react'; -import { className } from 'shared/util'; -import 'operator/css/TabGroup.css' +import React, { JSXElementConstructor, ReactElement } from "react"; +import { className } from "shared/util"; +import "operator/css/TabGroup.css"; export const Tab = (props: { - label: string, - active: boolean, - onClick: () => void, - pill: boolean + label: string; + active: boolean; + onClick: () => void; + pill: boolean; }) => { - const active = props.active + const active = props.active; - return ( - props.pill - ?
  • {props.label}
  • - : - ) -} + return props.pill ? ( +
  • + {props.label} +
  • + ) : ( + + ); +}; export const TabGroup = (props: { - tabLabels: string[], - tabContent: ((active: boolean) => React.JSX.Element)[], - startIdx: number, - onChange: (index: number) => void - pill: boolean, - }) => { - const tabLabels = props.tabLabels - const tabContent = props.tabContent - const [activeIndex, setActiveIndex] = React.useState(props.startIdx) + tabLabels: string[]; + tabContent: ((active: boolean) => React.JSX.Element)[]; + startIdx: number; + onChange: (index: number) => void; + pill: boolean; +}) => { + const tabLabels = props.tabLabels; + const tabContent = props.tabContent; + const [activeIndex, setActiveIndex] = React.useState(props.startIdx); - return ( -
    e.preventDefault()}> -
    - {tabLabels.map((label, index) => ( - { props.onChange(index); setActiveIndex(index) }} - pill={props.pill} - /> - ))} -
    -
    - {tabContent.map((renderFn, index) => ( - renderFn(activeIndex === index) - ))} -
    -
    - ) -} + return ( +
    e.preventDefault()}> +
    + {tabLabels.map((label, index) => ( + { + props.onChange(index); + setActiveIndex(index); + }} + pill={props.pill} + /> + ))} +
    +
    + {tabContent.map((renderFn, index) => renderFn(activeIndex === index))} +
    +
    + ); +}; diff --git a/src/pages/operator/tsx/create_component.md b/src/pages/operator/tsx/create_component.md index 3b799b0d..0b542630 100644 --- a/src/pages/operator/tsx/create_component.md +++ b/src/pages/operator/tsx/create_component.md @@ -3,13 +3,14 @@ To create a new component you'll want to follow these steps: 1. **Create a new type** for your component in `ComponentType` in `utils/component_definitions.tsx`. - * If there are going to be subtypes of your component then define an id for each of the subtypes like `CameraViewId` in `utils/component_definitions.tsx` - * If you component needs any other field in order for it to render (such as a `TabDefinition` having a `label`), then create a separate definition for your component with those fields. + + - If there are going to be subtypes of your component then define an id for each of the subtypes like `CameraViewId` in `utils/component_definitions.tsx` + - If you component needs any other field in order for it to render (such as a `TabDefinition` having a `label`), then create a separate definition for your component with those fields. 1. **Create a new file** in `layout_components` with the React code for your new component. The React functional component should take `CustomizableComponentProps` as its props. A field in `CustomizableComponentProps` is the `ComponentDefinition`, so you should be able to access all of the fields in the components definition there. Here are some more details about the React component you create: - 1. **Selecting the component**. In your React component code, make sure there's a way to select the component if the user should be able to move it around the layout. This means calling `props.sharedState.onSelect` with the definition and path of the component. For an example, see the `onSelect` function defined in the `ButtonPad` functional component in `layout_components/ButtonPad.tsx` - 1. **Setting the class name**. The standard is to use the [`className()`](../../../shared/util.tsx) util function to set the "customizing" and "selected" flags in a component classname. See [`CameraView.tsx`](./layout_components/CameraView.tsx) for an example. + 1. **Selecting the component**. In your React component code, make sure there's a way to select the component if the user should be able to move it around the layout. This means calling `props.sharedState.onSelect` with the definition and path of the component. For an example, see the `onSelect` function defined in the `ButtonPad` functional component in `layout_components/ButtonPad.tsx` + 1. **Setting the class name**. The standard is to use the [`className()`](../../../shared/util.tsx) util function to set the "customizing" and "selected" flags in a component classname. See [`CameraView.tsx`](./layout_components/CameraView.tsx) for an example. 1. **Add the component to `CustomizableComponent`**. In the switch statement within `CustomizableComponent` add a case associating the `ComponentType` for your component with the React functional component. diff --git a/src/pages/operator/tsx/customization_logic.md b/src/pages/operator/tsx/customization_logic.md index 53ec2917..e3d777ca 100644 --- a/src/pages/operator/tsx/customization_logic.md +++ b/src/pages/operator/tsx/customization_logic.md @@ -1,52 +1,61 @@ - # **Overview of Customization Logic** + # Layout The `layout` is passed into `Operator` as a property. The `layout` defines which components are in the layout and how they're arranged. Below is a very simple example: ```ts -import { LayoutDefinition, ComponentType, ActionMode, CameraViewId, ButtonPadId, OverheadVideoStreamDef, TabDefinition, PanelDefinition } from "/utils/component_definitions"; +import { + LayoutDefinition, + ComponentType, + ActionMode, + CameraViewId, + ButtonPadId, + OverheadVideoStreamDef, + TabDefinition, + PanelDefinition, +} from "/utils/component_definitions"; export const SIMPLE_LAYOUT: LayoutDefinition = { - // All components have a type - type: ComponentType.Layout, - // If voice control should be displayed on the operator page - displayVoiceControl: false, - // The state of the action mode dropdown - actionMode: ActionMode.StepActions, - - // The customizable components in the layout - children: [ + // All components have a type + type: ComponentType.Layout, + // If voice control should be displayed on the operator page + displayVoiceControl: false, + // The state of the action mode dropdown + actionMode: ActionMode.StepActions, + + // The customizable components in the layout + children: [ + { + // The layout contains a single panel + type: ComponentType.Panel, + children: [ { - // The layout contains a single panel - type: ComponentType.Panel, - children: [ + // The panel contains a single tab + type: ComponentType.SingleTab, + // The title of the tab is "Tab One" + label: "Tab One", + children: [ + { + // Tab contains a single camera view + type: ComponentType.CameraView, + // From the Stretch overhead camera + id: CameraViewId.overhead, + children: [ { - // The panel contains a single tab - type: ComponentType.SingleTab, - // The title of the tab is "Tab One" - label: 'Tab One', - children: [ - { - // Tab contains a single camera view - type: ComponentType.CameraView, - // From the Stretch overhead camera - id: CameraViewId.overhead, - children: [ - { - // Camera view has a button pad overlay - type: ComponentType.ButtonPad, - // Button pad to control the base - id: ButtonPadId.Drive - } - ] - } as OverheadVideoStreamDef - ] - } as TabDefinition - ] - } as PanelDefinition - ] -} + // Camera view has a button pad overlay + type: ComponentType.ButtonPad, + // Button pad to control the base + id: ButtonPadId.Drive, + }, + ], + } as OverheadVideoStreamDef, + ], + } as TabDefinition, + ], + } as PanelDefinition, + ], +}; ``` The layout is an object containing a set of nested `ComponentDefinitions`, where each `ComponentDefinition` represents the details for a `CustomizableComponent` to render. @@ -57,23 +66,23 @@ All of the definitions for the different component definitions can be found in ` # Logic for Adding, Rearranging, and Removing Components - ![drop zone example](../../../../documentation/assets/operator/dropzones.png) ## Selecting a Component `CustomizableComponents` can be **selected** in the `LayoutArea` or from the `Sidebar`'s component provider. The screenshot above shows the Realsense Camera View selected from the `Sidebar`'s component provider. -`Operator` **keeps track of the selected component** using its `selectedPath` and `selectedDefinition` fields. -* the `selectedDefinition` is the `ComponentDefinition` for the selected object -* the `selectedPath` is a string representing the path to the component in the layout. -* when nothing is selected, these fields are `undefined` +`Operator` **keeps track of the selected component** using its `selectedPath` and `selectedDefinition` fields. + +- the `selectedDefinition` is the `ComponentDefinition` for the selected object +- the `selectedPath` is a string representing the path to the component in the layout. +- when nothing is selected, these fields are `undefined` > For example: the path to the right video stream would be `0-0-1`, because it's at the 1 index in the tab, which is at the 0 index in the panel, which is in the 0 index in the layout. ## DropZones -The `LayoutArea` is made up of two main elements: `CustomizableComponent` and `DropZone`. +The `LayoutArea` is made up of two main elements: `CustomizableComponent` and `DropZone`. `DropZone` shows the user where they can place elements. In the image above the **gray areas with dotted outlines** are each a `DropZone`, where the user can click to place the camera view. @@ -83,16 +92,16 @@ When a user clicks on a `DropZone`, it executes a callback to `handleDrop()` (de ## Adding a Component -* When a component is selected from the `Sidebar`, `selectedDefinition` is set with the corresponding `type` (and `id` if applicable). -* `selectedPath` is undefined since components in the sidebar are not in the layout and thus do not have a `path`. -* When the user clicks on a `DropZone` the new `ComponentDefinition` is added to the `layout` in `Operator` +- When a component is selected from the `Sidebar`, `selectedDefinition` is set with the corresponding `type` (and `id` if applicable). +- `selectedPath` is undefined since components in the sidebar are not in the layout and thus do not have a `path`. +- When the user clicks on a `DropZone` the new `ComponentDefinition` is added to the `layout` in `Operator` ## Moving a Component -* When a component is selected from `LayoutArea`, the `CustomizableComponent` calls `handleSelect()` in `Operator` with its `path` and `definition`. This sets the `selectedPath` and `selectedDefinition` state in `Operator`. -* When the user clicks on a `DropZone` the component is removed from its old position in the `layout` and added to a new position at the `path` of the clicked `DropZone`. +- When a component is selected from `LayoutArea`, the `CustomizableComponent` calls `handleSelect()` in `Operator` with its `path` and `definition`. This sets the `selectedPath` and `selectedDefinition` state in `Operator`. +- When the user clicks on a `DropZone` the component is removed from its old position in the `layout` and added to a new position at the `path` of the clicked `DropZone`. ## Removing a Component -* Once a component from the `LayoutArea` is selected, the delete button in the bottom of the sidebar will become enabled. -* When the user clicks on the delete button, it calls `handleDelete()` in `Operator` which removes the component from the layout. +- Once a component from the `LayoutArea` is selected, the delete button in the bottom of the sidebar will become enabled. +- When the user clicks on the delete button, it calls `handleDelete()` in `Operator` which removes the component from the layout. diff --git a/src/pages/operator/tsx/default_layouts/SIMPLE_LAYOUT.tsx b/src/pages/operator/tsx/default_layouts/SIMPLE_LAYOUT.tsx index 6191cb43..a0e74b6f 100644 --- a/src/pages/operator/tsx/default_layouts/SIMPLE_LAYOUT.tsx +++ b/src/pages/operator/tsx/default_layouts/SIMPLE_LAYOUT.tsx @@ -1,99 +1,110 @@ -import { ComponentType, CameraViewId, ButtonPadId, CameraViewDefinition, ButtonPadDefinition, PanelDefinition, TabDefinition, LayoutDefinition, ActionMode, LayoutGridDefinition } from "../utils/component_definitions"; +import { + ComponentType, + CameraViewId, + ButtonPadId, + CameraViewDefinition, + ButtonPadDefinition, + PanelDefinition, + TabDefinition, + LayoutDefinition, + ActionMode, + LayoutGridDefinition, +} from "../utils/component_definitions"; /** * Basic Layout */ export const BASIC_LAYOUT: LayoutDefinition = { - type: ComponentType.Layout, - displayMovementRecorder: false, - displayLabels: true, - actionMode: ActionMode.PressAndHold, - children: [ + type: ComponentType.Layout, + displayMovementRecorder: false, + displayLabels: true, + actionMode: ActionMode.PressAndHold, + children: [ + { + type: ComponentType.LayoutGrid, + children: [ { - type: ComponentType.LayoutGrid, - children: [ + type: ComponentType.Panel, + children: [ + { + type: ComponentType.SingleTab, + label: "Camera Views", + children: [ + // Overhead camera { - type: ComponentType.Panel, - children: [ - { - type: ComponentType.SingleTab, - label: 'Camera Views', - children: [ - // Overhead camera - { - type: ComponentType.CameraView, - id: CameraViewId.overhead, - displayButtons: true, - children: [] - } as CameraViewDefinition, - { - type: ComponentType.CameraView, - id: CameraViewId.gripper, - displayButtons: true, - children: [] - } as CameraViewDefinition - ] - } - ] - } as PanelDefinition - ] - } as LayoutGridDefinition, + type: ComponentType.CameraView, + id: CameraViewId.overhead, + displayButtons: true, + children: [], + } as CameraViewDefinition, + { + type: ComponentType.CameraView, + id: CameraViewId.gripper, + displayButtons: true, + children: [], + } as CameraViewDefinition, + ], + }, + ], + } as PanelDefinition, + ], + } as LayoutGridDefinition, + { + type: ComponentType.LayoutGrid, + children: [ + { + type: ComponentType.Panel, + children: [ + { + type: ComponentType.SingleTab, + label: "Base", + children: [ + { + type: ComponentType.ButtonPad, + id: ButtonPadId.Base, + } as ButtonPadDefinition, + ], + } as TabDefinition, + { + type: ComponentType.SingleTab, + label: "Wrist & Gripper", + children: [ + { + type: ComponentType.ButtonPad, + id: ButtonPadId.DexWrist, + } as ButtonPadDefinition, + ], + } as TabDefinition, + { + type: ComponentType.SingleTab, + label: "Arm & Lift", + children: [ + { + type: ComponentType.ButtonPad, + id: ButtonPadId.Arm, + } as ButtonPadDefinition, + ], + } as TabDefinition, + ], + } as PanelDefinition, { - type: ComponentType.LayoutGrid, - children: [ + type: ComponentType.Panel, + children: [ + { + type: ComponentType.SingleTab, + label: "Safety", + children: [ { - type: ComponentType.Panel, - children: [ - { - type: ComponentType.SingleTab, - label: 'Base', - children: [ - { - type: ComponentType.ButtonPad, - id: ButtonPadId.Base, - } as ButtonPadDefinition - ] - } as TabDefinition, - { - type: ComponentType.SingleTab, - label: 'Wrist & Gripper', - children: [ - { - type: ComponentType.ButtonPad, - id: ButtonPadId.DexWrist, - } as ButtonPadDefinition - ] - } as TabDefinition, - { - type: ComponentType.SingleTab, - label: 'Arm & Lift', - children: [ - { - type: ComponentType.ButtonPad, - id: ButtonPadId.Arm, - } as ButtonPadDefinition - ] - } as TabDefinition - ] - } as PanelDefinition, + type: ComponentType.RunStopButton, + }, { - type: ComponentType.Panel, - children: [ - { - type: ComponentType.SingleTab, - label: 'Safety', - children: [ - { - type: ComponentType.RunStopButton - }, - { - type: ComponentType.BatteryGuage - } - ] - } - ] - } as PanelDefinition - ] - } as LayoutGridDefinition - ] -} \ No newline at end of file + type: ComponentType.BatteryGuage, + }, + ], + }, + ], + } as PanelDefinition, + ], + } as LayoutGridDefinition, + ], +}; diff --git a/src/pages/operator/tsx/function_providers/BatteryVoltageFunctionProvider.tsx b/src/pages/operator/tsx/function_providers/BatteryVoltageFunctionProvider.tsx index ef2e7fe6..21e243c7 100644 --- a/src/pages/operator/tsx/function_providers/BatteryVoltageFunctionProvider.tsx +++ b/src/pages/operator/tsx/function_providers/BatteryVoltageFunctionProvider.tsx @@ -1,52 +1,58 @@ -import { FunctionProvider } from "./FunctionProvider" +import { FunctionProvider } from "./FunctionProvider"; export type BatteryVoltageFunctions = { - getColor: () => string -} + getColor: () => string; +}; export class BatteryVoltageFunctionProvider extends FunctionProvider { - public voltage: number = 0.0; - public voltageChangeCallback: (color: string) => void; + public voltage: number = 0.0; + public voltageChangeCallback: (color: string) => void; - constructor() { - super() - this.updateVoltage = this.updateVoltage.bind(this) - } + constructor() { + super(); + this.updateVoltage = this.updateVoltage.bind(this); + } - public updateVoltage(voltage: number): void { - this.voltage = voltage - if (this.voltageChangeCallback) this.voltageChangeCallback(this.getColor()) - } + public updateVoltage(voltage: number): void { + this.voltage = voltage; + if (this.voltageChangeCallback) this.voltageChangeCallback(this.getColor()); + } - private getColor() { - let vbat_min = 10.0 - let vbat_max = 12.0 - let dv = (vbat_max - vbat_min)/4.0 - - if (!this.voltage) return 'red' // throw 'Cannot retrieve battery voltage' - - if (this.voltage < vbat_min) { - return 'red' // [64, 0, 0] - } else if (this.voltage >= vbat_min && this.voltage < (vbat_min + dv)) { - return 'orange-red' // [64, 32, 0] - } else if (this.voltage >= (vbat_min + dv) && this.voltage < (vbat_min + (2 * dv))) { - return 'orange-yellow' // [64, 64, 0] - } else if (this.voltage >= (vbat_min + (2 * dv)) && this.voltage < (vbat_min + (3 * dv))) { - return 'yellow' // [64, 64, 0] - } else if (this.voltage >= (vbat_min + (3 * dv)) && this.voltage < vbat_max) { - return 'yellow-green' // [32, 64, 0] - } else { - return 'green' // [0, 64, 0] - } - } + private getColor() { + let vbat_min = 10.0; + let vbat_max = 12.0; + let dv = (vbat_max - vbat_min) / 4.0; - /** - * Records a callback from the function provider. The callback is called - * whenever the battery voltage changes. - * - * @param callback callback to function provider - */ - public setVoltageChangeCallback(callback: (color: string) => void) { - this.voltageChangeCallback = callback; + if (!this.voltage) return "red"; // throw 'Cannot retrieve battery voltage' + + if (this.voltage < vbat_min) { + return "red"; // [64, 0, 0] + } else if (this.voltage >= vbat_min && this.voltage < vbat_min + dv) { + return "orange-red"; // [64, 32, 0] + } else if ( + this.voltage >= vbat_min + dv && + this.voltage < vbat_min + 2 * dv + ) { + return "orange-yellow"; // [64, 64, 0] + } else if ( + this.voltage >= vbat_min + 2 * dv && + this.voltage < vbat_min + 3 * dv + ) { + return "yellow"; // [64, 64, 0] + } else if (this.voltage >= vbat_min + 3 * dv && this.voltage < vbat_max) { + return "yellow-green"; // [32, 64, 0] + } else { + return "green"; // [0, 64, 0] } -} \ No newline at end of file + } + + /** + * Records a callback from the function provider. The callback is called + * whenever the battery voltage changes. + * + * @param callback callback to function provider + */ + public setVoltageChangeCallback(callback: (color: string) => void) { + this.voltageChangeCallback = callback; + } +} diff --git a/src/pages/operator/tsx/function_providers/ButtonFunctionProvider.tsx b/src/pages/operator/tsx/function_providers/ButtonFunctionProvider.tsx index d877c621..2148033a 100644 --- a/src/pages/operator/tsx/function_providers/ButtonFunctionProvider.tsx +++ b/src/pages/operator/tsx/function_providers/ButtonFunctionProvider.tsx @@ -1,64 +1,74 @@ -import { JOINT_VELOCITIES, JOINT_INCREMENTS, ValidJoints, ValidJointStateDict } from 'shared/util' -import { ActionMode } from '../utils/component_definitions' -import { FunctionProvider } from './FunctionProvider'; +import { + JOINT_VELOCITIES, + JOINT_INCREMENTS, + ValidJoints, + ValidJointStateDict, +} from "shared/util"; +import { ActionMode } from "../utils/component_definitions"; +import { FunctionProvider } from "./FunctionProvider"; -/** - * Each of the possible buttons which could be on a button pad. The string is +/** + * Each of the possible buttons which could be on a button pad. The string is * the label of the button which appears in the tooltip. */ export enum ButtonPadButton { - BaseForward = "Base Forward", - BaseReverse = "Base Reverse", - BaseRotateRight = "Base rotate right", - BaseRotateLeft = "Base rotate left", - ArmLift = "Arm lift", - ArmLower = "Arm lower", - ArmExtend = "Arm extend", - ArmRetract = "Arm retract", - GripperOpen = "Gripper open", - GripperClose = "Gripper close", - WristRotateIn = "Wrist rotate in", - WristRotateOut = "Wrist rotate out", - WristPitchUp = "Wrist pitch up", - WristPitchDown = "Wrist pitch down", - WristRollLeft = "Wrist roll left", - WristRollRight = "Wrist roll right", - CameraTiltUp = "Camera tilt up", - CameraTiltDown = "Camera tilt down", - CameraPanLeft = "Camera pan left", - CameraPanRight = "Camera pan right" + BaseForward = "Base Forward", + BaseReverse = "Base Reverse", + BaseRotateRight = "Base rotate right", + BaseRotateLeft = "Base rotate left", + ArmLift = "Arm lift", + ArmLower = "Arm lower", + ArmExtend = "Arm extend", + ArmRetract = "Arm retract", + GripperOpen = "Gripper open", + GripperClose = "Gripper close", + WristRotateIn = "Wrist rotate in", + WristRotateOut = "Wrist rotate out", + WristPitchUp = "Wrist pitch up", + WristPitchDown = "Wrist pitch down", + WristRollLeft = "Wrist roll left", + WristRollRight = "Wrist roll right", + CameraTiltUp = "Camera tilt up", + CameraTiltDown = "Camera tilt down", + CameraPanLeft = "Camera pan left", + CameraPanRight = "Camera pan right", } /** Array of the pan tilt buttons */ -export const panTiltButtons: ButtonPadButton[] = [ButtonPadButton.CameraTiltUp, ButtonPadButton.CameraTiltDown, ButtonPadButton.CameraPanLeft, ButtonPadButton.CameraPanRight]; +export const panTiltButtons: ButtonPadButton[] = [ + ButtonPadButton.CameraTiltUp, + ButtonPadButton.CameraTiltDown, + ButtonPadButton.CameraPanLeft, + ButtonPadButton.CameraPanRight, +]; /** Button functions which require moving a joint in the negative direction. */ const negativeButtonPadFunctions = new Set([ - ButtonPadButton.BaseReverse, - ButtonPadButton.BaseRotateRight, - ButtonPadButton.ArmLower, - ButtonPadButton.ArmRetract, - ButtonPadButton.GripperClose, - ButtonPadButton.WristRotateOut, - ButtonPadButton.WristPitchDown, - ButtonPadButton.WristRollLeft, - ButtonPadButton.CameraTiltDown, - ButtonPadButton.CameraPanRight -]) + ButtonPadButton.BaseReverse, + ButtonPadButton.BaseRotateRight, + ButtonPadButton.ArmLower, + ButtonPadButton.ArmRetract, + ButtonPadButton.GripperClose, + ButtonPadButton.WristRotateOut, + ButtonPadButton.WristPitchDown, + ButtonPadButton.WristRollLeft, + ButtonPadButton.CameraTiltDown, + ButtonPadButton.CameraPanRight, +]); /** Functions called when the user interacts with a button. */ export type ButtonFunctions = { - onClick: () => void, - onRelease?: () => void, - onLeave?: () => void -} + onClick: () => void; + onRelease?: () => void; + onLeave?: () => void; +}; /** State for a single button on a button pad. */ export enum ButtonState { - Inactive = "inactive", - Active = "active", - Collision = "collision", - Limit = "limit" + Inactive = "inactive", + Active = "active", + Collision = "collision", + Limit = "limit", } /** Mapping from each type of button pad button to the state for that button */ @@ -68,319 +78,376 @@ export type ButtonStateMap = Map; * Provides functions for the button pads */ export class ButtonFunctionProvider extends FunctionProvider { - private buttonStateMap: ButtonStateMap = new Map(); - - /** - * Callback function to update the button state map in the operator so it - * can rerender the button pads. - */ - private operatorCallback?: (buttonStateMap: ButtonStateMap) => void = undefined; - - constructor() { - super() - this.provideFunctions = this.provideFunctions.bind(this); - this.updateJointStates = this.updateJointStates.bind(this); - this.setButtonActiveState = this.setButtonActiveState.bind(this); - this.setButtonInactiveState = this.setButtonInactiveState.bind(this); - } - - /** - * Takes joint states and updates the button state map based on which joints - * are in collision or at their limit. - * - * @param inJointLimit dictionary of joints whose limit booleans have changed - * @param inCollision dictionary of joints whose collision booleans have changed - */ - public updateJointStates(inJointLimit: ValidJointStateDict, inCollision: ValidJointStateDict) { - Object.keys(inCollision).forEach((k: string) => { - const key = k as ValidJoints; - const [inCollisionNeg, inCollisionPos] = inCollision[key]!; - const buttons = getButtonsFromJointName(key); - if (!buttons) return; - let [buttonNeg, buttonPos] = key !== "joint_wrist_yaw" && key !== "joint_wrist_pitch" ? buttons : buttons.reverse() - - // TODO: i think there's still something wrong with this logic - const prevButtonStateNeg = this.buttonStateMap.get(buttonNeg) - const prevButtonStatePos = this.buttonStateMap.get(buttonPos) - const prevInCollisionNeg = prevButtonStateNeg === ButtonState.Collision - const prevInCollisionPos = prevButtonStatePos === ButtonState.Collision; - if (!prevButtonStateNeg || inCollisionNeg !== prevInCollisionNeg) this.buttonStateMap.set(buttonNeg, inCollisionNeg ? ButtonState.Collision : ButtonState.Inactive); - if (!prevButtonStatePos || inCollisionPos !== prevInCollisionPos) this.buttonStateMap.set(buttonPos, inCollisionPos ? ButtonState.Collision : ButtonState.Inactive); - }); - - Object.keys(inJointLimit).forEach((k: string) => { - const key = k as ValidJoints; - const [inLimitNeg, inLimitPos] = inJointLimit[key]!; - const buttons = getButtonsFromJointName(key); - if (!buttons) return; - const [buttonNeg, buttonPos] = buttons - const prevButtonStateNeg = this.buttonStateMap.get(buttonNeg) - const prevButtonStatePos = this.buttonStateMap.get(buttonPos) - const prevInLimitNeg = prevButtonStateNeg !== ButtonState.Limit - const prevInLimitPos = prevButtonStatePos !== ButtonState.Limit; - if (prevButtonStateNeg == undefined || inLimitNeg !== prevInLimitNeg) this.buttonStateMap.set(buttonNeg, inLimitNeg ? ButtonState.Inactive : ButtonState.Limit); - if (prevButtonStatePos == undefined || inLimitPos !== prevInLimitPos) this.buttonStateMap.set(buttonPos, inLimitPos ? ButtonState.Inactive : ButtonState.Limit); - }); - - if (this.operatorCallback) this.operatorCallback(this.buttonStateMap); - } - - /** - * Sets the local pointer to the operator's callback function, to be called - * whenever the button state map updates. - * - * @param callback operator's callback function to update the button state map - */ - public setOperatorCallback(callback: (buttonStateMap: ButtonStateMap) => void) { - this.operatorCallback = callback; - } - - /** - * Sets a type of a button pad button to active. - * - * @param buttonType the button pad button to set active - */ - private setButtonActiveState(buttonType: ButtonPadButton) { - const currentState = this.buttonStateMap.get(buttonType); - - // Don't set to active if in collision or at it's limit - if (currentState === ButtonState.Collision || currentState === ButtonState.Limit) - return; - - this.buttonStateMap.set(buttonType, ButtonState.Active); - if (this.operatorCallback) this.operatorCallback(this.buttonStateMap); - } - - /** - * Sets a type of a button pad button to inactive. - * - * @param buttonType the button pad button to set active - */ - private setButtonInactiveState(buttonType: ButtonPadButton) { - const currentState = this.buttonStateMap.get(buttonType); - - // Don't set to inactive if in collision or at it's limit - if (currentState === ButtonState.Collision || currentState === ButtonState.Limit || currentState === ButtonState.Inactive) - return; - - this.buttonStateMap.set(buttonType, ButtonState.Inactive); - if (this.operatorCallback) this.operatorCallback(this.buttonStateMap); - } - - /** - * Takes a ButtonPadFunction which indicates the type of button (e.g. drive - * base forward, lift arm), and returns a set of functions to execute when - * the user interacts with the button. - * - * @param buttonPadFunction the {@link ButtonPadButton} - * @returns the {@link ButtonFunctions} for the button - */ - public provideFunctions(buttonPadFunction: ButtonPadButton): ButtonFunctions { - let action: () => void; - const onLeave = () => { - this.stopCurrentAction(); - this.setButtonInactiveState(buttonPadFunction) + private buttonStateMap: ButtonStateMap = new Map< + ButtonPadButton, + ButtonState + >(); + + /** + * Callback function to update the button state map in the operator so it + * can rerender the button pads. + */ + private operatorCallback?: (buttonStateMap: ButtonStateMap) => void = + undefined; + + constructor() { + super(); + this.provideFunctions = this.provideFunctions.bind(this); + this.updateJointStates = this.updateJointStates.bind(this); + this.setButtonActiveState = this.setButtonActiveState.bind(this); + this.setButtonInactiveState = this.setButtonInactiveState.bind(this); + } + + /** + * Takes joint states and updates the button state map based on which joints + * are in collision or at their limit. + * + * @param inJointLimit dictionary of joints whose limit booleans have changed + * @param inCollision dictionary of joints whose collision booleans have changed + */ + public updateJointStates( + inJointLimit: ValidJointStateDict, + inCollision: ValidJointStateDict, + ) { + Object.keys(inCollision).forEach((k: string) => { + const key = k as ValidJoints; + const [inCollisionNeg, inCollisionPos] = inCollision[key]!; + const buttons = getButtonsFromJointName(key); + if (!buttons) return; + let [buttonNeg, buttonPos] = + key !== "joint_wrist_yaw" && key !== "joint_wrist_pitch" + ? buttons + : buttons.reverse(); + + // TODO: i think there's still something wrong with this logic + const prevButtonStateNeg = this.buttonStateMap.get(buttonNeg); + const prevButtonStatePos = this.buttonStateMap.get(buttonPos); + const prevInCollisionNeg = prevButtonStateNeg === ButtonState.Collision; + const prevInCollisionPos = prevButtonStatePos === ButtonState.Collision; + if (!prevButtonStateNeg || inCollisionNeg !== prevInCollisionNeg) + this.buttonStateMap.set( + buttonNeg, + inCollisionNeg ? ButtonState.Collision : ButtonState.Inactive, + ); + if (!prevButtonStatePos || inCollisionPos !== prevInCollisionPos) + this.buttonStateMap.set( + buttonPos, + inCollisionPos ? ButtonState.Collision : ButtonState.Inactive, + ); + }); + + Object.keys(inJointLimit).forEach((k: string) => { + const key = k as ValidJoints; + const [inLimitNeg, inLimitPos] = inJointLimit[key]!; + const buttons = getButtonsFromJointName(key); + if (!buttons) return; + const [buttonNeg, buttonPos] = buttons; + const prevButtonStateNeg = this.buttonStateMap.get(buttonNeg); + const prevButtonStatePos = this.buttonStateMap.get(buttonPos); + const prevInLimitNeg = prevButtonStateNeg !== ButtonState.Limit; + const prevInLimitPos = prevButtonStatePos !== ButtonState.Limit; + if (prevButtonStateNeg == undefined || inLimitNeg !== prevInLimitNeg) + this.buttonStateMap.set( + buttonNeg, + inLimitNeg ? ButtonState.Inactive : ButtonState.Limit, + ); + if (prevButtonStatePos == undefined || inLimitPos !== prevInLimitPos) + this.buttonStateMap.set( + buttonPos, + inLimitPos ? ButtonState.Inactive : ButtonState.Limit, + ); + }); + + if (this.operatorCallback) this.operatorCallback(this.buttonStateMap); + } + + /** + * Sets the local pointer to the operator's callback function, to be called + * whenever the button state map updates. + * + * @param callback operator's callback function to update the button state map + */ + public setOperatorCallback( + callback: (buttonStateMap: ButtonStateMap) => void, + ) { + this.operatorCallback = callback; + } + + /** + * Sets a type of a button pad button to active. + * + * @param buttonType the button pad button to set active + */ + private setButtonActiveState(buttonType: ButtonPadButton) { + const currentState = this.buttonStateMap.get(buttonType); + + // Don't set to active if in collision or at it's limit + if ( + currentState === ButtonState.Collision || + currentState === ButtonState.Limit + ) + return; + + this.buttonStateMap.set(buttonType, ButtonState.Active); + if (this.operatorCallback) this.operatorCallback(this.buttonStateMap); + } + + /** + * Sets a type of a button pad button to inactive. + * + * @param buttonType the button pad button to set active + */ + private setButtonInactiveState(buttonType: ButtonPadButton) { + const currentState = this.buttonStateMap.get(buttonType); + + // Don't set to inactive if in collision or at it's limit + if ( + currentState === ButtonState.Collision || + currentState === ButtonState.Limit || + currentState === ButtonState.Inactive + ) + return; + + this.buttonStateMap.set(buttonType, ButtonState.Inactive); + if (this.operatorCallback) this.operatorCallback(this.buttonStateMap); + } + + /** + * Takes a ButtonPadFunction which indicates the type of button (e.g. drive + * base forward, lift arm), and returns a set of functions to execute when + * the user interacts with the button. + * + * @param buttonPadFunction the {@link ButtonPadButton} + * @returns the {@link ButtonFunctions} for the button + */ + public provideFunctions(buttonPadFunction: ButtonPadButton): ButtonFunctions { + let action: () => void; + const onLeave = () => { + this.stopCurrentAction(); + this.setButtonInactiveState(buttonPadFunction); + }; + + const jointName: ValidJoints = + getJointNameFromButtonFunction(buttonPadFunction); + const multiplier: number = negativeButtonPadFunctions.has(buttonPadFunction) + ? -1 + : 1; + const velocity = + multiplier * + JOINT_VELOCITIES[jointName]! * + FunctionProvider.velocityScale; + const increment = + multiplier * + JOINT_INCREMENTS[jointName]! * + FunctionProvider.velocityScale; + + switch (FunctionProvider.actionMode) { + case ActionMode.StepActions: + switch (buttonPadFunction) { + case ButtonPadButton.BaseForward: + case ButtonPadButton.BaseReverse: + action = () => this.incrementalBaseDrive(velocity, 0.0); + break; + case ButtonPadButton.BaseRotateLeft: + case ButtonPadButton.BaseRotateRight: + action = () => this.incrementalBaseDrive(0.0, velocity); + break; + case ButtonPadButton.ArmLower: + case ButtonPadButton.ArmLift: + case ButtonPadButton.ArmExtend: + case ButtonPadButton.ArmRetract: + case ButtonPadButton.WristRotateIn: + case ButtonPadButton.WristRotateOut: + case ButtonPadButton.WristPitchUp: + case ButtonPadButton.WristPitchDown: + case ButtonPadButton.WristRollLeft: + case ButtonPadButton.WristRollRight: + case ButtonPadButton.GripperOpen: + case ButtonPadButton.GripperClose: + action = () => this.incrementalJointMovement(jointName, increment); + break; + case ButtonPadButton.CameraTiltUp: + case ButtonPadButton.CameraTiltDown: + case ButtonPadButton.CameraPanLeft: + case ButtonPadButton.CameraPanRight: + action = () => { + this.incrementalJointMovement(jointName, increment); + FunctionProvider.remoteRobot?.setToggle( + "setFollowGripper", + false, + ); + }; + break; + } + return { + onClick: () => { + action(); + this.setButtonActiveState(buttonPadFunction); + // Set button state inactive after 1 second + setTimeout( + () => this.setButtonInactiveState(buttonPadFunction), + 1000, + ); + }, + onLeave: onLeave, }; + case ActionMode.PressAndHold: + case ActionMode.ClickClick: + switch (buttonPadFunction) { + case ButtonPadButton.BaseForward: + case ButtonPadButton.BaseReverse: + action = () => this.continuousBaseDrive(velocity, 0.0); + break; + case ButtonPadButton.BaseRotateLeft: + case ButtonPadButton.BaseRotateRight: + action = () => this.continuousBaseDrive(0.0, velocity); + break; + + case ButtonPadButton.ArmLower: + case ButtonPadButton.ArmLift: + case ButtonPadButton.ArmExtend: + case ButtonPadButton.ArmRetract: + case ButtonPadButton.WristRotateIn: + case ButtonPadButton.WristRotateOut: + case ButtonPadButton.WristPitchUp: + case ButtonPadButton.WristPitchDown: + case ButtonPadButton.WristRollLeft: + case ButtonPadButton.WristRollRight: + case ButtonPadButton.GripperOpen: + case ButtonPadButton.GripperClose: + action = () => this.continuousJointMovement(jointName, increment); + break; + case ButtonPadButton.CameraTiltUp: + case ButtonPadButton.CameraTiltDown: + case ButtonPadButton.CameraPanLeft: + case ButtonPadButton.CameraPanRight: + action = () => { + this.continuousJointMovement(jointName, increment); + FunctionProvider.remoteRobot?.setToggle( + "setFollowGripper", + false, + ); + }; + break; + } - - const jointName: ValidJoints = getJointNameFromButtonFunction(buttonPadFunction); - const multiplier: number = negativeButtonPadFunctions.has(buttonPadFunction) ? -1 : 1; - const velocity = multiplier * JOINT_VELOCITIES[jointName]! * FunctionProvider.velocityScale; - const increment = multiplier * JOINT_INCREMENTS[jointName]! * FunctionProvider.velocityScale; - - switch (FunctionProvider.actionMode) { - case ActionMode.StepActions: - switch (buttonPadFunction) { - case ButtonPadButton.BaseForward: - case ButtonPadButton.BaseReverse: - action = () => this.incrementalBaseDrive(velocity, 0.0); - break; - case ButtonPadButton.BaseRotateLeft: - case ButtonPadButton.BaseRotateRight: - action = () => this.incrementalBaseDrive(0.0, velocity); - break; - case ButtonPadButton.ArmLower: - case ButtonPadButton.ArmLift: - case ButtonPadButton.ArmExtend: - case ButtonPadButton.ArmRetract: - case ButtonPadButton.WristRotateIn: - case ButtonPadButton.WristRotateOut: - case ButtonPadButton.WristPitchUp: - case ButtonPadButton.WristPitchDown: - case ButtonPadButton.WristRollLeft: - case ButtonPadButton.WristRollRight: - case ButtonPadButton.GripperOpen: - case ButtonPadButton.GripperClose: - action = () => this.incrementalJointMovement(jointName, increment); - break; - case (ButtonPadButton.CameraTiltUp): - case (ButtonPadButton.CameraTiltDown): - case (ButtonPadButton.CameraPanLeft): - case (ButtonPadButton.CameraPanRight): - action = () => { - this.incrementalJointMovement(jointName, increment); - FunctionProvider.remoteRobot?.setToggle("setFollowGripper", false); - } - break; - } - return { - onClick: () => { - action(); - this.setButtonActiveState(buttonPadFunction); - // Set button state inactive after 1 second - setTimeout(() => this.setButtonInactiveState(buttonPadFunction), 1000); - }, - onLeave: onLeave - }; - case ActionMode.PressAndHold: - case ActionMode.ClickClick: - switch (buttonPadFunction) { - case ButtonPadButton.BaseForward: - case ButtonPadButton.BaseReverse: - action = () => this.continuousBaseDrive(velocity, 0.0); - break; - case ButtonPadButton.BaseRotateLeft: - case ButtonPadButton.BaseRotateRight: - action = () => this.continuousBaseDrive(0.0, velocity); - break; - - case ButtonPadButton.ArmLower: - case ButtonPadButton.ArmLift: - case ButtonPadButton.ArmExtend: - case ButtonPadButton.ArmRetract: - case ButtonPadButton.WristRotateIn: - case ButtonPadButton.WristRotateOut: - case ButtonPadButton.WristPitchUp: - case ButtonPadButton.WristPitchDown: - case ButtonPadButton.WristRollLeft: - case ButtonPadButton.WristRollRight: - case ButtonPadButton.GripperOpen: - case ButtonPadButton.GripperClose: - action = () => this.continuousJointMovement(jointName, increment); - break; - case (ButtonPadButton.CameraTiltUp): - case (ButtonPadButton.CameraTiltDown): - case (ButtonPadButton.CameraPanLeft): - case (ButtonPadButton.CameraPanRight): - action = () => { - this.continuousJointMovement(jointName, increment); - FunctionProvider.remoteRobot?.setToggle("setFollowGripper", false); - } - break; - } - - return (FunctionProvider.actionMode === ActionMode.PressAndHold) ? { - onClick: () => { - action(); - this.setButtonActiveState(buttonPadFunction); - }, - // For press-release, stop when button released - onRelease: () => { - this.stopCurrentAction(); - this.setButtonInactiveState(buttonPadFunction) - }, - onLeave: onLeave - } : { - // For click-click, stop if button already active - onClick: () => { - if (this.activeVelocityAction) { - this.stopCurrentAction(); - this.setButtonInactiveState(buttonPadFunction); - } else { - action(); - this.setButtonActiveState(buttonPadFunction) - } - }, - onLeave: onLeave + return FunctionProvider.actionMode === ActionMode.PressAndHold + ? { + onClick: () => { + action(); + this.setButtonActiveState(buttonPadFunction); + }, + // For press-release, stop when button released + onRelease: () => { + this.stopCurrentAction(); + this.setButtonInactiveState(buttonPadFunction); + }, + onLeave: onLeave, + } + : { + // For click-click, stop if button already active + onClick: () => { + if (this.activeVelocityAction) { + this.stopCurrentAction(); + this.setButtonInactiveState(buttonPadFunction); + } else { + action(); + this.setButtonActiveState(buttonPadFunction); } - } + }, + onLeave: onLeave, + }; } + } } /** * Uses the name of a joint on the robot to get the two related button pad buttons. - * + * * @param jointName the name of the joint * @returns both of the corresponding button types (for moving the joint in the * negative or positive direction respectively) */ -function getButtonsFromJointName(jointName: ValidJoints): [ButtonPadButton, ButtonPadButton] | undefined { - switch (jointName) { - case ('joint_gripper_finger_left'): - return [ButtonPadButton.GripperClose, ButtonPadButton.GripperOpen] - case ('wrist_extension'): - return [ButtonPadButton.ArmRetract, ButtonPadButton.ArmExtend] - case ('joint_lift'): - return [ButtonPadButton.ArmLower, ButtonPadButton.ArmLift] - case ('joint_wrist_roll'): - return [ButtonPadButton.WristRollLeft, ButtonPadButton.WristRollRight] - case ('joint_wrist_pitch'): - return [ButtonPadButton.WristPitchDown, ButtonPadButton.WristPitchUp] - case ('joint_wrist_yaw'): - return [ButtonPadButton.WristRotateOut, ButtonPadButton.WristRotateIn] - case ("translate_mobile_base"): - return [ButtonPadButton.BaseForward, ButtonPadButton.BaseReverse]; - case ("rotate_mobile_base"): - return [ButtonPadButton.BaseRotateLeft, ButtonPadButton.BaseRotateRight]; - case ("joint_head_pan"): - return [ButtonPadButton.CameraPanRight, ButtonPadButton.CameraPanLeft]; - case ("joint_head_tilt"): - return [ButtonPadButton.CameraTiltDown, ButtonPadButton.CameraTiltUp]; - default: - return undefined; - } +function getButtonsFromJointName( + jointName: ValidJoints, +): [ButtonPadButton, ButtonPadButton] | undefined { + switch (jointName) { + case "joint_gripper_finger_left": + return [ButtonPadButton.GripperClose, ButtonPadButton.GripperOpen]; + case "wrist_extension": + return [ButtonPadButton.ArmRetract, ButtonPadButton.ArmExtend]; + case "joint_lift": + return [ButtonPadButton.ArmLower, ButtonPadButton.ArmLift]; + case "joint_wrist_roll": + return [ButtonPadButton.WristRollLeft, ButtonPadButton.WristRollRight]; + case "joint_wrist_pitch": + return [ButtonPadButton.WristPitchDown, ButtonPadButton.WristPitchUp]; + case "joint_wrist_yaw": + return [ButtonPadButton.WristRotateOut, ButtonPadButton.WristRotateIn]; + case "translate_mobile_base": + return [ButtonPadButton.BaseForward, ButtonPadButton.BaseReverse]; + case "rotate_mobile_base": + return [ButtonPadButton.BaseRotateLeft, ButtonPadButton.BaseRotateRight]; + case "joint_head_pan": + return [ButtonPadButton.CameraPanRight, ButtonPadButton.CameraPanLeft]; + case "joint_head_tilt": + return [ButtonPadButton.CameraTiltDown, ButtonPadButton.CameraTiltUp]; + default: + return undefined; + } } /** * Uses the type of a button pad button to get the corresponding joint name. - * + * * @param buttonType the type of button in a button pad * @returns the name of the corresponding joint */ -function getJointNameFromButtonFunction(buttonType: ButtonPadButton): ValidJoints { - switch (buttonType) { - - case (ButtonPadButton.BaseReverse): - case (ButtonPadButton.BaseForward): - return "translate_mobile_base"; - - case (ButtonPadButton.BaseRotateLeft): - case (ButtonPadButton.BaseRotateRight): - return "rotate_mobile_base"; - - case (ButtonPadButton.ArmLower): - case (ButtonPadButton.ArmLift): - return "joint_lift"; - - case (ButtonPadButton.ArmRetract): - case (ButtonPadButton.ArmExtend): - return "wrist_extension"; - - case (ButtonPadButton.GripperClose): - case (ButtonPadButton.GripperOpen): - return "joint_gripper_finger_left"; - - case ButtonPadButton.WristRollLeft: - case ButtonPadButton.WristRollRight: - return "joint_wrist_roll" - - case ButtonPadButton.WristPitchUp: - case ButtonPadButton.WristPitchDown: - return "joint_wrist_pitch" - - case (ButtonPadButton.WristRotateIn): - case (ButtonPadButton.WristRotateOut): - return "joint_wrist_yaw"; - - case (ButtonPadButton.CameraTiltUp): - case (ButtonPadButton.CameraTiltDown): - return "joint_head_tilt"; - - case (ButtonPadButton.CameraPanLeft): - case (ButtonPadButton.CameraPanRight): - return "joint_head_pan"; - - default: - throw Error('unknow button pad function' + buttonType); - } +function getJointNameFromButtonFunction( + buttonType: ButtonPadButton, +): ValidJoints { + switch (buttonType) { + case ButtonPadButton.BaseReverse: + case ButtonPadButton.BaseForward: + return "translate_mobile_base"; + + case ButtonPadButton.BaseRotateLeft: + case ButtonPadButton.BaseRotateRight: + return "rotate_mobile_base"; + + case ButtonPadButton.ArmLower: + case ButtonPadButton.ArmLift: + return "joint_lift"; + + case ButtonPadButton.ArmRetract: + case ButtonPadButton.ArmExtend: + return "wrist_extension"; + + case ButtonPadButton.GripperClose: + case ButtonPadButton.GripperOpen: + return "joint_gripper_finger_left"; + + case ButtonPadButton.WristRollLeft: + case ButtonPadButton.WristRollRight: + return "joint_wrist_roll"; + + case ButtonPadButton.WristPitchUp: + case ButtonPadButton.WristPitchDown: + return "joint_wrist_pitch"; + + case ButtonPadButton.WristRotateIn: + case ButtonPadButton.WristRotateOut: + return "joint_wrist_yaw"; + + case ButtonPadButton.CameraTiltUp: + case ButtonPadButton.CameraTiltDown: + return "joint_head_tilt"; + + case ButtonPadButton.CameraPanLeft: + case ButtonPadButton.CameraPanRight: + return "joint_head_pan"; + + default: + throw Error("unknown button pad function" + buttonType); + } } diff --git a/src/pages/operator/tsx/function_providers/FunctionProvider.tsx b/src/pages/operator/tsx/function_providers/FunctionProvider.tsx index b8556e7f..bc64ada9 100644 --- a/src/pages/operator/tsx/function_providers/FunctionProvider.tsx +++ b/src/pages/operator/tsx/function_providers/FunctionProvider.tsx @@ -1,79 +1,95 @@ -import { RemoteRobot } from "shared/remoterobot" -import { VelocityCommand } from 'shared/commands' +import { RemoteRobot } from "shared/remoterobot"; +import { VelocityCommand } from "shared/commands"; import { ValidJoints } from "shared/util"; import { ActionMode } from "../utils/component_definitions"; /** - * Provides logic to connect the {@link RemoteRobot} and the components in the + * Provides logic to connect the {@link RemoteRobot} and the components in the * interface */ export abstract class FunctionProvider { - protected static remoteRobot?: RemoteRobot; - public static velocityScale: number; - public static actionMode: ActionMode; - public activeVelocityAction?: VelocityCommand; - public velocityExecutionHeartbeat?: number // ReturnType + protected static remoteRobot?: RemoteRobot; + public static velocityScale: number; + public static actionMode: ActionMode; + public activeVelocityAction?: VelocityCommand; + public velocityExecutionHeartbeat?: number; // ReturnType - /** - * Adds a remote robot instance to this function provider. This must be called - * before any components of the interface will be able to execute functions - * to change the state of the robot. - * - * @param remoteRobot the remote robot instance to add - */ - static addRemoteRobot(remoteRobot: RemoteRobot) { - FunctionProvider.remoteRobot = remoteRobot; - } + /** + * Adds a remote robot instance to this function provider. This must be called + * before any components of the interface will be able to execute functions + * to change the state of the robot. + * + * @param remoteRobot the remote robot instance to add + */ + static addRemoteRobot(remoteRobot: RemoteRobot) { + FunctionProvider.remoteRobot = remoteRobot; + } - /** - * Sets the initial values for the velocity scale and action mode - * - * @param velocityScale initial velocity scale - * @param actionMode initial action mode - */ - static initialize(velocityScale: number, actionMode: ActionMode) { - this.velocityScale = velocityScale; - this.actionMode = actionMode; - } + /** + * Sets the initial values for the velocity scale and action mode + * + * @param velocityScale initial velocity scale + * @param actionMode initial action mode + */ + static initialize(velocityScale: number, actionMode: ActionMode) { + this.velocityScale = velocityScale; + this.actionMode = actionMode; + } - public incrementalBaseDrive(linVel: number, angVel: number) { - this.stopCurrentAction() - this.activeVelocityAction = FunctionProvider.remoteRobot?.driveBase(linVel, angVel) - } + public incrementalBaseDrive(linVel: number, angVel: number) { + this.stopCurrentAction(); + this.activeVelocityAction = FunctionProvider.remoteRobot?.driveBase( + linVel, + angVel, + ); + } - public incrementalJointMovement(jointName: ValidJoints, increment: number) { - this.stopCurrentAction() - this.activeVelocityAction = FunctionProvider.remoteRobot?.incrementalMove(jointName, increment) - } + public incrementalJointMovement(jointName: ValidJoints, increment: number) { + this.stopCurrentAction(); + this.activeVelocityAction = FunctionProvider.remoteRobot?.incrementalMove( + jointName, + increment, + ); + } - public continuousBaseDrive(linVel: number, angVel: number) { - this.stopCurrentAction() - this.activeVelocityAction = FunctionProvider.remoteRobot?.driveBase(linVel, angVel) - this.velocityExecutionHeartbeat = window.setInterval(() => { - this.activeVelocityAction = - FunctionProvider.remoteRobot?.driveBase(linVel, angVel) - }, 150); - } + public continuousBaseDrive(linVel: number, angVel: number) { + this.stopCurrentAction(); + this.activeVelocityAction = FunctionProvider.remoteRobot?.driveBase( + linVel, + angVel, + ); + this.velocityExecutionHeartbeat = window.setInterval(() => { + this.activeVelocityAction = FunctionProvider.remoteRobot?.driveBase( + linVel, + angVel, + ); + }, 150); + } - public continuousJointMovement(jointName: ValidJoints, increment: number) { - this.stopCurrentAction() - this.activeVelocityAction = FunctionProvider.remoteRobot?.incrementalMove(jointName, increment) - this.velocityExecutionHeartbeat = window.setInterval(() => { - this.activeVelocityAction = - FunctionProvider.remoteRobot?.incrementalMove(jointName, increment) - }, 150); - } + public continuousJointMovement(jointName: ValidJoints, increment: number) { + this.stopCurrentAction(); + this.activeVelocityAction = FunctionProvider.remoteRobot?.incrementalMove( + jointName, + increment, + ); + this.velocityExecutionHeartbeat = window.setInterval(() => { + this.activeVelocityAction = FunctionProvider.remoteRobot?.incrementalMove( + jointName, + increment, + ); + }, 150); + } - public stopCurrentAction() { - FunctionProvider.remoteRobot?.stopTrajectory() - if (this.activeVelocityAction) { - // No matter what region this is, stop the currently running action - this.activeVelocityAction.stop() - this.activeVelocityAction = undefined - } - if (this.velocityExecutionHeartbeat) { - clearInterval(this.velocityExecutionHeartbeat) - this.velocityExecutionHeartbeat = undefined - } + public stopCurrentAction() { + FunctionProvider.remoteRobot?.stopTrajectory(); + if (this.activeVelocityAction) { + // No matter what region this is, stop the currently running action + this.activeVelocityAction.stop(); + this.activeVelocityAction = undefined; + } + if (this.velocityExecutionHeartbeat) { + clearInterval(this.velocityExecutionHeartbeat); + this.velocityExecutionHeartbeat = undefined; } + } } diff --git a/src/pages/operator/tsx/function_providers/MapFunctionProvider.tsx b/src/pages/operator/tsx/function_providers/MapFunctionProvider.tsx index 953e2489..d73ac148 100644 --- a/src/pages/operator/tsx/function_providers/MapFunctionProvider.tsx +++ b/src/pages/operator/tsx/function_providers/MapFunctionProvider.tsx @@ -1,36 +1,37 @@ -import { ROSPose } from "shared/util" -import { MapFunction } from "../layout_components/Map" -import { FunctionProvider } from "./FunctionProvider" -import { occupancyGrid } from "operator/tsx/index" +import { ROSPose } from "shared/util"; +import { MapFunction } from "../layout_components/Map"; +import { FunctionProvider } from "./FunctionProvider"; +import { occupancyGrid } from "operator/tsx/index"; export class MapFunctionProvider extends FunctionProvider { - constructor() { - super() - this.provideFunctions = this.provideFunctions.bind(this) - FunctionProvider.remoteRobot?.getOccupancyGrid("getOccupancyGrid") - } + constructor() { + super(); + this.provideFunctions = this.provideFunctions.bind(this); + FunctionProvider.remoteRobot?.getOccupancyGrid("getOccupancyGrid"); + } - public provideFunctions(mapFunction: MapFunction) { - switch (mapFunction) { - case MapFunction.GetMap: - return occupancyGrid - case MapFunction.GetPose: - return () => { return FunctionProvider.remoteRobot?.getMapPose() } - case MapFunction.MoveBase: - return (pose: ROSPose) => { - // FunctionProvider.remoteRobot?.stopExecution() - FunctionProvider.remoteRobot?.moveBase(pose) - } - case MapFunction.GoalReached: - return () => { - let goalReached = FunctionProvider.remoteRobot?.isGoalReached() - if (goalReached) { - FunctionProvider.remoteRobot?.setGoalReached(false) - return true - } - return false - } - } - + public provideFunctions(mapFunction: MapFunction) { + switch (mapFunction) { + case MapFunction.GetMap: + return occupancyGrid; + case MapFunction.GetPose: + return () => { + return FunctionProvider.remoteRobot?.getMapPose(); + }; + case MapFunction.MoveBase: + return (pose: ROSPose) => { + // FunctionProvider.remoteRobot?.stopExecution() + FunctionProvider.remoteRobot?.moveBase(pose); + }; + case MapFunction.GoalReached: + return () => { + let goalReached = FunctionProvider.remoteRobot?.isGoalReached(); + if (goalReached) { + FunctionProvider.remoteRobot?.setGoalReached(false); + return true; + } + return false; + }; } -} \ No newline at end of file + } +} diff --git a/src/pages/operator/tsx/function_providers/MovementRecorderFunctionProvider.tsx b/src/pages/operator/tsx/function_providers/MovementRecorderFunctionProvider.tsx index f1b3248f..e5a2dc8e 100644 --- a/src/pages/operator/tsx/function_providers/MovementRecorderFunctionProvider.tsx +++ b/src/pages/operator/tsx/function_providers/MovementRecorderFunctionProvider.tsx @@ -1,90 +1,107 @@ -import { FunctionProvider } from "./FunctionProvider" -import { MovementRecorderFunctions, MovementRecorderFunction } from "../layout_components/MovementRecorder" -import { RobotPose, ValidJoints } from "shared/util" -import { StorageHandler } from "../storage_handler/StorageHandler" +import { FunctionProvider } from "./FunctionProvider"; +import { + MovementRecorderFunctions, + MovementRecorderFunction, +} from "../layout_components/MovementRecorder"; +import { RobotPose, ValidJoints } from "shared/util"; +import { StorageHandler } from "../storage_handler/StorageHandler"; export class MovementRecorderFunctionProvider extends FunctionProvider { - private recordPosesHeartbeat?: number // ReturnType - private poses: RobotPose[] - private storageHandler: StorageHandler + private recordPosesHeartbeat?: number; // ReturnType + private poses: RobotPose[]; + private storageHandler: StorageHandler; - constructor(storageHandler: StorageHandler) { - super() - this.provideFunctions = this.provideFunctions.bind(this) - this.poses = [] - this.storageHandler = storageHandler - } + constructor(storageHandler: StorageHandler) { + super(); + this.provideFunctions = this.provideFunctions.bind(this); + this.poses = []; + this.storageHandler = storageHandler; + } - public provideFunctions(poseRecordFunction: MovementRecorderFunction) { - switch (poseRecordFunction) { - case MovementRecorderFunction.Record: - return () => { - let lastJoint: ValidJoints | undefined; - this.recordPosesHeartbeat = window.setInterval(() => { - const currentPose: RobotPose = FunctionProvider.remoteRobot!.sensors.getRobotPose( - true, true, true - ) - const lastPose = this.poses.length == 0 ? undefined : this.poses[this.poses.length - 1] - if (lastPose) { - Object.keys(currentPose).map((key, index) => { - if (Math.abs(currentPose[key as ValidJoints]! - lastPose[key as ValidJoints]!) > 0.025) { - if (!lastJoint || lastJoint != key) { - lastJoint = key as ValidJoints - this.poses.push(currentPose) - return; - } else { - this.poses[this.poses.length - 1][lastJoint] = currentPose[key as ValidJoints] - } - } - }) - } else { - this.poses.push(currentPose) - } - }, 50) - } - case MovementRecorderFunction.SaveRecording: - return (name: string) => { - if (this.recordPosesHeartbeat) { - clearInterval(this.recordPosesHeartbeat) - this.recordPosesHeartbeat = undefined - } - this.storageHandler.savePoseRecording(name, this.poses) - this.poses = [] - } - case MovementRecorderFunction.StopRecording: - return () => { - if (this.recordPosesHeartbeat) { - clearInterval(this.recordPosesHeartbeat) - this.recordPosesHeartbeat = undefined - } - this.poses = [] + public provideFunctions(poseRecordFunction: MovementRecorderFunction) { + switch (poseRecordFunction) { + case MovementRecorderFunction.Record: + return () => { + let lastJoint: ValidJoints | undefined; + this.recordPosesHeartbeat = window.setInterval(() => { + const currentPose: RobotPose = + FunctionProvider.remoteRobot!.sensors.getRobotPose( + true, + true, + true, + ); + const lastPose = + this.poses.length == 0 + ? undefined + : this.poses[this.poses.length - 1]; + if (lastPose) { + Object.keys(currentPose).map((key, index) => { + if ( + Math.abs( + currentPose[key as ValidJoints]! - + lastPose[key as ValidJoints]!, + ) > 0.025 + ) { + if (!lastJoint || lastJoint != key) { + lastJoint = key as ValidJoints; + this.poses.push(currentPose); + return; + } else { + this.poses[this.poses.length - 1][lastJoint] = + currentPose[key as ValidJoints]; + } } - case MovementRecorderFunction.SavedRecordingNames: - return () => { - return this.storageHandler.getRecordingNames() - } - case MovementRecorderFunction.DeleteRecording: - return (recordingID: number) => { - let recordingNames = this.storageHandler.getRecordingNames() - this.storageHandler.deleteRecording(recordingNames[recordingID]) - } - case MovementRecorderFunction.DeleteRecordingName: - return (name: string) => { - this.storageHandler.deleteRecording(name) - } - case MovementRecorderFunction.LoadRecording: - return (recordingID: number) => { - let recordingNames = this.storageHandler.getRecordingNames() - let recording = this.storageHandler.getRecording(recordingNames[recordingID]) - FunctionProvider.remoteRobot?.playbackPoses(recording) - } - case MovementRecorderFunction.LoadRecordingName: - return (name: string) => { - let recording = this.storageHandler.getRecording(name) - FunctionProvider.remoteRobot?.playbackPoses(recording) - } - case MovementRecorderFunction.Cancel: - return () => FunctionProvider.remoteRobot?.stopTrajectory() - } + }); + } else { + this.poses.push(currentPose); + } + }, 50); + }; + case MovementRecorderFunction.SaveRecording: + return (name: string) => { + if (this.recordPosesHeartbeat) { + clearInterval(this.recordPosesHeartbeat); + this.recordPosesHeartbeat = undefined; + } + this.storageHandler.savePoseRecording(name, this.poses); + this.poses = []; + }; + case MovementRecorderFunction.StopRecording: + return () => { + if (this.recordPosesHeartbeat) { + clearInterval(this.recordPosesHeartbeat); + this.recordPosesHeartbeat = undefined; + } + this.poses = []; + }; + case MovementRecorderFunction.SavedRecordingNames: + return () => { + return this.storageHandler.getRecordingNames(); + }; + case MovementRecorderFunction.DeleteRecording: + return (recordingID: number) => { + let recordingNames = this.storageHandler.getRecordingNames(); + this.storageHandler.deleteRecording(recordingNames[recordingID]); + }; + case MovementRecorderFunction.DeleteRecordingName: + return (name: string) => { + this.storageHandler.deleteRecording(name); + }; + case MovementRecorderFunction.LoadRecording: + return (recordingID: number) => { + let recordingNames = this.storageHandler.getRecordingNames(); + let recording = this.storageHandler.getRecording( + recordingNames[recordingID], + ); + FunctionProvider.remoteRobot?.playbackPoses(recording); + }; + case MovementRecorderFunction.LoadRecordingName: + return (name: string) => { + let recording = this.storageHandler.getRecording(name); + FunctionProvider.remoteRobot?.playbackPoses(recording); + }; + case MovementRecorderFunction.Cancel: + return () => FunctionProvider.remoteRobot?.stopTrajectory(); } -} \ No newline at end of file + } +} diff --git a/src/pages/operator/tsx/function_providers/PredictiveDisplayFunctionProvider.tsx b/src/pages/operator/tsx/function_providers/PredictiveDisplayFunctionProvider.tsx index 46216141..dfa6061f 100644 --- a/src/pages/operator/tsx/function_providers/PredictiveDisplayFunctionProvider.tsx +++ b/src/pages/operator/tsx/function_providers/PredictiveDisplayFunctionProvider.tsx @@ -1,68 +1,84 @@ -import { FunctionProvider } from "./FunctionProvider" -import { PredictiveDisplayFunctions } from "../layout_components/PredictiveDisplay" -import { JOINT_VELOCITIES, JOINT_INCREMENTS, ValidJoints } from 'shared/util' -import { ActionMode } from "../utils/component_definitions" +import { FunctionProvider } from "./FunctionProvider"; +import { PredictiveDisplayFunctions } from "../layout_components/PredictiveDisplay"; +import { JOINT_VELOCITIES, JOINT_INCREMENTS, ValidJoints } from "shared/util"; +import { ActionMode } from "../utils/component_definitions"; export class PredictiveDisplayFunctionProvider extends FunctionProvider { - constructor() { - super() - this.provideFunctions = this.provideFunctions.bind(this) - } + constructor() { + super(); + this.provideFunctions = this.provideFunctions.bind(this); + } - /** - * Returns a set of functions to execute when - * the user interacts with predictive display mode - * - * @returns the {@link PredictiveDisplayFunctions} for the action modes - */ - public provideFunctions(setActiveCallback: (active: boolean) => void): PredictiveDisplayFunctions { - const baseLinVel = JOINT_VELOCITIES["translate_mobile_base"]! * FunctionProvider.velocityScale; - const baseAngVel = JOINT_VELOCITIES["rotate_mobile_base"]! * FunctionProvider.velocityScale; - switch (FunctionProvider.actionMode) { - case ActionMode.StepActions: - return { - onClick: (length: number, angle: number) => { - this.incrementalBaseDrive(baseLinVel * length, baseAngVel * angle); - setActiveCallback(true); - setTimeout(() => setActiveCallback(false), 1000); - }, - onLeave: () => { this.stopCurrentAction(); setActiveCallback(false); } - } - case ActionMode.PressAndHold: - return { - onClick: (length: number, angle: number) => { - this.continuousBaseDrive(baseLinVel * length, baseAngVel * angle); - setActiveCallback(true); - }, - onMove: (length: number, angle: number) => - this.activeVelocityAction ? this.continuousBaseDrive( - baseLinVel * length, - baseAngVel * angle - ) : null, - onRelease: () => { this.stopCurrentAction(); setActiveCallback(false); }, - onLeave: () => { this.stopCurrentAction(); setActiveCallback(false); } - } - case ActionMode.ClickClick: - return { - onClick: (length: number, angle: number) => { - if (this.activeVelocityAction) { - this.stopCurrentAction(); - setActiveCallback(false); - } else { - this.continuousBaseDrive(baseLinVel * length, baseAngVel * angle); - setActiveCallback(true); - } - }, - onMove: (length: number, angle: number) => { - if (this.activeVelocityAction) { - this.continuousBaseDrive( - baseLinVel * length, - baseAngVel * angle - ); - } - }, - onLeave: () => { this.stopCurrentAction(); setActiveCallback(false); } - } - } + /** + * Returns a set of functions to execute when + * the user interacts with predictive display mode + * + * @returns the {@link PredictiveDisplayFunctions} for the action modes + */ + public provideFunctions( + setActiveCallback: (active: boolean) => void, + ): PredictiveDisplayFunctions { + const baseLinVel = + JOINT_VELOCITIES["translate_mobile_base"]! * + FunctionProvider.velocityScale; + const baseAngVel = + JOINT_VELOCITIES["rotate_mobile_base"]! * FunctionProvider.velocityScale; + switch (FunctionProvider.actionMode) { + case ActionMode.StepActions: + return { + onClick: (length: number, angle: number) => { + this.incrementalBaseDrive(baseLinVel * length, baseAngVel * angle); + setActiveCallback(true); + setTimeout(() => setActiveCallback(false), 1000); + }, + onLeave: () => { + this.stopCurrentAction(); + setActiveCallback(false); + }, + }; + case ActionMode.PressAndHold: + return { + onClick: (length: number, angle: number) => { + this.continuousBaseDrive(baseLinVel * length, baseAngVel * angle); + setActiveCallback(true); + }, + onMove: (length: number, angle: number) => + this.activeVelocityAction + ? this.continuousBaseDrive( + baseLinVel * length, + baseAngVel * angle, + ) + : null, + onRelease: () => { + this.stopCurrentAction(); + setActiveCallback(false); + }, + onLeave: () => { + this.stopCurrentAction(); + setActiveCallback(false); + }, + }; + case ActionMode.ClickClick: + return { + onClick: (length: number, angle: number) => { + if (this.activeVelocityAction) { + this.stopCurrentAction(); + setActiveCallback(false); + } else { + this.continuousBaseDrive(baseLinVel * length, baseAngVel * angle); + setActiveCallback(true); + } + }, + onMove: (length: number, angle: number) => { + if (this.activeVelocityAction) { + this.continuousBaseDrive(baseLinVel * length, baseAngVel * angle); + } + }, + onLeave: () => { + this.stopCurrentAction(); + setActiveCallback(false); + }, + }; } -} \ No newline at end of file + } +} diff --git a/src/pages/operator/tsx/function_providers/RunStopFunctionProvider.tsx b/src/pages/operator/tsx/function_providers/RunStopFunctionProvider.tsx index 37e76939..e89e34f5 100644 --- a/src/pages/operator/tsx/function_providers/RunStopFunctionProvider.tsx +++ b/src/pages/operator/tsx/function_providers/RunStopFunctionProvider.tsx @@ -1,40 +1,40 @@ -import { FunctionProvider } from "./FunctionProvider" +import { FunctionProvider } from "./FunctionProvider"; export type RunStopFunctions = { - onClick: () => void -} + onClick: () => void; +}; export class RunStopFunctionProvider extends FunctionProvider { - private enabled: boolean; - private runStopStateChangeCallback: (enabled: boolean) => void; + private enabled: boolean; + private runStopStateChangeCallback: (enabled: boolean) => void; - constructor() { - super() - this.provideFunctions = this.provideFunctions.bind(this) - this.updateRunStopState = this.updateRunStopState.bind(this) - } + constructor() { + super(); + this.provideFunctions = this.provideFunctions.bind(this); + this.updateRunStopState = this.updateRunStopState.bind(this); + } - /** - * Records a callback from the function provider. The callback is called - * whenever the runstop state changes. - * - * @param callback callback to function provider - */ - public setRunStopStateChangeCallback(callback: (enabled: boolean) => void) { - this.runStopStateChangeCallback = callback; - } + /** + * Records a callback from the function provider. The callback is called + * whenever the runstop state changes. + * + * @param callback callback to function provider + */ + public setRunStopStateChangeCallback(callback: (enabled: boolean) => void) { + this.runStopStateChangeCallback = callback; + } - public updateRunStopState(enabled: boolean): void { - this.enabled = enabled - if (this.runStopStateChangeCallback) this.runStopStateChangeCallback(this.enabled) - } + public updateRunStopState(enabled: boolean): void { + this.enabled = enabled; + if (this.runStopStateChangeCallback) + this.runStopStateChangeCallback(this.enabled); + } - public provideFunctions(): RunStopFunctions { - return { - onClick: () => { - FunctionProvider.remoteRobot?.setToggle("setRunStop", !this.enabled) - } - } - } - -} \ No newline at end of file + public provideFunctions(): RunStopFunctions { + return { + onClick: () => { + FunctionProvider.remoteRobot?.setToggle("setRunStop", !this.enabled); + }, + }; + } +} diff --git a/src/pages/operator/tsx/function_providers/UnderMapFunctionProvider.tsx b/src/pages/operator/tsx/function_providers/UnderMapFunctionProvider.tsx index aeede067..aa76823e 100644 --- a/src/pages/operator/tsx/function_providers/UnderMapFunctionProvider.tsx +++ b/src/pages/operator/tsx/function_providers/UnderMapFunctionProvider.tsx @@ -1,147 +1,163 @@ -import { MoveBaseState, ROSPose, waitUntil } from "shared/util" -import { StorageHandler } from "../storage_handler/StorageHandler" -import { FunctionProvider } from "./FunctionProvider" -import { resolve } from "path" +import { MoveBaseState, ROSPose, waitUntil } from "shared/util"; +import { StorageHandler } from "../storage_handler/StorageHandler"; +import { FunctionProvider } from "./FunctionProvider"; +import { resolve } from "path"; export enum UnderMapButton { - SelectGoal, - DeleteGoal, - CancelGoal, - SaveGoal, - LoadGoal, - GetPose, - GetSavedPoseNames, - GetSavedPoseTypes, - GetSavedPoses, - NavigateToAruco, - GoalReached + SelectGoal, + DeleteGoal, + CancelGoal, + SaveGoal, + LoadGoal, + GetPose, + GetSavedPoseNames, + GetSavedPoseTypes, + GetSavedPoses, + NavigateToAruco, + GoalReached, } export class UnderMapFunctionProvider extends FunctionProvider { - private selectGoal: boolean - private storageHandler: StorageHandler - private navigationSuccess?: boolean - /** - * Callback function to update the move base state in the operator - */ - private operatorCallback?: (state: MoveBaseState) => void = undefined; + private selectGoal: boolean; + private storageHandler: StorageHandler; + private navigationSuccess?: boolean; + /** + * Callback function to update the move base state in the operator + */ + private operatorCallback?: (state: MoveBaseState) => void = undefined; - constructor(storageHandler: StorageHandler) { - super() - this.provideFunctions = this.provideFunctions.bind(this) - this.selectGoal = false - this.storageHandler = storageHandler - } + constructor(storageHandler: StorageHandler) { + super(); + this.provideFunctions = this.provideFunctions.bind(this); + this.selectGoal = false; + this.storageHandler = storageHandler; + } - public setMoveBaseState(state: MoveBaseState) { - if (state.alert_type == "success") this.navigationSuccess = true - if (this.operatorCallback) this.operatorCallback(state) - } + public setMoveBaseState(state: MoveBaseState) { + if (state.alert_type == "success") this.navigationSuccess = true; + if (this.operatorCallback) this.operatorCallback(state); + } - public provideFunctions(button: UnderMapButton) { - switch (button) { - case UnderMapButton.SelectGoal: - return (toggle: boolean) => { - this.selectGoal = toggle - } - case UnderMapButton.CancelGoal: - return () => FunctionProvider.remoteRobot?.stopMoveBase() - case UnderMapButton.DeleteGoal: - return (idx: number) => { - let poses = this.storageHandler.getMapPoseNames() - this.storageHandler.deleteMapPose(poses[idx]) - } - case UnderMapButton.SaveGoal: - return (name: string) => { - let pose = FunctionProvider.remoteRobot?.getMapPose() - if (!pose) throw 'Cannot save undefined map pose!' - this.storageHandler.saveMapPose(name, pose, "MAP") - } - case UnderMapButton.LoadGoal: - return (idx: number) => { - this.navigationSuccess = undefined - let poses = this.storageHandler.getMapPoseNames() - let pose = this.storageHandler.getMapPose(poses[idx]) - let rosPose = { - position: { - x: pose.translation.x, - y: pose.translation.y, - z: 0 - }, - orientation: { - x: pose.rotation.x, - y: pose.rotation.y, - z: pose.rotation.z, - w: pose.rotation.w, - } - } as ROSPose - FunctionProvider.remoteRobot?.moveBase(rosPose) - return pose.translation - } - case UnderMapButton.NavigateToAruco: - return (idx: number) => { - let poseTypes = this.storageHandler.getMapPoseTypes() - if (poseTypes[idx] != "ARUCO") return - waitUntil(() => this.navigationSuccess != undefined, 120000).then(() => { - // If navigation failed don't try navigating to marker - if (!this.navigationSuccess) { - this.navigationSuccess = undefined - return - } + public provideFunctions(button: UnderMapButton) { + switch (button) { + case UnderMapButton.SelectGoal: + return (toggle: boolean) => { + this.selectGoal = toggle; + }; + case UnderMapButton.CancelGoal: + return () => FunctionProvider.remoteRobot?.stopMoveBase(); + case UnderMapButton.DeleteGoal: + return (idx: number) => { + let poses = this.storageHandler.getMapPoseNames(); + this.storageHandler.deleteMapPose(poses[idx]); + }; + case UnderMapButton.SaveGoal: + return (name: string) => { + let pose = FunctionProvider.remoteRobot?.getMapPose(); + if (!pose) throw "Cannot save undefined map pose!"; + this.storageHandler.saveMapPose(name, pose, "MAP"); + }; + case UnderMapButton.LoadGoal: + return (idx: number) => { + this.navigationSuccess = undefined; + let poses = this.storageHandler.getMapPoseNames(); + let pose = this.storageHandler.getMapPose(poses[idx]); + let rosPose = { + position: { + x: pose.translation.x, + y: pose.translation.y, + z: 0, + }, + orientation: { + x: pose.rotation.x, + y: pose.rotation.y, + z: pose.rotation.z, + w: pose.rotation.w, + }, + } as ROSPose; + FunctionProvider.remoteRobot?.moveBase(rosPose); + return pose.translation; + }; + case UnderMapButton.NavigateToAruco: + return (idx: number) => { + let poseTypes = this.storageHandler.getMapPoseTypes(); + if (poseTypes[idx] != "ARUCO") return; + waitUntil(() => this.navigationSuccess != undefined, 120000).then( + () => { + // If navigation failed don't try navigating to marker + if (!this.navigationSuccess) { + this.navigationSuccess = undefined; + return; + } - this.navigationSuccess = undefined - let poseNames = this.storageHandler.getMapPoseNames() - let name = poseNames[idx] - let markerNames = this.storageHandler.getArucoMarkerNames() - let markerIndex = markerNames.indexOf(name) - if (markerIndex == -1) { - this.setMoveBaseState({ state: "Cannot find Aruco Marker", alert_type: "error" }) - return - } - let markerIDs = this.storageHandler.getArucoMarkerIDs() - let markerID = markerIDs[markerIndex] - let marker_info = this.storageHandler.getArucoMarkerInfo() - let pose = marker_info.aruco_marker_info[markerID].pose - if (!pose) { - this.setMoveBaseState({ state: "Cannot find Aruco Marker", alert_type: "error" }) - return - } - FunctionProvider.remoteRobot?.navigateToAruco(name, pose) - }) - } - case UnderMapButton.GetPose: - return () => { return FunctionProvider.remoteRobot?.getMapPose() } - case UnderMapButton.GetSavedPoseNames: - return () => { return this.storageHandler.getMapPoseNames() } - case UnderMapButton.GetSavedPoseTypes: - return () => { return this.storageHandler.getMapPoseTypes() } - case UnderMapButton.GetSavedPoses: - return () => { return this.storageHandler.getMapPoses() } - case UnderMapButton.GoalReached: - return () => { - const promise = new Promise((resolve, reject) => { - let interval = setInterval(() => { - let goalReached = FunctionProvider.remoteRobot?.isGoalReached() - if (goalReached) { - clearInterval(interval) - resolve(true) - } - }); - }); - return promise; - } - default: - throw Error(`Cannot get function for unknown UnderMapButton ${button}`) - } + this.navigationSuccess = undefined; + let poseNames = this.storageHandler.getMapPoseNames(); + let name = poseNames[idx]; + let markerNames = this.storageHandler.getArucoMarkerNames(); + let markerIndex = markerNames.indexOf(name); + if (markerIndex == -1) { + this.setMoveBaseState({ + state: "Cannot find Aruco Marker", + alert_type: "error", + }); + return; + } + let markerIDs = this.storageHandler.getArucoMarkerIDs(); + let markerID = markerIDs[markerIndex]; + let marker_info = this.storageHandler.getArucoMarkerInfo(); + let pose = marker_info.aruco_marker_info[markerID].pose; + if (!pose) { + this.setMoveBaseState({ + state: "Cannot find Aruco Marker", + alert_type: "error", + }); + return; + } + FunctionProvider.remoteRobot?.navigateToAruco(name, pose); + }, + ); + }; + case UnderMapButton.GetPose: + return () => { + return FunctionProvider.remoteRobot?.getMapPose(); + }; + case UnderMapButton.GetSavedPoseNames: + return () => { + return this.storageHandler.getMapPoseNames(); + }; + case UnderMapButton.GetSavedPoseTypes: + return () => { + return this.storageHandler.getMapPoseTypes(); + }; + case UnderMapButton.GetSavedPoses: + return () => { + return this.storageHandler.getMapPoses(); + }; + case UnderMapButton.GoalReached: + return () => { + const promise = new Promise((resolve, reject) => { + let interval = setInterval(() => { + let goalReached = FunctionProvider.remoteRobot?.isGoalReached(); + if (goalReached) { + clearInterval(interval); + resolve(true); + } + }); + }); + return promise; + }; + default: + throw Error(`Cannot get function for unknown UnderMapButton ${button}`); } + } - /** - * Sets the local pointer to the operator's callback function, to be called - * whenever the move base state changes. - * - * @param callback operator's callback function to update aruco navigation state - */ - public setOperatorCallback(callback: (state: MoveBaseState) => void) { - this.operatorCallback = callback; - } -} \ No newline at end of file + /** + * Sets the local pointer to the operator's callback function, to be called + * whenever the move base state changes. + * + * @param callback operator's callback function to update aruco navigation state + */ + public setOperatorCallback(callback: (state: MoveBaseState) => void) { + this.operatorCallback = callback; + } +} diff --git a/src/pages/operator/tsx/function_providers/UnderVideoFunctionProvider.tsx b/src/pages/operator/tsx/function_providers/UnderVideoFunctionProvider.tsx index 428cfcfd..01451247 100644 --- a/src/pages/operator/tsx/function_providers/UnderVideoFunctionProvider.tsx +++ b/src/pages/operator/tsx/function_providers/UnderVideoFunctionProvider.tsx @@ -1,98 +1,122 @@ -import { FunctionProvider } from "./FunctionProvider" -import { CENTER_WRIST, Marker, REALSENSE_BASE_POSE, REALSENSE_FORWARD_POSE, REALSENSE_GRIPPER_POSE, STOW_WRIST } from "../../../../shared/util" +import { FunctionProvider } from "./FunctionProvider"; +import { + CENTER_WRIST, + Marker, + REALSENSE_BASE_POSE, + REALSENSE_FORWARD_POSE, + REALSENSE_GRIPPER_POSE, + STOW_WRIST, +} from "../../../../shared/util"; export enum UnderVideoButton { - DriveView = "Drive View", - GripperView = "Gripper View", - LookAtGripper = "Look At Gripper", - LookAtBase = "Look At Base", - LookAhead= "Look Ahead", - FollowGripper = "Follow Gripper", - DepthSensing = "Depth Sensing", - ToggleArucoMarkers = "Toggle Aruco Markers", - CenterWrist = "Center Wrist", - StowWrist = "Stow Wrist", + DriveView = "Drive View", + GripperView = "Gripper View", + LookAtGripper = "Look At Gripper", + LookAtBase = "Look At Base", + LookAhead = "Look Ahead", + FollowGripper = "Follow Gripper", + DepthSensing = "Depth Sensing", + ToggleArucoMarkers = "Toggle Aruco Markers", + CenterWrist = "Center Wrist", + StowWrist = "Stow Wrist", } /** Array of different perspectives for the overhead camera */ export const overheadButtons: UnderVideoButton[] = [ - UnderVideoButton.DriveView, - UnderVideoButton.GripperView -] + UnderVideoButton.DriveView, + UnderVideoButton.GripperView, +]; /** Type to specify the different overhead camera perspectives */ -export type OverheadButtons = typeof overheadButtons[number] +export type OverheadButtons = (typeof overheadButtons)[number]; /** Array of different perspectives for the realsense camera */ export const realsenseButtons: UnderVideoButton[] = [ - UnderVideoButton.LookAhead, - UnderVideoButton.LookAtBase, - UnderVideoButton.LookAtGripper, -] + UnderVideoButton.LookAhead, + UnderVideoButton.LookAtBase, + UnderVideoButton.LookAtGripper, +]; /** Array of different actions for the wrist */ export const wristButtons: UnderVideoButton[] = [ - UnderVideoButton.CenterWrist, - UnderVideoButton.StowWrist -] + UnderVideoButton.CenterWrist, + UnderVideoButton.StowWrist, +]; /** Type to specify the different realsense camera perspectives */ -export type RealsenseButtons = typeof realsenseButtons[number] +export type RealsenseButtons = (typeof realsenseButtons)[number]; export type UnderVideoButtonFunctions = { - onClick?: () => void - onCheck?: (toggle: boolean) => void - getMarkers?: () => string[] - send?: (name: string) => void -} + onClick?: () => void; + onCheck?: (toggle: boolean) => void; + getMarkers?: () => string[]; + send?: (name: string) => void; +}; export class UnderVideoFunctionProvider extends FunctionProvider { - constructor() { - super() - this.provideFunctions = this.provideFunctions.bind(this) - } + constructor() { + super(); + this.provideFunctions = this.provideFunctions.bind(this); + } - public provideFunctions(button: UnderVideoButton): UnderVideoButtonFunctions { - switch (button) { - case UnderVideoButton.DriveView: - return { - onClick: () => FunctionProvider.remoteRobot?.setCameraPerspective("overhead", "nav") - } - case UnderVideoButton.GripperView: - return { - onClick: () => FunctionProvider.remoteRobot?.setCameraPerspective("overhead", "manip") - } - case UnderVideoButton.LookAtBase: - return { - onClick: () => FunctionProvider.remoteRobot?.setRobotPose(REALSENSE_BASE_POSE) - } - case UnderVideoButton.LookAhead: - return { - onClick: () => FunctionProvider.remoteRobot?.setRobotPose(REALSENSE_FORWARD_POSE) - } - case UnderVideoButton.LookAtGripper: - return { - onClick: () => FunctionProvider.remoteRobot?.lookAtGripper("lookAtGripper") //setRobotPose(REALSENSE_GRIPPER_POSE) - } - case UnderVideoButton.FollowGripper: - return { - onCheck: (toggle: boolean) => FunctionProvider.remoteRobot?.setToggle("setFollowGripper", toggle) - } - case UnderVideoButton.DepthSensing: - return { - onCheck: (toggle: boolean) => FunctionProvider.remoteRobot?.setToggle("setDepthSensing", toggle) - } - case UnderVideoButton.ToggleArucoMarkers: - return { - onCheck: (toggle: boolean) => FunctionProvider.remoteRobot?.setToggle("setArucoMarkers", toggle) - } - case UnderVideoButton.CenterWrist: - return { - onClick: () => FunctionProvider.remoteRobot?.setRobotPose(CENTER_WRIST) - } - case UnderVideoButton.StowWrist: - return { - onClick: () => FunctionProvider.remoteRobot?.setRobotPose(STOW_WRIST) - } - default: - throw Error(`Cannot get function for unknown UnderVideoButton ${button}`) - } + public provideFunctions(button: UnderVideoButton): UnderVideoButtonFunctions { + switch (button) { + case UnderVideoButton.DriveView: + return { + onClick: () => + FunctionProvider.remoteRobot?.setCameraPerspective( + "overhead", + "nav", + ), + }; + case UnderVideoButton.GripperView: + return { + onClick: () => + FunctionProvider.remoteRobot?.setCameraPerspective( + "overhead", + "manip", + ), + }; + case UnderVideoButton.LookAtBase: + return { + onClick: () => + FunctionProvider.remoteRobot?.setRobotPose(REALSENSE_BASE_POSE), + }; + case UnderVideoButton.LookAhead: + return { + onClick: () => + FunctionProvider.remoteRobot?.setRobotPose(REALSENSE_FORWARD_POSE), + }; + case UnderVideoButton.LookAtGripper: + return { + onClick: () => + FunctionProvider.remoteRobot?.lookAtGripper("lookAtGripper"), //setRobotPose(REALSENSE_GRIPPER_POSE) + }; + case UnderVideoButton.FollowGripper: + return { + onCheck: (toggle: boolean) => + FunctionProvider.remoteRobot?.setToggle("setFollowGripper", toggle), + }; + case UnderVideoButton.DepthSensing: + return { + onCheck: (toggle: boolean) => + FunctionProvider.remoteRobot?.setToggle("setDepthSensing", toggle), + }; + case UnderVideoButton.ToggleArucoMarkers: + return { + onCheck: (toggle: boolean) => + FunctionProvider.remoteRobot?.setToggle("setArucoMarkers", toggle), + }; + case UnderVideoButton.CenterWrist: + return { + onClick: () => + FunctionProvider.remoteRobot?.setRobotPose(CENTER_WRIST), + }; + case UnderVideoButton.StowWrist: + return { + onClick: () => FunctionProvider.remoteRobot?.setRobotPose(STOW_WRIST), + }; + default: + throw Error( + `Cannot get function for unknown UnderVideoButton ${button}`, + ); } -} \ No newline at end of file + } +} diff --git a/src/pages/operator/tsx/index.tsx b/src/pages/operator/tsx/index.tsx index 7fa4e17a..da2990c3 100644 --- a/src/pages/operator/tsx/index.tsx +++ b/src/pages/operator/tsx/index.tsx @@ -1,30 +1,40 @@ -import React from 'react' -import { createRoot, Root } from 'react-dom/client'; -import { WebRTCConnection } from 'shared/webrtcconnections'; -import { WebRTCMessage, RemoteStream, RobotPose, ROSOccupancyGrid, delay, waitUntil } from 'shared/util'; -import { RemoteRobot } from 'shared/remoterobot'; -import { cmd } from 'shared/commands'; -import { Operator } from './Operator'; -import { DEFAULT_VELOCITY_SCALE } from './static_components/SpeedControl'; -import { StorageHandler } from './storage_handler/StorageHandler'; -import { FirebaseStorageHandler } from './storage_handler/FirebaseStorageHandler'; -import { LocalStorageHandler } from './storage_handler/LocalStorageHandler'; -import { FirebaseOptions } from "firebase/app" -import { ButtonFunctionProvider } from './function_providers/ButtonFunctionProvider'; -import { FunctionProvider } from './function_providers/FunctionProvider'; -import { PredictiveDisplayFunctionProvider } from './function_providers/PredictiveDisplayFunctionProvider'; -import { UnderVideoFunctionProvider } from './function_providers/UnderVideoFunctionProvider'; -import { MapFunctionProvider } from './function_providers/MapFunctionProvider'; -import { UnderMapFunctionProvider } from './function_providers/UnderMapFunctionProvider'; -import { MovementRecorderFunctionProvider } from './function_providers/MovementRecorderFunctionProvider'; -import { MobileOperator } from './MobileOperator'; -import {isMobile} from 'react-device-detect'; +import React from "react"; +import { createRoot, Root } from "react-dom/client"; +import { WebRTCConnection } from "shared/webrtcconnections"; +import { + WebRTCMessage, + RemoteStream, + RobotPose, + ROSOccupancyGrid, + delay, + waitUntil, +} from "shared/util"; +import { RemoteRobot } from "shared/remoterobot"; +import { cmd } from "shared/commands"; +import { Operator } from "./Operator"; +import { DEFAULT_VELOCITY_SCALE } from "./static_components/SpeedControl"; +import { StorageHandler } from "./storage_handler/StorageHandler"; +import { FirebaseStorageHandler } from "./storage_handler/FirebaseStorageHandler"; +import { LocalStorageHandler } from "./storage_handler/LocalStorageHandler"; +import { FirebaseOptions } from "firebase/app"; +import { ButtonFunctionProvider } from "./function_providers/ButtonFunctionProvider"; +import { FunctionProvider } from "./function_providers/FunctionProvider"; +import { PredictiveDisplayFunctionProvider } from "./function_providers/PredictiveDisplayFunctionProvider"; +import { UnderVideoFunctionProvider } from "./function_providers/UnderVideoFunctionProvider"; +import { MapFunctionProvider } from "./function_providers/MapFunctionProvider"; +import { UnderMapFunctionProvider } from "./function_providers/UnderMapFunctionProvider"; +import { MovementRecorderFunctionProvider } from "./function_providers/MovementRecorderFunctionProvider"; +import { MobileOperator } from "./MobileOperator"; +import { isMobile } from "react-device-detect"; import "operator/css/index.css"; -import { RunStopFunctionProvider } from './function_providers/RunStopFunctionProvider'; -import { BatteryVoltageFunctionProvider } from './function_providers/BatteryVoltageFunctionProvider'; -import { waitUntilAsync } from '../../../shared/util'; +import { RunStopFunctionProvider } from "./function_providers/RunStopFunctionProvider"; +import { BatteryVoltageFunctionProvider } from "./function_providers/BatteryVoltageFunctionProvider"; +import { waitUntilAsync } from "../../../shared/util"; -let allRemoteStreams: Map = new Map() +let allRemoteStreams: Map = new Map< + string, + RemoteStream +>(); let remoteRobot: RemoteRobot; let connection: WebRTCConnection; let root: Root; @@ -32,80 +42,88 @@ export let hasBetaTeleopKit: boolean; export let occupancyGrid: ROSOccupancyGrid | undefined = undefined; export let storageHandler: StorageHandler; -// Create the function providers. These abstract the logic between the React +// Create the function providers. These abstract the logic between the React // components and remote robot. export var buttonFunctionProvider = new ButtonFunctionProvider(); -export var predicitiveDisplayFunctionProvider = new PredictiveDisplayFunctionProvider(); +export var predicitiveDisplayFunctionProvider = + new PredictiveDisplayFunctionProvider(); export var underVideoFunctionProvider = new UnderVideoFunctionProvider(); export var runStopFunctionProvider = new RunStopFunctionProvider(); -export var batteryVoltageFunctionProvider = new BatteryVoltageFunctionProvider(); +export var batteryVoltageFunctionProvider = + new BatteryVoltageFunctionProvider(); export var mapFunctionProvider: MapFunctionProvider; export var underMapFunctionProvider: UnderMapFunctionProvider; export var movementRecorderFunctionProvider: MovementRecorderFunctionProvider; // Create the WebRTC connection and connect the operator room connection = new WebRTCConnection({ - peerRole: "operator", - polite: true, - onMessage: handleWebRTCMessage, - onTrackAdded: handleRemoteTrackAdded, - onMessageChannelOpen: configureRemoteRobot, - onConnectionEnd: disconnectFromRobot + peerRole: "operator", + polite: true, + onMessage: handleWebRTCMessage, + onTrackAdded: handleRemoteTrackAdded, + onMessageChannelOpen: configureRemoteRobot, + onConnectionEnd: disconnectFromRobot, }); new Promise(async (resolve) => { - let connected = false; - while (!connected) { - connection.hangup() + let connected = false; + while (!connected) { + connection.hangup(); - // Attempt to join robot room - let joinedRobotRoom = await connection.addOperatorToRobotRoom() - if (!joinedRobotRoom) { - console.log('Operator failed to join robot room') - await delay(500) - continue; - } - - // Wait for WebRTC connection to resolve, timeout after 10 seconds - let isResolved = await waitUntil(() => connection.connectionState() == "connected", 10000) - if (!isResolved) { - console.warn('WebRTC connection could not resolve') - await delay(500) - continue; - } + // Attempt to join robot room + let joinedRobotRoom = await connection.addOperatorToRobotRoom(); + if (!joinedRobotRoom) { + console.log("Operator failed to join robot room"); + await delay(500); + continue; + } - // Wait for data to flow through the data channel, timeout after 10 seconds - connected = await waitUntilAsync(async () => await connection.isConnected(), 10000); - if (!connected) { - console.warn('No data flowing through data channel') - await delay(500) - continue; - } + // Wait for WebRTC connection to resolve, timeout after 10 seconds + let isResolved = await waitUntil( + () => connection.connectionState() == "connected", + 10000, + ); + if (!isResolved) { + console.warn("WebRTC connection could not resolve"); + await delay(500); + continue; + } - await delay(1000) // 1 second delay to allow data to flow through data channel - initializeOperator() - resolve() + // Wait for data to flow through the data channel, timeout after 10 seconds + connected = await waitUntilAsync( + async () => await connection.isConnected(), + 10000, + ); + if (!connected) { + console.warn("No data flowing through data channel"); + await delay(500); + continue; } -}) + + await delay(1000); // 1 second delay to allow data to flow through data channel + initializeOperator(); + resolve(); + } +}); // Create root once when index is loaded -const container = document.getElementById('root'); +const container = document.getElementById("root"); root = createRoot(container!); /** Handle when the WebRTC connection adds a new track on a camera video stream. */ function handleRemoteTrackAdded(event: RTCTrackEvent) { - console.log('Remote track added.'); - const track = event.track; - const stream = event.streams[0]; - console.log(stream.getVideoTracks()[0].getConstraints()) - console.log('got track id=' + track.id, track); - if (stream) { - console.log('stream id=' + stream.id, stream); - } - console.log('OPERATOR: adding remote tracks'); + console.log("Remote track added."); + const track = event.track; + const stream = event.streams[0]; + console.log(stream.getVideoTracks()[0].getConstraints()); + console.log("got track id=" + track.id, track); + if (stream) { + console.log("stream id=" + stream.id, stream); + } + console.log("OPERATOR: adding remote tracks"); - let streamName = connection.cameraInfo[stream.id] - allRemoteStreams.set(streamName, { 'track': track, 'stream': stream }); + let streamName = connection.cameraInfo[stream.id]; + allRemoteStreams.set(streamName, { track: track, stream: stream }); } /** @@ -113,178 +131,180 @@ function handleRemoteTrackAdded(event: RTCTrackEvent) { * @param message the {@link WebRTCMessage} or an array of messages. */ function handleWebRTCMessage(message: WebRTCMessage | WebRTCMessage[]) { - if (message instanceof Array) { - for (const subMessage of message) { - // Recursive call to handle each message in the array - handleWebRTCMessage(subMessage) - } - return + if (message instanceof Array) { + for (const subMessage of message) { + // Recursive call to handle each message in the array + handleWebRTCMessage(subMessage); } + return; + } - switch (message.type) { - case 'validJointState': - remoteRobot.sensors.checkValidJointState( - message.robotPose, - message.jointsInLimits, - message.jointsInCollision - ); - break; - case 'isRunStopped': - remoteRobot.sensors.setRunStopState(message.enabled) - break; - case 'hasBetaTeleopKit': - hasBetaTeleopKit = message.value - break; - case 'occupancyGrid': - if (!occupancyGrid) { - occupancyGrid = message.message - } else { - occupancyGrid.data = occupancyGrid.data.concat(message.message.data) - } - break; - case 'amclPose': - remoteRobot.setMapPose( - message.message - ) - break; - case 'goalStatus': - remoteRobot.setGoalReached(true) - break; - case 'moveBaseState': - console.log(message.message) - underMapFunctionProvider.setMoveBaseState(message.message) - break; - case 'relativePose': - remoteRobot.setRelativePose(message.message) - break; - case 'batteryVoltage': - remoteRobot.sensors.setBatteryVoltage(message.message) - break; - default: - throw Error(`unhandled WebRTC message type ${message.type}`) - } + switch (message.type) { + case "validJointState": + remoteRobot.sensors.checkValidJointState( + message.robotPose, + message.jointsInLimits, + message.jointsInCollision, + ); + break; + case "isRunStopped": + remoteRobot.sensors.setRunStopState(message.enabled); + break; + case "hasBetaTeleopKit": + hasBetaTeleopKit = message.value; + break; + case "occupancyGrid": + if (!occupancyGrid) { + occupancyGrid = message.message; + } else { + occupancyGrid.data = occupancyGrid.data.concat(message.message.data); + } + break; + case "amclPose": + remoteRobot.setMapPose(message.message); + break; + case "goalStatus": + remoteRobot.setGoalReached(true); + break; + case "moveBaseState": + console.log(message.message); + underMapFunctionProvider.setMoveBaseState(message.message); + break; + case "relativePose": + remoteRobot.setRelativePose(message.message); + break; + case "batteryVoltage": + remoteRobot.sensors.setBatteryVoltage(message.message); + break; + default: + throw Error(`unhandled WebRTC message type ${message.type}`); + } } /** - * Sets up remote robot, creates the storage handler, + * Sets up remote robot, creates the storage handler, * and renders the operator browser. */ function initializeOperator() { - // configureRemoteRobot(); - const storageHandlerReadyCallback = () => { - underMapFunctionProvider = new UnderMapFunctionProvider(storageHandler) - movementRecorderFunctionProvider = new MovementRecorderFunctionProvider(storageHandler) - renderOperator(storageHandler); - } - storageHandler = createStorageHandler(storageHandlerReadyCallback); + // configureRemoteRobot(); + const storageHandlerReadyCallback = () => { + underMapFunctionProvider = new UnderMapFunctionProvider(storageHandler); + movementRecorderFunctionProvider = new MovementRecorderFunctionProvider( + storageHandler, + ); + renderOperator(storageHandler); + }; + storageHandler = createStorageHandler(storageHandlerReadyCallback); } -/** - * Configures the remote robot, which connects with the robot browser over the +/** + * Configures the remote robot, which connects with the robot browser over the * WebRTC connection. */ function configureRemoteRobot() { - remoteRobot = new RemoteRobot({ - robotChannel: (message: cmd) => connection.sendData(message), - }); - occupancyGrid = undefined; - remoteRobot.getHasBetaTeleopKit("getHasBetaTeleopKit") - FunctionProvider.addRemoteRobot(remoteRobot); - mapFunctionProvider = new MapFunctionProvider(); - remoteRobot.sensors.setFunctionProviderCallback(buttonFunctionProvider.updateJointStates); - remoteRobot.sensors.setBatteryFunctionProviderCallback(batteryVoltageFunctionProvider.updateVoltage) - remoteRobot.sensors.setRunStopFunctionProviderCallback(runStopFunctionProvider.updateRunStopState) + remoteRobot = new RemoteRobot({ + robotChannel: (message: cmd) => connection.sendData(message), + }); + occupancyGrid = undefined; + remoteRobot.getHasBetaTeleopKit("getHasBetaTeleopKit"); + FunctionProvider.addRemoteRobot(remoteRobot); + mapFunctionProvider = new MapFunctionProvider(); + remoteRobot.sensors.setFunctionProviderCallback( + buttonFunctionProvider.updateJointStates, + ); + remoteRobot.sensors.setBatteryFunctionProviderCallback( + batteryVoltageFunctionProvider.updateVoltage, + ); + remoteRobot.sensors.setRunStopFunctionProviderCallback( + runStopFunctionProvider.updateRunStopState, + ); } /** - * Creates a storage handler based on the `storage` property in the process + * Creates a storage handler based on the `storage` property in the process * environment. * @param storageHandlerReadyCallback callback when the storage handler is ready * @returns the storage handler */ function createStorageHandler(storageHandlerReadyCallback: () => void) { - switch (process.env.storage) { - case ('firebase'): - const config: FirebaseOptions = { - apiKey: process.env.apiKey, - authDomain: process.env.authDomain, - projectId: process.env.projectId, - storageBucket: process.env.storageBucket, - messagingSenderId: process.env.messagingSenderId, - appId: process.env.appId, - measurementId: process.env.measurementId - } - return new FirebaseStorageHandler( - storageHandlerReadyCallback, - config - ); - default: - return new LocalStorageHandler(storageHandlerReadyCallback); - } + switch (process.env.storage) { + case "firebase": + const config: FirebaseOptions = { + apiKey: process.env.apiKey, + authDomain: process.env.authDomain, + projectId: process.env.projectId, + storageBucket: process.env.storageBucket, + messagingSenderId: process.env.messagingSenderId, + appId: process.env.appId, + measurementId: process.env.measurementId, + }; + return new FirebaseStorageHandler(storageHandlerReadyCallback, config); + default: + return new LocalStorageHandler(storageHandlerReadyCallback); + } } /** * Renders the operator browser. - * + * * @param storageHandler the storage handler */ function renderOperator(storageHandler: StorageHandler) { - const layout = storageHandler.loadCurrentLayoutOrDefault(); - FunctionProvider.initialize(DEFAULT_VELOCITY_SCALE, layout.actionMode); - - !isMobile ? - root.render( - - ) - : - root.render( - - ) + const layout = storageHandler.loadCurrentLayoutOrDefault(); + FunctionProvider.initialize(DEFAULT_VELOCITY_SCALE, layout.actionMode); - if (!isMobile) { - var loader = document.createElement('div'); - loader.className = 'loader' - var loaderText = document.createElement('div'); - loaderText.className = 'reconnecting-text' - var text = document.createElement('p') - text.textContent = 'Reconnecting...' - loaderText.appendChild(text) - var loaderBackground = document.createElement('div'); - loaderBackground.className = 'loader-background' + !isMobile + ? root.render( + , + ) + : root.render( + , + ); - setInterval(async() => { - let connected = await connection.isConnected() - if (!connected && !window.document.body.contains(loader)) { - window.document.body.appendChild(loaderBackground) - window.document.body.appendChild(loaderText) - window.document.body.appendChild(loader) - } else if (connected && window.document.body.contains(loader)) { - window.document.body.removeChild(loaderBackground) - window.document.body.removeChild(loaderText) - window.document.body.removeChild(loader) - } - }, 1000) - } + if (!isMobile) { + var loader = document.createElement("div"); + loader.className = "loader"; + var loaderText = document.createElement("div"); + loaderText.className = "reconnecting-text"; + var text = document.createElement("p"); + text.textContent = "Reconnecting..."; + loaderText.appendChild(text); + var loaderBackground = document.createElement("div"); + loaderBackground.className = "loader-background"; + + setInterval(async () => { + let connected = await connection.isConnected(); + if (!connected && !window.document.body.contains(loader)) { + window.document.body.appendChild(loaderBackground); + window.document.body.appendChild(loaderText); + window.document.body.appendChild(loader); + } else if (connected && window.document.body.contains(loader)) { + window.document.body.removeChild(loaderBackground); + window.document.body.removeChild(loaderText); + window.document.body.removeChild(loader); + } + }, 1000); + } } function disconnectFromRobot() { - connection.hangup() - connection.stop() + connection.hangup(); + connection.stop(); } window.onbeforeunload = () => { - connection.hangup() - connection.stop() + connection.hangup(); + connection.stop(); }; window.onunload = () => { - connection.hangup() - connection.stop() -}; \ No newline at end of file + connection.hangup(); + connection.stop(); +}; diff --git a/src/pages/operator/tsx/layout_components/ButtonGrid.tsx b/src/pages/operator/tsx/layout_components/ButtonGrid.tsx index 27ab6309..b7198c4d 100644 --- a/src/pages/operator/tsx/layout_components/ButtonGrid.tsx +++ b/src/pages/operator/tsx/layout_components/ButtonGrid.tsx @@ -1,104 +1,121 @@ import { buttonFunctionProvider } from "operator/tsx/index"; -import { ButtonFunctions, ButtonPadButton, ButtonState } from "../function_providers/ButtonFunctionProvider"; -import { CustomizableComponentProps, isSelected } from "./CustomizableComponent"; +import { + ButtonFunctions, + ButtonPadButton, + ButtonState, +} from "../function_providers/ButtonFunctionProvider"; +import { + CustomizableComponentProps, + isSelected, +} from "./CustomizableComponent"; import { className } from "shared/util"; -import "operator/css/ButtonGrid.css" +import "operator/css/ButtonGrid.css"; const BUTTON_NAMES = [ - "Forward", - "Backwards", - "Turn Left", - "Turn Right", + "Forward", + "Backwards", + "Turn Left", + "Turn Right", - "Move Lift Up", - "Move Lift Down", - "Extend Arm", - "Collapse Arm", + "Move Lift Up", + "Move Lift Down", + "Extend Arm", + "Collapse Arm", - "Roll Left", - "Roll Right", - "Pitch Up", - "Pitch Down", - "Rotate Left", - "Rotate Right", + "Roll Left", + "Roll Right", + "Pitch Up", + "Pitch Down", + "Rotate Left", + "Rotate Right", - "Open Gripper", - "Close Gripper" -] + "Open Gripper", + "Close Gripper", +]; const BUTTON_FUNCTIONS = [ - ButtonPadButton.BaseForward, - ButtonPadButton.BaseReverse, - ButtonPadButton.BaseRotateLeft, - ButtonPadButton.BaseRotateRight, - ButtonPadButton.ArmLift, - ButtonPadButton.ArmLower, - ButtonPadButton.ArmExtend, - ButtonPadButton.ArmRetract, - ButtonPadButton.WristRollLeft, - ButtonPadButton.WristRollRight, - ButtonPadButton.WristPitchUp, - ButtonPadButton.WristPitchDown, - ButtonPadButton.WristRotateIn, - ButtonPadButton.WristRotateOut, - ButtonPadButton.GripperOpen, - ButtonPadButton.GripperClose -] + ButtonPadButton.BaseForward, + ButtonPadButton.BaseReverse, + ButtonPadButton.BaseRotateLeft, + ButtonPadButton.BaseRotateRight, + ButtonPadButton.ArmLift, + ButtonPadButton.ArmLower, + ButtonPadButton.ArmExtend, + ButtonPadButton.ArmRetract, + ButtonPadButton.WristRollLeft, + ButtonPadButton.WristRollRight, + ButtonPadButton.WristPitchUp, + ButtonPadButton.WristPitchDown, + ButtonPadButton.WristRotateIn, + ButtonPadButton.WristRotateOut, + ButtonPadButton.GripperOpen, + ButtonPadButton.GripperClose, +]; const HEADER_NAMES = [ - "Basic Driving Controls", - "Basic Arm Controls", - "Wrist Controls", - "Gripper Controls" -] + "Basic Driving Controls", + "Basic Arm Controls", + "Wrist Controls", + "Gripper Controls", +]; const BACKGROUND_COLORS: JSX.Element[] = []; for (let i = 0; i < 4; i++) { - BACKGROUND_COLORS.push( - - ) + BACKGROUND_COLORS.push( + , + ); } export const ButtonGrid = (props: CustomizableComponentProps) => { - const { customizing } = props.sharedState; - const selected = isSelected(props); - function handleSelect(event: React.MouseEvent) { - event.stopPropagation(); - props.sharedState.onSelect(props.definition, props.path); - } - return ( -
    - {BACKGROUND_COLORS} - {HEADER_NAMES.map((headerName, idx) => ( -

    {headerName}

    - ))} - {BUTTON_NAMES.map((buttonName, idx) => { - const buttonFunction = BUTTON_FUNCTIONS[idx]; - const buttonState: ButtonState = props.sharedState.buttonStateMap?.get(buttonFunction) || ButtonState.Inactive; - const functs: ButtonFunctions = buttonFunctionProvider.provideFunctions(buttonFunction); - const clickProps = props.sharedState.customizing ? {} : { - onMouseDown: functs.onClick, - onMouseUp: functs.onRelease, - onMouseLeave: functs.onLeave - } - return ( - - ); - })} -
    - ) -} \ No newline at end of file + const { customizing } = props.sharedState; + const selected = isSelected(props); + function handleSelect(event: React.MouseEvent) { + event.stopPropagation(); + props.sharedState.onSelect(props.definition, props.path); + } + return ( +
    + {BACKGROUND_COLORS} + {HEADER_NAMES.map((headerName, idx) => ( +

    + {headerName} +

    + ))} + {BUTTON_NAMES.map((buttonName, idx) => { + const buttonFunction = BUTTON_FUNCTIONS[idx]; + const buttonState: ButtonState = + props.sharedState.buttonStateMap?.get(buttonFunction) || + ButtonState.Inactive; + const functs: ButtonFunctions = + buttonFunctionProvider.provideFunctions(buttonFunction); + const clickProps = props.sharedState.customizing + ? {} + : { + onMouseDown: functs.onClick, + onMouseUp: functs.onRelease, + onMouseLeave: functs.onLeave, + }; + return ( + + ); + })} +
    + ); +}; diff --git a/src/pages/operator/tsx/layout_components/ButtonPad.tsx b/src/pages/operator/tsx/layout_components/ButtonPad.tsx index 45f8bdf1..0d710c39 100644 --- a/src/pages/operator/tsx/layout_components/ButtonPad.tsx +++ b/src/pages/operator/tsx/layout_components/ButtonPad.tsx @@ -1,252 +1,277 @@ import React from "react"; -import { CustomizableComponentProps, SharedState, isSelected } from "./CustomizableComponent"; -import { ButtonPadDefinition, ButtonPadId, ButtonPadIdMobile } from "../utils/component_definitions"; +import { + CustomizableComponentProps, + SharedState, + isSelected, +} from "./CustomizableComponent"; +import { + ButtonPadDefinition, + ButtonPadId, + ButtonPadIdMobile, +} from "../utils/component_definitions"; import { className } from "shared/util"; import { buttonFunctionProvider } from "operator/tsx/index"; -import { ButtonPadShape, getIcon, getPathsFromShape, SVG_RESOLUTION } from "../utils/svg"; -import { ButtonFunctions, ButtonPadButton, ButtonState } from "../function_providers/ButtonFunctionProvider"; -import {isMobile} from 'react-device-detect'; -import "operator/css/ButtonPad.css" +import { + ButtonPadShape, + getIcon, + getPathsFromShape, + SVG_RESOLUTION, +} from "../utils/svg"; +import { + ButtonFunctions, + ButtonPadButton, + ButtonState, +} from "../function_providers/ButtonFunctionProvider"; +import { isMobile } from "react-device-detect"; +import "operator/css/ButtonPad.css"; /** Properties for {@link ButtonPad} */ type ButtonPadProps = CustomizableComponentProps & { - /* If the button pad is overlaid on a camera view. */ - overlay?: boolean; - /* Aspect ratio of the button pad */ - aspectRatio?: number; -} + /* If the button pad is overlaid on a camera view. */ + overlay?: boolean; + /* Aspect ratio of the button pad */ + aspectRatio?: number; +}; /** - * A set of buttons which can be overlaid as a child of a camera view or + * A set of buttons which can be overlaid as a child of a camera view or * standalone. - * + * * @param props {@link ButtonPadProps} */ export const ButtonPad = (props: ButtonPadProps) => { - /** Reference to the SVG which makes up the button pad */ - const svgRef = React.useRef(null); - /** List of path shapes for each button on the button pad */ - const definition = props.definition as ButtonPadDefinition; - const id: ButtonPadId = definition.id; - if (!id) throw Error("Undefined button pad ID at path " + props.path); - const [shape, functions] = getShapeAndFunctionsFromId(definition.id); - const [paths, iconPositions] = getPathsFromShape(shape, props.aspectRatio); + /** Reference to the SVG which makes up the button pad */ + const svgRef = React.useRef(null); + /** List of path shapes for each button on the button pad */ + const definition = props.definition as ButtonPadDefinition; + const id: ButtonPadId = definition.id; + if (!id) throw Error("Undefined button pad ID at path " + props.path); + const [shape, functions] = getShapeAndFunctionsFromId(definition.id); + const [paths, iconPositions] = getPathsFromShape(shape, props.aspectRatio); - // Paths and functions should be the same length - if (paths.length !== functions.length) { - throw Error(`paths length: ${paths.length}, functions length: ${functions.length}`); - } + // Paths and functions should be the same length + if (paths.length !== functions.length) { + throw Error( + `paths length: ${paths.length}, functions length: ${functions.length}`, + ); + } - const { customizing } = props.sharedState; - const { overlay } = props; - const selected = isSelected(props); + const { customizing } = props.sharedState; + const { overlay } = props; + const selected = isSelected(props); - /** Uses the paths and buttonsProps to create the buttons */ - function mapPaths(svgPath: string, i: number) { - const buttonProps = { - iconPosition: iconPositions[i], - svgPath, - funct: functions[i], - sharedState: props.sharedState - } - // Buttons will not function during customization mode - return ; - } + /** Uses the paths and buttonsProps to create the buttons */ + function mapPaths(svgPath: string, i: number) { + const buttonProps = { + iconPosition: iconPositions[i], + svgPath, + funct: functions[i], + sharedState: props.sharedState, + }; + // Buttons will not function during customization mode + return ; + } - /** Callback when SVG is clicked during customize mode */ - const onSelect = (event: React.MouseEvent) => { - // Make sure the container of the button pad doesn't get selected - event.stopPropagation(); - props.sharedState.onSelect(props.definition, props.path);; - } + /** Callback when SVG is clicked during customize mode */ + const onSelect = (event: React.MouseEvent) => { + // Make sure the container of the button pad doesn't get selected + event.stopPropagation(); + props.sharedState.onSelect(props.definition, props.path); + }; - // In customizing state add onClick callback to button pad SVG element - // note: if overlaid on a video stream, let the parent video stream handle the click - const selectProp = customizing && !overlay ? { - "onClick": onSelect - } : {}; - - return ( -
    - {/* {!overlay && !isMobile?

    {id}

    : <>} */} - - {paths.map(mapPaths)} - -
    - ); -} + // In customizing state add onClick callback to button pad SVG element + // note: if overlaid on a video stream, let the parent video stream handle the click + const selectProp = + customizing && !overlay + ? { + onClick: onSelect, + } + : {}; + return ( +
    + {/* {!overlay && !isMobile?

    {id}

    : <>} */} + + {paths.map(mapPaths)} + +
    + ); +}; /** Properties for a single button on a button pad */ export type SingleButtonProps = { - svgPath: string, - funct: ButtonPadButton, - sharedState: SharedState, - iconPosition: { x: number, y: number } -} + svgPath: string; + funct: ButtonPadButton; + sharedState: SharedState; + iconPosition: { x: number; y: number }; +}; const SingleButton = (props: SingleButtonProps) => { - const functs: ButtonFunctions = buttonFunctionProvider.provideFunctions(props.funct); - const clickProps = props.sharedState.customizing ? {} : { + const functs: ButtonFunctions = buttonFunctionProvider.provideFunctions( + props.funct, + ); + const clickProps = props.sharedState.customizing + ? {} + : { onPointerDown: functs.onClick, onPointerUp: functs.onRelease, - onPointerLeave: functs.onLeave - } - const buttonState: ButtonState = props.sharedState.buttonStateMap?.get(props.funct) || ButtonState.Inactive; - const icon = getIcon(props.funct); - const title = props.funct; - const height = isMobile ? 75 : 85; - const width = isMobile ? 75 : 85; - const x = props.iconPosition.x - width / 2; - const y = props.iconPosition.y - height / 2; - return ( - - - {title} - - -

    {title}

    -
    - ) -} + onPointerLeave: functs.onLeave, + }; + const buttonState: ButtonState = + props.sharedState.buttonStateMap?.get(props.funct) || ButtonState.Inactive; + const icon = getIcon(props.funct); + const title = props.funct; + const height = isMobile ? 75 : 85; + const width = isMobile ? 75 : 85; + const x = props.iconPosition.x - width / 2; + const y = props.iconPosition.y - height / 2; + return ( + + + {title} + + +

    {title}

    +
    + ); +}; /** - * Provides the shape and fuctions for a button pad based on the identifier - * + * Provides the shape and functions for a button pad based on the identifier + * * @param id the identifier of the button pad - * @returns the shape of the button pad {@link ButtonPadShape} and a list of + * @returns the shape of the button pad {@link ButtonPadShape} and a list of * {@link ButtonPadButton} where each element informs the function of * the corresponding button on the button pad */ -function getShapeAndFunctionsFromId(id: ButtonPadId | ButtonPadIdMobile): [ButtonPadShape, ButtonPadButton[]] { - let shape: ButtonPadShape; - let functions: ButtonPadButton[]; - const B = ButtonPadButton; - switch (id) { - // case ButtonPadId.Drive: - // functions = [ - // B.BaseForward, - // B.BaseRotateRight, - // B.BaseReverse, - // B.BaseRotateLeft - // ]; - // shape = ButtonPadShape.Directional; - // break; - case ButtonPadId.ManipRealsense: - functions = [ - B.WristRotateIn, - B.WristRotateOut, - B.ArmExtend, - B.ArmRetract, - B.BaseForward, - B.BaseReverse, - B.ArmLift, - B.ArmLower, - B.GripperClose, - B.GripperOpen - ] - shape = ButtonPadShape.ManipRealsense; - break; - case ButtonPadId.GripperLift: - functions = [ - B.ArmLift, - B.ArmLower, - B.WristRotateIn, - B.WristRotateOut, - B.GripperOpen, - B.GripperClose, - ] - shape = ButtonPadShape.GripperLift; - break; - case ButtonPadId.DexWrist: - functions = [ - B.WristPitchUp, - B.WristPitchDown, - B.WristRotateIn, - B.WristRotateOut, - B.WristRollLeft, - B.WristRollRight, - B.GripperOpen, - B.GripperClose - ]; - shape = ButtonPadShape.DexWrist; - break; - case ButtonPadId.Base: - functions = [ - B.BaseForward, - B.BaseReverse, - B.BaseRotateLeft, - B.BaseRotateRight - ]; - shape = ButtonPadShape.SimpleButtonPad; - break; - case ButtonPadId.Camera: - functions = [ - B.CameraTiltUp, - B.CameraTiltDown, - B.CameraPanLeft, - B.CameraPanRight - ]; - shape = ButtonPadShape.SimpleButtonPad; - break; - // case ButtonPadId.Wrist: - // functions = [ - // B.WristRollLeft, - // B.WristRollRight, - // B.WristPitchUp, - // B.WristPitchDown, - // B.WristRotateIn, - // B.WristRotateOut, - // B.GripperOpen, - // B.GripperClose - // ]; - // shape = ButtonPadShape.StackedButtonPad; - // break; - case ButtonPadId.Arm: - functions = [ - B.ArmLift, - B.ArmLower, - B.ArmRetract, - B.ArmExtend - ]; - shape = ButtonPadShape.SimpleButtonPad; - break; - case ButtonPadIdMobile.Arm: - functions = [ - B.ArmLift, - B.ArmLower, - B.ArmRetract, - B.ArmExtend - ]; - shape = ButtonPadShape.RowButtonPad; - break; - case ButtonPadIdMobile.Gripper: - functions = [ - B.WristRotateIn, - B.WristRotateOut, - B.GripperOpen, - B.GripperClose - ]; - shape = ButtonPadShape.RowButtonPad; - break; - case ButtonPadIdMobile.Drive: - functions = [ - B.BaseForward, - B.BaseReverse, - B.BaseRotateLeft, - B.BaseRotateRight - ]; - shape = ButtonPadShape.RowButtonPad; - break; - default: - throw new Error(`unknow button pad id: ${id}`); - } +function getShapeAndFunctionsFromId( + id: ButtonPadId | ButtonPadIdMobile, +): [ButtonPadShape, ButtonPadButton[]] { + let shape: ButtonPadShape; + let functions: ButtonPadButton[]; + const B = ButtonPadButton; + switch (id) { + // case ButtonPadId.Drive: + // functions = [ + // B.BaseForward, + // B.BaseRotateRight, + // B.BaseReverse, + // B.BaseRotateLeft + // ]; + // shape = ButtonPadShape.Directional; + // break; + case ButtonPadId.ManipRealsense: + functions = [ + B.WristRotateIn, + B.WristRotateOut, + B.ArmExtend, + B.ArmRetract, + B.BaseForward, + B.BaseReverse, + B.ArmLift, + B.ArmLower, + B.GripperClose, + B.GripperOpen, + ]; + shape = ButtonPadShape.ManipRealsense; + break; + case ButtonPadId.GripperLift: + functions = [ + B.ArmLift, + B.ArmLower, + B.WristRotateIn, + B.WristRotateOut, + B.GripperOpen, + B.GripperClose, + ]; + shape = ButtonPadShape.GripperLift; + break; + case ButtonPadId.DexWrist: + functions = [ + B.WristPitchUp, + B.WristPitchDown, + B.WristRotateIn, + B.WristRotateOut, + B.WristRollLeft, + B.WristRollRight, + B.GripperOpen, + B.GripperClose, + ]; + shape = ButtonPadShape.DexWrist; + break; + case ButtonPadId.Base: + functions = [ + B.BaseForward, + B.BaseReverse, + B.BaseRotateLeft, + B.BaseRotateRight, + ]; + shape = ButtonPadShape.SimpleButtonPad; + break; + case ButtonPadId.Camera: + functions = [ + B.CameraTiltUp, + B.CameraTiltDown, + B.CameraPanLeft, + B.CameraPanRight, + ]; + shape = ButtonPadShape.SimpleButtonPad; + break; + // case ButtonPadId.Wrist: + // functions = [ + // B.WristRollLeft, + // B.WristRollRight, + // B.WristPitchUp, + // B.WristPitchDown, + // B.WristRotateIn, + // B.WristRotateOut, + // B.GripperOpen, + // B.GripperClose + // ]; + // shape = ButtonPadShape.StackedButtonPad; + // break; + case ButtonPadId.Arm: + functions = [B.ArmLift, B.ArmLower, B.ArmRetract, B.ArmExtend]; + shape = ButtonPadShape.SimpleButtonPad; + break; + case ButtonPadIdMobile.Arm: + functions = [B.ArmLift, B.ArmLower, B.ArmRetract, B.ArmExtend]; + shape = ButtonPadShape.RowButtonPad; + break; + case ButtonPadIdMobile.Gripper: + functions = [ + B.WristRotateIn, + B.WristRotateOut, + B.GripperOpen, + B.GripperClose, + ]; + shape = ButtonPadShape.RowButtonPad; + break; + case ButtonPadIdMobile.Drive: + functions = [ + B.BaseForward, + B.BaseReverse, + B.BaseRotateLeft, + B.BaseRotateRight, + ]; + shape = ButtonPadShape.RowButtonPad; + break; + default: + throw new Error(`unknow button pad id: ${id}`); + } - return [shape, functions]; -} \ No newline at end of file + return [shape, functions]; +} diff --git a/src/pages/operator/tsx/layout_components/CameraView.tsx b/src/pages/operator/tsx/layout_components/CameraView.tsx index 84c3932c..2c07404c 100644 --- a/src/pages/operator/tsx/layout_components/CameraView.tsx +++ b/src/pages/operator/tsx/layout_components/CameraView.tsx @@ -1,287 +1,368 @@ import React from "react"; -import { className, gripperProps, navigationProps, realsenseProps, RemoteStream } from "../../../../shared/util"; -import { CameraViewDefinition, ComponentType, CameraViewId, ComponentDefinition, FixedOverheadVideoStreamDef, RealsenseVideoStreamDef, AdjustableOverheadVideoStreamDef, GripperVideoStreamDef } from "../utils/component_definitions"; +import { + className, + gripperProps, + navigationProps, + realsenseProps, + RemoteStream, +} from "../../../../shared/util"; +import { + CameraViewDefinition, + ComponentType, + CameraViewId, + ComponentDefinition, + FixedOverheadVideoStreamDef, + RealsenseVideoStreamDef, + AdjustableOverheadVideoStreamDef, + GripperVideoStreamDef, +} from "../utils/component_definitions"; import { ButtonPad } from "./ButtonPad"; -import { CustomizableComponentProps, isSelected, SharedState } from "./CustomizableComponent"; +import { + CustomizableComponentProps, + isSelected, + SharedState, +} from "./CustomizableComponent"; import { DropZone } from "./DropZone"; import { PredictiveDisplay } from "./PredictiveDisplay"; -import { buttonFunctionProvider, hasBetaTeleopKit, underVideoFunctionProvider } from ".."; -import { ButtonPadButton, panTiltButtons } from "../function_providers/ButtonFunctionProvider"; -import { OverheadButtons, realsenseButtons, RealsenseButtons, UnderVideoButton, wristButtons } from "../function_providers/UnderVideoFunctionProvider"; +import { + buttonFunctionProvider, + hasBetaTeleopKit, + underVideoFunctionProvider, +} from ".."; +import { + ButtonPadButton, + panTiltButtons, +} from "../function_providers/ButtonFunctionProvider"; +import { + OverheadButtons, + realsenseButtons, + RealsenseButtons, + UnderVideoButton, + wristButtons, +} from "../function_providers/UnderVideoFunctionProvider"; import { CheckToggleButton } from "../basic_components/CheckToggleButton"; import { AccordionSelect } from "../basic_components/AccordionSelect"; -import "operator/css/CameraView.css" +import "operator/css/CameraView.css"; /** * Displays a video stream with an optional button pad overlay - * + * * @param props properties */ export const CameraView = (props: CustomizableComponentProps) => { - // Reference to the video element - const videoRef = React.useRef(null); - // X and Y position of the cursor when user clicks on the video - const [clickXY, setClickXY] = React.useState<[number, number] | null>(null); - const definition = props.definition as CameraViewDefinition; - if (!definition.children) console.warn(`Video stream definition at ${props.path} should have a 'children' property.`); - // Get the stream to display inside the video - const stream: MediaStream = getStream(definition.id, props.sharedState.remoteStreams); - // Refrence to the div immediately around the video element - const videoAreaRef = React.useRef(null); - // Boolean representing if the video stream needs to be constrained by height - // (constrained by width otherwise) - const [constrainedHeight, setConstrainedHeight] = React.useState(false); - const [predictiveDisplay, setPredictiveDisplay] = React.useState(false); - - React.useEffect(() => { - executeVideoSettings(definition); - }, [definition]) - - // Create the overlay - const overlayDefinition = predictiveDisplay ? { type: ComponentType.PredictiveDisplay } : (definition.children && definition.children.length > 0) ? definition.children[0] : undefined; - const videoAspectRatio = getVideoAspectRatio(definition); - const overlay = createOverlay(overlayDefinition, props.path, props.sharedState, videoAspectRatio); - - // Update the source of the video stream - React.useEffect(() => { - if (!videoRef?.current) return; - videoRef.current.srcObject = stream; - }, [stream]); - - const { customizing } = props.sharedState; - const selected = isSelected(props); - const videoClass = className("video-canvas", { customizing, selected }) - const realsense = props.definition.id === CameraViewId.realsense - const overhead = props.definition.id === CameraViewId.overhead - - /** Mark this video stream as selected */ - function selectSelf() { - props.sharedState.onSelect(props.definition, props.path); - setClickXY(null); - } - - /** Mark the button pad child as selected */ - function selectChild() { - props.sharedState.onSelect(overlayDefinition!, props.path + '-0'); - setClickXY(null); - } - - /** Opens a popup */ - function handleClick(event: React.MouseEvent) { - event.stopPropagation(); - - // If no button pad overlay then select self and return - if (!overlayDefinition || overlayDefinition.type !== ComponentType.ButtonPad) { - selectSelf(); - return; - } - - // Create context menu popup where user can choose between selecting - // the button pad or the video stream - const { clientX, clientY } = event; - const { left, top } = videoRef.current!.getBoundingClientRect(); - const x = clientX - left; - const y = clientY - top; - setClickXY([x, y]); + // Reference to the video element + const videoRef = React.useRef(null); + // X and Y position of the cursor when user clicks on the video + const [clickXY, setClickXY] = React.useState<[number, number] | null>(null); + const definition = props.definition as CameraViewDefinition; + if (!definition.children) + console.warn( + `Video stream definition at ${props.path} should have a 'children' property.`, + ); + // Get the stream to display inside the video + const stream: MediaStream = getStream( + definition.id, + props.sharedState.remoteStreams, + ); + // Reference to the div immediately around the video element + const videoAreaRef = React.useRef(null); + // Boolean representing if the video stream needs to be constrained by height + // (constrained by width otherwise) + const [constrainedHeight, setConstrainedHeight] = + React.useState(false); + const [predictiveDisplay, setPredictiveDisplay] = + React.useState(false); + + React.useEffect(() => { + executeVideoSettings(definition); + }, [definition]); + + // Create the overlay + const overlayDefinition = predictiveDisplay + ? { type: ComponentType.PredictiveDisplay } + : definition.children && definition.children.length > 0 + ? definition.children[0] + : undefined; + const videoAspectRatio = getVideoAspectRatio(definition); + const overlay = createOverlay( + overlayDefinition, + props.path, + props.sharedState, + videoAspectRatio, + ); + + // Update the source of the video stream + React.useEffect(() => { + if (!videoRef?.current) return; + videoRef.current.srcObject = stream; + }, [stream]); + + const { customizing } = props.sharedState; + const selected = isSelected(props); + const videoClass = className("video-canvas", { customizing, selected }); + const realsense = props.definition.id === CameraViewId.realsense; + const overhead = props.definition.id === CameraViewId.overhead; + + /** Mark this video stream as selected */ + function selectSelf() { + props.sharedState.onSelect(props.definition, props.path); + setClickXY(null); + } + + /** Mark the button pad child as selected */ + function selectChild() { + props.sharedState.onSelect(overlayDefinition!, props.path + "-0"); + setClickXY(null); + } + + /** Opens a popup */ + function handleClick(event: React.MouseEvent) { + event.stopPropagation(); + + // If no button pad overlay then select self and return + if ( + !overlayDefinition || + overlayDefinition.type !== ComponentType.ButtonPad + ) { + selectSelf(); + return; } - // Constrain the width or height when the stream gets too large - React.useEffect(() => { - const resizeObserver = new ResizeObserver(entries => { - - // height and width of area around the video stream - const { height, width } = entries[0].contentRect; - - // height and width of video stream - if (!videoRef?.current) return; - const videoRect = videoRef.current.getBoundingClientRect(); - - if (Math.abs(videoRect.height - height) > 1.0) { - setConstrainedHeight(true); - } else if (Math.abs(videoRect.width - width) > 1.0) { - setConstrainedHeight(false); - } - }); - if (!videoAreaRef?.current) return; - resizeObserver.observe(videoAreaRef.current); - return () => resizeObserver.disconnect(); - }, []); - - - const overlayContainer = ( + // Create context menu popup where user can choose between selecting + // the button pad or the video stream + const { clientX, clientY } = event; + const { left, top } = videoRef.current!.getBoundingClientRect(); + const x = clientX - left; + const y = clientY - top; + setClickXY([x, y]); + } + + // Constrain the width or height when the stream gets too large + React.useEffect(() => { + const resizeObserver = new ResizeObserver((entries) => { + // height and width of area around the video stream + const { height, width } = entries[0].contentRect; + + // height and width of video stream + if (!videoRef?.current) return; + const videoRect = videoRef.current.getBoundingClientRect(); + + if (Math.abs(videoRect.height - height) > 1.0) { + setConstrainedHeight(true); + } else if (Math.abs(videoRect.width - width) > 1.0) { + setConstrainedHeight(false); + } + }); + if (!videoAreaRef?.current) return; + resizeObserver.observe(videoAreaRef.current); + return () => resizeObserver.disconnect(); + }, []); + + const overlayContainer = ( +
    + { + // Display overlay on top of video stream + overlay ? ( + overlay + ) : ( + + ) + } + { + // When clickXY is set, display context menu + clickXY ? ( + setClickXY(null)} + /> + ) : undefined + } +
    + ); + // If the video is from the Realsense camera then include the pan-tilt + // buttons around the video, otherwise return the video + let videoOverlay = <>; + if (props.definition.id === CameraViewId.realsense) { + videoOverlay = ( + <>
    - { - // Display overlay on top of video stream - overlay ? overlay : - - } - { - // When clickXY is set, display context menu - clickXY ? setClickXY(null)} - /> : undefined - } -
    - ) - // If the video is from the Realsense camera then include the pan-tilt - // buttons around the video, otherwise return the video - let videoOverlay = (<>) - if (props.definition.id === CameraViewId.realsense) { - videoOverlay = ( - <> -
    - {panTiltButtons.map(dir => )} -
    - - ) - } else if (props.definition.id == CameraViewId.overhead) { - videoOverlay = ( - <> - { overlayDefinition?.type !== ComponentType.PredictiveDisplay && !props.sharedState.hasBetaTeleopKit ? -
    - {panTiltButtons.map(dir => )} -
    - : - <> - } - - ) - } - - const videoComponent = ( -
    - {videoOverlay} -
    - ) - - return ( -
    - {videoComponent} - { - definition.displayButtons ? -
    - -
    - : - <> - } + {panTiltButtons.map((dir) => ( + + ))}
    + ); -} + } else if (props.definition.id == CameraViewId.overhead) { + videoOverlay = ( + <> + {overlayDefinition?.type !== ComponentType.PredictiveDisplay && + !props.sharedState.hasBetaTeleopKit ? ( +
    + {panTiltButtons.map((dir) => ( + + ))} +
    + ) : ( + <> + )} + + ); + } + + const videoComponent = ( +
    + {videoOverlay} +
    + ); + + return ( +
    + {videoComponent} + {definition.displayButtons ? ( +
    + +
    + ) : ( + <> + )} +
    + ); +}; /** * Creates a single button for controlling the pan or tilt of the realsense camera * * @param props the direction of the button {@link PanTiltButton} - */ + */ const PanTiltButton = (props: { direction: ButtonPadButton }) => { - let gridPosition: { gridRow: number, gridColumn: number }; // the position in the 3x3 grid around the video element - let rotation: string; // how to rotate the arrow icon to point in the correct direction - const functs = buttonFunctionProvider.provideFunctions(props.direction); - - // Specify button details based on the direction - switch (props.direction) { - case (ButtonPadButton.CameraTiltUp): - gridPosition = { gridRow: 1, gridColumn: 2 }; - rotation = "-90"; - break; - case (ButtonPadButton.CameraTiltDown): - gridPosition = { gridRow: 3, gridColumn: 2 }; - rotation = "90"; - break; - case (ButtonPadButton.CameraPanLeft): - gridPosition = { gridRow: 2, gridColumn: 1 }; - rotation = "180"; - break; - case (ButtonPadButton.CameraPanRight): - gridPosition = { gridRow: 2, gridColumn: 3 }; - rotation = "0"; // by default the arrow icon points right - break; - default: - throw Error(`unknown pan tilt button direction ${props.direction}`) - } - - return ( - - ) -} + let gridPosition: { gridRow: number; gridColumn: number }; // the position in the 3x3 grid around the video element + let rotation: string; // how to rotate the arrow icon to point in the correct direction + const functs = buttonFunctionProvider.provideFunctions(props.direction); + + // Specify button details based on the direction + switch (props.direction) { + case ButtonPadButton.CameraTiltUp: + gridPosition = { gridRow: 1, gridColumn: 2 }; + rotation = "-90"; + break; + case ButtonPadButton.CameraTiltDown: + gridPosition = { gridRow: 3, gridColumn: 2 }; + rotation = "90"; + break; + case ButtonPadButton.CameraPanLeft: + gridPosition = { gridRow: 2, gridColumn: 1 }; + rotation = "180"; + break; + case ButtonPadButton.CameraPanRight: + gridPosition = { gridRow: 2, gridColumn: 3 }; + rotation = "0"; // by default the arrow icon points right + break; + default: + throw Error(`unknown pan tilt button direction ${props.direction}`); + } + + return ( + + ); +}; /** * Creates a single button for controlling the pan or tilt of the realsense camera * * @param props the direction of the button {@link PanTiltButton} - */ - const PanTiltButtonOverlay = (props: { direction: ButtonPadButton }) => { - const functs = buttonFunctionProvider.provideFunctions(props.direction); - const dir = props.direction.split(" ")[2] - let rotation: string; - - // Specify button details based on the direction - switch (props.direction) { - case (ButtonPadButton.CameraTiltUp): - rotation = "-90"; - break; - case (ButtonPadButton.CameraTiltDown): - rotation = "90"; - break; - case (ButtonPadButton.CameraPanLeft): - rotation = "180"; - break; - case (ButtonPadButton.CameraPanRight): - rotation = "0"; // by default the arrow icon points right - break; - default: - throw Error(`unknown pan tilt button direction ${props.direction}`) - } - - return ( - - ) -} + */ +const PanTiltButtonOverlay = (props: { direction: ButtonPadButton }) => { + const functs = buttonFunctionProvider.provideFunctions(props.direction); + const dir = props.direction.split(" ")[2]; + let rotation: string; + + // Specify button details based on the direction + switch (props.direction) { + case ButtonPadButton.CameraTiltUp: + rotation = "-90"; + break; + case ButtonPadButton.CameraTiltDown: + rotation = "90"; + break; + case ButtonPadButton.CameraPanLeft: + rotation = "180"; + break; + case ButtonPadButton.CameraPanRight: + rotation = "0"; // by default the arrow icon points right + break; + default: + throw Error(`unknown pan tilt button direction ${props.direction}`); + } + + return ( + + ); +}; /****************************************************************************** * Select context menu @@ -289,15 +370,15 @@ const PanTiltButton = (props: { direction: ButtonPadButton }) => { /** Props for {@link SelectContexMenu} */ type SelectContexMenuProps = { - /** x and y location to render the context menu popup */ - clickXY: [number, number]; - /** Callback to select the video stream */ - selectSelf: () => void; - /** Callback to select the child button pad */ - selectChild: () => void; - /** Callback to hide the context menu popup when click outside */ - clickOut: () => void; -} + /** x and y location to render the context menu popup */ + clickXY: [number, number]; + /** Callback to select the video stream */ + selectSelf: () => void; + /** Callback to select the child button pad */ + selectChild: () => void; + /** Callback to hide the context menu popup when click outside */ + clickOut: () => void; +}; /** * Creates a context menu popup when user clicks during @@ -305,54 +386,53 @@ type SelectContexMenuProps = { * parent video stream. * * @param props {@link SelectContexMenuProps} - */ + */ const SelectContexMenu = (props: SelectContexMenuProps) => { - const ref = React.useRef(null); - const [x, y] = props.clickXY; - - // Handler to close dropdown when click outside - React.useEffect(() => { - - /** Closes context menu if user clicks outside */ - const handler = (e: any) => { - // If didn't click inside the context menu or the existing SVG, then - // hide the popup - if (ref.current && !ref.current.contains(e.target)) { - props.clickOut(); - console.log('clicked') - } - }; - window.addEventListener("click", handler, true); - return () => { - window.removeEventListener("click", handler); - }; - }, []); - - /** - * Handles when the user clicks on one of the context menu options - * @param e mouse event of the click - * @param self if true selects itself (a button pad), if false selects its - * parent (the video stream) - */ - function handleClick(e: React.MouseEvent, self: boolean) { - self ? props.selectSelf() : props.selectChild(); - - // Make sure background elements don't receive a click - e.stopPropagation(); - } - - return ( - -
      -
    • handleClick(e, false)}>Button Pad
    • -
    • handleClick(e, true)}>Video Stream
    • -
    - ); -} + const ref = React.useRef(null); + const [x, y] = props.clickXY; + + // Handler to close dropdown when click outside + React.useEffect(() => { + /** Closes context menu if user clicks outside */ + const handler = (e: any) => { + // If didn't click inside the context menu or the existing SVG, then + // hide the popup + if (ref.current && !ref.current.contains(e.target)) { + props.clickOut(); + console.log("clicked"); + } + }; + window.addEventListener("click", handler, true); + return () => { + window.removeEventListener("click", handler); + }; + }, []); + + /** + * Handles when the user clicks on one of the context menu options + * @param e mouse event of the click + * @param self if true selects itself (a button pad), if false selects its + * parent (the video stream) + */ + function handleClick(e: React.MouseEvent, self: boolean) { + self ? props.selectSelf() : props.selectChild(); + + // Make sure background elements don't receive a click + e.stopPropagation(); + } + + return ( +
      +
    • handleClick(e, false)}>Button Pad
    • +
    • handleClick(e, true)}>Video Stream
    • +
    + ); +}; /******************************************************************************* * Helper functions @@ -360,21 +440,21 @@ const SelectContexMenu = (props: SelectContexMenuProps) => { /** * Get the aspect ratio of the video based on the definition - * + * * @param definition definition of the video stream * @returns aspect ratio of the video stream */ function getVideoAspectRatio(definition: CameraViewDefinition): number { - switch (definition.id) { - case (CameraViewId.gripper): - return gripperProps.width / gripperProps.height; - case (CameraViewId.overhead): - return navigationProps.width / navigationProps.height; - case (CameraViewId.realsense): - return realsenseProps.width / realsenseProps.height; - default: - throw Error(`undefined aspect ratio for ${definition.type}`) - } + switch (definition.id) { + case CameraViewId.gripper: + return gripperProps.width / gripperProps.height; + case CameraViewId.overhead: + return navigationProps.width / navigationProps.height; + case CameraViewId.realsense: + return realsenseProps.width / realsenseProps.height; + default: + throw Error(`undefined aspect ratio for ${definition.type}`); + } } /** @@ -383,35 +463,41 @@ function getVideoAspectRatio(definition: CameraViewDefinition): number { * @param overlayDefinition definition for the component to overlay on the video stream * @param path path to the parent video stream component * @param sharedState {@link SharedState} - * @returns overlay element, or undefined if video stream doesn't have an overlay - */ + * @returns overlay element, or undefined if video stream doesn't have an overlay + */ function createOverlay( - overlayDefinition: ComponentDefinition | undefined, - path: string, - sharedState: SharedState, - aspectRatio: number): JSX.Element | undefined { - // If overlay definition is undefined then there's no overlay for this stream - if (!overlayDefinition) return undefined; - if (!overlayDefinition.type) { - console.warn(`Video stream at path ${path} has child with undefined type:`) - console.warn(overlayDefinition); - return undefined; - } - - const overlayProps = { - definition: overlayDefinition, - path: path + "-0", - sharedState: sharedState - } as CustomizableComponentProps; - - switch (overlayDefinition.type) { - case (ComponentType.ButtonPad): - return - case (ComponentType.PredictiveDisplay): - return - default: - throw Error('Video stream at path ' + path + ' cannot overlay child of type' + overlayDefinition.type); - } + overlayDefinition: ComponentDefinition | undefined, + path: string, + sharedState: SharedState, + aspectRatio: number, +): JSX.Element | undefined { + // If overlay definition is undefined then there's no overlay for this stream + if (!overlayDefinition) return undefined; + if (!overlayDefinition.type) { + console.warn(`Video stream at path ${path} has child with undefined type:`); + console.warn(overlayDefinition); + return undefined; + } + + const overlayProps = { + definition: overlayDefinition, + path: path + "-0", + sharedState: sharedState, + } as CustomizableComponentProps; + + switch (overlayDefinition.type) { + case ComponentType.ButtonPad: + return ; + case ComponentType.PredictiveDisplay: + return ; + default: + throw Error( + "Video stream at path " + + path + + " cannot overlay child of type" + + overlayDefinition.type, + ); + } } /** @@ -419,76 +505,88 @@ function createOverlay( * * @param id identifier for the video stream * @param remoteStreams map of {@link RemoteStream} - * @returns the corresponding stream - */ -function getStream(id: CameraViewId, remoteStreams: Map): MediaStream { - let streamName: string; - switch (id) { - case CameraViewId.overhead: - streamName = "overhead"; - break; - case CameraViewId.realsense: - streamName = "realsense"; - break; - case CameraViewId.gripper: - streamName = "gripper"; - break; - default: - throw Error(`unknow video stream id: ${id}`); - } - return remoteStreams.get(streamName)!.stream; + * @returns the corresponding stream + */ +function getStream( + id: CameraViewId, + remoteStreams: Map, +): MediaStream { + let streamName: string; + switch (id) { + case CameraViewId.overhead: + streamName = "overhead"; + break; + case CameraViewId.realsense: + streamName = "realsense"; + break; + case CameraViewId.gripper: + streamName = "gripper"; + break; + default: + throw Error(`unknow video stream id: ${id}`); + } + return remoteStreams.get(streamName)!.stream; } /** * Executes any functions required when this video stream is rendered. This might * be changing the view cropping for the overhead camera, hiding the depth sensing * for the Realsense, etc. - * + * * @param definition {@link CameraViewDefinition} */ function executeVideoSettings(definition: CameraViewDefinition) { - switch (definition.id) { - case (CameraViewId.gripper): - break; - case (CameraViewId.overhead): - // executeFixedOverheadSettings(definition as FixedOverheadVideoStreamDef); - executeAdjustableOverheadettings(definition as AdjustableOverheadVideoStreamDef); - break; - case (CameraViewId.realsense): - executeRealsenseSettings(definition as RealsenseVideoStreamDef); - break; - default: - throw Error(`unknow video stream id: ${definition.id}`); - } + switch (definition.id) { + case CameraViewId.gripper: + break; + case CameraViewId.overhead: + // executeFixedOverheadSettings(definition as FixedOverheadVideoStreamDef); + executeAdjustableOverheadettings( + definition as AdjustableOverheadVideoStreamDef, + ); + break; + case CameraViewId.realsense: + executeRealsenseSettings(definition as RealsenseVideoStreamDef); + break; + default: + throw Error(`unknow video stream id: ${definition.id}`); + } } /** * Executes functions to prepare for rendering the overhead video stream. - * + * * @param definition {@link FixedOverheadVideoStreamDef} */ function executeFixedOverheadSettings(definition: FixedOverheadVideoStreamDef) { - const overheadViewButton = definition.gripperView ? UnderVideoButton.GripperView : UnderVideoButton.DriveView; - underVideoFunctionProvider.provideFunctions(overheadViewButton).onClick!(); + const overheadViewButton = definition.gripperView + ? UnderVideoButton.GripperView + : UnderVideoButton.DriveView; + underVideoFunctionProvider.provideFunctions(overheadViewButton).onClick!(); } /** * Executes functions to prepare for rendering the Realsense video stream. - * + * * @param definition {@link AdjustableOverheadVideoStreamDef} */ - function executeAdjustableOverheadettings(definition: AdjustableOverheadVideoStreamDef) { - underVideoFunctionProvider.provideFunctions(UnderVideoButton.FollowGripper).onCheck!(definition.followGripper || false); +function executeAdjustableOverheadettings( + definition: AdjustableOverheadVideoStreamDef, +) { + underVideoFunctionProvider.provideFunctions(UnderVideoButton.FollowGripper) + .onCheck!(definition.followGripper || false); } /** * Executes functions to prepare for rendering the Realsense video stream. - * + * * @param definition {@link RealsenseVideoStreamDef} */ function executeRealsenseSettings(definition: RealsenseVideoStreamDef) { - underVideoFunctionProvider.provideFunctions(UnderVideoButton.FollowGripper).onCheck!(definition.followGripper || false); - underVideoFunctionProvider.provideFunctions(UnderVideoButton.DepthSensing).onCheck!(definition.depthSensing || false); + underVideoFunctionProvider.provideFunctions(UnderVideoButton.FollowGripper) + .onCheck!(definition.followGripper || false); + underVideoFunctionProvider.provideFunctions(UnderVideoButton.DepthSensing) + .onCheck!(definition.depthSensing || false); } /******************************************************************************* @@ -496,139 +594,164 @@ function executeRealsenseSettings(definition: RealsenseVideoStreamDef) { */ /** - * Buttons to display under a video stream (e.g. toggle cropping of overhead + * Buttons to display under a video stream (e.g. toggle cropping of overhead * stream, display depth sensing on Realsense, etc.) */ const UnderVideoButtons = (props: { - definition: CameraViewDefinition, - setPredictiveDisplay: (enabled: boolean) => void, - betaTeleopKit: boolean, + definition: CameraViewDefinition; + setPredictiveDisplay: (enabled: boolean) => void; + betaTeleopKit: boolean; }) => { - let buttons: JSX.Element | null; - switch (props.definition.id) { - case (CameraViewId.gripper): - buttons = ; - break; - case (CameraViewId.overhead): - buttons = hasBetaTeleopKit ? - - : - - break; - case (CameraViewId.realsense): - buttons = ; - break; - default: - throw Error(`unknow video stream id: ${props.definition.id}`); - } - return buttons; -} + let buttons: JSX.Element | null; + switch (props.definition.id) { + case CameraViewId.gripper: + buttons = ; + break; + case CameraViewId.overhead: + buttons = hasBetaTeleopKit ? ( + + ) : ( + + ); + break; + case CameraViewId.realsense: + buttons = ; + break; + default: + throw Error(`unknow video stream id: ${props.definition.id}`); + } + return buttons; +}; /** * Buttons to display under the overhead video stream. */ -const UnderOverheadButtons = (props: {definition: FixedOverheadVideoStreamDef, setPredictiveDisplay: (enabled: boolean) => void}) => { - const [rerender, setRerender] = React.useState(false); - - return ( - - { - if (!props.definition.predictiveDisplay) { - props.definition.predictiveDisplay = true - props.setPredictiveDisplay(true) - } else { - props.definition.predictiveDisplay = false - props.setPredictiveDisplay(false) - } - setRerender(!rerender); - }} - label="Predictive Display" - /> - - ) -} +const UnderOverheadButtons = (props: { + definition: FixedOverheadVideoStreamDef; + setPredictiveDisplay: (enabled: boolean) => void; +}) => { + const [rerender, setRerender] = React.useState(false); + + return ( + + { + if (!props.definition.predictiveDisplay) { + props.definition.predictiveDisplay = true; + props.setPredictiveDisplay(true); + } else { + props.definition.predictiveDisplay = false; + props.setPredictiveDisplay(false); + } + setRerender(!rerender); + }} + label="Predictive Display" + /> + + ); +}; /** * Buttons to display under the adjustable overhead video stream. */ - const UnderAdjustableOverheadButtons = (props: {definition: AdjustableOverheadVideoStreamDef, setPredictiveDisplay: (enabled: boolean) => void}) => { - const [rerender, setRerender] = React.useState(false); - - return ( - - { - underVideoFunctionProvider.provideFunctions(realsenseButtons[idx]).onClick!(); - }} - /> - { - props.definition.followGripper = !props.definition.followGripper; - setRerender(!rerender); - underVideoFunctionProvider.provideFunctions(UnderVideoButton.FollowGripper).onCheck!(props.definition.followGripper) - }} - label="Follow Gripper" - /> - { - if (!props.definition.predictiveDisplay) { - underVideoFunctionProvider.provideFunctions(UnderVideoButton.LookAtBase).onClick!(); - props.setPredictiveDisplay(true) - props.definition.predictiveDisplay = true - } else { - props.setPredictiveDisplay(false) - props.definition.predictiveDisplay = false - } - setRerender(!rerender); - }} - label="Predictive Display" - /> - - ) -} +const UnderAdjustableOverheadButtons = (props: { + definition: AdjustableOverheadVideoStreamDef; + setPredictiveDisplay: (enabled: boolean) => void; +}) => { + const [rerender, setRerender] = React.useState(false); + + return ( + + { + underVideoFunctionProvider.provideFunctions(realsenseButtons[idx]) + .onClick!(); + }} + /> + { + props.definition.followGripper = !props.definition.followGripper; + setRerender(!rerender); + underVideoFunctionProvider.provideFunctions( + UnderVideoButton.FollowGripper, + ).onCheck!(props.definition.followGripper); + }} + label="Follow Gripper" + /> + { + if (!props.definition.predictiveDisplay) { + underVideoFunctionProvider.provideFunctions( + UnderVideoButton.LookAtBase, + ).onClick!(); + props.setPredictiveDisplay(true); + props.definition.predictiveDisplay = true; + } else { + props.setPredictiveDisplay(false); + props.definition.predictiveDisplay = false; + } + setRerender(!rerender); + }} + label="Predictive Display" + /> + + ); +}; /** * Buttons to display under the Realsense video stream. */ -const UnderRealsenseButtons = (props: {definition: RealsenseVideoStreamDef}) => { - const [rerender, setRerender] = React.useState(false); - const [selectedIdx, setSelectedIdx] = React.useState(); - // const [markers, setMarkers] = React.useState(['light_switch']) - - return ( - - { - underVideoFunctionProvider.provideFunctions(realsenseButtons[idx]).onClick!(); - }} - /> - { - props.definition.followGripper = !props.definition.followGripper; - setRerender(!rerender); - underVideoFunctionProvider.provideFunctions(UnderVideoButton.FollowGripper).onCheck!(props.definition.followGripper) - }} - label="Follow Gripper" - /> - { - props.definition.depthSensing = !props.definition.depthSensing; - setRerender(!rerender); - underVideoFunctionProvider.provideFunctions(UnderVideoButton.DepthSensing).onCheck!(props.definition.depthSensing) - }} - label="Depth Sensing" - /> - {/* { + const [rerender, setRerender] = React.useState(false); + const [selectedIdx, setSelectedIdx] = React.useState(); + // const [markers, setMarkers] = React.useState(['light_switch']) + + return ( + + { + underVideoFunctionProvider.provideFunctions(realsenseButtons[idx]) + .onClick!(); + }} + /> + { + props.definition.followGripper = !props.definition.followGripper; + setRerender(!rerender); + underVideoFunctionProvider.provideFunctions( + UnderVideoButton.FollowGripper, + ).onCheck!(props.definition.followGripper); + }} + label="Follow Gripper" + /> + { + props.definition.depthSensing = !props.definition.depthSensing; + setRerender(!rerender); + underVideoFunctionProvider.provideFunctions( + UnderVideoButton.DepthSensing, + ).onCheck!(props.definition.depthSensing); + }} + label="Depth Sensing" + /> + {/* { props.definition.arucoMarkers = !props.definition.arucoMarkers; @@ -637,7 +760,7 @@ const UnderRealsenseButtons = (props: {definition: RealsenseVideoStreamDef}) => }} label="Aruco Markers" /> */} - {/* } } }> - Play + Play play_circle */} - - ) -} + + ); +}; /** * Buttons to display under the overhead video stream. */ - const UnderGripperButtons = (props: {definition: GripperVideoStreamDef}) => { - return ( - - { - underVideoFunctionProvider.provideFunctions(wristButtons[idx]).onClick!(); - }} - /> - - ) -} +const UnderGripperButtons = (props: { definition: GripperVideoStreamDef }) => { + return ( + + { + underVideoFunctionProvider.provideFunctions(wristButtons[idx]) + .onClick!(); + }} + /> + + ); +}; /** * Button to change the camera perspective for a given video stream. */ const CameraPerspectiveButton = (props: { - /** - * When the button is clicked, the corresponding video stream should change - * to this perspective. - */ - perspective: OverheadButtons | RealsenseButtons + /** + * When the button is clicked, the corresponding video stream should change + * to this perspective. + */ + perspective: OverheadButtons | RealsenseButtons; }) => { - const onClick = underVideoFunctionProvider.provideFunctions(props.perspective).onClick; - return ( - - ) -} \ No newline at end of file + const onClick = underVideoFunctionProvider.provideFunctions( + props.perspective, + ).onClick; + return ; +}; diff --git a/src/pages/operator/tsx/layout_components/ComponentList.tsx b/src/pages/operator/tsx/layout_components/ComponentList.tsx index 8d816002..d6fbceb3 100644 --- a/src/pages/operator/tsx/layout_components/ComponentList.tsx +++ b/src/pages/operator/tsx/layout_components/ComponentList.tsx @@ -1,58 +1,70 @@ import React from "react"; -import { CustomizableComponent, CustomizableComponentProps, SharedState } from "./CustomizableComponent"; +import { + CustomizableComponent, + CustomizableComponentProps, + SharedState, +} from "./CustomizableComponent"; import { DropZone } from "./DropZone"; -import { ParentComponentDefinition, ComponentDefinition, ComponentType } from "../utils/component_definitions"; +import { + ParentComponentDefinition, + ComponentDefinition, + ComponentType, +} from "../utils/component_definitions"; /** Properties for {@link ComponentList} */ export type ComponentListProps = { - /** Path of the container element (e.g. the path to the tabs structure rendering) */ - path: string, - sharedState: SharedState, - definition: ParentComponentDefinition -} + /** Path of the container element (e.g. the path to the tabs structure rendering) */ + path: string; + sharedState: SharedState; + definition: ParentComponentDefinition; +}; /** * Creates a {@link CustomizableComponent}s list with {@link DropZone}s between. - * + * * @param props {@link ComponentListProps} */ export const ComponentList = (props: ComponentListProps) => { - const { path } = props; - const components = props.definition.children; - return ( - <> - {components.map((compDef: ComponentDefinition, index: number) => { - const curPath = (path ? path + "-" : "") + `${index}`; - const cProps: CustomizableComponentProps = { - definition: compDef, - path: curPath, - sharedState: props.sharedState - } - const { type } = compDef - return ( - - { type !== ComponentType.RunStopButton && type !== ComponentType.BatteryGuage ? - - : - <> - } - - - ); - })} - { components.length > 0 && components[components.length -1].type !== ComponentType.BatteryGuage || components.length === 0 ? - - : - <> - } - - ) -} + const { path } = props; + const components = props.definition.children; + return ( + <> + {components.map((compDef: ComponentDefinition, index: number) => { + const curPath = (path ? path + "-" : "") + `${index}`; + const cProps: CustomizableComponentProps = { + definition: compDef, + path: curPath, + sharedState: props.sharedState, + }; + const { type } = compDef; + return ( + + {type !== ComponentType.RunStopButton && + type !== ComponentType.BatteryGuage ? ( + + ) : ( + <> + )} + + + ); + })} + {(components.length > 0 && + components[components.length - 1].type !== + ComponentType.BatteryGuage) || + components.length === 0 ? ( + + ) : ( + <> + )} + + ); +}; diff --git a/src/pages/operator/tsx/layout_components/CustomizableComponent.tsx b/src/pages/operator/tsx/layout_components/CustomizableComponent.tsx index 8ff3aa86..b75da919 100644 --- a/src/pages/operator/tsx/layout_components/CustomizableComponent.tsx +++ b/src/pages/operator/tsx/layout_components/CustomizableComponent.tsx @@ -1,5 +1,8 @@ import React from "react"; -import { ComponentDefinition, ComponentType } from "../utils/component_definitions"; +import { + ComponentDefinition, + ComponentType, +} from "../utils/component_definitions"; import { DropZoneState } from "./DropZone"; import { Panel } from "./Panel"; import { RemoteStream } from "shared/util"; @@ -15,81 +18,83 @@ import { BatteryGuage } from "../static_components/BatteryGauge"; /** State required for all elements */ export type SharedState = { - customizing: boolean, - /** Called when user clicks on a component */ - onSelect: (def: ComponentDefinition, path?: string) => void, - /** Remote robot video streams */ - remoteStreams: Map - /** State required for all dropzones */ - dropZoneState: DropZoneState, - /** Path to the active component */ - selectedPath?: string, - /** Mapping of each button pad function to a {@link ButtonState} */ - buttonStateMap?: ButtonStateMap, - /** Whether or not to hide the button labels */ - hideLabels?: boolean, - /** Whether or not the beta teleop cameras are being used */ - hasBetaTeleopKit: boolean + customizing: boolean; + /** Called when user clicks on a component */ + onSelect: (def: ComponentDefinition, path?: string) => void; + /** Remote robot video streams */ + remoteStreams: Map; + /** State required for all dropzones */ + dropZoneState: DropZoneState; + /** Path to the active component */ + selectedPath?: string; + /** Mapping of each button pad function to a {@link ButtonState} */ + buttonStateMap?: ButtonStateMap; + /** Whether or not to hide the button labels */ + hideLabels?: boolean; + /** Whether or not the beta teleop cameras are being used */ + hasBetaTeleopKit: boolean; }; /** Properties for any of the customizable components: tabs, video streams, or * button pads. */ export type CustomizableComponentProps = { - /** - * Path to the component - * @example "0-2" would represent the 2nd child of the 0th element in the layout - */ - path: string, - /** - * Definition of the component (all the info required to know that type - * of component to render - */ - definition: ComponentDefinition; - /** see {@link SharedState} */ - sharedState: SharedState, -} + /** + * Path to the component + * @example "0-2" would represent the 2nd child of the 0th element in the layout + */ + path: string; + /** + * Definition of the component (all the info required to know that type + * of component to render + */ + definition: ComponentDefinition; + /** see {@link SharedState} */ + sharedState: SharedState; +}; /** * Takes a definition for a component and returns the react component. - * + * * @note switch on the component definition's `type` field * @returns rendered component */ export const CustomizableComponent = (props: CustomizableComponentProps) => { - if (!props.definition.type) { - throw new Error(`Component at ${props.path} is missing type`); - } + if (!props.definition.type) { + throw new Error(`Component at ${props.path} is missing type`); + } - // switch on the component type to render specific type of component - switch (props.definition.type) { - case ComponentType.Panel: - return - case ComponentType.ButtonPad: - return ; - case ComponentType.CameraView: - return ; - case ComponentType.PredictiveDisplay: - return ; - case ComponentType.ButtonGrid: - return ; - case ComponentType.VirtualJoystick: - return ; - case ComponentType.Map: - return ; - case ComponentType.RunStopButton: - return ; - case ComponentType.BatteryGuage: - return ; - default: - throw Error(`CustomizableComponent cannot render component of unknown type: ${props.definition.type}\nYou may need to add a case for this component in the switch statement in CustomizableComponent.`); - } -} + // switch on the component type to render specific type of component + switch (props.definition.type) { + case ComponentType.Panel: + return ; + case ComponentType.ButtonPad: + return ; + case ComponentType.CameraView: + return ; + case ComponentType.PredictiveDisplay: + return ; + case ComponentType.ButtonGrid: + return ; + case ComponentType.VirtualJoystick: + return ; + case ComponentType.Map: + return ; + case ComponentType.RunStopButton: + return ; + case ComponentType.BatteryGuage: + return ; + default: + throw Error( + `CustomizableComponent cannot render component of unknown type: ${props.definition.type}\nYou may need to add a case for this component in the switch statement in CustomizableComponent.`, + ); + } +}; /** * Checks if the component is currently selected * @returns true if selected, otherwise false */ export function isSelected(props: CustomizableComponentProps): boolean { - return props.path === props.sharedState.selectedPath; -} \ No newline at end of file + return props.path === props.sharedState.selectedPath; +} diff --git a/src/pages/operator/tsx/layout_components/DropZone.tsx b/src/pages/operator/tsx/layout_components/DropZone.tsx index 96328d6f..64aeb32d 100644 --- a/src/pages/operator/tsx/layout_components/DropZone.tsx +++ b/src/pages/operator/tsx/layout_components/DropZone.tsx @@ -1,237 +1,285 @@ import React from "react"; -import { ComponentDefinition, ComponentType, TabDefinition, PanelDefinition, ParentComponentDefinition, LayoutDefinition } from "../utils/component_definitions"; +import { + ComponentDefinition, + ComponentType, + TabDefinition, + PanelDefinition, + ParentComponentDefinition, + LayoutDefinition, +} from "../utils/component_definitions"; import { SharedState } from "./CustomizableComponent"; import { className } from "shared/util"; import { PopupModal } from "../basic_components/PopupModal"; -import "operator/css/DropZone.css" +import "operator/css/DropZone.css"; /** State required for drop zones */ export type DropZoneState = { - /** - * Callback when the DropZone is clicked to handle dropping the active - * selected component into the position of the drop zone - * - * @param path path to the drop zone - */ - onDrop: (path: string) => void, - /** Definition of the active selected component */ - selectedDefinition?: ComponentDefinition -} + /** + * Callback when the DropZone is clicked to handle dropping the active + * selected component into the position of the drop zone + * + * @param path path to the drop zone + */ + onDrop: (path: string) => void; + /** Definition of the active selected component */ + selectedDefinition?: ComponentDefinition; +}; /** Properties for {@link DropZone} */ export type DropZoneProps = { - /** Path to the drop zone, same as CustomizableComponent path */ - path: string, - /** Definition of the CustomizableComponent containing this drop zone */ - parentDef: ParentComponentDefinition, - /** State shared between all components */ - sharedState: SharedState -} + /** Path to the drop zone, same as CustomizableComponent path */ + path: string; + /** Definition of the CustomizableComponent containing this drop zone */ + parentDef: ParentComponentDefinition; + /** State shared between all components */ + sharedState: SharedState; +}; /** * Area where the user can click to move the selected component to a new place. - * + * * @param props {@link DropZoneProps} */ export const DropZone = (props: DropZoneProps) => { - const [showNewPanelModal, setShowNewPanelModal] = React.useState(false); - - function canDrop(): boolean { - const { path, parentDef } = props; - const { selectedPath } = props.sharedState; - const { selectedDefinition } = props.sharedState.dropZoneState; - - // If no active object selected - if (!selectedDefinition) { - return false; - } - - // Must pass drop zone rules about which components can go where - if (!dropzoneRules(selectedDefinition.type, parentDef.type)) return false; - - // Don't need to check if dropzone is adjacent if the active component - // is coming from the sidebar component provider - if (!selectedPath) return true; - - // If Layout Grid only has one child panel and the panel is selected, hide dropzones to the - // left and right of the panel as these dropzones would not move the active panel - if (parentDef.type == ComponentType.Layout && selectedDefinition.type == ComponentType.Panel) { - let selectedLayoutGridIdx = Number(selectedPath?.split('-')[0]) - let pathLayoutGridIdx = Number(path.split('-')[0]) - - if (pathLayoutGridIdx === selectedLayoutGridIdx || pathLayoutGridIdx === selectedLayoutGridIdx + 1) { - let parentIdx = Number(selectedPath?.split('-')[0]) - let def = parentDef as LayoutDefinition - if (def.children[parentIdx]?.children.length < 2) return false; - } - } - - // Can't drop if dropzone is right next to the active element - // (that wouldn't move the active element at all) - return !pathsAdjacent(selectedPath, path); - } + const [showNewPanelModal, setShowNewPanelModal] = + React.useState(false); - /** Calls onDrop function from Operator with the path of this dropzone */ - function handleClick(e: React.MouseEvent) { - if (!props.sharedState.customizing) return; - e.stopPropagation(); - // If adding a new tabs component from the sidebar - if (props.sharedState.dropZoneState.selectedDefinition?.type === ComponentType.Panel && - props.sharedState.selectedPath === undefined) { - setShowNewPanelModal(true); - return; - } - props.sharedState.dropZoneState.onDrop(props.path); + function canDrop(): boolean { + const { path, parentDef } = props; + const { selectedPath } = props.sharedState; + const { selectedDefinition } = props.sharedState.dropZoneState; + + // If no active object selected + if (!selectedDefinition) { + return false; } - /** - * When adding a panel from the sidebar (so it's not already in the interface), - * this adds a new tab child and drops the new panel into the drop zone. - * - * @param newTabName the name of the tab child within the new panel - */ - function createNewPanel(newTabName: string) { - if (props.sharedState.dropZoneState.selectedDefinition?.type !== ComponentType.Panel) - throw Error(`Should only call createNewPanel() when the active selected component is of type Panel`); - - const def = props.sharedState.dropZoneState.selectedDefinition as PanelDefinition; - - if (def.children.length > 1) - throw Error(`createNewPanel() called with active panel definition that already has children: ${def.children}`) - - if (props.sharedState.selectedPath !== undefined) - throw Error(`Called createNewPanel() when active selected path was not undefined ${props.sharedState.selectedPath}`); - - // Create a child tab and add it to the Panel's children - def.children.push({ - type: ComponentType.SingleTab, - label: newTabName, - children: [] - } as TabDefinition) - - // Drop the new panel into the drop zone - props.sharedState.dropZoneState.onDrop(props.path); + // Must pass drop zone rules about which components can go where + if (!dropzoneRules(selectedDefinition.type, parentDef.type)) return false; + + // Don't need to check if dropzone is adjacent if the active component + // is coming from the sidebar component provider + if (!selectedPath) return true; + + // If Layout Grid only has one child panel and the panel is selected, hide dropzones to the + // left and right of the panel as these dropzones would not move the active panel + if ( + parentDef.type == ComponentType.Layout && + selectedDefinition.type == ComponentType.Panel + ) { + let selectedLayoutGridIdx = Number(selectedPath?.split("-")[0]); + let pathLayoutGridIdx = Number(path.split("-")[0]); + + if ( + pathLayoutGridIdx === selectedLayoutGridIdx || + pathLayoutGridIdx === selectedLayoutGridIdx + 1 + ) { + let parentIdx = Number(selectedPath?.split("-")[0]); + let def = parentDef as LayoutDefinition; + if (def.children[parentIdx]?.children.length < 2) return false; + } } - const isActive = props.sharedState.customizing && canDrop(); - const inTab = props.parentDef.type === ComponentType.Panel; - const overlay = props.parentDef.type === ComponentType.CameraView; - const standard = !(inTab || overlay); - - return ( - - - - + // Can't drop if dropzone is right next to the active element + // (that wouldn't move the active element at all) + return !pathsAdjacent(selectedPath, path); + } + + /** Calls onDrop function from Operator with the path of this dropzone */ + function handleClick(e: React.MouseEvent) { + if (!props.sharedState.customizing) return; + e.stopPropagation(); + // If adding a new tabs component from the sidebar + if ( + props.sharedState.dropZoneState.selectedDefinition?.type === + ComponentType.Panel && + props.sharedState.selectedPath === undefined + ) { + setShowNewPanelModal(true); + return; + } + props.sharedState.dropZoneState.onDrop(props.path); + } + /** + * When adding a panel from the sidebar (so it's not already in the interface), + * this adds a new tab child and drops the new panel into the drop zone. + * + * @param newTabName the name of the tab child within the new panel + */ + function createNewPanel(newTabName: string) { + if ( + props.sharedState.dropZoneState.selectedDefinition?.type !== + ComponentType.Panel ) -} + throw Error( + `Should only call createNewPanel() when the active selected component is of type Panel`, + ); + + const def = props.sharedState.dropZoneState + .selectedDefinition as PanelDefinition; + + if (def.children.length > 1) + throw Error( + `createNewPanel() called with active panel definition that already has children: ${def.children}`, + ); + + if (props.sharedState.selectedPath !== undefined) + throw Error( + `Called createNewPanel() when active selected path was not undefined ${props.sharedState.selectedPath}`, + ); + + // Create a child tab and add it to the Panel's children + def.children.push({ + type: ComponentType.SingleTab, + label: newTabName, + children: [], + } as TabDefinition); + + // Drop the new panel into the drop zone + props.sharedState.dropZoneState.onDrop(props.path); + } + + const isActive = props.sharedState.customizing && canDrop(); + const inTab = props.parentDef.type === ComponentType.Panel; + const overlay = props.parentDef.type === ComponentType.CameraView; + const standard = !(inTab || overlay); + + return ( + + + + + ); +}; /** Popup to name the first tab when a new panel component is added to the interface. */ const NewPanelModal = (props: { - /** If the modal should be shown. */ - show: boolean, - /** Callback to change the state of `show` */ - setShow: (show: boolean) => void, - /** - * Callback to add the panel (drop into the dropzone). - * @param tabName the name of the tab child in the new panel - */ - addPanel: (tabName: string) => void + /** If the modal should be shown. */ + show: boolean; + /** Callback to change the state of `show` */ + setShow: (show: boolean) => void; + /** + * Callback to add the panel (drop into the dropzone). + * @param tabName the name of the tab child in the new panel + */ + addPanel: (tabName: string) => void; }) => { - const [text, setText] = React.useState(""); - /** Call `addPanel` with the text in the text entry. */ - function handleAccept() { - if (text.length > 0) props.addPanel(text); - } - /** Update the text entry when the user types in it. */ - function handleChange(e: React.ChangeEvent) { setText(e.target.value); } - return ( - - - - - ); -} + const [text, setText] = React.useState(""); + /** Call `addPanel` with the text in the text entry. */ + function handleAccept() { + if (text.length > 0) props.addPanel(text); + } + /** Update the text entry when the user types in it. */ + function handleChange(e: React.ChangeEvent) { + setText(e.target.value); + } + return ( + + + + + ); +}; /** * Checks if the active component is allowed to be placed in this drop zone. - * + * * @param active type of the active component the user selected * @param parent type of the parent component containing this drop zone * @returns true if the active component is allowed to be dropped, false otherwise */ function dropzoneRules(active: ComponentType, parent: ComponentType) { - // Tabs can only go into layout - if (active === ComponentType.Panel && parent !== ComponentType.LayoutGrid && parent !== ComponentType.Layout) - return false; + // Tabs can only go into layout + if ( + active === ComponentType.Panel && + parent !== ComponentType.LayoutGrid && + parent !== ComponentType.Layout + ) + return false; - // Single tab can only go into tabs - if (active === ComponentType.SingleTab && parent !== ComponentType.Panel) - return false; + // Single tab can only go into tabs + if (active === ComponentType.SingleTab && parent !== ComponentType.Panel) + return false; - // Only tabs can go into panel - if (active !== ComponentType.Panel && (parent === ComponentType.LayoutGrid || parent == ComponentType.Layout)) - return false; + // Only tabs can go into panel + if ( + active !== ComponentType.Panel && + (parent === ComponentType.LayoutGrid || parent == ComponentType.Layout) + ) + return false; - // Only single tab can go into tabs - if (active !== ComponentType.SingleTab && parent === ComponentType.Panel) - return false; + // Only single tab can go into tabs + if (active !== ComponentType.SingleTab && parent === ComponentType.Panel) + return false; - // Only button pad can go into video stream - if (active !== ComponentType.ButtonPad && parent === ComponentType.CameraView) - return false; + // Only button pad can go into video stream + if (active !== ComponentType.ButtonPad && parent === ComponentType.CameraView) + return false; - return true; + return true; } /** * Checks if two paths are adjacent to one another. - * - * @note this prevents displaying drop zones directly adjacent to the selected + * + * @note this prevents displaying drop zones directly adjacent to the selected * component, which would have no effect on the components position. - * + * * @param selectedPath path to the active element * @param path path to this drop zone * @returns true if the paths are directly adjacent, false otherwise */ function pathsAdjacent(selectedPath: string, path: string) { - // Check paths same length - const splitActivePath = selectedPath.split('-'); - const splitSelfPath = path.split('-'); - const sameLength = splitActivePath.length == splitSelfPath.length; - if (!sameLength) return false; - - // Should have same parent - const activePrefix = splitActivePath.slice(0, -1); - const selfPrefix = splitSelfPath.slice(0, -1); - const matchingPrefix = activePrefix.every((val, index) => val === selfPrefix[index]); - if (!matchingPrefix) return false; - - // Check if last indicies are adjacent - const activeLast = +splitActivePath.slice(-1)[0]; - const selfLast = +splitSelfPath.slice(-1)[0]; - const adjacent = activeLast == selfLast || activeLast + 1 == selfLast; - - return adjacent; -} \ No newline at end of file + // Check paths same length + const splitActivePath = selectedPath.split("-"); + const splitSelfPath = path.split("-"); + const sameLength = splitActivePath.length == splitSelfPath.length; + if (!sameLength) return false; + + // Should have same parent + const activePrefix = splitActivePath.slice(0, -1); + const selfPrefix = splitSelfPath.slice(0, -1); + const matchingPrefix = activePrefix.every( + (val, index) => val === selfPrefix[index], + ); + if (!matchingPrefix) return false; + + // Check if last indices are adjacent + const activeLast = +splitActivePath.slice(-1)[0]; + const selfLast = +splitSelfPath.slice(-1)[0]; + const adjacent = activeLast == selfLast || activeLast + 1 == selfLast; + + return adjacent; +} diff --git a/src/pages/operator/tsx/layout_components/Map.tsx b/src/pages/operator/tsx/layout_components/Map.tsx index 248130d8..9c10f407 100644 --- a/src/pages/operator/tsx/layout_components/Map.tsx +++ b/src/pages/operator/tsx/layout_components/Map.tsx @@ -1,12 +1,21 @@ import React, { useEffect } from "react"; -import 'latest-createjs'; -import { CustomizableComponentProps, isSelected } from "./CustomizableComponent"; +import "latest-createjs"; +import { + CustomizableComponentProps, + isSelected, +} from "./CustomizableComponent"; import { MapDefinition } from "../utils/component_definitions"; import { mapFunctionProvider, occupancyGrid } from "operator/tsx/index"; -import "operator/css/Map.css" +import "operator/css/Map.css"; import { Canvas } from "../static_components/Canvas"; import { OccupancyGrid } from "../static_components/OccupancyGrid"; -import { AMCLPose, ROSOccupancyGrid, ROSPose, className, waitUntil } from "shared/util"; +import { + AMCLPose, + ROSOccupancyGrid, + ROSPose, + className, + waitUntil, +} from "shared/util"; import ROSLIB from "roslib"; import { UnderMapButton } from "../function_providers/UnderMapFunctionProvider"; import { underMapFunctionProvider } from "operator/tsx/index"; @@ -14,333 +23,430 @@ import { CheckToggleButton } from "../basic_components/CheckToggleButton"; import { useState } from "react"; import { Dropdown } from "../basic_components/Dropdown"; import { PopupModal } from "../basic_components/PopupModal"; -import { Tooltip } from "operator/tsx/static_components/Tooltip" +import { Tooltip } from "operator/tsx/static_components/Tooltip"; import { isMobile } from "react-device-detect"; import { RadioFunctions, RadioGroup } from "../basic_components/RadioGroup"; export enum MapFunction { - GetMap, - GetPose, - MoveBase, - GoalReached, + GetMap, + GetPose, + MoveBase, + GoalReached, } export interface MapFunctions { - GetMap: ROSOccupancyGrid - GetPose: () => ROSLIB.Transform - MoveBase: (pose: ROSPose) => void - GoalReached: () => boolean - SelectGoal: () => boolean, - SetSelectGoal: (selectGoal: boolean) => void + GetMap: ROSOccupancyGrid; + GetPose: () => ROSLIB.Transform; + MoveBase: (pose: ROSPose) => void; + GoalReached: () => boolean; + SelectGoal: () => boolean; + SetSelectGoal: (selectGoal: boolean) => void; } export interface UnderMapFunctions { - SelectGoal: (toggle: boolean) => void - CancelGoal: () => void - DeleteGoal: (goalId: number) => void - SaveGoal: (name: string) => void - LoadGoal: (goalID: number) => void - GetPose: () => ROSLIB.Transform - GetSavedPoseNames: () => string[] - GetSavedPoseTypes: () => string[] - GetSavedPoses: () => ROSLIB.Transform[] - DisplayPoseMarkers: (toggle: boolean, poses: ROSLIB.Transform[], poseNames: string[], poseTypes: string[]) => void - DisplayGoalMarker: (pose: ROSLIB.Vector3) => void, - NavigateToAruco: (goalID: number) => void, - Play: () => void, - RemoveGoalMarker: () => void, - GoalReached: () => Promise + SelectGoal: (toggle: boolean) => void; + CancelGoal: () => void; + DeleteGoal: (goalId: number) => void; + SaveGoal: (name: string) => void; + LoadGoal: (goalID: number) => void; + GetPose: () => ROSLIB.Transform; + GetSavedPoseNames: () => string[]; + GetSavedPoseTypes: () => string[]; + GetSavedPoses: () => ROSLIB.Transform[]; + DisplayPoseMarkers: ( + toggle: boolean, + poses: ROSLIB.Transform[], + poseNames: string[], + poseTypes: string[], + ) => void; + DisplayGoalMarker: (pose: ROSLIB.Vector3) => void; + NavigateToAruco: (goalID: number) => void; + Play: () => void; + RemoveGoalMarker: () => void; + GoalReached: () => Promise; } export const Map = (props: CustomizableComponentProps) => { - const definition = props.definition as MapDefinition - const [active, setActive] = React.useState(false); - const [occupancyGrid, setOccupanyGrid] = React.useState() - const [selectGoal, setSelectGoal] = React.useState(false) - const { customizing, hideLabels } = props.sharedState; - const selected = isSelected(props); - - // Constrain the width or height when the stream gets too large - React.useEffect(() => { - let map = mapFn.GetMap - let width = map ? map.info.width : 60 - let height = map ? map.info.height : 100 - var canvas = new Canvas({ - divID: 'map', - className: 'mapCanvas', - width: width * 5, // Scale width to avoid blurriness when making map larger - height: height * 5 // Scale height to avoid blurriness when making map larger - }); - var occupancyGridMap = new OccupancyGrid({ - functs: mapFn, - rootObject: canvas.scene! - }) - canvas.scaleToDimensions(occupancyGridMap.width, occupancyGridMap.height); - setOccupanyGrid(occupancyGridMap) - }, []); + const definition = props.definition as MapDefinition; + const [active, setActive] = React.useState(false); + const [occupancyGrid, setOccupanyGrid] = React.useState(); + const [selectGoal, setSelectGoal] = React.useState(false); + const { customizing, hideLabels } = props.sharedState; + const selected = isSelected(props); - function handleSelect(event: React.MouseEvent) { - event.stopPropagation(); - props.sharedState.onSelect(props.definition, props.path); - } + // Constrain the width or height when the stream gets too large + React.useEffect(() => { + let map = mapFn.GetMap; + let width = map ? map.info.width : 60; + let height = map ? map.info.height : 100; + var canvas = new Canvas({ + divID: "map", + className: "mapCanvas", + width: width * 5, // Scale width to avoid blurriness when making map larger + height: height * 5, // Scale height to avoid blurriness when making map larger + }); + var occupancyGridMap = new OccupancyGrid({ + functs: mapFn, + rootObject: canvas.scene!, + }); + canvas.scaleToDimensions(occupancyGridMap.width, occupancyGridMap.height); + setOccupanyGrid(occupancyGridMap); + }, []); - const handleSelectGoal = React.useCallback((selectGoal: boolean) => { - setSelectGoal(selectGoal); - mapFn.SelectGoal = (): boolean => { return selectGoal } - }, []); + function handleSelect(event: React.MouseEvent) { + event.stopPropagation(); + props.sharedState.onSelect(props.definition, props.path); + } - let mapFn: MapFunctions = { - GetMap: mapFunctionProvider.provideFunctions(MapFunction.GetMap) as ROSOccupancyGrid, - GetPose: mapFunctionProvider.provideFunctions(MapFunction.GetPose) as () => ROSLIB.Transform, - MoveBase: mapFunctionProvider.provideFunctions(MapFunction.MoveBase) as (pose: ROSPose) => void, - GoalReached: mapFunctionProvider.provideFunctions(MapFunction.GoalReached) as () => boolean, - SelectGoal: (): boolean => { return selectGoal }, - SetSelectGoal: (selectGoal: boolean) => {handleSelectGoal(selectGoal)}, - } + const handleSelectGoal = React.useCallback((selectGoal: boolean) => { + setSelectGoal(selectGoal); + mapFn.SelectGoal = (): boolean => { + return selectGoal; + }; + }, []); - let underMapFn: UnderMapFunctions = { - SelectGoal: underMapFunctionProvider.provideFunctions(UnderMapButton.SelectGoal) as () => void, - CancelGoal: underMapFunctionProvider.provideFunctions(UnderMapButton.CancelGoal) as () => void, - DeleteGoal: underMapFunctionProvider.provideFunctions(UnderMapButton.DeleteGoal) as (goalID: number) => void, - SaveGoal: underMapFunctionProvider.provideFunctions(UnderMapButton.SaveGoal) as (name: string) => void, - LoadGoal: underMapFunctionProvider.provideFunctions(UnderMapButton.LoadGoal) as () => void, - GetPose: underMapFunctionProvider.provideFunctions(UnderMapButton.GetPose) as () => ROSLIB.Transform, - GetSavedPoseNames: underMapFunctionProvider.provideFunctions(UnderMapButton.GetSavedPoseNames) as () => string[], - GetSavedPoseTypes: underMapFunctionProvider.provideFunctions(UnderMapButton.GetSavedPoseTypes) as () => string[], - GetSavedPoses: underMapFunctionProvider.provideFunctions(UnderMapButton.GetSavedPoses) as () => ROSLIB.Transform[], - DisplayPoseMarkers: (toggle: boolean, poses: ROSLIB.Transform[], poseNames: string[], poseTypes: string[]) => { - return occupancyGrid!.displayPoseMarkers(toggle, poses, poseNames, poseTypes) - }, - DisplayGoalMarker: (pose: ROSLIB.Vector3) => occupancyGrid!.createGoalMarker(pose.x, pose.y, true), - NavigateToAruco: underMapFunctionProvider.provideFunctions(UnderMapButton.NavigateToAruco) as (goalID: number) => void, - Play: () => occupancyGrid!.play(), - RemoveGoalMarker: () => occupancyGrid!.removeGoalMarker(), - GoalReached: underMapFunctionProvider.provideFunctions(UnderMapButton.GoalReached) as () => Promise - } + let mapFn: MapFunctions = { + GetMap: mapFunctionProvider.provideFunctions( + MapFunction.GetMap, + ) as ROSOccupancyGrid, + GetPose: mapFunctionProvider.provideFunctions( + MapFunction.GetPose, + ) as () => ROSLIB.Transform, + MoveBase: mapFunctionProvider.provideFunctions(MapFunction.MoveBase) as ( + pose: ROSPose, + ) => void, + GoalReached: mapFunctionProvider.provideFunctions( + MapFunction.GoalReached, + ) as () => boolean, + SelectGoal: (): boolean => { + return selectGoal; + }, + SetSelectGoal: (selectGoal: boolean) => { + handleSelectGoal(selectGoal); + }, + }; - return ( - -
    - {!isMobile ?

    Map

    : <>} -
    - {!isMobile && - //
    - - //
    - } -
    - {isMobile && - - } -
    - ) -} + let underMapFn: UnderMapFunctions = { + SelectGoal: underMapFunctionProvider.provideFunctions( + UnderMapButton.SelectGoal, + ) as () => void, + CancelGoal: underMapFunctionProvider.provideFunctions( + UnderMapButton.CancelGoal, + ) as () => void, + DeleteGoal: underMapFunctionProvider.provideFunctions( + UnderMapButton.DeleteGoal, + ) as (goalID: number) => void, + SaveGoal: underMapFunctionProvider.provideFunctions( + UnderMapButton.SaveGoal, + ) as (name: string) => void, + LoadGoal: underMapFunctionProvider.provideFunctions( + UnderMapButton.LoadGoal, + ) as () => void, + GetPose: underMapFunctionProvider.provideFunctions( + UnderMapButton.GetPose, + ) as () => ROSLIB.Transform, + GetSavedPoseNames: underMapFunctionProvider.provideFunctions( + UnderMapButton.GetSavedPoseNames, + ) as () => string[], + GetSavedPoseTypes: underMapFunctionProvider.provideFunctions( + UnderMapButton.GetSavedPoseTypes, + ) as () => string[], + GetSavedPoses: underMapFunctionProvider.provideFunctions( + UnderMapButton.GetSavedPoses, + ) as () => ROSLIB.Transform[], + DisplayPoseMarkers: ( + toggle: boolean, + poses: ROSLIB.Transform[], + poseNames: string[], + poseTypes: string[], + ) => { + return occupancyGrid!.displayPoseMarkers( + toggle, + poses, + poseNames, + poseTypes, + ); + }, + DisplayGoalMarker: (pose: ROSLIB.Vector3) => + occupancyGrid!.createGoalMarker(pose.x, pose.y, true), + NavigateToAruco: underMapFunctionProvider.provideFunctions( + UnderMapButton.NavigateToAruco, + ) as (goalID: number) => void, + Play: () => occupancyGrid!.play(), + RemoveGoalMarker: () => occupancyGrid!.removeGoalMarker(), + GoalReached: underMapFunctionProvider.provideFunctions( + UnderMapButton.GoalReached, + ) as () => Promise, + }; + + return ( + +
    + {!isMobile ?

    Map

    : <>} +
    + { + !isMobile && ( + //
    + + ) + //
    + } +
    + {isMobile && ( + + )} +
    + ); +}; /** * Buttons to display under the map. */ -const UnderMapButtons = (props: { - handleSelectGoal: (selectGoal: boolean) => void, - functs: UnderMapFunctions, - hideLabels?: boolean +const UnderMapButtons = (props: { + handleSelectGoal: (selectGoal: boolean) => void; + functs: UnderMapFunctions; + hideLabels?: boolean; }) => { - const [poses, setPoses] = useState(props.functs.GetSavedPoseNames()) - const [selectedIdx, setSelectedIdx] = React.useState(); - const [selectGoal, setSelectGoal] = React.useState(false) - const [displayGoals, setDisplayGoals] = React.useState(false) - const [showSavePoseModal, setShowSavePoseModal] = useState(false); - const [play, setPlay] = useState(false); + const [poses, setPoses] = useState( + props.functs.GetSavedPoseNames(), + ); + const [selectedIdx, setSelectedIdx] = React.useState(); + const [selectGoal, setSelectGoal] = React.useState(false); + const [displayGoals, setDisplayGoals] = React.useState(false); + const [showSavePoseModal, setShowSavePoseModal] = useState(false); + const [play, setPlay] = useState(false); - let radioFuncts: RadioFunctions = { - Delete: (label: string) => props.functs.DeleteGoal(props.functs.GetSavedPoseNames().indexOf(label)), - GetLabels: () => props.functs.GetSavedPoseNames(), - SelectedLabel: (label: string) => setSelectedIdx(props.functs.GetSavedPoseNames().indexOf(label)) - } + let radioFuncts: RadioFunctions = { + Delete: (label: string) => + props.functs.DeleteGoal(props.functs.GetSavedPoseNames().indexOf(label)), + GetLabels: () => props.functs.GetSavedPoseNames(), + SelectedLabel: (label: string) => + setSelectedIdx(props.functs.GetSavedPoseNames().indexOf(label)), + }; - const SavePoseModal = (props: { - functs: UnderMapFunctions, - setShow: (show: boolean) => void, - show: boolean - }) => { - const [name, setName] = React.useState(""); - function handleAccept() { - if (name.length > 0) { - if (!poses.includes(name)) { - setPoses(poses => [...poses, name]) - } - props.functs.SaveGoal(name) - props.functs.DisplayPoseMarkers( - displayGoals, - props.functs.GetSavedPoses(), - props.functs.GetSavedPoseNames(), - props.functs.GetSavedPoseTypes() - ) - } - setName(""); + const SavePoseModal = (props: { + functs: UnderMapFunctions; + setShow: (show: boolean) => void; + show: boolean; + }) => { + const [name, setName] = React.useState(""); + function handleAccept() { + if (name.length > 0) { + if (!poses.includes(name)) { + setPoses((poses) => [...poses, name]); } + props.functs.SaveGoal(name); + props.functs.DisplayPoseMarkers( + displayGoals, + props.functs.GetSavedPoses(), + props.functs.GetSavedPoseNames(), + props.functs.GetSavedPoseTypes(), + ); + } + setName(""); + } - return ( - - {/* + return ( + + {/*
    */} -
    - {/* */} - setName(e.target.value)} - placeholder="Enter name of destination" - /> -
    -
    - ) - } +
    + {/* */} + setName(e.target.value)} + placeholder="Enter name of destination" + /> +
    +
    + ); + }; - function formatNamesandTypes(names: string[], types: string[]): React.JSX.Element[]{ - let elements: React.JSX.Element[] = [] - names.map((name, index) => { - elements.push(

    {types[index]} {name}

    ) - }) - return elements - } + function formatNamesandTypes( + names: string[], + types: string[], + ): React.JSX.Element[] { + let elements: React.JSX.Element[] = []; + names.map((name, index) => { + elements.push( +

    + {types[index]} {name} +

    , + ); + }); + return elements; + } - return ( !isMobile - ? -
    - { - props.handleSelectGoal(!selectGoal) - setSelectGoal(!selectGoal) - if (selectGoal) props.functs.RemoveGoalMarker() - else radioFuncts.SelectedLabel(undefined) - }} - label="Select Goal" - /> - {!play && } - {play && } - -
    - - -
    - : - -
    -
    { - setShowSavePoseModal(true) - // Disable select goal to stop accidental navigation - if (selectGoal) { - props.handleSelectGoal(false) - setSelectGoal(false) - } - }}> - - - save - -
    - {!play &&
    { - if (!play && selectGoal) { - props.functs.Play() - setPlay(true) - setSelectGoal(false) - props.functs.GoalReached().then(goalReached => setPlay(false)) - } else if (!play && selectedIdx != undefined) { - let pose: ROSLIB.Vector3 = props.functs.LoadGoal(selectedIdx)! - props.functs.DisplayGoalMarker(pose) - props.functs.NavigateToAruco(selectedIdx) - setPlay(true) - setSelectGoal(false) - props.functs.GoalReached().then(goalReached => setPlay(false)) - } - } - }> - Play - play_circle - - -
    } - {play &&
    { - props.functs.CancelGoal() - setPlay(!play) - } - }> - Cancel - cancel - - -
    } - { - props.handleSelectGoal(!selectGoal) - setSelectGoal(!selectGoal) - }} - label="Select Goal" - /> -
    - -
    - ) -} \ No newline at end of file + return !isMobile ? ( + +
    + { + props.handleSelectGoal(!selectGoal); + setSelectGoal(!selectGoal); + if (selectGoal) props.functs.RemoveGoalMarker(); + else radioFuncts.SelectedLabel(undefined); + }} + label="Select Goal" + /> + {!play && ( + + )} + {play && ( + + )} + +
    + + +
    + ) : ( + + +
    +
    { + setShowSavePoseModal(true); + // Disable select goal to stop accidental navigation + if (selectGoal) { + props.handleSelectGoal(false); + setSelectGoal(false); + } + }} + > + + save +
    + {!play && ( +
    { + if (!play && selectGoal) { + props.functs.Play(); + setPlay(true); + setSelectGoal(false); + props.functs + .GoalReached() + .then((goalReached) => setPlay(false)); + } else if (!play && selectedIdx != undefined) { + let pose: ROSLIB.Vector3 = props.functs.LoadGoal(selectedIdx)!; + props.functs.DisplayGoalMarker(pose); + props.functs.NavigateToAruco(selectedIdx); + setPlay(true); + setSelectGoal(false); + props.functs + .GoalReached() + .then((goalReached) => setPlay(false)); + } + }} + > + Play + play_circle +
    + )} + {play && ( +
    { + props.functs.CancelGoal(); + setPlay(!play); + }} + > + Cancel + cancel +
    + )} + { + props.handleSelectGoal(!selectGoal); + setSelectGoal(!selectGoal); + }} + label="Select Goal" + /> +
    + +
    + ); +}; diff --git a/src/pages/operator/tsx/layout_components/MovementRecorder.tsx b/src/pages/operator/tsx/layout_components/MovementRecorder.tsx index c810fd35..5bb5c6bf 100644 --- a/src/pages/operator/tsx/layout_components/MovementRecorder.tsx +++ b/src/pages/operator/tsx/layout_components/MovementRecorder.tsx @@ -3,182 +3,219 @@ import { PopupModal } from "../basic_components/PopupModal"; import { movementRecorderFunctionProvider } from "operator/tsx/index"; import { Dropdown } from "../basic_components/Dropdown"; import { Tooltip } from "../static_components/Tooltip"; -import "operator/css/MovementRecorder.css" -import "operator/css/basic_components.css" +import "operator/css/MovementRecorder.css"; +import "operator/css/basic_components.css"; import { isMobile } from "react-device-detect"; import { RadioFunctions, RadioGroup } from "../basic_components/RadioGroup"; /** All the possible button functions */ export enum MovementRecorderFunction { - Record, - SaveRecording, - StopRecording, - SavedRecordingNames, - DeleteRecording, - LoadRecording, - Cancel, - DeleteRecordingName, - LoadRecordingName + Record, + SaveRecording, + StopRecording, + SavedRecordingNames, + DeleteRecording, + LoadRecording, + Cancel, + DeleteRecordingName, + LoadRecordingName, } export interface MovementRecorderFunctions { - Record: () => void - SaveRecording: (name: string) => void, - StopRecording: () => void, - SavedRecordingNames: () => string[], - DeleteRecording: (recordingID: number) => void, - LoadRecording: (recordingID: number) => void + Record: () => void; + SaveRecording: (name: string) => void; + StopRecording: () => void; + SavedRecordingNames: () => string[]; + DeleteRecording: (recordingID: number) => void; + LoadRecording: (recordingID: number) => void; } -export const MovementRecorder = (props: { - hideLabels: boolean, - globalRecord?: boolean, - isRecording?: boolean, +export const MovementRecorder = (props: { + hideLabels: boolean; + globalRecord?: boolean; + isRecording?: boolean; }) => { - let functions: MovementRecorderFunctions = { - Record: movementRecorderFunctionProvider.provideFunctions(MovementRecorderFunction.Record) as () => void, - SaveRecording: movementRecorderFunctionProvider.provideFunctions(MovementRecorderFunction.SaveRecording) as (name: string) => void, - StopRecording: movementRecorderFunctionProvider.provideFunctions(MovementRecorderFunction.StopRecording) as () => void, - SavedRecordingNames: movementRecorderFunctionProvider.provideFunctions(MovementRecorderFunction.SavedRecordingNames) as () => string[], - DeleteRecording: movementRecorderFunctionProvider.provideFunctions(MovementRecorderFunction.DeleteRecording) as (recordingID: number) => void, - LoadRecording: movementRecorderFunctionProvider.provideFunctions(MovementRecorderFunction.LoadRecording) as (recordingID: number) => void - } + let functions: MovementRecorderFunctions = { + Record: movementRecorderFunctionProvider.provideFunctions( + MovementRecorderFunction.Record, + ) as () => void, + SaveRecording: movementRecorderFunctionProvider.provideFunctions( + MovementRecorderFunction.SaveRecording, + ) as (name: string) => void, + StopRecording: movementRecorderFunctionProvider.provideFunctions( + MovementRecorderFunction.StopRecording, + ) as () => void, + SavedRecordingNames: movementRecorderFunctionProvider.provideFunctions( + MovementRecorderFunction.SavedRecordingNames, + ) as () => string[], + DeleteRecording: movementRecorderFunctionProvider.provideFunctions( + MovementRecorderFunction.DeleteRecording, + ) as (recordingID: number) => void, + LoadRecording: movementRecorderFunctionProvider.provideFunctions( + MovementRecorderFunction.LoadRecording, + ) as (recordingID: number) => void, + }; - let radioFuncts: RadioFunctions = { - Delete: movementRecorderFunctionProvider.provideFunctions(MovementRecorderFunction.DeleteRecordingName) as (name: string) => void, - GetLabels: functions.SavedRecordingNames, - SelectedLabel: (label: string) => setSelectedIdx(functions.SavedRecordingNames().indexOf(label)) - } + let radioFuncts: RadioFunctions = { + Delete: movementRecorderFunctionProvider.provideFunctions( + MovementRecorderFunction.DeleteRecordingName, + ) as (name: string) => void, + GetLabels: functions.SavedRecordingNames, + SelectedLabel: (label: string) => + setSelectedIdx(functions.SavedRecordingNames().indexOf(label)), + }; - const [recordings, setRecordings] = useState(functions.SavedRecordingNames()); - const [selectedIdx, setSelectedIdx] = React.useState(); - const [showSaveRecordingModal, setShowSaveRecordingModal] = useState(false); - const [isRecording, setIsRecording] = React.useState(props.isRecording ? props.isRecording : false) + const [recordings, setRecordings] = useState( + functions.SavedRecordingNames(), + ); + const [selectedIdx, setSelectedIdx] = React.useState(); + const [showSaveRecordingModal, setShowSaveRecordingModal] = + useState(false); + const [isRecording, setIsRecording] = React.useState( + props.isRecording ? props.isRecording : false, + ); - const SaveRecordingModal = (props: { - setShow: (show: boolean) => void, - show: boolean - }) => { - const [name, setName] = React.useState(""); - function handleAccept() { - if (name.length > 0) { - if (!recordings.includes(name)) { - setRecordings(recordings => [...recordings, name]) - } - functions.SaveRecording(name); - } - setName(""); + const SaveRecordingModal = (props: { + setShow: (show: boolean) => void; + show: boolean; + }) => { + const [name, setName] = React.useState(""); + function handleAccept() { + if (name.length > 0) { + if (!recordings.includes(name)) { + setRecordings((recordings) => [...recordings, name]); } + functions.SaveRecording(name); + } + setName(""); + } - return ( - functions.StopRecording()} - id="save-recording-modal" - acceptButtonText="Save" - acceptDisabled={name.length < 1} - size={isMobile ? "small" : "large"} - mobile={isMobile} - > - {/* + return ( + functions.StopRecording()} + id="save-recording-modal" + acceptButtonText="Save" + acceptDisabled={name.length < 1} + size={isMobile ? "small" : "large"} + mobile={isMobile} + > + {/*
    */} -
    - {/* */} - setName(e.target.value)} - placeholder="Enter name of movement" - /> -
    -
    - ) +
    + {/* */} + setName(e.target.value)} + placeholder="Enter name of movement" + /> +
    +
    + ); + }; + + useEffect(() => { + if (props.isRecording == undefined) { + return; + } else if (props.isRecording) { + functions.Record(); + } else { + setShowSaveRecordingModal(true); } + }, [props.isRecording]); - useEffect(() => { - if (props.isRecording == undefined) { - return - } else if (props.isRecording) { - functions.Record() - } else { - setShowSaveRecordingModal(true) - } - }, [props.isRecording]) + if (props.globalRecord !== undefined && !props.globalRecord) + return ( + + ); - if (props.globalRecord !== undefined && !props.globalRecord) return ( - +
    Movement Recorder
    +
    + - ) - - return ( - !isMobile ? - -
    Movement Recorder
    -
    - - - - - - - - - - -
    - -
    - : - - -
    - {/*
    { + + + + + + + + + +
    + + + ) : ( + + +
    + {/*
    { if (!isRecording) { setIsRecording(true) functions.Record() @@ -196,21 +233,22 @@ export const MovementRecorder = (props: { } {!isRecording ? Record : Save }
    */} -
    { - if (selectedIdx != undefined && selectedIdx > -1) { - functions.LoadRecording(selectedIdx)} - } - }> - - play_circle - - Play -
    -
    - -
    - ) -} \ No newline at end of file +
    { + if (selectedIdx != undefined && selectedIdx > -1) { + functions.LoadRecording(selectedIdx); + } + }} + > + play_circle + Play +
    +
    + +
    + ); +}; diff --git a/src/pages/operator/tsx/layout_components/Panel.tsx b/src/pages/operator/tsx/layout_components/Panel.tsx index c0ae618c..98fb4e0c 100644 --- a/src/pages/operator/tsx/layout_components/Panel.tsx +++ b/src/pages/operator/tsx/layout_components/Panel.tsx @@ -1,13 +1,21 @@ import React from "react"; -import { ComponentType, ParentComponentDefinition, TabDefinition, PanelDefinition } from "../utils/component_definitions" +import { + ComponentType, + ParentComponentDefinition, + TabDefinition, + PanelDefinition, +} from "../utils/component_definitions"; import { className } from "shared/util"; import { PopupModal } from "../basic_components/PopupModal"; import { ComponentListProps, ComponentList } from "./ComponentList"; import { DropZone } from "./DropZone"; -import { CustomizableComponentProps, isSelected } from "./CustomizableComponent"; -import "operator/css/Panel.css" +import { + CustomizableComponentProps, + isSelected, +} from "./CustomizableComponent"; +import "operator/css/Panel.css"; -/* +/* TODO: implement behavior: - delete of all tabs deletes the tabs element @@ -19,201 +27,222 @@ implement behavior: * @param props {@link CustomizableComponentProps} */ export const Panel = (props: CustomizableComponentProps) => { - // Index of the active tab - let [activeTab, setActiveTab] = React.useState(0); - // If should show the popup to name a new tab - const [showTabModal, setShowTabModal] = React.useState(false); - const definition = props.definition as PanelDefinition; - const countChildren = definition.children.length; - - // Handle case where active tab was moved or deleted, just use last remaining tab - if (activeTab >= countChildren) { - setActiveTab(countChildren - 1); - activeTab = countChildren - 1; + // Index of the active tab + let [activeTab, setActiveTab] = React.useState(0); + // If should show the popup to name a new tab + const [showTabModal, setShowTabModal] = React.useState(false); + const definition = props.definition as PanelDefinition; + const countChildren = definition.children.length; + + // Handle case where active tab was moved or deleted, just use last remaining tab + if (activeTab >= countChildren) { + setActiveTab(countChildren - 1); + activeTab = countChildren - 1; + } + + const activeTabDef = definition.children[activeTab] as TabDefinition; + if (!activeTabDef) { + throw Error( + `Tabs at: ${props.path}\nActive tab not defined\nActive tab: ${activeTab}`, + ); + } + if (activeTabDef.type != ComponentType.SingleTab) { + throw Error( + `Tabs element at path ${props.path} has child of type ${activeTabDef.type}`, + ); + } + + // Should take up screen size proportional to number of children + const flex = + activeTabDef.label === "Safety" + ? 1 + : Math.max(activeTabDef.children.length + 1, 1); + + /** Props for rendering the children elements inside the active tab */ + const componentListProps: ComponentListProps = { + path: props.path + "-" + activeTab, + sharedState: props.sharedState, + // Use active tab as the definition for what to render + definition: activeTabDef, + }; + + /** + * Creates a definition for the new tab and adds it to the layout + * @param name the label for the new tab + */ + function addTab(name: string) { + // Define new tab + const newTabDef = { + type: ComponentType.SingleTab, + label: name, + children: [], + } as TabDefinition; + // Add it as a new child + definition.children.push(newTabDef); + // Set as selected element (and rerender) + props.sharedState.onSelect(newTabDef, undefined); + } + + /** + * Callback when a tab label is clicked on. Sets the tab as active. If already + * active and in customize mode, selects the tab for customization. + * @param idx index of the clicked tab + */ + function clickTab(idx: number) { + // If customizing and tab already active + if (props.sharedState.customizing && idx === activeTab) { + // Mark tab as selected + const tabPath = props.path + "-" + idx; + props.sharedState.onSelect(definition.children[idx], tabPath); + return; } - - const activeTabDef = definition.children[activeTab] as TabDefinition; - if (!activeTabDef) { - throw Error(`Tabs at: ${props.path}\nActive tab not defined\nActive tab: ${activeTab}`) - } - if (activeTabDef.type != ComponentType.SingleTab) { - throw Error(`Tabs element at path ${props.path} has child of type ${activeTabDef.type}`) - } - - // Should take up screen size proportional to number of children - const flex = activeTabDef.label === "Safety" ? 1 : Math.max(activeTabDef.children.length + 1, 1); - - /** Props for rendering the children elements inside the active tab */ - const componentListProps: ComponentListProps = { - path: props.path + '-' + activeTab, - sharedState: props.sharedState, - // Use active tab as the definition for what to render - definition: activeTabDef - } - - /** - * Creates a definition for the new tab and adds it to the layout - * @param name the label for the new tab - */ - function addTab(name: string) { - // Define new tab - const newTabDef = { - type: ComponentType.SingleTab, - label: name, - children: [] - } as TabDefinition; - // Add it as a new child - definition.children.push(newTabDef); - // Set as selected element (and rerender) - props.sharedState.onSelect(newTabDef, undefined); - } - - /** - * Callback when a tab label is clicked on. Sets the tab as active. If already - * active and in customize mode, selects the tab for customization. - * @param idx index of the clicked tab - */ - function clickTab(idx: number) { - // If customizing and tab already active - if (props.sharedState.customizing && idx === activeTab) { - // Mark tab as selected - const tabPath = props.path + '-' + idx; - props.sharedState.onSelect(definition.children[idx], tabPath); - return; - } - // Set tab as active - setActiveTab(idx); - } - - /** - * Handle click on tab content during customization mode. Marks this entire - * tabs component as selected. - */ - function selectContent() { - props.sharedState.onSelect(props.definition, props.path); - } - - // Add onClick listener to tab content in customization mode - const selectProp = props.sharedState.customizing ? { - onClick: selectContent - } : {}; - - /** - * Checks if this tabs or one of its immediate children is currently selected - * - * @returns null if currently selected component is not either this tabs - * or one of it's immediate SingleTab children, -1 if the selected component - * is this entire tabs component, or the index of the selected single tab - * child. - */ - function checkChildTabSelected(): number | null { - const selectedPath = props.sharedState.selectedPath; - if (!selectedPath) return null; // nothing is selected/active - const activeSplitPath = selectedPath.split('-'); - const thisSplitPath = props.path.split('-'); - const activeChild = thisSplitPath.every((val, index) => val === activeSplitPath[index]); - if (!activeChild) return null; // active path is not a child element - // The paths are exactly the same, the entire Tabs structure is selected - if (activeSplitPath.length == thisSplitPath.length) return -1; - // Path points to a child of a tab - if (activeSplitPath.length - 1 > thisSplitPath.length) return null; - // Return the child index - return +activeSplitPath.slice(-1); - } - const childTabSelected: number | null = checkChildTabSelected(); - - /** - * Maps children list to a set of buttons with labels for switching tabs - * @param tabDef definition of the child single tab component - * @param idx index of the child component in the children array - * @returns A button to switch tabs - */ - function mapTabLabels(tabDef: TabDefinition, idx: number) { - const active = activeTab === idx; - const selected = childTabSelected === idx; - return ( - - - - - ); - } - - const thisSelected = childTabSelected === -1; - + // Set tab as active + setActiveTab(idx); + } + + /** + * Handle click on tab content during customization mode. Marks this entire + * tabs component as selected. + */ + function selectContent() { + props.sharedState.onSelect(props.definition, props.path); + } + + // Add onClick listener to tab content in customization mode + const selectProp = props.sharedState.customizing + ? { + onClick: selectContent, + } + : {}; + + /** + * Checks if this tabs or one of its immediate children is currently selected + * + * @returns null if currently selected component is not either this tabs + * or one of it's immediate SingleTab children, -1 if the selected component + * is this entire tabs component, or the index of the selected single tab + * child. + */ + function checkChildTabSelected(): number | null { + const selectedPath = props.sharedState.selectedPath; + if (!selectedPath) return null; // nothing is selected/active + const activeSplitPath = selectedPath.split("-"); + const thisSplitPath = props.path.split("-"); + const activeChild = thisSplitPath.every( + (val, index) => val === activeSplitPath[index], + ); + if (!activeChild) return null; // active path is not a child element + // The paths are exactly the same, the entire Tabs structure is selected + if (activeSplitPath.length == thisSplitPath.length) return -1; + // Path points to a child of a tab + if (activeSplitPath.length - 1 > thisSplitPath.length) return null; + // Return the child index + return +activeSplitPath.slice(-1); + } + const childTabSelected: number | null = checkChildTabSelected(); + + /** + * Maps children list to a set of buttons with labels for switching tabs + * @param tabDef definition of the child single tab component + * @param idx index of the child component in the children array + * @returns A button to switch tabs + */ + function mapTabLabels(tabDef: TabDefinition, idx: number) { + const active = activeTab === idx; + const selected = childTabSelected === idx; return ( -
    + + : undefined - } -
    -
    - -
    - - -
    - ) -} + {tabDef.label} + + + ); + } + + const thisSelected = childTabSelected === -1; + + return ( +
    +
    + {definition.children.map(mapTabLabels)} + + { + // In customization mode show an extra plus to add a new tab + props.sharedState.customizing ? ( + + ) : undefined + } +
    +
    + +
    + +
    + ); +}; /** Modal for creating a new tab on a panel component. */ const NewTabModal = (props: { - show: boolean, - setShow: (show: boolean) => void, - addTab: (name: string) => void + show: boolean; + setShow: (show: boolean) => void; + addTab: (name: string) => void; }) => { - const [text, setText] = React.useState(""); - function handleAccept() { - if (text.length > 0) { - props.addTab(text); - } + const [text, setText] = React.useState(""); + function handleAccept() { + if (text.length > 0) { + props.addTab(text); } - - return ( - - - setText(e.target.value)} - placeholder="label for the new tab" - /> - - ) -} \ No newline at end of file + } + + return ( + + + setText(e.target.value)} + placeholder="label for the new tab" + /> + + ); +}; diff --git a/src/pages/operator/tsx/layout_components/PredictiveDisplay.tsx b/src/pages/operator/tsx/layout_components/PredictiveDisplay.tsx index 2672823c..a22e7a6b 100644 --- a/src/pages/operator/tsx/layout_components/PredictiveDisplay.tsx +++ b/src/pages/operator/tsx/layout_components/PredictiveDisplay.tsx @@ -1,9 +1,13 @@ -import React from "react" +import React from "react"; import { className, navigationProps } from "shared/util"; import { CustomizableComponentProps } from "./CustomizableComponent"; import { predicitiveDisplayFunctionProvider } from "operator/tsx/index"; -import { SVG_RESOLUTION, percent2Pixel, OVERHEAD_ROBOT_BASE as BASE } from "../utils/svg"; -import "operator/css/PredictiveDisplay.css" +import { + SVG_RESOLUTION, + percent2Pixel, + OVERHEAD_ROBOT_BASE as BASE, +} from "../utils/svg"; +import "operator/css/PredictiveDisplay.css"; /** * Scales height values to fit in the navigation camera @@ -11,13 +15,13 @@ import "operator/css/PredictiveDisplay.css" * @returns scaled number */ function scaleToNavAspectRatio(y: number) { - return y / navigationProps.width * navigationProps.height; + return (y / navigationProps.width) * navigationProps.height; } /**Arguments for drawing the dashed line in the center of the path */ -const strokeDasharray = "4 10" +const strokeDasharray = "4 10"; /**Height of the predictive display SVG */ -const resolution_height = scaleToNavAspectRatio(SVG_RESOLUTION) +const resolution_height = scaleToNavAspectRatio(SVG_RESOLUTION); /**Pixel location of the front of the robot */ const baseFront = scaleToNavAspectRatio(BASE.centerY - BASE.height / 2); /**Pixel location of the back of the robot */ @@ -33,187 +37,220 @@ const rotateArcRadius = percent2Pixel(10); /** Functions required for predictive display */ export type PredictiveDisplayFunctions = { - /** Callback function when mouse is clicked in predicitive display area */ - onClick: (length: number, angle: number) => void; - /** Callback function when cursor is moved in predictive display area */ - onMove?: (length: number, angle: number) => void; - /** Callback function for release */ - onRelease?: () => void; - /** Callback function for leaving predictive display area */ - onLeave?: () => void; -} + /** Callback function when mouse is clicked in predicitive display area */ + onClick: (length: number, angle: number) => void; + /** Callback function when cursor is moved in predictive display area */ + onMove?: (length: number, angle: number) => void; + /** Callback function for release */ + onRelease?: () => void; + /** Callback function for leaving predictive display area */ + onLeave?: () => void; +}; -/** +/** * Example trajectory to display while in customizing mode so the user can see * the predictive display overlay on the overhead camera. */ const customizingTrajectory = drawForwardTraj(106, 161)[2]; /** - * Overlay for overhead video stream where a curved path follows the cursor, + * Overlay for overhead video stream where a curved path follows the cursor, * and clicking translates and/or rotates the robot base. - * + * * @param props {@link CustomizableComponentProps} */ export const PredictiveDisplay = (props: CustomizableComponentProps) => { - const svgRef = React.useRef(null); - const { customizing } = props.sharedState; - const [trajectory, setTrajectory] = React.useState(undefined); - const [moving, setMoving] = React.useState(false); - const functions = predicitiveDisplayFunctionProvider.provideFunctions(setMoving); - const length = React.useRef(0); - const angle = React.useRef(0); - const holding = React.useRef(false); + const svgRef = React.useRef(null); + const { customizing } = props.sharedState; + const [trajectory, setTrajectory] = React.useState( + undefined, + ); + const [moving, setMoving] = React.useState(false); + const functions = + predicitiveDisplayFunctionProvider.provideFunctions(setMoving); + const length = React.useRef(0); + const angle = React.useRef(0); + const holding = React.useRef(false); - function handleLeave() { - setTrajectory(undefined); - if (functions.onLeave) { - functions.onLeave() - } + function handleLeave() { + setTrajectory(undefined); + if (functions.onLeave) { + functions.onLeave(); } + } - function handleClick() { - holding.current = true; - if (functions.onClick) { - functions.onClick(length.current, angle.current); - } + function handleClick() { + holding.current = true; + if (functions.onClick) { + functions.onClick(length.current, angle.current); } + } - function handleRelease() { - holding.current = false; - if (functions.onRelease) { - functions.onRelease() - } + function handleRelease() { + holding.current = false; + if (functions.onRelease) { + functions.onRelease(); } + } - /** Rerenders the trajectory based on the cursor location */ - function handleMove(event: React.MouseEvent) { - const { clientX, clientY } = event; - const svg = svgRef.current; - if (!svg) return; + /** Rerenders the trajectory based on the cursor location */ + function handleMove(event: React.MouseEvent) { + const { clientX, clientY } = event; + const svg = svgRef.current; + if (!svg) return; - // Get x and y in terms of the SVG element - const rect = svg.getBoundingClientRect(); - const x = (clientX - rect.left) / rect.width * SVG_RESOLUTION; - const pixelY = (clientY - rect.top) / rect.height; - const y = scaleToNavAspectRatio(pixelY * SVG_RESOLUTION); - const ret = drawTrajectory(x, y); + // Get x and y in terms of the SVG element + const rect = svg.getBoundingClientRect(); + const x = ((clientX - rect.left) / rect.width) * SVG_RESOLUTION; + const pixelY = (clientY - rect.top) / rect.height; + const y = scaleToNavAspectRatio(pixelY * SVG_RESOLUTION); + const ret = drawTrajectory(x, y); - length.current = ret[0]; - angle.current = ret[1]; - setTrajectory(ret[2]); + length.current = ret[0]; + angle.current = ret[1]; + setTrajectory(ret[2]); - if (holding && functions.onMove) { - functions.onMove(length.current, angle.current); - } + if (holding && functions.onMove) { + functions.onMove(length.current, angle.current); } + } - // If customizing, disable all user interaction - const controlProps = customizing ? {} : { + // If customizing, disable all user interaction + const controlProps = customizing + ? {} + : { onMouseMove: handleMove, onMouseLeave: handleLeave, onMouseDown: handleClick, - onMouseUp: handleRelease - }; + onMouseUp: handleRelease, + }; - return ( - - {customizing ? customizingTrajectory : trajectory} - - ) -} + return ( + + {customizing ? customizingTrajectory : trajectory} + + ); +}; /** * Creates a trajectory based on the cursor location - * + * * @param x horizontal position of the cursor * @param y vertical position of the cursor * @returns the linear distance, the angle, and the trajectory element */ function drawTrajectory(x: number, y: number): [number, number, JSX.Element] { - let ret: [number, number, JSX.Element]; - if (y < baseFront) { - ret = drawForwardTraj(x, y) - } else if (y < baseBack) { - // Next to base, draw rotate trajectory - ret = drawRotate(x < BASE.centerX); - } else { - // Move backward - ret = drawBackward(y); - } - return ret; + let ret: [number, number, JSX.Element]; + if (y < baseFront) { + ret = drawForwardTraj(x, y); + } else if (y < baseBack) { + // Next to base, draw rotate trajectory + ret = drawRotate(x < BASE.centerX); + } else { + // Move backward + ret = drawBackward(y); + } + return ret; } /** * Draws an arc from the base to the cursor, such that the arc is normal * to the base. - * + * * @param x horizontal position of the cursor * @param y vertical position of the cursor * @returns the linear distance, the angle, and the trajectory element */ function drawForwardTraj(x: number, y: number): [number, number, JSX.Element] { - const dx = BASE.centerX - x; - const dy = baseFront - y; - const heading = Math.atan2(-dx, dy) - const sweepFlag = dx < 0; + const dx = BASE.centerX - x; + const dy = baseFront - y; + const heading = Math.atan2(-dx, dy); + const sweepFlag = dx < 0; - const distance = Math.sqrt(dx * dx + dy * dy) // length from base to cursor - const radius = distance / (2 * Math.sin(heading)) // radius of the center curve - const centerPath = makeArc(BASE.centerX, baseFront, radius, sweepFlag, x, y); + const distance = Math.sqrt(dx * dx + dy * dy); // length from base to cursor + const radius = distance / (2 * Math.sin(heading)); // radius of the center curve + const centerPath = makeArc(BASE.centerX, baseFront, radius, sweepFlag, x, y); - const leftEndX = x - BASE.width / 2 * Math.cos(2 * heading) - const leftEndY = y - BASE.width / 2 * Math.sin(2 * heading) - const leftRadius = radius + BASE.width / 2 - const leftPath = makeArc(baseLeft, baseFront, leftRadius, sweepFlag, leftEndX, leftEndY); + const leftEndX = x - (BASE.width / 2) * Math.cos(2 * heading); + const leftEndY = y - (BASE.width / 2) * Math.sin(2 * heading); + const leftRadius = radius + BASE.width / 2; + const leftPath = makeArc( + baseLeft, + baseFront, + leftRadius, + sweepFlag, + leftEndX, + leftEndY, + ); - const rightEndX = x + BASE.width / 2 * Math.cos(2 * heading) - const rightEndY = y + BASE.width / 2 * Math.sin(2 * heading) - const rightRadius = radius - BASE.width / 2 - const rightPath = makeArc(baseRight, baseFront, rightRadius, sweepFlag, rightEndX, rightEndY); + const rightEndX = x + (BASE.width / 2) * Math.cos(2 * heading); + const rightEndY = y + (BASE.width / 2) * Math.sin(2 * heading); + const rightRadius = radius - BASE.width / 2; + const rightPath = makeArc( + baseRight, + baseFront, + rightRadius, + sweepFlag, + rightEndX, + rightEndY, + ); - const trajectory = ( - <> - - - - - ); + const trajectory = ( + <> + + + + + ); - // Normalize the distance - const maxX = SVG_RESOLUTION / 2; - const maxY = baseFront; - const maxDistance = Math.sqrt(maxX * maxX + maxY * maxY); - const normalizedDistance = distance / maxDistance; - return [normalizedDistance, -1 * heading, trajectory]; + // Normalize the distance + const maxX = SVG_RESOLUTION / 2; + const maxY = baseFront; + const maxDistance = Math.sqrt(maxX * maxX + maxY * maxY); + const normalizedDistance = distance / maxDistance; + return [normalizedDistance, -1 * heading, trajectory]; } /** * Creates the SVG path elements for circular arrows around the base. - * + * * @param rotateLeft if true draws a path counter-clockwise, otherwise clockwise * @returns SVG path string description of the arrows */ function makeArrowPath(rotateLeft: boolean) { - const arrowLength = percent2Pixel(2.5); - const top = baseCenterY - rotateArcRadius; - const bottom = baseCenterY + rotateArcRadius; - const left = BASE.centerX - rotateArcRadius; - const right = BASE.centerX + rotateArcRadius; - const arrowDx = rotateLeft ? arrowLength : -arrowLength; + const arrowLength = percent2Pixel(2.5); + const top = baseCenterY - rotateArcRadius; + const bottom = baseCenterY + rotateArcRadius; + const left = BASE.centerX - rotateArcRadius; + const right = BASE.centerX + rotateArcRadius; + const arrowDx = rotateLeft ? arrowLength : -arrowLength; - let arrows = makeArc(rotateLeft ? right : left, baseCenterY, rotateArcRadius, !rotateLeft, BASE.centerX, top) - arrows += `L ${BASE.centerX + arrowDx} ${top - arrowLength}` + let arrows = makeArc( + rotateLeft ? right : left, + baseCenterY, + rotateArcRadius, + !rotateLeft, + BASE.centerX, + top, + ); + arrows += `L ${BASE.centerX + arrowDx} ${top - arrowLength}`; - arrows += makeArc(rotateLeft ? left : right, baseCenterY, rotateArcRadius, !rotateLeft, BASE.centerX, bottom) - arrows += `L ${BASE.centerX - arrowDx} ${bottom + arrowLength}` - return arrows + arrows += makeArc( + rotateLeft ? left : right, + baseCenterY, + rotateArcRadius, + !rotateLeft, + BASE.centerX, + bottom, + ); + arrows += `L ${BASE.centerX - arrowDx} ${bottom + arrowLength}`; + return arrows; } /** Path to draw for turning left in place */ @@ -223,44 +260,49 @@ const rightArrowPath: string = makeArrowPath(false); /** * Draws circular arrows around the base for rotating in place - * - * @param rotateLeft if true draws counterclockwise arrow, if false draws + * + * @param rotateLeft if true draws counterclockwise arrow, if false draws * clockwise * @returns the linear distance, the angle, and the trajectory element */ function drawRotate(rotateLeft: boolean): [number, number, JSX.Element] { - const path = rotateLeft ? leftArrowPath : rightArrowPath; - const trajectory = ( - - ); - return [0, rotateLeft ? 1 : -1, trajectory] + const path = rotateLeft ? leftArrowPath : rightArrowPath; + const trajectory = ; + return [0, rotateLeft ? 1 : -1, trajectory]; } /** * Draws a straight path backward from the base to the y position of the mouse - * + * * @param y y position of the mouse on the SVG canvas * @returns the linear distance, the angle, and the trajectory element */ function drawBackward(y: number): [number, number, JSX.Element] { - const leftPath = `M ${baseLeft} ${baseBack} ${baseLeft} ${y}` - const rightPath = `M ${baseRight} ${baseBack} ${baseRight} ${y}` - const centerPath = `M ${BASE.centerX} ${baseBack} ${BASE.centerX} ${y}` - const trajectory = ( - <> - - - - - ); + const leftPath = `M ${baseLeft} ${baseBack} ${baseLeft} ${y}`; + const rightPath = `M ${baseRight} ${baseBack} ${baseRight} ${y}`; + const centerPath = `M ${BASE.centerX} ${baseBack} ${BASE.centerX} ${y}`; + const trajectory = ( + <> + + + + + ); - const distance = baseBack - y; - const maxDistance = resolution_height - baseBack; - return [distance / maxDistance, 0, trajectory]; + const distance = baseBack - y; + const maxDistance = resolution_height - baseBack; + return [distance / maxDistance, 0, trajectory]; } /**Formats the SVG path arc string. */ -function makeArc(startX: number, startY: number, radius: number, sweepFlag: boolean, endX: number, endY: number) { - const sweep = sweepFlag ? 1 : 0; - return `M ${startX},${startY} A ${radius} ${radius} 0 0 ${sweep} ${endX},${endY}` -} \ No newline at end of file +function makeArc( + startX: number, + startY: number, + radius: number, + sweepFlag: boolean, + endX: number, + endY: number, +) { + const sweep = sweepFlag ? 1 : 0; + return `M ${startX},${startY} A ${radius} ${radius} 0 0 ${sweep} ${endX},${endY}`; +} diff --git a/src/pages/operator/tsx/layout_components/SimpleCameraView.tsx b/src/pages/operator/tsx/layout_components/SimpleCameraView.tsx index 420e0ae8..c9dcccaf 100644 --- a/src/pages/operator/tsx/layout_components/SimpleCameraView.tsx +++ b/src/pages/operator/tsx/layout_components/SimpleCameraView.tsx @@ -2,150 +2,183 @@ import React from "react"; import { className, RemoteStream } from "shared/util"; import { buttonFunctionProvider } from ".."; import { CameraViewId } from "../utils/component_definitions"; -import { ButtonPadButton, ButtonState, panTiltButtons } from "../function_providers/ButtonFunctionProvider"; -import "operator/css/SimpleCameraView.css" +import { + ButtonPadButton, + ButtonState, + panTiltButtons, +} from "../function_providers/ButtonFunctionProvider"; +import "operator/css/SimpleCameraView.css"; import { getIcon } from "../utils/svg"; /** * Displays a video stream with an optional button pad overlay - * + * * @param props properties */ -export const SimpleCameraView = (props: { id: CameraViewId, remoteStreams: Map }) => { - // Reference to the video element - const videoRef = React.useRef(null); - // Get the stream to display inside the video - const stream: MediaStream = getStream(props.id, props.remoteStreams); - // Refrence to the div immediately around the video element - const videoAreaRef = React.useRef(null); - // Boolean representing if the video stream needs to be constrained by height - // (constrained by width otherwise) - const [constrainedHeight, setConstrainedHeight] = React.useState(false); - - // Update the source of the video stream - React.useEffect(() => { - if (!videoRef?.current) return; - videoRef.current.srcObject = stream; - }, [stream]); +export const SimpleCameraView = (props: { + id: CameraViewId; + remoteStreams: Map; +}) => { + // Reference to the video element + const videoRef = React.useRef(null); + // Get the stream to display inside the video + const stream: MediaStream = getStream(props.id, props.remoteStreams); + // Reference to the div immediately around the video element + const videoAreaRef = React.useRef(null); + // Boolean representing if the video stream needs to be constrained by height + // (constrained by width otherwise) + const [constrainedHeight, setConstrainedHeight] = + React.useState(false); - function setVideoSize(videoRef) { - const videoRect = videoRef.current.getBoundingClientRect(); - let marginTop = 0; - let min_control_panel_size = 300; // px - marginTop = Math.min(window.innerHeight - min_control_panel_size - videoRect.height, 0); - document.querySelector('.btn-down')?.setAttribute('style', 'margin-top:' + (videoRect.height + marginTop - 70).toString() + "px;") - document.querySelector('.depth-sensing')?.setAttribute('style', 'margin-top:' + (videoRect.height + marginTop - 42).toString() + "px;") - videoRef.current.style.marginTop = marginTop.toString() + "px"; - } + // Update the source of the video stream + React.useEffect(() => { + if (!videoRef?.current) return; + videoRef.current.srcObject = stream; + }, [stream]); - // Constrain the width or height when the stream gets too large - React.useEffect(() => { - const resizeObserver = new ResizeObserver(entries => { - - // height and width of area around the video stream - const { height, width } = entries[0].contentRect; + function setVideoSize(videoRef) { + const videoRect = videoRef.current.getBoundingClientRect(); + let marginTop = 0; + let min_control_panel_size = 300; // px + marginTop = Math.min( + window.innerHeight - min_control_panel_size - videoRect.height, + 0, + ); + document + .querySelector(".btn-down") + ?.setAttribute( + "style", + "margin-top:" + (videoRect.height + marginTop - 70).toString() + "px;", + ); + document + .querySelector(".depth-sensing") + ?.setAttribute( + "style", + "margin-top:" + (videoRect.height + marginTop - 42).toString() + "px;", + ); + videoRef.current.style.marginTop = marginTop.toString() + "px"; + } - // height and width of video stream - if (!videoRef?.current) return; - const videoRect = videoRef.current.getBoundingClientRect(); + // Constrain the width or height when the stream gets too large + React.useEffect(() => { + const resizeObserver = new ResizeObserver((entries) => { + // height and width of area around the video stream + const { height, width } = entries[0].contentRect; - if (videoRect.height > height) { - setConstrainedHeight(true); - } else if (videoRect.width > width) { - setConstrainedHeight(false); - } - }); - if (!videoAreaRef?.current) return; - resizeObserver.observe(videoAreaRef.current); - return () => resizeObserver.disconnect(); - }, []); + // height and width of video stream + if (!videoRef?.current) return; + const videoRect = videoRef.current.getBoundingClientRect(); - const videoComponent = (props.id === CameraViewId.realsense || props.id === CameraViewId.overhead) ? - ( - <> -
    - {panTiltButtons.map(dir => )} -
    -
    -
    - - ) - : - ( - <> -
    -
    - - ) + if (videoRect.height > height) { + setConstrainedHeight(true); + } else if (videoRect.width > width) { + setConstrainedHeight(false); + } + }); + if (!videoAreaRef?.current) return; + resizeObserver.observe(videoAreaRef.current); + return () => resizeObserver.disconnect(); + }, []); - return ( -
    - {videoComponent} + const videoComponent = + props.id === CameraViewId.realsense || + props.id === CameraViewId.overhead ? ( + <> +
    + {panTiltButtons.map((dir) => ( + + ))} +
    +
    + + ) : ( + <> +
    +
    + ); -} + + return ( +
    + {videoComponent} +
    + ); +}; /** * Creates a single button for controlling the pan or tilt of the realsense camera * * @param props the direction of the button {@link PanTiltButton} - */ + */ const PanTiltButton = (props: { direction: ButtonPadButton }) => { - const functs = buttonFunctionProvider.provideFunctions(props.direction); - const dir = props.direction.split(" ")[2] - let rotation: string; + const functs = buttonFunctionProvider.provideFunctions(props.direction); + const dir = props.direction.split(" ")[2]; + let rotation: string; - // Specify button details based on the direction - switch (props.direction) { - case (ButtonPadButton.CameraTiltUp): - rotation = "-90"; - break; - case (ButtonPadButton.CameraTiltDown): - rotation = "90"; - break; - case (ButtonPadButton.CameraPanLeft): - rotation = "180"; - break; - case (ButtonPadButton.CameraPanRight): - rotation = "0"; // by default the arrow icon points right - break; - default: - throw Error(`unknown pan tilt button direction ${props.direction}`) - } + // Specify button details based on the direction + switch (props.direction) { + case ButtonPadButton.CameraTiltUp: + rotation = "-90"; + break; + case ButtonPadButton.CameraTiltDown: + rotation = "90"; + break; + case ButtonPadButton.CameraPanLeft: + rotation = "180"; + break; + case ButtonPadButton.CameraPanRight: + rotation = "0"; // by default the arrow icon points right + break; + default: + throw Error(`unknown pan tilt button direction ${props.direction}`); + } - return ( - - ) -} + return ( + + ); +}; /******************************************************************************* * Helper functions @@ -156,23 +189,25 @@ const PanTiltButton = (props: { direction: ButtonPadButton }) => { * * @param id identifier for the video stream * @param remoteStreams map of {@link RemoteStream} - * @returns the corresponding stream - */ -function getStream(id: CameraViewId, remoteStreams: Map): MediaStream { - let streamName: string; - switch (id) { - case CameraViewId.overhead: - streamName = "overhead"; - break; - case CameraViewId.realsense: - streamName = "realsense"; - break; - case CameraViewId.gripper: - streamName = "gripper"; - break; - default: - throw Error(`unknow video stream id: ${id}`); - } - return remoteStreams.get(streamName)!.stream; + * @returns the corresponding stream + */ +function getStream( + id: CameraViewId, + remoteStreams: Map, +): MediaStream { + let streamName: string; + switch (id) { + case CameraViewId.overhead: + streamName = "overhead"; + break; + case CameraViewId.realsense: + streamName = "realsense"; + break; + case CameraViewId.gripper: + streamName = "gripper"; + break; + default: + throw Error(`unknow video stream id: ${id}`); + } + return remoteStreams.get(streamName)!.stream; } - diff --git a/src/pages/operator/tsx/layout_components/VirtualJoystick.tsx b/src/pages/operator/tsx/layout_components/VirtualJoystick.tsx index 60e386e4..1d85ebc4 100644 --- a/src/pages/operator/tsx/layout_components/VirtualJoystick.tsx +++ b/src/pages/operator/tsx/layout_components/VirtualJoystick.tsx @@ -1,114 +1,132 @@ import React from "react"; -import { CustomizableComponentProps, isSelected } from "./CustomizableComponent"; +import { + CustomizableComponentProps, + isSelected, +} from "./CustomizableComponent"; import { className } from "shared/util"; import { SVG_RESOLUTION } from "../utils/svg"; import { predicitiveDisplayFunctionProvider } from ".."; -import "operator/css/VirtualJoystick.css" +import "operator/css/VirtualJoystick.css"; const OUTER_RADIUS = SVG_RESOLUTION / 2; const JOYSTICK_RADIUS = SVG_RESOLUTION / 3.3; - export const VirtualJoystick = (props: CustomizableComponentProps) => { - const svgRef = React.useRef(null); - const [active, setActive] = React.useState(false); - const { customizing } = props.sharedState; - const selected = isSelected(props); - const [joystick, setJoystick] = React.useState(drawJoystickCenter()); - const functions = predicitiveDisplayFunctionProvider.provideFunctions(handleSetActive); - - function handleSetActive(active: boolean) { - if (!active) { - setJoystick(drawJoystickCenter()); - } - setActive(active); - } - - const length = React.useRef(0); - const angle = React.useRef(0); - - function drawJoystickCenter() { - return drawJoystick(SVG_RESOLUTION / 2, SVG_RESOLUTION / 2) - } - - function drawJoystick(x: number, y: number) { - const newJoystick = ( - - ); - return newJoystick + const svgRef = React.useRef(null); + const [active, setActive] = React.useState(false); + const { customizing } = props.sharedState; + const selected = isSelected(props); + const [joystick, setJoystick] = + React.useState(drawJoystickCenter()); + const functions = + predicitiveDisplayFunctionProvider.provideFunctions(handleSetActive); + + function handleSetActive(active: boolean) { + if (!active) { + setJoystick(drawJoystickCenter()); } + setActive(active); + } - function handleLeave() { - setJoystick(drawJoystickCenter); - if (functions.onLeave) { - functions.onLeave(); - } - } + const length = React.useRef(0); + const angle = React.useRef(0); - function handleClick(event: React.MouseEvent) { - if (!active) { - setJoystickToMouse(event); - } - if (functions.onClick) functions.onClick(length.current, angle.current); - } - - function handleRelease() { - if (functions.onRelease) functions.onRelease(); - } + function drawJoystickCenter() { + return drawJoystick(SVG_RESOLUTION / 2, SVG_RESOLUTION / 2); + } - function setLengthAndWidth(x: number, y: number) { - const xLocal = x - SVG_RESOLUTION / 2; - const yLocal = y - SVG_RESOLUTION / 2; - - angle.current = -xLocal / (SVG_RESOLUTION / 2) - length.current = -yLocal / (SVG_RESOLUTION / 2); - } - - function setJoystickToMouse(event: React.MouseEvent): [number, number] { - const { clientX, clientY } = event; - const svg = svgRef.current; - if (!svg) return [0, 0]; - - // Get x and y in terms of the SVG element - const rect = svg.getBoundingClientRect(); - const x = (clientX - rect.left) / rect.width * SVG_RESOLUTION; - const y = (clientY - rect.top) / rect.height * SVG_RESOLUTION; - setLengthAndWidth(x, y); - setJoystick(drawJoystick(x, y)); - return [x, y]; - - } + function drawJoystick(x: number, y: number) { + const newJoystick = ( + + ); + return newJoystick; + } - function handleMove(event: React.MouseEvent) { - if (!active) return; - setJoystickToMouse(event); - if (functions.onMove) functions.onMove(length.current, angle.current); + function handleLeave() { + setJoystick(drawJoystickCenter); + if (functions.onLeave) { + functions.onLeave(); } + } - function handleSelect(event: React.MouseEvent) { - event.stopPropagation(); - props.sharedState.onSelect(props.definition, props.path) + function handleClick(event: React.MouseEvent) { + if (!active) { + setJoystickToMouse(event); } - - const controlProps = customizing ? { onClick: handleSelect } : { + if (functions.onClick) functions.onClick(length.current, angle.current); + } + + function handleRelease() { + if (functions.onRelease) functions.onRelease(); + } + + function setLengthAndWidth(x: number, y: number) { + const xLocal = x - SVG_RESOLUTION / 2; + const yLocal = y - SVG_RESOLUTION / 2; + + angle.current = -xLocal / (SVG_RESOLUTION / 2); + length.current = -yLocal / (SVG_RESOLUTION / 2); + } + + function setJoystickToMouse( + event: React.MouseEvent, + ): [number, number] { + const { clientX, clientY } = event; + const svg = svgRef.current; + if (!svg) return [0, 0]; + + // Get x and y in terms of the SVG element + const rect = svg.getBoundingClientRect(); + const x = ((clientX - rect.left) / rect.width) * SVG_RESOLUTION; + const y = ((clientY - rect.top) / rect.height) * SVG_RESOLUTION; + setLengthAndWidth(x, y); + setJoystick(drawJoystick(x, y)); + return [x, y]; + } + + function handleMove(event: React.MouseEvent) { + if (!active) return; + setJoystickToMouse(event); + if (functions.onMove) functions.onMove(length.current, angle.current); + } + + function handleSelect(event: React.MouseEvent) { + event.stopPropagation(); + props.sharedState.onSelect(props.definition, props.path); + } + + const controlProps = customizing + ? { onClick: handleSelect } + : { onPointerMove: active ? handleMove : undefined, onPointerLeave: handleLeave, onPointerDown: handleClick, - onPointerUp: handleRelease - }; - return ( -
    - - - {/* + onPointerUp: handleRelease, + }; + return ( +
    + + + {/* */} - {joystick} - -
    - ); -} + {joystick} + +
    + ); +}; diff --git a/src/pages/operator/tsx/static_components/BatteryGauge.tsx b/src/pages/operator/tsx/static_components/BatteryGauge.tsx index ff95ab1b..e2233b9c 100644 --- a/src/pages/operator/tsx/static_components/BatteryGauge.tsx +++ b/src/pages/operator/tsx/static_components/BatteryGauge.tsx @@ -1,28 +1,28 @@ -import "operator/css/BatteryGuage.css" +import "operator/css/BatteryGuage.css"; import { useState } from "react"; import { className } from "shared/util"; import { CustomizableComponentProps } from "../layout_components/CustomizableComponent"; -import batteryGauge from "operator/icons/Battery_Gauge.svg" +import batteryGauge from "operator/icons/Battery_Gauge.svg"; import { batteryVoltageFunctionProvider } from ".."; import React from "react"; import { BatteryVoltageFunctions } from "../function_providers/BatteryVoltageFunctionProvider"; export const BatteryGuage = (props: CustomizableComponentProps) => { - const [color, setColor] = useState('green') + const [color, setColor] = useState("green"); - batteryVoltageFunctionProvider.setVoltageChangeCallback(setColor) - - return ( -
    - - {/*
    + batteryVoltageFunctionProvider.setVoltageChangeCallback(setColor); + + return ( +
    + + {/*
    */} - Battery Gauge -
    - ) -} \ No newline at end of file + Battery Gauge +
    + ); +}; diff --git a/src/pages/operator/tsx/static_components/Canvas.tsx b/src/pages/operator/tsx/static_components/Canvas.tsx index 950beffe..b36f74d5 100644 --- a/src/pages/operator/tsx/static_components/Canvas.tsx +++ b/src/pages/operator/tsx/static_components/Canvas.tsx @@ -2,50 +2,50 @@ import React from "react"; import createjs from "createjs-module"; export class Canvas extends React.Component { - private divID: string; - private className: string; - private width: number; - private height: number; - public scene?: createjs.Stage; - - constructor(props: { - divID: string, - className: string, - width: number, - height: number - }) { - super(props); - this.divID = props.divID - this.className = props.className - this.width = props.width - this.height = props.height - this.createCanvas() - } - - createCanvas() { - // create the canvas to render to - var canvas = document.createElement('canvas'); - canvas.setAttribute("class", this.className); - canvas.width = this.width; - canvas.height = this.height - - // create the easel to use - this.scene = new createjs.Stage(canvas); - - // add the renderer to the page - document.getElementById(this.divID)!.appendChild(canvas); - - // update at 30fps - createjs.Ticker.framerate = 30; - createjs.Ticker.addEventListener('tick', this.scene); - } - - scaleToDimensions(width: number, height: number) { - if (!this.scene) throw 'Canvas scene is undefined!' - - // save scene scaling - this.scene.scaleX = this.width / width; - this.scene.scaleY = this.height / height; - this.scene.update() - } -} \ No newline at end of file + private divID: string; + private className: string; + private width: number; + private height: number; + public scene?: createjs.Stage; + + constructor(props: { + divID: string; + className: string; + width: number; + height: number; + }) { + super(props); + this.divID = props.divID; + this.className = props.className; + this.width = props.width; + this.height = props.height; + this.createCanvas(); + } + + createCanvas() { + // create the canvas to render to + var canvas = document.createElement("canvas"); + canvas.setAttribute("class", this.className); + canvas.width = this.width; + canvas.height = this.height; + + // create the easel to use + this.scene = new createjs.Stage(canvas); + + // add the renderer to the page + document.getElementById(this.divID)!.appendChild(canvas); + + // update at 30fps + createjs.Ticker.framerate = 30; + createjs.Ticker.addEventListener("tick", this.scene); + } + + scaleToDimensions(width: number, height: number) { + if (!this.scene) throw "Canvas scene is undefined!"; + + // save scene scaling + this.scene.scaleX = this.width / width; + this.scene.scaleY = this.height / height; + this.scene.update(); + } +} diff --git a/src/pages/operator/tsx/static_components/CustomizeButton.tsx b/src/pages/operator/tsx/static_components/CustomizeButton.tsx index dff801f0..800d19c2 100644 --- a/src/pages/operator/tsx/static_components/CustomizeButton.tsx +++ b/src/pages/operator/tsx/static_components/CustomizeButton.tsx @@ -1,26 +1,25 @@ -import "operator/css/CustomizeButton.css" +import "operator/css/CustomizeButton.css"; type CustomizeButtonProps = { - /** If the interface is in customization mode */ - customizing: boolean; - /** Callback for clicking the button */ - onClick: () => void; -} - + /** If the interface is in customization mode */ + customizing: boolean; + /** Callback for clicking the button */ + onClick: () => void; +}; /** Button to toggle customization mode. */ export const CustomizeButton = (props: CustomizeButtonProps) => { - const icon = props.customizing ? "check" : "build"; - const text = props.customizing ? "Done" : "Customize"; - return ( - - ) -} -// Uses icons from https://fonts.google.com/icons \ No newline at end of file + const icon = props.customizing ? "check" : "build"; + const text = props.customizing ? "Done" : "Customize"; + return ( + + ); +}; +// Uses icons from https://fonts.google.com/icons diff --git a/src/pages/operator/tsx/static_components/LayoutArea.tsx b/src/pages/operator/tsx/static_components/LayoutArea.tsx index cd01a50b..e04ed74f 100644 --- a/src/pages/operator/tsx/static_components/LayoutArea.tsx +++ b/src/pages/operator/tsx/static_components/LayoutArea.tsx @@ -1,56 +1,66 @@ import React from "react"; -import { ComponentDefinition, LayoutDefinition, LayoutGridDefinition, PanelDefinition, ParentComponentDefinition } from 'operator/tsx/utils/component_definitions' +import { + ComponentDefinition, + LayoutDefinition, + LayoutGridDefinition, + PanelDefinition, + ParentComponentDefinition, +} from "operator/tsx/utils/component_definitions"; import { SharedState } from "../layout_components/CustomizableComponent"; -import { ComponentList, ComponentListProps } from "../layout_components/ComponentList"; -import "operator/css/LayoutArea.css" +import { + ComponentList, + ComponentListProps, +} from "../layout_components/ComponentList"; +import "operator/css/LayoutArea.css"; import { DropZone } from "../layout_components/DropZone"; /** Properties for {@link LayoutArea} */ type LayoutAreaProps = { - /** Layout structure to render */ - layout: LayoutDefinition; - sharedState: SharedState; -} + /** Layout structure to render */ + layout: LayoutDefinition; + sharedState: SharedState; +}; /** Main area of the interface where the user can add, remove, or rearrange elements. */ export const LayoutArea = (props: LayoutAreaProps) => { - // const componentListProps: ComponentListProps = { - // definition: props.layout, - // path: "", - // sharedState: props.sharedState - // } - const panelColumn = props.layout.children - const dropZoneIdx = 0 - return ( - <> - {panelColumn.map((compDef: LayoutGridDefinition, index: number) => { - return ( - // compDef.children.length > 0 ? - <> - -
    - -
    - - // : - // <> - ) - })} + // const componentListProps: ComponentListProps = { + // definition: props.layout, + // path: "", + // sharedState: props.sharedState + // } + const panelColumn = props.layout.children; + const dropZoneIdx = 0; + return ( + <> + {panelColumn.map((compDef: LayoutGridDefinition, index: number) => { + return ( + // compDef.children.length > 0 ? + <> - - ); -} - - +
    + +
    + + // : + // <> + ); + })} + + + ); +}; diff --git a/src/pages/operator/tsx/static_components/OccupancyGrid.tsx b/src/pages/operator/tsx/static_components/OccupancyGrid.tsx index b3a01d41..407cd3fa 100644 --- a/src/pages/operator/tsx/static_components/OccupancyGrid.tsx +++ b/src/pages/operator/tsx/static_components/OccupancyGrid.tsx @@ -1,4 +1,4 @@ -// Adapted from ros2djs and nav2djs +// Adapted from ros2djs and nav2djs import React from "react"; import createjs from "createjs-module"; @@ -7,330 +7,350 @@ import ROSLIB from "roslib"; import { MapFunctions } from "../layout_components/Map"; export class OccupancyGrid extends React.Component { - private rootObject: createjs.Stage - private origin?: ROSLIB.Pose - private bitmap?: createjs.Bitmap - public width: number - public height: number - private scaleX?: number - private scaleY?: number - private map: ROSOccupancyGrid - private goal_position?: ROSPoint - private goalMarker?: createjs.Shape - private getGoalReached?: NodeJS.Timer - private savedPoseMarkers: { circle: createjs.Shape, label: createjs.Text }[] - private savedPoseMarkersLabels: string[] - private functs: MapFunctions - constructor(props: { - functs: MapFunctions - rootObject: createjs.Stage - }) { - super(props); - this.rootObject = props.rootObject - this.rootObject.enableMouseOver(); - createjs.Touch.enable(this.rootObject); - this.width = 0 - this.height = 0 - this.map = props.functs.GetMap - this.functs = props.functs - this.savedPoseMarkers = [] - this.savedPoseMarkersLabels = [] - this.createOccupancyGridClient() - } - - drawSavedPoseMarker(x: number, y: number, color: number[], text: string) { - var circle = new createjs.Shape() - var radius = 30 - - var graphics = new createjs.Graphics(); - graphics.beginFill(createjs.Graphics.getRGB(color[0], color[1], color[2], 0.5)) - graphics.drawCircle(0, 0, radius) - - createjs.Shape.call(circle, graphics); - - circle.x = x - circle.y = y - circle.scaleX = 1.0 / this.rootObject.scaleX - circle.scaleY = 1.0 / this.rootObject.scaleY - - var label = new createjs.Text(text, "bold 40px Arial", "#ff7700"); - label.x = x - label.y = y - 10 - label.textAlign = "center" - label.scaleX = 1.0 / this.rootObject.scaleX - label.scaleY = 1.0 / this.rootObject.scaleY - label.textBaseline = "alphabetic"; - - circle.on("mouseover", (event) => { - label.visible = true - }) - circle.on("mouseout", (event) => { - label.visible = false - }) - return { circle, label } - } - - drawNavigationArrow(pulse: boolean, color: number[]) { - var arrow = new createjs.Shape() - var size = 40; - var strokeSize = 0; - var strokeColor = createjs.Graphics.getRGB(color[0], color[1], color[2], 0.7); - var fillColor = createjs.Graphics.getRGB(color[0], color[1], color[2], 0.7); - - // draw the arrow - var graphics = new createjs.Graphics(); - - // line width - graphics.setStrokeStyle(strokeSize); - graphics.moveTo(0.0, size / 1.5); - graphics.beginStroke(strokeColor); - graphics.beginFill(fillColor); - graphics.lineTo(-size / 2.0, -size / 2.0); - graphics.lineTo(size / 2.0, -size / 2.0); - graphics.lineTo(0.0, size / 1.5); - graphics.closePath(); - graphics.endFill(); - graphics.endStroke(); - - // create the shape - createjs.Shape.call(arrow, graphics); - - // check if we are pulsing - if (pulse) { - // have the model "pulse" - var growCount = 0; - var growing = true; - createjs.Ticker.addEventListener('tick', () => { - if (growing) { - arrow.scaleX *= 1.035; - arrow.scaleY *= 1.035; - growing = (++growCount < 10); - } - else { - arrow.scaleX /= 1.035; - arrow.scaleY /= 1.035; - growing = (--growCount < 0); - } - }); - } - return arrow - }; - - createOccupancyGrid() { - // internal drawing canvas - var canvas = document.createElement('canvas') - var context = canvas!.getContext('2d', { willReadFrequently: true }); - - if (!this.map) { - var rect = new createjs.Shape(); - rect.graphics.beginStroke('#000000'); - rect.graphics.setStrokeStyle(3) - rect.graphics.drawRect(0, 0, 300, 500); - rect.graphics.endStroke(); - var text = new createjs.Text('Could not load map', "30px Arial") - text.x = 20; - text.y = 250; - this.rootObject.addChild(rect) - this.rootObject.addChild(text) - return - } - - // save the metadata we need - this.origin = new ROSLIB.Pose({ - position: this.map.info.origin.position, - orientation: this.map.info.origin.orientation - }); - // set the size - this.width = this.map.info.width; - this.height = this.map.info.height; - canvas.width = this.width - canvas.height = this.height - - var imageData = context!.createImageData(this.width, this.height); - for (var row = 0; row < this.height; row++) { - for (var col = 0; col < this.width; col++) { - // determine the index into the map data - var mapI = col + ((this.height - row - 1) * this.width); - // determine the value - var data = this.map.data[mapI]; - var val; - if (data === 100) { - val = 0; - } - else if (data === 0) { - val = 255; - } - else { - val = 127; - } - // determine the index into the image data array - var i = (col + (row * this.width)) * 4; - // r - imageData.data[i] = val; - // g - imageData.data[++i] = val; - // b - imageData.data[++i] = val; - // a - imageData.data[++i] = 255; - } + private rootObject: createjs.Stage; + private origin?: ROSLIB.Pose; + private bitmap?: createjs.Bitmap; + public width: number; + public height: number; + private scaleX?: number; + private scaleY?: number; + private map: ROSOccupancyGrid; + private goal_position?: ROSPoint; + private goalMarker?: createjs.Shape; + private getGoalReached?: NodeJS.Timer; + private savedPoseMarkers: { circle: createjs.Shape; label: createjs.Text }[]; + private savedPoseMarkersLabels: string[]; + private functs: MapFunctions; + constructor(props: { functs: MapFunctions; rootObject: createjs.Stage }) { + super(props); + this.rootObject = props.rootObject; + this.rootObject.enableMouseOver(); + createjs.Touch.enable(this.rootObject); + this.width = 0; + this.height = 0; + this.map = props.functs.GetMap; + this.functs = props.functs; + this.savedPoseMarkers = []; + this.savedPoseMarkersLabels = []; + this.createOccupancyGridClient(); + } + + drawSavedPoseMarker(x: number, y: number, color: number[], text: string) { + var circle = new createjs.Shape(); + var radius = 30; + + var graphics = new createjs.Graphics(); + graphics.beginFill( + createjs.Graphics.getRGB(color[0], color[1], color[2], 0.5), + ); + graphics.drawCircle(0, 0, radius); + + createjs.Shape.call(circle, graphics); + + circle.x = x; + circle.y = y; + circle.scaleX = 1.0 / this.rootObject.scaleX; + circle.scaleY = 1.0 / this.rootObject.scaleY; + + var label = new createjs.Text(text, "bold 40px Arial", "#ff7700"); + label.x = x; + label.y = y - 10; + label.textAlign = "center"; + label.scaleX = 1.0 / this.rootObject.scaleX; + label.scaleY = 1.0 / this.rootObject.scaleY; + label.textBaseline = "alphabetic"; + + circle.on("mouseover", (event) => { + label.visible = true; + }); + circle.on("mouseout", (event) => { + label.visible = false; + }); + return { circle, label }; + } + + drawNavigationArrow(pulse: boolean, color: number[]) { + var arrow = new createjs.Shape(); + var size = 40; + var strokeSize = 0; + var strokeColor = createjs.Graphics.getRGB( + color[0], + color[1], + color[2], + 0.7, + ); + var fillColor = createjs.Graphics.getRGB(color[0], color[1], color[2], 0.7); + + // draw the arrow + var graphics = new createjs.Graphics(); + + // line width + graphics.setStrokeStyle(strokeSize); + graphics.moveTo(0.0, size / 1.5); + graphics.beginStroke(strokeColor); + graphics.beginFill(fillColor); + graphics.lineTo(-size / 2.0, -size / 2.0); + graphics.lineTo(size / 2.0, -size / 2.0); + graphics.lineTo(0.0, size / 1.5); + graphics.closePath(); + graphics.endFill(); + graphics.endStroke(); + + // create the shape + createjs.Shape.call(arrow, graphics); + + // check if we are pulsing + if (pulse) { + // have the model "pulse" + var growCount = 0; + var growing = true; + createjs.Ticker.addEventListener("tick", () => { + if (growing) { + arrow.scaleX *= 1.035; + arrow.scaleY *= 1.035; + growing = ++growCount < 10; + } else { + arrow.scaleX /= 1.035; + arrow.scaleY /= 1.035; + growing = --growCount < 0; } - - context!.putImageData(imageData, 0, 0); - - // create the bitmap - this.bitmap = new createjs.Bitmap(canvas); - this.rootObject.addChild(this.bitmap) - - // scale the image - this.scaleX = this.map.info.resolution; - this.scaleY = this.map.info.resolution; + }); } - - rosToGlobal(translation: ROSLIB.Vector3) { - var x = ((this.width * this.scaleX!) - (-translation.x + this.width * this.scaleX! + this.origin!.position.x)) / this.scaleX!; - var y = (-translation.y + this.height * this.scaleY! + this.origin!.position.y) / this.scaleY!; - return { - x: x, - y: y - }; + return arrow; + } + + createOccupancyGrid() { + // internal drawing canvas + var canvas = document.createElement("canvas"); + var context = canvas!.getContext("2d", { willReadFrequently: true }); + + if (!this.map) { + var rect = new createjs.Shape(); + rect.graphics.beginStroke("#000000"); + rect.graphics.setStrokeStyle(3); + rect.graphics.drawRect(0, 0, 300, 500); + rect.graphics.endStroke(); + var text = new createjs.Text("Could not load map", "30px Arial"); + text.x = 20; + text.y = 250; + this.rootObject.addChild(rect); + this.rootObject.addChild(text); + return; } - // https://github.com/RobotWebTools/ros2djs/blob/develop/src/Ros2D.js#L34C1-L44C3 - // convert a ROS quaternion to theta in degrees - rosQuaternionToGlobalTheta(orientation: ROSLIB.Quaternion) { - // See https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles#Rotation_matrices - // here we use [x y z] = R * [1 0 0] - var w = orientation.w; - var x = orientation.x; - var y = orientation.y; - var z = orientation.z; - // Canvas rotation is clock wise and in degrees - return -Math.atan2(2 * (w * z + x * y), 1 - 2 * (y * y + z * z)) * 180.0 / Math.PI; - }; - - globalToRos(x: number, y: number) { - var rosX = (x / 5) * this.scaleX! + this.origin!.position.x - var rosY = (this.height - y / 5) * this.scaleY! + this.origin!.position.y - console.log(rosX, rosY) - return { - x: rosX, - y: rosY, - z: 0 - } as ROSPoint - } - - addCurrenPoseMarker() { - var robotMarker = this.drawNavigationArrow(false, [255, 128, 0]) - this.rootObject.addChild(robotMarker) - - const setPoseInterval = setInterval(() => { - let pose = this.functs.GetPose() - let globalCoord = this.rosToGlobal(pose.translation) - robotMarker.x = globalCoord.x - robotMarker.y = globalCoord.y - let theta = this.rosQuaternionToGlobalTheta(pose.rotation) - robotMarker.rotation = theta - 90.0 - robotMarker.scaleX = 1.0 / this.rootObject.scaleX - robotMarker.scaleY = 1.0 / this.rootObject.scaleY - robotMarker.visible = true - this.rootObject.update() - }, 1000); - } - - public displayPoseMarkers(display: boolean, poses: ROSLIB.Transform[], poseNames: string[], poseTypes: string[]) { - if (!display) { - this.savedPoseMarkers.forEach(marker => { - marker.circle.visible = false - marker.label.visible = false - }) + // save the metadata we need + this.origin = new ROSLIB.Pose({ + position: this.map.info.origin.position, + orientation: this.map.info.origin.orientation, + }); + // set the size + this.width = this.map.info.width; + this.height = this.map.info.height; + canvas.width = this.width; + canvas.height = this.height; + + var imageData = context!.createImageData(this.width, this.height); + for (var row = 0; row < this.height; row++) { + for (var col = 0; col < this.width; col++) { + // determine the index into the map data + var mapI = col + (this.height - row - 1) * this.width; + // determine the value + var data = this.map.data[mapI]; + var val; + if (data === 100) { + val = 0; + } else if (data === 0) { + val = 255; } else { - // Re-draw or add pose markers - poses.forEach((pose, index) => { - // Recreate marker - let globalCoord = this.rosToGlobal(pose.translation) - let color = poseTypes[index] == "MAP" ? [0, 0, 255] : [255, 0, 0] - var poseMarker = this.drawSavedPoseMarker( - globalCoord.x, globalCoord.y, color, poseNames[index] - ) - poseMarker.circle.visible = true - poseMarker.label.visible = false - - var label_idx = this.savedPoseMarkersLabels.indexOf(poseNames[index]) - // If old pose marker label exists, overwrite marker - if (label_idx !== -1) { - var oldPoseMarker = this.savedPoseMarkers[label_idx] - this.rootObject.removeChild(oldPoseMarker.circle) - this.rootObject.removeChild(oldPoseMarker.label) - this.savedPoseMarkers[label_idx] = poseMarker - this.savedPoseMarkersLabels[label_idx] = poseNames[index] - } else { - this.savedPoseMarkers.push(poseMarker) - this.savedPoseMarkersLabels.push(poseNames[index]) - } - - this.rootObject.addChild(poseMarker.circle) - this.rootObject.addChild(poseMarker.label) - }) + val = 127; } - this.rootObject.update() + // determine the index into the image data array + var i = (col + row * this.width) * 4; + // r + imageData.data[i] = val; + // g + imageData.data[++i] = val; + // b + imageData.data[++i] = val; + // a + imageData.data[++i] = 255; + } } - public createGoalMarker(x: number, y: number, ros: boolean) { - let globalCoord = {x: x, y: y} - if (ros) globalCoord = this.rosToGlobal({x: x, y: y, z: 0} as ROSLIB.Vector3) - if (this.getGoalReached) clearInterval(this.getGoalReached) - if (this.goalMarker) this.rootObject.removeChild(this.goalMarker) - this.goalMarker = this.drawNavigationArrow(true, [255, 0, 0]) - this.goalMarker.x = globalCoord.x - this.goalMarker.y = globalCoord.y - this.goalMarker.scaleX = 1.0 / this.rootObject.scaleX - this.goalMarker.scaleY = 1.0 / this.rootObject.scaleY - this.goalMarker.visible = true - this.rootObject.addChild(this.goalMarker) - - this.getGoalReached = setInterval(() => { - if (this.functs.GoalReached()) { - this.rootObject.removeChild(this.goalMarker!) - clearInterval(this.getGoalReached) - } - }, 1000); - } - - play() { - if (this.goal_position) { - this.functs.MoveBase({ - position: this.goal_position, - orientation: { x: 0, y: 0, z: -0.45, w: 0.893 } - } as ROSPose) + context!.putImageData(imageData, 0, 0); + + // create the bitmap + this.bitmap = new createjs.Bitmap(canvas); + this.rootObject.addChild(this.bitmap); + + // scale the image + this.scaleX = this.map.info.resolution; + this.scaleY = this.map.info.resolution; + } + + rosToGlobal(translation: ROSLIB.Vector3) { + var x = + (this.width * this.scaleX! - + (-translation.x + + this.width * this.scaleX! + + this.origin!.position.x)) / + this.scaleX!; + var y = + (-translation.y + this.height * this.scaleY! + this.origin!.position.y) / + this.scaleY!; + return { + x: x, + y: y, + }; + } + + // https://github.com/RobotWebTools/ros2djs/blob/develop/src/Ros2D.js#L34C1-L44C3 + // convert a ROS quaternion to theta in degrees + rosQuaternionToGlobalTheta(orientation: ROSLIB.Quaternion) { + // See https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles#Rotation_matrices + // here we use [x y z] = R * [1 0 0] + var w = orientation.w; + var x = orientation.x; + var y = orientation.y; + var z = orientation.z; + // Canvas rotation is clock wise and in degrees + return ( + (-Math.atan2(2 * (w * z + x * y), 1 - 2 * (y * y + z * z)) * 180.0) / + Math.PI + ); + } + + globalToRos(x: number, y: number) { + var rosX = (x / 5) * this.scaleX! + this.origin!.position.x; + var rosY = (this.height - y / 5) * this.scaleY! + this.origin!.position.y; + console.log(rosX, rosY); + return { + x: rosX, + y: rosY, + z: 0, + } as ROSPoint; + } + + addCurrenPoseMarker() { + var robotMarker = this.drawNavigationArrow(false, [255, 128, 0]); + this.rootObject.addChild(robotMarker); + + const setPoseInterval = setInterval(() => { + let pose = this.functs.GetPose(); + let globalCoord = this.rosToGlobal(pose.translation); + robotMarker.x = globalCoord.x; + robotMarker.y = globalCoord.y; + let theta = this.rosQuaternionToGlobalTheta(pose.rotation); + robotMarker.rotation = theta - 90.0; + robotMarker.scaleX = 1.0 / this.rootObject.scaleX; + robotMarker.scaleY = 1.0 / this.rootObject.scaleY; + robotMarker.visible = true; + this.rootObject.update(); + }, 1000); + } + + public displayPoseMarkers( + display: boolean, + poses: ROSLIB.Transform[], + poseNames: string[], + poseTypes: string[], + ) { + if (!display) { + this.savedPoseMarkers.forEach((marker) => { + marker.circle.visible = false; + marker.label.visible = false; + }); + } else { + // Re-draw or add pose markers + poses.forEach((pose, index) => { + // Recreate marker + let globalCoord = this.rosToGlobal(pose.translation); + let color = poseTypes[index] == "MAP" ? [0, 0, 255] : [255, 0, 0]; + var poseMarker = this.drawSavedPoseMarker( + globalCoord.x, + globalCoord.y, + color, + poseNames[index], + ); + poseMarker.circle.visible = true; + poseMarker.label.visible = false; + + var label_idx = this.savedPoseMarkersLabels.indexOf(poseNames[index]); + // If old pose marker label exists, overwrite marker + if (label_idx !== -1) { + var oldPoseMarker = this.savedPoseMarkers[label_idx]; + this.rootObject.removeChild(oldPoseMarker.circle); + this.rootObject.removeChild(oldPoseMarker.label); + this.savedPoseMarkers[label_idx] = poseMarker; + this.savedPoseMarkersLabels[label_idx] = poseNames[index]; + } else { + this.savedPoseMarkers.push(poseMarker); + this.savedPoseMarkersLabels.push(poseNames[index]); } - this.goal_position = undefined - // if (isMobile) this.functs.SetSelectGoal(false) - this.functs.SetSelectGoal(false) - } - removeGoalMarker() { - console.log("removing") - this.goal_position = undefined - if (this.goalMarker) this.rootObject.removeChild(this.goalMarker) + this.rootObject.addChild(poseMarker.circle); + this.rootObject.addChild(poseMarker.label); + }); } - - createOccupancyGridClient() { - this.createOccupancyGrid() - - if (!this.map) return; - - this.addCurrenPoseMarker() - - this.rootObject.on('mousedown', (event) => { - let evt = event as createjs.MouseEvent - // convert to ROS coordinates - this.goal_position = this.globalToRos(evt.stageX, evt.stageY); - - if (this.functs.SelectGoal()) { - this.createGoalMarker(evt.stageX / 5, evt.stageY / 5, false) - this.functs.SetSelectGoal(false) - } - }); + this.rootObject.update(); + } + + public createGoalMarker(x: number, y: number, ros: boolean) { + let globalCoord = { x: x, y: y }; + if (ros) + globalCoord = this.rosToGlobal({ x: x, y: y, z: 0 } as ROSLIB.Vector3); + if (this.getGoalReached) clearInterval(this.getGoalReached); + if (this.goalMarker) this.rootObject.removeChild(this.goalMarker); + this.goalMarker = this.drawNavigationArrow(true, [255, 0, 0]); + this.goalMarker.x = globalCoord.x; + this.goalMarker.y = globalCoord.y; + this.goalMarker.scaleX = 1.0 / this.rootObject.scaleX; + this.goalMarker.scaleY = 1.0 / this.rootObject.scaleY; + this.goalMarker.visible = true; + this.rootObject.addChild(this.goalMarker); + + this.getGoalReached = setInterval(() => { + if (this.functs.GoalReached()) { + this.rootObject.removeChild(this.goalMarker!); + clearInterval(this.getGoalReached); + } + }, 1000); + } + + play() { + if (this.goal_position) { + this.functs.MoveBase({ + position: this.goal_position, + orientation: { x: 0, y: 0, z: -0.45, w: 0.893 }, + } as ROSPose); } -} \ No newline at end of file + this.goal_position = undefined; + // if (isMobile) this.functs.SetSelectGoal(false) + this.functs.SetSelectGoal(false); + } + + removeGoalMarker() { + console.log("removing"); + this.goal_position = undefined; + if (this.goalMarker) this.rootObject.removeChild(this.goalMarker); + } + + createOccupancyGridClient() { + this.createOccupancyGrid(); + + if (!this.map) return; + + this.addCurrenPoseMarker(); + + this.rootObject.on("mousedown", (event) => { + let evt = event as createjs.MouseEvent; + // convert to ROS coordinates + this.goal_position = this.globalToRos(evt.stageX, evt.stageY); + + if (this.functs.SelectGoal()) { + this.createGoalMarker(evt.stageX / 5, evt.stageY / 5, false); + this.functs.SetSelectGoal(false); + } + }); + } +} diff --git a/src/pages/operator/tsx/static_components/RunStop.tsx b/src/pages/operator/tsx/static_components/RunStop.tsx index 442da811..08c970ce 100644 --- a/src/pages/operator/tsx/static_components/RunStop.tsx +++ b/src/pages/operator/tsx/static_components/RunStop.tsx @@ -1,25 +1,29 @@ -import "operator/css/RunStopButton.css" +import "operator/css/RunStopButton.css"; import { className } from "shared/util"; import { runStopFunctionProvider } from ".."; import { RunStopFunctions } from "../function_providers/RunStopFunctionProvider"; import { CustomizableComponentProps } from "../layout_components/CustomizableComponent"; -import runStopButton from "operator/icons/button.svg" +import runStopButton from "operator/icons/button.svg"; import React, { useState } from "react"; export const RunStopButton = (props: CustomizableComponentProps) => { - const functs: RunStopFunctions = runStopFunctionProvider.provideFunctions(); - const [enabled, setEnabled] = useState(false); + const functs: RunStopFunctions = runStopFunctionProvider.provideFunctions(); + const [enabled, setEnabled] = useState(false); - runStopFunctionProvider.setRunStopStateChangeCallback(setEnabled) - - return ( -
    - - {enabled ? Run Stop: Enabled : Run Stop: Disabled} -
    - ) -} \ No newline at end of file + runStopFunctionProvider.setRunStopStateChangeCallback(setEnabled); + + return ( +
    + + {enabled ? ( + Run Stop: Enabled + ) : ( + Run Stop: Disabled + )} +
    + ); +}; diff --git a/src/pages/operator/tsx/static_components/Sidebar.tsx b/src/pages/operator/tsx/static_components/Sidebar.tsx index 3665ad86..02b6a035 100644 --- a/src/pages/operator/tsx/static_components/Sidebar.tsx +++ b/src/pages/operator/tsx/static_components/Sidebar.tsx @@ -1,62 +1,78 @@ -import React from "react" +import React from "react"; import { className } from "shared/util"; -import { ButtonPadDefinition, ButtonPadId, ComponentDefinition, ComponentId, ComponentType, LayoutDefinition, ParentComponentDefinition, TabDefinition, PanelDefinition, CameraViewDefinition, CameraViewId, MapDefinition } from "../utils/component_definitions"; +import { + ButtonPadDefinition, + ButtonPadId, + ComponentDefinition, + ComponentId, + ComponentType, + LayoutDefinition, + ParentComponentDefinition, + TabDefinition, + PanelDefinition, + CameraViewDefinition, + CameraViewId, + MapDefinition, +} from "../utils/component_definitions"; import { PopupModal } from "../basic_components/PopupModal"; import { Dropdown } from "../basic_components/Dropdown"; -import "operator/css/Sidebar.css" +import "operator/css/Sidebar.css"; import { storageHandler } from "operator/tsx/index"; type SidebarProps = { - hidden: boolean; - onDelete: () => void; - updateLayout: () => void; - onSelect: (def: ComponentDefinition, path?: string) => void; - selectedDefinition?: ComponentDefinition; - selectedPath?: string; - globalOptionsProps: GlobalOptionsProps -} + hidden: boolean; + onDelete: () => void; + updateLayout: () => void; + onSelect: (def: ComponentDefinition, path?: string) => void; + selectedDefinition?: ComponentDefinition; + selectedPath?: string; + globalOptionsProps: GlobalOptionsProps; +}; /** Popup on the right side of the screen while in customization mode. */ export const Sidebar = (props: SidebarProps) => { - const deleteDisabled = props.selectedPath === undefined; - const deleteTooltip = deleteDisabled ? "You must select an element before you can delete it" : ""; - const selectedDescription = props.selectedDefinition ? componentDescription(props.selectedDefinition) : "none"; - return ( - - ) -} + const deleteDisabled = props.selectedPath === undefined; + const deleteTooltip = deleteDisabled + ? "You must select an element before you can delete it" + : ""; + const selectedDescription = props.selectedDefinition + ? componentDescription(props.selectedDefinition) + : "none"; + return ( + + ); +}; /** * Creates a text description based on a component definition @@ -64,20 +80,22 @@ export const Sidebar = (props: SidebarProps) => { * @returns string description of the component */ function componentDescription(definition: ComponentDefinition): string { - switch (definition.type) { - case (ComponentType.ButtonPad): - case (ComponentType.CameraView): - return `${(definition as CameraViewDefinition | ButtonPadDefinition).id} ${definition.type}` - case (ComponentType.SingleTab): - return `\"${(definition as TabDefinition).label}\" Tab`; - case (ComponentType.Panel): - case (ComponentType.VirtualJoystick): - case (ComponentType.ButtonGrid): - case (ComponentType.Map): - return definition.type; - default: - throw Error(`Cannot get description for component type ${definition.type}\nYou may need to add a case for this component in the switch statement.`) - } + switch (definition.type) { + case ComponentType.ButtonPad: + case ComponentType.CameraView: + return `${(definition as CameraViewDefinition | ButtonPadDefinition).id} ${definition.type}`; + case ComponentType.SingleTab: + return `\"${(definition as TabDefinition).label}\" Tab`; + case ComponentType.Panel: + case ComponentType.VirtualJoystick: + case ComponentType.ButtonGrid: + case ComponentType.Map: + return definition.type; + default: + throw Error( + `Cannot get description for component type ${definition.type}\nYou may need to add a case for this component in the switch statement.`, + ); + } } /******************************************************************************* @@ -86,326 +104,343 @@ function componentDescription(definition: ComponentDefinition): string { /** Properties for {@link SidebarGlobalOptions} */ export type GlobalOptionsProps = { - /** If the save/load poses should be displayed. */ - displayMovementRecorder: boolean; - setDisplayMovementRecorder: (displayMovementRecorder: boolean) => void; - - /** If the button text labels should be displayed */ - displayLabels: boolean; - setDisplayLabels: (displayLabels: boolean) => void; - - /** List of names of the default layouts. */ - defaultLayouts: string[], - /** List of names of the user's custom layouts. */ - customLayouts: string[], - /** - * Callback when the user loads a layout. - * @param layoutName name of the layout to load - * @param dflt if it's a default layout, if false then it's a custom layout. - */ - loadLayout: (layoutName: string, dflt: boolean) => void, - /** - * Callback when the user saves a layout. - * @param layoutName name of the layout to save. - */ - saveLayout: (layoutName: string) => void, -} + /** If the save/load poses should be displayed. */ + displayMovementRecorder: boolean; + setDisplayMovementRecorder: (displayMovementRecorder: boolean) => void; + + /** If the button text labels should be displayed */ + displayLabels: boolean; + setDisplayLabels: (displayLabels: boolean) => void; + + /** List of names of the default layouts. */ + defaultLayouts: string[]; + /** List of names of the user's custom layouts. */ + customLayouts: string[]; + /** + * Callback when the user loads a layout. + * @param layoutName name of the layout to load + * @param dflt if it's a default layout, if false then it's a custom layout. + */ + loadLayout: (layoutName: string, dflt: boolean) => void; + /** + * Callback when the user saves a layout. + * @param layoutName name of the layout to save. + */ + saveLayout: (layoutName: string) => void; +}; /** Options which apply to the entire operator page. */ const SidebarGlobalOptions = (props: GlobalOptionsProps) => { - const [showLoadLayoutModal, setShowLoadLayoutModal] = React.useState(false); - const [showSaveLayoutModal, setShowSaveLayoutModal] = React.useState(false); - - return ( - -
    - {/*

    Global settings:

    */} - props.setDisplayLabels(!props.displayLabels)} - label="Display button text labels" - /> - props.setDisplayMovementRecorder(!props.displayMovementRecorder)} - label="Display movement recorder" - /> - - -
    - - -
    - ) -} + const [showLoadLayoutModal, setShowLoadLayoutModal] = + React.useState(false); + const [showSaveLayoutModal, setShowSaveLayoutModal] = + React.useState(false); + + return ( + +
    + {/*

    Global settings:

    */} + props.setDisplayLabels(!props.displayLabels)} + label="Display button text labels" + /> + + props.setDisplayMovementRecorder(!props.displayMovementRecorder) + } + label="Display movement recorder" + /> + + +
    + + +
    + ); +}; /** Popup so the user can load a default layout or one of their custom layouts. */ const LoadLayoutModal = (props: { - defaultLayouts: string[], - customLayouts: string[], - loadLayout: (layoutName: string, dflt: boolean) => void, - setShow: (show: boolean) => void, - show: boolean, + defaultLayouts: string[]; + customLayouts: string[]; + loadLayout: (layoutName: string, dflt: boolean) => void; + setShow: (show: boolean) => void; + show: boolean; }) => { - const [selectedIdx, setSelectedIdx] = React.useState(); - - function handleAccept() { - if (selectedIdx === undefined) return; - let dflt: boolean, layoutName: string; - if (selectedIdx < props.defaultLayouts.length) { - layoutName = props.defaultLayouts[selectedIdx]; - dflt = true; - } else { - layoutName = props.customLayouts[selectedIdx - props.defaultLayouts.length]; - dflt = false; - } - console.log('loading layout', layoutName, dflt); - props.loadLayout(layoutName, dflt); - - } - - function mapFunct(layoutName: string, dflt: boolean) { - const prefix = dflt ? "DEFAULT" : "CUSTOM"; - return

    {prefix} {layoutName}

    + const [selectedIdx, setSelectedIdx] = React.useState(); + + function handleAccept() { + if (selectedIdx === undefined) return; + let dflt: boolean, layoutName: string; + if (selectedIdx < props.defaultLayouts.length) { + layoutName = props.defaultLayouts[selectedIdx]; + dflt = true; + } else { + layoutName = + props.customLayouts[selectedIdx - props.defaultLayouts.length]; + dflt = false; } + console.log("loading layout", layoutName, dflt); + props.loadLayout(layoutName, dflt); + } - const defaultOptions = props.defaultLayouts.map(layoutName => mapFunct(layoutName, true)); - const customOptions = props.customLayouts.map(layoutName => mapFunct(layoutName, false)); - + function mapFunct(layoutName: string, dflt: boolean) { + const prefix = dflt ? "DEFAULT" : "CUSTOM"; return ( - - - - - ) -} +

    + {prefix} {layoutName} +

    + ); + } + + const defaultOptions = props.defaultLayouts.map((layoutName) => + mapFunct(layoutName, true), + ); + const customOptions = props.customLayouts.map((layoutName) => + mapFunct(layoutName, false), + ); + + return ( + + + + + ); +}; /** Popup so the user can save their current layout. */ const SaveLayoutModal = (props: { - saveLayout: (layoutName: string) => void, - customLayouts: string[], - setShow: (show: boolean) => void, - show: boolean, + saveLayout: (layoutName: string) => void; + customLayouts: string[]; + setShow: (show: boolean) => void; + show: boolean; }) => { - const [name, setName] = React.useState(""); - function handleAccept() { - if (name.length > 0) { - props.saveLayout(name); - props.customLayouts.push(name) - } - setName(""); + const [name, setName] = React.useState(""); + function handleAccept() { + if (name.length > 0) { + props.saveLayout(name); + props.customLayouts.push(name); } - return ( - - - setName(e.target.value)} - placeholder="Name for this layout" - /> - - ) -} + setName(""); + } + return ( + + + setName(e.target.value)} + placeholder="Name for this layout" + /> + + ); +}; /******************************************************************************* * Component specific options */ type OptionsProps = { - /** Definition of the currently selected component from operator. */ - selectedDefinition: ComponentDefinition; - /** Callback to rerender the layout in operator. */ - updateLayout: () => void; -} + /** Definition of the currently selected component from operator. */ + selectedDefinition: ComponentDefinition; + /** Callback to rerender the layout in operator. */ + updateLayout: () => void; +}; /** Displays options for the currently selected layout component. */ const SidebarOptions = (props: OptionsProps) => { - let contents: JSX.Element | null = null; - switch (props.selectedDefinition.type) { - case (ComponentType.CameraView): - switch ((props.selectedDefinition as CameraViewDefinition).id!) { - case (CameraViewId.overhead): - contents = ; - break; - case (CameraViewId.realsense): - contents = ; - break; - case (CameraViewId.gripper): - contents = ; - break; - } - break; - case (ComponentType.SingleTab): - contents = ; - } - return ( - - ) -} + let contents: JSX.Element | null = null; + switch (props.selectedDefinition.type) { + case ComponentType.CameraView: + switch ((props.selectedDefinition as CameraViewDefinition).id!) { + case CameraViewId.overhead: + contents = ; + break; + case CameraViewId.realsense: + contents = ; + break; + case CameraViewId.gripper: + contents = ; + break; + } + break; + case ComponentType.SingleTab: + contents = ; + } + return ; +}; /** Options for the overhead camera video stream layout component. */ const OverheadVideoStreamOptions = (props: OptionsProps) => { - const definition = props.selectedDefinition as CameraViewDefinition; - const pd = definition.children.length > 0 && definition.children[0].type == ComponentType.PredictiveDisplay; - const [predictiveDisplayOn, setPredictiveDisplayOn] = React.useState(pd); - const [showButtons, setShowButtons] = React.useState(true); - - function togglePredictiveDisplay() { - const newPdOn = !predictiveDisplayOn; - setPredictiveDisplayOn(newPdOn); - if (newPdOn) { - // Add predictive display to the stream - definition.children = [{ type: ComponentType.PredictiveDisplay }]; - } else { - definition.children = []; - } - props.updateLayout(); + const definition = props.selectedDefinition as CameraViewDefinition; + const pd = + definition.children.length > 0 && + definition.children[0].type == ComponentType.PredictiveDisplay; + const [predictiveDisplayOn, setPredictiveDisplayOn] = React.useState(pd); + const [showButtons, setShowButtons] = React.useState(true); + + function togglePredictiveDisplay() { + const newPdOn = !predictiveDisplayOn; + setPredictiveDisplayOn(newPdOn); + if (newPdOn) { + // Add predictive display to the stream + definition.children = [{ type: ComponentType.PredictiveDisplay }]; + } else { + definition.children = []; } - - function toggleButtons() { - setShowButtons(!showButtons); - definition.displayButtons = showButtons - props.updateLayout(); - } - - return ( - - {/* + {/* */} - - - ) -} + + + ); +}; /** Options for the camera video stream layout component. */ const VideoStreamOptions = (props: OptionsProps) => { - const definition = props.selectedDefinition as CameraViewDefinition; - const [showButtons, setShowButtons] = React.useState(true); - - function toggleButtons() { - setShowButtons(!showButtons); - definition.displayButtons = showButtons - props.updateLayout(); - } - - return ( - - - - ) -} + const definition = props.selectedDefinition as CameraViewDefinition; + const [showButtons, setShowButtons] = React.useState(true); + + function toggleButtons() { + setShowButtons(!showButtons); + definition.displayButtons = showButtons; + props.updateLayout(); + } + + return ( + + + + ); +}; /** Options when user selects a single tab within a panel. */ const TabOptions = (props: OptionsProps) => { - const definition = props.selectedDefinition as TabDefinition; - const [showRenameModal, setShowRenameModal] = React.useState(false); - const [renameText, setRenameText] = React.useState(""); - function handleRename() { - if (renameText.length > 0) { - definition.label = renameText; - props.updateLayout(); - } - setRenameText(""); + const definition = props.selectedDefinition as TabDefinition; + const [showRenameModal, setShowRenameModal] = React.useState(false); + const [renameText, setRenameText] = React.useState(""); + function handleRename() { + if (renameText.length > 0) { + definition.label = renameText; + props.updateLayout(); } - return ( - - - - - setRenameText(e.target.value)} - placeholder={definition.label} - /> - - - ) -} + setRenameText(""); + } + return ( + + + + + setRenameText(e.target.value)} + placeholder={definition.label} + /> + + + ); +}; /** Properties for {@link OnOffToggleButton} */ type OnOffToggleButtonProps = { - on: boolean; - /** Callback when the button is clicked */ - onClick: () => void; - /** Text label to display to the right of the on/off button. */ - label: string; -} + on: boolean; + /** Callback when the button is clicked */ + onClick: () => void; + /** Text label to display to the right of the on/off button. */ + label: string; +}; /** A single toggle button with a color and on/off label corresponding to it's state. */ const OnOffToggleButton = (props: OnOffToggleButtonProps) => { - const text = props.on ? "on" : "off"; - const colorClass = props.on ? "btn-turquoise font-white" : "btn-red"; - return ( -
    - - {props.label} -
    - ); -} + const text = props.on ? "on" : "off"; + const colorClass = props.on ? "btn-turquoise font-white" : "btn-red"; + return ( +
    + + {props.label} +
    + ); +}; /******************************************************************************* * Component provider @@ -413,123 +448,121 @@ const OnOffToggleButton = (props: OnOffToggleButtonProps) => { /** Properties for {@link SidebarComponentProvider} */ type SidebarComponentProviderProps = { - /** Definition of the currently selected component from operator. */ - selectedDefinition?: ComponentDefinition; - /** Callback function when a component is selected from the sidebar. */ - onSelect: (def: ComponentDefinition, path?: string) => void; -} + /** Definition of the currently selected component from operator. */ + selectedDefinition?: ComponentDefinition; + /** Callback function when a component is selected from the sidebar. */ + onSelect: (def: ComponentDefinition, path?: string) => void; +}; /** Displays all the components which can be added to the interface */ const SidebarComponentProvider = (props: SidebarComponentProviderProps) => { - const [expandedType, setExpandedType] = React.useState(); - - /** The options for possible components to add */ - const outlines: ComponentProviderTabOutline[] = [ - { type: ComponentType.Panel }, - { type: ComponentType.CameraView, ids: Object.values(CameraViewId) }, - { type: ComponentType.ButtonPad, ids: Object.values(ButtonPadId) }, - { type: ComponentType.ButtonGrid }, - { type: ComponentType.VirtualJoystick }, - { type: ComponentType.Map } - ]; - - function handleSelect(type: ComponentType, id?: ComponentId) { - const definition: ComponentDefinition = id ? { type, id } : { type }; - - // Add children based on the component type - switch (type) { - case (ComponentType.Panel): - case (ComponentType.CameraView): - (definition as ParentComponentDefinition).children = [] - break; - case (ComponentType.Map): - (definition as MapDefinition).storageHandler = storageHandler - break; - } - - props.onSelect(definition); - } - - function mapTabs(outline: ComponentProviderTabOutline) { - const expanded = outline.type === expandedType; - const tabProps: ComponentProviderTabProps = { - ...outline, - expanded, - selectedDefinition: props.selectedDefinition, - onExpand: () => setExpandedType(expanded ? undefined : outline.type), - onSelect: (id?: ComponentId) => handleSelect(outline.type, id) - } - return + const [expandedType, setExpandedType] = React.useState(); + + /** The options for possible components to add */ + const outlines: ComponentProviderTabOutline[] = [ + { type: ComponentType.Panel }, + { type: ComponentType.CameraView, ids: Object.values(CameraViewId) }, + { type: ComponentType.ButtonPad, ids: Object.values(ButtonPadId) }, + { type: ComponentType.ButtonGrid }, + { type: ComponentType.VirtualJoystick }, + { type: ComponentType.Map }, + ]; + + function handleSelect(type: ComponentType, id?: ComponentId) { + const definition: ComponentDefinition = id ? { type, id } : { type }; + + // Add children based on the component type + switch (type) { + case ComponentType.Panel: + case ComponentType.CameraView: + (definition as ParentComponentDefinition).children = []; + break; + case ComponentType.Map: + (definition as MapDefinition).storageHandler = storageHandler; + break; } - return ( - - ) -} + props.onSelect(definition); + } + + function mapTabs(outline: ComponentProviderTabOutline) { + const expanded = outline.type === expandedType; + const tabProps: ComponentProviderTabProps = { + ...outline, + expanded, + selectedDefinition: props.selectedDefinition, + onExpand: () => setExpandedType(expanded ? undefined : outline.type), + onSelect: (id?: ComponentId) => handleSelect(outline.type, id), + }; + return ; + } + + return ( + + ); +}; /** An outline representing a component provider tab. */ type ComponentProviderTabOutline = { - /** The type of component this tab represents. */ - type: ComponentType, - /** - * The list of different identifiers for this component type. Is undefined - * when a component doesn't have sub identifiers, for example a Panel component. - */ - ids?: ComponentId[] -} + /** The type of component this tab represents. */ + type: ComponentType; + /** + * The list of different identifiers for this component type. Is undefined + * when a component doesn't have sub identifiers, for example a Panel component. + */ + ids?: ComponentId[]; +}; /** Properties for a single tab representing a single component type. */ type ComponentProviderTabProps = ComponentProviderTabOutline & { - expanded: boolean; - selectedDefinition?: ComponentDefinition; - onSelect: (id?: ComponentId) => void; - onExpand: () => void; -} + expanded: boolean; + selectedDefinition?: ComponentDefinition; + onSelect: (id?: ComponentId) => void; + onExpand: () => void; +}; -/** - * Displays a single dropdown tab within the component provider. If the ids field - * in `props` is undefined then this represents a component without seperate +/** + * Displays a single dropdown tab within the component provider. If the ids field + * in `props` is undefined then this represents a component without separate * identifiers, so it will be a button without a dropdown. */ const ComponentProviderTab = (props: ComponentProviderTabProps) => { - const tabActive = props.type === props.selectedDefinition?.type; - function mapIds(id: ComponentId) { - const active = tabActive && id === props.selectedDefinition?.id; - return ( - - -
    - ) -} \ No newline at end of file + + ); + } + + function clickExpand() { + console.log("click", props.type); + props.ids ? props.onExpand() : props.onSelect(); + } + + return ( +
    + + +
    + ); +}; diff --git a/src/pages/operator/tsx/static_components/SpeedControl.tsx b/src/pages/operator/tsx/static_components/SpeedControl.tsx index 01284053..383a57b2 100644 --- a/src/pages/operator/tsx/static_components/SpeedControl.tsx +++ b/src/pages/operator/tsx/static_components/SpeedControl.tsx @@ -3,31 +3,31 @@ import "operator/css/SpeedControl.css"; /**Details of a velocity setting */ type VelocityDetails = { - /**Name of the setting to display on the button */ - label: string, - /**The speed of this setting */ - scale: number + /**Name of the setting to display on the button */ + label: string; + /**The speed of this setting */ + scale: number; }; /**Props for {@link SpeedControl} */ type SpeedControlProps = { - /** Initial speed when interface first loaded. */ - scale: number; - /** - * Callback function when a new speed is selected. - * @param newSpeed the new selected speed - */ - onChange: (newScale: number) => void; -} + /** Initial speed when interface first loaded. */ + scale: number; + /** + * Callback function when a new speed is selected. + * @param newSpeed the new selected speed + */ + onChange: (newScale: number) => void; +}; /**The different velocity settings to display. */ export const VELOCITY_SCALE: VelocityDetails[] = [ - { label: "Slowest", scale: 0.2 }, - { label: "Slow", scale: 0.4 }, - { label: "Medium", scale: 0.8 }, - { label: "Fast", scale: 1.2 }, - { label: "Fastest", scale: 1.6 } -] + { label: "Slowest", scale: 0.2 }, + { label: "Slow", scale: 0.4 }, + { label: "Medium", scale: 0.8 }, + { label: "Fast", scale: 1.2 }, + { label: "Fastest", scale: 1.6 }, +]; /**The speed the interface should initialize with */ export const DEFAULT_VELOCITY_SCALE: number = VELOCITY_SCALE[2].scale; @@ -37,21 +37,21 @@ export const DEFAULT_VELOCITY_SCALE: number = VELOCITY_SCALE[2].scale; * @param props see {@link SpeedControlProps} */ export const SpeedControl = (props: SpeedControlProps) => { - /** Maps the velocity labels and speeds to radio buttons */ - const mapFunc = ({ scale, label }: VelocityDetails) => { - const active = scale === props.scale; - return ( - - ) - } - + /** Maps the velocity labels and speeds to radio buttons */ + const mapFunc = ({ scale, label }: VelocityDetails) => { + const active = scale === props.scale; return ( -
    - {VELOCITY_SCALE.map(mapFunc)} -
    + ); -} \ No newline at end of file + }; + + return ( +
    {VELOCITY_SCALE.map(mapFunc)}
    + ); +}; diff --git a/src/pages/operator/tsx/static_components/Swipe.tsx b/src/pages/operator/tsx/static_components/Swipe.tsx index b828380e..08e7bdf1 100644 --- a/src/pages/operator/tsx/static_components/Swipe.tsx +++ b/src/pages/operator/tsx/static_components/Swipe.tsx @@ -1,47 +1,48 @@ // https://stackoverflow.com/questions/70612769/how-do-i-recognize-swipe-events-in-react -import {TouchEvent, useState} from "react"; +import { TouchEvent, useState } from "react"; interface SwipeInput { - onSwipedLeft: () => void - onSwipedRight: () => void + onSwipedLeft: () => void; + onSwipedRight: () => void; } interface SwipeOutput { - onTouchStart: (e: TouchEvent) => void - onTouchMove: (e: TouchEvent) => void - onTouchEnd: () => void + onTouchStart: (e: TouchEvent) => void; + onTouchMove: (e: TouchEvent) => void; + onTouchEnd: () => void; } export default (input: SwipeInput): SwipeOutput => { - const [touchStart, setTouchStart] = useState(0); - const [touchEnd, setTouchEnd] = useState(0); - - const minSwipeDistance = 50; - - const onTouchStart = (e: TouchEvent) => { - setTouchEnd(0); // otherwise the swipe is fired even with usual touch events - setTouchStart(e.targetTouches[0].clientX); + const [touchStart, setTouchStart] = useState(0); + const [touchEnd, setTouchEnd] = useState(0); + + const minSwipeDistance = 50; + + const onTouchStart = (e: TouchEvent) => { + setTouchEnd(0); // otherwise the swipe is fired even with usual touch events + setTouchStart(e.targetTouches[0].clientX); + }; + + const onTouchMove = (e: TouchEvent) => + setTouchEnd(e.targetTouches[0].clientX); + + const onTouchEnd = () => { + if (!touchStart || !touchEnd) return; + const distance = touchStart - touchEnd; + const isLeftSwipe = distance > minSwipeDistance; + const isRightSwipe = distance < -minSwipeDistance; + if (isLeftSwipe) { + input.onSwipedLeft(); } - - const onTouchMove = (e: TouchEvent) => setTouchEnd(e.targetTouches[0].clientX); - - const onTouchEnd = () => { - if (!touchStart || !touchEnd) return; - const distance = touchStart - touchEnd; - const isLeftSwipe = distance > minSwipeDistance; - const isRightSwipe = distance < -minSwipeDistance; - if (isLeftSwipe) { - input.onSwipedLeft(); - } - if (isRightSwipe) { - input.onSwipedRight(); - } - } - - return { - onTouchStart, - onTouchMove, - onTouchEnd + if (isRightSwipe) { + input.onSwipedRight(); } -} \ No newline at end of file + }; + + return { + onTouchStart, + onTouchMove, + onTouchEnd, + }; +}; diff --git a/src/pages/operator/tsx/static_components/Tooltip.tsx b/src/pages/operator/tsx/static_components/Tooltip.tsx index e15e0765..349846e0 100644 --- a/src/pages/operator/tsx/static_components/Tooltip.tsx +++ b/src/pages/operator/tsx/static_components/Tooltip.tsx @@ -4,19 +4,17 @@ import React from "react"; import "operator/css/Tooltip.css"; type TooltipProps = { - children: any, - text: string, - divProps?: { [x: string]: any } - position: "top" | "bottom" | "left" | "right" -} + children: any; + text: string; + divProps?: { [x: string]: any }; + position: "top" | "bottom" | "left" | "right"; +}; export const Tooltip = (props: TooltipProps) => { - return ( -
    - {props.children} -
    - {props.text} -
    -
    - ); -} \ No newline at end of file + return ( +
    + {props.children} +
    {props.text}
    +
    + ); +}; diff --git a/src/pages/operator/tsx/storage_handler/FirebaseStorageHandler.tsx b/src/pages/operator/tsx/storage_handler/FirebaseStorageHandler.tsx index afca50c4..a61e154b 100644 --- a/src/pages/operator/tsx/storage_handler/FirebaseStorageHandler.tsx +++ b/src/pages/operator/tsx/storage_handler/FirebaseStorageHandler.tsx @@ -1,229 +1,257 @@ import { StorageHandler } from "./StorageHandler"; import { LayoutDefinition } from "../utils/component_definitions"; -import { FirebaseOptions, FirebaseError, initializeApp, FirebaseApp } from "firebase/app"; -import { Auth, getAuth, User, signInWithPopup, GoogleAuthProvider, onAuthStateChanged } from 'firebase/auth' -import { Database, getDatabase, child, get, ref, update, push } from 'firebase/database' +import { + FirebaseOptions, + FirebaseError, + initializeApp, + FirebaseApp, +} from "firebase/app"; +import { + Auth, + getAuth, + User, + signInWithPopup, + GoogleAuthProvider, + onAuthStateChanged, +} from "firebase/auth"; +import { + Database, + getDatabase, + child, + get, + ref, + update, + push, +} from "firebase/database"; import { ArucoMarkersInfo, RobotPose } from "shared/util"; import ROSLIB from "roslib"; /** Uses Firebase to store data. */ export class FirebaseStorageHandler extends StorageHandler { - private config: FirebaseOptions; - private app: FirebaseApp; - private database: Database; - private auth: Auth; - private GAuthProvider: GoogleAuthProvider; - - private userEmail: string; - private uid: string; - private layouts: { [name: string]: LayoutDefinition }; - private currentLayout: LayoutDefinition | null; - private poses: { [name: string]: RobotPose }; - private mapPoses: { [name: string]: ROSLIB.Transform }; - private mapPoseTypes: { [name: string]: string }; - private recordings: { [name: string]: RobotPose[] }; - private markerNames: string[]; - private markerIDs: string[]; - private markerInfo: ArucoMarkersInfo; - - constructor(onStorageHandlerReadyCallback: () => void, config: FirebaseOptions) { - super(onStorageHandlerReadyCallback); - this.config = config; - this.app = initializeApp(this.config); - this.database = getDatabase(this.app); - this.auth = getAuth(this.app); - this.GAuthProvider = new GoogleAuthProvider(); - - this.userEmail = "" - this.uid = "" - this.layouts = {}; - this.currentLayout = null; - this.poses = {}; - this.mapPoses = {} - this.mapPoseTypes = {} - this.recordings = {} - this.markerNames = [] - this.markerIDs = [] - this.markerInfo = {} as ArucoMarkersInfo - onAuthStateChanged(this.auth, (user) => this.handleAuthStateChange(user)); - - this.signInWithGoogle() - } - - private handleAuthStateChange(user: User | null) { - if (user) { - this.uid = user.uid; - this.userEmail = user.email!; - - this.getUserDataFirebase().then(async (userData) => { - this.layouts = userData.layouts; - this.currentLayout = userData.currentLayout - this.mapPoses = userData.map_poses - this.mapPoseTypes = userData.map_pose_types - this.recordings = userData.recordings - - this.onReadyCallback() - }).catch((error) => { - console.log("Detected that FirebaseModel isn't initialized for user ", this.uid); - this.onReadyCallback() - }) - } - } - - private async getUserDataFirebase() { - const snapshot = await get(child(ref(this.database), '/users/' + (this.uid))) - - if (snapshot.exists()) { - return snapshot.val(); - } else { - throw "No data available"; - } - } - - public signInWithGoogle() { - if (this.userEmail == "") { - signInWithPopup(this.auth, this.GAuthProvider) - .then((result) => { - const credential = GoogleAuthProvider.credentialFromResult(result); - const token = credential!.accessToken; - const user = result.user; - return Promise.resolve() - }) - .catch(this.handleError); - } - } - - private handleError(error: FirebaseError) { - const errorCode = error.code; - const errorMessage = error.message; - console.error("firebaseError: " + errorCode + ": " + errorMessage); - console.trace(); - return Promise.reject() - } - - public loadCustomLayout(layoutName: string): LayoutDefinition { - let layout = this.layouts![layoutName] - if (!layout) throw Error(`Could not load custom layout ${layoutName}`); - return JSON.parse(JSON.stringify(layout)); - } - - public saveCustomLayout(layout: LayoutDefinition, layoutName: string): void { - this.layouts[layoutName] = layout - this.writeLayouts(this.layouts) - } - - public saveCurrentLayout(layout: LayoutDefinition): void { - this.currentLayout = layout - - let updates: any = {}; - updates['/users/' + (this.uid) + '/currentLayout'] = layout; - update(ref(this.database), updates); - } - - public loadCurrentLayout(): LayoutDefinition | null { - return this.currentLayout - } - - public getCustomLayoutNames(): string[] { - if (!this.layouts) return [] - return Object.keys(this.layouts) - } - - private async writeLayouts(layouts: { [name: string]: LayoutDefinition }) { - this.layouts = layouts; - - let updates: any = {}; - updates['/users/' + (this.uid) + '/layouts'] = layouts; - return update(ref(this.database), updates); - } - - public getMapPoseNames(): string[] { - if (!this.mapPoses) return [] - return Object.keys(this.mapPoses) - } - - public saveMapPose(name: string, pose: ROSLIB.Transform, poseType: string) { - this.mapPoses[name] = pose - this.mapPoseTypes[name] = poseType - this.writeMapPoses(this.mapPoses) - this.writeMapPoseTypes(this.mapPoseTypes) - } - - private async writeMapPoses(poses: { [name: string]: ROSLIB.Transform }) { - this.mapPoses = poses; - - let updates: any = {}; - updates['/users/' + (this.uid) + '/map_poses'] = poses; - return update(ref(this.database), updates); - } - - private async writeMapPoseTypes(poseTypes: { [name: string]: string }) { - this.mapPoseTypes = poseTypes; - - let updates: any = {}; - updates['/users/' + (this.uid) + '/map_pose_types'] = poseTypes; - return update(ref(this.database), updates); - } - - public getMapPose(poseName: string): ROSLIB.Transform { - let pose = this.mapPoses![poseName] - if (!pose) throw Error(`Could not load pose ${poseName}`); - return JSON.parse(JSON.stringify(pose)); - } - - public getMapPoses(): ROSLIB.Transform[] { - const poseNames = this.getMapPoseNames() - var poses: ROSLIB.Transform[] = [] - poseNames.forEach(poseName => { - const pose = this.getMapPose(poseName) - poses.push(pose) + private config: FirebaseOptions; + private app: FirebaseApp; + private database: Database; + private auth: Auth; + private GAuthProvider: GoogleAuthProvider; + + private userEmail: string; + private uid: string; + private layouts: { [name: string]: LayoutDefinition }; + private currentLayout: LayoutDefinition | null; + private poses: { [name: string]: RobotPose }; + private mapPoses: { [name: string]: ROSLIB.Transform }; + private mapPoseTypes: { [name: string]: string }; + private recordings: { [name: string]: RobotPose[] }; + private markerNames: string[]; + private markerIDs: string[]; + private markerInfo: ArucoMarkersInfo; + + constructor( + onStorageHandlerReadyCallback: () => void, + config: FirebaseOptions, + ) { + super(onStorageHandlerReadyCallback); + this.config = config; + this.app = initializeApp(this.config); + this.database = getDatabase(this.app); + this.auth = getAuth(this.app); + this.GAuthProvider = new GoogleAuthProvider(); + + this.userEmail = ""; + this.uid = ""; + this.layouts = {}; + this.currentLayout = null; + this.poses = {}; + this.mapPoses = {}; + this.mapPoseTypes = {}; + this.recordings = {}; + this.markerNames = []; + this.markerIDs = []; + this.markerInfo = {} as ArucoMarkersInfo; + onAuthStateChanged(this.auth, (user) => this.handleAuthStateChange(user)); + + this.signInWithGoogle(); + } + + private handleAuthStateChange(user: User | null) { + if (user) { + this.uid = user.uid; + this.userEmail = user.email!; + + this.getUserDataFirebase() + .then(async (userData) => { + this.layouts = userData.layouts; + this.currentLayout = userData.currentLayout; + this.mapPoses = userData.map_poses; + this.mapPoseTypes = userData.map_pose_types; + this.recordings = userData.recordings; + + this.onReadyCallback(); }) - return poses + .catch((error) => { + console.log( + "Detected that FirebaseModel isn't initialized for user ", + this.uid, + ); + this.onReadyCallback(); + }); } + } - public getMapPoseTypes(): string[] { - if (!this.mapPoseTypes) return [] - return Object.keys(this.mapPoseTypes) - } - - public deleteMapPose(poseName: string): void { - let pose = this.mapPoses![poseName] - if (!pose) throw Error(`Could not delete pose ${poseName}`); - delete this.mapPoses[poseName] - delete this.mapPoseTypes[poseName] - this.writeMapPoses(this.mapPoses) - this.writeMapPoseTypes(this.mapPoseTypes) - } - - public getRecordingNames(): string[] { - if (!this.recordings) return [] - return Object.keys(this.recordings) - } - - public getRecording(recordingName: string): RobotPose[] { - let recording = this.recordings![recordingName] - if (!recording) throw Error(`Could not load recording ${recordingName}`); - return JSON.parse(JSON.stringify(recording)); - } - - public savePoseRecording(recordingName: string, poses: RobotPose[]): void { - this.recordings[recordingName] = poses - this.writeRecordings(this.recordings) - } + private async getUserDataFirebase() { + const snapshot = await get(child(ref(this.database), "/users/" + this.uid)); - private async writeRecordings(recordings: { [name: string]: RobotPose[] }) { - this.recordings = recordings; - - let updates: any = {}; - updates['/users/' + (this.uid) + '/recordings'] = recordings; - return update(ref(this.database), updates); + if (snapshot.exists()) { + return snapshot.val(); + } else { + throw "No data available"; } - - public deleteRecording(recordingName: string): void { - let recording = this.recordings![recordingName] - if (!recording) throw Error(`Could not delete recording ${recordingName}`); - delete this.recordings[recordingName] - this.writeRecordings(this.recordings) + } + + public signInWithGoogle() { + if (this.userEmail == "") { + signInWithPopup(this.auth, this.GAuthProvider) + .then((result) => { + const credential = GoogleAuthProvider.credentialFromResult(result); + const token = credential!.accessToken; + const user = result.user; + return Promise.resolve(); + }) + .catch(this.handleError); } -} \ No newline at end of file + } + + private handleError(error: FirebaseError) { + const errorCode = error.code; + const errorMessage = error.message; + console.error("firebaseError: " + errorCode + ": " + errorMessage); + console.trace(); + return Promise.reject(); + } + + public loadCustomLayout(layoutName: string): LayoutDefinition { + let layout = this.layouts![layoutName]; + if (!layout) throw Error(`Could not load custom layout ${layoutName}`); + return JSON.parse(JSON.stringify(layout)); + } + + public saveCustomLayout(layout: LayoutDefinition, layoutName: string): void { + this.layouts[layoutName] = layout; + this.writeLayouts(this.layouts); + } + + public saveCurrentLayout(layout: LayoutDefinition): void { + this.currentLayout = layout; + + let updates: any = {}; + updates["/users/" + this.uid + "/currentLayout"] = layout; + update(ref(this.database), updates); + } + + public loadCurrentLayout(): LayoutDefinition | null { + return this.currentLayout; + } + + public getCustomLayoutNames(): string[] { + if (!this.layouts) return []; + return Object.keys(this.layouts); + } + + private async writeLayouts(layouts: { [name: string]: LayoutDefinition }) { + this.layouts = layouts; + + let updates: any = {}; + updates["/users/" + this.uid + "/layouts"] = layouts; + return update(ref(this.database), updates); + } + + public getMapPoseNames(): string[] { + if (!this.mapPoses) return []; + return Object.keys(this.mapPoses); + } + + public saveMapPose(name: string, pose: ROSLIB.Transform, poseType: string) { + this.mapPoses[name] = pose; + this.mapPoseTypes[name] = poseType; + this.writeMapPoses(this.mapPoses); + this.writeMapPoseTypes(this.mapPoseTypes); + } + + private async writeMapPoses(poses: { [name: string]: ROSLIB.Transform }) { + this.mapPoses = poses; + + let updates: any = {}; + updates["/users/" + this.uid + "/map_poses"] = poses; + return update(ref(this.database), updates); + } + + private async writeMapPoseTypes(poseTypes: { [name: string]: string }) { + this.mapPoseTypes = poseTypes; + + let updates: any = {}; + updates["/users/" + this.uid + "/map_pose_types"] = poseTypes; + return update(ref(this.database), updates); + } + + public getMapPose(poseName: string): ROSLIB.Transform { + let pose = this.mapPoses![poseName]; + if (!pose) throw Error(`Could not load pose ${poseName}`); + return JSON.parse(JSON.stringify(pose)); + } + + public getMapPoses(): ROSLIB.Transform[] { + const poseNames = this.getMapPoseNames(); + var poses: ROSLIB.Transform[] = []; + poseNames.forEach((poseName) => { + const pose = this.getMapPose(poseName); + poses.push(pose); + }); + return poses; + } + + public getMapPoseTypes(): string[] { + if (!this.mapPoseTypes) return []; + return Object.keys(this.mapPoseTypes); + } + + public deleteMapPose(poseName: string): void { + let pose = this.mapPoses![poseName]; + if (!pose) throw Error(`Could not delete pose ${poseName}`); + delete this.mapPoses[poseName]; + delete this.mapPoseTypes[poseName]; + this.writeMapPoses(this.mapPoses); + this.writeMapPoseTypes(this.mapPoseTypes); + } + + public getRecordingNames(): string[] { + if (!this.recordings) return []; + return Object.keys(this.recordings); + } + + public getRecording(recordingName: string): RobotPose[] { + let recording = this.recordings![recordingName]; + if (!recording) throw Error(`Could not load recording ${recordingName}`); + return JSON.parse(JSON.stringify(recording)); + } + + public savePoseRecording(recordingName: string, poses: RobotPose[]): void { + this.recordings[recordingName] = poses; + this.writeRecordings(this.recordings); + } + + private async writeRecordings(recordings: { [name: string]: RobotPose[] }) { + this.recordings = recordings; + + let updates: any = {}; + updates["/users/" + this.uid + "/recordings"] = recordings; + return update(ref(this.database), updates); + } + + public deleteRecording(recordingName: string): void { + let recording = this.recordings![recordingName]; + if (!recording) throw Error(`Could not delete recording ${recordingName}`); + delete this.recordings[recordingName]; + this.writeRecordings(this.recordings); + } +} diff --git a/src/pages/operator/tsx/storage_handler/LocalStorageHandler.tsx b/src/pages/operator/tsx/storage_handler/LocalStorageHandler.tsx index 5514ae19..ad4768f2 100644 --- a/src/pages/operator/tsx/storage_handler/LocalStorageHandler.tsx +++ b/src/pages/operator/tsx/storage_handler/LocalStorageHandler.tsx @@ -5,133 +5,172 @@ import ROSLIB from "roslib"; /** Uses browser local storage to store data. */ export class LocalStorageHandler extends StorageHandler { - public static CURRENT_LAYOUT_KEY = "user_custom_layout"; - public static LAYOUT_NAMES_KEY = "user_custom_layout_names"; - public static POSE_NAMES_KEY = "user_pose_names"; - public static MAP_POSE_NAMES_KEY = "user_map_pose_names"; - public static MAP_POSE_TYPES_KEY = "user_map_pose_types"; - public static POSE_RECORDING_NAMES_KEY = "user_pose_recording_names"; - - constructor(onStorageHandlerReadyCallback: () => void) { - super(onStorageHandlerReadyCallback); - // Allow the initialization process to complete before invoking the callback - setTimeout(() => { - this.getCustomLayoutNames() - this.onReadyCallback(); - }, 0); - } - - public loadCustomLayout(layoutName: string): LayoutDefinition { - const storedJson = localStorage.getItem(layoutName); - if (!storedJson) throw Error(`Could not load custom layout ${layoutName}`); - return JSON.parse(storedJson); - } - - public saveCustomLayout(layout: LayoutDefinition, layoutName: string): void { - const layoutNames = this.getCustomLayoutNames(); - layoutNames.push(layoutName); - localStorage.setItem(LocalStorageHandler.LAYOUT_NAMES_KEY, JSON.stringify(layoutNames)); - localStorage.setItem(layoutName, JSON.stringify(layout)); - } - - public saveCurrentLayout(layout: LayoutDefinition): void { - localStorage.setItem(LocalStorageHandler.CURRENT_LAYOUT_KEY, JSON.stringify(layout)); - } - - public loadCurrentLayout(): LayoutDefinition | null { - const storedJson = localStorage.getItem(LocalStorageHandler.CURRENT_LAYOUT_KEY); - if (!storedJson) return null; - return JSON.parse(storedJson); - } - - public getCustomLayoutNames(): string[] { - const storedJson = localStorage.getItem(LocalStorageHandler.LAYOUT_NAMES_KEY); - if (!storedJson) return []; - return JSON.parse(storedJson); - } - - public saveMapPose(poseName: string, pose: ROSLIB.Transform, poseType: string) { - const poseNames = this.getMapPoseNames(); - const poseTypes = this.getMapPoseTypes(); - // If pose name does not exist add the name, type and pose, otherwise replace the - // type and pose for the given name - if (!poseNames.includes(poseName)) { - poseNames.push(poseName); - poseTypes.push(poseType) - } else { - let idx = poseNames.indexOf(poseName) - poseTypes[idx] = poseType - } - localStorage.setItem(LocalStorageHandler.MAP_POSE_NAMES_KEY, JSON.stringify(poseNames)); - localStorage.setItem(LocalStorageHandler.MAP_POSE_TYPES_KEY, JSON.stringify(poseTypes)); - localStorage.setItem('map_' + poseName, JSON.stringify(pose)); - } - - public getMapPoseNames(): string[] { - const storedJson = localStorage.getItem(LocalStorageHandler.MAP_POSE_NAMES_KEY); - if (!storedJson) return []; - return JSON.parse(storedJson) - } - - public getMapPose(poseName: string): ROSLIB.Transform { - const storedJson = localStorage.getItem('map_' + poseName); - if (!storedJson) throw Error(`Could not load pose ${poseName}`); - return JSON.parse(storedJson); - } - - public getMapPoses(): ROSLIB.Transform[] { - const poseNames = this.getMapPoseNames() - var poses: ROSLIB.Transform[] = [] - poseNames.forEach(poseName => { - const pose = this.getMapPose(poseName) - poses.push(pose) - }); - return poses - } - - public getMapPoseTypes(): string[] { - const storedJson = localStorage.getItem(LocalStorageHandler.MAP_POSE_TYPES_KEY); - if (!storedJson) return []; - return JSON.parse(storedJson) - } - - public deleteMapPose(poseName: string): void { - const poseNames = this.getMapPoseNames(); - if (!poseNames.includes(poseName)) return; - localStorage.removeItem('map_' + poseName) - const index = poseNames.indexOf(poseName) - poseNames.splice(index, 1) - const poseTypes = this.getMapPoseTypes() - poseTypes.splice(index, 1) - localStorage.setItem(LocalStorageHandler.MAP_POSE_NAMES_KEY, JSON.stringify(poseNames)); - localStorage.setItem(LocalStorageHandler.MAP_POSE_TYPES_KEY, JSON.stringify(poseTypes)); - } - - public getRecordingNames(): string[] { - const storedJson = localStorage.getItem(LocalStorageHandler.POSE_RECORDING_NAMES_KEY); - if (!storedJson) return []; - return JSON.parse(storedJson) - } - - public getRecording(recordingName: string): RobotPose[] { - const storedJson = localStorage.getItem('recording_' + recordingName); - if (!storedJson) throw Error(`Could not load recording ${recordingName}`); - return JSON.parse(storedJson); - } - - public savePoseRecording(recordingName: string, poses: RobotPose[]): void { - const recordingNames = this.getRecordingNames(); - if (!recordingNames.includes(recordingName)) recordingNames.push(recordingName); - localStorage.setItem(LocalStorageHandler.POSE_RECORDING_NAMES_KEY, JSON.stringify(recordingNames)); - localStorage.setItem('recording_' + recordingName, JSON.stringify(poses)); - } - - public deleteRecording(recordingName: string): void { - const recordingNames = this.getRecordingNames(); - if (!recordingNames.includes(recordingName)) return; - localStorage.removeItem('recording_' + recordingName) - const index = recordingNames.indexOf(recordingName) - recordingNames.splice(index, 1) - localStorage.setItem(LocalStorageHandler.POSE_RECORDING_NAMES_KEY, JSON.stringify(recordingNames)); - } -} \ No newline at end of file + public static CURRENT_LAYOUT_KEY = "user_custom_layout"; + public static LAYOUT_NAMES_KEY = "user_custom_layout_names"; + public static POSE_NAMES_KEY = "user_pose_names"; + public static MAP_POSE_NAMES_KEY = "user_map_pose_names"; + public static MAP_POSE_TYPES_KEY = "user_map_pose_types"; + public static POSE_RECORDING_NAMES_KEY = "user_pose_recording_names"; + + constructor(onStorageHandlerReadyCallback: () => void) { + super(onStorageHandlerReadyCallback); + // Allow the initialization process to complete before invoking the callback + setTimeout(() => { + this.getCustomLayoutNames(); + this.onReadyCallback(); + }, 0); + } + + public loadCustomLayout(layoutName: string): LayoutDefinition { + const storedJson = localStorage.getItem(layoutName); + if (!storedJson) throw Error(`Could not load custom layout ${layoutName}`); + return JSON.parse(storedJson); + } + + public saveCustomLayout(layout: LayoutDefinition, layoutName: string): void { + const layoutNames = this.getCustomLayoutNames(); + layoutNames.push(layoutName); + localStorage.setItem( + LocalStorageHandler.LAYOUT_NAMES_KEY, + JSON.stringify(layoutNames), + ); + localStorage.setItem(layoutName, JSON.stringify(layout)); + } + + public saveCurrentLayout(layout: LayoutDefinition): void { + localStorage.setItem( + LocalStorageHandler.CURRENT_LAYOUT_KEY, + JSON.stringify(layout), + ); + } + + public loadCurrentLayout(): LayoutDefinition | null { + const storedJson = localStorage.getItem( + LocalStorageHandler.CURRENT_LAYOUT_KEY, + ); + if (!storedJson) return null; + return JSON.parse(storedJson); + } + + public getCustomLayoutNames(): string[] { + const storedJson = localStorage.getItem( + LocalStorageHandler.LAYOUT_NAMES_KEY, + ); + if (!storedJson) return []; + return JSON.parse(storedJson); + } + + public saveMapPose( + poseName: string, + pose: ROSLIB.Transform, + poseType: string, + ) { + const poseNames = this.getMapPoseNames(); + const poseTypes = this.getMapPoseTypes(); + // If pose name does not exist add the name, type and pose, otherwise replace the + // type and pose for the given name + if (!poseNames.includes(poseName)) { + poseNames.push(poseName); + poseTypes.push(poseType); + } else { + let idx = poseNames.indexOf(poseName); + poseTypes[idx] = poseType; + } + localStorage.setItem( + LocalStorageHandler.MAP_POSE_NAMES_KEY, + JSON.stringify(poseNames), + ); + localStorage.setItem( + LocalStorageHandler.MAP_POSE_TYPES_KEY, + JSON.stringify(poseTypes), + ); + localStorage.setItem("map_" + poseName, JSON.stringify(pose)); + } + + public getMapPoseNames(): string[] { + const storedJson = localStorage.getItem( + LocalStorageHandler.MAP_POSE_NAMES_KEY, + ); + if (!storedJson) return []; + return JSON.parse(storedJson); + } + + public getMapPose(poseName: string): ROSLIB.Transform { + const storedJson = localStorage.getItem("map_" + poseName); + if (!storedJson) throw Error(`Could not load pose ${poseName}`); + return JSON.parse(storedJson); + } + + public getMapPoses(): ROSLIB.Transform[] { + const poseNames = this.getMapPoseNames(); + var poses: ROSLIB.Transform[] = []; + poseNames.forEach((poseName) => { + const pose = this.getMapPose(poseName); + poses.push(pose); + }); + return poses; + } + + public getMapPoseTypes(): string[] { + const storedJson = localStorage.getItem( + LocalStorageHandler.MAP_POSE_TYPES_KEY, + ); + if (!storedJson) return []; + return JSON.parse(storedJson); + } + + public deleteMapPose(poseName: string): void { + const poseNames = this.getMapPoseNames(); + if (!poseNames.includes(poseName)) return; + localStorage.removeItem("map_" + poseName); + const index = poseNames.indexOf(poseName); + poseNames.splice(index, 1); + const poseTypes = this.getMapPoseTypes(); + poseTypes.splice(index, 1); + localStorage.setItem( + LocalStorageHandler.MAP_POSE_NAMES_KEY, + JSON.stringify(poseNames), + ); + localStorage.setItem( + LocalStorageHandler.MAP_POSE_TYPES_KEY, + JSON.stringify(poseTypes), + ); + } + + public getRecordingNames(): string[] { + const storedJson = localStorage.getItem( + LocalStorageHandler.POSE_RECORDING_NAMES_KEY, + ); + if (!storedJson) return []; + return JSON.parse(storedJson); + } + + public getRecording(recordingName: string): RobotPose[] { + const storedJson = localStorage.getItem("recording_" + recordingName); + if (!storedJson) throw Error(`Could not load recording ${recordingName}`); + return JSON.parse(storedJson); + } + + public savePoseRecording(recordingName: string, poses: RobotPose[]): void { + const recordingNames = this.getRecordingNames(); + if (!recordingNames.includes(recordingName)) + recordingNames.push(recordingName); + localStorage.setItem( + LocalStorageHandler.POSE_RECORDING_NAMES_KEY, + JSON.stringify(recordingNames), + ); + localStorage.setItem("recording_" + recordingName, JSON.stringify(poses)); + } + + public deleteRecording(recordingName: string): void { + const recordingNames = this.getRecordingNames(); + if (!recordingNames.includes(recordingName)) return; + localStorage.removeItem("recording_" + recordingName); + const index = recordingNames.indexOf(recordingName); + recordingNames.splice(index, 1); + localStorage.setItem( + LocalStorageHandler.POSE_RECORDING_NAMES_KEY, + JSON.stringify(recordingNames), + ); + } +} diff --git a/src/pages/operator/tsx/storage_handler/README.md b/src/pages/operator/tsx/storage_handler/README.md index 3dba57aa..b74f7c61 100644 --- a/src/pages/operator/tsx/storage_handler/README.md +++ b/src/pages/operator/tsx/storage_handler/README.md @@ -1,11 +1,15 @@ # Firebase + Firebase is a set of application development platforms and backen cloud computing services. We will be using Firebase's Realtime Database for data storage. ## Setting up Firebase + ### Creating a Firebase Project -Sign into [Firebase](https://firebase.google.com/) with your google account then open the Firebase [console](https://console.firebase.google.com/) and create a new project. The project will default to using the no-cost [Spark plan](https://firebase.google.com/pricing?hl=en&authuser=1). + +Sign into [Firebase](https://firebase.google.com/) with your google account then open the Firebase [console](https://console.firebase.google.com/) and create a new project. The project will default to using the no-cost [Spark plan](https://firebase.google.com/pricing?hl=en&authuser=1). Add a web app to your firebase project. You shouldn't need to worry about installing the Firebase SDK because it is already in the `package.json` dependencies for this repo. This will generate a configuration for your web app that looks something like this: + ``` const firebaseConfig = { apiKey: ..., @@ -19,6 +23,7 @@ const firebaseConfig = { ``` Create a file named `.env` in `stretch-web-interface` and add the config to the `.env` file. The config will need to be reformatted slightly so the contents of `.env` look like this: + ``` apiKey=DEzaSyAzZEQ89KBuKXgKJ-UWV9vm3xM authDomain=stretch-teleop-interface.firebaseapp.com @@ -30,7 +35,9 @@ measurementId=T-6GMDF5W03Z ``` ### Setup the Realtime Database + Select the `Realtime Database` option under Build in the Firebase console for your project, then create a database. Select "Start in **locked mode**" in `Security Rules` and click `Enable`. Add the following to the database rules: + ``` { "rules": { @@ -54,5 +61,5 @@ Select the `Realtime Database` option under Build in the Firebase console for yo Replace `'user1@example.com'` and `'user2@example.com'` with the email addresses of the users you'd like to give write access to. You can add as many users as you'd like by separating them with `||`. ### Setup Authentication -Select the `Authentication` option under Build in the Firebase console for your project, then click `Get Started`. Click `Email/Password` and enable it. Do not enable passwordless sign-in. Click `Add new Provider` and `Anonymous` then enable it and click `Save`. Finally, add another provider, click `Google` and add a `Project public-facing name`, select a support email and click `Save`. We will primarily be using `Google` for authentication. +Select the `Authentication` option under Build in the Firebase console for your project, then click `Get Started`. Click `Email/Password` and enable it. Do not enable passwordless sign-in. Click `Add new Provider` and `Anonymous` then enable it and click `Save`. Finally, add another provider, click `Google` and add a `Project public-facing name`, select a support email and click `Save`. We will primarily be using `Google` for authentication. diff --git a/src/pages/operator/tsx/storage_handler/StorageHandler.tsx b/src/pages/operator/tsx/storage_handler/StorageHandler.tsx index 611a7688..b4a85153 100644 --- a/src/pages/operator/tsx/storage_handler/StorageHandler.tsx +++ b/src/pages/operator/tsx/storage_handler/StorageHandler.tsx @@ -8,147 +8,158 @@ import { ARUCO_MARKER_INFO } from "../utils/aruco_markers_dict"; export type DefaultLayoutName = "Basic Layout"; /** Object with all the default layouts. */ -export const DEFAULT_LAYOUTS: { [key in DefaultLayoutName]: LayoutDefinition } = { +export const DEFAULT_LAYOUTS: { [key in DefaultLayoutName]: LayoutDefinition } = + { "Basic Layout": BASIC_LAYOUT, -} + }; -/** +/** * Handles logic to store data, specifically maintain state between browser * reloads and save user custom layouts. */ export abstract class StorageHandler { - /** - * Callback to execute once the storage is ready, for example after the - * user has signed into Firebase. - */ - public onReadyCallback: () => void; - - constructor(onStorageHandlerReadyCallback: () => void) { - this.onReadyCallback = onStorageHandlerReadyCallback.bind(this) - } - - /** - * Loads a user saved custom layout. - * @param layoutName name of the layout to load - * @returns the layout defintion - */ - public abstract loadCustomLayout(layoutName: string): LayoutDefinition; - - /** - * Saves a layout to the storage device. - * @param layout the definition of the layout to save - * @param layoutName the name of the layout - */ - public abstract saveCustomLayout(layout: LayoutDefinition, layoutName: string): void; - - /** - * Saves the current layout to preserve state between reloading the browser. - * @param layout the current layout - */ - public abstract saveCurrentLayout(layout: LayoutDefinition): void; - - /** - * Loads the last used layout to preserve state between reloading the browser. - */ - public abstract loadCurrentLayout(): LayoutDefinition | null; - - /** - * Gets the list of all the user's saved layouts - * @returns list of layout names - */ - public abstract getCustomLayoutNames(): string[]; - - /** - * Save the map pose and its identifier - * @param name the name of the pose - * @param pose the pose on the map to save - */ - public abstract saveMapPose(poseName: string, pose: ROSLIB.Transform, poseType: string): void; - - /** - * Get an array of all saved map poses - * @returns array of all saved map poses - */ - public abstract getMapPoseNames(): string[]; - - /** - * Gets the map pose associated with the given name - * @param name the name of the map pose - * @returns a map pose associated with the given name - */ - public abstract getMapPose(poseName: string): ROSLIB.Transform; - - /** - * Gets an array of all saved poses - * @returns an array of all saved poses - */ - public abstract getMapPoses(): ROSLIB.Transform[]; - - /** - * Get an array of all the saved map pose types (map or aruco) - * @returns an array of all saved map pose types - */ - public abstract getMapPoseTypes(): string[]; - - /** - * Removes the map pose from storage - * @param name the name of the map pose - */ - public abstract deleteMapPose(poseName: string): void; - - /** - * Get the list of all saved pose sequence recordings - * @returns list of all saved pose sequence recordings - */ - public abstract getRecordingNames(): string[]; - - /** - * Gets the recording associated with the given name - * @param recordingName the name of the recording - * @returns a recording associated with the given name - */ - public abstract getRecording(recordingName: string): RobotPose[]; - - /** - * Save the pose sequence and its identifier - * @param recordingName the name of the recording - * @param poses the pose sequence to save - */ - public abstract savePoseRecording(recordingName: string, poses: RobotPose[]): void; - - /** - * Removes the recording from storage - * @param recordingName the name of the recording - */ - public abstract deleteRecording(recordingName: string): void; - - /** - * Gets the last saved state from the user's layout, or gets the default - * layout if the user has no saved state. - * @returns layout definition for the layout that should be loaded into the - * operator page. - */ - public loadCurrentLayoutOrDefault(): LayoutDefinition { - const currentLayout = this.loadCurrentLayout(); - if (!currentLayout) return Object.values(DEFAULT_LAYOUTS)[0]; - console.log('loading saved layout') - return currentLayout; - } - - /** - * Gets all the default layout names - * @returns list of default layout names - */ - public getDefaultLayoutNames(): string[] { - return Object.keys(DEFAULT_LAYOUTS); - } - - /** - * Gets a default layout - * @param layoutName default layout to load - * @returns the layout definition for the default layout - */ - public loadDefaultLayout(layoutName: DefaultLayoutName): LayoutDefinition { - return JSON.parse(JSON.stringify(DEFAULT_LAYOUTS[layoutName])); - } -} \ No newline at end of file + /** + * Callback to execute once the storage is ready, for example after the + * user has signed into Firebase. + */ + public onReadyCallback: () => void; + + constructor(onStorageHandlerReadyCallback: () => void) { + this.onReadyCallback = onStorageHandlerReadyCallback.bind(this); + } + + /** + * Loads a user saved custom layout. + * @param layoutName name of the layout to load + * @returns the layout definition + */ + public abstract loadCustomLayout(layoutName: string): LayoutDefinition; + + /** + * Saves a layout to the storage device. + * @param layout the definition of the layout to save + * @param layoutName the name of the layout + */ + public abstract saveCustomLayout( + layout: LayoutDefinition, + layoutName: string, + ): void; + + /** + * Saves the current layout to preserve state between reloading the browser. + * @param layout the current layout + */ + public abstract saveCurrentLayout(layout: LayoutDefinition): void; + + /** + * Loads the last used layout to preserve state between reloading the browser. + */ + public abstract loadCurrentLayout(): LayoutDefinition | null; + + /** + * Gets the list of all the user's saved layouts + * @returns list of layout names + */ + public abstract getCustomLayoutNames(): string[]; + + /** + * Save the map pose and its identifier + * @param name the name of the pose + * @param pose the pose on the map to save + */ + public abstract saveMapPose( + poseName: string, + pose: ROSLIB.Transform, + poseType: string, + ): void; + + /** + * Get an array of all saved map poses + * @returns array of all saved map poses + */ + public abstract getMapPoseNames(): string[]; + + /** + * Gets the map pose associated with the given name + * @param name the name of the map pose + * @returns a map pose associated with the given name + */ + public abstract getMapPose(poseName: string): ROSLIB.Transform; + + /** + * Gets an array of all saved poses + * @returns an array of all saved poses + */ + public abstract getMapPoses(): ROSLIB.Transform[]; + + /** + * Get an array of all the saved map pose types (map or aruco) + * @returns an array of all saved map pose types + */ + public abstract getMapPoseTypes(): string[]; + + /** + * Removes the map pose from storage + * @param name the name of the map pose + */ + public abstract deleteMapPose(poseName: string): void; + + /** + * Get the list of all saved pose sequence recordings + * @returns list of all saved pose sequence recordings + */ + public abstract getRecordingNames(): string[]; + + /** + * Gets the recording associated with the given name + * @param recordingName the name of the recording + * @returns a recording associated with the given name + */ + public abstract getRecording(recordingName: string): RobotPose[]; + + /** + * Save the pose sequence and its identifier + * @param recordingName the name of the recording + * @param poses the pose sequence to save + */ + public abstract savePoseRecording( + recordingName: string, + poses: RobotPose[], + ): void; + + /** + * Removes the recording from storage + * @param recordingName the name of the recording + */ + public abstract deleteRecording(recordingName: string): void; + + /** + * Gets the last saved state from the user's layout, or gets the default + * layout if the user has no saved state. + * @returns layout definition for the layout that should be loaded into the + * operator page. + */ + public loadCurrentLayoutOrDefault(): LayoutDefinition { + const currentLayout = this.loadCurrentLayout(); + if (!currentLayout) return Object.values(DEFAULT_LAYOUTS)[0]; + console.log("loading saved layout"); + return currentLayout; + } + + /** + * Gets all the default layout names + * @returns list of default layout names + */ + public getDefaultLayoutNames(): string[] { + return Object.keys(DEFAULT_LAYOUTS); + } + + /** + * Gets a default layout + * @param layoutName default layout to load + * @returns the layout definition for the default layout + */ + public loadDefaultLayout(layoutName: DefaultLayoutName): LayoutDefinition { + return JSON.parse(JSON.stringify(DEFAULT_LAYOUTS[layoutName])); + } +} diff --git a/src/pages/operator/tsx/utils/aruco_markers_dict.tsx b/src/pages/operator/tsx/utils/aruco_markers_dict.tsx index fe242e87..159b231b 100644 --- a/src/pages/operator/tsx/utils/aruco_markers_dict.tsx +++ b/src/pages/operator/tsx/utils/aruco_markers_dict.tsx @@ -2,174 +2,174 @@ import ROSLIB from "roslib"; import { ArucoMarkersInfo } from "shared/util"; export const ARUCO_MARKER_INFO: ArucoMarkersInfo = { - aruco_marker_info: { - '130': { - length_mm: 47, - use_rgb_only: false, - name: 'base_left', - link: 'link_aruco_left_base' - }, - '131': { - length_mm: 47, - use_rgb_only: false, - name: 'base_right', - link: 'link_aruco_right_base' - }, - '132': { - length_mm: 23.5, - use_rgb_only: false, - name: 'wrist_inside', - link: 'link_aruco_inner_wrist' - }, - '133': { - length_mm: 23.5, - use_rgb_only: false, - name: 'wrist_top', - link: 'link_aruco_top_wrist' - }, - '134': { - length_mm: 31.4, - use_rgb_only: false, - name: 'shoulder_top', - link: 'link_aruco_shoulder' - }, - '245': { - length_mm: 88.0, - use_rgb_only: false, - name: 'docking_station', - pose: { - transform: new ROSLIB.Transform({ - translation: { - x: 0.0, - y: -0.5, - z: 0.47 - }, - rotation: { - x: -0.382, - y: -0.352, - z: -0.604, - w: 0.604 - } - }) - } - }, - '246': { - length_mm: 179.0, - use_rgb_only: false, - name: 'floor_0', - }, - '247': { - length_mm: 179.0, - use_rgb_only: false, - name: 'floor_1', - }, - '248': { - length_mm: 179.0, - use_rgb_only: false, - name: 'floor_2', - }, - '249': { - length_mm: 179.0, - use_rgb_only: false, - name: 'floor_3', - }, - '10': { - length_mm: 24, - use_rgb_only: false, - name: 'target_object', - }, - '21': { - length_mm: 86, - use_rgb_only: false, - name: 'user_pointer', - }, - 'default': { - length_mm: 24, - use_rgb_only: false, - name: 'unknown', - }, - // "0": { - // length_mm: 40, - // use_rgb_only: false, - // name: "Brush", - // link: null, - // pose: { - // transform: new ROSLIB.Transform({ - // translation: { - // x: 0.0, - // y: -0.6165221897614768, - // z:-0.8765156889436512 - // }, - // rotation: { - // x: -0.03965766243238807, - // y: 0.04420070564372583, - // z: 0.9982143182308036, - // w: 0.006462207990295707 - // } - // }) - // } - // }, - // "1": { - // length_mm: 22, - // use_rgb_only: false, - // name: "Feeding Tool", - // link: null, - // pose: { - // transform: new ROSLIB.Transform({ - // translation: { - // x: 0.0, - // y: -0.6165221897614768, - // z: -0.8765156889436512 - // }, - // rotation: { - // x: -0.03965766243238807, - // y: 0.04420070564372583, - // z: 0.9982143182308036, - // w: 0.006462207990295707 - // } - // }) - // } - // }, - // "2": { - // length_mm: 40, - // use_rgb_only: false, - // name: "Button Pusher", - // link: null, - // pose: { - // transform: new ROSLIB.Transform({ - // translation: { - // x: 0.0, - // y: -0.6165221897614768, - // z:-0.8765156889436512 - // }, - // rotation: { - // x: -0.03965766243238807, - // y: 0.04420070564372583, - // z: 0.9982143182308036, - // w: 0.006462207990295707 - // } - // }) - // } - // }, - "20": { - length_mm: 68, - use_rgb_only: false, - name: "Tool Shelf", - pose: { - transform: new ROSLIB.Transform({ - translation: { - x: 0.0, - y: -1.2395535657717278, - z: 0.655349797002288 - }, - rotation: { - x: -0.007660462539611933, - y: 0.720369323410184, - z: 0.6932368040571439, - w: -0.020787303947235967 - } - }) - } - }, - } -} \ No newline at end of file + aruco_marker_info: { + "130": { + length_mm: 47, + use_rgb_only: false, + name: "base_left", + link: "link_aruco_left_base", + }, + "131": { + length_mm: 47, + use_rgb_only: false, + name: "base_right", + link: "link_aruco_right_base", + }, + "132": { + length_mm: 23.5, + use_rgb_only: false, + name: "wrist_inside", + link: "link_aruco_inner_wrist", + }, + "133": { + length_mm: 23.5, + use_rgb_only: false, + name: "wrist_top", + link: "link_aruco_top_wrist", + }, + "134": { + length_mm: 31.4, + use_rgb_only: false, + name: "shoulder_top", + link: "link_aruco_shoulder", + }, + "245": { + length_mm: 88.0, + use_rgb_only: false, + name: "docking_station", + pose: { + transform: new ROSLIB.Transform({ + translation: { + x: 0.0, + y: -0.5, + z: 0.47, + }, + rotation: { + x: -0.382, + y: -0.352, + z: -0.604, + w: 0.604, + }, + }), + }, + }, + "246": { + length_mm: 179.0, + use_rgb_only: false, + name: "floor_0", + }, + "247": { + length_mm: 179.0, + use_rgb_only: false, + name: "floor_1", + }, + "248": { + length_mm: 179.0, + use_rgb_only: false, + name: "floor_2", + }, + "249": { + length_mm: 179.0, + use_rgb_only: false, + name: "floor_3", + }, + "10": { + length_mm: 24, + use_rgb_only: false, + name: "target_object", + }, + "21": { + length_mm: 86, + use_rgb_only: false, + name: "user_pointer", + }, + default: { + length_mm: 24, + use_rgb_only: false, + name: "unknown", + }, + // "0": { + // length_mm: 40, + // use_rgb_only: false, + // name: "Brush", + // link: null, + // pose: { + // transform: new ROSLIB.Transform({ + // translation: { + // x: 0.0, + // y: -0.6165221897614768, + // z:-0.8765156889436512 + // }, + // rotation: { + // x: -0.03965766243238807, + // y: 0.04420070564372583, + // z: 0.9982143182308036, + // w: 0.006462207990295707 + // } + // }) + // } + // }, + // "1": { + // length_mm: 22, + // use_rgb_only: false, + // name: "Feeding Tool", + // link: null, + // pose: { + // transform: new ROSLIB.Transform({ + // translation: { + // x: 0.0, + // y: -0.6165221897614768, + // z: -0.8765156889436512 + // }, + // rotation: { + // x: -0.03965766243238807, + // y: 0.04420070564372583, + // z: 0.9982143182308036, + // w: 0.006462207990295707 + // } + // }) + // } + // }, + // "2": { + // length_mm: 40, + // use_rgb_only: false, + // name: "Button Pusher", + // link: null, + // pose: { + // transform: new ROSLIB.Transform({ + // translation: { + // x: 0.0, + // y: -0.6165221897614768, + // z:-0.8765156889436512 + // }, + // rotation: { + // x: -0.03965766243238807, + // y: 0.04420070564372583, + // z: 0.9982143182308036, + // w: 0.006462207990295707 + // } + // }) + // } + // }, + "20": { + length_mm: 68, + use_rgb_only: false, + name: "Tool Shelf", + pose: { + transform: new ROSLIB.Transform({ + translation: { + x: 0.0, + y: -1.2395535657717278, + z: 0.655349797002288, + }, + rotation: { + x: -0.007660462539611933, + y: 0.720369323410184, + z: 0.6932368040571439, + w: -0.020787303947235967, + }, + }), + }, + }, + }, +}; diff --git a/src/pages/operator/tsx/utils/component_definitions.tsx b/src/pages/operator/tsx/utils/component_definitions.tsx index 6192c0bf..014ec8d9 100644 --- a/src/pages/operator/tsx/utils/component_definitions.tsx +++ b/src/pages/operator/tsx/utils/component_definitions.tsx @@ -4,202 +4,202 @@ /** Enumerator for the possible action modes */ export enum ActionMode { - StepActions = 'Step-Actions', - PressAndHold = 'Press-And-Hold', - ClickClick = 'Click-Click' + StepActions = "Step-Actions", + PressAndHold = "Press-And-Hold", + ClickClick = "Click-Click", } /** * High-level type of the component */ export enum ComponentType { - Layout = "Layout", - LayoutGrid = "Layout Grid", - Panel = "Panel", - SingleTab = "Tab", - CameraView = "Camera View", - ButtonPad = "Button Pad", - PredictiveDisplay = "Predictive Display", - ButtonGrid = "Button Grid", - VirtualJoystick = "Joystick", - Map = "Map", - RunStopButton = "Run Stop Button", - BatteryGuage = "Battery Gauge" + Layout = "Layout", + LayoutGrid = "Layout Grid", + Panel = "Panel", + SingleTab = "Tab", + CameraView = "Camera View", + ButtonPad = "Button Pad", + PredictiveDisplay = "Predictive Display", + ButtonGrid = "Button Grid", + VirtualJoystick = "Joystick", + Map = "Map", + RunStopButton = "Run Stop Button", + BatteryGuage = "Battery Gauge", } /** * ID for the video stream, one for each of the cameras */ export enum CameraViewId { - overhead = "Overhead", - realsense = "Realsense", - gripper = "Gripper" + overhead = "Overhead", + realsense = "Realsense", + gripper = "Gripper", } /** * ID for a button pad describes the shape and button functions of the button pad */ export enum ButtonPadId { - // Drive = "Drive", - Base = "Drive", - Arm = "Arm & Lift", - DexWrist = "Dex Wrist", - GripperLift = "Gripper & Lift", - ManipRealsense = "Drive/Arm/Gripper/Wrist", - Camera = "Camera", - // Wrist = "Wrist", + // Drive = "Drive", + Base = "Drive", + Arm = "Arm & Lift", + DexWrist = "Dex Wrist", + GripperLift = "Gripper & Lift", + ManipRealsense = "Drive/Arm/Gripper/Wrist", + Camera = "Camera", + // Wrist = "Wrist", } export enum ButtonPadIdMobile { - Arm = "Arm Mobile", - Gripper = "Gripper Mobile", - Drive = "Drive Mobile" + Arm = "Arm Mobile", + Gripper = "Gripper Mobile", + Drive = "Drive Mobile", } /** - * Identifier for the subtype of the component + * Identifier for the subtype of the component * (e.g. which video stream camera, or which button pad) * @note any new components with ID fields should be added to this type */ export type ComponentId = CameraViewId | ButtonPadId | ButtonPadIdMobile; /** - * Definition for any interface component. Any video stream, button pad, + * Definition for any interface component. Any video stream, button pad, * tabs, etc. definition will have these fields. */ export type ComponentDefinition = { - /** Indicates the type of the component */ - type: ComponentType; - /** Indicates the identifier for the sub-type of the component */ - id?: ComponentId; -} + /** Indicates the type of the component */ + type: ComponentType; + /** Indicates the identifier for the sub-type of the component */ + id?: ComponentId; +}; /** * Definition for a button pad component */ export type ButtonPadDefinition = ComponentDefinition & { - /** Indicates the shape and functions on the button pad*/ - id: ButtonPadId | ButtonPadIdMobile; -} + /** Indicates the shape and functions on the button pad*/ + id: ButtonPadId | ButtonPadIdMobile; +}; export type ParentComponentDefinition = ComponentDefinition & { - children: ComponentDefinition[]; -} + children: ComponentDefinition[]; +}; export type LayoutDefinition = ComponentDefinition & { - displayMovementRecorder: boolean; - displayLabels: boolean; - actionMode: ActionMode; - children: LayoutGridDefinition[]; -} + displayMovementRecorder: boolean; + displayLabels: boolean; + actionMode: ActionMode; + children: LayoutGridDefinition[]; +}; export type LayoutGridDefinition = ComponentDefinition & { - children: PanelDefinition[]; + children: PanelDefinition[]; }; /** * Definition for a tabs component */ export type PanelDefinition = ComponentDefinition & { - /** List of definitions for individual tabs */ - children: TabDefinition[]; -} + /** List of definitions for individual tabs */ + children: TabDefinition[]; +}; /** * Definition for a single tab in a tabs component */ - export type TabDefinition = ParentComponentDefinition & { - /** The label that appears at the top of the tabs object. */ - label: string; -} +export type TabDefinition = ParentComponentDefinition & { + /** The label that appears at the top of the tabs object. */ + label: string; +}; /** * Definition for a video stream component */ export type CameraViewDefinition = ParentComponentDefinition & { - /** Indicates the camera video of the video stream */ - id: CameraViewId; - /** Whether to display the default buttons under the camera view */ - displayButtons: boolean; -} + /** Indicates the camera video of the video stream */ + id: CameraViewId; + /** Whether to display the default buttons under the camera view */ + displayButtons: boolean; +}; /** * Definition for the gripper stream component - * + * * @note these modifications to the overhead view are implemented in the - * backend, so if multiple overhead streams are visible to the user - * simultaneously, any change to this defintion for one view will impact + * backend, so if multiple overhead streams are visible to the user + * simultaneously, any change to this definition for one view will impact * all views. */ - export type GripperVideoStreamDef = CameraViewDefinition +export type GripperVideoStreamDef = CameraViewDefinition; /** * Definition for the fixed overhead stream component - * + * * @note these modifications to the overhead view are implemented in the - * backend, so if multiple overhead streams are visible to the user - * simultaneously, any change to this defintion for one view will impact + * backend, so if multiple overhead streams are visible to the user + * simultaneously, any change to this definition for one view will impact * all views. */ export type FixedOverheadVideoStreamDef = CameraViewDefinition & { - /** - * Predictive display toggle - */ - predictiveDisplay?: boolean; -} + /** + * Predictive display toggle + */ + predictiveDisplay?: boolean; +}; /** * Definition for the adjustable overhead stream component - * + * * @note these modifications to the overhead view are implemented in the - * backend, so if multiple overhead streams are visible to the user - * simultaneously, any change to this defintion for one view will impact + * backend, so if multiple overhead streams are visible to the user + * simultaneously, any change to this definition for one view will impact * all views. */ - export type AdjustableOverheadVideoStreamDef = CameraViewDefinition & { - /** - * If the Realsense camera should pan and tilt to keep the gripper centered - * in the view. - */ - followGripper?: boolean; - /** - * Predictive display toggle - */ - predictiveDisplay?: boolean; -} +export type AdjustableOverheadVideoStreamDef = CameraViewDefinition & { + /** + * If the Realsense camera should pan and tilt to keep the gripper centered + * in the view. + */ + followGripper?: boolean; + /** + * Predictive display toggle + */ + predictiveDisplay?: boolean; +}; /** * Definition for the Realsense video stream component - * + * * @note these modifications to the Realsense view are implemented in the - * backend, so if multiple Realsense streams are visible to the user - * simultaneously, any change to this defintion for one view will impact + * backend, so if multiple Realsense streams are visible to the user + * simultaneously, any change to this definition for one view will impact * all views. */ export type RealsenseVideoStreamDef = CameraViewDefinition & { - /** - * If the Realsense camera should pan and tilt to keep the gripper centered - * in the view. - */ - followGripper?: boolean; - /** - * If the AR depth ring should be shown to indicate the extent of the - * reachable area. - */ - depthSensing?: boolean; -} + /** + * If the Realsense camera should pan and tilt to keep the gripper centered + * in the view. + */ + followGripper?: boolean; + /** + * If the AR depth ring should be shown to indicate the extent of the + * reachable area. + */ + depthSensing?: boolean; +}; /** * Definition for the map component */ export type MapDefinition = ComponentDefinition & { - /** - * Enable/disable the click listener on the map for settings a goal - */ - selectGoal?: boolean -} + /** + * Enable/disable the click listener on the map for settings a goal + */ + selectGoal?: boolean; +}; /** * Definition for the run stop button */ - export type RunStopDefinition = ComponentDefinition \ No newline at end of file +export type RunStopDefinition = ComponentDefinition; diff --git a/src/pages/operator/tsx/utils/layout_helpers.tsx b/src/pages/operator/tsx/utils/layout_helpers.tsx index c726fce2..26e7dc87 100644 --- a/src/pages/operator/tsx/utils/layout_helpers.tsx +++ b/src/pages/operator/tsx/utils/layout_helpers.tsx @@ -1,4 +1,10 @@ -import { ParentComponentDefinition, ComponentDefinition, ComponentType, LayoutDefinition, LayoutGridDefinition } from "operator/tsx/utils/component_definitions"; +import { + ParentComponentDefinition, + ComponentDefinition, + ComponentType, + LayoutDefinition, + LayoutGridDefinition, +} from "operator/tsx/utils/component_definitions"; /** * Moves a component from an old path to a new path @@ -9,77 +15,76 @@ import { ParentComponentDefinition, ComponentDefinition, ComponentType, LayoutDe * than the `newPath`) */ export function moveInLayout( - oldPath: string, - newPath: string, - layout: LayoutDefinition + oldPath: string, + newPath: string, + layout: LayoutDefinition, ): string { - // Get the child and its old parent - console.log('old path', oldPath); - console.log('newpath', newPath); - const oldPathSplit = oldPath.split('-'); - const oldParent = getParent(oldPathSplit, layout); - console.log('old parent', oldParent); - let oldChildIdx = +oldPathSplit.slice(-1); - console.log('old child index', oldChildIdx); - const temp = getChildFromParent(oldParent, oldChildIdx); - console.log('temp', temp) - - // Get the new parent - let newPathSplit = newPath.split('-'); - const newChildIdx = +newPathSplit.slice(-1); - console.log('newChildIdx', newChildIdx) - const newParent = getParent(newPathSplit, layout); - console.log('newparent', newParent) - - // Remove the child from the old parent - removeChildFromParent(oldParent, oldPathSplit, oldChildIdx, layout); - - // Put the child into the new parent - putChildInParent(newParent, temp, newChildIdx); - console.log('after adding child', newParent.children); - - // Same parent and moved to lower index, previous position index is now higher - if (oldParent === newParent && oldChildIdx > newChildIdx) - oldChildIdx++; - - // If newPath only contains one element it is a layout grid with one panel - // Add 0 to the path to point to select first child in layout grid - // if (newParent.type == ComponentType.LayoutGrid) { - // console.log(newPathSplit) - if (newParent.type == ComponentType.LayoutGrid) { - if (+newPathSplit[0] === layout.children.length) newPathSplit[0] = (+newPathSplit[0] - 1).toString() - return newPathSplit.join('-') - } - if (newParent.type == ComponentType.Layout) { - if (+newPathSplit[0] === layout.children.length) newPathSplit[0] = (+newPathSplit[0] - 1).toString() - if (newPathSplit.length === 1) newPathSplit.push('0') - return newPathSplit.join('-') - } - // } - - // Check if removing the old path changes the new path - // note: this happens when the old path was a sibling with a lower index to - // any node in the - if (newPathSplit.length < oldPathSplit.length) - return newPath; - - const oldPathLastIdx = oldPathSplit.length - 1; - const oldPrefix = oldPathSplit.slice(0, oldPathLastIdx); - const newPrefix = newPathSplit.slice(0, oldPathLastIdx) - const sameParent = oldPrefix.every((val, index) => val === newPrefix[index]) - - if (!sameParent) - return newPath; - - // index of new sibling node - const newCorrespondingIdx = +newPathSplit[oldPathLastIdx]; - if (oldChildIdx < newCorrespondingIdx) { - // decrease new path index since the old path is deleted - newPathSplit[oldPathLastIdx] = "" + (+newPathSplit[oldPathLastIdx] - 1); - console.log('updated path', newPathSplit.join('-')) - } - - return newPathSplit.join('-'); + // Get the child and its old parent + console.log("old path", oldPath); + console.log("newpath", newPath); + const oldPathSplit = oldPath.split("-"); + const oldParent = getParent(oldPathSplit, layout); + console.log("old parent", oldParent); + let oldChildIdx = +oldPathSplit.slice(-1); + console.log("old child index", oldChildIdx); + const temp = getChildFromParent(oldParent, oldChildIdx); + console.log("temp", temp); + + // Get the new parent + let newPathSplit = newPath.split("-"); + const newChildIdx = +newPathSplit.slice(-1); + console.log("newChildIdx", newChildIdx); + const newParent = getParent(newPathSplit, layout); + console.log("newparent", newParent); + + // Remove the child from the old parent + removeChildFromParent(oldParent, oldPathSplit, oldChildIdx, layout); + + // Put the child into the new parent + putChildInParent(newParent, temp, newChildIdx); + console.log("after adding child", newParent.children); + + // Same parent and moved to lower index, previous position index is now higher + if (oldParent === newParent && oldChildIdx > newChildIdx) oldChildIdx++; + + // If newPath only contains one element it is a layout grid with one panel + // Add 0 to the path to point to select first child in layout grid + // if (newParent.type == ComponentType.LayoutGrid) { + // console.log(newPathSplit) + if (newParent.type == ComponentType.LayoutGrid) { + if (+newPathSplit[0] === layout.children.length) + newPathSplit[0] = (+newPathSplit[0] - 1).toString(); + return newPathSplit.join("-"); + } + if (newParent.type == ComponentType.Layout) { + if (+newPathSplit[0] === layout.children.length) + newPathSplit[0] = (+newPathSplit[0] - 1).toString(); + if (newPathSplit.length === 1) newPathSplit.push("0"); + return newPathSplit.join("-"); + } + // } + + // Check if removing the old path changes the new path + // note: this happens when the old path was a sibling with a lower index to + // any node in the + if (newPathSplit.length < oldPathSplit.length) return newPath; + + const oldPathLastIdx = oldPathSplit.length - 1; + const oldPrefix = oldPathSplit.slice(0, oldPathLastIdx); + const newPrefix = newPathSplit.slice(0, oldPathLastIdx); + const sameParent = oldPrefix.every((val, index) => val === newPrefix[index]); + + if (!sameParent) return newPath; + + // index of new sibling node + const newCorrespondingIdx = +newPathSplit[oldPathLastIdx]; + if (oldChildIdx < newCorrespondingIdx) { + // decrease new path index since the old path is deleted + newPathSplit[oldPathLastIdx] = "" + (+newPathSplit[oldPathLastIdx] - 1); + console.log("updated path", newPathSplit.join("-")); + } + + return newPathSplit.join("-"); } /** @@ -89,18 +94,18 @@ export function moveInLayout( * @param layout the entire layout structure */ export function addToLayout( - definition: ComponentDefinition, - newPath: string, - layout: LayoutDefinition + definition: ComponentDefinition, + newPath: string, + layout: LayoutDefinition, ) { - let newPathSplit = newPath.split('-'); - const newChildIdx = +newPathSplit.slice(-1); - const newParent = getParent(newPathSplit, layout); - putChildInParent(newParent, definition, newChildIdx); - - // If newPath only contains one element it is a layout grid with one panel - // Add 0 to the path to point to select first child in layout grid - return newPathSplit.length == 1 ? newPath + '-0' : newPath + let newPathSplit = newPath.split("-"); + const newChildIdx = +newPathSplit.slice(-1); + const newParent = getParent(newPathSplit, layout); + putChildInParent(newParent, definition, newChildIdx); + + // If newPath only contains one element it is a layout grid with one panel + // Add 0 to the path to point to select first child in layout grid + return newPathSplit.length == 1 ? newPath + "-0" : newPath; } /** @@ -108,44 +113,41 @@ export function addToLayout( * @param path path to the component to delete * @param layout the entire layout structure */ -export function removeFromLayout( - path: string, - layout: LayoutDefinition -) { - const splitPath = path.split('-'); - const childIdx = +splitPath.slice(-1); - const parent = getParent(splitPath, layout); - removeChildFromParent(parent, splitPath, childIdx, layout); +export function removeFromLayout(path: string, layout: LayoutDefinition) { + const splitPath = path.split("-"); + const childIdx = +splitPath.slice(-1); + const parent = getParent(splitPath, layout); + removeChildFromParent(parent, splitPath, childIdx, layout); } /** - * Removes the child from the + * Removes the child from the * @param parent parent component definition * @param childSplitPath path to child element, split into list of indices * @param childIdx index of child (last element in `splitPath`) * @param layout entire layout structure */ function removeChildFromParent( - parent: ParentComponentDefinition, - childSplitPath: string[], - childIdx: number, - layout: LayoutDefinition + parent: ParentComponentDefinition, + childSplitPath: string[], + childIdx: number, + layout: LayoutDefinition, ) { - // If this is the last tab in a panel, then delete the entire panel - if (parent.type === ComponentType.Panel && parent.children.length === 1) { - const parentIdx = +childSplitPath.slice(-2, -1); - // note: since tabs cannot be nested, we can assume the layout is the - // is the parent of the tabs component - layout.children.splice(parentIdx, 1); - } - parent.children.splice(childIdx, 1); - console.log(parent.type, parent.children.length) - - if (parent.type == ComponentType.LayoutGrid && parent.children.length === 0) { - const parentIdx = +childSplitPath.slice(0, 1); - console.log(layout.children, childSplitPath, parentIdx) - layout.children.splice(parentIdx, 1); - } + // If this is the last tab in a panel, then delete the entire panel + if (parent.type === ComponentType.Panel && parent.children.length === 1) { + const parentIdx = +childSplitPath.slice(-2, -1); + // note: since tabs cannot be nested, we can assume the layout is the + // is the parent of the tabs component + layout.children.splice(parentIdx, 1); + } + parent.children.splice(childIdx, 1); + console.log(parent.type, parent.children.length); + + if (parent.type == ComponentType.LayoutGrid && parent.children.length === 0) { + const parentIdx = +childSplitPath.slice(0, 1); + console.log(layout.children, childSplitPath, parentIdx); + layout.children.splice(parentIdx, 1); + } } /** @@ -154,15 +156,18 @@ function removeChildFromParent( * @param layout the layout object * @returns the parent definition */ -function getParent(splitPath: string[], layout: ParentComponentDefinition): ParentComponentDefinition { - let pathIdx = 0; - let parent: ParentComponentDefinition = layout; - while (pathIdx < splitPath.length - 1) { - const childIdx = +splitPath[pathIdx]; - parent = parent.children[childIdx] as ParentComponentDefinition; - pathIdx++; - } - return parent!; +function getParent( + splitPath: string[], + layout: ParentComponentDefinition, +): ParentComponentDefinition { + let pathIdx = 0; + let parent: ParentComponentDefinition = layout; + while (pathIdx < splitPath.length - 1) { + const childIdx = +splitPath[pathIdx]; + parent = parent.children[childIdx] as ParentComponentDefinition; + pathIdx++; + } + return parent!; } /** @@ -171,8 +176,11 @@ function getParent(splitPath: string[], layout: ParentComponentDefinition): Pare * @param childIdx index of the child element to retrieve * @returns the child component definition */ -function getChildFromParent (parent: ParentComponentDefinition, childIdx: number): ComponentDefinition { - return parent.children[childIdx]; +function getChildFromParent( + parent: ParentComponentDefinition, + childIdx: number, +): ComponentDefinition { + return parent.children[childIdx]; } /** @@ -181,13 +189,19 @@ function getChildFromParent (parent: ParentComponentDefinition, childIdx: number * @param child definition of child component * @param childIdx index where to insert the child */ -function putChildInParent (parent: ParentComponentDefinition, child: ComponentDefinition, childIdx: number) { - if (parent.children) { - parent.type == ComponentType.Layout ? - parent.children.splice(childIdx, 0, {type: ComponentType.LayoutGrid, children: [child]} as LayoutGridDefinition) - : - parent.children.splice(childIdx, 0, child) - } else { - parent.children = [child] - } -} \ No newline at end of file +function putChildInParent( + parent: ParentComponentDefinition, + child: ComponentDefinition, + childIdx: number, +) { + if (parent.children) { + parent.type == ComponentType.Layout + ? parent.children.splice(childIdx, 0, { + type: ComponentType.LayoutGrid, + children: [child], + } as LayoutGridDefinition) + : parent.children.splice(childIdx, 0, child); + } else { + parent.children = [child]; + } +} diff --git a/src/pages/operator/tsx/utils/svg.tsx b/src/pages/operator/tsx/utils/svg.tsx index 04a6a8fa..7d65b303 100644 --- a/src/pages/operator/tsx/utils/svg.tsx +++ b/src/pages/operator/tsx/utils/svg.tsx @@ -1,78 +1,77 @@ -import armDown from "operator/icons/Arm_Down.svg" -import armExtend from "operator/icons/Arm_Out.svg" -import armRetract from "operator/icons/Arm_In.svg" -import armUp from "operator/icons/Arm_Up.svg" -import driveLeft from "operator/icons/Drive_Left.svg" -import driveRight from "operator/icons/Drive_Right.svg" -import gripClose from "operator/icons/Grip_Grasp.svg" -import gripLeft from "operator/icons/Grip_Left.svg" -import gripOpen from "operator/icons/Grip_Open.svg" -import gripRight from "operator/icons/Grip_Right.svg" -import driveForward from "operator/icons/Drive_Forward.svg" -import driveReverse from "operator/icons/Drive_Backward.svg" -import panLeft from "operator/icons/Pan_Left.svg" -import panRight from "operator/icons/Pan_Right.svg" -import tiltUp from "operator/icons/Tilt_Up.svg" -import tiltDown from "operator/icons/Tilt_Down.svg" -import armIn from "operator/icons/Arm_In.svg" -import armOut from "operator/icons/Arm_Out.svg" -import rollLeft from "operator/icons/Roll_Left.svg" -import rollRight from "operator/icons/Roll_Right.svg" -import pitchDown from "operator/icons/Pitch_Down.svg" -import pitchUp from "operator/icons/Pitch_Up.svg" -import yawLeft from "operator/icons/Yaw_Left.svg" -import yawRight from "operator/icons/Yaw_Right.svg" +import armDown from "operator/icons/Arm_Down.svg"; +import armExtend from "operator/icons/Arm_Out.svg"; +import armRetract from "operator/icons/Arm_In.svg"; +import armUp from "operator/icons/Arm_Up.svg"; +import driveLeft from "operator/icons/Drive_Left.svg"; +import driveRight from "operator/icons/Drive_Right.svg"; +import gripClose from "operator/icons/Grip_Grasp.svg"; +import gripLeft from "operator/icons/Grip_Left.svg"; +import gripOpen from "operator/icons/Grip_Open.svg"; +import gripRight from "operator/icons/Grip_Right.svg"; +import driveForward from "operator/icons/Drive_Forward.svg"; +import driveReverse from "operator/icons/Drive_Backward.svg"; +import panLeft from "operator/icons/Pan_Left.svg"; +import panRight from "operator/icons/Pan_Right.svg"; +import tiltUp from "operator/icons/Tilt_Up.svg"; +import tiltDown from "operator/icons/Tilt_Down.svg"; +import armIn from "operator/icons/Arm_In.svg"; +import armOut from "operator/icons/Arm_Out.svg"; +import rollLeft from "operator/icons/Roll_Left.svg"; +import rollRight from "operator/icons/Roll_Right.svg"; +import pitchDown from "operator/icons/Pitch_Down.svg"; +import pitchUp from "operator/icons/Pitch_Up.svg"; +import yawLeft from "operator/icons/Yaw_Left.svg"; +import yawRight from "operator/icons/Yaw_Right.svg"; -import { ButtonPadButton } from "../function_providers/ButtonFunctionProvider" -import { isMobile } from "react-device-detect" +import { ButtonPadButton } from "../function_providers/ButtonFunctionProvider"; +import { isMobile } from "react-device-detect"; /** The pixel width of SVG components. */ export const SVG_RESOLUTION = 500; - -/** - * Possible layouts for the button pad (i.e. the shape and arrangement of the +/** + * Possible layouts for the button pad (i.e. the shape and arrangement of the * buttons) */ export enum ButtonPadShape { - Directional, - ManipRealsense, - GripperLift, - DexWrist, - SimpleButtonPad, - RowButtonPad, - StackedButtonPad + Directional, + ManipRealsense, + GripperLift, + DexWrist, + SimpleButtonPad, + RowButtonPad, + StackedButtonPad, } /** * Takes a percentage value and returns a pixel value based on {@link SVG_RESOLUTION} - * + * * @param percentage value between 0 and 100 * @returns the pixel location * @example 0 -> 0, 50 -> resolution/2, 100 -> resolution */ export function percent2Pixel(percentage: number) { - return SVG_RESOLUTION / 100 * percentage; + return (SVG_RESOLUTION / 100) * percentage; } /** * Position and dimensions of the robot base from the overhead camera view */ export const OVERHEAD_ROBOT_BASE = { - centerX: percent2Pixel(49), - centerY: percent2Pixel(75), - height: percent2Pixel(10), - width: percent2Pixel(15) -} + centerX: percent2Pixel(49), + centerY: percent2Pixel(75), + height: percent2Pixel(10), + width: percent2Pixel(15), +}; /**Creates the SVG path for a rectangle * @param x left edge location * @param y top edge location * @param width the width * @param height the height -*/ + */ export function rect(x: number, y: number, width: number, height: number) { - return `M ${x} ${y} ${x + width} ${y} ${x + width} ${y + height} + return `M ${x} ${y} ${x + width} ${y} ${x + width} ${y + height} ${x} ${y + height} Z`; } @@ -87,343 +86,381 @@ export function rect(x: number, y: number, width: number, height: number) { * @return {String} Rounded rectangle SVG path data */ -export function roundedRect(x: number, y: number, width: number, height: number) { +export function roundedRect( + x: number, + y: number, + width: number, + height: number, +) { return `M${x},${y} h${width} a20,20 0 0 1 20,20 v${height} a20,20 0 0 1 -20,20 h-${width} a20,20 0 0 1 -20,-20 v-${height} a20,20 0 0 1 20,-20 z - ` -}; + `; +} // M${x},${y} h${width} a20,20 0 0 1 20,20 v${height} a20,20 0 0 1 -20,20 h-${width} a20,20 0 0 1 -20,-20 v-${height} a20,20 0 0 1 20,-20 z /** Represents the position and size of a box */ type BoxPosition = { - centerX: number, - centerY: number, - height: number, - width: number -} + centerX: number; + centerY: number; + height: number; + width: number; +}; -/** Default box position with the box centered and a height and width +/** Default box position with the box centered and a height and width * of 10% */ const DEFAULT_POSITION = { - centerX: percent2Pixel(50), - centerY: percent2Pixel(50), - height: percent2Pixel(10), - width: percent2Pixel(10) -} + centerX: percent2Pixel(50), + centerY: percent2Pixel(50), + height: percent2Pixel(10), + width: percent2Pixel(10), +}; /** * Gets a list of path string descriptions for each button based on the {@link ButtonPadShape} - * + * * @param shape {@link ButtonPadShape} enum representing the shape of the button pad * @returns a list of strings where each string is a path description for the shape of a single button */ -export function getPathsFromShape(shape: ButtonPadShape, aspectRatio?: number): [string[], { x: number, y: number }[]] { - const width = SVG_RESOLUTION; - const height = aspectRatio ? SVG_RESOLUTION / aspectRatio : SVG_RESOLUTION; - switch (shape) { - case ButtonPadShape.Directional: - return getDirectionalPaths(width, height); - case ButtonPadShape.ManipRealsense: - return getManipRealsensePaths(width, height); - case ButtonPadShape.GripperLift: - return getGripperLiftPaths(width, height); - case ButtonPadShape.DexWrist: - return getDexWristPaths(width, height); - case ButtonPadShape.SimpleButtonPad: - return getSimpleButtonPadPaths(width, height); - case ButtonPadShape.RowButtonPad: - return getRowButtonPadPaths(width, height) - case ButtonPadShape.StackedButtonPad: - return getStackedButtonPadPaths(width, height) - default: - throw Error(`Cannot get paths of unknown button pad shape ${ButtonPadShape}`); - } +export function getPathsFromShape( + shape: ButtonPadShape, + aspectRatio?: number, +): [string[], { x: number; y: number }[]] { + const width = SVG_RESOLUTION; + const height = aspectRatio ? SVG_RESOLUTION / aspectRatio : SVG_RESOLUTION; + switch (shape) { + case ButtonPadShape.Directional: + return getDirectionalPaths(width, height); + case ButtonPadShape.ManipRealsense: + return getManipRealsensePaths(width, height); + case ButtonPadShape.GripperLift: + return getGripperLiftPaths(width, height); + case ButtonPadShape.DexWrist: + return getDexWristPaths(width, height); + case ButtonPadShape.SimpleButtonPad: + return getSimpleButtonPadPaths(width, height); + case ButtonPadShape.RowButtonPad: + return getRowButtonPadPaths(width, height); + case ButtonPadShape.StackedButtonPad: + return getStackedButtonPadPaths(width, height); + default: + throw Error( + `Cannot get paths of unknown button pad shape ${ButtonPadShape}`, + ); + } } /** - * Directional button pad made up of four trapazoids around a box in the + * Directional button pad made up of four trapazoids around a box in the * center of the button pad. - * + * * Ordered: top, right, bottom, left (clockwise starting with the top) - * + * * @param onRobot if the square should be around the robot, centered if false */ -function getDirectionalPaths(width: number, height: number, onRobot: boolean = true): [string[], { x: number, y: number }[]] { - const boxPosition: BoxPosition = onRobot ? OVERHEAD_ROBOT_BASE : DEFAULT_POSITION; - const { centerX, centerY, height: boxHeight, width: boxWidth } = boxPosition; - const top = (centerY - boxHeight / 2) / width * height; - const bot = (centerY + boxHeight / 2) / width * height; - const lft = centerX - boxWidth / 2; - const rgt = centerX + boxWidth / 2; +function getDirectionalPaths( + width: number, + height: number, + onRobot: boolean = true, +): [string[], { x: number; y: number }[]] { + const boxPosition: BoxPosition = onRobot + ? OVERHEAD_ROBOT_BASE + : DEFAULT_POSITION; + const { centerX, centerY, height: boxHeight, width: boxWidth } = boxPosition; + const top = ((centerY - boxHeight / 2) / width) * height; + const bot = ((centerY + boxHeight / 2) / width) * height; + const lft = centerX - boxWidth / 2; + const rgt = centerX + boxWidth / 2; - const pathTop = `M 0 0 ${width} 0 ${rgt} ${top} ${lft} ${top} Z` - const pathRgt = `M ${width} 0 ${width} ${height} - ${rgt} ${bot} ${rgt} ${top} Z` - const pathBot = `M 0 ${height} ${width} ${height} - ${rgt} ${bot} ${lft} ${bot} Z` - const pathLft = `M 0 0 0 ${height} ${lft} ${bot} ${lft} ${top} Z` + const pathTop = `M 0 0 ${width} 0 ${rgt} ${top} ${lft} ${top} Z`; + const pathRgt = `M ${width} 0 ${width} ${height} + ${rgt} ${bot} ${rgt} ${top} Z`; + const pathBot = `M 0 ${height} ${width} ${height} + ${rgt} ${bot} ${lft} ${bot} Z`; + const pathLft = `M 0 0 0 ${height} ${lft} ${bot} ${lft} ${top} Z`; - const paths = [pathTop, pathRgt, pathBot, pathLft] - const iconPositions = [ - { x: centerX, y: top / 2 }, - { x: (SVG_RESOLUTION + rgt) / 2, y: centerY / width * height }, - { x: centerX, y: (SVG_RESOLUTION / width * height + bot) / 2 }, - { x: (lft) / 2, y: centerY / width * height } - ] - return [paths, iconPositions]; + const paths = [pathTop, pathRgt, pathBot, pathLft]; + const iconPositions = [ + { x: centerX, y: top / 2 }, + { x: (SVG_RESOLUTION + rgt) / 2, y: (centerY / width) * height }, + { x: centerX, y: ((SVG_RESOLUTION / width) * height + bot) / 2 }, + { x: lft / 2, y: (centerY / width) * height }, + ]; + return [paths, iconPositions]; } /** - * Ordered: top left, top right, then top, bottom, left, right trapezoids, then + * Ordered: top left, top right, then top, bottom, left, right trapezoids, then * top and bottom center buttons, and finally bottom left and bottom right. */ -function getManipRealsensePaths(width: number, height: number): [string[], { x: number, y: number }[]] { - /**Number of button layers from top to bottom in the display*/ - const numVerticalLayers = 6; - /**How tall each layer of buttons should be.*/ - const layerHeight = height / numVerticalLayers; - const centerWidth = percent2Pixel(30); - const centerLeft = (width - centerWidth) / 2; - const centerRight = centerLeft + centerWidth; - const center = percent2Pixel(50); - const paths = [ - // Top two buttons: left, right - rect(0, 0, center, layerHeight), - rect(center, 0, center, layerHeight), - // Center directional trapezoid buttons: top, bottom, left, right - `M 0 ${layerHeight} ${width} ${layerHeight} ${centerRight} ${layerHeight * 2} +function getManipRealsensePaths( + width: number, + height: number, +): [string[], { x: number; y: number }[]] { + /**Number of button layers from top to bottom in the display*/ + const numVerticalLayers = 6; + /**How tall each layer of buttons should be.*/ + const layerHeight = height / numVerticalLayers; + const centerWidth = percent2Pixel(30); + const centerLeft = (width - centerWidth) / 2; + const centerRight = centerLeft + centerWidth; + const center = percent2Pixel(50); + const paths = [ + // Top two buttons: left, right + rect(0, 0, center, layerHeight), + rect(center, 0, center, layerHeight), + // Center directional trapezoid buttons: top, bottom, left, right + `M 0 ${layerHeight} ${width} ${layerHeight} ${centerRight} ${layerHeight * 2} ${centerLeft} ${layerHeight * 2} Z`, - `M 0 ${layerHeight * 5} ${width} ${layerHeight * 5} + `M 0 ${layerHeight * 5} ${width} ${layerHeight * 5} ${centerRight},${layerHeight * 4} ${centerLeft},${layerHeight * 4} Z`, - `M 0 ${layerHeight} 0 ${layerHeight * 5} ${centerLeft},${layerHeight * 4} + `M 0 ${layerHeight} 0 ${layerHeight * 5} ${centerLeft},${layerHeight * 4} ${centerLeft},${layerHeight * 2} Z`, - `M ${width} ${layerHeight} ${width} ${layerHeight * 5} + `M ${width} ${layerHeight} ${width} ${layerHeight * 5} ${centerRight},${layerHeight * 4} ${centerRight},${layerHeight * 2} Z`, - // // Center two rectangle buttons: top, bottom - rect(centerLeft, layerHeight * 2, centerWidth, layerHeight), - rect(centerLeft, layerHeight * 3, centerWidth, layerHeight), - // // Bottom two buttons: left, right - rect(0, layerHeight * 5, center, layerHeight), - rect(center, layerHeight * 5, center, layerHeight) - ] - const iconPositions = [ - // Top two - { x: center / 2, y: layerHeight / 2 }, - { x: (width + center) / 2, y: layerHeight / 2 }, - // Center directional trapezoid buttons - { x: width / 2, y: layerHeight * 3 / 2 }, - { x: width / 2, y: layerHeight * 9 / 2 }, - { x: centerLeft / 2, y: layerHeight * 6 / 2 }, - { x: (width + centerRight) / 2, y: layerHeight * 6 / 2 }, - // Center two rectangle buttons - { x: width / 2, y: layerHeight * 5 / 2 }, - { x: width / 2, y: layerHeight * 7 / 2 }, - // Bottom two buttons - { x: center / 2, y: layerHeight * 11 / 2 }, - { x: (width + center) / 2, y: layerHeight * 11 / 2 } - ] - return [paths, iconPositions]; + // // Center two rectangle buttons: top, bottom + rect(centerLeft, layerHeight * 2, centerWidth, layerHeight), + rect(centerLeft, layerHeight * 3, centerWidth, layerHeight), + // // Bottom two buttons: left, right + rect(0, layerHeight * 5, center, layerHeight), + rect(center, layerHeight * 5, center, layerHeight), + ]; + const iconPositions = [ + // Top two + { x: center / 2, y: layerHeight / 2 }, + { x: (width + center) / 2, y: layerHeight / 2 }, + // Center directional trapezoid buttons + { x: width / 2, y: (layerHeight * 3) / 2 }, + { x: width / 2, y: (layerHeight * 9) / 2 }, + { x: centerLeft / 2, y: (layerHeight * 6) / 2 }, + { x: (width + centerRight) / 2, y: (layerHeight * 6) / 2 }, + // Center two rectangle buttons + { x: width / 2, y: (layerHeight * 5) / 2 }, + { x: width / 2, y: (layerHeight * 7) / 2 }, + // Bottom two buttons + { x: center / 2, y: (layerHeight * 11) / 2 }, + { x: (width + center) / 2, y: (layerHeight * 11) / 2 }, + ]; + return [paths, iconPositions]; } /** - * Ordered top, botton, left, right, larger center, smaller center + * Ordered top, bottom, left, right, larger center, smaller center */ -function getGripperLiftPaths(width: number, height: number): [string[], { x: number, y: number }[]] { - /**Number of button layers from top to bottom in the display*/ - const numLayers = 5; - /**How wide each layer of buttons should be.*/ - const xMargin = width / numLayers; - /**How tall each layer of buttons should be.*/ - const yMargin = height / numLayers; - const paths = [ - rect(0, 0, width, yMargin), // top - rect(0, height - yMargin, width, yMargin), // bottom - rect(0, yMargin, xMargin, yMargin * 3), // left - rect(width - xMargin, yMargin, xMargin, yMargin * 3), // right - // gripper open - // Outside (clockwise) - `M - ${xMargin} ${yMargin} - ${width - xMargin} ${yMargin} - ${width - xMargin} ${height - yMargin} - ${xMargin} ${height - yMargin} - Z` - // Inside (counterclockwise) - + `M - ${xMargin * 2} ${yMargin * 2} +function getGripperLiftPaths( + width: number, + height: number, +): [string[], { x: number; y: number }[]] { + /**Number of button layers from top to bottom in the display*/ + const numLayers = 5; + /**How wide each layer of buttons should be.*/ + const xMargin = width / numLayers; + /**How tall each layer of buttons should be.*/ + const yMargin = height / numLayers; + const paths = [ + rect(0, 0, width, yMargin), // top + rect(0, height - yMargin, width, yMargin), // bottom + rect(0, yMargin, xMargin, yMargin * 3), // left + rect(width - xMargin, yMargin, xMargin, yMargin * 3), // right + // gripper open + // Outside (clockwise) + `M + ${xMargin} ${yMargin} + ${width - xMargin} ${yMargin} + ${width - xMargin} ${height - yMargin} + ${xMargin} ${height - yMargin} + Z` + + // Inside (counterclockwise) + `M + ${xMargin * 2} ${yMargin * 2} ${xMargin * 2} ${height - yMargin * 2} - ${width - xMargin * 2} ${height - yMargin * 2} + ${width - xMargin * 2} ${height - yMargin * 2} ${width - xMargin * 2} ${yMargin * 2} Z`, - rect(xMargin * 2, yMargin * 2, xMargin, yMargin) // gripper close - ] - const iconPositions = [ - { x: width / 2, y: yMargin / 2 }, // top - { x: width / 2, y: (2 * height - yMargin) / 2 }, // bottom - { x: yMargin / 2, y: height / 2 }, // left - { x: (2 * width - yMargin) / 2, y: height / 2 }, // right - { x: yMargin * 7 / 2, y: height / 2 }, // gripper open - { x: width / 2, y: height / 2 }, // gripper close - ] - return [paths, iconPositions]; + rect(xMargin * 2, yMargin * 2, xMargin, yMargin), // gripper close + ]; + const iconPositions = [ + { x: width / 2, y: yMargin / 2 }, // top + { x: width / 2, y: (2 * height - yMargin) / 2 }, // bottom + { x: yMargin / 2, y: height / 2 }, // left + { x: (2 * width - yMargin) / 2, y: height / 2 }, // right + { x: (yMargin * 7) / 2, y: height / 2 }, // gripper open + { x: width / 2, y: height / 2 }, // gripper close + ]; + return [paths, iconPositions]; } - /** * Ordered top, bottom, far left, far right, inside left, inside right */ -function getDexWristPaths(width: number, height: number): [string[], { x: number, y: number }[]] { - const sidesPercent = 0.25; - const sideWidth = width * sidesPercent; - const centerWidth = width - 2 * sideWidth; - const yLayer = height / 3; +function getDexWristPaths( + width: number, + height: number, +): [string[], { x: number; y: number }[]] { + const sidesPercent = 0.25; + const sideWidth = width * sidesPercent; + const centerWidth = width - 2 * sideWidth; + const yLayer = height / 3; - const paths = [ - rect(sideWidth, 0, centerWidth, yLayer), // top - rect(sideWidth, height - yLayer, centerWidth, yLayer), // bottom - rect(0, 0, sideWidth, height / 2), // far left top - rect(width - sideWidth, 0, sideWidth, height / 2), // far right top - rect(0, height / 2, sideWidth, height / 2), // far left bottom - rect(width - sideWidth, height / 2, sideWidth, height / 2), // far right bottom - rect(sideWidth, yLayer, centerWidth / 2, yLayer), // inside left - rect(sideWidth + centerWidth / 2, yLayer, centerWidth / 2, yLayer), // inside right - ]; - const iconPositions = [ - { x: width / 2, y: yLayer / 2 }, // top - { x: width / 2, y: height - yLayer / 2 }, // bottom - { x: sideWidth / 2, y: height / 4 }, // far left top - { x: width - sideWidth / 2, y: height / 4 }, // far right top - { x: sideWidth / 2, y: 3 * height / 4 }, // far left top - { x: width - sideWidth / 2, y: 3 * height / 4 }, // far right top - { x: sideWidth + centerWidth / 4, y: height / 2 }, // inside left - { x: width - sideWidth - centerWidth / 4, y: height / 2 }, // inside right - ] - return [paths, iconPositions]; + const paths = [ + rect(sideWidth, 0, centerWidth, yLayer), // top + rect(sideWidth, height - yLayer, centerWidth, yLayer), // bottom + rect(0, 0, sideWidth, height / 2), // far left top + rect(width - sideWidth, 0, sideWidth, height / 2), // far right top + rect(0, height / 2, sideWidth, height / 2), // far left bottom + rect(width - sideWidth, height / 2, sideWidth, height / 2), // far right bottom + rect(sideWidth, yLayer, centerWidth / 2, yLayer), // inside left + rect(sideWidth + centerWidth / 2, yLayer, centerWidth / 2, yLayer), // inside right + ]; + const iconPositions = [ + { x: width / 2, y: yLayer / 2 }, // top + { x: width / 2, y: height - yLayer / 2 }, // bottom + { x: sideWidth / 2, y: height / 4 }, // far left top + { x: width - sideWidth / 2, y: height / 4 }, // far right top + { x: sideWidth / 2, y: (3 * height) / 4 }, // far left top + { x: width - sideWidth / 2, y: (3 * height) / 4 }, // far right top + { x: sideWidth + centerWidth / 4, y: height / 2 }, // inside left + { x: width - sideWidth - centerWidth / 4, y: height / 2 }, // inside right + ]; + return [paths, iconPositions]; } /** * Ordered top, bottom, left, right */ -function getSimpleButtonPadPaths(width: number, height: number): [string[], { x: number, y: number }[]] { - const endsPercent = 0.30; - const endsHeight = height * endsPercent; - const center = width / 2; - const middleHeight = height - endsHeight * 2; +function getSimpleButtonPadPaths( + width: number, + height: number, +): [string[], { x: number; y: number }[]] { + const endsPercent = 0.3; + const endsHeight = height * endsPercent; + const center = width / 2; + const middleHeight = height - endsHeight * 2; - const paths = [ - rect(0, 0, width, endsHeight), // top - rect(0, height - endsHeight, width, endsHeight), // bottom - rect(0, endsHeight, center, middleHeight), // left - rect(center, endsHeight, center, middleHeight) // right - ]; - const iconPositions = [ - { x: width / 2, y: endsHeight / 2 }, // top - { x: width / 2, y: height - endsHeight / 2 }, // bottom - { x: center / 2, y: height / 2 }, // left - { x: width - center / 2, y: height / 2 }, // right - ] - return [paths, iconPositions]; + const paths = [ + rect(0, 0, width, endsHeight), // top + rect(0, height - endsHeight, width, endsHeight), // bottom + rect(0, endsHeight, center, middleHeight), // left + rect(center, endsHeight, center, middleHeight), // right + ]; + const iconPositions = [ + { x: width / 2, y: endsHeight / 2 }, // top + { x: width / 2, y: height - endsHeight / 2 }, // bottom + { x: center / 2, y: height / 2 }, // left + { x: width - center / 2, y: height / 2 }, // right + ]; + return [paths, iconPositions]; } /** * Ordered from left to right */ -function getRowButtonPadPaths(width: number, height: number): [string[], { x: number, y: number }[]] { - height = height * 2; - - const paths = [ - roundedRect(width / 13, height / 16, width / 10, height/4), - roundedRect(width / 13 + width / 4, height / 16, width / 10, height/4), - roundedRect(width / 13 + width / 2, height / 16, width / 10, height/4), - roundedRect(width / 13 + (3 * width) / 4, height / 16, width / 10, height/4) - ]; +function getRowButtonPadPaths( + width: number, + height: number, +): [string[], { x: number; y: number }[]] { + height = height * 2; + + const paths = [ + roundedRect(width / 13, height / 16, width / 10, height / 4), + roundedRect(width / 13 + width / 4, height / 16, width / 10, height / 4), + roundedRect(width / 13 + width / 2, height / 16, width / 10, height / 4), + roundedRect( + width / 13 + (3 * width) / 4, + height / 16, + width / 10, + height / 4, + ), + ]; - const iconPositions = [ - { x: width / 8, y: 0.25 * height }, - { x: (3*width) / 8, y: 0.25 * height }, - { x: (5*width) / 8, y: 0.25 * height }, - { x: (7*width) / 8, y: 0.25 * height }, - ] - return [paths, iconPositions]; + const iconPositions = [ + { x: width / 8, y: 0.25 * height }, + { x: (3 * width) / 8, y: 0.25 * height }, + { x: (5 * width) / 8, y: 0.25 * height }, + { x: (7 * width) / 8, y: 0.25 * height }, + ]; + return [paths, iconPositions]; } -function getStackedButtonPadPaths(width: number, height: number): [string[], { x: number, y: number }[]] { - const endsPercent = 0.25; - const endsHeight = height * endsPercent; +function getStackedButtonPadPaths( + width: number, + height: number, +): [string[], { x: number; y: number }[]] { + const endsPercent = 0.25; + const endsHeight = height * endsPercent; - const paths = [ - rect(0, 0, width / 2, endsHeight), // top left - rect(width / 2, 0, width / 2, endsHeight), // top right - rect(0, height - 3 * endsHeight, width / 2, endsHeight), // middle 1 left - rect(width / 2, height - 3 * endsHeight, width / 2, endsHeight), // middle 1 right - rect(0, height - 2 * endsHeight, width / 2, endsHeight), // middle 2 left - rect(width / 2, height - 2 * endsHeight, width / 2, endsHeight), // middle 2 right - rect(0, height - endsHeight, width / 2, endsHeight), // bottom left - rect(width / 2, height - endsHeight, width / 2, endsHeight), // bottom right - ]; - const iconPositions = [ - { x: width / 4, y: endsHeight / 2 }, // top left - { x: (3 * width) / 4, y: endsHeight / 2 }, // top right - { x: width / 4, y: height - 5 * endsHeight / 2 }, // middle 1 left - { x: (3 * width) / 4, y: height - 5 * endsHeight / 2 }, // middle 1 right - { x: width / 4, y: height - 3 * endsHeight / 2 }, // middle 2 left - { x: (3 * width) / 4, y: height - 3 * endsHeight / 2 }, // middle 2 right - { x: width / 4, y: height - endsHeight / 2 }, // bottom left - { x: (3 * width) / 4, y: height - endsHeight / 2 }, // bottom right - ] - return [paths, iconPositions]; + const paths = [ + rect(0, 0, width / 2, endsHeight), // top left + rect(width / 2, 0, width / 2, endsHeight), // top right + rect(0, height - 3 * endsHeight, width / 2, endsHeight), // middle 1 left + rect(width / 2, height - 3 * endsHeight, width / 2, endsHeight), // middle 1 right + rect(0, height - 2 * endsHeight, width / 2, endsHeight), // middle 2 left + rect(width / 2, height - 2 * endsHeight, width / 2, endsHeight), // middle 2 right + rect(0, height - endsHeight, width / 2, endsHeight), // bottom left + rect(width / 2, height - endsHeight, width / 2, endsHeight), // bottom right + ]; + const iconPositions = [ + { x: width / 4, y: endsHeight / 2 }, // top left + { x: (3 * width) / 4, y: endsHeight / 2 }, // top right + { x: width / 4, y: height - (5 * endsHeight) / 2 }, // middle 1 left + { x: (3 * width) / 4, y: height - (5 * endsHeight) / 2 }, // middle 1 right + { x: width / 4, y: height - (3 * endsHeight) / 2 }, // middle 2 left + { x: (3 * width) / 4, y: height - (3 * endsHeight) / 2 }, // middle 2 right + { x: width / 4, y: height - endsHeight / 2 }, // bottom left + { x: (3 * width) / 4, y: height - endsHeight / 2 }, // bottom right + ]; + return [paths, iconPositions]; } /** * Gets the icon corresponding to a button in a button pad. - * - * @param buttonPadButton + * + * @param buttonPadButton * @returns icon source */ export function getIcon(buttonPadButton: ButtonPadButton) { - switch (buttonPadButton) { - case (ButtonPadButton.BaseForward): - return driveForward; - case (ButtonPadButton.BaseReverse): - return driveReverse; - case (ButtonPadButton.BaseRotateRight): - return driveRight; - case (ButtonPadButton.BaseRotateLeft): - return driveLeft; - case (ButtonPadButton.ArmLift): - return armUp; - case (ButtonPadButton.ArmLower): - return armDown; - case (ButtonPadButton.ArmExtend): - return armExtend; - case (ButtonPadButton.ArmRetract): - return armRetract; - case (ButtonPadButton.GripperOpen): - return gripOpen; - case (ButtonPadButton.GripperClose): - return gripClose; - case (ButtonPadButton.WristRollLeft): - return rollLeft - case (ButtonPadButton.WristRollRight): - return rollRight - case (ButtonPadButton.WristPitchUp): - return pitchUp - case (ButtonPadButton.WristPitchDown): - return pitchDown - case (ButtonPadButton.WristRotateIn): - return yawLeft; - case (ButtonPadButton.WristRotateOut): - return yawRight - case (ButtonPadButton.CameraPanLeft): - return panLeft - case (ButtonPadButton.CameraPanRight): - return panRight - case (ButtonPadButton.CameraTiltUp): - return tiltUp - case (ButtonPadButton.CameraTiltDown): - return tiltDown - default: - console.warn(`cannot get icon for ${buttonPadButton}`); - return null; - } -} \ No newline at end of file + switch (buttonPadButton) { + case ButtonPadButton.BaseForward: + return driveForward; + case ButtonPadButton.BaseReverse: + return driveReverse; + case ButtonPadButton.BaseRotateRight: + return driveRight; + case ButtonPadButton.BaseRotateLeft: + return driveLeft; + case ButtonPadButton.ArmLift: + return armUp; + case ButtonPadButton.ArmLower: + return armDown; + case ButtonPadButton.ArmExtend: + return armExtend; + case ButtonPadButton.ArmRetract: + return armRetract; + case ButtonPadButton.GripperOpen: + return gripOpen; + case ButtonPadButton.GripperClose: + return gripClose; + case ButtonPadButton.WristRollLeft: + return rollLeft; + case ButtonPadButton.WristRollRight: + return rollRight; + case ButtonPadButton.WristPitchUp: + return pitchUp; + case ButtonPadButton.WristPitchDown: + return pitchDown; + case ButtonPadButton.WristRotateIn: + return yawLeft; + case ButtonPadButton.WristRotateOut: + return yawRight; + case ButtonPadButton.CameraPanLeft: + return panLeft; + case ButtonPadButton.CameraPanRight: + return panRight; + case ButtonPadButton.CameraTiltUp: + return tiltUp; + case ButtonPadButton.CameraTiltDown: + return tiltDown; + default: + console.warn(`cannot get icon for ${buttonPadButton}`); + return null; + } +} diff --git a/src/pages/robot/css/index.css b/src/pages/robot/css/index.css index 0d15a340..ddd403c0 100644 --- a/src/pages/robot/css/index.css +++ b/src/pages/robot/css/index.css @@ -1,6 +1,6 @@ body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", + "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; @@ -15,13 +15,13 @@ body { } code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; } #root { margin: 0; - height: 100% + height: 100%; } .imageViewer { @@ -35,4 +35,4 @@ code { display: flex; justify-content: space-between; clear: both; -} \ No newline at end of file +} diff --git a/src/pages/robot/html/index.html b/src/pages/robot/html/index.html index 9ccd8a38..7552307e 100644 --- a/src/pages/robot/html/index.html +++ b/src/pages/robot/html/index.html @@ -1,10 +1,10 @@ - + - - + + Robot - - + +
    - - \ No newline at end of file + + diff --git a/src/pages/robot/tsx/index.tsx b/src/pages/robot/tsx/index.tsx index 3a0c623d..99e5c3ff 100644 --- a/src/pages/robot/tsx/index.tsx +++ b/src/pages/robot/tsx/index.tsx @@ -1,234 +1,270 @@ -import React from 'react'; -import { createRoot } from 'react-dom/client'; -import 'robot/css/index.css'; -import { Robot } from '../../robot/tsx/robot' -import { WebRTCConnection } from '../../../shared/webrtcconnections' -import { navigationProps, realsenseProps, gripperProps, WebRTCMessage, ValidJointStateDict, ValidJointStateMessage, IsRunStoppedMessage, RobotPose, ROSOccupancyGrid, OccupancyGridMessage, MapPoseMessage, GoalStatusMessage, MoveBaseState, MoveBaseStateMessage, ROSBatteryState, BatteryVoltageMessage } from 'shared/util' -import { AllVideoStreamComponent, VideoStream } from './videostreams'; -import ROSLIB from 'roslib'; -import { HasBetaTeleopKitMessage } from '../../../shared/util'; - -export const robot = new Robot({ - jointStateCallback: forwardJointStates, - batteryStateCallback: forwardBatteryState, - occupancyGridCallback: forwardOccupancyGrid, - moveBaseResultCallback: forwardMoveBaseState, - amclPoseCallback: forwardAMCLPose, - isRunStoppedCallback: forwardIsRunStopped, - hasBetaTeleopKitCallback: forwardHasBetaTeleopKit -}) +import React from "react"; +import { createRoot } from "react-dom/client"; +import "robot/css/index.css"; +import { Robot } from "../../robot/tsx/robot"; +import { WebRTCConnection } from "../../../shared/webrtcconnections"; +import { + navigationProps, + realsenseProps, + gripperProps, + WebRTCMessage, + ValidJointStateDict, + ValidJointStateMessage, + IsRunStoppedMessage, + RobotPose, + ROSOccupancyGrid, + OccupancyGridMessage, + MapPoseMessage, + GoalStatusMessage, + MoveBaseState, + MoveBaseStateMessage, + ROSBatteryState, + BatteryVoltageMessage, +} from "shared/util"; +import { AllVideoStreamComponent, VideoStream } from "./videostreams"; +import ROSLIB from "roslib"; +import { HasBetaTeleopKitMessage } from "../../../shared/util"; + +export const robot = new Robot({ + jointStateCallback: forwardJointStates, + batteryStateCallback: forwardBatteryState, + occupancyGridCallback: forwardOccupancyGrid, + moveBaseResultCallback: forwardMoveBaseState, + amclPoseCallback: forwardAMCLPose, + isRunStoppedCallback: forwardIsRunStopped, + hasBetaTeleopKitCallback: forwardHasBetaTeleopKit, +}); export let connection: WebRTCConnection; export let navigationStream = new VideoStream(navigationProps); -export let realsenseStream = new VideoStream(realsenseProps) +export let realsenseStream = new VideoStream(realsenseProps); export let gripperStream = new VideoStream(gripperProps); // let occupancyGrid: ROSOccupancyGrid | undefined; connection = new WebRTCConnection({ - peerRole: 'robot', - polite: false, - onRobotConnectionStart: handleSessionStart, - onMessage: handleMessage, - onConnectionEnd: disconnectFromRobot -}) + peerRole: "robot", + polite: false, + onRobotConnectionStart: handleSessionStart, + onMessage: handleMessage, + onConnectionEnd: disconnectFromRobot, +}); robot.connect().then(() => { - robot.subscribeToVideo({ - topicName: "/navigation_camera/image_raw/rotated/compressed", - callback: navigationStream.updateImage - }) - navigationStream.start() - - robot.subscribeToVideo({ - topicName: "/camera/color/image_raw/rotated/compressed", - callback: realsenseStream.updateImage - }) - realsenseStream.start() - - robot.subscribeToVideo({ - topicName: "/gripper_camera/image_raw/cropped/compressed", - callback: gripperStream.updateImage - }) - gripperStream.start() - - robot.getOccupancyGrid() - robot.getJointLimits() - - connection.joinRobotRoom() -}) + robot.subscribeToVideo({ + topicName: "/navigation_camera/image_raw/rotated/compressed", + callback: navigationStream.updateImage, + }); + navigationStream.start(); + + robot.subscribeToVideo({ + topicName: "/camera/color/image_raw/rotated/compressed", + callback: realsenseStream.updateImage, + }); + realsenseStream.start(); + + robot.subscribeToVideo({ + topicName: "/gripper_camera/image_raw/cropped/compressed", + callback: gripperStream.updateImage, + }); + gripperStream.start(); + + robot.getOccupancyGrid(); + robot.getJointLimits(); + + connection.joinRobotRoom(); +}); function handleSessionStart() { - connection.removeTracks() + connection.removeTracks(); - console.log('adding local media stream to peer connection'); + console.log("adding local media stream to peer connection"); - let stream: MediaStream = navigationStream.outputVideoStream!; - stream.getTracks().forEach(track => connection.addTrack(track, stream, "overhead")) + let stream: MediaStream = navigationStream.outputVideoStream!; + stream + .getTracks() + .forEach((track) => connection.addTrack(track, stream, "overhead")); - stream = realsenseStream.outputVideoStream!; - stream.getTracks().forEach(track => connection.addTrack(track, stream, "realsense")) + stream = realsenseStream.outputVideoStream!; + stream + .getTracks() + .forEach((track) => connection.addTrack(track, stream, "realsense")); - stream = gripperStream.outputVideoStream!; - stream.getTracks().forEach(track => connection.addTrack(track, stream, "gripper")) + stream = gripperStream.outputVideoStream!; + stream + .getTracks() + .forEach((track) => connection.addTrack(track, stream, "gripper")); - connection.openDataChannels() + connection.openDataChannels(); } function forwardMoveBaseState(state: MoveBaseState) { - if (!connection) throw 'WebRTC connection undefined!' - - if (state.alert_type != "info") { - connection.sendData({ - type: "goalStatus", - message: state - } as GoalStatusMessage) - } + if (!connection) throw "WebRTC connection undefined!"; + if (state.alert_type != "info") { connection.sendData({ - type: "moveBaseState", - message: state - } as MoveBaseStateMessage) + type: "goalStatus", + message: state, + } as GoalStatusMessage); + } + + connection.sendData({ + type: "moveBaseState", + message: state, + } as MoveBaseStateMessage); } function forwardIsRunStopped(isRunStopped: boolean) { - if (!connection) throw 'WebRTC connection undefined!' + if (!connection) throw "WebRTC connection undefined!"; - connection.sendData({ - type: "isRunStopped", - enabled: isRunStopped, - } as IsRunStoppedMessage); + connection.sendData({ + type: "isRunStopped", + enabled: isRunStopped, + } as IsRunStoppedMessage); } function forwardHasBetaTeleopKit(value: boolean) { - if (!connection) throw 'WebRTC connection undefined!' + if (!connection) throw "WebRTC connection undefined!"; - connection.sendData({ - type: "hasBetaTeleopKit", - value: value, - } as HasBetaTeleopKitMessage); + connection.sendData({ + type: "hasBetaTeleopKit", + value: value, + } as HasBetaTeleopKitMessage); } -function forwardJointStates(robotPose: RobotPose, jointValues: ValidJointStateDict, effortValues: ValidJointStateDict) { - if (!connection) throw 'WebRTC connection undefined!' - - connection.sendData({ - type: "validJointState", - robotPose: robotPose, - jointsInLimits: jointValues, - jointsInCollision: effortValues - } as ValidJointStateMessage); +function forwardJointStates( + robotPose: RobotPose, + jointValues: ValidJointStateDict, + effortValues: ValidJointStateDict, +) { + if (!connection) throw "WebRTC connection undefined!"; + + connection.sendData({ + type: "validJointState", + robotPose: robotPose, + jointsInLimits: jointValues, + jointsInCollision: effortValues, + } as ValidJointStateMessage); } function forwardBatteryState(batteryState: ROSBatteryState) { - if (!connection) throw 'WebRTC connection undefined' + if (!connection) throw "WebRTC connection undefined"; - connection.sendData({ - type: 'batteryVoltage', - message: batteryState.voltage - } as BatteryVoltageMessage); + connection.sendData({ + type: "batteryVoltage", + message: batteryState.voltage, + } as BatteryVoltageMessage); } function forwardOccupancyGrid(occupancyGrid: ROSOccupancyGrid) { - if (!connection) throw 'WebRTC connection undefined' - - let splitOccupancyGrid: ROSOccupancyGrid = { - header: occupancyGrid.header, - info: occupancyGrid.info, - data: [] - } - - const data_size = 50000 - for (let i = 0; i < occupancyGrid.data.length; i += data_size) { - const data_chunk = occupancyGrid.data.slice(i, i + data_size); - splitOccupancyGrid.data = data_chunk - connection.sendData({ - type: 'occupancyGrid', - message: splitOccupancyGrid - } as OccupancyGridMessage); - } - - // occupancyGrid.data = occupancyGrid.data.slice(0, 70000) - // console.log('forwarding', occupancyGrid) - // connection.sendData({ - // type: 'occupancyGrid', - // message: occupancyGrid - // } as OccupancyGridMessage); + if (!connection) throw "WebRTC connection undefined"; + + let splitOccupancyGrid: ROSOccupancyGrid = { + header: occupancyGrid.header, + info: occupancyGrid.info, + data: [], + }; + + const data_size = 50000; + for (let i = 0; i < occupancyGrid.data.length; i += data_size) { + const data_chunk = occupancyGrid.data.slice(i, i + data_size); + splitOccupancyGrid.data = data_chunk; + connection.sendData({ + type: "occupancyGrid", + message: splitOccupancyGrid, + } as OccupancyGridMessage); + } + + // occupancyGrid.data = occupancyGrid.data.slice(0, 70000) + // console.log('forwarding', occupancyGrid) + // connection.sendData({ + // type: 'occupancyGrid', + // message: occupancyGrid + // } as OccupancyGridMessage); } function forwardAMCLPose(transform: ROSLIB.Transform) { - if (!connection) throw 'WebRTC connection undefined' + if (!connection) throw "WebRTC connection undefined"; - connection.sendData({ - type: 'amclPose', - message: transform - } as MapPoseMessage) + connection.sendData({ + type: "amclPose", + message: transform, + } as MapPoseMessage); } function handleMessage(message: WebRTCMessage) { - if (!("type" in message)) { - console.error("Malformed message:", message) - return - } - - switch (message.type) { - case "driveBase": - robot.executeBaseVelocity(message.modifier) - break; - case "incrementalMove": - robot.executeIncrementalMove(message.jointName, message.increment) - break - case "stopTrajectory": - robot.stopTrajectoryClient() - break - case "stopMoveBase": - robot.stopMoveBaseClient() - break - case "setRobotMode": - message.modifier == "navigation" ? robot.switchToNavigationMode() : robot.switchToPositionMode() - break - case "setCameraPerspective": - robot.setCameraPerspective({ camera: message.camera, perspective: message.perspective }) - break - case "setRobotPose": - robot.executePoseGoal(message.pose) - break - case "playbackPoses": - robot.executePoseGoals(message.poses, 0) - break - case "moveBase": - robot.executeMoveBaseGoal(message.pose) - break - case "setFollowGripper": - robot.setPanTiltFollowGripper(message.toggle) - break - case "setDepthSensing": - robot.setDepthSensing(message.toggle) - break - case "setRunStop": - robot.setRunStop(message.toggle) - break - case "lookAtGripper": - robot.lookAtGripper(0, 0) - break - case "getOccupancyGrid": - robot.getOccupancyGrid() - break - case "getHasBetaTeleopKit": - robot.getHasBetaTeleopKit() - } -}; + if (!("type" in message)) { + console.error("Malformed message:", message); + return; + } + + switch (message.type) { + case "driveBase": + robot.executeBaseVelocity(message.modifier); + break; + case "incrementalMove": + robot.executeIncrementalMove(message.jointName, message.increment); + break; + case "stopTrajectory": + robot.stopTrajectoryClient(); + break; + case "stopMoveBase": + robot.stopMoveBaseClient(); + break; + case "setRobotMode": + message.modifier == "navigation" + ? robot.switchToNavigationMode() + : robot.switchToPositionMode(); + break; + case "setCameraPerspective": + robot.setCameraPerspective({ + camera: message.camera, + perspective: message.perspective, + }); + break; + case "setRobotPose": + robot.executePoseGoal(message.pose); + break; + case "playbackPoses": + robot.executePoseGoals(message.poses, 0); + break; + case "moveBase": + robot.executeMoveBaseGoal(message.pose); + break; + case "setFollowGripper": + robot.setPanTiltFollowGripper(message.toggle); + break; + case "setDepthSensing": + robot.setDepthSensing(message.toggle); + break; + case "setRunStop": + robot.setRunStop(message.toggle); + break; + case "lookAtGripper": + robot.lookAtGripper(0, 0); + break; + case "getOccupancyGrid": + robot.getOccupancyGrid(); + break; + case "getHasBetaTeleopKit": + robot.getHasBetaTeleopKit(); + } +} function disconnectFromRobot() { - robot.closeROSConnection() - connection.hangup() + robot.closeROSConnection(); + connection.hangup(); } window.onbeforeunload = () => { - robot.closeROSConnection() - connection.hangup() + robot.closeROSConnection(); + connection.hangup(); }; // New method of rendering in react 18 -const container = document.getElementById('root'); +const container = document.getElementById("root"); const root = createRoot(container!); // createRoot(container!) if you use TypeScript -root.render(); \ No newline at end of file +root.render( + , +); diff --git a/src/pages/robot/tsx/robot.tsx b/src/pages/robot/tsx/robot.tsx index 4c80e7ca..b5aed8e1 100644 --- a/src/pages/robot/tsx/robot.tsx +++ b/src/pages/robot/tsx/robot.tsx @@ -1,675 +1,747 @@ -import React from 'react' +import React from "react"; import ROSLIB from "roslib"; -import { ROSJointState, ROSCompressedImage, ValidJoints, VideoProps, ROSOccupancyGrid, ROSPose, MoveBaseState, NavigateToPoseActionResult, NavigateToPoseActionStatusList, ROSBatteryState } from 'shared/util'; -import { rosJointStatetoRobotPose, ValidJointStateDict, RobotPose, IsRunStoppedMessage } from '../../../shared/util'; - -export var robotMode: "navigation" | "position" = "position" +import { + ROSJointState, + ROSCompressedImage, + ValidJoints, + VideoProps, + ROSOccupancyGrid, + ROSPose, + MoveBaseState, + NavigateToPoseActionResult, + NavigateToPoseActionStatusList, + ROSBatteryState, +} from "shared/util"; +import { + rosJointStatetoRobotPose, + ValidJointStateDict, + RobotPose, + IsRunStoppedMessage, +} from "../../../shared/util"; + +export var robotMode: "navigation" | "position" = "position"; export var rosConnected = false; export class Robot extends React.Component { - private ros: ROSLIB.Ros; - private jointLimits: { [key in ValidJoints]?: [number, number] } = {} - private jointState?: ROSJointState; - private poseGoal?: ROSLIB.ActionGoal; - private poseGoalComplete?: boolean; - private isRunStopped?: boolean; - private moveBaseGoal?: ROSLIB.ActionGoal; - private trajectoryClient?: ROSLIB.ActionClient; - private moveBaseClient?: ROSLIB.ActionClient; - private cmdVelTopic?: ROSLIB.Topic; - private switchToNavigationService?: ROSLIB.Service; - private switchToPositionService?: ROSLIB.Service; - private setCameraPerspectiveService?: ROSLIB.Service; - private setDepthSensingService?: ROSLIB.Service; - private setRunStopService?: ROSLIB.Service; - private robotFrameTfClient?: ROSLIB.TFClient; - private mapFrameTfClient?: ROSLIB.TFClient; - private linkGripperFingerLeftTF?: ROSLIB.Transform - private linkHeadTiltTF?: ROSLIB.Transform - private jointStateCallback: (robotPose: RobotPose, jointValues: ValidJointStateDict, effortValues: ValidJointStateDict) => void - private batteryStateCallback: (batteryState: ROSBatteryState) => void - private occupancyGridCallback: (occupancyGrid: ROSOccupancyGrid) => void - private moveBaseResultCallback: (goalState: MoveBaseState) => void - private amclPoseCallback: (pose: ROSLIB.Transform) => void - private isRunStoppedCallback: (isRunStopped: boolean) => void - private hasBetaTeleopKitCallback: (value: boolean) => void - private lookAtGripperInterval?: number // ReturnType - private subscriptions: ROSLIB.Topic[] = [] - private hasBetaTeleopKitParam: ROSLIB.Param; - - constructor(props: { - jointStateCallback: (robotPose: RobotPose, jointValues: ValidJointStateDict, effortValues: ValidJointStateDict) => void, - batteryStateCallback: (batteryState: ROSBatteryState) => void, - occupancyGridCallback: (occupancyGrid: ROSOccupancyGrid) => void, - moveBaseResultCallback: (goalState: MoveBaseState) => void, - amclPoseCallback: (pose: ROSLIB.Transform) => void, - isRunStoppedCallback: (isRunStopped: boolean) => void, - hasBetaTeleopKitCallback: (value: boolean) => void - }) { - super(props); - this.jointStateCallback = props.jointStateCallback - this.batteryStateCallback = props.batteryStateCallback - this.occupancyGridCallback = props.occupancyGridCallback - this.moveBaseResultCallback = props.moveBaseResultCallback - this.amclPoseCallback = props.amclPoseCallback - this.isRunStoppedCallback = props.isRunStoppedCallback - this.hasBetaTeleopKitCallback = props.hasBetaTeleopKitCallback - } - - async connect(): Promise { - this.ros = new ROSLIB.Ros({ - // set this to false to use the new service interface to - // tf2_web_republisher. true is the default and means roslibjs - // will use the action interface - groovyCompatibility : false, - url: 'wss://localhost:9090' - }); - - return new Promise((resolve, reject) => { - this.ros.on('connection', async () => { - await this.onConnect(); - resolve() - }) - this.ros.on('error', (error) => { - reject(error) - }); - - this.ros.on('close', () => { - reject('Connection to websocket has been closed.') - }); - }); - } - - async onConnect() { - this.subscribeToJointState() - this.subscribeToJointLimits() - this.subscribeToBatteryState() - this.subscribeToMoveBaseResult() - this.subscribeToIsRunStopped() - this.createTrajectoryClient() - this.createMoveBaseClient() - this.createCmdVelTopic() - this.createSwitchToNavigationService() - this.createSwitchToPositionService() - this.createDepthSensingService() - this.createRunStopService() - this.createRobotFrameTFClient() - this.createMapFrameTFClient() - this.subscribeToGripperFingerTF() - this.subscribeToHeadTiltTF() - this.subscribeToMapTF() - - return Promise.resolve() - } - - closeROSConnection() { - this.subscriptions.forEach((topic) => { - topic.unsubscribe() - }) - this.ros.close() - } - - isROSConnected() { - return this.ros.isConnected - } - - subscribeToJointState() { - const jointStateTopic: ROSLIB.Topic = new ROSLIB.Topic({ - ros: this.ros, - name: '/stretch/joint_states', - messageType: 'sensor_msgs/msg/JointState' - }); - this.subscriptions.push(jointStateTopic) - - jointStateTopic.subscribe((msg: ROSJointState) => { - this.jointState = msg - let robotPose: RobotPose = rosJointStatetoRobotPose(this.jointState) - let jointValues: ValidJointStateDict = {} - let effortValues: ValidJointStateDict = {} - this.jointState.name.forEach((name?: ValidJoints) => { - let inLimits = this.inJointLimits(name); - let collision = this.inCollision(name); - if (inLimits) jointValues[name!] = inLimits; - if (collision) effortValues[name!] = collision; - }) - - if (this.jointStateCallback) this.jointStateCallback(robotPose, jointValues, effortValues) - }); - }; - - subscribeToJointLimits() { - const jointLimitsTopic: ROSLIB.Topic = new ROSLIB.Topic({ - ros: this.ros, - name: '/joint_limits', - messageType: 'sensor_msgs/msg/JointState' - }); - this.subscriptions.push(jointLimitsTopic) - - jointLimitsTopic.subscribe((msg: ROSJointState) => { - msg.name.forEach((name, idx) => { - if (name == "joint_arm") name = "wrist_extension" - this.jointLimits[name] = [msg.position[idx], msg.velocity[idx]] - }) - }); - }; - - subscribeToBatteryState() { - const batteryStateTopic: ROSLIB.Topic = new ROSLIB.Topic({ - ros: this.ros, - name: '/battery', - messageType: 'sensor_msgs/msg/BatteryState' - }); - this.subscriptions.push(batteryStateTopic) - - batteryStateTopic.subscribe((msg: ROSBatteryState) => { - if (this.batteryStateCallback) this.batteryStateCallback(msg) - }); - }; - - subscribeToVideo(props: VideoProps) { - let topic: ROSLIB.Topic = new ROSLIB.Topic({ - ros: this.ros, - name: props.topicName, - messageType: 'sensor_msgs/CompressedImage' - }); - topic.subscribe(props.callback) - this.subscriptions.push(topic) - } - - getHasBetaTeleopKit() { - this.hasBetaTeleopKitParam = new ROSLIB.Param({ - ros : this.ros, - name : '/configure_video_streams:has_beta_teleop_kit' - }); - - this.hasBetaTeleopKitParam.get((value: boolean) => { - console.log("has beta teleop kit: ", value) - if (this.hasBetaTeleopKitCallback) this.hasBetaTeleopKitCallback(value) - }) - } - - getOccupancyGrid() { - let getMapService = new ROSLIB.Service({ - ros: this.ros, - name: '/map_server/map', - serviceType: 'nav2_msgs/srv/GetMap' - }); - - var request = new ROSLIB.ServiceRequest({}) - getMapService?.callService(request, (response: {map: ROSOccupancyGrid}) => { - if (this.occupancyGridCallback) this.occupancyGridCallback(response.map) - }) - } - - getJointLimits() { - let getJointLimitsService = new ROSLIB.Service({ - ros: this.ros, - name: '/get_joint_states', - serviceType: 'std_srvs/Trigger' - }); - - var request = new ROSLIB.ServiceRequest({}); - getJointLimitsService.callService(request, () => {}); - } - - subscribeToMoveBaseResult() { - let topic: ROSLIB.Topic = new ROSLIB.Topic({ - ros: this.ros, - name: '/navigate_to_pose/_action/status', - messageType: 'action_msgs/msg/GoalStatusArray' - }); - this.subscriptions.push(topic) - - topic.subscribe((msg: NavigateToPoseActionStatusList) => { - let status = msg.status_list.pop()?.status - if (this.moveBaseResultCallback) { - if (status == 5) this.moveBaseResultCallback({state: "Navigation cancelled!", alert_type: "error"}) - else if (status == 4) this.moveBaseResultCallback({state: "Navigation succeeded!", alert_type: "success"}) - else if (status == 6) this.moveBaseResultCallback({state: "Navigation failed!", alert_type: "error"}) - } - }); - }; - - subscribeToIsRunStopped() { - let topic: ROSLIB.Topic = new ROSLIB.Topic({ - ros: this.ros, - name: 'is_runstopped', - messageType: 'std_msgs/msg/Bool' - }); - this.subscriptions.push(topic) - - topic.subscribe((msg) => { - if (this.isRunStoppedCallback) this.isRunStoppedCallback(msg.data) - }); - } - - createTrajectoryClient() { - this.trajectoryClient = new ROSLIB.ActionHandle({ - ros: this.ros, - name: '/stretch_controller/follow_joint_trajectory', - actionType: 'control_msgs/action/FollowJointTrajectory', - }); - } - - createMoveBaseClient() { - this.moveBaseClient = new ROSLIB.ActionHandle({ - ros: this.ros, - name: '/navigate_to_pose', - actionType: 'nav2_msgs/action/NavigateToPose', - // timeout: 100 - }); - } - - createCmdVelTopic() { - this.cmdVelTopic = new ROSLIB.Topic({ - ros: this.ros, - name: '/stretch/cmd_vel', - messageType: 'geometry_msgs/Twist' - }); - } - - createSwitchToNavigationService() { - this.switchToNavigationService = new ROSLIB.Service({ - ros: this.ros, - name: '/switch_to_navigation_mode', - serviceType: 'std_srvs/Trigger' - }); - } - - createSwitchToPositionService() { - this.switchToPositionService = new ROSLIB.Service({ - ros: this.ros, - name: '/switch_to_position_mode', - serviceType: 'std_srvs/Trigger' - }); - } - - createDepthSensingService() { - this.setDepthSensingService = new ROSLIB.Service({ - ros: this.ros, - name: '/depth_ar', - serviceType: 'std_srvs/srv/SetBool' - }) - } - - createRunStopService() { - this.setRunStopService = new ROSLIB.Service({ - ros: this.ros, - name: '/runstop', - serviceType: 'std_srvs/srv/SetBool' - }) - } - - createRobotFrameTFClient() { - this.robotFrameTfClient = new ROSLIB.TFClient({ - ros: this.ros, - fixedFrame: 'base_link', - angularThres: 0.001, - transThres: 0.001, - rate: 10 - }); - } - - createMapFrameTFClient() { - this.mapFrameTfClient = new ROSLIB.TFClient({ - ros: this.ros, - fixedFrame: 'map', - angularThres: 0.001, - transThres: 0.001, - rate: 10 - }); - } - - subscribeToGripperFingerTF() { - this.robotFrameTfClient?.subscribe('link_gripper_finger_left', transform => { - this.linkGripperFingerLeftTF = transform; - }); - } - - subscribeToHeadTiltTF () { - this.robotFrameTfClient?.subscribe('link_head_tilt', transform => { - this.linkHeadTiltTF = transform; - }) - } - - subscribeToMapTF() { - this.mapFrameTfClient?.subscribe('base_link', transform => { - if (this.amclPoseCallback) this.amclPoseCallback(transform) - }) - } - - setDepthSensing(toggle: boolean) { - var request = new ROSLIB.ServiceRequest({data: toggle}) - this.setDepthSensingService?.callService(request, (response: boolean) => { - response ? console.log("Enable depth sensing") : console.log("Disabled depth sensing") - }) - } - - setRunStop(toggle: boolean) { - var request = new ROSLIB.ServiceRequest({data: toggle}) - this.setRunStopService?.callService(request, (response: boolean) => {}) - } - - switchToNavigationMode() { - var request = new ROSLIB.ServiceRequest({}); - if (robotMode !== "navigation") { - this.switchToNavigationService!.callService(request, () => { - robotMode = "navigation" - console.log("Switched to navigation mode") - }); - } - } - - switchToPositionMode = () => { - var request = new ROSLIB.ServiceRequest({}); - if (robotMode !== "position") { - this.switchToPositionService!.callService(request, () => { - robotMode = "position" - console.log("Switched to position mode") - }); - } - } - - executeBaseVelocity = (props: {linVel: number, angVel: number}): void => { - this.switchToNavigationMode() - this.stopExecution() - let twist = new ROSLIB.Message({ - linear: { - x: props.linVel, - y: 0, - z: 0 + private ros: ROSLIB.Ros; + private jointLimits: { [key in ValidJoints]?: [number, number] } = {}; + private jointState?: ROSJointState; + private poseGoal?: ROSLIB.ActionGoal; + private poseGoalComplete?: boolean; + private isRunStopped?: boolean; + private moveBaseGoal?: ROSLIB.ActionGoal; + private trajectoryClient?: ROSLIB.ActionClient; + private moveBaseClient?: ROSLIB.ActionClient; + private cmdVelTopic?: ROSLIB.Topic; + private switchToNavigationService?: ROSLIB.Service; + private switchToPositionService?: ROSLIB.Service; + private setCameraPerspectiveService?: ROSLIB.Service; + private setDepthSensingService?: ROSLIB.Service; + private setRunStopService?: ROSLIB.Service; + private robotFrameTfClient?: ROSLIB.TFClient; + private mapFrameTfClient?: ROSLIB.TFClient; + private linkGripperFingerLeftTF?: ROSLIB.Transform; + private linkHeadTiltTF?: ROSLIB.Transform; + private jointStateCallback: ( + robotPose: RobotPose, + jointValues: ValidJointStateDict, + effortValues: ValidJointStateDict, + ) => void; + private batteryStateCallback: (batteryState: ROSBatteryState) => void; + private occupancyGridCallback: (occupancyGrid: ROSOccupancyGrid) => void; + private moveBaseResultCallback: (goalState: MoveBaseState) => void; + private amclPoseCallback: (pose: ROSLIB.Transform) => void; + private isRunStoppedCallback: (isRunStopped: boolean) => void; + private hasBetaTeleopKitCallback: (value: boolean) => void; + private lookAtGripperInterval?: number; // ReturnType + private subscriptions: ROSLIB.Topic[] = []; + private hasBetaTeleopKitParam: ROSLIB.Param; + + constructor(props: { + jointStateCallback: ( + robotPose: RobotPose, + jointValues: ValidJointStateDict, + effortValues: ValidJointStateDict, + ) => void; + batteryStateCallback: (batteryState: ROSBatteryState) => void; + occupancyGridCallback: (occupancyGrid: ROSOccupancyGrid) => void; + moveBaseResultCallback: (goalState: MoveBaseState) => void; + amclPoseCallback: (pose: ROSLIB.Transform) => void; + isRunStoppedCallback: (isRunStopped: boolean) => void; + hasBetaTeleopKitCallback: (value: boolean) => void; + }) { + super(props); + this.jointStateCallback = props.jointStateCallback; + this.batteryStateCallback = props.batteryStateCallback; + this.occupancyGridCallback = props.occupancyGridCallback; + this.moveBaseResultCallback = props.moveBaseResultCallback; + this.amclPoseCallback = props.amclPoseCallback; + this.isRunStoppedCallback = props.isRunStoppedCallback; + this.hasBetaTeleopKitCallback = props.hasBetaTeleopKitCallback; + } + + async connect(): Promise { + this.ros = new ROSLIB.Ros({ + // set this to false to use the new service interface to + // tf2_web_republisher. true is the default and means roslibjs + // will use the action interface + groovyCompatibility: false, + url: "wss://localhost:9090", + }); + + return new Promise((resolve, reject) => { + this.ros.on("connection", async () => { + await this.onConnect(); + resolve(); + }); + this.ros.on("error", (error) => { + reject(error); + }); + + this.ros.on("close", () => { + reject("Connection to websocket has been closed."); + }); + }); + } + + async onConnect() { + this.subscribeToJointState(); + this.subscribeToJointLimits(); + this.subscribeToBatteryState(); + this.subscribeToMoveBaseResult(); + this.subscribeToIsRunStopped(); + this.createTrajectoryClient(); + this.createMoveBaseClient(); + this.createCmdVelTopic(); + this.createSwitchToNavigationService(); + this.createSwitchToPositionService(); + this.createDepthSensingService(); + this.createRunStopService(); + this.createRobotFrameTFClient(); + this.createMapFrameTFClient(); + this.subscribeToGripperFingerTF(); + this.subscribeToHeadTiltTF(); + this.subscribeToMapTF(); + + return Promise.resolve(); + } + + closeROSConnection() { + this.subscriptions.forEach((topic) => { + topic.unsubscribe(); + }); + this.ros.close(); + } + + isROSConnected() { + return this.ros.isConnected; + } + + subscribeToJointState() { + const jointStateTopic: ROSLIB.Topic = new ROSLIB.Topic({ + ros: this.ros, + name: "/stretch/joint_states", + messageType: "sensor_msgs/msg/JointState", + }); + this.subscriptions.push(jointStateTopic); + + jointStateTopic.subscribe((msg: ROSJointState) => { + this.jointState = msg; + let robotPose: RobotPose = rosJointStatetoRobotPose(this.jointState); + let jointValues: ValidJointStateDict = {}; + let effortValues: ValidJointStateDict = {}; + this.jointState.name.forEach((name?: ValidJoints) => { + let inLimits = this.inJointLimits(name); + let collision = this.inCollision(name); + if (inLimits) jointValues[name!] = inLimits; + if (collision) effortValues[name!] = collision; + }); + + if (this.jointStateCallback) + this.jointStateCallback(robotPose, jointValues, effortValues); + }); + } + + subscribeToJointLimits() { + const jointLimitsTopic: ROSLIB.Topic = new ROSLIB.Topic({ + ros: this.ros, + name: "/joint_limits", + messageType: "sensor_msgs/msg/JointState", + }); + this.subscriptions.push(jointLimitsTopic); + + jointLimitsTopic.subscribe((msg: ROSJointState) => { + msg.name.forEach((name, idx) => { + if (name == "joint_arm") name = "wrist_extension"; + this.jointLimits[name] = [msg.position[idx], msg.velocity[idx]]; + }); + }); + } + + subscribeToBatteryState() { + const batteryStateTopic: ROSLIB.Topic = new ROSLIB.Topic({ + ros: this.ros, + name: "/battery", + messageType: "sensor_msgs/msg/BatteryState", + }); + this.subscriptions.push(batteryStateTopic); + + batteryStateTopic.subscribe((msg: ROSBatteryState) => { + if (this.batteryStateCallback) this.batteryStateCallback(msg); + }); + } + + subscribeToVideo(props: VideoProps) { + let topic: ROSLIB.Topic = new ROSLIB.Topic({ + ros: this.ros, + name: props.topicName, + messageType: "sensor_msgs/CompressedImage", + }); + topic.subscribe(props.callback); + this.subscriptions.push(topic); + } + + getHasBetaTeleopKit() { + this.hasBetaTeleopKitParam = new ROSLIB.Param({ + ros: this.ros, + name: "/configure_video_streams:has_beta_teleop_kit", + }); + + this.hasBetaTeleopKitParam.get((value: boolean) => { + console.log("has beta teleop kit: ", value); + if (this.hasBetaTeleopKitCallback) this.hasBetaTeleopKitCallback(value); + }); + } + + getOccupancyGrid() { + let getMapService = new ROSLIB.Service({ + ros: this.ros, + name: "/map_server/map", + serviceType: "nav2_msgs/srv/GetMap", + }); + + var request = new ROSLIB.ServiceRequest({}); + getMapService?.callService( + request, + (response: { map: ROSOccupancyGrid }) => { + if (this.occupancyGridCallback) + this.occupancyGridCallback(response.map); + }, + ); + } + + getJointLimits() { + let getJointLimitsService = new ROSLIB.Service({ + ros: this.ros, + name: "/get_joint_states", + serviceType: "std_srvs/Trigger", + }); + + var request = new ROSLIB.ServiceRequest({}); + getJointLimitsService.callService(request, () => {}); + } + + subscribeToMoveBaseResult() { + let topic: ROSLIB.Topic = new ROSLIB.Topic({ + ros: this.ros, + name: "/navigate_to_pose/_action/status", + messageType: "action_msgs/msg/GoalStatusArray", + }); + this.subscriptions.push(topic); + + topic.subscribe((msg: NavigateToPoseActionStatusList) => { + let status = msg.status_list.pop()?.status; + if (this.moveBaseResultCallback) { + if (status == 5) + this.moveBaseResultCallback({ + state: "Navigation cancelled!", + alert_type: "error", + }); + else if (status == 4) + this.moveBaseResultCallback({ + state: "Navigation succeeded!", + alert_type: "success", + }); + else if (status == 6) + this.moveBaseResultCallback({ + state: "Navigation failed!", + alert_type: "error", + }); + } + }); + } + + subscribeToIsRunStopped() { + let topic: ROSLIB.Topic = new ROSLIB.Topic({ + ros: this.ros, + name: "is_runstopped", + messageType: "std_msgs/msg/Bool", + }); + this.subscriptions.push(topic); + + topic.subscribe((msg) => { + if (this.isRunStoppedCallback) this.isRunStoppedCallback(msg.data); + }); + } + + createTrajectoryClient() { + this.trajectoryClient = new ROSLIB.ActionHandle({ + ros: this.ros, + name: "/stretch_controller/follow_joint_trajectory", + actionType: "control_msgs/action/FollowJointTrajectory", + }); + } + + createMoveBaseClient() { + this.moveBaseClient = new ROSLIB.ActionHandle({ + ros: this.ros, + name: "/navigate_to_pose", + actionType: "nav2_msgs/action/NavigateToPose", + // timeout: 100 + }); + } + + createCmdVelTopic() { + this.cmdVelTopic = new ROSLIB.Topic({ + ros: this.ros, + name: "/stretch/cmd_vel", + messageType: "geometry_msgs/Twist", + }); + } + + createSwitchToNavigationService() { + this.switchToNavigationService = new ROSLIB.Service({ + ros: this.ros, + name: "/switch_to_navigation_mode", + serviceType: "std_srvs/Trigger", + }); + } + + createSwitchToPositionService() { + this.switchToPositionService = new ROSLIB.Service({ + ros: this.ros, + name: "/switch_to_position_mode", + serviceType: "std_srvs/Trigger", + }); + } + + createDepthSensingService() { + this.setDepthSensingService = new ROSLIB.Service({ + ros: this.ros, + name: "/depth_ar", + serviceType: "std_srvs/srv/SetBool", + }); + } + + createRunStopService() { + this.setRunStopService = new ROSLIB.Service({ + ros: this.ros, + name: "/runstop", + serviceType: "std_srvs/srv/SetBool", + }); + } + + createRobotFrameTFClient() { + this.robotFrameTfClient = new ROSLIB.TFClient({ + ros: this.ros, + fixedFrame: "base_link", + angularThres: 0.001, + transThres: 0.001, + rate: 10, + }); + } + + createMapFrameTFClient() { + this.mapFrameTfClient = new ROSLIB.TFClient({ + ros: this.ros, + fixedFrame: "map", + angularThres: 0.001, + transThres: 0.001, + rate: 10, + }); + } + + subscribeToGripperFingerTF() { + this.robotFrameTfClient?.subscribe( + "link_gripper_finger_left", + (transform) => { + this.linkGripperFingerLeftTF = transform; + }, + ); + } + + subscribeToHeadTiltTF() { + this.robotFrameTfClient?.subscribe("link_head_tilt", (transform) => { + this.linkHeadTiltTF = transform; + }); + } + + subscribeToMapTF() { + this.mapFrameTfClient?.subscribe("base_link", (transform) => { + if (this.amclPoseCallback) this.amclPoseCallback(transform); + }); + } + + setDepthSensing(toggle: boolean) { + var request = new ROSLIB.ServiceRequest({ data: toggle }); + this.setDepthSensingService?.callService(request, (response: boolean) => { + response + ? console.log("Enable depth sensing") + : console.log("Disabled depth sensing"); + }); + } + + setRunStop(toggle: boolean) { + var request = new ROSLIB.ServiceRequest({ data: toggle }); + this.setRunStopService?.callService(request, (response: boolean) => {}); + } + + switchToNavigationMode() { + var request = new ROSLIB.ServiceRequest({}); + if (robotMode !== "navigation") { + this.switchToNavigationService!.callService(request, () => { + robotMode = "navigation"; + console.log("Switched to navigation mode"); + }); + } + } + + switchToPositionMode = () => { + var request = new ROSLIB.ServiceRequest({}); + if (robotMode !== "position") { + this.switchToPositionService!.callService(request, () => { + robotMode = "position"; + console.log("Switched to position mode"); + }); + } + }; + + executeBaseVelocity = (props: { linVel: number; angVel: number }): void => { + this.switchToNavigationMode(); + this.stopExecution(); + let twist = new ROSLIB.Message({ + linear: { + x: props.linVel, + y: 0, + z: 0, + }, + angular: { + x: 0, + y: 0, + z: props.angVel, + }, + }); + if (!this.cmdVelTopic) throw "trajectoryClient is undefined"; + this.cmdVelTopic.publish(twist); + }; + + makeIncrementalMoveGoal( + jointName: ValidJoints, + jointValueInc: number, + ): ROSLIB.Goal | undefined { + if (!this.jointState) throw "jointState is undefined"; + let newJointValue = this.getJointValue(jointName); + // Paper over Hello's fake joints + if ( + jointName === "translate_mobile_base" || + jointName === "rotate_mobile_base" + ) { + // These imaginary joints are floating, always have 0 as their reference + newJointValue = 0; + } + + let collision = this.inCollision({ + jointStateMessage: this.jointState, + jointName: jointName, + }); + let collisionIndex = jointValueInc <= 0 ? 0 : 1; + if (jointName === "joint_wrist_yaw") { + collisionIndex = jointValueInc <= 0 ? 1 : 0; + } + // Negative joint increment is for lower/retract/wrist out + // Positive joint increment is for lift/extend/wrist in + let index = jointValueInc <= 0 ? 0 : 1; + // If request to move the joint in the direction of collision, cancel movement + if (collision[collisionIndex]) return; + + newJointValue = newJointValue + jointValueInc; + + // Make sure new joint value is within limits + if (jointName in this.jointLimits) { + let inLimits = this.inJointLimitsHelper(newJointValue, jointName); + if (!inLimits) throw "invalid joint name"; + // console.log(newJointValue, this.jointLimits[jointName]![index], inLimits[index]) + if (!inLimits[index]) newJointValue = this.jointLimits[jointName]![index]; + } + + let pose = { [jointName]: newJointValue }; + if (!this.trajectoryClient) throw "trajectoryClient is undefined"; + return this.makePoseGoal(pose); + } + + makeMoveBaseGoal(pose: ROSPose) { + if (!this.moveBaseClient) throw "moveBaseClient is undefined"; + + let newGoal = new ROSLIB.ActionGoal({ + pose: { + header: { + frame_id: "map", + }, + pose: pose, + }, + }); + + return newGoal; + } + + makePoseGoal(pose: RobotPose) { + let jointNames: ValidJoints[] = []; + let jointPositions: number[] = []; + for (let key in pose) { + jointNames.push(key as ValidJoints); + jointPositions.push(pose[key as ValidJoints]!); + } + + console.log(this.trajectoryClient); + + if (!this.trajectoryClient) throw "trajectoryClient is undefined"; + let newGoal = new ROSLIB.ActionGoal({ + trajectory: { + header: { + stamp: { + secs: 0, + nsecs: 0, + }, + }, + joint_names: jointNames, + points: [ + { + positions: jointPositions, + // The following might causing the jumpiness in continuous motions + time_from_start: { + secs: 1, + nsecs: 0, }, - angular: { - x: 0, - y: 0, - z: props.angVel - } - }); - if (!this.cmdVelTopic) throw 'trajectoryClient is undefined'; - this.cmdVelTopic.publish(twist) - } - - makeIncrementalMoveGoal(jointName: ValidJoints, jointValueInc: number): ROSLIB.Goal | undefined { - if (!this.jointState) throw 'jointState is undefined'; - let newJointValue = this.getJointValue(jointName) - // Paper over Hello's fake joints - if (jointName === "translate_mobile_base" || jointName === "rotate_mobile_base") { - // These imaginary joints are floating, always have 0 as their reference - newJointValue = 0 - } - - let collision = this.inCollision({ jointStateMessage: this.jointState, jointName: jointName }) - let collisionIndex = jointValueInc <= 0 ? 0 : 1 - if (jointName === "joint_wrist_yaw") { - collisionIndex = jointValueInc <= 0 ? 1 : 0 - } - // Negative joint increment is for lower/retract/wrist out - // Positive joint increment is for lift/extend/wrist in - let index = jointValueInc <= 0 ? 0 : 1 - // If request to move the joint in the direction of collision, cancel movement - if (collision[collisionIndex]) return; - - newJointValue = newJointValue + jointValueInc - - // Make sure new joint value is within limits - if (jointName in this.jointLimits) { - let inLimits = this.inJointLimitsHelper(newJointValue, jointName) - if (!inLimits) throw 'invalid joint name' - // console.log(newJointValue, this.jointLimits[jointName]![index], inLimits[index]) - if (!inLimits[index]) newJointValue = this.jointLimits[jointName]![index] + }, + ], + }, + }); + + return newGoal; + } + + makePoseGoals(poses: RobotPose[]) { + let jointNames: ValidJoints[] = []; + for (let key in poses[0]) { + jointNames.push(key as ValidJoints); + } + + let points: any = []; + let jointPositions: number[] = []; + poses.forEach((pose, index) => { + jointPositions = []; + for (let key in pose) { + jointPositions.push(pose[key as ValidJoints]!); + } + points.push({ + positions: jointPositions, + time_from_start: { + secs: 10, + nsecs: 0, + }, + }); + }); + + if (!this.trajectoryClient) throw "trajectoryClient is undefined"; + let newGoal = new ROSLIB.ActionGoal({ + trajectory: { + header: { + stamp: { + secs: 0, + nsecs: 0, + }, + }, + joint_names: jointNames, + points: points, + }, + }); + + return newGoal; + } + + executePoseGoal(pose: RobotPose) { + this.switchToPositionMode(); + this.poseGoal = this.makePoseGoal(pose); + this.trajectoryClient.createClient(this.poseGoal); + } + + async executePoseGoals(poses: RobotPose[], index: number) { + this.switchToPositionMode(); + this.poseGoal = this.makePoseGoals(poses); + this.trajectoryClient.createClient(this.poseGoal); + } + + executeMoveBaseGoal(pose: ROSPose) { + this.switchToNavigationMode(); + // this.stopExecution() + this.moveBaseGoal = this.makeMoveBaseGoal(pose); + this.moveBaseClient.createClient(this.moveBaseGoal); + // this.moveBaseResultCallback({state: "Navigating to selected goal...", alert_type: "info"}) + // this.moveBaseGoal.send() + } + + executeIncrementalMove(jointName: ValidJoints, increment: number) { + this.switchToPositionMode(); + this.poseGoal = this.makeIncrementalMoveGoal(jointName, increment); + this.trajectoryClient.createClient(this.poseGoal); + } + + stopExecution() { + this.stopTrajectoryClient(); + this.stopMoveBaseClient(); + } + + stopTrajectoryClient() { + if (!this.trajectoryClient) throw "trajectoryClient is undefined"; + if (this.poseGoal) { + this.trajectoryClient.cancelGoal(); + // this.poseGoal.cancel() + this.poseGoal = undefined; + } + } + + stopMoveBaseClient() { + if (!this.moveBaseClient) throw "moveBaseClient is undefined"; + if (this.moveBaseGoal) { + this.moveBaseClient.cancelGoal(); + // this.moveBaseGoal.cancel() + this.moveBaseGoal = undefined; + } + } + + setPanTiltFollowGripper(followGripper: boolean) { + if (this.lookAtGripperInterval && followGripper) return; + + if (followGripper) { + let panOffset = 0; + let tiltOffset = 0; + let lookIfReadyAndRepeat = () => { + if (this.linkGripperFingerLeftTF && this.linkHeadTiltTF) { + this.lookAtGripper(panOffset, tiltOffset); } + this.lookAtGripperInterval = window.setTimeout( + lookIfReadyAndRepeat, + 500, + ); + }; + lookIfReadyAndRepeat(); + } else { + this.stopExecution(); + clearTimeout(this.lookAtGripperInterval); + this.lookAtGripperInterval = undefined; + } + } + + lookAtGripper(panOffset: number, tiltOffset: number) { + if (!this.linkGripperFingerLeftTF) + throw "linkGripperFingerLeftTF is undefined"; + if (!this.linkHeadTiltTF) throw "linkHeadTiltTF is undefined"; + let posDifference = { + x: + this.linkGripperFingerLeftTF.translation.x - + this.linkHeadTiltTF.translation.x, + y: + this.linkGripperFingerLeftTF.translation.y - + this.linkHeadTiltTF.translation.y, + z: + this.linkGripperFingerLeftTF.translation.z - + this.linkHeadTiltTF.translation.z, + }; - let pose = { [jointName]: newJointValue } - if (!this.trajectoryClient) throw 'trajectoryClient is undefined'; - return this.makePoseGoal(pose) - } - - makeMoveBaseGoal(pose: ROSPose) { - if (!this.moveBaseClient) throw 'moveBaseClient is undefined'; - - let newGoal = new ROSLIB.ActionGoal({ - pose: { - header: { - frame_id: 'map' - }, - pose: pose - } - }) - - return newGoal - } - - makePoseGoal(pose: RobotPose) { - let jointNames: ValidJoints[] = [] - let jointPositions: number[] = [] - for (let key in pose) { - jointNames.push(key as ValidJoints) - jointPositions.push(pose[key as ValidJoints]!) - } - - console.log(this.trajectoryClient) - - if (!this.trajectoryClient) throw 'trajectoryClient is undefined'; - let newGoal = new ROSLIB.ActionGoal({ - trajectory: { - header: { - - stamp: { - secs: 0, - nsecs: 0 - } - }, - joint_names: jointNames, - points: [ - { - positions: jointPositions, - // The following might causing the jumpiness in continuous motions - time_from_start: { - secs: 1, - nsecs: 0 - } - - } - ] - } - }); - - return newGoal - } - - makePoseGoals(poses: RobotPose[]) { - let jointNames: ValidJoints[] = [] - for (let key in poses[0]) { - jointNames.push(key as ValidJoints) - } - - let points: any = [] - let jointPositions: number[] = [] - poses.forEach((pose, index) => { - jointPositions = [] - for (let key in pose) { - jointPositions.push(pose[key as ValidJoints]!) - } - points.push({ - positions: jointPositions, - time_from_start: { - secs: 10, - nsecs: 0 - } - }) - }); - - if (!this.trajectoryClient) throw 'trajectoryClient is undefined'; - let newGoal = new ROSLIB.ActionGoal({ - trajectory: { - header: { - stamp: { - secs: 0, - nsecs: 0 - } - }, - joint_names: jointNames, - points: points - } - }); - - return newGoal - } - - executePoseGoal(pose: RobotPose) { - this.switchToPositionMode() - this.poseGoal = this.makePoseGoal(pose) - this.trajectoryClient.createClient(this.poseGoal) - } - - async executePoseGoals(poses: RobotPose[], index: number) { - this.switchToPositionMode() - this.poseGoal = this.makePoseGoals(poses) - this.trajectoryClient.createClient(this.poseGoal) - } - - executeMoveBaseGoal(pose: ROSPose) { - this.switchToNavigationMode() - // this.stopExecution() - this.moveBaseGoal = this.makeMoveBaseGoal(pose) - this.moveBaseClient.createClient(this.moveBaseGoal) - // this.moveBaseResultCallback({state: "Navigating to selected goal...", alert_type: "info"}) - // this.moveBaseGoal.send() - } - - executeIncrementalMove(jointName: ValidJoints, increment: number) { - this.switchToPositionMode() - this.poseGoal = this.makeIncrementalMoveGoal(jointName, increment) - this.trajectoryClient.createClient(this.poseGoal) - } - - stopExecution() { - this.stopTrajectoryClient() - this.stopMoveBaseClient() - } - - stopTrajectoryClient() { - if (!this.trajectoryClient) throw 'trajectoryClient is undefined'; - if (this.poseGoal) { - this.trajectoryClient.cancelGoal() - // this.poseGoal.cancel() - this.poseGoal = undefined - } - } - - stopMoveBaseClient() { - if (!this.moveBaseClient) throw 'moveBaseClient is undefined'; - if (this.moveBaseGoal) { - this.moveBaseClient.cancelGoal() - // this.moveBaseGoal.cancel() - this.moveBaseGoal = undefined - } - } - - setPanTiltFollowGripper(followGripper: boolean) { - if (this.lookAtGripperInterval && followGripper) return; - - if (followGripper) { - let panOffset = 0; - let tiltOffset = 0; - let lookIfReadyAndRepeat = () => { - if (this.linkGripperFingerLeftTF && this.linkHeadTiltTF) { - this.lookAtGripper(panOffset, tiltOffset); - } - this.lookAtGripperInterval = window.setTimeout(lookIfReadyAndRepeat, 500) - } - lookIfReadyAndRepeat() - } else { - this.stopExecution() - clearTimeout(this.lookAtGripperInterval) - this.lookAtGripperInterval = undefined - } - } - - lookAtGripper(panOffset: number, tiltOffset: number) { - if (!this.linkGripperFingerLeftTF) throw 'linkGripperFingerLeftTF is undefined'; - if (!this.linkHeadTiltTF) throw 'linkHeadTiltTF is undefined'; - let posDifference = { - x: this.linkGripperFingerLeftTF.translation.x - this.linkHeadTiltTF.translation.x, - y: this.linkGripperFingerLeftTF.translation.y - this.linkHeadTiltTF.translation.y, - z: this.linkGripperFingerLeftTF.translation.z - this.linkHeadTiltTF.translation.z - }; - - // Normalize posDifference - const scalar = Math.sqrt(posDifference.x ** 2 + posDifference.y ** 2 + posDifference.z ** 2); - posDifference.x /= scalar; - posDifference.y /= scalar; - posDifference.z /= scalar; - - const pan = Math.atan2(posDifference.y, posDifference.x) + panOffset; - const tilt = Math.atan2(posDifference.z, -posDifference.y) + tiltOffset; - - // Goals really close to current state cause some whiplash in these joints in simulation. - // Ignoring small goals is a temporary fix - if (!this.jointState) throw 'jointState is undefined'; - let panDiff = Math.abs(this.getJointValue("joint_head_pan") - pan); - let tiltDiff = Math.abs(this.getJointValue("joint_head_tilt") - tilt); - if (panDiff < 0.02 && tiltDiff < 0.02) { - return - } - - this.executePoseGoal({ - 'joint_head_pan': pan + panOffset, - 'joint_head_tilt': tilt + tiltOffset - }) - } - - getJointValue(jointName: ValidJoints): number { - // Paper over Hello's fake joint implementation - if (jointName === "joint_arm" || jointName === "wrist_extension") { - return this.getJointValue("joint_arm_l0") + - this.getJointValue("joint_arm_l1") + - this.getJointValue("joint_arm_l2") + - this.getJointValue("joint_arm_l3"); - } else if (jointName === "translate_mobile_base" || jointName === "rotate_mobile_base") { - return 0; - } - - let jointIndex = this.jointState.name.indexOf(jointName) - return this.jointState.position[jointIndex] - } + // Normalize posDifference + const scalar = Math.sqrt( + posDifference.x ** 2 + posDifference.y ** 2 + posDifference.z ** 2, + ); + posDifference.x /= scalar; + posDifference.y /= scalar; + posDifference.z /= scalar; + + const pan = Math.atan2(posDifference.y, posDifference.x) + panOffset; + const tilt = Math.atan2(posDifference.z, -posDifference.y) + tiltOffset; + + // Goals really close to current state cause some whiplash in these joints in simulation. + // Ignoring small goals is a temporary fix + if (!this.jointState) throw "jointState is undefined"; + let panDiff = Math.abs(this.getJointValue("joint_head_pan") - pan); + let tiltDiff = Math.abs(this.getJointValue("joint_head_tilt") - tilt); + if (panDiff < 0.02 && tiltDiff < 0.02) { + return; + } + + this.executePoseGoal({ + joint_head_pan: pan + panOffset, + joint_head_tilt: tilt + tiltOffset, + }); + } + + getJointValue(jointName: ValidJoints): number { + // Paper over Hello's fake joint implementation + if (jointName === "joint_arm" || jointName === "wrist_extension") { + return ( + this.getJointValue("joint_arm_l0") + + this.getJointValue("joint_arm_l1") + + this.getJointValue("joint_arm_l2") + + this.getJointValue("joint_arm_l3") + ); + } else if ( + jointName === "translate_mobile_base" || + jointName === "rotate_mobile_base" + ) { + return 0; + } + + let jointIndex = this.jointState.name.indexOf(jointName); + return this.jointState.position[jointIndex]; + } + + inJointLimits(jointName: ValidJoints) { + let jointValue = this.getJointValue(jointName); + return this.inJointLimitsHelper(jointValue, jointName); + } + + inJointLimitsHelper(jointValue: number, jointName: ValidJoints) { + let jointLimits = this.jointLimits[jointName]; + if (!jointLimits) return; + + var eps = 0.03; + let inLimits: [boolean, boolean] = [true, true]; + inLimits[0] = jointValue - eps >= jointLimits[0]; // Lower joint limit + inLimits[1] = jointValue + eps <= jointLimits[1]; // Upper joint limit + return inLimits; + } + + inCollision(jointName: ValidJoints) { + let inCollision: [boolean, boolean] = [false, false]; + const MAX_EFFORTS: { [key in ValidJoints]?: [number, number] } = { + joint_head_tilt: [-50, 50], + joint_head_pan: [-50, 50], + wrist_extension: [-40, 40], + joint_lift: [0, 70], + // "joint_wrist_yaw": [-10, 10], + // "joint_wrist_pitch": [-10, 10], + // "joint_wrist_roll": [-10, 10], + }; - inJointLimits(jointName: ValidJoints) { - let jointValue = this.getJointValue(jointName) - return this.inJointLimitsHelper(jointValue, jointName) - } + if (!(jointName in MAX_EFFORTS)) return inCollision; - inJointLimitsHelper(jointValue: number, jointName: ValidJoints) { - let jointLimits = this.jointLimits[jointName] - if (!jointLimits) return; - - var eps = 0.03 - let inLimits: [boolean, boolean] = [true, true] - inLimits[0] = jointValue - eps >= jointLimits[0] // Lower joint limit - inLimits[1] = jointValue + eps <= jointLimits[1] // Upper joint limit - return inLimits - } + let jointIndex = this.jointState.name.indexOf(jointName); + // In collision if joint is applying more than 50% effort when moving downward/inward/backward + inCollision[0] = + this.jointState.effort[jointIndex] < MAX_EFFORTS[jointName]![0]; + // In collision if joint is applying more than 50% effort when moving upward/outward/forward + inCollision[1] = + this.jointState.effort[jointIndex] > MAX_EFFORTS[jointName]![1]; - inCollision(jointName: ValidJoints) { - let inCollision: [boolean, boolean] = [false, false] - const MAX_EFFORTS: { [key in ValidJoints]?: [number, number] } = { - "joint_head_tilt": [-50, 50], - "joint_head_pan": [-50, 50], - "wrist_extension": [-40, 40], - "joint_lift": [0, 70], - // "joint_wrist_yaw": [-10, 10], - // "joint_wrist_pitch": [-10, 10], - // "joint_wrist_roll": [-10, 10], - } - - if (!(jointName in MAX_EFFORTS)) return inCollision; - - let jointIndex = this.jointState.name.indexOf(jointName) - // In collision if joint is applying more than 50% effort when moving downward/inward/backward - inCollision[0] = this.jointState.effort[jointIndex] < MAX_EFFORTS[jointName]![0] - // In collision if joint is applying more than 50% effort when moving upward/outward/forward - inCollision[1] = this.jointState.effort[jointIndex] > MAX_EFFORTS[jointName]![1] - - return inCollision - } -} \ No newline at end of file + return inCollision; + } +} diff --git a/src/pages/robot/tsx/videostreams.tsx b/src/pages/robot/tsx/videostreams.tsx index fc0910c8..72355c93 100644 --- a/src/pages/robot/tsx/videostreams.tsx +++ b/src/pages/robot/tsx/videostreams.tsx @@ -1,106 +1,109 @@ import React from "react"; import { ROSCompressedImage } from "shared/util"; -var jpeg = require('jpeg-js'); +var jpeg = require("jpeg-js"); type VideoStreamProps = { - width: number, - height: number, - scale: number, - fps: number -} + width: number; + height: number; + scale: number; + fps: number; +}; export class VideoStream extends React.Component { - canvas = React.createRef(); - img: HTMLImageElement; - video: HTMLVideoElement; - width: number; - height: number; - scale: number; - fps: number; - className?: string; - outputVideoStream?: MediaStream - aspectRatio: any; + canvas = React.createRef(); + img: HTMLImageElement; + video: HTMLVideoElement; + width: number; + height: number; + scale: number; + fps: number; + className?: string; + outputVideoStream?: MediaStream; + aspectRatio: any; - constructor(props: VideoStreamProps) { - super(props); - this.width = props.width; - this.height = props.height; - this.scale = props.scale; - this.fps = props.fps; - this.img = document.createElement("img"); - this.video = document.createElement("video"); - this.video.style.display = "block"; - this.video.setAttribute("width", this.width.toString()); - this.video.setAttribute("height", this.height.toString()); - this.outputVideoStream = new MediaStream(); + constructor(props: VideoStreamProps) { + super(props); + this.width = props.width; + this.height = props.height; + this.scale = props.scale; + this.fps = props.fps; + this.img = document.createElement("img"); + this.video = document.createElement("video"); + this.video.style.display = "block"; + this.video.setAttribute("width", this.width.toString()); + this.video.setAttribute("height", this.height.toString()); + this.outputVideoStream = new MediaStream(); - this.updateImage = this.updateImage.bind(this); - } + this.updateImage = this.updateImage.bind(this); + } - get imageReceived() { - return this.img.src !== ""; - } + get imageReceived() { + return this.img.src !== ""; + } - renderVideo() { - if (!this.imageReceived) { - return; - } - this.canvas.current?.getContext('2d')?.drawImage(this.img, 0, 0, this.width, this.height) + renderVideo() { + if (!this.imageReceived) { + return; } + this.canvas.current + ?.getContext("2d") + ?.drawImage(this.img, 0, 0, this.width, this.height); + } - updateImage(message: ROSCompressedImage) { - if (!this.imageReceived) { - let {width, height, data} = jpeg.decode(Uint8Array.from(atob(message.data), c => c.charCodeAt(0)), true) - this.aspectRatio = width / height - this.height = Math.max(height * this.aspectRatio, 1000) - this.width = this.height * this.aspectRatio; - this.canvas.current!.width = this.width - this.canvas.current!.height = this.height - } - this.img.src = 'data:image/jpg;base64,' + message.data; + updateImage(message: ROSCompressedImage) { + if (!this.imageReceived) { + let { width, height, data } = jpeg.decode( + Uint8Array.from(atob(message.data), (c) => c.charCodeAt(0)), + true, + ); + this.aspectRatio = width / height; + this.height = Math.max(height * this.aspectRatio, 1000); + this.width = this.height * this.aspectRatio; + this.canvas.current!.width = this.width; + this.canvas.current!.height = this.height; } + this.img.src = "data:image/jpg;base64," + message.data; + } - drawVideo() { - this.renderVideo(); - requestAnimationFrame(this.drawVideo.bind(this)); - } + drawVideo() { + this.renderVideo(); + requestAnimationFrame(this.drawVideo.bind(this)); + } - start() { - if (!this.canvas.current) throw 'Video stream canvas null' - this.outputVideoStream = this.canvas.current.captureStream(this.fps); - this.video.srcObject = this.outputVideoStream; - this.drawVideo(); - } + start() { + if (!this.canvas.current) throw "Video stream canvas null"; + this.outputVideoStream = this.canvas.current.captureStream(this.fps); + this.video.srcObject = this.outputVideoStream; + this.drawVideo(); + } - render() { - return ( - - ) - } + render() { + return ( + + ); + } } - /** Renders all three video streams side by side */ export const AllVideoStreamComponent = (props: { streams: VideoStream[] }) => { - console.log(props.streams) - // let buttonPads = Bp.ExampleButtonPads; - // let buttonPads = [undefined, undefined, undefined]; - // Replace the overhead button pad with predictive display - // buttonPads[0] = console.log(`Length: ${len}, Angle: ${ang}`)} />; - const widths = ["30%", "22.5%", "45%"]; - return ( -
    - {props.streams.map((stream, i) => ( -
    - {stream.render()} -
    - ) - )} + console.log(props.streams); + // let buttonPads = Bp.ExampleButtonPads; + // let buttonPads = [undefined, undefined, undefined]; + // Replace the overhead button pad with predictive display + // buttonPads[0] = console.log(`Length: ${len}, Angle: ${ang}`)} />; + const widths = ["30%", "22.5%", "45%"]; + return ( +
    + {props.streams.map((stream, i) => ( +
    + {stream.render()}
    - ); -}; \ No newline at end of file + ))} +
    + ); +}; diff --git a/src/shared/commands.tsx b/src/shared/commands.tsx index 5186ea04..055e8880 100644 --- a/src/shared/commands.tsx +++ b/src/shared/commands.tsx @@ -1,79 +1,93 @@ -import ROSLIB from "roslib" -import { ROSPose, RobotPose } from "./util" -import { ValidJoints } from "./util" - -export type cmd = DriveCommand | IncrementalMove | setRobotModeCommand | CameraPerspectiveCommand | RobotPoseCommand | ToggleCommand | LookAtGripper | GetOccupancyGrid | MoveBaseCommand | StopTrajectoryCommand | StopMoveBaseCommand | PlaybackPosesCommand | GetBatteryVoltageCommand | GetHasBetaTeleopKit +import ROSLIB from "roslib"; +import { ROSPose, RobotPose } from "./util"; +import { ValidJoints } from "./util"; + +export type cmd = + | DriveCommand + | IncrementalMove + | setRobotModeCommand + | CameraPerspectiveCommand + | RobotPoseCommand + | ToggleCommand + | LookAtGripper + | GetOccupancyGrid + | MoveBaseCommand + | StopTrajectoryCommand + | StopMoveBaseCommand + | PlaybackPosesCommand + | GetBatteryVoltageCommand + | GetHasBetaTeleopKit; export interface VelocityCommand { - stop: () => void, - affirm?: () => void + stop: () => void; + affirm?: () => void; } export interface DriveCommand { - type: "driveBase", - modifier: { - linVel: number, - angVel: number - } + type: "driveBase"; + modifier: { + linVel: number; + angVel: number; + }; } export interface IncrementalMove { - type: "incrementalMove" - jointName: ValidJoints, - increment: number + type: "incrementalMove"; + jointName: ValidJoints; + increment: number; } export interface RobotPoseCommand { - type: "setRobotPose" - pose: RobotPose + type: "setRobotPose"; + pose: RobotPose; } export interface PlaybackPosesCommand { - type: "playbackPoses" - poses: RobotPose[] + type: "playbackPoses"; + poses: RobotPose[]; } export interface setRobotModeCommand { - type: "setRobotMode", - modifier: "position" | "navigation" + type: "setRobotMode"; + modifier: "position" | "navigation"; } export interface CameraPerspectiveCommand { - type: "setCameraPerspective" - camera: "overhead" | "realsense" | "gripper" - perspective: string + type: "setCameraPerspective"; + camera: "overhead" | "realsense" | "gripper"; + perspective: string; } export interface ToggleCommand { - type: "setFollowGripper" | "setDepthSensing" | "setRunStop" - toggle: boolean + type: "setFollowGripper" | "setDepthSensing" | "setRunStop"; + toggle: boolean; } export interface LookAtGripper { - type: "lookAtGripper" + type: "lookAtGripper"; } export interface GetOccupancyGrid { - type: "getOccupancyGrid" + type: "getOccupancyGrid"; } export interface GetHasBetaTeleopKit { - type: "getHasBetaTeleopKit" + type: "getHasBetaTeleopKit"; } export interface MoveBaseCommand { - type: "moveBase" - pose: ROSPose + type: "moveBase"; + pose: ROSPose; } export interface StopTrajectoryCommand { - type: "stopTrajectory" + type: "stopTrajectory"; } export interface StopMoveBaseCommand { - type: "stopMoveBase" + type: "stopMoveBase"; } export interface GetBatteryVoltageCommand { - type: "getBatteryVoltage" -} \ No newline at end of file + type: "getBatteryVoltage"; +} diff --git a/src/shared/remoterobot.tsx b/src/shared/remoterobot.tsx index 5c4572b2..10db27a8 100644 --- a/src/shared/remoterobot.tsx +++ b/src/shared/remoterobot.tsx @@ -1,303 +1,354 @@ -import React from 'react' -import ROSLIB from 'roslib'; -import { cmd, DriveCommand, CameraPerspectiveCommand, IncrementalMove, setRobotModeCommand, VelocityCommand, RobotPoseCommand, ToggleCommand, LookAtGripper, GetOccupancyGrid, MoveBaseCommand, PlaybackPosesCommand } from 'shared/commands'; -import { ValidJointStateDict, RobotPose, ValidJoints, ROSPose, waitUntil } from 'shared/util'; -import { GetHasBetaTeleopKit } from './commands'; +import React from "react"; +import ROSLIB from "roslib"; +import { + cmd, + DriveCommand, + CameraPerspectiveCommand, + IncrementalMove, + setRobotModeCommand, + VelocityCommand, + RobotPoseCommand, + ToggleCommand, + LookAtGripper, + GetOccupancyGrid, + MoveBaseCommand, + PlaybackPosesCommand, +} from "shared/commands"; +import { + ValidJointStateDict, + RobotPose, + ValidJoints, + ROSPose, + waitUntil, +} from "shared/util"; +import { GetHasBetaTeleopKit } from "./commands"; export type robotMessageChannel = (message: cmd) => void; -export class RemoteRobot extends React.Component<{},any> { - robotChannel: robotMessageChannel; - sensors: RobotSensors - isRunStopped: boolean - batteryVoltage: number - mapPose: ROSLIB.Transform; - moveBaseGoalReached: boolean; - moveBaseState?: string - - constructor(props: { robotChannel: robotMessageChannel }) { - super(props); - this.robotChannel = props.robotChannel - this.sensors = new RobotSensors({}) - this.isRunStopped = false - this.batteryVoltage = 13.0 - this.mapPose = { - translation: { - x: 0, y: 0, z: 0 - } as ROSLIB.Vector3, - rotation: { - x: 0, y: 0, z: 0, w: 0 - } as ROSLIB.Quaternion - } as ROSLIB.Transform - this.moveBaseGoalReached = false - } - - setGoalReached(reached: boolean) { - this.moveBaseGoalReached = reached - } - - isGoalReached() { - return this.moveBaseGoalReached - } - - driveBase(linVel: number, angVel: number): VelocityCommand { - let cmd: DriveCommand = { - type: "driveBase", - modifier: { linVel: linVel, angVel: angVel } +export class RemoteRobot extends React.Component<{}, any> { + robotChannel: robotMessageChannel; + sensors: RobotSensors; + isRunStopped: boolean; + batteryVoltage: number; + mapPose: ROSLIB.Transform; + moveBaseGoalReached: boolean; + moveBaseState?: string; + + constructor(props: { robotChannel: robotMessageChannel }) { + super(props); + this.robotChannel = props.robotChannel; + this.sensors = new RobotSensors({}); + this.isRunStopped = false; + this.batteryVoltage = 13.0; + this.mapPose = { + translation: { + x: 0, + y: 0, + z: 0, + } as ROSLIB.Vector3, + rotation: { + x: 0, + y: 0, + z: 0, + w: 0, + } as ROSLIB.Quaternion, + } as ROSLIB.Transform; + this.moveBaseGoalReached = false; + } + + setGoalReached(reached: boolean) { + this.moveBaseGoalReached = reached; + } + + isGoalReached() { + return this.moveBaseGoalReached; + } + + driveBase(linVel: number, angVel: number): VelocityCommand { + let cmd: DriveCommand = { + type: "driveBase", + modifier: { linVel: linVel, angVel: angVel }, + }; + this.robotChannel(cmd); + + return { + stop: () => { + let stopEvent: DriveCommand = { + type: "driveBase", + modifier: { linVel: 0, angVel: 0 }, }; - this.robotChannel(cmd); - - return { - "stop": () => { - let stopEvent: DriveCommand = { - type: "driveBase", - modifier: { linVel: 0, angVel: 0 } - } - this.robotChannel(stopEvent) - }, - "affirm": () => { - let affirmEvent: DriveCommand = { - type: "driveBase", - modifier: { linVel: linVel, angVel: angVel } - } - this.robotChannel(affirmEvent) - } - } - } - - incrementalMove(jointName: ValidJoints, increment: number): VelocityCommand { - let cmd: IncrementalMove = { - type: "incrementalMove", - jointName: jointName, - increment: increment - }; - this.robotChannel(cmd); - - return { - "stop": () => { - this.robotChannel({ type: "stopTrajectory" }) - } - } - } - - setRobotMode(mode: "position" | "navigation") { - let cmd: setRobotModeCommand = { - type: "setRobotMode", - modifier: mode + this.robotChannel(stopEvent); + }, + affirm: () => { + let affirmEvent: DriveCommand = { + type: "driveBase", + modifier: { linVel: linVel, angVel: angVel }, }; - this.robotChannel(cmd) - } - - setCameraPerspective(camera: "overhead" | "realsense" | "gripper", perspective: string) { - let cmd: CameraPerspectiveCommand = { - type: "setCameraPerspective", - camera: camera, - perspective: perspective - } - this.robotChannel(cmd) - } - - setRobotPose(pose: RobotPose) { - let cmd: RobotPoseCommand = { - type: "setRobotPose", - pose: pose, - } - this.robotChannel(cmd) - } - - playbackPoses(poses: RobotPose[]) { - let cmd: PlaybackPosesCommand = { - type: "playbackPoses", - poses: poses - } - this.robotChannel(cmd) - } - - moveBase(pose: ROSPose) { - let cmd: MoveBaseCommand = { - type: "moveBase", - pose: pose - } - this.robotChannel(cmd) - } - - setToggle(type: "setFollowGripper" | "setDepthSensing" | "setRunStop", toggle: boolean) { - let cmd: ToggleCommand = { - type: type, - toggle: toggle - } - this.robotChannel(cmd) - } - - lookAtGripper(type: "lookAtGripper") { - let cmd: LookAtGripper = { - type: type - } - this.robotChannel(cmd) - } - - getOccupancyGrid(type: "getOccupancyGrid") { - let cmd: GetOccupancyGrid = { - type: type - } - this.robotChannel(cmd) - } - - getHasBetaTeleopKit(type: "getHasBetaTeleopKit") { - let cmd: GetHasBetaTeleopKit = { - type: type - } - this.robotChannel(cmd) - } - - setMapPose(pose: ROSLIB.Transform) { - this.mapPose = pose - } - - getMapPose() { - return this.mapPose - } - - stopTrajectory() { - this.robotChannel({ type: "stopTrajectory" }) - } - - stopMoveBase() { - this.robotChannel({ type: "stopMoveBase" }) - } + this.robotChannel(affirmEvent); + }, + }; + } + + incrementalMove(jointName: ValidJoints, increment: number): VelocityCommand { + let cmd: IncrementalMove = { + type: "incrementalMove", + jointName: jointName, + increment: increment, + }; + this.robotChannel(cmd); + + return { + stop: () => { + this.robotChannel({ type: "stopTrajectory" }); + }, + }; + } + + setRobotMode(mode: "position" | "navigation") { + let cmd: setRobotModeCommand = { + type: "setRobotMode", + modifier: mode, + }; + this.robotChannel(cmd); + } + + setCameraPerspective( + camera: "overhead" | "realsense" | "gripper", + perspective: string, + ) { + let cmd: CameraPerspectiveCommand = { + type: "setCameraPerspective", + camera: camera, + perspective: perspective, + }; + this.robotChannel(cmd); + } + + setRobotPose(pose: RobotPose) { + let cmd: RobotPoseCommand = { + type: "setRobotPose", + pose: pose, + }; + this.robotChannel(cmd); + } + + playbackPoses(poses: RobotPose[]) { + let cmd: PlaybackPosesCommand = { + type: "playbackPoses", + poses: poses, + }; + this.robotChannel(cmd); + } + + moveBase(pose: ROSPose) { + let cmd: MoveBaseCommand = { + type: "moveBase", + pose: pose, + }; + this.robotChannel(cmd); + } + + setToggle( + type: "setFollowGripper" | "setDepthSensing" | "setRunStop", + toggle: boolean, + ) { + let cmd: ToggleCommand = { + type: type, + toggle: toggle, + }; + this.robotChannel(cmd); + } + + lookAtGripper(type: "lookAtGripper") { + let cmd: LookAtGripper = { + type: type, + }; + this.robotChannel(cmd); + } + + getOccupancyGrid(type: "getOccupancyGrid") { + let cmd: GetOccupancyGrid = { + type: type, + }; + this.robotChannel(cmd); + } + + getHasBetaTeleopKit(type: "getHasBetaTeleopKit") { + let cmd: GetHasBetaTeleopKit = { + type: type, + }; + this.robotChannel(cmd); + } + + setMapPose(pose: ROSLIB.Transform) { + this.mapPose = pose; + } + + getMapPose() { + return this.mapPose; + } + + stopTrajectory() { + this.robotChannel({ type: "stopTrajectory" }); + } + + stopMoveBase() { + this.robotChannel({ type: "stopMoveBase" }); + } } class RobotSensors extends React.Component { - private batteryVoltage: number = 0.0; - private robotPose: RobotPose = {}; - private inJointLimits: ValidJointStateDict = {}; - private inCollision: ValidJointStateDict = {}; - private runStopEnabled: boolean = false; - private functionProviderCallback?: (inJointLimits: ValidJointStateDict, inCollision: ValidJointStateDict) => void; - private batteryFunctionProviderCallback?: (voltage: number) => void; - private runStopFunctionProviderCallback?: (enabled: boolean) => void; - - constructor(props: {}) { - super(props) - this.functionProviderCallback = () => { } - this.batteryFunctionProviderCallback = () => { } - this.runStopFunctionProviderCallback = () => { } - this.setFunctionProviderCallback = this.setFunctionProviderCallback.bind(this) - this.setBatteryFunctionProviderCallback = this.setBatteryFunctionProviderCallback.bind(this) - this.setRunStopFunctionProviderCallback = this.setRunStopFunctionProviderCallback.bind(this) + private batteryVoltage: number = 0.0; + private robotPose: RobotPose = {}; + private inJointLimits: ValidJointStateDict = {}; + private inCollision: ValidJointStateDict = {}; + private runStopEnabled: boolean = false; + private functionProviderCallback?: ( + inJointLimits: ValidJointStateDict, + inCollision: ValidJointStateDict, + ) => void; + private batteryFunctionProviderCallback?: (voltage: number) => void; + private runStopFunctionProviderCallback?: (enabled: boolean) => void; + + constructor(props: {}) { + super(props); + this.functionProviderCallback = () => {}; + this.batteryFunctionProviderCallback = () => {}; + this.runStopFunctionProviderCallback = () => {}; + this.setFunctionProviderCallback = + this.setFunctionProviderCallback.bind(this); + this.setBatteryFunctionProviderCallback = + this.setBatteryFunctionProviderCallback.bind(this); + this.setRunStopFunctionProviderCallback = + this.setRunStopFunctionProviderCallback.bind(this); + } + + /** + * Handler for joint state messages with information about if individual + * joints are in collision or at their limit. + * + * @param jointValues mapping of joint name to a pair of booleans for + * [joint is within lower limit, joint is within upper limit] + * @param effortValues mapping for joint name to pair of booleans for + * [joint in collision at lower end, joint is in + * collision at upper end] + */ + checkValidJointState( + robotPose: RobotPose, + jointValues: ValidJointStateDict, + effortValues: ValidJointStateDict, + ) { + if (robotPose !== this.robotPose) { + this.robotPose = robotPose; } - /** - * Handler for joint state messages with information about if individual - * joints are in collision or at their limit. - * - * @param jointValues mapping of joint name to a pair of booleans for - * [joint is within lower limit, joint is within upper limit] - * @param effortValues mapping for joint name to pair of booleans for - * [joint in collision at lower end, joint is in - * collision at upper end] - */ - checkValidJointState(robotPose: RobotPose, jointValues: ValidJointStateDict, effortValues: ValidJointStateDict) { - if (robotPose !== this.robotPose) { - this.robotPose = robotPose; - } - - // Remove existing values from list - let change = false; - Object.keys(jointValues).forEach((k) => { - const key = k as ValidJoints; - - const same = key in this.inJointLimits ? - jointValues[key]![0] == this.inJointLimits[key]![0] && - jointValues[key]![1] == this.inJointLimits[key]![1] : false; - // If same value, remove from dict so not passed to callback - if (same) delete jointValues[key]; - else { - change = true; - this.inJointLimits[key] = jointValues[key]; - } - }) - Object.keys(effortValues).forEach((k) => { - const key = k as ValidJoints; - const same = key in this.inCollision ? - effortValues[key]![0] == this.inCollision[key]![0] && - effortValues[key]![1] == this.inCollision[key]![1] : false; - // If same value, remove from dict so not passed to callback - if (same) delete effortValues[key]; - else { - change = true; - this.inCollision[key] = effortValues[key]; - } - }) - - // Only callback when value has changed - if (change && this.functionProviderCallback) { - this.functionProviderCallback(jointValues, effortValues); - } + // Remove existing values from list + let change = false; + Object.keys(jointValues).forEach((k) => { + const key = k as ValidJoints; + + const same = + key in this.inJointLimits + ? jointValues[key]![0] == this.inJointLimits[key]![0] && + jointValues[key]![1] == this.inJointLimits[key]![1] + : false; + // If same value, remove from dict so not passed to callback + if (same) delete jointValues[key]; + else { + change = true; + this.inJointLimits[key] = jointValues[key]; + } + }); + Object.keys(effortValues).forEach((k) => { + const key = k as ValidJoints; + const same = + key in this.inCollision + ? effortValues[key]![0] == this.inCollision[key]![0] && + effortValues[key]![1] == this.inCollision[key]![1] + : false; + // If same value, remove from dict so not passed to callback + if (same) delete effortValues[key]; + else { + change = true; + this.inCollision[key] = effortValues[key]; + } + }); + + // Only callback when value has changed + if (change && this.functionProviderCallback) { + this.functionProviderCallback(jointValues, effortValues); } - - /** - * Records a callback from the function provider. The callback is called - * whenever a joint state "at limit" or "in collision" changes. - * - * @param callback callback to function provider - */ - setFunctionProviderCallback(callback: (inJointLimits: ValidJointStateDict, inCollision: ValidJointStateDict) => void) { - this.functionProviderCallback = callback; + } + + /** + * Records a callback from the function provider. The callback is called + * whenever a joint state "at limit" or "in collision" changes. + * + * @param callback callback to function provider + */ + setFunctionProviderCallback( + callback: ( + inJointLimits: ValidJointStateDict, + inCollision: ValidJointStateDict, + ) => void, + ) { + this.functionProviderCallback = callback; + } + + setBatteryVoltage(voltage: number) { + let change = Math.abs(this.batteryVoltage - voltage) > 0.01; + if (change && this.batteryFunctionProviderCallback) { + this.batteryVoltage = voltage; + this.batteryFunctionProviderCallback(this.batteryVoltage); } - - setBatteryVoltage(voltage: number) { - let change = Math.abs(this.batteryVoltage - voltage) > 0.01 - if (change && this.batteryFunctionProviderCallback) { - this.batteryVoltage = voltage - this.batteryFunctionProviderCallback(this.batteryVoltage) - } + } + + setRunStopState(enabled: boolean) { + this.runStopEnabled = enabled; + if (this.runStopFunctionProviderCallback) + this.runStopFunctionProviderCallback(this.runStopEnabled); + } + + /** + * Records a callback from the function provider. The callback is called + * whenever the battery voltage changes. + * + * @param callback callback to function provider + */ + setBatteryFunctionProviderCallback(callback: (voltage: number) => void) { + this.batteryFunctionProviderCallback = callback; + } + + /** + * Records a callback from the function provider. The callback is called + * whenever the battery voltage changes. + * + * @param callback callback to function provider + */ + setRunStopFunctionProviderCallback(callback: (enabled: boolean) => void) { + this.runStopFunctionProviderCallback = callback; + } + + /** + * @returns current robot pose + */ + getRobotPose(head: boolean, gripper: boolean, arm: boolean): RobotPose { + let filteredPose: RobotPose = {}; + if (head) { + filteredPose["joint_head_tilt"] = this.robotPose["joint_head_tilt"]; + filteredPose["joint_head_pan"] = this.robotPose["joint_head_pan"]; } - - setRunStopState(enabled: boolean) { - this.runStopEnabled = enabled - if (this.runStopFunctionProviderCallback) this.runStopFunctionProviderCallback(this.runStopEnabled) + if (gripper) { + filteredPose["joint_wrist_roll"] = this.robotPose["joint_wrist_roll"]; + filteredPose["joint_wrist_pitch"] = this.robotPose["joint_wrist_pitch"]; + filteredPose["joint_wrist_yaw"] = this.robotPose["joint_wrist_yaw"]; + filteredPose["joint_gripper_finger_left"] = + this.robotPose["joint_gripper_finger_left"]; } - - /** - * Records a callback from the function provider. The callback is called - * whenever the battery voltage changes. - * - * @param callback callback to function provider - */ - setBatteryFunctionProviderCallback(callback: (voltage: number) => void) { - this.batteryFunctionProviderCallback = callback; + if (arm) { + filteredPose["joint_lift"] = this.robotPose["joint_lift"]; + filteredPose["wrist_extension"] = this.robotPose["wrist_extension"]; } - - /** - * Records a callback from the function provider. The callback is called - * whenever the battery voltage changes. - * - * @param callback callback to function provider - */ - setRunStopFunctionProviderCallback(callback: (enabled: boolean) => void) { - this.runStopFunctionProviderCallback = callback; - } - - /** - * @returns current robot pose - */ - getRobotPose(head: boolean, gripper: boolean, arm: boolean): RobotPose { - let filteredPose: RobotPose = {} - if (head) { - filteredPose["joint_head_tilt"] = this.robotPose["joint_head_tilt"] - filteredPose["joint_head_pan"] = this.robotPose["joint_head_pan"] - } - if (gripper) { - filteredPose["joint_wrist_roll"] = this.robotPose["joint_wrist_roll"] - filteredPose["joint_wrist_pitch"] = this.robotPose["joint_wrist_pitch"] - filteredPose["joint_wrist_yaw"] = this.robotPose["joint_wrist_yaw"] - filteredPose["joint_gripper_finger_left"] = this.robotPose["joint_gripper_finger_left"] - } - if (arm) { - filteredPose["joint_lift"] = this.robotPose["joint_lift"] - filteredPose["wrist_extension"] = this.robotPose["wrist_extension"] - } - return filteredPose - } -} \ No newline at end of file + return filteredPose; + } +} diff --git a/src/shared/util.tsx b/src/shared/util.tsx index b309a327..e52bdd4a 100644 --- a/src/shared/util.tsx +++ b/src/shared/util.tsx @@ -1,376 +1,432 @@ -import ROSLIB, { Message } from 'roslib' -import { cmd } from './commands'; - -export type ValidJoints = 'joint_head_tilt' | 'joint_head_pan' | 'joint_gripper_finger_left' | 'joint_arm' | 'wrist_extension' | 'joint_lift' | 'joint_wrist_roll' | 'joint_wrist_pitch' | 'joint_wrist_yaw' | "translate_mobile_base" | "rotate_mobile_base" | 'gripper_aperture' | 'joint_arm_l0' | 'joint_arm_l1' | 'joint_arm_l2' | 'joint_arm_l3'; - -export type VelocityGoalArray = [{ [key in ValidJoints]?: number }, { [key in ValidJoints]?: number }] +import ROSLIB, { Message } from "roslib"; +import { cmd } from "./commands"; + +export type ValidJoints = + | "joint_head_tilt" + | "joint_head_pan" + | "joint_gripper_finger_left" + | "joint_arm" + | "wrist_extension" + | "joint_lift" + | "joint_wrist_roll" + | "joint_wrist_pitch" + | "joint_wrist_yaw" + | "translate_mobile_base" + | "rotate_mobile_base" + | "gripper_aperture" + | "joint_arm_l0" + | "joint_arm_l1" + | "joint_arm_l2" + | "joint_arm_l3"; + +export type VelocityGoalArray = [ + { [key in ValidJoints]?: number }, + { [key in ValidJoints]?: number }, +]; export type RemoteStream = { - stream: MediaStream; - track: MediaStreamTrack -} - -export const AllJoints: ValidJoints[] = ['joint_head_tilt', 'joint_head_pan', 'joint_gripper_finger_left', 'joint_arm', 'wrist_extension', 'joint_lift', 'joint_wrist_roll', 'joint_wrist_pitch', 'joint_wrist_yaw', "translate_mobile_base", "rotate_mobile_base"]; - -export type ValidJointStateDict = { [key in ValidJoints]?: [boolean, boolean] } + stream: MediaStream; + track: MediaStreamTrack; +}; + +export const AllJoints: ValidJoints[] = [ + "joint_head_tilt", + "joint_head_pan", + "joint_gripper_finger_left", + "joint_arm", + "wrist_extension", + "joint_lift", + "joint_wrist_roll", + "joint_wrist_pitch", + "joint_wrist_yaw", + "translate_mobile_base", + "rotate_mobile_base", +]; + +export type ValidJointStateDict = { [key in ValidJoints]?: [boolean, boolean] }; export interface ROSJointState extends Message { - name: [ValidJoints?], - position: [number], - effort: [number], - velocity: [number], + name: [ValidJoints?]; + position: [number]; + effort: [number]; + velocity: [number]; } export interface ROSBatteryState extends Message { - voltage: number + voltage: number; } export interface ROSCompressedImage extends Message { - header: string, - format: "jpeg" | "png", - data: string + header: string; + format: "jpeg" | "png"; + data: string; } export interface CameraInfo { - [key: string]: string + [key: string]: string; } export interface SignallingMessage { - candidate?: RTCIceCandidate, - sessionDescription?: RTCSessionDescription, - cameraInfo?: CameraInfo + candidate?: RTCIceCandidate; + sessionDescription?: RTCSessionDescription; + cameraInfo?: CameraInfo; } export interface Transform { - transform: ROSLIB.Transform -} - -export type WebRTCMessage = ValidJointStateMessage | OccupancyGridMessage | MapPoseMessage | StopTrajectoryMessage | StopMoveBaseMessage | FollowJointTrajectoryActionResultMessage | MoveBaseActionResultMessage | BatteryVoltageMessage | MoveBaseStateMessage | IsRunStoppedMessage | HasBetaTeleopKitMessage | cmd; + transform: ROSLIB.Transform; +} + +export type WebRTCMessage = + | ValidJointStateMessage + | OccupancyGridMessage + | MapPoseMessage + | StopTrajectoryMessage + | StopMoveBaseMessage + | FollowJointTrajectoryActionResultMessage + | MoveBaseActionResultMessage + | BatteryVoltageMessage + | MoveBaseStateMessage + | IsRunStoppedMessage + | HasBetaTeleopKitMessage + | cmd; interface StopTrajectoryMessage { - type: "stopTrajectory" + type: "stopTrajectory"; } interface StopMoveBaseMessage { - type: "stopMoveBase" + type: "stopMoveBase"; } -export type RobotPose = { [key in ValidJoints]?: number } +export type RobotPose = { [key in ValidJoints]?: number }; export interface ValidJointStateMessage { - type: "validJointState", - robotPose: RobotPose, - jointsInLimits: { [key in ValidJoints]?: [boolean, boolean] } - jointsInCollision: { [key in ValidJoints]?: [boolean, boolean] } + type: "validJointState"; + robotPose: RobotPose; + jointsInLimits: { [key in ValidJoints]?: [boolean, boolean] }; + jointsInCollision: { [key in ValidJoints]?: [boolean, boolean] }; } export interface IsRunStoppedMessage { - type: "isRunStopped", - enabled: boolean + type: "isRunStopped"; + enabled: boolean; } export interface HasBetaTeleopKitMessage { - type: "hasBetaTeleopKit", - value: boolean + type: "hasBetaTeleopKit"; + value: boolean; } export interface FollowJointTrajectoryActionResultMessage { - type: "goalStatus" - message: FollowJointTrajectoryActionResult + type: "goalStatus"; + message: FollowJointTrajectoryActionResult; } export interface FollowJointTrajectoryActionResult { - header: string - status: GoalStatus - result: string + header: string; + status: GoalStatus; + result: string; } export interface NavigateToPoseActionStatusList { - status_list: NavigateToPoseActionStatus[] + status_list: NavigateToPoseActionStatus[]; } -export interface NavigateToPoseActionStatus{ - status: number +export interface NavigateToPoseActionStatus { + status: number; } export interface GoalStatus { - goal_id: string - status: number - text: string + goal_id: string; + status: number; + text: string; } export interface MoveBaseState { - state: string - alert_type: string + state: string; + alert_type: string; } export interface MoveBaseStateMessage { - type: "moveBaseState" - message: MoveBaseState + type: "moveBaseState"; + message: MoveBaseState; } export interface MoveBaseActionResultMessage { - type: "moveBaseActionResult" - message: MoveBaseActionResult + type: "moveBaseActionResult"; + message: MoveBaseActionResult; } export interface MoveBaseActionResult { - header: string - status: GoalStatus - result: string + header: string; + status: GoalStatus; + result: string; } export interface OccupancyGridMessage { - type: "occupancyGrid", - message: ROSOccupancyGrid + type: "occupancyGrid"; + message: ROSOccupancyGrid; } export interface MapPoseMessage { - type: 'amclPose', - message: ROSLIB.Transform + type: "amclPose"; + message: ROSLIB.Transform; } export interface BatteryVoltageMessage { - type: 'batteryVoltage', - message: number + type: "batteryVoltage"; + message: number; } export interface AMCLPose extends Message { - header: string, - pose: { - pose: ROSPose, - covariance: number[] - } + header: string; + pose: { + pose: ROSPose; + covariance: number[]; + }; } export interface MarkerArray { - markers: Marker[] + markers: Marker[]; } export interface Marker { - text: string, - id: number + text: string; + id: number; } export interface ROSPoint extends Message { - x: number, - y: number, - z: number + x: number; + y: number; + z: number; } export interface ROSQuaternion extends Message { - x: number, - y: number, - z: number, - w: number + x: number; + y: number; + z: number; + w: number; } -export interface ROSPose extends Message{ - position: ROSPoint - orientation: ROSQuaternion +export interface ROSPose extends Message { + position: ROSPoint; + orientation: ROSQuaternion; } export interface ROSMapMetaData extends Message { - map_load_time: number, - resolution: number, - width: number, - height: number, - origin: ROSPose + map_load_time: number; + resolution: number; + width: number; + height: number; + origin: ROSPose; } export interface ROSOccupancyGrid { - header: string, - info: ROSMapMetaData, - data: number[] + header: string; + info: ROSMapMetaData; + data: number[]; } export const STOW_WRIST: RobotPose = { - "joint_wrist_roll": 0.0, - "joint_wrist_pitch": -0.49700, - "joint_wrist_yaw": 3.19579 -} + joint_wrist_roll: 0.0, + joint_wrist_pitch: -0.497, + joint_wrist_yaw: 3.19579, +}; export const CENTER_WRIST: RobotPose = { - "joint_wrist_roll": 0.0, - "joint_wrist_pitch": 0.0, - "joint_wrist_yaw": 0.0 -} + joint_wrist_roll: 0.0, + joint_wrist_pitch: 0.0, + joint_wrist_yaw: 0.0, +}; export const REALSENSE_FORWARD_POSE: RobotPose = { - "joint_head_pan": 0.075, - "joint_head_tilt": 0.0 -} + joint_head_pan: 0.075, + joint_head_tilt: 0.0, +}; export const REALSENSE_BASE_POSE: RobotPose = { - "joint_head_pan": 0.075, - "joint_head_tilt": -1.1 -} + joint_head_pan: 0.075, + joint_head_tilt: -1.1, +}; export const REALSENSE_GRIPPER_POSE: RobotPose = { - "joint_head_pan": -1.7, - "joint_head_tilt": -1.35 -} + joint_head_pan: -1.7, + joint_head_tilt: -1.35, +}; export const JOINT_LIMITS: { [key in ValidJoints]?: [number, number] } = { - "wrist_extension": [0.001, .518], - "joint_wrist_roll": [-2.95, 2.94], - "joint_wrist_pitch": [-1.57, 0.57], - "joint_wrist_yaw": [-1.37, 4.41], - "joint_lift": [0.001, 1.1], - "translate_mobile_base": [-30.0, 30.0], - "rotate_mobile_base": [-3.14, 3.14], - "joint_gripper_finger_left": [-0.37, 0.17], - "joint_head_tilt": [-1.6, 0.3], - "joint_head_pan": [-3.95, 1.7] -} + wrist_extension: [0.001, 0.518], + joint_wrist_roll: [-2.95, 2.94], + joint_wrist_pitch: [-1.57, 0.57], + joint_wrist_yaw: [-1.37, 4.41], + joint_lift: [0.001, 1.1], + translate_mobile_base: [-30.0, 30.0], + rotate_mobile_base: [-3.14, 3.14], + joint_gripper_finger_left: [-0.37, 0.17], + joint_head_tilt: [-1.6, 0.3], + joint_head_pan: [-3.95, 1.7], +}; export const JOINT_VELOCITIES: { [key in ValidJoints]?: number } = { - "joint_head_tilt": .3, - "joint_head_pan": .3, - "wrist_extension": .04, - "joint_lift": .04, - "joint_wrist_roll": .1, - "joint_wrist_pitch": .1, - "joint_wrist_yaw": .4, - "translate_mobile_base": .1, - "rotate_mobile_base": .3 -} + joint_head_tilt: 0.3, + joint_head_pan: 0.3, + wrist_extension: 0.04, + joint_lift: 0.04, + joint_wrist_roll: 0.1, + joint_wrist_pitch: 0.1, + joint_wrist_yaw: 0.4, + translate_mobile_base: 0.1, + rotate_mobile_base: 0.3, +}; export const JOINT_INCREMENTS: { [key in ValidJoints]?: number } = { - "joint_head_tilt": 0.1, - "joint_head_pan": 0.1, - "joint_gripper_finger_left": .075, - "wrist_extension": 0.075, - "joint_lift": .075, - "joint_wrist_roll": .2, - "joint_wrist_pitch": .2, - "joint_wrist_yaw": .2, - "translate_mobile_base": .1, - "rotate_mobile_base": .2 -} + joint_head_tilt: 0.1, + joint_head_pan: 0.1, + joint_gripper_finger_left: 0.075, + wrist_extension: 0.075, + joint_lift: 0.075, + joint_wrist_roll: 0.2, + joint_wrist_pitch: 0.2, + joint_wrist_yaw: 0.2, + translate_mobile_base: 0.1, + rotate_mobile_base: 0.2, +}; export const navigationProps = { - width: 768, // 800, - height: 768, // 1280, - scale: 1, - fps: 6.0 -} + width: 768, // 800, + height: 768, // 1280, + scale: 1, + fps: 6.0, +}; export const realsenseProps = { - width: 360, - height: 640, - scale: 1, - fps: 6.0 -} + width: 360, + height: 640, + scale: 1, + fps: 6.0, +}; export const gripperProps = { - width: 768, // 1024 - height: 768, - scale: 1, - fps: 6.0 -} + width: 768, // 1024 + height: 768, + scale: 1, + fps: 6.0, +}; export interface VideoProps { - topicName: string, - callback: (message: ROSCompressedImage) => void + topicName: string; + callback: (message: ROSCompressedImage) => void; } export function rosJointStatetoRobotPose(jointState: ROSJointState): RobotPose { - let robotPose: RobotPose = {} - const names = jointState.name - const positions = jointState.position - names.map((name, index) => { - if (name) { - robotPose[name] = positions[index] - } - }) - return robotPose + let robotPose: RobotPose = {}; + const names = jointState.name; + const positions = jointState.position; + names.map((name, index) => { + if (name) { + robotPose[name] = positions[index]; + } + }); + return robotPose; } //////////////////////////////////////////////////////////// // safelyParseJSON code copied from // https://stackoverflow.com/questions/29797946/handling-bad-json-parse-in-node-safely // on August 18, 2017 export function safelyParseJSON(json: string): T { - // This function cannot be optimized, it's best to - // keep it small! - let parsed; - - try { - parsed = JSON.parse(json); - } catch (e) { - console.warn(e); - } + // This function cannot be optimized, it's best to + // keep it small! + let parsed; - return parsed; // Could be undefined! + try { + parsed = JSON.parse(json); + } catch (e) { + console.warn(e); + } + + return parsed; // Could be undefined! } export type uuid = string; // From: https://stackoverflow.com/a/2117523/6454085 export function generateUUID(): uuid { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { - var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); - return v.toString(16); - }); -} - -export async function waitUntil(condition, timeout=5000, checkInterval=100) { - let interval; - let waitPromise = new Promise(resolve => { - interval = setInterval(() => { - if (!condition()) { - return; - } - clearInterval(interval); - resolve(true); - }, checkInterval) - }) - let timeoutPromise = new Promise(resolve => setTimeout(() => { - clearInterval(interval) - resolve(false) - }, timeout)) - return await Promise.any([waitPromise, timeoutPromise]) -} - -export async function waitUntilAsync(condition, timeout=5000, checkInterval=100) { - let interval; - let waitPromise = new Promise(resolve => { - interval = setInterval(async () => { - let value = await condition() - if (!value) { - return; - } - clearInterval(interval); - resolve(true); - }, checkInterval) - }) - let timeoutPromise = new Promise(resolve => setTimeout(() => { - clearInterval(interval) - resolve(false) - }, timeout)) - return await Promise.any([waitPromise, timeoutPromise]) -} - -export const delay = (ms: number) => new Promise(res => setTimeout(res, ms)); + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { + var r = (Math.random() * 16) | 0, + v = c == "x" ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +} + +export async function waitUntil( + condition, + timeout = 5000, + checkInterval = 100, +) { + let interval; + let waitPromise = new Promise((resolve) => { + interval = setInterval(() => { + if (!condition()) { + return; + } + clearInterval(interval); + resolve(true); + }, checkInterval); + }); + let timeoutPromise = new Promise((resolve) => + setTimeout(() => { + clearInterval(interval); + resolve(false); + }, timeout), + ); + return await Promise.any([waitPromise, timeoutPromise]); +} + +export async function waitUntilAsync( + condition, + timeout = 5000, + checkInterval = 100, +) { + let interval; + let waitPromise = new Promise((resolve) => { + interval = setInterval(async () => { + let value = await condition(); + if (!value) { + return; + } + clearInterval(interval); + resolve(true); + }, checkInterval); + }); + let timeoutPromise = new Promise((resolve) => + setTimeout(() => { + clearInterval(interval); + resolve(false); + }, timeout), + ); + return await Promise.any([waitPromise, timeoutPromise]); +} + +export const delay = (ms: number) => new Promise((res) => setTimeout(res, ms)); /** * Creates a class name string based on a base class name and additional flags * to include. - * + * * @param baseName base name of the class * @param flags additional flags to append to the class name * @returns returns a string for the class name - * + * * @example ```js * className("foreground-button", {"active": false, "customizing": true}) * // returns "foreground-button customizing" * ``` */ export function className(baseName: string, flags: {}): string { - let className = baseName; - for (const [k, b] of Object.entries(flags)) { - if (b) { - className += " " + k; - } + let className = baseName; + for (const [k, b] of Object.entries(flags)) { + if (b) { + className += " " + k; } - return className; -} \ No newline at end of file + } + return className; +} diff --git a/src/shared/webrtcconnections.tsx b/src/shared/webrtcconnections.tsx index 5cbbd58f..eca9a9f9 100644 --- a/src/shared/webrtcconnections.tsx +++ b/src/shared/webrtcconnections.tsx @@ -1,319 +1,344 @@ -import React from 'react' +import React from "react"; import { CameraInfo, SignallingMessage, WebRTCMessage } from "shared/util"; -import io, { Socket } from 'socket.io-client'; -import { safelyParseJSON, generateUUID } from 'shared/util' +import io, { Socket } from "socket.io-client"; +import { safelyParseJSON, generateUUID } from "shared/util"; const peerConstraints = { - iceServers: [ - { - urls: 'stun:stun1.l.google.com:19302' - } - ], + iceServers: [ + { + urls: "stun:stun1.l.google.com:19302", + }, + ], }; interface WebRTCProps { - peerRole: "operator" | "robot"; - polite: boolean; - onTrackAdded?: (ev: RTCTrackEvent) => void; - onMessage: (message: WebRTCMessage) => void; - onRobotConnectionStart?: () => void; - onMessageChannelOpen?: () => void; - onConnectionEnd?: () => void; + peerRole: "operator" | "robot"; + polite: boolean; + onTrackAdded?: (ev: RTCTrackEvent) => void; + onMessage: (message: WebRTCMessage) => void; + onRobotConnectionStart?: () => void; + onMessageChannelOpen?: () => void; + onConnectionEnd?: () => void; } export class WebRTCConnection extends React.Component { - private socket: Socket; - private peerConnection?: RTCPeerConnection - private peerRole: string - private polite: boolean - private makingOffer = false - private ignoreOffer: boolean = false - private isSettingRemoteAnswerPending = false - private pendingIceCandidates: RTCIceCandidate[] - private dataChannelReceivedByteCount: number = 0; - private dataChannelReceivedTimestamp: number = 0; - private dataChannelConnectionState: boolean = false; - public robotAvailable: boolean; - - cameraInfo: CameraInfo = {} - - private messageChannel?: RTCDataChannel - - private onTrackAdded?: (ev: RTCTrackEvent) => void - private onMessage: (obj: WebRTCMessage) => void - private onRobotConnectionStart?: () => void - private onMessageChannelOpen?: () => void - private onConnectionEnd?: () => void - - constructor(props: WebRTCProps) { - super(props); - this.peerRole = props.peerRole - this.polite = props.polite - this.onRobotConnectionStart = props.onRobotConnectionStart - this.onMessage = props.onMessage - this.onTrackAdded = props.onTrackAdded - this.onMessageChannelOpen = props.onMessageChannelOpen - this.onConnectionEnd = props.onConnectionEnd - this.pendingIceCandidates = [] - - this.createPeerConnection() - - this.socket = io(); - - this.socket.on('connect', () => { - console.log("socket connected") - }) - - this.socket.on('join', (room: string) => { - console.log('Another peer made a request to join room ' + room); - console.log('I am ' + this.peerRole + '!'); - }); - - this.socket.on('bye', () => { - console.log('Session terminated.'); - this.stop(); - }) - - this.socket.on('joined', (room: String) => { - console.log('I am ' + this.peerRole + '!'); - console.log('joined: ' + room); - if (this.onRobotConnectionStart) this.onRobotConnectionStart() - }); - - this.socket.on('signalling', (message: SignallingMessage) => { - let {candidate, sessionDescription, cameraInfo} = message; - console.log('Received message:', message); - if (cameraInfo) { - if (Object.keys(cameraInfo).length === 0) { - console.warn("Received a camera info mapping with no entries") - } - this.cameraInfo = cameraInfo - } - if (sessionDescription?.type === 'offer' || sessionDescription?.type === 'answer') { - if (!this.peerConnection) throw 'peerConnection is undefined'; - const readyForOffer = - !this.makingOffer && - (this.peerConnection.signalingState === "stable" || this.isSettingRemoteAnswerPending); - const offerCollision = sessionDescription.type === "offer" && !readyForOffer; - - this.ignoreOffer = !this.polite && offerCollision; - - if (this.ignoreOffer) { - console.error("Ignoring offer") - return; - } - this.isSettingRemoteAnswerPending = sessionDescription.type === "answer"; - - this.peerConnection.setRemoteDescription(sessionDescription).then(async () => { - this.isSettingRemoteAnswerPending = false; - if (!this.peerConnection) throw 'peerConnection is undefined'; - if (sessionDescription?.type === "offer") { - return this.peerConnection.setLocalDescription(); - } else { - return false; - } - }).then(() => { - if (!this.peerConnection?.localDescription) throw 'peerConnection is undefined'; - this.sendSignallingMessage({sessionDescription: this.peerConnection.localDescription}); - }); - } else if (candidate !== undefined && this.peerConnection) { - // Note that the last candidate will be null, so we check whether - // the candidate variable is defined at all versus being truthy. - // if (this.peerConnection.remoteDescription !== null) { - this.peerConnection.addIceCandidate(candidate).catch(e => { - if (!this.ignoreOffer) { - console.log("Failure during addIceCandidate(): " + e.name); - throw e; - } - }); + private socket: Socket; + private peerConnection?: RTCPeerConnection; + private peerRole: string; + private polite: boolean; + private makingOffer = false; + private ignoreOffer: boolean = false; + private isSettingRemoteAnswerPending = false; + private pendingIceCandidates: RTCIceCandidate[]; + private dataChannelReceivedByteCount: number = 0; + private dataChannelReceivedTimestamp: number = 0; + private dataChannelConnectionState: boolean = false; + public robotAvailable: boolean; + + cameraInfo: CameraInfo = {}; + + private messageChannel?: RTCDataChannel; + + private onTrackAdded?: (ev: RTCTrackEvent) => void; + private onMessage: (obj: WebRTCMessage) => void; + private onRobotConnectionStart?: () => void; + private onMessageChannelOpen?: () => void; + private onConnectionEnd?: () => void; + + constructor(props: WebRTCProps) { + super(props); + this.peerRole = props.peerRole; + this.polite = props.polite; + this.onRobotConnectionStart = props.onRobotConnectionStart; + this.onMessage = props.onMessage; + this.onTrackAdded = props.onTrackAdded; + this.onMessageChannelOpen = props.onMessageChannelOpen; + this.onConnectionEnd = props.onConnectionEnd; + this.pendingIceCandidates = []; + + this.createPeerConnection(); + + this.socket = io(); + + this.socket.on("connect", () => { + console.log("socket connected"); + }); + + this.socket.on("join", (room: string) => { + console.log("Another peer made a request to join room " + room); + console.log("I am " + this.peerRole + "!"); + }); + + this.socket.on("bye", () => { + console.log("Session terminated."); + this.stop(); + }); + + this.socket.on("joined", (room: String) => { + console.log("I am " + this.peerRole + "!"); + console.log("joined: " + room); + if (this.onRobotConnectionStart) this.onRobotConnectionStart(); + }); + + this.socket.on("signalling", (message: SignallingMessage) => { + let { candidate, sessionDescription, cameraInfo } = message; + console.log("Received message:", message); + if (cameraInfo) { + if (Object.keys(cameraInfo).length === 0) { + console.warn("Received a camera info mapping with no entries"); + } + this.cameraInfo = cameraInfo; + } + if ( + sessionDescription?.type === "offer" || + sessionDescription?.type === "answer" + ) { + if (!this.peerConnection) throw "peerConnection is undefined"; + const readyForOffer = + !this.makingOffer && + (this.peerConnection.signalingState === "stable" || + this.isSettingRemoteAnswerPending); + const offerCollision = + sessionDescription.type === "offer" && !readyForOffer; + + this.ignoreOffer = !this.polite && offerCollision; + + if (this.ignoreOffer) { + console.error("Ignoring offer"); + return; + } + this.isSettingRemoteAnswerPending = + sessionDescription.type === "answer"; + + this.peerConnection + .setRemoteDescription(sessionDescription) + .then(async () => { + this.isSettingRemoteAnswerPending = false; + if (!this.peerConnection) throw "peerConnection is undefined"; + if (sessionDescription?.type === "offer") { + return this.peerConnection.setLocalDescription(); } else { - console.error("Unable to handle message") + return false; } + }) + .then(() => { + if (!this.peerConnection?.localDescription) + throw "peerConnection is undefined"; + this.sendSignallingMessage({ + sessionDescription: this.peerConnection.localDescription, + }); + }); + } else if (candidate !== undefined && this.peerConnection) { + // Note that the last candidate will be null, so we check whether + // the candidate variable is defined at all versus being truthy. + // if (this.peerConnection.remoteDescription !== null) { + this.peerConnection.addIceCandidate(candidate).catch((e) => { + if (!this.ignoreOffer) { + console.log("Failure during addIceCandidate(): " + e.name); + throw e; + } }); - } - - /** - * Called to create bidirectional message channels with the connected peer. - * Note that the peer is expected to be using this class to manage their end of the connection as well, - * in which case the necessary event handlers will be installed in `createPeerConnection` during `ondatachannel`. - */ - openDataChannels() { - console.log("opened data channels") - if (!this.peerConnection) throw 'peerConnection undefined' - this.messageChannel = this.peerConnection.createDataChannel('messages'); - this.messageChannel.onmessage = this.onReceiveMessageCallback.bind(this); - } + } else { + console.error("Unable to handle message"); + } + }); + } + + /** + * Called to create bidirectional message channels with the connected peer. + * Note that the peer is expected to be using this class to manage their end of the connection as well, + * in which case the necessary event handlers will be installed in `createPeerConnection` during `ondatachannel`. + */ + openDataChannels() { + console.log("opened data channels"); + if (!this.peerConnection) throw "peerConnection undefined"; + this.messageChannel = this.peerConnection.createDataChannel("messages"); + this.messageChannel.onmessage = this.onReceiveMessageCallback.bind(this); + } + + createPeerConnection() { + try { + this.peerConnection = new RTCPeerConnection(peerConstraints); + this.peerConnection.onicecandidate = (event) => { + console.log("ICE candidate available"); + this.sendSignallingMessage({ + candidate: event.candidate!, + }); + }; + + this.peerConnection.ontrack = this.onTrackAdded!; + + this.peerConnection.ondatachannel = (event) => { + // The remote has opened a data channel. We'll set up different handlers for the different channels. + if (event.channel.label === "messages") { + this.messageChannel = event.channel; + this.messageChannel.onmessage = + this.onReceiveMessageCallback.bind(this); + let onDataChannelStateChange = () => { + if (!this.messageChannel) throw "messageChannel is undefined"; + const readyState = this.messageChannel.readyState; + console.log("Data channel state is: " + readyState); + if (readyState === "open") { + if (this.onMessageChannelOpen) this.onMessageChannelOpen(); + } + }; + this.messageChannel.onopen = onDataChannelStateChange; + this.messageChannel.onclose = onDataChannelStateChange; + } else { + console.error("Unknown channel opened:", event.channel.label); + } + }; - createPeerConnection() { + this.peerConnection.onnegotiationneeded = async () => { + console.log("Negotiation needed"); try { - this.peerConnection = new RTCPeerConnection( peerConstraints ); - this.peerConnection.onicecandidate = (event) => { - console.log("ICE candidate available") - this.sendSignallingMessage({ - candidate: event.candidate! - }); - }; - - this.peerConnection.ontrack = this.onTrackAdded! - - this.peerConnection.ondatachannel = event => { - // The remote has opened a data channel. We'll set up different handlers for the different channels. - if (event.channel.label === "messages") { - this.messageChannel = event.channel; - this.messageChannel.onmessage = this.onReceiveMessageCallback.bind(this); - let onDataChannelStateChange = () => { - if (!this.messageChannel) throw 'messageChannel is undefined'; - const readyState = this.messageChannel.readyState; - console.log('Data channel state is: ' + readyState); - if (readyState === 'open') { - if (this.onMessageChannelOpen) this.onMessageChannelOpen(); - } - } - this.messageChannel.onopen = onDataChannelStateChange; - this.messageChannel.onclose = onDataChannelStateChange; - } else { - console.error("Unknown channel opened:", event.channel.label) - } - }; - - this.peerConnection.onnegotiationneeded = async () => { - console.log("Negotiation needed") - try { - this.makingOffer = true; - if (!this.peerConnection) throw 'peerConnection is undefined'; - await this.peerConnection.setLocalDescription(); - if (this.peerConnection.localDescription) { - this.sendSignallingMessage({ - sessionDescription: this.peerConnection.localDescription, - cameraInfo: this.cameraInfo - }); - } - } catch (err) { - console.error(err); - } finally { - this.makingOffer = false; - } - }; - - this.peerConnection.oniceconnectionstatechange = () => { - if (!this.peerConnection) throw 'peerConnection is undefined'; - if (this.peerConnection.iceConnectionState === "failed") { - this.peerConnection.restartIce(); - } - }; - - this.peerConnection.onconnectionstatechange = () => { - if (!this.peerConnection) throw 'pc is undefined'; - console.log(this.peerConnection.connectionState) - if (this.peerConnection.connectionState === "failed") { - console.error(this.peerConnection.connectionState, "Resetting the PeerConnection") - if (this.onConnectionEnd) this.onConnectionEnd(); - this.createPeerConnection() - this.peerRole == "operator" ? this.addOperatorToRobotRoom() : this.joinRobotRoom() - } - }; - - this.peerConnection.onicecandidateerror = (event) => { - console.error('ICE candidate gathering error:', event.errorCode, ' ', event.errorText); - }; - - console.log('Created RTCPeerConnection'); - } catch (e: any) { - console.error('Failed to create PeerConnection, exception: ' + e.message); - return; + this.makingOffer = true; + if (!this.peerConnection) throw "peerConnection is undefined"; + await this.peerConnection.setLocalDescription(); + if (this.peerConnection.localDescription) { + this.sendSignallingMessage({ + sessionDescription: this.peerConnection.localDescription, + cameraInfo: this.cameraInfo, + }); + } + } catch (err) { + console.error(err); + } finally { + this.makingOffer = false; } - } - - connectionState() { - return this.peerConnection?.connectionState - } + }; - joinRobotRoom() { - console.log('attempting to join room = robot'); - this.socket.emit('join', 'robot') - } - - addOperatorToRobotRoom() { - return new Promise((resolve) => { - this.socket.emit('add operator to robot room', (response) => { - console.log("SUCCESS: ", response.success) - resolve(response.success) - }) - }); - } - - addTrack(track: MediaStreamTrack, stream: MediaStream, streamName: string) { - this.cameraInfo[stream.id] = streamName - if (!this.peerConnection) throw 'pc is undefined'; - this.peerConnection.addTrack(track, stream); - console.log("added track") - } - - hangup() { - // Tell the other end that we're ending the call so they can stop, and get us kicked out of the robot room - console.warn("Hanging up") - this.socket.emit('bye', this.peerRole); - if (!this.peerConnection) throw 'pc is undefined'; - if (this.peerConnection.connectionState === "new") { - // Don't reset PCs that don't have any state to reset - return; + this.peerConnection.oniceconnectionstatechange = () => { + if (!this.peerConnection) throw "peerConnection is undefined"; + if (this.peerConnection.iceConnectionState === "failed") { + this.peerConnection.restartIce(); + } + }; + + this.peerConnection.onconnectionstatechange = () => { + if (!this.peerConnection) throw "pc is undefined"; + console.log(this.peerConnection.connectionState); + if (this.peerConnection.connectionState === "failed") { + console.error( + this.peerConnection.connectionState, + "Resetting the PeerConnection", + ); + if (this.onConnectionEnd) this.onConnectionEnd(); + this.createPeerConnection(); + this.peerRole == "operator" + ? this.addOperatorToRobotRoom() + : this.joinRobotRoom(); } - console.log('Hanging up.'); - this.stop(); + }; + + this.peerConnection.onicecandidateerror = (event) => { + console.error( + "ICE candidate gathering error:", + event.errorCode, + " ", + event.errorText, + ); + }; + + console.log("Created RTCPeerConnection"); + } catch (e: any) { + console.error("Failed to create PeerConnection, exception: " + e.message); + return; } - - stop() { - if (!this.peerConnection) throw 'peerConnection is undefined'; - this.removeTracks() - this.peerConnection.close(); - this.createPeerConnection() + } + + connectionState() { + return this.peerConnection?.connectionState; + } + + joinRobotRoom() { + console.log("attempting to join room = robot"); + this.socket.emit("join", "robot"); + } + + addOperatorToRobotRoom() { + return new Promise((resolve) => { + this.socket.emit("add operator to robot room", (response) => { + console.log("SUCCESS: ", response.success); + resolve(response.success); + }); + }); + } + + addTrack(track: MediaStreamTrack, stream: MediaStream, streamName: string) { + this.cameraInfo[stream.id] = streamName; + if (!this.peerConnection) throw "pc is undefined"; + this.peerConnection.addTrack(track, stream); + console.log("added track"); + } + + hangup() { + // Tell the other end that we're ending the call so they can stop, and get us kicked out of the robot room + console.warn("Hanging up"); + this.socket.emit("bye", this.peerRole); + if (!this.peerConnection) throw "pc is undefined"; + if (this.peerConnection.connectionState === "new") { + // Don't reset PCs that don't have any state to reset + return; } - - removeTracks() { - if (!this.peerConnection) throw 'peerConnection is undefined'; - const senders = this.peerConnection.getSenders(); - senders.forEach((sender) => this.peerConnection?.removeTrack(sender)); + console.log("Hanging up."); + this.stop(); + } + + stop() { + if (!this.peerConnection) throw "peerConnection is undefined"; + this.removeTracks(); + this.peerConnection.close(); + this.createPeerConnection(); + } + + removeTracks() { + if (!this.peerConnection) throw "peerConnection is undefined"; + const senders = this.peerConnection.getSenders(); + senders.forEach((sender) => this.peerConnection?.removeTrack(sender)); + } + + sendSignallingMessage(message: SignallingMessage) { + this.socket.emit("signalling", message); + } + + //////////////////////////////////////////////////////////// + // RTCDataChannel + // on Sept. 15, 2017 copied initial code from + // https://github.com/googlecodelabs/webrtc-web/blob/master/step-03/js/main.js + // initial code licensed with Apache License 2.0 + //////////////////////////////////////////////////////////// + + sendData(obj: WebRTCMessage | WebRTCMessage[]) { + if (!this.messageChannel || this.messageChannel.readyState !== "open") { + // console.warn("Trying to send data, but data channel isn't ready") + return; } - - sendSignallingMessage(message: SignallingMessage) { - this.socket.emit('signalling', message); - } - - //////////////////////////////////////////////////////////// - // RTCDataChannel - // on Sept. 15, 2017 copied initial code from - // https://github.com/googlecodelabs/webrtc-web/blob/master/step-03/js/main.js - // initial code licensed with Apache License 2.0 - //////////////////////////////////////////////////////////// - - sendData(obj: WebRTCMessage | WebRTCMessage[]) { - if (!this.messageChannel || (this.messageChannel.readyState !== 'open')) { - // console.warn("Trying to send data, but data channel isn't ready") - return; + const data = JSON.stringify(obj); + this.messageChannel.send(data); + } + + onReceiveMessageCallback(event: { data: string }) { + const obj: WebRTCMessage | WebRTCMessage[] = safelyParseJSON(event.data); + if (this.onMessage) this.onMessage(obj); + } + + async isConnected() { + await this.peerConnection?.getStats(null).then((stats) => { + stats.forEach((report) => { + if (report.type == "data-channel") { + // Ignore repeated reports + if (report.timestamp <= this.dataChannelReceivedTimestamp) + return this.dataChannelConnectionState; + + this.dataChannelConnectionState = + report.bytesReceived > this.dataChannelReceivedByteCount; + this.dataChannelReceivedByteCount = report.bytesReceived; + this.dataChannelReceivedTimestamp = report.timestamp; } - const data = JSON.stringify(obj); - this.messageChannel.send(data) - } - - onReceiveMessageCallback(event: { data: string }) { - const obj: WebRTCMessage | WebRTCMessage[] = safelyParseJSON(event.data); - if (this.onMessage) this.onMessage(obj) - } - - async isConnected () { - await this.peerConnection?.getStats(null).then(stats => { - stats.forEach(report => { - if (report.type == 'data-channel') { - // Ignore repeated reports - if (report.timestamp <= this.dataChannelReceivedTimestamp) return this.dataChannelConnectionState - - this.dataChannelConnectionState = report.bytesReceived > this.dataChannelReceivedByteCount - this.dataChannelReceivedByteCount = report.bytesReceived; - this.dataChannelReceivedTimestamp = report.timestamp; - } - }) - }); + }); + }); - return this.dataChannelConnectionState; - } + return this.dataChannelConnectionState; + } } diff --git a/start_robot_browser.js b/start_robot_browser.js index 812a08d7..8b8e71cd 100755 --- a/start_robot_browser.js +++ b/start_robot_browser.js @@ -1,53 +1,55 @@ #!/usr/bin/env node -const { firefox } = require('playwright'); -const logId = 'start_robot_browser.js'; +const { firefox } = require("playwright"); +const logId = "start_robot_browser.js"; // You may want to change this to test that the // robot is using certificates that are valid for its real hostname -let robotHostname = "localhost" // or NGROK_URL +let robotHostname = "localhost"; // or NGROK_URL if (process.argv.length > 2) { - robotHostname = process.argv[2] + robotHostname = process.argv[2]; } (async () => { - const navigation_timeout_ms = 30000; //30 seconds (default is 30 seconds) - const min_idle_time = 1000; - var try_again = true; - var num_tries = 0; - var max_tries = 50; // -1 means try forever - - /////////////////////////////////////////////// - // sleep code from - // https://stackoverflow.com/questions/951021/what-is-the-javascript-version-of-sleep - function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); - } - /////////////////////////////////////////////// - - const browser = await firefox.launch({ - headless: true, // default is true - defaultViewport: null, - args: ['--use-fake-ui-for-media-stream', //gives permission to access the robot's cameras and microphones (cleaner and simpler than changing the user directory) - '--disable-features=WebRtcHideLocalIpsWithMdns', // Disables mDNS hostname use in local network P2P discovery. Necessary for enterprise networks that don't forward mDNS traffic - '--ignore-certificate-errors'] - }); - - const context = await browser.newContext({ ignoreHTTPSErrors: true }); // avoid ERR_CERT_COMMON_NAME_INVALID - - const page = await context.newPage(); - - while (try_again) { - try { - await page.goto(`https://${robotHostname}/robot`); - console.log(logId + ': finished loading'); - try_again = false; - } catch (e) { - console.log(logId + ': trying again'); - console.log(e); - await sleep(200); - try_again = true; - } - } - - console.log(logId + ': start script complete'); + const navigation_timeout_ms = 30000; //30 seconds (default is 30 seconds) + const min_idle_time = 1000; + var try_again = true; + var num_tries = 0; + var max_tries = 50; // -1 means try forever + + /////////////////////////////////////////////// + // sleep code from + // https://stackoverflow.com/questions/951021/what-is-the-javascript-version-of-sleep + function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + /////////////////////////////////////////////// + + const browser = await firefox.launch({ + headless: true, // default is true + defaultViewport: null, + args: [ + "--use-fake-ui-for-media-stream", //gives permission to access the robot's cameras and microphones (cleaner and simpler than changing the user directory) + "--disable-features=WebRtcHideLocalIpsWithMdns", // Disables mDNS hostname use in local network P2P discovery. Necessary for enterprise networks that don't forward mDNS traffic + "--ignore-certificate-errors", + ], + }); + + const context = await browser.newContext({ ignoreHTTPSErrors: true }); // avoid ERR_CERT_COMMON_NAME_INVALID + + const page = await context.newPage(); + + while (try_again) { + try { + await page.goto(`https://${robotHostname}/robot`); + console.log(logId + ": finished loading"); + try_again = false; + } catch (e) { + console.log(logId + ": trying again"); + console.log(e); + await sleep(200); + try_again = true; + } + } + + console.log(logId + ": start script complete"); })(); diff --git a/stop_interface.sh b/stop_interface.sh index bb936ee3..46b77fbe 100755 --- a/stop_interface.sh +++ b/stop_interface.sh @@ -1,3 +1,6 @@ +#!/bin/bash + +# Usage: ./stop_interface.sh screen -S "web_teleop_ros" -X stuff '^C' if [[ $? -ne 0 ]]; then echo "Using pkill" diff --git a/webpack.config.js b/webpack.config.js index d044ef03..56485a08 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,109 +1,107 @@ -const path = require('path'); -const HtmlWebpackPlugin = require('html-webpack-plugin'); -const webpack = require('webpack') -const dotenv = require('dotenv'); +const path = require("path"); +const HtmlWebpackPlugin = require("html-webpack-plugin"); +const webpack = require("webpack"); +const dotenv = require("dotenv"); -const pages = ['robot', 'operator']; +const pages = ["robot", "operator"]; -// call dotenv and it will return an Object with a parsed key +// call dotenv and it will return an Object with a parsed key const env = dotenv.config().parsed; // reduce it to a nice object, the same as before const envKeys = Object.keys(env).reduce((prev, next) => { - prev[`process.env.${next}`] = JSON.stringify(env[next]); - return prev; + prev[`process.env.${next}`] = JSON.stringify(env[next]); + return prev; }, {}); module.exports = (env) => { - envKeys['process.env.storage'] = JSON.stringify(env.storage) - console.log(envKeys) + envKeys["process.env.storage"] = JSON.stringify(env.storage); + console.log(envKeys); return { - mode: 'development', + mode: "development", entry: pages.reduce((config, page) => { - config[page] = `./src/pages/${page}/tsx/index.tsx`; - return config; + config[page] = `./src/pages/${page}/tsx/index.tsx`; + return config; }, {}), output: { filename: "[name]/bundle.js", path: path.resolve(__dirname, "dist"), }, optimization: { - splitChunks: { - chunks: "all", - }, + splitChunks: { + chunks: "all", + }, }, - // node: { - // __dirname: false, - // }, - plugins: [ - // Work around for Buffer is undefined: - // https://github.com/webpack/changelog-v5/issues/10 - new webpack.ProvidePlugin({ - Buffer: ['buffer', 'Buffer'], - }), - // new webpack.ProvidePlugin({ - // process: 'process/browser', - // }), - new webpack.DefinePlugin(envKeys), - ].concat( - pages.map( - (page) => - new HtmlWebpackPlugin({ - inject: true, - template: `./src/pages/${page}/html/index.html`, - filename: `${page}/index.html`, - chunks: [page], - }) - ) - ), - module: { - rules: [ - { - test: /\.(ts)x?$/, - use: [ - { - loader: 'babel-loader', - options: { - presets: [ - '@babel/preset-env', - ["@babel/preset-react", {"runtime": "automatic"}], - '@babel/preset-typescript' - ], - plugins: [ - '@babel/plugin-transform-runtime' - ] + // node: { + // __dirname: false, + // }, + plugins: [ + // Work around for Buffer is undefined: + // https://github.com/webpack/changelog-v5/issues/10 + new webpack.ProvidePlugin({ + Buffer: ["buffer", "Buffer"], + }), + // new webpack.ProvidePlugin({ + // process: 'process/browser', + // }), + new webpack.DefinePlugin(envKeys), + ].concat( + pages.map( + (page) => + new HtmlWebpackPlugin({ + inject: true, + template: `./src/pages/${page}/html/index.html`, + filename: `${page}/index.html`, + chunks: [page], + }), + ), + ), + module: { + rules: [ + { + test: /\.(ts)x?$/, + use: [ + { + loader: "babel-loader", + options: { + presets: [ + "@babel/preset-env", + ["@babel/preset-react", { runtime: "automatic" }], + "@babel/preset-typescript", + ], + plugins: ["@babel/plugin-transform-runtime"], + }, }, - }, - ], - exclude: /node_modules/, + ], + exclude: /node_modules/, + }, + { + test: /\.css$/i, + use: ["style-loader", "css-loader"], + }, + { + test: /\.(jpe?g|png|gif|svg)$/i, + use: "file-loader", + }, + ], + }, + externals: { + express: "commonjs express", + }, + resolve: { + extensions: [".tsx", ".js"], + alias: { + shared: path.resolve(__dirname, "./src/shared/"), + operator: path.resolve(__dirname, "./src/pages/operator/"), + robot: path.resolve(__dirname, "./src/pages/robot/"), }, - { - test: /\.css$/i, - use: ["style-loader", "css-loader"], + fallback: { + fs: false, + stream: false, + zlib: false, }, - { - test: /\.(jpe?g|png|gif|svg)$/i, - use: 'file-loader' - } - ], - }, - externals: { - 'express': 'commonjs express' - }, - resolve: { - extensions: ['.tsx', '.js'], - alias: { - "shared": path.resolve(__dirname, './src/shared/'), - "operator": path.resolve(__dirname, './src/pages/operator/'), - "robot": path.resolve(__dirname, './src/pages/robot/'), - }, - fallback: { - "fs": false, - "stream": false, - "zlib": false }, - }, - watch: true, - } + watch: true, + }; }; From 4cfaee60b5b1e2ffe15c07b65184a287824bc1d2 Mon Sep 17 00:00:00 2001 From: Amal Nanavati Date: Tue, 21 May 2024 11:35:13 -0700 Subject: [PATCH 5/5] Removed pylint from pull request template --- .github/pull_request_template.md | 1 - .pre-commit-config.yaml | 1 + .prettierignore | 2 -- 3 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 .prettierignore diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index acf22a8c..a071449f 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -11,7 +11,6 @@ From the top-level of this repository, run: - \[ \] `pre-commit run --all-files` -- \[ \] `pylint --recursive=y --rcfile=.pylintrc .`. All warnings but `fixme` must be addressed. # To merge diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 47018106..8a9dbb8f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -61,3 +61,4 @@ repos: rev: v4.0.0-alpha.8 hooks: - id: prettier + types_or: [css, html, javascript, json, jsx, ts, tsx] diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index e3604eec..00000000 --- a/.prettierignore +++ /dev/null @@ -1,2 +0,0 @@ -# Prettier should not reformat any markdown files -*.md