Merge branch 'main' into py-docs
1
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -22,6 +22,7 @@ If applicable, add screenshots to help explain your problem. Additionally, provi
|
||||
|
||||
**Platform:**
|
||||
- Hardware Platform (ex. Raspberry Pi 4, Windows x64):
|
||||
- How is it powered? (ex. Zinc-V, Pololu Buck Converter, Battery Bank):
|
||||
- Network Configuration (Connection between the Radio and any devices in between, such as a Network Switch):
|
||||
- PhotonVision Version:
|
||||
- Browser (with Version) (Chrome, Edge, Firefox, etc.):
|
||||
|
||||
17
.github/ISSUE_TEMPLATE/docs_issue.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: Documentation Request
|
||||
about: Something needs to be documented/updated in the documentation
|
||||
title: ''
|
||||
labels: documentation
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Are you requesting documentation for a new feature, or updated documentation for an old feature?**
|
||||
Put the feature you are requesting documentation for here, along with whether the documentation is stale and needs to be updated, or whether the documentation does not exist, and needs to be created.
|
||||
|
||||
**Where is it?**
|
||||
Put the location of the documentation that needs to be updated here. If you're requesting documenation for a new feature, put where you think it should go.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
5
.github/workflows/build.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 22
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
- name: Build Production Client
|
||||
@@ -557,7 +557,8 @@ jobs:
|
||||
rm: true
|
||||
files: |
|
||||
**/*.xz
|
||||
**/*.jar
|
||||
**/*linux*.jar
|
||||
**/*win*.jar
|
||||
**/photonlib*.json
|
||||
**/photonlib*.zip
|
||||
if: github.event_name == 'push'
|
||||
|
||||
2
.github/workflows/lint-format.yml
vendored
@@ -68,7 +68,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 22
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
- name: Check Linting
|
||||
|
||||
2
.github/workflows/photon-api-docs.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 22
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
- name: Build Production Client
|
||||
|
||||
3
.gitignore
vendored
@@ -7,8 +7,7 @@ __pycache__/
|
||||
backend/settings/
|
||||
.vscode/
|
||||
# Docs
|
||||
source/_build
|
||||
source/docs/_build
|
||||
_build
|
||||
# Compiled class file
|
||||
*.class
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ ext {
|
||||
wpimathVersion = wpilibVersion
|
||||
openCVYear = "2025"
|
||||
openCVversion = "4.10.0-3"
|
||||
joglVersion = "2.4.0"
|
||||
javalinVersion = "5.6.2"
|
||||
libcameraDriverVersion = "v2025.0.3"
|
||||
rknnVersion = "dev-v2025.0.0-1-g33b6263"
|
||||
|
||||
@@ -23,30 +23,41 @@ Ensure that your camera is calibrated and 3D mode is enabled. Navigate to the Ou
|
||||
By default, enabling multi-target will disable calculating camera-to-target transforms for each observed AprilTag target to increase performance; the X/Y/angle numbers shown in the target table of the UI are instead calculated using the tag's expected location (per the field layout JSON) and the field-to-camera transform calculated using MultiTag. If you additionally want the individual camera-to-target transform calculated using SolvePNP for each target, enable "Always Do Single-Target Estimation".
|
||||
:::
|
||||
|
||||
This multi-target pose estimate can be accessed using PhotonLib. We suggest using {ref}`the PhotonPoseEstimator class <docs/programming/photonlib/robot-pose-estimator:AprilTags and PhotonPoseEstimator>` with the `MULTI_TAG_PNP_ON_COPROCESSOR` strategy to simplify code, but the transform can be directly accessed using `getMultiTagResult`/`MultiTagResult()` (Java/C++).
|
||||
This multi-target pose estimate can be accessed using PhotonLib. We suggest using {ref}`the PhotonPoseEstimator class <docs/programming/photonlib/robot-pose-estimator:AprilTags and PhotonPoseEstimator>` with the `MULTI_TAG_PNP_ON_COPROCESSOR` strategy to simplify code, but the transform can be directly accessed using `getMultiTagResult`/`MultiTagResult()`/`multitagResult` (Java/C++/Python).
|
||||
|
||||
```{eval-rst}
|
||||
.. tab-set-code::
|
||||
|
||||
.. code-block:: Java
|
||||
|
||||
var result = camera.getLatestResult();
|
||||
if (result.getMultiTagResult().estimatedPose.isPresent) {
|
||||
Transform3d fieldToCamera = result.getMultiTagResult().estimatedPose.best;
|
||||
var results = camera.getAllUnreadResults();
|
||||
for (var result : results) {
|
||||
var multiTagResult = result.getMultiTagResult();
|
||||
if (multiTagResult.isPresent()) {
|
||||
var fieldToCamera = multiTagResult.get().estimatedPose.best;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.. code-block:: C++
|
||||
|
||||
auto result = camera.GetLatestResult();
|
||||
if (result.MultiTagResult().result.isPresent) {
|
||||
frc::Transform3d fieldToCamera = result.MultiTagResult().result.best;
|
||||
auto results = camera.GetAllUnreadResults();
|
||||
for (auto &result : results)
|
||||
{
|
||||
auto multiTagResult = result.MultiTagResult();
|
||||
if (multiTagResult.has_value()) {
|
||||
frc::Transform3d fieldToCamera = multiTagResult->estimatedPose.best;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.. code-block:: Python
|
||||
|
||||
# Coming Soon!
|
||||
|
||||
results = camera.getAllUnreadResults()
|
||||
for result in results:
|
||||
multitagResult = result.multitagResult
|
||||
if multitagResult is not None:
|
||||
fieldToCamera = multitagResult.estimatedPose.best
|
||||
```
|
||||
|
||||
:::{note}
|
||||
|
||||
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
8
docs/source/docs/camera-specific-configuration/index.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Camera-Specifc Configuration
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 2
|
||||
|
||||
arducam-cameras
|
||||
picamconfig
|
||||
```
|
||||
@@ -23,6 +23,7 @@ Windows may report "There is a problem with this drive". This should be ignored.
|
||||
Locate `config.txt` in the folder, and open it with your favorite text editor.
|
||||
|
||||
```{image} images/bootConfigTxt.png
|
||||
|
||||
```
|
||||
|
||||
Within the file, find this block of text:
|
||||
@@ -12,17 +12,7 @@ This section contains the build instructions from the source code available at [
|
||||
|
||||
**Node JS:**
|
||||
|
||||
The UI is written in Node JS. To compile the UI, Node 18.20.4 to Node 20.0.0 is required. To install Node JS follow the instructions for your platform [on the official Node JS website](https://nodejs.org/en/download/). However, modify this line
|
||||
|
||||
```bash
|
||||
nvm install 20
|
||||
```
|
||||
|
||||
so that it instead reads
|
||||
|
||||
```javascript
|
||||
nvm install 18.20.4
|
||||
```
|
||||
The UI is written in Node JS. To compile the UI, Node 22.15.0 is required. To install Node JS follow the instructions for your platform [on the official Node JS website](https://nodejs.org/en/download/).
|
||||
|
||||
## Compiling Instructions
|
||||
|
||||
@@ -283,3 +273,9 @@ Using the [GitHub CLI](https://cli.github.com/), we can download artifacts from
|
||||
```
|
||||
~/photonvision$ gh run download 11759699679 -n jar-Linux
|
||||
```
|
||||
|
||||
#### MacOS Builds
|
||||
|
||||
MacOS builds are not published to releases as MacOS is not an officially
|
||||
supported platform. However, MacOS builds are still available from the MacOS
|
||||
build action, which can be found [here](https://github.com/PhotonVision/photonvision/actions/workflows/build.yml).
|
||||
|
||||
@@ -11,7 +11,7 @@ PhotonVision has a myriad of advantages over similar solutions, including:
|
||||
|
||||
### Affordable
|
||||
|
||||
Compared to alternatives, PhotonVision is much cheaper to use (at the cost of your coprocessor and camera) compared to alternatives that cost \$400. This allows your team to save money while still being competitive.
|
||||
PhotonVision offers a more affordable solution to vision, with costs being from your coprocessor(s) and camera(s). Teams may choose to run multiple cameras from one coprocessor. This makes it a great solution for teams with limited budgets.
|
||||
|
||||
### Easy to Use User Interface
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Combining Aiming and Getting in Range
|
||||
|
||||
The following example is from the PhotonLib example repository ([Java](https://github.com/PhotonVision/photonvision/tree/main/photonlib-java-examples/aimandrange)/[C++](https://github.com/PhotonVision/photonvision/tree/main/photonlib-cpp-examples/aimandrange)).
|
||||
The following example is from the PhotonLib example repository ([Java](https://github.com/PhotonVision/photonvision/tree/main/photonlib-java-examples/aimandrange)/[C++](https://github.com/PhotonVision/photonvision/tree/main/photonlib-cpp-examples/aimandrange)/[Python](https://github.com/PhotonVision/photonvision/tree/main/photonlib-python-examples/aimandrange))
|
||||
|
||||
## Knowledge and Equipment Needed
|
||||
|
||||
@@ -10,7 +10,7 @@ The following example is from the PhotonLib example repository ([Java](https://g
|
||||
|
||||
Now that you know how to aim toward the AprilTag, let's also drive the correct distance from the AprilTag.
|
||||
|
||||
To do this, we'll use the *pitch* of the target in the camera image and trigonometry to figure out how far away the robot is from the AprilTag. Then, like before, we'll use the P term of a PID controller to drive the robot to the correct distance.
|
||||
To do this, we'll use the _pitch_ of the target in the camera image and trigonometry to figure out how far away the robot is from the AprilTag. Then, like before, we'll use the P term of a PID controller to drive the robot to the correct distance.
|
||||
|
||||
```{eval-rst}
|
||||
.. tab-set::
|
||||
@@ -43,8 +43,8 @@ To do this, we'll use the *pitch* of the target in the camera image and trigonom
|
||||
|
||||
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/abe95dfaa055bbe3609f72cfcaaba0f96ee7978c/photonlib-python-examples/aimandrange/robot.py
|
||||
:language: python
|
||||
:lines: 44-95
|
||||
:lines: 52-91
|
||||
:linenos:
|
||||
:lineno-start: 44
|
||||
:lineno-start: 52
|
||||
|
||||
```
|
||||
|
||||
@@ -4,6 +4,5 @@
|
||||
:maxdepth: 2
|
||||
|
||||
selecting-hardware
|
||||
picamconfig
|
||||
customhardware
|
||||
```
|
||||
|
||||
@@ -47,9 +47,17 @@ Before beginning, it is necessary to install the [rknn-toolkit2](https://github.
|
||||
PhotonVision currently ONLY supports 640x640 Ultralytics YOLOv5, YOLOv8, and YOLOv11 models trained and converted to `.rknn` format for RK3588 CPUs! Other models require different post-processing code and will NOT work. The model conversion process is also highly particular. Proceed with care.
|
||||
:::
|
||||
|
||||
:::{warning}
|
||||
Non-quantized models are not supported! If you have the option, make sure quantization is enabled when exporting to .rknn format. This will represent the weights and activations of the model as 8-bit integers, instead of 32-bit floats which PhotonVision doesn't support. Quantized models are also much faster.
|
||||
:::
|
||||
|
||||
In the settings, under `Device Control`, there's an option to upload a new object detection model. Naming convention
|
||||
should be `name-verticalResolution-horizontalResolution-yolovXXX`. The
|
||||
`name` should only include alphanumeric characters, periods, and underscores. Additionally, the labels
|
||||
file ought to have the same name as the RKNN file, with `-labels` appended to the end. For
|
||||
example, if the RKNN file is named `Algae_1.03.2025-640-640-yolov5s.rknn`, the labels file should be
|
||||
named `Algae_1.03.2025-640-640-yolov5s-labels.txt`.
|
||||
|
||||
:::{note}
|
||||
Currently there is no way to delete custom models in the GUI, though this is a planned feature. To do this, you have to SSH into the coprocessor and delete the files manually from `/opt/photonvision/photonvision_config/models`.
|
||||
:::
|
||||
|
||||
@@ -8,24 +8,38 @@ PhotonLibPy is a minimal, pure-python implementation of PhotonLib.
|
||||
|
||||
## Online Install - Java/C++
|
||||
|
||||
Click on the WPI icon on the top right of your VS Code window or hit Ctrl+Shift+P (Cmd+Shift+P on macOS) to bring up the command palette. Type, "Manage Vendor Libraries" and select the "WPILib: Manage Vendor Libraries" option. Then, select the "Install new library (online)" option.
|
||||
Click on the WPILib logo in the activity bar to access the Vendor Dependencies interface.
|
||||
|
||||
```{image} images/adding-offline-library.png
|
||||
```{image} images/wpilib-vendor-dependencies.png
|
||||
:scale: 50%
|
||||
:align: center
|
||||
:alt: WPILib Vendor Dependencies
|
||||
```
|
||||
|
||||
Paste the following URL into the box that pops up:
|
||||
Select the install button for the "PhotonLib" dependency.
|
||||
|
||||
`https://maven.photonvision.org/repository/internal/org/photonvision/photonlib-json/1.0/photonlib-json-1.0.json`
|
||||
```{image} images/photonlib-install.png
|
||||
:scale: 50%
|
||||
:align: center
|
||||
:alt: PhotonLib Install Button
|
||||
```
|
||||
|
||||
:::{note}
|
||||
It is recommended to Build Robot Code at least once when connected to the Internet before heading to an area where Internet connectivity is limited (for example, a competition). This ensures that the relevant files are downloaded to your filesystem.
|
||||
The Dependency Manager will automatically build your program when it loses focus. This allows you to use the changed dependencies.
|
||||
:::
|
||||
|
||||
When an update is available for PhotonLib, a "To Latest" button will become available. This will update the vendordep to the latest version of PhotonLib.
|
||||
|
||||
```{image} images/photonlib-to-latest.png
|
||||
:align: center
|
||||
:alt: PhotonLib Update Button
|
||||
```
|
||||
|
||||
Refer to [The WPILib docs](https://docs.wpilib.org/en/stable/docs/software/vscode-overview/3rd-party-libraries.html#installing-libraries) for more details on installing vendor libraries.
|
||||
|
||||
## Offline Install - Java/C++
|
||||
|
||||
Download the latest PhotonLib release from our GitHub releases page (named something like `` photonlib-VERSION.zip` ``), and extract the contents to `$HOME/wpilib/YEAR`. This adds PhotonLib maven artifacts to your local maven repository. PhotonLib will now also appear available in the "install vendor libraries (offline)" menu in WPILib VSCode. Refer to [the WPILib docs](https://docs.wpilib.org/en/stable/docs/software/vscode-overview/3rd-party-libraries.html#installing-libraries) for more details on installing vendor libraries offline.
|
||||
Download the latest PhotonLib release from our [GitHub releases page](https://github.com/PhotonVision/photonvision/releases) (named in the format `photonlib-VERSION.zip`), and extract the contents to `~/wpilib/YYYY/vendordeps` (where YYYY is the year and ~ is `C:\Users\Public` on Windows). This adds PhotonLib maven artifacts to your local maven repository. PhotonLib will now also appear available in the "install vendor libraries (offline)" menu in WPILib VSCode. Refer to [the WPILib docs](https://docs.wpilib.org/en/stable/docs/software/vscode-overview/3rd-party-libraries.html#how-does-it-work) for more details on installing vendor libraries offline.
|
||||
|
||||
## Install - Python
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ Each pipeline result has a `hasTargets()`/`HasTargets()` (Java and C++ respectiv
|
||||
```
|
||||
|
||||
:::{warning}
|
||||
In Java/C++, You must *always* check if the result has a target via `hasTargets()`/`HasTargets()` before getting targets or else you may get a null pointer exception. Further, you must use the same result in every subsequent call in that loop.
|
||||
In Java/C++, You must _always_ check if the result has a target via `hasTargets()`/`HasTargets()` before getting targets or else you may get a null pointer exception. Further, you must use the same result in every subsequent call in that loop.
|
||||
:::
|
||||
|
||||
## Getting a List of Targets
|
||||
@@ -140,7 +140,7 @@ You can get the {ref}`best target <docs/reflectiveAndShape/contour-filtering:Con
|
||||
|
||||
## Getting Data From A Target
|
||||
|
||||
- double `getYaw()`/`GetYaw()`: The yaw of the target in degrees (positive right).
|
||||
- double `getYaw()`/`GetYaw()`: The yaw of the target in degrees (positive left).
|
||||
- double `getPitch()`/`GetPitch()`: The pitch of the target in degrees (positive up).
|
||||
- double `getArea()`/`GetArea()`: The area (how much of the camera feed the bounding box takes up) as a percent (0-100).
|
||||
- double `getSkew()`/`GetSkew()`: The skew of the target in degrees (counter-clockwise positive).
|
||||
|
||||
|
Before Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 98 KiB |
@@ -4,30 +4,46 @@
|
||||
For more information on how to methods to get AprilTag data, look {ref}`here <docs/programming/photonlib/getting-target-data:Getting AprilTag Data From A Target>`.
|
||||
:::
|
||||
|
||||
PhotonLib includes a `PhotonPoseEstimator` class, which allows you to combine the pose data from all tags in view in order to get a field relative pose. The `PhotonPoseEstimator` class works with one camera per object instance, but more than one instance may be created.
|
||||
PhotonLib includes a `PhotonPoseEstimator` class, which allows you to combine the pose data from all tags in view in order to get a field relative pose. For each camera, a separate instance of the `PhotonPoseEstimator` class should be created.
|
||||
|
||||
## Creating an `AprilTagFieldLayout`
|
||||
|
||||
`AprilTagFieldLayout` is used to represent a layout of AprilTags within a space (field, shop at home, classroom, etc.). WPILib provides a JSON that describes the layout of AprilTags on the field which you can then use in the AprilTagFieldLayout constructor. You can also specify a custom layout.
|
||||
|
||||
The API documentation can be found in here: [Java](https://github.wpilib.org/allwpilib/docs/release/java/edu/wpi/first/apriltag/AprilTagFieldLayout.html) and [C++](https://github.wpilib.org/allwpilib/docs/release/cpp/classfrc_1_1_april_tag_field_layout.html).
|
||||
The API documentation can be found in here: [Java](https://github.wpilib.org/allwpilib/docs/release/java/edu/wpi/first/apriltag/AprilTagFieldLayout.html), [C++](https://github.wpilib.org/allwpilib/docs/release/cpp/classfrc_1_1_april_tag_field_layout.html), and [Python](https://robotpy.readthedocs.io/projects/apriltag/en/stable/robotpy_apriltag/AprilTagFieldLayout.html#robotpy_apriltag.AprilTagFieldLayout).
|
||||
|
||||
```{eval-rst}
|
||||
.. tab-set-code::
|
||||
.. code-block:: Java
|
||||
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/refs/heads/main/photonlib-java-examples/poseest/src/main/java/frc/robot/Constants.java
|
||||
:language: java
|
||||
:lines: 48-49
|
||||
|
||||
// The field from AprilTagFields will be different depending on the game.
|
||||
AprilTagFieldLayout aprilTagFieldLayout = AprilTagFieldLayout.loadField(AprilTagFields.kDefaultField);
|
||||
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/refs/heads/main/photonlib-cpp-examples/poseest/src/main/include/Constants.h
|
||||
:language: c++
|
||||
:lines: 46-47
|
||||
|
||||
.. code-block:: C++
|
||||
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/refs/heads/main/photonlib-python-examples/poseest/robot.py
|
||||
:language: python
|
||||
:lines: 46
|
||||
```
|
||||
|
||||
// The parameter for LoadAPrilTagLayoutField will be different depending on the game.
|
||||
frc::AprilTagFieldLayout aprilTagFieldLayout = frc::LoadAprilTagLayoutField(frc::AprilTagField::kDefaultField);
|
||||
## Defining the Robot to Camera `Transform3d`
|
||||
|
||||
.. code-block:: Python
|
||||
Another necessary argument for creating a `PhotonPoseEstimator` is the `Transform3d` representing the robot-relative location and orientation of the camera. A `Transform3d` contains a `Translation3d` and a `Rotation3d`. The `Translation3d` is created in meters and the `Rotation3d` is created with radians. For more information on the coordinate system, please see the {ref}`Coordinate Systems <docs/apriltag-pipelines/coordinate-systems:Coordinate Systems>` documentation.
|
||||
|
||||
# Coming Soon!
|
||||
```{eval-rst}
|
||||
.. tab-set-code::
|
||||
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/refs/heads/main/photonlib-java-examples/poseest/src/main/java/frc/robot/Constants.java
|
||||
:language: java
|
||||
:lines: 44-45
|
||||
|
||||
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/refs/heads/main/photonlib-cpp-examples/poseest/src/main/include/Constants.h
|
||||
:language: c++
|
||||
:lines: 43-45
|
||||
|
||||
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/refs/heads/main/photonlib-python-examples/poseest/robot.py
|
||||
:language: python
|
||||
:lines: 33-36
|
||||
```
|
||||
|
||||
## Creating a `PhotonPoseEstimator`
|
||||
@@ -35,127 +51,116 @@ The API documentation can be found in here: [Java](https://github.wpilib.org/all
|
||||
The PhotonPoseEstimator has a constructor that takes an `AprilTagFieldLayout` (see above), `PoseStrategy`, `PhotonCamera`, and `Transform3d`. `PoseStrategy` has nine possible values:
|
||||
|
||||
- MULTI_TAG_PNP_ON_COPROCESSOR
|
||||
- Calculates a new robot position estimate by combining all visible tag corners. Recommended for all teams as it will be the most accurate.
|
||||
- Must configure the AprilTagFieldLayout properly in the UI, please see {ref}`here <docs/apriltag-pipelines/multitag:multitag localization>` for more information.
|
||||
- Calculates a new robot position estimate by combining all visible tag corners. Recommended for all teams as it will be the most accurate.
|
||||
- Must configure the AprilTagFieldLayout properly in the UI, please see {ref}`here <docs/apriltag-pipelines/multitag:multitag localization>` for more information.
|
||||
- LOWEST_AMBIGUITY
|
||||
- Choose the Pose with the lowest ambiguity.
|
||||
- Choose the Pose with the lowest ambiguity.
|
||||
- CLOSEST_TO_CAMERA_HEIGHT
|
||||
- Choose the Pose which is closest to the camera height.
|
||||
- Choose the Pose which is closest to the camera height.
|
||||
- CLOSEST_TO_REFERENCE_POSE
|
||||
- Choose the Pose which is closest to the pose from setReferencePose().
|
||||
- Choose the Pose which is closest to the pose from setReferencePose().
|
||||
- CLOSEST_TO_LAST_POSE
|
||||
- Choose the Pose which is closest to the last pose calculated.
|
||||
- Choose the Pose which is closest to the last pose calculated.
|
||||
- AVERAGE_BEST_TARGETS
|
||||
- Choose the Pose which is the average of all the poses from each tag.
|
||||
- Choose the Pose which is the average of all the poses from each tag.
|
||||
- MULTI_TAG_PNP_ON_RIO
|
||||
- A slower, older version of MULTI_TAG_PNP_ON_COPROCESSOR, not recommended for use.
|
||||
- A slower, older version of MULTI_TAG_PNP_ON_COPROCESSOR, not recommended for use.
|
||||
- PNP_DISTANCE_TRIG_SOLVE
|
||||
- Use distance data from best visible tag to compute a Pose. This runs on the RoboRIO in order
|
||||
to access the robot's yaw heading, and MUST have addHeadingData called every frame so heading
|
||||
data is up-to-date. Based on a reference implementation by [FRC Team 6328 Mechanical Advantage](https://www.chiefdelphi.com/t/frc-6328-mechanical-advantage-2025-build-thread/477314/98).
|
||||
- Use distance data from best visible tag to compute a Pose. This runs on the RoboRIO in order
|
||||
to access the robot's yaw heading, and MUST have addHeadingData called every frame so heading
|
||||
data is up-to-date. Based on a reference implementation by [FRC Team 6328 Mechanical Advantage](https://www.chiefdelphi.com/t/frc-6328-mechanical-advantage-2025-build-thread/477314/98).
|
||||
- CONSTRAINED_SOLVEPNP
|
||||
- Solve a constrained version of the Perspective-n-Point problem with the robot's drivebase
|
||||
flat on the floor. This computation takes place on the RoboRIO, and should not take more than 2ms.
|
||||
This also requires addHeadingData to be called every frame so heading data is up to date.
|
||||
If Multi-Tag PNP is enabled on the coprocessor, it will be used to provide an initial seed to
|
||||
the optimization algorithm -- otherwise, the multi-tag fallback strategy will be used as the
|
||||
seed.
|
||||
- Solve a constrained version of the Perspective-n-Point problem with the robot's drivebase
|
||||
flat on the floor. This computation takes place on the RoboRIO, and should not take more than 2ms.
|
||||
This also requires addHeadingData to be called every frame so heading data is up to date.
|
||||
If Multi-Tag PNP is enabled on the coprocessor, it will be used to provide an initial seed to
|
||||
the optimization algorithm -- otherwise, the multi-tag fallback strategy will be used as the
|
||||
seed.
|
||||
|
||||
```{eval-rst}
|
||||
.. tab-set-code::
|
||||
.. code-block:: Java
|
||||
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/refs/heads/main/photonlib-java-examples/poseest/src/main/java/frc/robot/Vision.java
|
||||
:language: java
|
||||
:lines: 65-66
|
||||
|
||||
//Forward Camera
|
||||
cam = new PhotonCamera("testCamera");
|
||||
Transform3d robotToCam = new Transform3d(new Translation3d(0.5, 0.0, 0.5), new Rotation3d(0,0,0)); //Cam mounted facing forward, half a meter forward of center, half a meter up from center.
|
||||
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/refs/heads/main/photonlib-cpp-examples/poseest/src/main/include/Vision.h
|
||||
:language: c++
|
||||
:lines: 150-153
|
||||
|
||||
// Construct PhotonPoseEstimator
|
||||
PhotonPoseEstimator photonPoseEstimator = new PhotonPoseEstimator(aprilTagFieldLayout, PoseStrategy.CLOSEST_TO_REFERENCE_POSE, cam, robotToCam);
|
||||
|
||||
.. code-block:: C++
|
||||
|
||||
// Forward Camera
|
||||
std::shared_ptr<photonlib::PhotonCamera> cameraOne =
|
||||
std::make_shared<photonlib::PhotonCamera>("testCamera");
|
||||
// Camera is mounted facing forward, half a meter forward of center, half a
|
||||
// meter up from center.
|
||||
frc::Transform3d robotToCam =
|
||||
frc::Transform3d(frc::Translation3d(0.5_m, 0_m, 0.5_m),
|
||||
frc::Rotation3d(0_rad, 0_rad, 0_rad));
|
||||
|
||||
// ... Add other cameras here
|
||||
|
||||
// Assemble the list of cameras & mount locations
|
||||
std::vector<
|
||||
std::pair<std::shared_ptr<photonlib::PhotonCamera>, frc::Transform3d>>
|
||||
cameras;
|
||||
cameras.push_back(std::make_pair(cameraOne, robotToCam));
|
||||
|
||||
photonlib::RobotPoseEstimator estimator(
|
||||
aprilTags, photonlib::CLOSEST_TO_REFERENCE_POSE, cameras);
|
||||
|
||||
.. code-block:: Python
|
||||
|
||||
kRobotToCam = wpimath.geometry.Transform3d(
|
||||
wpimath.geometry.Translation3d(0.5, 0.0, 0.5),
|
||||
wpimath.geometry.Rotation3d.fromDegrees(0.0, -30.0, 0.0),
|
||||
)
|
||||
|
||||
self.cam = PhotonCamera("YOUR CAMERA NAME")
|
||||
|
||||
self.camPoseEst = PhotonPoseEstimator(
|
||||
loadAprilTagLayoutField(AprilTagField.kDefaultField),
|
||||
PoseStrategy.CLOSEST_TO_REFERENCE_POSE,
|
||||
self.cam,
|
||||
kRobotToCam
|
||||
)
|
||||
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/refs/heads/main/photonlib-python-examples/poseest/robot.py
|
||||
:language: python
|
||||
:lines: 45-50
|
||||
```
|
||||
|
||||
:::{note}
|
||||
Python still takes a `PhotonCamera` in the constructor, so you must create the camera as shown in the next section and then return and use it to create the `PhotonPoseEstimator`.
|
||||
:::
|
||||
|
||||
## Using a `PhotonPoseEstimator`
|
||||
|
||||
Calling `update()` on your `PhotonPoseEstimator` will return an `EstimatedRobotPose`, which includes a `Pose3d` of the latest estimated pose (using the selected strategy) along with a `double` of the timestamp when the robot pose was estimated. You should be updating your [drivetrain pose estimator](https://docs.wpilib.org/en/latest/docs/software/advanced-controls/state-space/state-space-pose-estimators.html) with the result from the `PhotonPoseEstimator` every loop using `addVisionMeasurement()`.
|
||||
The final prerequisite to using your `PhotonPoseEstimator` is creating a `PhotonCamera`. To do this, you must set the name of your camera in Photon Client. From there you can define the camera in code.
|
||||
|
||||
```{eval-rst}
|
||||
.. tab-set-code::
|
||||
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/357d8a518a93f7a1f8084a79449249e613b605a7/photonlib-java-examples/apriltagExample/src/main/java/frc/robot/PhotonCameraWrapper.java
|
||||
:language: java
|
||||
:lines: 85-88
|
||||
|
||||
.. code-block:: C++
|
||||
|
||||
std::pair<frc::Pose2d, units::millisecond_t> getEstimatedGlobalPose(
|
||||
frc::Pose3d prevEstimatedRobotPose) {
|
||||
robotPoseEstimator.SetReferencePose(prevEstimatedRobotPose);
|
||||
units::millisecond_t currentTime = frc::Timer::GetFPGATimestamp();
|
||||
auto result = robotPoseEstimator.Update();
|
||||
if (result.second) {
|
||||
return std::make_pair<>(result.first.ToPose2d(),
|
||||
currentTime - result.second);
|
||||
} else {
|
||||
return std::make_pair(frc::Pose2d(), 0_ms);
|
||||
}
|
||||
}
|
||||
|
||||
.. code-block:: Python
|
||||
|
||||
# Coming Soon!
|
||||
|
||||
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/refs/heads/main/photonlib-java-examples/poseest/src/main/java/frc/robot/Vision.java
|
||||
:language: java
|
||||
:lines: 63
|
||||
|
||||
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/refs/heads/main/photonlib-cpp-examples/aimattarget/src/main/include/Robot.h
|
||||
:language: c++
|
||||
:lines: 55
|
||||
|
||||
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/refs/heads/main/photonlib-python-examples/poseest/robot.py
|
||||
:language: python
|
||||
:lines: 44
|
||||
```
|
||||
|
||||
You should be updating your [drivetrain pose estimator](https://docs.wpilib.org/en/latest/docs/software/advanced-controls/state-space/state-space-pose-estimators.html) with the result from the `RobotPoseEstimator` every loop using `addVisionMeasurement()`. TODO: add example note
|
||||
Calling `update()` on your `PhotonPoseEstimator` will return an `EstimatedRobotPose`, which includes a `Pose3d` of the latest estimated pose (using the selected strategy) along with a `double` of the timestamp when the robot pose was estimated.
|
||||
|
||||
```{eval-rst}
|
||||
.. tab-set-code::
|
||||
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/refs/heads/main/photonlib-java-examples/poseest/src/main/java/frc/robot/Vision.java
|
||||
:language: java
|
||||
:lines: 93-116
|
||||
|
||||
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/refs/heads/main/photonlib-cpp-examples/poseest/src/main/include/Vision.h
|
||||
:language: c++
|
||||
:lines: 80-100
|
||||
|
||||
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/refs/heads/main/photonlib-python-examples/poseest/robot.py
|
||||
:language: python
|
||||
:lines: 53
|
||||
```
|
||||
|
||||
You should be updating your [drivetrain pose estimator](https://docs.wpilib.org/en/latest/docs/software/advanced-controls/state-space/state-space-pose-estimators.html) with the result from the `PhotonPoseEstimator` every loop using `addVisionMeasurement()`.
|
||||
|
||||
```{eval-rst}
|
||||
.. tab-set-code::
|
||||
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/refs/heads/main/photonlib-java-examples/poseest/src/main/java/frc/robot/Robot.java
|
||||
:language: java
|
||||
:lines: 49
|
||||
|
||||
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/refs/heads/main/photonlib-cpp-examples/poseest/src/main/include/Robot.h
|
||||
:language: c++
|
||||
:lines: 54-57
|
||||
|
||||
.. rli:: https://raw.githubusercontent.com/PhotonVision/photonvision/refs/heads/main/photonlib-python-examples/poseest/robot.py
|
||||
:language: python
|
||||
:lines: 54-57
|
||||
```
|
||||
|
||||
## Complete Examples
|
||||
|
||||
The complete examples for the `PhotonPoseEstimator` can be found in the following locations:
|
||||
|
||||
- [Java](https://github.com/PhotonVision/photonvision/tree/main/photonlib-java-examples/poseest)
|
||||
- [C++](https://github.com/PhotonVision/photonvision/tree/main/photonlib-cpp-examples/poseest)
|
||||
- [Python](https://github.com/PhotonVision/photonvision/tree/main/photonlib-python-examples/poseest)
|
||||
|
||||
## Additional `PhotonPoseEstimator` Methods
|
||||
|
||||
### `setReferencePose(Pose3d referencePose)`
|
||||
For more information on the `PhotonPoseEstimator` class, please see the API documentation.
|
||||
|
||||
Updates the stored reference pose when using the CLOSEST_TO_REFERENCE_POSE strategy.
|
||||
|
||||
### `setLastPose(Pose3d lastPose)`
|
||||
|
||||
Update the stored last pose. Useful for setting the initial estimate when using the CLOSEST_TO_LAST_POSE strategy.
|
||||
|
||||
### `addHeadingData(double timestampSeconds, Rotation2d heading)`
|
||||
|
||||
Adds robot heading data to be stored in buffer. Must be called periodically with a proper timestamp for the PNP_DISTANCE_TRIG_SOLVE and CONSTRAINED_SOLVEPNP strategies
|
||||
- [Java Documentation](https://javadocs.photonvision.org/release/org/photonvision/PhotonPoseEstimator.html)
|
||||
- [C++ Documentation](https://cppdocs.photonvision.org/release/classphoton_1_1_photon_pose_estimator.html)
|
||||
- [Python Documentation](https://pydocs.photonvision.org/release/reference/photonPoseEstimator/)
|
||||
|
||||
41
docs/source/docs/quick-start/camera-matching.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Camera Matching
|
||||
|
||||
## Activating and Deactivating Cameras
|
||||
|
||||
When you first plug in a camera, it will be detected and added to the list of cameras with the "Unassigned" status, as shown below. You can press the "Activate" button to enable PhotonVision to use the camera.
|
||||
|
||||
```{image} images/camera-matching/unassigned-camera.png
|
||||
:scale: 50%
|
||||
```
|
||||
|
||||
If a camera has been activated in the past, it will be listed as "Deactivated" in the camera list. You can press the "Activate" button to enable PhotonVision to use the camera.
|
||||
|
||||
```{image} images/camera-matching/deactivated-camera.png
|
||||
:scale: 50%
|
||||
```
|
||||
|
||||
Once a camera is activated, it will be listed as "Active" in the camera list. You can press the "Deactivate" button to stop PhotonVision from using the camera.
|
||||
|
||||
```{image} images/camera-matching/activated-camera.png
|
||||
:scale: 50%
|
||||
```
|
||||
|
||||
## Deleting Cameras
|
||||
|
||||
If you want to remove a camera from the list, you can press the delete button. This will clear all settings for that particular camera, including the calibration data and any other settings you have configured. It is recommended to make a backup of the camera's settings before deleting it, as this action cannot be undone.
|
||||
|
||||
## Matching Cameras
|
||||
|
||||
When you plug in a camera, PhotonVision will attempt to match it to a previously configured camera based on the physical USB port it is connected to. If you plug another camera into that port, the cameras will have a "Camera Mismatch" status, indicating that the camera is not recognized as the one that was previously configured.
|
||||
|
||||
Additionally, pressing on the Details button will show you the details of the camera mismatch, allowing you to compare the current camera with the previously configured camera.
|
||||
|
||||
```{image} images/camera-matching/camera-mismatch-details.png
|
||||
:scale: 50%
|
||||
```
|
||||
|
||||
```{note}
|
||||
Camera matching is based on the USB ports on the device. If you unplug a camera and plug it into a different port, PhotonVision will attempt to use settings from the camera that was previously configured in that port, causing unexpected behavior.
|
||||
```
|
||||
|
||||
To resolve the camera mismatch, you should ensure each camera is plugged into the same port that you configured it in.
|
||||
@@ -34,7 +34,7 @@ Innomaker and Arducam are common manufacturers of hardware designed specifically
|
||||
- Driver Camera
|
||||
- OV9281
|
||||
- OV9782
|
||||
- Pi Camera Module V1 {ref}`(More setup info)<docs/hardware/picamconfig:Pi Camera Configuration>`
|
||||
- Pi Camera Module V1 {ref}`(More setup info)<docs/camera-specific-configuration/picamconfig:Pi Camera Configuration>`
|
||||
|
||||
Feel free to get started with any color webcam you have sitting around.
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 287 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 212 KiB |
|
After Width: | Height: | Size: 83 KiB |
@@ -7,7 +7,7 @@ common-setups
|
||||
quick-install
|
||||
wiring
|
||||
networking
|
||||
arducam-cameras
|
||||
camera-matching
|
||||
camera-calibration
|
||||
quick-configure
|
||||
```
|
||||
|
||||
@@ -54,13 +54,13 @@ Only use a static IP when connected to the **robot radio**, and never when testi
|
||||
5. Change your IP to Static.
|
||||
6. Set your coprocessor's IP address to “10.TE.AM.11”. More information on IP format can be found [here](https://docs.wpilib.org/en/stable/docs/networking/networking-introduction/ip-configurations.html#on-the-field-static-configuration).
|
||||
7. Click the “Save” button.
|
||||
8. Set your roboRIO to the following static IP address: “10.TE.AM.2”. This can be done via the [roboRIO web dashboard](https://docs.wpilib.org/en/stable/docs/software/roborio-info/roborio-web-dashboard.html#roborio-web-dashboard).
|
||||
|
||||
Power-cycle your robot and then you will now be access the PhotonVision dashboard at `10.TE.AM.11:5800`.
|
||||
|
||||
```{image} images/static.png
|
||||
:alt: Correctly set static IP
|
||||
```
|
||||
|
||||
The "team number" field will accept (in addition to a team number) an IP address or hostname. This is useful for testing PhotonVision on the same computer as a simulated robot program;
|
||||
you can set the team number to "localhost", and PhotonVision will send data to the network tables in the simulated robot.
|
||||
|
||||
|
||||
@@ -78,7 +78,11 @@ This diagram shows how to use the recommended regulator to power a coprocessor.
|
||||
|
||||
Pigtails can be purchased from many sources we recommend [(USB C)](https://ctr-electronics.com/products/usb-type-c-wire-breakout?_pos=19&_sid=bf06b6a6b&_ss=r) [(Micro USB)](https://ctr-electronics.com/products/usb-micro-power-wire-breakout?pr_prod_strat=e5_desc&pr_rec_id=10bf36ce7&pr_rec_pid=7863771070637&pr_ref_pid=7863771103405&pr_seq=uniform)
|
||||
|
||||
## Coprocessor with Passive POE (Pi with SnakeEyes and Limelight)
|
||||
## Limelight
|
||||
|
||||
Follow the wiring instructions located in the [Limelight Documentation](https://docs.limelightvision.io/) for your Limelight model.
|
||||
|
||||
## Coprocessor with Passive POE (Pi with SnakeEyes)
|
||||
|
||||
1. Plug the [passive POE injector](https://www.revrobotics.com/rev-11-1210/) into the coprocessor and wire it to PDP/PDH (NOT the VRM).
|
||||
2. Add a breaker to relevant slot in your PDP/PDH
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Pi Cameras
|
||||
|
||||
If you haven't yet, please refer to {ref}`the Pi CSI Camera Configuration page <docs/hardware/picamconfig:Pi Camera Configuration>` for information on updating {code}`config.txt` for your use case. If you've tried that, and things still aren't working, restart PhotonVision using the restart button in the settings tab, and press tilde (\`) in the web UI once connection is restored. This should show the most recent boot log.
|
||||
If you haven't yet, please refer to {ref}`the Pi CSI Camera Configuration page <docs/camera-specific-configuration/picamconfig:Pi Camera Configuration>` for information on updating {code}`config.txt` for your use case. If you've tried that, and things still aren't working, restart PhotonVision using the restart button in the settings tab, and press tilde (\`) in the web UI once connection is restored. This should show the most recent boot log.
|
||||
|
||||
| | Expected output | Bad |
|
||||
| ------------------------------- | ----------------------------------------------------- | ---------------------------------- |
|
||||
@@ -12,7 +12,7 @@ If you haven't yet, please refer to {ref}`the Pi CSI Camera Configuration page <
|
||||
|
||||
If the driver isn't loaded, you may be using a non-official Pi image, or an image not new enough. Try updating to the most recent image available (one released for 2023) -- if that doesn't resolve the problem, {ref}`contact us<index:Contact Us>` with your settings ZIP file and Pi version/camera version/config.txt file used.
|
||||
|
||||
If the camera is not detected, the most likely cause is either a config.txt file incorrectly set-up, or a ribbon cable attached backwards. Review the {ref}`picam configuration page <docs/hardware/picamconfig:pi camera configuration>`, and verify the ribbon cable is properly oriented at both ends, and that it is \_fully\_ inserted into the FFC connector. Then, {ref}`contact us<index:Contact Us>` with your settings ZIP file and Pi version/camera version/config.txt file used.
|
||||
If the camera is not detected, the most likely cause is either a config.txt file incorrectly set-up, or a ribbon cable attached backwards. Review the {ref}`picam configuration page <docs/camera-specific-configuration/picamconfig:pi camera configuration>`, and verify the ribbon cable is properly oriented at both ends, and that it is _fully_ inserted into the FFC connector. Then, {ref}`contact us<index:Contact Us>` with your settings ZIP file and Pi version/camera version/config.txt file used.
|
||||
|
||||
## USB cameras
|
||||
|
||||
@@ -27,7 +27,7 @@ USB cameras supported by CSCore require no libcamera driver initialization to wo
|
||||
|
||||
On Linux devices (including Raspberry Pi), PhotonVision uses WPILib's CSCore to interact with video devices, which internally uses Video4Linux (v4l2). CSCore, and therefore Photon, requires that cameras attached have good v4l drivers for proper functionality. These should be built into the Linux kernel, and do not need to be installed manually. Valid picamera setup (from /boot/config.txt) can also be determined using these steps. The list-devices command will show all valid video devices detected, and list-formats the list of "video modes" each camera can be in.
|
||||
|
||||
- For picams: edit the config.txt file as described in the {ref}`picam configuration page <docs/hardware/picamconfig:pi camera configuration>`
|
||||
- For picams: edit the config.txt file as described in the {ref}`picam configuration page <docs/camera-specific-configuration/picamconfig:pi camera configuration>`
|
||||
- SSH into your Pi: {code}`ssh pi@photonvision.local` and enter the username "pi" & password "raspberry"
|
||||
- run {code}`v4l2-ctl --list-devices` and {code}`v4l2-ctl --list-formats`
|
||||
|
||||
|
||||
@@ -7,4 +7,5 @@ common-errors
|
||||
logging
|
||||
camera-troubleshooting
|
||||
networking-troubleshooting
|
||||
unix-commands
|
||||
```
|
||||
|
||||
@@ -20,3 +20,21 @@ Logs are stored inside the {code}`photonvision_config/logs` directory. Exporting
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
```
|
||||
|
||||
Robot mode transitions are also recorded in program logs. These transition messages look something like the two shown below, and show the contents of the [HAL Control Word](https://github.wpilib.org/allwpilib/docs/release/java/edu/wpi/first/hal/ControlWord.html) that the robot was in previously, and what it is now in. This includes:
|
||||
- Enabled state
|
||||
- Robot state (autonomous vs teleoperated)
|
||||
- If the robot e-stop is active
|
||||
|
||||
If the robot is connected to the FMS at an event, we will additionally print out:
|
||||
- Event name
|
||||
- Match type and number
|
||||
- Driver station position
|
||||
|
||||
|
||||
```
|
||||
[2025-04-19 19:52:08] [NetworkTables - NTDriverStation] [INFO] ROBOT TRANSITIONED MODES! From NtControlWord[m_enabled=true, m_autonomous=false, m_test=false, m_emergencyStop=false, m_fmsAttached=true, m_dsAttached=true] to NtControlWord[m_enabled=true, m_autonomous=false, m_test=true, m_emergencyStop=false, m_fmsAttached=true, m_dsAttached=true]
|
||||
|
||||
[2025-04-19 19:52:09] [NetworkTables - NTDriverStation] [INFO] ROBOT TRANSITIONED MODES! From NtControlWord[m_enabled=true, m_autonomous=false, m_test=true, m_emergencyStop=false, m_fmsAttached=true, m_dsAttached=true] to NtControlWord[m_enabled=false, m_autonomous=false, m_test=false, m_emergencyStop=false, m_fmsAttached=false, m_dsAttached=false]
|
||||
[2025-04-19 19:52:19] [NetworkTables - NTDriverStation] [INFO] ROBOT TRANSITIONED MODES! From NtControlWord[m_enabled=false, m_autonomous=false, m_test=false, m_emergencyStop=false, m_fmsAttached=false, m_dsAttached=false] to NtControlWord[m_enabled=true, m_autonomous=true, m_test=false, m_emergencyStop=false, m_fmsAttached=true, m_dsAttached=true]
|
||||
```
|
||||
|
||||
132
docs/source/docs/troubleshooting/unix-commands.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# Useful Unix Commands
|
||||
|
||||
## Networking
|
||||
|
||||
### SSH
|
||||
|
||||
[SSH (Secure Shell)](https://www.mankier.com/1/ssh) is used to securely connect from a local to a remote system (ex. from a laptop to a coprocessor). Unlike other commands on this page, ssh is not Unix specific and can be done on Windows and MacOS from their respective terminals.
|
||||
|
||||
:::{note}
|
||||
You may see a warning similar to `The authenticity of host 'xxx' can't be established...` or `WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!`, in most cases this can be safely ignored if you have confirmed that you are connecting to the correct host over a secure connection, and the fingerprint will change when your operating system is reinstalled or PhotonVision's coprocessor image is re-flashed. This can also occur if you have multiple coprocessors with the same hostname on your network. You can read more about it [here](https://superuser.com/questions/421997/what-is-a-ssh-key-fingerprint-and-how-is-it-generated)
|
||||
:::
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
ssh pi@hostname
|
||||
```
|
||||
|
||||
For PhotonVision, the username will be `pi` and the password will be `raspberry`.
|
||||
|
||||
### ip
|
||||
|
||||
Run [ip address](https://www.mankier.com/8/ip) with your coprocessor connected to a monitor in order to see its IP address and other network configuration information.
|
||||
|
||||
Your output might look something like this:
|
||||
|
||||
```
|
||||
2: end1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
|
||||
link/ether de:9a:8f:7d:31:aa brd ff:ff:ff:ff:ff:ff
|
||||
inet 10.88.47.12/24 brd 10.88.47.255 scope global dynamic noprefixroute end1
|
||||
valid_lft 27367sec preferred_lft 27367sec
|
||||
```
|
||||
|
||||
In this example, the numbers following `inet` (10.88.47.12) are your IP address.
|
||||
|
||||
### ping
|
||||
|
||||
[ping](https://www.mankier.com/8/ping) is a command-line utility used to test the reachability of a host on an IP network. It also measures the round-trip time for messages sent from the originating host to a destination computer. It can be used to determine if a network interface is available, which can be helpful when debugging.
|
||||
|
||||
## File Transfer
|
||||
|
||||
All files under `/opt/photonvision` are owned by the root user. This means that if you want to modify them, the commands to do so must be ran as sudo.
|
||||
|
||||
### SCP
|
||||
|
||||
[SCP (Secure Copy)](https://www.mankier.com/1/scp) is used to securely transfer files between local and remote systems.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
scp [file] pi@hostname:/path/to/destination
|
||||
```
|
||||
|
||||
### SFTP
|
||||
|
||||
[SFTP (SSH File Transfer Protocol)](https://www.mankier.com/1/sftp#) is another option for transferring files between local and remote systems.
|
||||
|
||||
### Filezilla
|
||||
|
||||
[Filezilla](https://filezilla-project.org/) is a GUI alternative to SCP and SFTP. It is available for Windows, MacOS, and Linux.
|
||||
|
||||
## Miscellaneous
|
||||
|
||||
### v4l2-ctl
|
||||
|
||||
[v4l2-ctl](https://www.mankier.com/1/v4l2-ctl) is a command-line tool for controlling video devices.
|
||||
|
||||
List available video devices (used to verify the device recognized a connected camera):
|
||||
|
||||
```
|
||||
v4l2-ctl --list-devices
|
||||
```
|
||||
|
||||
List supported formats and resolutions for a specific video device:
|
||||
|
||||
```
|
||||
v4l2-ctl --list-formats-ext --device /path/to/video_device
|
||||
```
|
||||
|
||||
List all video device's controls and their values:
|
||||
|
||||
```
|
||||
v4l2-ctl --list-ctrls --device path/to/video_device
|
||||
```
|
||||
|
||||
:::{note}
|
||||
This command is especially useful in helping to debug when certain camera controls, like exposure, aren't behaving as expected. If you see an error in the logs similar to `WARNING 30: failed to set property [property name] (UsbCameraImpl.cpp:646)`, that means that PhotonVision is trying to use a control that doesn't exist or has a different name on your hardware. If you encounter this issue, please [file an issue](https://github.com/PhotonVision/photonvision/issues) with the necessary logs and output of the `v4l2-ctl --list-ctrls` command.
|
||||
:::
|
||||
|
||||
### systemctl
|
||||
|
||||
[systemctl](https://www.mankier.com/1/systemctl) is a command that controls the `systemd` system and service manager.
|
||||
|
||||
Start PhotonVision:
|
||||
|
||||
```
|
||||
systemctl start photonvision
|
||||
```
|
||||
|
||||
Stop PhotonVision:
|
||||
|
||||
```
|
||||
systemctl stop photonvision
|
||||
```
|
||||
|
||||
Restart PhotonVision:
|
||||
|
||||
```
|
||||
systemctl restart photonvision
|
||||
```
|
||||
|
||||
Check the status of PhotonVision:
|
||||
|
||||
```
|
||||
systemctl status photonvision
|
||||
```
|
||||
|
||||
### journalctl
|
||||
|
||||
[journalctl](https://www.mankier.com/1/journalctl) is a command that queries the systemd journal, which is a logging system used by many Linux distributions.
|
||||
|
||||
View the PhotonVision logs:
|
||||
|
||||
```
|
||||
journalctl -u photonvision
|
||||
```
|
||||
|
||||
View the PhotonVision logs in real-time:
|
||||
|
||||
```
|
||||
journalctl -u photonvision -f
|
||||
```
|
||||
@@ -91,6 +91,7 @@ docs/description
|
||||
docs/quick-start/index
|
||||
docs/hardware/index
|
||||
docs/advanced-installation/index
|
||||
docs/camera-specific-configuration/index
|
||||
```
|
||||
|
||||
```{toctree}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": [
|
||||
"plugin:vue/vue3-recommended",
|
||||
"eslint:recommended",
|
||||
"@vue/eslint-config-typescript",
|
||||
"@vue/eslint-config-prettier/skip-formatting"
|
||||
],
|
||||
"rules": {
|
||||
"quotes": ["error", "double"],
|
||||
"comma-dangle": ["error", "never"],
|
||||
"comma-spacing": ["error", { "before": false, "after": true }],
|
||||
"semi": ["error", "always"],
|
||||
"eol-last": "error",
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"quote-props": ["error", "as-needed"],
|
||||
"no-case-declarations": "off",
|
||||
"vue/require-default-prop": "off",
|
||||
"vue/v-on-event-hyphenation": "off"
|
||||
}
|
||||
}
|
||||
38
photon-client/eslint.config.mjs
Normal file
@@ -0,0 +1,38 @@
|
||||
import pluginVue from "eslint-plugin-vue";
|
||||
import { defineConfigWithVueTs, vueTsConfigs } from "@vue/eslint-config-typescript";
|
||||
|
||||
import skipFormattingConfig from "@vue/eslint-config-prettier/skip-formatting";
|
||||
|
||||
export default defineConfigWithVueTs(
|
||||
pluginVue.configs["flat/recommended"],
|
||||
vueTsConfigs.recommended,
|
||||
skipFormattingConfig,
|
||||
{
|
||||
ignores: ["**/dist/**"]
|
||||
},
|
||||
{
|
||||
//extends: ["js/recommended"],
|
||||
rules: {
|
||||
quotes: ["error", "double"],
|
||||
"comma-dangle": ["error", "never"],
|
||||
|
||||
"comma-spacing": [
|
||||
"error",
|
||||
{
|
||||
before: false,
|
||||
after: true
|
||||
}
|
||||
],
|
||||
|
||||
semi: ["error", "always"],
|
||||
"eol-last": "error",
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"quote-props": ["error", "as-needed"],
|
||||
"no-case-declarations": "off",
|
||||
"vue/require-default-prop": "off",
|
||||
"vue/v-on-event-hyphenation": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"vue/valid-v-slot": ["error", { allowModifiers: true }]
|
||||
}
|
||||
}
|
||||
);
|
||||
6074
photon-client/package-lock.json
generated
@@ -9,44 +9,39 @@
|
||||
"preview": "vite preview --port 4173",
|
||||
"build-only": "vite build",
|
||||
"build-demo": "vite build --mode demo",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
||||
"format": "prettier --write src/",
|
||||
"lint-ci": "eslint . --max-warnings 0 --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"lint-ci": "eslint . --max-warnings 0 --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
||||
"format-ci": "prettier --check src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/prompt": "^5.0.9",
|
||||
"@eslint/js": "^9.26.0",
|
||||
"@fontsource/prompt": "^5.2.6",
|
||||
"@mdi/font": "^7.4.47",
|
||||
"@msgpack/msgpack": "^3.0.0-beta2",
|
||||
"axios": "^1.6.3",
|
||||
"jspdf": "^2.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"pinia": "^2.1.4",
|
||||
"three": "^0.160.0",
|
||||
"vue": "^2.7.14",
|
||||
"vue-router": "^3.6.5",
|
||||
"vue-virtual-scroll-list": "^2.3.5",
|
||||
"vue2-helpers": "^2.1.1",
|
||||
"vuetify": "^2.7.1"
|
||||
"@msgpack/msgpack": "^3.1.1",
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"axios": "^1.9.0",
|
||||
"jspdf": "^3.0.1",
|
||||
"pinia": "^3.0.2",
|
||||
"three": "^0.176.0",
|
||||
"vite-plugin-vuetify": "^2.1.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.1",
|
||||
"vue3-virtual-scroll-list": "^0.2.1",
|
||||
"vuetify": "^3.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.3.2",
|
||||
"@types/node": "^18.19.45",
|
||||
"@types/three": "^0.160.0",
|
||||
"@vitejs/plugin-vue2": "^2.3.1",
|
||||
"@vue/eslint-config-prettier": "^9.0.0",
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"deepmerge": "^4.3.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-vue": "^9.19.2",
|
||||
"@types/node": "^22.15.14",
|
||||
"@types/three": "^0.176.0",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vue/eslint-config-typescript": "^14.5.0",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"eslint": "^9.26.0",
|
||||
"eslint-plugin-vue": "^10.1.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "3.2.2",
|
||||
"sass": "~1.32",
|
||||
"sass-loader": "^13.3.2",
|
||||
"terser": "^5.14.2",
|
||||
"typescript": "^5.3.3",
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"vite": "^5.4.2"
|
||||
"prettier": "^3.5.3",
|
||||
"sass": "^1.87.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,9 +58,9 @@ if (!is_demo) {
|
||||
<v-main>
|
||||
<v-container class="main-container" fluid fill-height>
|
||||
<v-layout>
|
||||
<v-flex>
|
||||
<v-container class="align-start pa-0 ma-0" fluid>
|
||||
<router-view />
|
||||
</v-flex>
|
||||
</v-container>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
</v-main>
|
||||
@@ -71,9 +71,10 @@ if (!is_demo) {
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import "vuetify/src/styles/settings/_variables";
|
||||
@use "@/assets/styles/settings";
|
||||
@use "@/assets/styles/variables";
|
||||
|
||||
@media #{map-get($display-breakpoints, 'md-and-down')} {
|
||||
@media #{map-get(settings.$display-breakpoints, 'md-and-down')} {
|
||||
html {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
@@ -102,7 +103,14 @@ if (!is_demo) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.v-overlay__scrim {
|
||||
background-color: #202020;
|
||||
}
|
||||
|
||||
#title {
|
||||
color: #ffd843;
|
||||
}
|
||||
div.v-layout {
|
||||
overflow: unset !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
4
photon-client/src/assets/styles/settings.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
@forward "vuetify/settings" with (
|
||||
$button-colored-disabled: false,
|
||||
$button-disabled-opacity: 0.4
|
||||
);
|
||||
@@ -9,15 +9,15 @@ body {
|
||||
background: $body-background;
|
||||
}
|
||||
|
||||
.v-application {
|
||||
html {
|
||||
font-family: $default-font !important;
|
||||
}
|
||||
|
||||
.v-row-group__header {
|
||||
background: #005281 !important;
|
||||
}
|
||||
.theme--dark.v-data-table
|
||||
> .v-data-table__wrapper
|
||||
.v-table
|
||||
> .v-table__wrapper
|
||||
> table
|
||||
> tbody
|
||||
> tr:hover:not(.v-data-table__expanded__content):not(.v-data-table__empty-wrapper) {
|
||||
@@ -27,3 +27,7 @@ body {
|
||||
.v-card__title {
|
||||
word-break: break-word !important;
|
||||
}
|
||||
|
||||
.v-field__input {
|
||||
padding: 0px !important;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { computed, inject, ref, onBeforeUnmount } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import loadingImage from "@/assets/images/loading-transparent.svg";
|
||||
import type { StyleValue } from "vue/types/jsx";
|
||||
import type { StyleValue } from "vue";
|
||||
import PvIcon from "@/components/common/pv-icon.vue";
|
||||
import type { UiCameraConfiguration } from "@/types/SettingTypes";
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useStateStore } from "@/stores/StateStore";
|
||||
<template>
|
||||
<v-snackbar
|
||||
v-model="useStateStore().snackbarData.show"
|
||||
top
|
||||
location="top"
|
||||
:color="useStateStore().snackbarData.color"
|
||||
:timeout="useStateStore().snackbarData.timeout"
|
||||
>
|
||||
|
||||
@@ -7,13 +7,13 @@ const props = defineProps<{ source: LogMessage }>();
|
||||
const logColorClass = computed<string>(() => {
|
||||
switch (props.source.level) {
|
||||
case LogLevel.ERROR:
|
||||
return "red--text";
|
||||
return "text-red";
|
||||
case LogLevel.WARN:
|
||||
return "yellow--text";
|
||||
return "text-yellow";
|
||||
case LogLevel.INFO:
|
||||
return "light-blue--text";
|
||||
return "text-light-blue";
|
||||
case LogLevel.DEBUG:
|
||||
return "white--text";
|
||||
return "text-white";
|
||||
}
|
||||
return "";
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { computed, inject, ref, watch } from "vue";
|
||||
import { LogLevel, type LogMessage } from "@/types/SettingTypes";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import LogEntry from "@/components/app/photon-log-entry.vue";
|
||||
import VirtualList from "vue-virtual-scroll-list";
|
||||
import VirtualList from "vue3-virtual-scroll-list";
|
||||
|
||||
const backendHost = inject<string>("backendHost");
|
||||
|
||||
@@ -74,15 +74,15 @@ document.addEventListener("keydown", (e) => {
|
||||
|
||||
<template>
|
||||
<v-dialog v-model="useStateStore().showLogModal" width="1500" dark>
|
||||
<v-card dark class="dialog-container pa-6" color="primary" flat>
|
||||
<v-card class="dialog-container pa-6" color="primary" flat>
|
||||
<!-- Logs header -->
|
||||
<v-row class="no-gutters pb-3">
|
||||
<v-row class="pb-3">
|
||||
<v-col cols="4">
|
||||
<v-card-title class="pa-0">Program Logs</v-card-title>
|
||||
</v-col>
|
||||
<v-col class="align-self-center pl-3" style="text-align: right">
|
||||
<v-btn text color="white" @click="handleLogExport">
|
||||
<v-icon left class="menu-icon"> mdi-download </v-icon>
|
||||
<v-btn variant="text" color="white" @click="handleLogExport">
|
||||
<v-icon start class="menu-icon"> mdi-download </v-icon>
|
||||
<span class="menu-label">Download</span>
|
||||
|
||||
<!-- Special hidden link that gets 'clicked' when the user exports journalctl logs -->
|
||||
@@ -94,12 +94,12 @@ document.addEventListener("keydown", (e) => {
|
||||
target="_blank"
|
||||
/>
|
||||
</v-btn>
|
||||
<v-btn text color="white" @click="handleLogClear">
|
||||
<v-icon left class="menu-icon"> mdi-trash-can-outline </v-icon>
|
||||
<v-btn variant="text" color="white" @click="handleLogClear">
|
||||
<v-icon start class="menu-icon"> mdi-trash-can-outline </v-icon>
|
||||
<span class="menu-label">Clear Client Logs</span>
|
||||
</v-btn>
|
||||
<v-btn text color="white" @click="() => (useStateStore().showLogModal = false)">
|
||||
<v-icon left class="menu-icon"> mdi-close </v-icon>
|
||||
<v-btn variant="text" color="white" @click="() => (useStateStore().showLogModal = false)">
|
||||
<v-icon start class="menu-icon"> mdi-close </v-icon>
|
||||
<span class="menu-label">Close</span>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
@@ -109,38 +109,28 @@ document.addEventListener("keydown", (e) => {
|
||||
|
||||
<div class="dialog-data">
|
||||
<!-- Log view options -->
|
||||
<v-row class="pt-4 pt-md-0 no-gutters">
|
||||
<v-col cols="12" md="5" class="align-self-center">
|
||||
<v-row no-gutters class="pt-4 pt-md-0" style="display: flex; justify-content: space-between">
|
||||
<v-col cols="12" md="7" style="display: flex; align-items: center">
|
||||
<v-text-field
|
||||
v-model="searchQuery"
|
||||
dark
|
||||
dense
|
||||
density="compact"
|
||||
clearable
|
||||
hide-details="auto"
|
||||
prepend-icon="mdi-magnify"
|
||||
color="accent"
|
||||
label="Search"
|
||||
variant="underlined"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="2" style="display: flex; align-items: center">
|
||||
<input v-model="timeInput" type="time" step="1" class="white--text pl-0 pl-md-8" />
|
||||
<v-btn icon class="ml-3" @click="timeInput = undefined">
|
||||
<input v-model="timeInput" type="time" step="1" class="text-white pl-3" />
|
||||
<v-btn icon variant="flat" @click="timeInput = undefined">
|
||||
<v-icon>mdi-close-circle-outline</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" md="5" class="pr-3">
|
||||
<v-row class="no-gutters">
|
||||
<v-col v-for="level in [0, 1, 2, 3]" :key="level">
|
||||
<v-row dense align="center">
|
||||
<v-col cols="6" md="8" style="text-align: right">
|
||||
{{ getLogLevelFromIndex(level) }}
|
||||
</v-col>
|
||||
<v-col cols="6" md="4">
|
||||
<v-switch v-model="selectedLogLevels[level]" dark color="#ffd843" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-col v-for="level in [0, 1, 2, 3]" :key="level" class="pr-3">
|
||||
<div class="pb-0 pt-0" style="display: flex; align-items: center; flex: min-content">
|
||||
{{ getLogLevelFromIndex(level)
|
||||
}}<v-switch v-model="selectedLogLevels[level]" class="pl-2" hide-details color="#ffd843"></v-switch>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
import { computed } from "vue";
|
||||
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { PlaceholderCameraSettings } from "@/types/SettingTypes";
|
||||
import { useRoute } from "vue2-helpers/vue-router";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useDisplay } from "vuetify";
|
||||
|
||||
const compact = computed<boolean>({
|
||||
get: () => {
|
||||
@@ -14,9 +15,9 @@ const compact = computed<boolean>({
|
||||
useStateStore().setSidebarFolded(val);
|
||||
}
|
||||
});
|
||||
const { mdAndUp } = useDisplay();
|
||||
|
||||
// Vuetify2 doesn't yet support the useDisplay API so this is required to access the prop when using the Composition API
|
||||
const mdAndUp = computed<boolean>(() => getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndUp || false);
|
||||
const renderCompact = computed<boolean>(() => compact.value || !mdAndUp.value);
|
||||
|
||||
const needsCamerasConfigured = computed<boolean>(() => {
|
||||
return (
|
||||
@@ -27,115 +28,99 @@ const needsCamerasConfigured = computed<boolean>(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-navigation-drawer dark app permanent :mini-variant="compact || !mdAndUp" color="primary">
|
||||
<v-list>
|
||||
<v-navigation-drawer permanent :rail="renderCompact" color="primary">
|
||||
<v-list nav>
|
||||
<!-- List item for the heading; note that there are some tricks in setting padding and image width make things look right -->
|
||||
<v-list-item :class="compact || !mdAndUp ? 'pr-0 pl-0' : ''" style="display: flex; justify-content: center">
|
||||
<v-list-item-icon class="mr-0">
|
||||
<img v-if="!(compact || !mdAndUp)" class="logo" src="@/assets/images/logoLarge.svg" alt="large logo" />
|
||||
<v-list-item :class="renderCompact ? 'pr-0 pl-0' : ''" style="display: flex; justify-content: center">
|
||||
<template #prepend>
|
||||
<img v-if="!renderCompact" class="logo" src="@/assets/images/logoLarge.svg" alt="large logo" />
|
||||
<img v-else class="logo" src="@/assets/images/logoSmall.svg" alt="small logo" />
|
||||
</v-list-item-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item link to="/dashboard">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-view-dashboard</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Dashboard</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
<v-list-item link to="/dashboard" prepend-icon="mdi-view-dashboard">
|
||||
<v-list-item-title>Dashboard</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item link to="/settings">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-cog</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Settings</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
<v-list-item link to="/settings" prepend-icon="mdi-cog">
|
||||
<v-list-item-title>Settings</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item ref="camerasTabOpener" link to="/cameras">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-camera</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Camera</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
<v-list-item ref="camerasTabOpener" link to="/cameras" prepend-icon="mdi-camera">
|
||||
<v-list-item-title>Camera</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
link
|
||||
to="/cameraConfigs"
|
||||
:class="{ cameraicon: needsCamerasConfigured && useRoute().path !== '/cameraConfigs' }"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon :class="{ 'red--text': needsCamerasConfigured }">mdi-swap-horizontal-bold</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title :class="{ 'red--text': needsCamerasConfigured }">Camera Matching</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
<template #prepend>
|
||||
<v-icon :class="{ 'text-red': needsCamerasConfigured }">mdi-swap-horizontal-bold</v-icon>
|
||||
</template>
|
||||
<v-list-item-title :class="{ 'text-red': needsCamerasConfigured }">Camera Matching</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item link to="/docs">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-bookshelf</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Documentation</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
<v-list-item link to="/docs" prepend-icon="mdi-bookshelf">
|
||||
<v-list-item-title>Documentation</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<div style="position: absolute; bottom: 0; left: 0">
|
||||
<v-list-item v-if="mdAndUp" link @click="() => (compact = !compact)">
|
||||
<v-list-item-icon>
|
||||
<v-icon v-if="compact || !mdAndUp"> mdi-chevron-right </v-icon>
|
||||
<v-icon v-else> mdi-chevron-left </v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Compact Mode</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-list-item-icon>
|
||||
<v-icon v-if="useSettingsStore().network.runNTServer"> mdi-server </v-icon>
|
||||
<v-icon v-else-if="useStateStore().ntConnectionStatus.connected"> mdi-robot </v-icon>
|
||||
<v-icon v-else style="border-radius: 100%"> mdi-robot-off </v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-if="useSettingsStore().network.runNTServer" class="text-wrap">
|
||||
NetworkTables server running for
|
||||
<span class="accent--text">{{ useStateStore().ntConnectionStatus.clients || 0 }}</span> clients
|
||||
</v-list-item-title>
|
||||
<v-list-item-title
|
||||
v-else-if="useStateStore().ntConnectionStatus.connected && useStateStore().backendConnected"
|
||||
class="text-wrap"
|
||||
style="flex-direction: column; display: flex"
|
||||
>
|
||||
NetworkTables Server Connected!
|
||||
<span class="accent--text">
|
||||
{{ useStateStore().ntConnectionStatus.address }}
|
||||
</span>
|
||||
</v-list-item-title>
|
||||
<v-list-item-title v-else class="text-wrap" style="flex-direction: column; display: flex">
|
||||
Not connected to NetworkTables Server!
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<v-list-item-icon>
|
||||
<v-icon v-if="useStateStore().backendConnected"> mdi-server-network </v-icon>
|
||||
<v-icon v-else style="border-radius: 100%"> mdi-server-network-off </v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="text-wrap">
|
||||
{{ useStateStore().backendConnected ? "Backend connected" : "Trying to connect to backend" }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</div>
|
||||
</v-list>
|
||||
<template #append>
|
||||
<v-list nav>
|
||||
<v-list-item
|
||||
v-if="mdAndUp"
|
||||
link
|
||||
:prepend-icon="`mdi-chevron-${compact || !mdAndUp ? 'right' : 'left'}`"
|
||||
@click="() => (compact = !compact)"
|
||||
>
|
||||
<v-list-item-title>Compact Mode</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
:prepend-icon="
|
||||
useSettingsStore().network.runNTServer
|
||||
? 'mdi-server'
|
||||
: useStateStore().ntConnectionStatus.connected
|
||||
? 'mdi-robot'
|
||||
: 'mdi-robot-off'
|
||||
"
|
||||
>
|
||||
<v-list-item-title v-if="useSettingsStore().network.runNTServer" v-show="!renderCompact" class="text-wrap">
|
||||
NetworkTables server running for
|
||||
<span class="text-accent">{{ useStateStore().ntConnectionStatus.clients || 0 }}</span> clients
|
||||
</v-list-item-title>
|
||||
<v-list-item-title
|
||||
v-else-if="useStateStore().ntConnectionStatus.connected && useStateStore().backendConnected"
|
||||
v-show="!renderCompact"
|
||||
class="text-wrap"
|
||||
style="flex-direction: column; display: flex"
|
||||
>
|
||||
NetworkTables Server Connected!
|
||||
<span class="text-accent">
|
||||
{{ useStateStore().ntConnectionStatus.address }}
|
||||
</span>
|
||||
</v-list-item-title>
|
||||
<v-list-item-title
|
||||
v-else
|
||||
v-show="!renderCompact"
|
||||
class="text-wrap"
|
||||
style="flex-direction: column; display: flex"
|
||||
>
|
||||
Not connected to NetworkTables Server!
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item :prepend-icon="useStateStore().backendConnected ? 'mdi-server-network' : 'mdi-server-network-off'">
|
||||
<v-list-item-title v-show="!renderCompact" class="text-wrap">
|
||||
{{ useStateStore().backendConnected ? "Backend connected" : "Trying to connect to backend" }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</template>
|
||||
</v-navigation-drawer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.v-list-item-title {
|
||||
font-size: 1rem !important;
|
||||
line-height: 1.2rem !important;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 100%;
|
||||
height: 70px;
|
||||
|
||||
@@ -101,9 +101,8 @@ const downloadCalibBoard = () => {
|
||||
|
||||
switch (boardType.value) {
|
||||
case CalibrationBoardTypes.Chessboard:
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const chessboardStartX = (paperWidth - patternWidth.value * squareSizeIn.value) / 2;
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
|
||||
const chessboardStartY = (paperHeight - patternWidth.value * squareSizeIn.value) / 2;
|
||||
|
||||
for (let squareY = 0; squareY < patternHeight.value; squareY++) {
|
||||
@@ -203,10 +202,10 @@ const endCalibration = () => {
|
||||
});
|
||||
};
|
||||
|
||||
let drawAllSnapshots = ref(true);
|
||||
const drawAllSnapshots = ref(true);
|
||||
|
||||
let showCalDialog = ref(false);
|
||||
let selectedVideoFormat = ref<VideoFormat | undefined>(undefined);
|
||||
const showCalDialog = ref(false);
|
||||
const selectedVideoFormat = ref<VideoFormat | undefined>(undefined);
|
||||
const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
selectedVideoFormat.value = format;
|
||||
showCalDialog.value = true;
|
||||
@@ -218,8 +217,8 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
<v-card class="mb-3" color="primary" dark>
|
||||
<v-card-title class="pa-6 pb-3">Camera Calibration</v-card-title>
|
||||
<v-card-text v-show="!isCalibrating">
|
||||
<v-card-subtitle class="pt-3 pl-2 pb-4 white--text">Current Calibration</v-card-subtitle>
|
||||
<v-simple-table fixed-header height="100%" dense>
|
||||
<v-card-subtitle class="pt-3 pl-2 pb-4 text-white">Current Calibration</v-card-subtitle>
|
||||
<v-table fixed-header height="100%" density="compact">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Resolution</th>
|
||||
@@ -239,20 +238,20 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
<td>{{ value.horizontalFOV !== undefined ? value.horizontalFOV.toFixed(2) + "°" : "-" }}</td>
|
||||
<td>{{ value.verticalFOV !== undefined ? value.verticalFOV.toFixed(2) + "°" : "-" }}</td>
|
||||
<td>{{ value.diagonalFOV !== undefined ? value.diagonalFOV.toFixed(2) + "°" : "-" }}</td>
|
||||
<v-tooltip bottom>
|
||||
<template #activator="{ on, attrs }">
|
||||
<td v-bind="attrs" v-on="on" @click="setSelectedVideoFormat(value)">
|
||||
<v-icon small class="mr-2">mdi-information</v-icon>
|
||||
<v-tooltip location="bottom">
|
||||
<template #activator="{ props }">
|
||||
<td v-bind="props" @click="setSelectedVideoFormat(value)">
|
||||
<v-icon size="small" class="mr-2">mdi-information</v-icon>
|
||||
</td>
|
||||
</template>
|
||||
<span>Click for more info on this calibration.</span>
|
||||
</v-tooltip>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
</v-table>
|
||||
</v-card-text>
|
||||
<v-card-text v-if="useCameraSettingsStore().isConnected" class="d-flex flex-column pa-6 pt-0">
|
||||
<v-card-subtitle v-show="!isCalibrating" class="pl-0 pb-3 pt-3 white--text"
|
||||
<v-card-subtitle v-show="!isCalibrating" class="pl-0 pb-3 pt-3 text-white"
|
||||
>Configure New Calibration</v-card-subtitle
|
||||
>
|
||||
<v-form ref="form" v-model="settingsValid">
|
||||
@@ -272,7 +271,9 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
tooltip="Resolution to which camera frames are downscaled for detection. Calibration still uses full-res"
|
||||
:items="calibrationDivisors"
|
||||
:select-cols="8"
|
||||
@input="(v) => useCameraSettingsStore().changeCurrentPipelineSetting({ streamingFrameDivisor: +v }, false)"
|
||||
@update:modelValue="
|
||||
(v) => useCameraSettingsStore().changeCurrentPipelineSetting({ streamingFrameDivisor: +v }, false)
|
||||
"
|
||||
/>
|
||||
<pv-select
|
||||
v-model="boardType"
|
||||
@@ -353,14 +354,18 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
label="Draw Collected Corners"
|
||||
:switch-cols="8"
|
||||
tooltip="Draw all snapshots"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ drawAllSnapshots: args }, false)"
|
||||
@update:modelValue="
|
||||
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ drawAllSnapshots: args }, false)
|
||||
"
|
||||
/>
|
||||
<pv-switch
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraAutoExposure"
|
||||
label="Auto Exposure"
|
||||
:label-cols="4"
|
||||
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoExposure: args }, false)"
|
||||
@update:modelValue="
|
||||
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoExposure: args }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraExposureRaw"
|
||||
@@ -371,7 +376,9 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
:max="useCameraSettingsStore().maxExposureRaw"
|
||||
:slider-cols="7"
|
||||
:step="1"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraExposureRaw: args }, false)"
|
||||
@update:modelValue="
|
||||
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraExposureRaw: args }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBrightness"
|
||||
@@ -379,7 +386,9 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="7"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBrightness: args }, false)"
|
||||
@update:modelValue="
|
||||
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBrightness: args }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.cameraGain >= 0"
|
||||
@@ -389,7 +398,9 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="7"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraGain: args }, false)"
|
||||
@update:modelValue="
|
||||
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraGain: args }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.cameraRedGain !== -1"
|
||||
@@ -399,7 +410,9 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
:max="100"
|
||||
:slider-cols="7"
|
||||
tooltip="Controls red automatic white balance gain, which affects how the camera captures colors in different conditions"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraRedGain: args }, false)"
|
||||
@update:modelValue="
|
||||
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraRedGain: args }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain !== -1"
|
||||
@@ -409,7 +422,9 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
:max="100"
|
||||
:slider-cols="7"
|
||||
tooltip="Controls blue automatic white balance gain, which affects how the camera captures colors in different conditions"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBlueGain: args }, false)"
|
||||
@update:modelValue="
|
||||
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBlueGain: args }, false)
|
||||
"
|
||||
/>
|
||||
<v-banner
|
||||
v-if="tooManyPoints"
|
||||
@@ -423,7 +438,11 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
</v-banner>
|
||||
</v-card-text>
|
||||
<v-card-text v-if="isCalibrating" class="d-flex justify-center align-center pa-6 pt-0">
|
||||
<v-chip label :color="useStateStore().calibrationData.hasEnoughImages ? 'secondary' : 'gray'">
|
||||
<v-chip
|
||||
variant="flat"
|
||||
label
|
||||
:color="useStateStore().calibrationData.hasEnoughImages ? 'secondary' : 'grey-darken-2'"
|
||||
>
|
||||
Snapshots: {{ useStateStore().calibrationData.imageCount }} of at least
|
||||
{{ useStateStore().calibrationData.minimumImageCount }}
|
||||
</v-chip>
|
||||
@@ -431,26 +450,25 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
<v-card-text class="d-flex pa-6 pt-0">
|
||||
<v-col cols="6" class="pa-0 pr-2">
|
||||
<v-btn
|
||||
small
|
||||
size="small"
|
||||
block
|
||||
color="secondary"
|
||||
:disabled="!settingsValid || tooManyPoints"
|
||||
@click="isCalibrating ? useCameraSettingsStore().takeCalibrationSnapshot() : startCalibration()"
|
||||
>
|
||||
<v-icon left class="calib-btn-icon"> {{ isCalibrating ? "mdi-camera" : "mdi-flag-outline" }} </v-icon>
|
||||
<v-icon start class="calib-btn-icon"> {{ isCalibrating ? "mdi-camera" : "mdi-flag-outline" }} </v-icon>
|
||||
<span class="calib-btn-label">{{ isCalibrating ? "Take Snapshot" : "Start Calibration" }}</span>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="6" class="pa-0 pl-2">
|
||||
<v-btn
|
||||
small
|
||||
size="small"
|
||||
block
|
||||
:color="useStateStore().calibrationData.hasEnoughImages ? 'accent' : 'error'"
|
||||
:class="useStateStore().calibrationData.hasEnoughImages ? 'black--text' : 'white---text'"
|
||||
:disabled="!isCalibrating || !settingsValid"
|
||||
@click="endCalibration"
|
||||
>
|
||||
<v-icon left class="calib-btn-icon">
|
||||
<v-icon start class="calib-btn-icon">
|
||||
{{ useStateStore().calibrationData.hasEnoughImages ? "mdi-flag-checkered" : "mdi-flag-off-outline" }}
|
||||
</v-icon>
|
||||
<span class="calib-btn-label">{{
|
||||
@@ -460,8 +478,15 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
</v-col>
|
||||
</v-card-text>
|
||||
<v-card-text class="pa-6 pt-0">
|
||||
<v-btn color="accent" small block outlined :disabled="!settingsValid" @click="downloadCalibBoard">
|
||||
<v-icon left class="calib-btn-icon"> mdi-download </v-icon>
|
||||
<v-btn
|
||||
color="accent"
|
||||
size="small"
|
||||
block
|
||||
variant="outlined"
|
||||
:disabled="!settingsValid"
|
||||
@click="downloadCalibBoard"
|
||||
>
|
||||
<v-icon start class="calib-btn-icon"> mdi-download </v-icon>
|
||||
<span class="calib-btn-label">Generate Board</span>
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
@@ -507,7 +532,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
</div>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn v-if="!isCalibrating" color="white" text @click="showCalibEndDialog = false"> OK </v-btn>
|
||||
<v-btn v-if="!isCalibrating" color="white" variant="text" @click="showCalibEndDialog = false"> OK </v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
@@ -518,7 +543,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.v-data-table {
|
||||
.v-table {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ const calibrationImageURL = (index: number) =>
|
||||
</v-col>
|
||||
<v-col cols="6" md="3" class="d-flex align-center pt-0 pt-md-3 pl-6 pl-md-3">
|
||||
<v-btn color="secondary" style="width: 100%" @click="openUploadPhotonCalibJsonPrompt">
|
||||
<v-icon left> mdi-import</v-icon>
|
||||
<v-icon start> mdi-import</v-icon>
|
||||
<span>Import</span>
|
||||
</v-btn>
|
||||
<input
|
||||
@@ -115,7 +115,7 @@ const calibrationImageURL = (index: number) =>
|
||||
style="width: 100%"
|
||||
@click="openExportCalibrationPrompt"
|
||||
>
|
||||
<v-icon left>mdi-export</v-icon>
|
||||
<v-icon start>mdi-export</v-icon>
|
||||
<span>Export</span>
|
||||
</v-btn>
|
||||
<a
|
||||
@@ -126,16 +126,24 @@ const calibrationImageURL = (index: number) =>
|
||||
/>
|
||||
</v-col>
|
||||
</div>
|
||||
<v-card-title class="pt-0 pb-3"
|
||||
<v-card-title class="pl-6 pt-0 pb-0"
|
||||
>{{ useCameraSettingsStore().currentCameraName }}@{{ getResolutionString(videoFormat.resolution) }}</v-card-title
|
||||
>
|
||||
<v-card-text v-if="!currentCalibrationCoeffs">
|
||||
<v-banner rounded color="secondary" text-color="white" class="mt-3" icon="mdi-alert-circle-outline">
|
||||
<v-banner
|
||||
rounded
|
||||
bg-color="secondary"
|
||||
color="secondary"
|
||||
text-color="white"
|
||||
class="pt-3 pb-3 mt-3"
|
||||
density="compact"
|
||||
icon="mdi-alert-circle-outline"
|
||||
>
|
||||
The selected video format has not been calibrated.
|
||||
</v-banner>
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
<v-simple-table dense style="width: 100%">
|
||||
<v-card-text class="pt-0">
|
||||
<v-table density="compact" style="width: 100%">
|
||||
<template #default>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -238,25 +246,36 @@ const calibrationImageURL = (index: number) =>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-table>
|
||||
</v-card-text>
|
||||
<v-card-title v-if="currentCalibrationCoeffs" class="pt-0">Individual Observations</v-card-title>
|
||||
<v-card-text v-if="currentCalibrationCoeffs">
|
||||
<v-data-table
|
||||
dense
|
||||
density="compact"
|
||||
style="width: 100%"
|
||||
:headers="[
|
||||
{ text: 'Observation Id', value: 'index' },
|
||||
{ text: 'Mean Reprojection Error', value: 'mean' },
|
||||
{ text: '', value: 'data-table-expand' }
|
||||
{ title: 'Observation Id', key: 'index' },
|
||||
{ title: 'Mean Reprojection Error', key: 'mean' },
|
||||
{ title: '', key: 'data-table-expand' }
|
||||
]"
|
||||
:items="getObservationDetails()"
|
||||
item-key="index"
|
||||
item-value="index"
|
||||
show-expand
|
||||
expand-icon="mdi-eye"
|
||||
>
|
||||
<template #expanded-item="{ headers, item }">
|
||||
<td :colspan="headers.length">
|
||||
<template #item.data-table-expand="{ internalItem, toggleExpand }">
|
||||
<v-btn
|
||||
icon="mdi-eye"
|
||||
class="text-none"
|
||||
color="medium-emphasis"
|
||||
size="small"
|
||||
variant="text"
|
||||
slim
|
||||
@click="toggleExpand(internalItem)"
|
||||
></v-btn>
|
||||
</template>
|
||||
|
||||
<template #expanded-row="{ columns, item }">
|
||||
<td :colspan="columns.length">
|
||||
<div style="display: flex; justify-content: center; width: 100%">
|
||||
<img :src="calibrationImageURL(item.index)" alt="observation image" class="snapshot-preview pt-2 pb-2" />
|
||||
</div>
|
||||
|
||||
@@ -91,16 +91,16 @@ const expanded = ref([]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card dark style="background-color: #006492">
|
||||
<v-card style="background-color: #006492">
|
||||
<v-card-title>Camera Control</v-card-title>
|
||||
<v-card-text>
|
||||
<v-btn color="secondary" @click="fetchSnapshots">
|
||||
<v-icon left class="open-icon"> mdi-folder </v-icon>
|
||||
<v-icon start class="open-icon"> mdi-folder </v-icon>
|
||||
<span class="open-label">Show Saved Snapshots</span>
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
<v-dialog v-model="showSnapshotViewerDialog">
|
||||
<v-card dark class="pt-3 pl-5 pr-5" color="primary" flat>
|
||||
<v-card class="pt-3 pl-5 pr-5" color="primary" flat>
|
||||
<v-card-title> View Saved Frame Snapshots </v-card-title>
|
||||
<v-divider />
|
||||
<v-card-text v-if="imgData.length === 0" style="font-size: 18px; font-weight: 600" class="pt-4">
|
||||
@@ -110,22 +110,33 @@ const expanded = ref([]);
|
||||
<v-data-table
|
||||
v-model:expanded="expanded"
|
||||
:headers="[
|
||||
{ text: 'Snapshot Name', value: 'snapshotShortName', sortable: false },
|
||||
{ text: 'Camera Unique Name', value: 'cameraUniqueName' },
|
||||
{ text: 'Camera Nickname', value: 'cameraNickname' },
|
||||
{ text: 'Stream Type', value: 'streamType' },
|
||||
{ text: 'Time Created', value: 'timeCreated' },
|
||||
{ text: 'Actions', value: 'actions', sortable: false }
|
||||
{ title: 'Snapshot Name', key: 'snapshotShortName', sortable: false },
|
||||
{ title: 'Camera Unique Name', key: 'cameraUniqueName' },
|
||||
{ title: 'Camera Nickname', key: 'cameraNickname' },
|
||||
{ title: 'Stream Type', key: 'streamType' },
|
||||
{ title: 'Time Created', key: 'timeCreated' },
|
||||
{ title: 'Actions', key: 'actions', sortable: false }
|
||||
]"
|
||||
:items="imgData"
|
||||
group-by="cameraUniqueName"
|
||||
:group-by="[{ key: 'cameraUniqueName' }]"
|
||||
class="elevation-0"
|
||||
item-key="index"
|
||||
item-value="index"
|
||||
show-expand
|
||||
expand-icon="mdi-eye"
|
||||
>
|
||||
<template #expanded-item="{ headers, item }">
|
||||
<td :colspan="headers.length">
|
||||
<template #item.data-table-expand="{ internalItem, toggleExpand }">
|
||||
<v-btn
|
||||
icon="mdi-eye"
|
||||
class="text-none"
|
||||
color="medium-emphasis"
|
||||
size="small"
|
||||
variant="text"
|
||||
slim
|
||||
@click="toggleExpand(internalItem)"
|
||||
></v-btn>
|
||||
</template>
|
||||
|
||||
<template #expanded-row="{ item, columns }">
|
||||
<td :colspan="columns.length">
|
||||
<div style="display: flex; justify-content: center; width: 100%">
|
||||
<img :src="item.snapshotSrc" alt="snapshot-image" class="snapshot-preview pt-2 pb-2" />
|
||||
</div>
|
||||
@@ -135,7 +146,7 @@ const expanded = ref([]);
|
||||
<template #item.actions="{ item }">
|
||||
<div style="display: flex; justify-content: center">
|
||||
<a :download="item.snapshotName" :href="item.snapshotSrc">
|
||||
<v-icon small> mdi-download </v-icon>
|
||||
<v-icon size="small"> mdi-download </v-icon>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
@@ -157,7 +168,7 @@ const expanded = ref([]);
|
||||
.v-btn {
|
||||
width: 100%;
|
||||
}
|
||||
.v-data-table {
|
||||
.v-table {
|
||||
text-align: center;
|
||||
background-color: #006492 !important;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import PvSelect, { type SelectItem } from "@/components/common/pv-select.vue";
|
||||
import PvInput from "@/components/common/pv-input.vue";
|
||||
import PvNumberInput from "@/components/common/pv-number-input.vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
@@ -203,30 +204,30 @@ const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
</v-card-text>
|
||||
<v-card-text class="d-flex pa-6 pt-0">
|
||||
<v-col cols="6" class="pa-0 pr-2">
|
||||
<v-btn block small color="secondary" :disabled="!settingsHaveChanged()" @click="saveCameraSettings">
|
||||
<v-icon left> mdi-content-save </v-icon>
|
||||
<v-btn block size="small" color="secondary" :disabled="!settingsHaveChanged()" @click="saveCameraSettings">
|
||||
<v-icon start> mdi-content-save </v-icon>
|
||||
Save Changes
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="6" class="pa-0 pl-2">
|
||||
<v-btn block small color="error" @click="() => (showDeleteCamera = true)">
|
||||
<v-icon left> mdi-trash-can-outline </v-icon>
|
||||
<v-btn block size="small" color="error" @click="() => (showDeleteCamera = true)">
|
||||
<v-icon start> mdi-trash-can-outline </v-icon>
|
||||
Delete Camera
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-card-text>
|
||||
|
||||
<v-dialog v-model="showDeleteCamera" dark width="800">
|
||||
<v-card dark class="dialog-container pa-3 pb-2" color="primary" flat>
|
||||
<v-dialog v-model="showDeleteCamera" width="800">
|
||||
<v-card class="dialog-container pa-3 pb-2" color="primary" flat>
|
||||
<v-card-title> Delete {{ useCameraSettingsStore().currentCameraSettings.nickname }}? </v-card-title>
|
||||
<v-card-text>
|
||||
<v-row class="align-center pt-6">
|
||||
<v-col cols="12" md="6">
|
||||
<span class="white--text"> This will delete ALL OF YOUR SETTINGS and restart PhotonVision. </span>
|
||||
<span class="text-white"> This will delete ALL OF YOUR SETTINGS and restart PhotonVision. </span>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-btn color="secondary" block @click="openExportSettingsPrompt">
|
||||
<v-icon left class="open-icon"> mdi-export </v-icon>
|
||||
<v-icon start class="open-icon"> mdi-export </v-icon>
|
||||
<span class="open-label">Backup Settings</span>
|
||||
<a
|
||||
ref="exportSettings"
|
||||
@@ -257,7 +258,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
:loading="deletingCamera"
|
||||
@click="deleteThisCamera"
|
||||
>
|
||||
<v-icon left class="open-icon"> mdi-trash-can-outline </v-icon>
|
||||
<v-icon start class="open-icon"> mdi-trash-can-outline </v-icon>
|
||||
<span class="open-label">DELETE (UNRECOVERABLE)</span>
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
|
||||
@@ -6,19 +6,7 @@ import { PipelineType } from "@/types/PipelineTypes";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
|
||||
const props = defineProps<{
|
||||
// TODO fully update v-model usage in custom components on Vue3 update
|
||||
value: number[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: number[]): void;
|
||||
}>();
|
||||
|
||||
const localValue = computed({
|
||||
get: () => props.value,
|
||||
set: (v) => emit("input", v)
|
||||
});
|
||||
const value = defineModel<number[]>({ required: true });
|
||||
|
||||
const driverMode = computed<boolean>({
|
||||
get: () => useCameraSettingsStore().isDriverMode,
|
||||
@@ -52,15 +40,14 @@ const fpsTooLow = computed<boolean>(() => {
|
||||
v-if="useCameraSettingsStore().currentCameraSettings.isConnected"
|
||||
label
|
||||
:color="fpsTooLow ? 'error' : 'transparent'"
|
||||
:text-color="fpsTooLow ? '#C7EA46' : '#ff4d00'"
|
||||
style="font-size: 1rem; padding: 0; margin: 0"
|
||||
>
|
||||
<span class="pr-1">
|
||||
<span class="pr-1" :style="{ color: fpsTooLow ? '#C7EA46' : '#ff4d00' }">
|
||||
{{ Math.round(useStateStore().currentPipelineResults?.fps || 0) }} FPS –
|
||||
{{ Math.min(Math.round(useStateStore().currentPipelineResults?.latency || 0), 9999) }} ms latency
|
||||
</span>
|
||||
</v-chip>
|
||||
<v-chip v-else label color="transparent" text-color="red" style="font-size: 1rem; padding: 0; margin: 0">
|
||||
<v-chip v-else label color="red" variant="text" style="font-size: 1rem; padding: 0; margin: 0">
|
||||
<span class="pr-1">Camera not connected</span>
|
||||
</v-chip>
|
||||
</div>
|
||||
@@ -98,13 +85,20 @@ const fpsTooLow = computed<boolean>(() => {
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-text class="pt-0">
|
||||
<v-btn-toggle v-model="localValue" :multiple="true" mandatory dark class="fill" style="width: 100%">
|
||||
<v-btn-toggle
|
||||
v-model="value"
|
||||
:multiple="true"
|
||||
mandatory
|
||||
class="fill"
|
||||
style="width: 100%"
|
||||
base-color="surface-variant"
|
||||
>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
class="fill"
|
||||
:disabled="useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode"
|
||||
>
|
||||
<v-icon left class="mode-btn-icon">mdi-import</v-icon>
|
||||
<v-icon start class="mode-btn-icon">mdi-import</v-icon>
|
||||
<span class="mode-btn-label">Raw</span>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
@@ -112,7 +106,7 @@ const fpsTooLow = computed<boolean>(() => {
|
||||
class="fill"
|
||||
:disabled="useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode"
|
||||
>
|
||||
<v-icon left class="mode-btn-icon">mdi-export</v-icon>
|
||||
<v-icon start class="mode-btn-icon">mdi-export</v-icon>
|
||||
<span class="mode-btn-label">Processed</span>
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
@@ -123,7 +117,6 @@ const fpsTooLow = computed<boolean>(() => {
|
||||
<style scoped>
|
||||
.v-btn-toggle.fill {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.v-btn-toggle.fill > .v-btn {
|
||||
width: 50%;
|
||||
|
||||
@@ -24,7 +24,7 @@ const cameraInfoFor: any = (camera: PVCameraInfo) => {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-simple-table dense :style="{ backgroundColor: 'var(--v-primary-base)' }">
|
||||
<v-table density="compact" :style="{ backgroundColor: 'var(--v-primary-base)' }">
|
||||
<tbody>
|
||||
<tr v-if="cameraInfoFor(camera).dev !== undefined && cameraInfoFor(camera).dev !== null">
|
||||
<td>Device Number:</td>
|
||||
@@ -66,6 +66,6 @@ const cameraInfoFor: any = (camera: PVCameraInfo) => {
|
||||
<td>{{ cameraInfoFor(camera).otherPaths }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
</v-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { PVCameraInfo } from "@/types/SettingTypes";
|
||||
import _ from "lodash";
|
||||
|
||||
function isEqual<T>(a: T, b: T): boolean {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const bothAreObjects = a && b && typeof a === "object" && typeof b === "object";
|
||||
|
||||
return (
|
||||
bothAreObjects &&
|
||||
Object.keys(a).length === Object.keys(b).length &&
|
||||
Object.entries(a).every(([k, v]) => isEqual(v, b[k as keyof T]))
|
||||
);
|
||||
}
|
||||
|
||||
const { saved, current } = defineProps({
|
||||
saved: {
|
||||
@@ -29,7 +42,7 @@ const cameraInfoFor = (camera: PVCameraInfo): any => {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-simple-table dense :style="{ backgroundColor: 'var(--v-primary-base)' }">
|
||||
<v-table density="compact" :style="{ backgroundColor: 'var(--v-primary-base)' }">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th></th>
|
||||
@@ -105,14 +118,14 @@ const cameraInfoFor = (camera: PVCameraInfo): any => {
|
||||
</tr>
|
||||
<tr
|
||||
v-if="cameraInfoFor(saved).otherPaths !== undefined && cameraInfoFor(saved).otherPaths !== null"
|
||||
:class="!_.isEqual(cameraInfoFor(saved).otherPaths, cameraInfoFor(current).otherPaths) ? 'mismatch' : ''"
|
||||
:class="isEqual(cameraInfoFor(saved).otherPaths, cameraInfoFor(current).otherPaths) ? '' : 'mismatch'"
|
||||
>
|
||||
<td>Other Paths:</td>
|
||||
<td>{{ cameraInfoFor(saved).otherPaths }}</td>
|
||||
<td>{{ cameraInfoFor(current).otherPaths }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
</v-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
const props = withDefaults(
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
iconName: string;
|
||||
disabled?: boolean;
|
||||
@@ -18,20 +18,17 @@ const props = withDefaults(
|
||||
defineEmits<{
|
||||
(e: "click"): void;
|
||||
}>();
|
||||
|
||||
const hoverClass = props.hover ? "hover" : "";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-tooltip :right="right" :bottom="!right" nudge-right="10" :disabled="tooltip === undefined">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-tooltip :right="right" :location="!right ? 'bottom' : undefined" offset="10" :disabled="tooltip === undefined">
|
||||
<template #activator="{ props }">
|
||||
<v-icon
|
||||
:class="hoverClass"
|
||||
:class="hover ? 'hover' : ''"
|
||||
:color="color"
|
||||
v-bind="attrs"
|
||||
v-bind="props"
|
||||
:disabled="disabled"
|
||||
v-on="on"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
{{ iconName }}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
|
||||
|
||||
const value = defineModel<string>({ required: true });
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
// TODO fully update v-model usage in custom components on Vue3 update
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
errorMessage?: string;
|
||||
placeholder?: string;
|
||||
@@ -22,23 +21,17 @@ const props = withDefaults(
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: string): void;
|
||||
(e: "onEnter", value: string): void;
|
||||
(e: "onEscape"): void;
|
||||
}>();
|
||||
|
||||
const localValue = computed({
|
||||
get: () => props.value,
|
||||
set: (v) => emit("input", v)
|
||||
});
|
||||
|
||||
const handleKeydown = ({ key }) => {
|
||||
switch (key) {
|
||||
case "Enter":
|
||||
// Explicitly check that all rule props return true
|
||||
if (!props.rules?.every((rule) => rule(localValue.value) === true)) return;
|
||||
if (!props.rules?.every((rule) => rule(value.value) === true)) return;
|
||||
|
||||
emit("onEnter", localValue.value);
|
||||
emit("onEnter", value.value);
|
||||
break;
|
||||
case "Escape":
|
||||
emit("onEscape");
|
||||
@@ -55,9 +48,8 @@ const handleKeydown = ({ key }) => {
|
||||
|
||||
<v-col :cols="inputCols" class="d-flex align-center pr-0">
|
||||
<v-text-field
|
||||
v-model="localValue"
|
||||
dark
|
||||
dense
|
||||
v-model="value"
|
||||
density="compact"
|
||||
color="accent"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
@@ -65,6 +57,7 @@ const handleKeydown = ({ key }) => {
|
||||
:rules="rules"
|
||||
hide-details="auto"
|
||||
class="light-error"
|
||||
variant="underlined"
|
||||
@keydown="handleKeydown"
|
||||
/>
|
||||
</v-col>
|
||||
@@ -75,9 +68,3 @@ const handleKeydown = ({ key }) => {
|
||||
margin-top: 0px;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
.light-error .error--text {
|
||||
color: red !important;
|
||||
caret-color: red !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = withDefaults(
|
||||
const value = defineModel<number>({
|
||||
required: true
|
||||
});
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
// TODO fully update v-model usage in custom components on Vue3 update
|
||||
value: number;
|
||||
disabled?: boolean;
|
||||
labelCols?: number;
|
||||
rules?: ((v: number) => boolean | string)[];
|
||||
@@ -20,13 +20,9 @@ const props = withDefaults(
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: number): void;
|
||||
}>();
|
||||
|
||||
const localValue = computed({
|
||||
get: () => props.value,
|
||||
set: (v) => emit("input", parseFloat(v as unknown as string))
|
||||
get: () => value.value,
|
||||
set: (v) => (value.value = parseFloat(v as unknown as string))
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -38,12 +34,13 @@ const localValue = computed({
|
||||
<v-col class="pr-0">
|
||||
<v-text-field
|
||||
v-model="localValue"
|
||||
dark
|
||||
class="mt-0 pt-0"
|
||||
density="compact"
|
||||
hide-details
|
||||
single-line
|
||||
color="accent"
|
||||
type="number"
|
||||
variant="underlined"
|
||||
style="width: 70px"
|
||||
:step="step"
|
||||
:disabled="disabled"
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
|
||||
const value = defineModel<number>({
|
||||
required: true
|
||||
});
|
||||
|
||||
const props = withDefaults(
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
// TODO fully update v-model usage in custom components on Vue3 update
|
||||
value: number;
|
||||
disabled?: boolean;
|
||||
inputCols?: number;
|
||||
list: string[];
|
||||
@@ -17,15 +17,6 @@ const props = withDefaults(
|
||||
inputCols: 8
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: number): void;
|
||||
}>();
|
||||
|
||||
const localValue = computed({
|
||||
get: () => props.value,
|
||||
set: (v) => emit("input", v)
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -34,13 +25,14 @@ const localValue = computed({
|
||||
<tooltipped-label :tooltip="tooltip" :label="label" />
|
||||
</v-col>
|
||||
<v-col :cols="inputCols" class="d-flex align-center pr-0">
|
||||
<v-radio-group v-model="localValue" row dark :mandatory="true" hide-details="auto">
|
||||
<v-radio-group v-model="value" row:mandatory="true" hide-details="auto">
|
||||
<v-radio
|
||||
v-for="(radioName, index) in list"
|
||||
:key="index"
|
||||
:value="index"
|
||||
color="#ffd843"
|
||||
:label="radioName"
|
||||
:value="index"
|
||||
:model-value="index"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
</v-radio-group>
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
|
||||
import type { WebsocketNumberPair } from "@/types/WebsocketDataTypes";
|
||||
|
||||
const value = defineModel<[number, number] | WebsocketNumberPair>({
|
||||
required: true
|
||||
});
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
// TODO fully update v-model usage in custom components on Vue3 update
|
||||
// value: [number, number] | WebsocketNumberPair, // Vue doesnt like Union types for the value prop for some reason.
|
||||
value: [number, number];
|
||||
min: number;
|
||||
max: number;
|
||||
step?: number;
|
||||
@@ -24,19 +25,15 @@ const props = withDefaults(
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: [number, number]): void;
|
||||
}>();
|
||||
|
||||
const localValue = computed<[number, number]>({
|
||||
get: (): [number, number] => {
|
||||
return Object.values(props.value) as [number, number];
|
||||
return Object.values(value.value) as [number, number];
|
||||
},
|
||||
set: (v) => {
|
||||
for (let i = 0; i < v.length; i++) {
|
||||
v[i] = parseFloat(v[i] as unknown as string);
|
||||
}
|
||||
emit("input", v);
|
||||
value.value = v;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -69,8 +66,7 @@ const checkNumberRange = (v: string): boolean => {
|
||||
:min="min"
|
||||
:disabled="disabled"
|
||||
hide-details
|
||||
class="align-center"
|
||||
dark
|
||||
class="align-center ml-0 mr-0"
|
||||
:color="inverted ? 'rgba(255, 255, 255, 0.2)' : 'accent'"
|
||||
:track-color="inverted ? 'accent' : undefined"
|
||||
thumb-color="accent"
|
||||
@@ -78,36 +74,38 @@ const checkNumberRange = (v: string): boolean => {
|
||||
>
|
||||
<template #prepend>
|
||||
<v-text-field
|
||||
:value="localValue[0]"
|
||||
dark
|
||||
:model-value="localValue[0]"
|
||||
color="accent"
|
||||
class="mt-0 pt-0"
|
||||
density="compact"
|
||||
hide-details
|
||||
single-line
|
||||
variant="underlined"
|
||||
:max="max"
|
||||
:min="min"
|
||||
:step="step"
|
||||
:rules="[checkNumberRange]"
|
||||
type="number"
|
||||
style="width: 60px"
|
||||
@input="(v) => changeFromSlot(v, 0)"
|
||||
@update:modelValue="(v) => changeFromSlot(v, 0)"
|
||||
/>
|
||||
</template>
|
||||
<template #append>
|
||||
<v-text-field
|
||||
:value="localValue[1]"
|
||||
dark
|
||||
:model-value="localValue[1]"
|
||||
color="accent"
|
||||
class="mt-0 pt-0"
|
||||
density="compact"
|
||||
hide-details
|
||||
single-line
|
||||
variant="underlined"
|
||||
:max="max"
|
||||
:min="min"
|
||||
:step="step"
|
||||
:rules="[checkNumberRange]"
|
||||
type="number"
|
||||
style="width: 60px"
|
||||
@input="(v) => changeFromSlot(v, 1)"
|
||||
@update:modelValue="(v) => changeFromSlot(v, 1)"
|
||||
/>
|
||||
</template>
|
||||
</v-range-slider>
|
||||
|
||||
@@ -7,14 +7,13 @@ export interface SelectItem {
|
||||
value: string | number;
|
||||
disabled?: boolean;
|
||||
}
|
||||
const value = defineModel<string | number | undefined>({ required: true });
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
selectCols?: number;
|
||||
// TODO fully update v-model usage in custom components on Vue3 update
|
||||
value: any;
|
||||
disabled?: boolean;
|
||||
items: string[] | number[] | SelectItem[];
|
||||
}>(),
|
||||
@@ -24,15 +23,6 @@ const props = withDefaults(
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: string): void;
|
||||
}>();
|
||||
|
||||
const localValue = computed({
|
||||
get: () => props.value,
|
||||
set: (v) => emit("input", v)
|
||||
});
|
||||
|
||||
// Computed in case items changes
|
||||
const items = computed<SelectItem[]>(() => {
|
||||
// Trivial case for empty list; we have no data
|
||||
@@ -55,16 +45,15 @@ const items = computed<SelectItem[]>(() => {
|
||||
</v-col>
|
||||
<v-col :cols="selectCols" class="d-flex align-center pr-0">
|
||||
<v-select
|
||||
v-model="localValue"
|
||||
v-model="value"
|
||||
:items="items"
|
||||
item-text="name"
|
||||
item-title="name"
|
||||
item-value="value"
|
||||
item-disabled="disabled"
|
||||
dark
|
||||
color="accent"
|
||||
item-color="secondary"
|
||||
item-props.disabled="disabled"
|
||||
:disabled="disabled"
|
||||
hide-details="auto"
|
||||
variant="underlined"
|
||||
density="compact"
|
||||
/>
|
||||
</v-col>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
// TODO fully update v-model usage in custom components on Vue3 update
|
||||
value: number;
|
||||
modelValue: number;
|
||||
min: number;
|
||||
max: number;
|
||||
step?: number;
|
||||
@@ -20,9 +19,8 @@ const props = withDefaults(
|
||||
sliderCols: 8
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: number): void;
|
||||
(e: "update:modelValue", value: number): void;
|
||||
}>();
|
||||
|
||||
// Debounce function
|
||||
@@ -35,11 +33,11 @@ function debounce(func: (...args: any[]) => void, wait: number) {
|
||||
}
|
||||
|
||||
const debouncedEmit = debounce((v: number) => {
|
||||
emit("input", v);
|
||||
emit("update:modelValue", v);
|
||||
}, 20);
|
||||
|
||||
const localValue = computed({
|
||||
get: () => props.value,
|
||||
get: () => props.modelValue,
|
||||
set: (v) => debouncedEmit(parseFloat(v as unknown as string))
|
||||
});
|
||||
</script>
|
||||
@@ -49,10 +47,9 @@ const localValue = computed({
|
||||
<v-col :cols="12 - sliderCols" class="pl-0 d-flex align-center">
|
||||
<tooltipped-label :tooltip="tooltip" :label="label" />
|
||||
</v-col>
|
||||
<v-col :cols="sliderCols - 1">
|
||||
<v-col :cols="sliderCols - 1" class="pl-0">
|
||||
<v-slider
|
||||
v-model="localValue"
|
||||
dark
|
||||
class="align-center"
|
||||
:max="max"
|
||||
:min="min"
|
||||
@@ -68,16 +65,17 @@ const localValue = computed({
|
||||
</v-col>
|
||||
<v-col :cols="1" class="pr-0">
|
||||
<v-text-field
|
||||
:value="localValue"
|
||||
dark
|
||||
:model-value="localValue"
|
||||
color="accent"
|
||||
:max="max"
|
||||
:min="min"
|
||||
:disabled="disabled"
|
||||
class="mt-0 pt-0"
|
||||
density="compact"
|
||||
hide-details
|
||||
single-line
|
||||
type="number"
|
||||
variant="underlined"
|
||||
style="width: 100%"
|
||||
:step="step"
|
||||
:hide-spin-buttons="true"
|
||||
|
||||
@@ -1,43 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = withDefaults(
|
||||
const value = defineModel<boolean>();
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
// TODO fully update v-model usage in custom components on Vue3 update
|
||||
value: boolean;
|
||||
disabled?: boolean;
|
||||
labelCols?: number;
|
||||
switchCols?: number;
|
||||
dense?: boolean;
|
||||
}>(),
|
||||
{
|
||||
disabled: false,
|
||||
labelCols: 2,
|
||||
switchCols: 8,
|
||||
dense: false
|
||||
switchCols: 8
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: boolean): void;
|
||||
}>();
|
||||
|
||||
const localValue = computed({
|
||||
get: () => props.value,
|
||||
set: (v) => emit("input", v)
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="d-flex">
|
||||
<v-col :cols="12 - switchCols || labelCols" class="d-flex align-center pl-0">
|
||||
<v-col :cols="12 - switchCols || labelCols" class="d-flex align-center pl-0 pt-2 pb-2">
|
||||
<tooltipped-label :tooltip="tooltip" :label="label" />
|
||||
</v-col>
|
||||
<v-col :cols="switchCols || 12 - labelCols" class="d-flex align-center pr-0">
|
||||
<v-switch v-model="localValue" dark :disabled="disabled" color="#ffd843" hide-details="auto" class="pb-1" />
|
||||
<v-col :cols="switchCols || 12 - labelCols" class="d-flex align-center pr-0 pt-2 pb-2">
|
||||
<v-switch v-model="value" :disabled="disabled" color="#ffd843" hide-details density="compact" />
|
||||
</v-col>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -7,9 +7,9 @@ defineProps<{
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-tooltip :disabled="tooltip === undefined" right open-delay="300">
|
||||
<template #activator="{ on, attrs }">
|
||||
<span style="cursor: text !important" class="white--text" v-bind="attrs" v-on="on">{{ label }}</span>
|
||||
<v-tooltip :disabled="tooltip === undefined" location="right" open-delay="300">
|
||||
<template #activator="{ props }">
|
||||
<span style="cursor: text !important" class="text-white" v-bind="props">{{ label }}</span>
|
||||
</template>
|
||||
<span>{{ tooltip }}</span>
|
||||
</v-tooltip>
|
||||
|
||||
@@ -249,7 +249,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
v-model="useStateStore().currentCameraUniqueName"
|
||||
label="Camera"
|
||||
:items="wrappedCameras"
|
||||
@input="changeCurrentCameraUniqueName"
|
||||
@update:modelValue="changeCurrentCameraUniqueName"
|
||||
/>
|
||||
<pv-input
|
||||
v-else
|
||||
@@ -270,7 +270,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
:disabled="checkCameraName(currentCameraName) !== true"
|
||||
@click="() => saveCameraNameEdit(currentCameraName)"
|
||||
/>
|
||||
<pv-icon icon-name="mdi-cancel" color="red darken-2" @click="cancelCameraNameEdit" />
|
||||
<pv-icon icon-name="mdi-cancel" color="red-darken-2" @click="cancelCameraNameEdit" />
|
||||
</div>
|
||||
<pv-icon
|
||||
v-else
|
||||
@@ -285,7 +285,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
<v-col cols="10" class="pa-0">
|
||||
<pv-select
|
||||
v-if="!isPipelineNameEdit"
|
||||
:value="useCameraSettingsStore().currentCameraSettings.currentPipelineIndex"
|
||||
:model-value="useCameraSettingsStore().currentCameraSettings.currentPipelineIndex"
|
||||
label="Pipeline"
|
||||
tooltip="Each pipeline runs on a camera output and stores a unique set of processing settings"
|
||||
:disabled="
|
||||
@@ -294,7 +294,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
!useCameraSettingsStore().hasConnected
|
||||
"
|
||||
:items="pipelineNamesWrapper"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineIndex(args, true)"
|
||||
@update:modelValue="(args) => useCameraSettingsStore().changeCurrentPipelineIndex(args, true)"
|
||||
/>
|
||||
<pv-input
|
||||
v-else
|
||||
@@ -314,13 +314,13 @@ const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
:disabled="checkPipelineName(currentPipelineName) !== true"
|
||||
@click="() => savePipelineNameEdit(currentPipelineName)"
|
||||
/>
|
||||
<pv-icon icon-name="mdi-cancel" color="red darken-2" @click="cancelPipelineNameEdit" />
|
||||
<pv-icon icon-name="mdi-cancel" color="red-darken-2" @click="cancelPipelineNameEdit" />
|
||||
</div>
|
||||
<v-menu v-else-if="!useCameraSettingsStore().isDriverMode" offset-y nudge-bottom="7" auto>
|
||||
<template #activator="{ on }">
|
||||
<v-icon color="#c5c5c5" v-on="on" @click="cancelPipelineNameEdit"> mdi-menu </v-icon>
|
||||
<v-menu v-else-if="!useCameraSettingsStore().isDriverMode" offset="7">
|
||||
<template #activator="{ props }">
|
||||
<v-icon color="#c5c5c5" v-bind="props" @click="cancelPipelineNameEdit"> mdi-menu </v-icon>
|
||||
</template>
|
||||
<v-list dark dense color="primary">
|
||||
<v-list density="compact" color="primary">
|
||||
<v-list-item @click="startPipelineNameEdit">
|
||||
<v-list-item-title>
|
||||
<pv-icon color="#c5c5c5" :right="true" icon-name="mdi-pencil" tooltip="Edit pipeline name" />
|
||||
@@ -333,7 +333,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
</v-list-item>
|
||||
<v-list-item @click="showPipelineDeletionConfirmationDialog = true">
|
||||
<v-list-item-title>
|
||||
<pv-icon color="red darken-2" :right="true" icon-name="mdi-delete" tooltip="Delete pipeline" />
|
||||
<pv-icon color="red-darken-2" :right="true" icon-name="mdi-delete" tooltip="Delete pipeline" />
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="duplicateCurrentPipeline">
|
||||
@@ -365,12 +365,12 @@ const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
!useCameraSettingsStore().hasConnected
|
||||
"
|
||||
:items="pipelineTypesWrapper"
|
||||
@input="showPipelineTypeChangeDialog = true"
|
||||
@update:modelValue="showPipelineTypeChangeDialog = true"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-dialog v-model="showPipelineCreationDialog" dark persistent width="500">
|
||||
<v-card dark color="primary">
|
||||
<v-dialog v-model="showPipelineCreationDialog" persistent width="500">
|
||||
<v-card color="primary">
|
||||
<v-card-title> Create New Pipeline </v-card-title>
|
||||
<v-card-text>
|
||||
<pv-input
|
||||
@@ -394,18 +394,18 @@ const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="#ffd843"
|
||||
class="black--text"
|
||||
:disabled="checkPipelineName(newPipelineName) !== true"
|
||||
variant="flat"
|
||||
@click="createNewPipeline"
|
||||
>
|
||||
Save
|
||||
</v-btn>
|
||||
<v-btn color="error" @click="cancelPipelineCreation"> Cancel </v-btn>
|
||||
<v-btn color="error" variant="elevated" @click="cancelPipelineCreation"> Cancel </v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<v-dialog v-model="showPipelineDeletionConfirmationDialog" dark width="500">
|
||||
<v-card dark color="primary">
|
||||
<v-dialog v-model="showPipelineDeletionConfirmationDialog" width="500">
|
||||
<v-card color="primary">
|
||||
<v-card-title> Pipeline Deletion Confirmation </v-card-title>
|
||||
<v-card-text>
|
||||
Are you sure you want to delete the pipeline
|
||||
@@ -417,8 +417,13 @@ const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
<v-divider />
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn color="error" @click="confirmDeleteCurrentPipeline"> Yes, I'm sure </v-btn>
|
||||
<v-btn color="#ffd843" class="black--text" @click="showPipelineDeletionConfirmationDialog = false">
|
||||
<v-btn variant="flat" color="error" @click="confirmDeleteCurrentPipeline"> Yes, I'm sure </v-btn>
|
||||
<v-btn
|
||||
variant="flat"
|
||||
color="#ffd843"
|
||||
class="text-black"
|
||||
@click="showPipelineDeletionConfirmationDialog = false"
|
||||
>
|
||||
No, take me back
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
@@ -435,8 +440,10 @@ const wrappedCameras = computed<SelectItem[]>(() =>
|
||||
<v-divider />
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn color="error" @click="confirmChangePipelineType"> Yes, I'm sure </v-btn>
|
||||
<v-btn color="#ffd843" class="black--text" @click="cancelChangePipelineType"> No, take me back </v-btn>
|
||||
<v-btn color="error" variant="elevated" @click="confirmChangePipelineType"> Yes, I'm sure </v-btn>
|
||||
<v-btn color="#ffd843" variant="elevated" class="text-black" @click="cancelChangePipelineType">
|
||||
No, take me back
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
@@ -6,10 +6,7 @@ import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
import { PipelineType } from "@/types/PipelineTypes";
|
||||
import PhotonCameraStream from "@/components/app/photon-camera-stream.vue";
|
||||
|
||||
defineProps<{
|
||||
// TODO fully update v-model usage in custom components on Vue3 update
|
||||
value: number[];
|
||||
}>();
|
||||
const value = defineModel<number[]>();
|
||||
|
||||
const driverMode = computed<boolean>({
|
||||
get: () => useCameraSettingsStore().isDriverMode,
|
||||
@@ -43,20 +40,21 @@ const performanceRecommendation = computed<string>(() => {
|
||||
|
||||
<template>
|
||||
<v-card color="primary" height="100%" class="d-flex flex-column" dark>
|
||||
<v-card-title class="justify-space-between align-center pt-3 pb-3">
|
||||
<v-card-title class="justify-space-between align-center pt-1 pb-1 d-flex">
|
||||
<span>Cameras</span>
|
||||
<v-chip
|
||||
v-if="useCameraSettingsStore().currentCameraSettings.isConnected"
|
||||
label
|
||||
:color="fpsTooLow ? 'error' : 'transparent'"
|
||||
:text-color="fpsTooLow ? '#C7EA46' : '#ff4d00'"
|
||||
:color="fpsTooLow ? 'error' : ''"
|
||||
style="font-size: 1rem; padding: 0; margin: 0"
|
||||
:variant="fpsTooLow ? 'tonal' : 'text'"
|
||||
:style="{ color: fpsTooLow ? '#C7EA46' : '#ff4d00' }"
|
||||
>
|
||||
<span class="pr-1"
|
||||
>Processing @ {{ Math.round(useStateStore().currentPipelineResults?.fps || 0) }} FPS –</span
|
||||
><span>{{ performanceRecommendation }}</span>
|
||||
</v-chip>
|
||||
<v-chip v-else label color="transparent" text-color="red" style="font-size: 1rem; padding: 0; margin: 0">
|
||||
<v-chip v-else label variant="text" color="red" style="font-size: 1rem; padding: 0; margin: 0">
|
||||
<span class="pr-1"> Camera not connected </span>
|
||||
</v-chip>
|
||||
<v-switch
|
||||
@@ -69,7 +67,7 @@ const performanceRecommendation = computed<string>(() => {
|
||||
</v-card-title>
|
||||
<v-divider class="ml-3 mr-3" />
|
||||
<v-row class="stream-viewer-container pa-3 align-center">
|
||||
<v-col v-if="value.includes(0)" class="stream-view">
|
||||
<v-col v-if="value?.includes(0)" class="stream-view">
|
||||
<photon-camera-stream
|
||||
id="input-camera-stream"
|
||||
:camera-settings="useCameraSettingsStore().currentCameraSettings"
|
||||
@@ -77,7 +75,7 @@ const performanceRecommendation = computed<string>(() => {
|
||||
style="width: 100%; height: auto"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col v-if="value.includes(1)" class="stream-view">
|
||||
<v-col v-if="value?.includes(1)" class="stream-view">
|
||||
<photon-camera-stream
|
||||
id="output-camera-stream"
|
||||
:camera-settings="useCameraSettingsStore().currentCameraSettings"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { Component } from "vue";
|
||||
import { computed, getCurrentInstance, onBeforeUpdate, ref } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import InputTab from "@/components/dashboard/tabs/InputTab.vue";
|
||||
@@ -14,6 +14,7 @@ import TargetsTab from "@/components/dashboard/tabs/TargetsTab.vue";
|
||||
import PnPTab from "@/components/dashboard/tabs/PnPTab.vue";
|
||||
import Map3DTab from "@/components/dashboard/tabs/Map3DTab.vue";
|
||||
import { WebsocketPipelineType } from "@/types/WebsocketDataTypes";
|
||||
import { useDisplay } from "vuetify/lib/composables/display";
|
||||
|
||||
interface ConfigOption {
|
||||
tabName: string;
|
||||
@@ -64,15 +65,12 @@ const allTabs = Object.freeze({
|
||||
});
|
||||
|
||||
const selectedTabs = ref([0, 0, 0, 0]);
|
||||
const getTabGroups = (): ConfigOption[][] => {
|
||||
const smAndDown = getCurrentInstance()?.proxy.$vuetify.breakpoint.smAndDown || false;
|
||||
const mdAndDown = getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false;
|
||||
const lgAndDown = getCurrentInstance()?.proxy.$vuetify.breakpoint.lgAndDown || false;
|
||||
const xl = getCurrentInstance()?.proxy.$vuetify.breakpoint.xl || false;
|
||||
const { smAndDown, mdAndDown, lgAndDown, xl } = useDisplay();
|
||||
|
||||
if (smAndDown || useCameraSettingsStore().isDriverMode || (mdAndDown && !useStateStore().sidebarFolded)) {
|
||||
const getTabGroups = (): ConfigOption[][] => {
|
||||
if (smAndDown.value || useCameraSettingsStore().isDriverMode) {
|
||||
return [Object.values(allTabs)];
|
||||
} else if (mdAndDown || !useStateStore().sidebarFolded) {
|
||||
} else if (mdAndDown.value || !useStateStore().sidebarFolded) {
|
||||
return [
|
||||
[
|
||||
allTabs.inputTab,
|
||||
@@ -85,7 +83,7 @@ const getTabGroups = (): ConfigOption[][] => {
|
||||
],
|
||||
[allTabs.targetsTab, allTabs.pnpTab, allTabs.map3dTab]
|
||||
];
|
||||
} else if (lgAndDown) {
|
||||
} else if (lgAndDown.value) {
|
||||
return [
|
||||
[allTabs.inputTab],
|
||||
[
|
||||
@@ -98,7 +96,7 @@ const getTabGroups = (): ConfigOption[][] => {
|
||||
],
|
||||
[allTabs.targetsTab, allTabs.pnpTab, allTabs.map3dTab]
|
||||
];
|
||||
} else if (xl) {
|
||||
} else if (xl.value) {
|
||||
return [
|
||||
[allTabs.inputTab],
|
||||
[allTabs.thresholdTab],
|
||||
@@ -135,12 +133,12 @@ const tabGroups = computed<ConfigOption[][]>(() => {
|
||||
.filter((it) => it.length); // Remove empty tab groups
|
||||
});
|
||||
|
||||
onBeforeUpdate(() => {
|
||||
const onBeforeTabUpdate = () => {
|
||||
// Force the current tab to the input tab on driver mode change
|
||||
if (useCameraSettingsStore().isDriverMode) {
|
||||
selectedTabs.value[0] = 0;
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -148,7 +146,7 @@ onBeforeUpdate(() => {
|
||||
<template v-if="!useCameraSettingsStore().hasConnected">
|
||||
<v-col cols="12">
|
||||
<v-card color="error">
|
||||
<v-card-title class="white--text">
|
||||
<v-card-title class="text-white">
|
||||
Camera has not connected. Please check your connection and try again.
|
||||
</v-card-title>
|
||||
</v-card>
|
||||
@@ -158,17 +156,12 @@ onBeforeUpdate(() => {
|
||||
<v-col
|
||||
v-for="(tabGroupData, tabGroupIndex) in tabGroups"
|
||||
:key="tabGroupIndex"
|
||||
:cols="tabGroupIndex == 1 && useCameraSettingsStore().currentPipelineSettings.doMultiTarget ? 7 : ''"
|
||||
:class="tabGroupIndex !== tabGroups.length - 1 && 'pr-3'"
|
||||
@vue:before-update="onBeforeTabUpdate"
|
||||
>
|
||||
<v-card color="primary" height="100%" class="pr-4 pl-4">
|
||||
<v-tabs
|
||||
v-model="selectedTabs[tabGroupIndex]"
|
||||
grow
|
||||
background-color="primary"
|
||||
dark
|
||||
height="48"
|
||||
slider-color="accent"
|
||||
>
|
||||
<v-tabs v-model="selectedTabs[tabGroupIndex]" grow bg-color="primary" height="48" slider-color="accent">
|
||||
<v-tab v-for="(tabConfig, index) in tabGroupData" :key="index">
|
||||
{{ tabConfig.tabName }}
|
||||
</v-tab>
|
||||
|
||||
@@ -3,19 +3,7 @@ import { computed } from "vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
|
||||
const props = defineProps<{
|
||||
// TODO fully update v-model usage in custom components on Vue3 update
|
||||
value: number[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: number[]): void;
|
||||
}>();
|
||||
|
||||
const localValue = computed({
|
||||
get: () => props.value,
|
||||
set: (v) => emit("input", v)
|
||||
});
|
||||
const value = defineModel<number[]>();
|
||||
|
||||
const processingMode = computed<number>({
|
||||
get: () => (useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled ? 1 : 0),
|
||||
@@ -32,14 +20,18 @@ const processingMode = computed<number>({
|
||||
:disabled="useCameraSettingsStore().isDriverMode || useStateStore().colorPickingMode"
|
||||
class="mt-3"
|
||||
color="primary"
|
||||
style="height: 100%; display: flex; flex-direction: column"
|
||||
style="flex-grow: 1; display: flex; flex-direction: column"
|
||||
>
|
||||
<v-row class="pa-3 pb-0 align-center">
|
||||
<v-col class="pa-4">
|
||||
<p style="color: white">Processing Mode</p>
|
||||
<v-btn-toggle v-model="processingMode" mandatory dark class="fill">
|
||||
<v-btn color="secondary" :disabled="!useCameraSettingsStore().hasConnected">
|
||||
<v-icon left>mdi-square-outline</v-icon>
|
||||
<v-btn-toggle v-model="processingMode" mandatory base-color="surface-variant" class="fill w-100">
|
||||
<v-btn
|
||||
color="secondary"
|
||||
:disabled="!useCameraSettingsStore().hasConnected"
|
||||
class="w-50"
|
||||
prepend-icon="mdi-square-outline"
|
||||
>
|
||||
<span>2D</span>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
@@ -47,8 +39,9 @@ const processingMode = computed<number>({
|
||||
:disabled="
|
||||
!useCameraSettingsStore().hasConnected || !useCameraSettingsStore().isCurrentVideoFormatCalibrated
|
||||
"
|
||||
class="w-50"
|
||||
prepend-icon="mdi-cube-outline"
|
||||
>
|
||||
<v-icon left>mdi-cube-outline</v-icon>
|
||||
<span>3D</span>
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
@@ -57,13 +50,13 @@ const processingMode = computed<number>({
|
||||
<v-row class="pa-3 pt-0 align-center">
|
||||
<v-col class="pa-4 pt-0">
|
||||
<p style="color: white">Stream Display</p>
|
||||
<v-btn-toggle v-model="localValue" :multiple="true" mandatory dark class="fill">
|
||||
<v-btn color="secondary" class="fill">
|
||||
<v-icon left class="mode-btn-icon">mdi-import</v-icon>
|
||||
<v-btn-toggle v-model="value" :multiple="true" mandatory base-color="surface-variant" class="fill w-100">
|
||||
<v-btn color="secondary" class="fill w-50">
|
||||
<v-icon start class="mode-btn-icon">mdi-import</v-icon>
|
||||
<span class="mode-btn-label">Raw</span>
|
||||
</v-btn>
|
||||
<v-btn color="secondary" class="fill">
|
||||
<v-icon left class="mode-btn-icon">mdi-export</v-icon>
|
||||
<v-btn color="secondary" class="fill w-50">
|
||||
<v-icon start class="mode-btn-icon">mdi-export</v-icon>
|
||||
<span class="mode-btn-label">Processed</span>
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
@@ -73,14 +66,8 @@ const processingMode = computed<number>({
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.v-btn-toggle.fill {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.v-btn-toggle.fill > .v-btn {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
.v-btn--disabled {
|
||||
background-color: #191919 !important;
|
||||
}
|
||||
|
||||
th {
|
||||
|
||||
@@ -3,22 +3,20 @@ import { PipelineType } from "@/types/PipelineTypes";
|
||||
import PvSelect from "@/components/common/pv-select.vue";
|
||||
import PvSlider from "@/components/common/pv-slider.vue";
|
||||
import PvSwitch from "@/components/common/pv-switch.vue";
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
import { computed } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import type { ActivePipelineSettings } from "@/types/PipelineTypes";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { useDisplay } from "vuetify";
|
||||
|
||||
// TODO fix pipeline typing in order to fix this, the store settings call should be able to infer that only valid pipeline type settings are exposed based on pre-checks for the entire config section
|
||||
// Defer reference to store access method
|
||||
const currentPipelineSettings = computed<ActivePipelineSettings>(
|
||||
() => useCameraSettingsStore().currentPipelineSettings
|
||||
);
|
||||
|
||||
const { mdAndDown } = useDisplay();
|
||||
const interactiveCols = computed(() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
? 8
|
||||
: 7
|
||||
mdAndDown.value && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode) ? 8 : 7
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -29,7 +27,7 @@ const interactiveCols = computed(() =>
|
||||
label="Target family"
|
||||
:items="['AprilTag 36h11 (6.5in)', 'AprilTag 16h5 (6in)']"
|
||||
:select-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)"
|
||||
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.decimate"
|
||||
@@ -38,7 +36,7 @@ const interactiveCols = computed(() =>
|
||||
tooltip="Increases FPS at the expense of range by reducing image resolution initially"
|
||||
:min="1"
|
||||
:max="8"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ decimate: value }, false)"
|
||||
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ decimate: value }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.blur"
|
||||
@@ -48,7 +46,7 @@ const interactiveCols = computed(() =>
|
||||
:min="0"
|
||||
:max="5"
|
||||
:step="0.1"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ blur: value }, false)"
|
||||
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ blur: value }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.threads"
|
||||
@@ -57,7 +55,7 @@ const interactiveCols = computed(() =>
|
||||
tooltip="Number of threads spawned by the AprilTag detector"
|
||||
:min="1"
|
||||
:max="8"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threads: value }, false)"
|
||||
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threads: value }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.decisionMargin"
|
||||
@@ -66,7 +64,9 @@ const interactiveCols = computed(() =>
|
||||
tooltip="Tags with a 'margin' (decoding quality score) less than this wil be rejected. Increase this to reduce the number of false positive detections"
|
||||
:min="0"
|
||||
:max="250"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ decisionMargin: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ decisionMargin: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.numIterations"
|
||||
@@ -75,14 +75,18 @@ const interactiveCols = computed(() =>
|
||||
tooltip="Number of iterations the pose estimation algorithm will run, 50-100 is a good starting point"
|
||||
:min="0"
|
||||
:max="500"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ numIterations: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ numIterations: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-switch
|
||||
v-model="currentPipelineSettings.refineEdges"
|
||||
:switch-cols="interactiveCols"
|
||||
label="Refine Edges"
|
||||
tooltip="Further refines the AprilTag corner position initial estimate, suggested left on"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ refineEdges: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ refineEdges: value }, false)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -5,20 +5,18 @@ import PvSlider from "@/components/common/pv-slider.vue";
|
||||
import PvSwitch from "@/components/common/pv-switch.vue";
|
||||
import PvRangeSlider from "@/components/common/pv-range-slider.vue";
|
||||
import PvSelect from "@/components/common/pv-select.vue";
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
import { computed } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { useDisplay } from "vuetify";
|
||||
|
||||
// TODO fix pipeline typing in order to fix this, the store settings call should be able to infer that only valid pipeline type settings are exposed based on pre-checks for the entire config section
|
||||
// Defer reference to store access method
|
||||
const currentPipelineSettings = computed<ActivePipelineSettings>(
|
||||
() => useCameraSettingsStore().currentPipelineSettings
|
||||
);
|
||||
|
||||
const { mdAndDown } = useDisplay();
|
||||
const interactiveCols = computed(() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
? 8
|
||||
: 7
|
||||
mdAndDown.value && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode) ? 8 : 7
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -29,7 +27,7 @@ const interactiveCols = computed(() =>
|
||||
label="Target family"
|
||||
:items="['AprilTag Family 36h11', 'AprilTag Family 16h5']"
|
||||
:select-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)"
|
||||
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)"
|
||||
/>
|
||||
<pv-range-slider
|
||||
v-model="currentPipelineSettings.threshWinSizes"
|
||||
@@ -39,7 +37,9 @@ const interactiveCols = computed(() =>
|
||||
:max="255"
|
||||
:slider-cols="interactiveCols"
|
||||
:step="2"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threshWinSizes: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threshWinSizes: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.threshStepSize"
|
||||
@@ -49,7 +49,9 @@ const interactiveCols = computed(() =>
|
||||
:min="2"
|
||||
:max="128"
|
||||
:step="1"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threshStepSize: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threshStepSize: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.threshConstant"
|
||||
@@ -59,21 +61,27 @@ const interactiveCols = computed(() =>
|
||||
:min="0"
|
||||
:max="128"
|
||||
:step="1"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threshConstant: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threshConstant: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-switch
|
||||
v-model="currentPipelineSettings.useCornerRefinement"
|
||||
label="Refine Corners"
|
||||
tooltip="Further refine the initial corners with subpixel accuracy."
|
||||
:switch-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ useCornerRefinement: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ useCornerRefinement: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-switch
|
||||
v-model="currentPipelineSettings.debugThreshold"
|
||||
label="Debug Threshold"
|
||||
tooltip="Display the first threshold step to the color stream."
|
||||
:switch-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ debugThreshold: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ debugThreshold: value }, false)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -4,8 +4,9 @@ import { type ActivePipelineSettings, PipelineType } from "@/types/PipelineTypes
|
||||
import PvRangeSlider from "@/components/common/pv-range-slider.vue";
|
||||
import PvSelect from "@/components/common/pv-select.vue";
|
||||
import PvSlider from "@/components/common/pv-slider.vue";
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
import { computed } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { useDisplay } from "vuetify";
|
||||
|
||||
// TODO fix pipeline typing in order to fix this, the store settings call should be able to infer that only valid pipeline type settings are exposed based on pre-checks for the entire config section
|
||||
// Defer reference to store access method
|
||||
@@ -48,12 +49,9 @@ const contourRadius = computed<[number, number]>({
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const { mdAndDown } = useDisplay();
|
||||
const interactiveCols = computed(() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
? 8
|
||||
: 7
|
||||
mdAndDown.value && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode) ? 8 : 7
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -65,7 +63,7 @@ const interactiveCols = computed(() =>
|
||||
tooltip="Used to determine how to calculate target landmarks, as well as aspect ratio"
|
||||
:items="['Portrait', 'Landscape']"
|
||||
:select-cols="interactiveCols"
|
||||
@input="
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
|
||||
"
|
||||
/>
|
||||
@@ -75,7 +73,9 @@ const interactiveCols = computed(() =>
|
||||
tooltip="Chooses the sorting mode used to determine the 'best' targets to provide to user code"
|
||||
:select-cols="interactiveCols"
|
||||
:items="['Largest', 'Smallest', 'Highest', 'Lowest', 'Rightmost', 'Leftmost', 'Centermost']"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-range-slider
|
||||
v-model="contourArea"
|
||||
@@ -84,7 +84,9 @@ const interactiveCols = computed(() =>
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
:step="0.01"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourArea: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourArea: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-range-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineType !== PipelineType.ColoredShape"
|
||||
@@ -95,7 +97,9 @@ const interactiveCols = computed(() =>
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
:step="0.1"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRatio: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRatio: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-range-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineType === PipelineType.ColoredShape"
|
||||
@@ -105,7 +109,9 @@ const interactiveCols = computed(() =>
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFullness: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFullness: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-range-slider
|
||||
v-if="currentPipelineSettings.pipelineType === PipelineType.ColoredShape"
|
||||
@@ -115,7 +121,9 @@ const interactiveCols = computed(() =>
|
||||
:min="0"
|
||||
:max="4000"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourPerimeter: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourPerimeter: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourSpecklePercentage"
|
||||
@@ -124,7 +132,7 @@ const interactiveCols = computed(() =>
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSpecklePercentage: value }, false)
|
||||
"
|
||||
/>
|
||||
@@ -137,7 +145,9 @@ const interactiveCols = computed(() =>
|
||||
:max="4"
|
||||
:step="0.1"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFilterRangeX: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFilterRangeX: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.contourFilterRangeY"
|
||||
@@ -147,7 +157,9 @@ const interactiveCols = computed(() =>
|
||||
:max="4"
|
||||
:step="0.1"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFilterRangeY: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFilterRangeY: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourGroupingMode"
|
||||
@@ -155,7 +167,9 @@ const interactiveCols = computed(() =>
|
||||
tooltip="Whether or not every two targets are paired with each other (good for e.g. 2019 targets)"
|
||||
:select-cols="interactiveCols"
|
||||
:items="['Single', 'Dual', 'Two or More']"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourGroupingMode: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourGroupingMode: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourIntersection"
|
||||
@@ -164,7 +178,9 @@ const interactiveCols = computed(() =>
|
||||
:select-cols="interactiveCols"
|
||||
:items="['None', 'Up', 'Down', 'Left', 'Right']"
|
||||
:disabled="useCameraSettingsStore().currentPipelineSettings.contourGroupingMode === 0"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourIntersection: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourIntersection: value }, false)
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="currentPipelineSettings.pipelineType === PipelineType.ColoredShape">
|
||||
@@ -174,7 +190,9 @@ const interactiveCols = computed(() =>
|
||||
tooltip="The shape of targets to look for"
|
||||
:select-cols="interactiveCols"
|
||||
:items="['Circle', 'Polygon', 'Triangle', 'Quadrilateral']"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourShape: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourShape: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-if="currentPipelineSettings.contourShape >= 1"
|
||||
@@ -185,7 +203,9 @@ const interactiveCols = computed(() =>
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ accuracyPercentage: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ accuracyPercentage: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-if="currentPipelineSettings.contourShape === 0"
|
||||
@@ -196,7 +216,7 @@ const interactiveCols = computed(() =>
|
||||
:min="1"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ circleDetectThreshold: value }, false)
|
||||
"
|
||||
/>
|
||||
@@ -208,7 +228,9 @@ const interactiveCols = computed(() =>
|
||||
:min="1"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ maxCannyThresh: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ maxCannyThresh: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-if="currentPipelineSettings.contourShape === 0"
|
||||
@@ -218,7 +240,9 @@ const interactiveCols = computed(() =>
|
||||
:min="1"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ circleAccuracy: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ circleAccuracy: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-range-slider
|
||||
v-if="currentPipelineSettings.contourShape === 0"
|
||||
@@ -228,7 +252,9 @@ const interactiveCols = computed(() =>
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRadius: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRadius: value }, false)
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -3,10 +3,11 @@ import PvSlider from "@/components/common/pv-slider.vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import PvSwitch from "@/components/common/pv-switch.vue";
|
||||
import PvSelect from "@/components/common/pv-select.vue";
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
import { computed } from "vue";
|
||||
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { getResolutionString } from "@/lib/PhotonUtils";
|
||||
import { useDisplay } from "vuetify";
|
||||
|
||||
// Due to something with libcamera or something else IDK much about, the 90° rotations need to be disabled if the libcamera drivers are being used.
|
||||
const cameraRotations = computed(() =>
|
||||
@@ -62,12 +63,10 @@ const handleStreamResolutionChange = (value: number) => {
|
||||
false
|
||||
);
|
||||
};
|
||||
const { mdAndDown } = useDisplay();
|
||||
|
||||
const interactiveCols = computed(() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
? 8
|
||||
: 7
|
||||
mdAndDown.value && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode) ? 8 : 7
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -75,11 +74,12 @@ const interactiveCols = computed(() =>
|
||||
<div>
|
||||
<pv-switch
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraAutoExposure"
|
||||
class="pt-2"
|
||||
label="Auto Exposure"
|
||||
:switch-cols="interactiveCols"
|
||||
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoExposure: args }, false)"
|
||||
@update:modelValue="
|
||||
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoExposure: args }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraExposureRaw"
|
||||
@@ -90,7 +90,9 @@ const interactiveCols = computed(() =>
|
||||
:max="useCameraSettingsStore().maxExposureRaw"
|
||||
:slider-cols="interactiveCols"
|
||||
:step="1"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraExposureRaw: args }, false)"
|
||||
@update:modelValue="
|
||||
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraExposureRaw: args }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBrightness"
|
||||
@@ -98,7 +100,9 @@ const interactiveCols = computed(() =>
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBrightness: args }, false)"
|
||||
@update:modelValue="
|
||||
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBrightness: args }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.cameraGain >= 0"
|
||||
@@ -108,7 +112,7 @@ const interactiveCols = computed(() =>
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraGain: args }, false)"
|
||||
@update:modelValue="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraGain: args }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.cameraRedGain !== -1"
|
||||
@@ -118,7 +122,9 @@ const interactiveCols = computed(() =>
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
tooltip="Controls red automatic white balance gain, which affects how the camera captures colors in different conditions"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraRedGain: args }, false)"
|
||||
@update:modelValue="
|
||||
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraRedGain: args }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain !== -1"
|
||||
@@ -128,14 +134,18 @@ const interactiveCols = computed(() =>
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
tooltip="Controls blue automatic white balance gain, which affects how the camera captures colors in different conditions"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBlueGain: args }, false)"
|
||||
@update:modelValue="
|
||||
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBlueGain: args }, false)
|
||||
"
|
||||
/>
|
||||
<pv-switch
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraAutoWhiteBalance"
|
||||
label="Auto White Balance"
|
||||
:switch-cols="interactiveCols"
|
||||
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoWhiteBalance: args }, false)"
|
||||
@update:modelValue="
|
||||
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoWhiteBalance: args }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraWhiteBalanceTemp"
|
||||
@@ -144,7 +154,9 @@ const interactiveCols = computed(() =>
|
||||
:min="useCameraSettingsStore().minWhiteBalanceTemp"
|
||||
:max="useCameraSettingsStore().maxWhiteBalanceTemp"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraWhiteBalanceTemp: args }, false)"
|
||||
@update:modelValue="
|
||||
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraWhiteBalanceTemp: args }, false)
|
||||
"
|
||||
/>
|
||||
<pv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.inputImageRotationMode"
|
||||
@@ -152,7 +164,9 @@ const interactiveCols = computed(() =>
|
||||
tooltip="Rotates the camera stream. Rotation not available when camera has been calibrated."
|
||||
:items="cameraRotations"
|
||||
:select-cols="interactiveCols"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ inputImageRotationMode: args }, false)"
|
||||
@update:modelValue="
|
||||
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ inputImageRotationMode: args }, false)
|
||||
"
|
||||
/>
|
||||
<pv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraVideoModeIndex"
|
||||
@@ -160,7 +174,7 @@ const interactiveCols = computed(() =>
|
||||
tooltip="Resolution and FPS the camera should directly capture at"
|
||||
:items="cameraResolutions"
|
||||
:select-cols="interactiveCols"
|
||||
@input="(args) => handleResolutionChange(args)"
|
||||
@update:modelValue="(args) => handleResolutionChange(args)"
|
||||
/>
|
||||
<pv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor"
|
||||
@@ -168,7 +182,7 @@ const interactiveCols = computed(() =>
|
||||
tooltip="Resolution to which camera frames are downscaled for streaming to the dashboard"
|
||||
:items="streamResolutions"
|
||||
:select-cols="interactiveCols"
|
||||
@input="(args) => handleStreamResolutionChange(args)"
|
||||
@update:modelValue="(args) => handleStreamResolutionChange(args)"
|
||||
/>
|
||||
<pv-switch
|
||||
v-if="useCameraSettingsStore().isDriverMode"
|
||||
@@ -176,7 +190,7 @@ const interactiveCols = computed(() =>
|
||||
label="Crosshair"
|
||||
:switch-cols="interactiveCols"
|
||||
tooltip="Enables or disables a crosshair overlay on the camera stream"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ crosshair: args }, false)"
|
||||
@update:modelValue="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ crosshair: args }, false)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -11,7 +11,7 @@ const trackedTargets = computed<PhotonTarget[]>(() => useStateStore().currentPip
|
||||
<div>
|
||||
<v-row style="width: 100%">
|
||||
<v-col>
|
||||
<span class="white--text">Target Visualization</span>
|
||||
<span class="text-white">Target Visualization</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row style="width: 100%">
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { type ObjectDetectionPipelineSettings, PipelineType } from "@/types/PipelineTypes";
|
||||
import PvSlider from "@/components/common/pv-slider.vue";
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
import PvSelect from "@/components/common/pv-select.vue";
|
||||
import PvRangeSlider from "@/components/common/pv-range-slider.vue";
|
||||
import { computed } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
import { useDisplay } from "vuetify";
|
||||
|
||||
// TODO fix pipeline typing in order to fix this, the store settings call should be able to infer that only valid pipeline type settings are exposed based on pre-checks for the entire config section
|
||||
// Defer reference to store access method
|
||||
@@ -22,11 +25,10 @@ const contourRatio = computed<[number, number]>({
|
||||
set: (v) => (useCameraSettingsStore().currentPipelineSettings.contourRatio = v)
|
||||
});
|
||||
|
||||
const { mdAndDown } = useDisplay();
|
||||
|
||||
const interactiveCols = computed(() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
? 9
|
||||
: 8
|
||||
mdAndDown.value && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode) ? 9 : 8
|
||||
);
|
||||
|
||||
// Filters out models that are not supported by the current backend, and returns a flattened list.
|
||||
@@ -36,10 +38,11 @@ const supportedModels = computed(() => {
|
||||
});
|
||||
|
||||
const selectedModel = computed({
|
||||
get: () => supportedModels.value.indexOf(currentPipelineSettings.value.model),
|
||||
set: (v) => {
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting({ model: supportedModels.value[v] }, false);
|
||||
}
|
||||
get: () => {
|
||||
const index = supportedModels.value.indexOf(currentPipelineSettings.value.model);
|
||||
return index === -1 ? undefined : index;
|
||||
},
|
||||
set: (v) => v && useCameraSettingsStore().changeCurrentPipelineSetting({ model: supportedModels.value[v] }, false)
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -61,7 +64,9 @@ const selectedModel = computed({
|
||||
:min="0"
|
||||
:max="1"
|
||||
:step="0.01"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ confidence: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ confidence: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-range-slider
|
||||
v-model="contourArea"
|
||||
@@ -70,7 +75,9 @@ const selectedModel = computed({
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
:step="0.01"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourArea: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourArea: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-range-slider
|
||||
v-model="contourRatio"
|
||||
@@ -80,7 +87,9 @@ const selectedModel = computed({
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
:step="0.01"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRatio: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRatio: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOrientation"
|
||||
@@ -88,7 +97,7 @@ const selectedModel = computed({
|
||||
tooltip="Used to determine how to calculate target landmarks, as well as aspect ratio"
|
||||
:items="['Portrait', 'Landscape']"
|
||||
:select-cols="interactiveCols"
|
||||
@input="
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
|
||||
"
|
||||
/>
|
||||
@@ -98,7 +107,9 @@ const selectedModel = computed({
|
||||
tooltip="Chooses the sorting mode used to determine the 'best' targets to provide to user code"
|
||||
:select-cols="interactiveCols"
|
||||
:items="['Largest', 'Smallest', 'Highest', 'Lowest', 'Rightmost', 'Leftmost', 'Centermost']"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -3,9 +3,10 @@ import PvSelect from "@/components/common/pv-select.vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { type ActivePipelineSettings, PipelineType, RobotOffsetPointMode } from "@/types/PipelineTypes";
|
||||
import PvSwitch from "@/components/common/pv-switch.vue";
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
import { computed } from "vue";
|
||||
import { RobotOffsetType } from "@/types/SettingTypes";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { useDisplay } from "vuetify";
|
||||
|
||||
const isTagPipeline = computed(
|
||||
() =>
|
||||
@@ -45,12 +46,10 @@ const offsetPoints = computed<MetricItem[]>(() => {
|
||||
const currentPipelineSettings = computed<ActivePipelineSettings>(
|
||||
() => useCameraSettingsStore().currentPipelineSettings
|
||||
);
|
||||
const { mdAndDown } = useDisplay();
|
||||
|
||||
const interactiveCols = computed(() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
? 8
|
||||
: 7
|
||||
mdAndDown.value && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode) ? 8 : 7
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -62,7 +61,7 @@ const interactiveCols = computed(() =>
|
||||
tooltip="If enabled, up to five targets will be displayed and sent via PhotonLib, instead of just one"
|
||||
:disabled="isTagPipeline"
|
||||
:switch-cols="interactiveCols"
|
||||
@input="
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ outputShowMultipleTargets: value }, false)
|
||||
"
|
||||
/>
|
||||
@@ -78,7 +77,9 @@ const interactiveCols = computed(() =>
|
||||
tooltip="If enabled, all visible fiducial targets will be used to provide a single pose estimate from their combined model."
|
||||
:switch-cols="interactiveCols"
|
||||
:disabled="!isTagPipeline"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ doMultiTarget: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ doMultiTarget: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-switch
|
||||
v-if="
|
||||
@@ -92,7 +93,9 @@ const interactiveCols = computed(() =>
|
||||
tooltip="If disabled, visible fiducial targets used for multi-target estimation will not also be used for single-target estimation."
|
||||
:switch-cols="interactiveCols"
|
||||
:disabled="!isTagPipeline || !currentPipelineSettings.doMultiTarget"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ doSingleTargetAlways: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ doSingleTargetAlways: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOffsetPointEdge"
|
||||
@@ -100,7 +103,7 @@ const interactiveCols = computed(() =>
|
||||
tooltip="Changes where the 'center' of the target is (used for calculating e.g. pitch and yaw)"
|
||||
:items="['Center', 'Top', 'Bottom', 'Left', 'Right']"
|
||||
:select-cols="interactiveCols"
|
||||
@input="
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOffsetPointEdge: value }, false)
|
||||
"
|
||||
/>
|
||||
@@ -111,7 +114,7 @@ const interactiveCols = computed(() =>
|
||||
tooltip="Used to determine how to calculate target landmarks (e.g. the top, left, or bottom of the target)"
|
||||
:items="['Portrait', 'Landscape']"
|
||||
:select-cols="interactiveCols"
|
||||
@input="
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
|
||||
"
|
||||
/>
|
||||
@@ -121,7 +124,9 @@ const interactiveCols = computed(() =>
|
||||
tooltip="Used to add an arbitrary offset to the location of the targeting crosshair"
|
||||
:items="['None', 'Single Point', 'Dual Point']"
|
||||
:select-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ offsetRobotOffsetMode: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ offsetRobotOffsetMode: value }, false)
|
||||
"
|
||||
/>
|
||||
<table
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode !== RobotOffsetPointMode.None"
|
||||
@@ -148,10 +153,10 @@ const interactiveCols = computed(() =>
|
||||
>
|
||||
<v-col cols="6" class="pl-0">
|
||||
<v-btn
|
||||
small
|
||||
size="small"
|
||||
block
|
||||
color="accent"
|
||||
class="black--text"
|
||||
class="text-black"
|
||||
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Single)"
|
||||
>
|
||||
Take Point
|
||||
@@ -159,9 +164,9 @@ const interactiveCols = computed(() =>
|
||||
</v-col>
|
||||
<v-col cols="6" class="pr-0">
|
||||
<v-btn
|
||||
small
|
||||
size="small"
|
||||
block
|
||||
color="yellow darken-3"
|
||||
color="yellow-darken-3"
|
||||
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Clear)"
|
||||
>
|
||||
Clear All Points
|
||||
@@ -174,10 +179,10 @@ const interactiveCols = computed(() =>
|
||||
>
|
||||
<v-col cols="6" lg="4" class="pl-0 pr-2">
|
||||
<v-btn
|
||||
small
|
||||
size="small"
|
||||
block
|
||||
color="accent"
|
||||
class="black--text"
|
||||
class="text-black"
|
||||
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.DualFirst)"
|
||||
>
|
||||
Take First Point
|
||||
@@ -185,10 +190,10 @@ const interactiveCols = computed(() =>
|
||||
</v-col>
|
||||
<v-col cols="6" lg="4" class="pl-2 pr-0 pr-lg-2">
|
||||
<v-btn
|
||||
small
|
||||
size="small"
|
||||
block
|
||||
color="accent"
|
||||
class="black--text"
|
||||
class="text-black"
|
||||
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.DualSecond)"
|
||||
>
|
||||
Take Second Point
|
||||
@@ -196,9 +201,9 @@ const interactiveCols = computed(() =>
|
||||
</v-col>
|
||||
<v-col cols="12" lg="4" class="pl-0 pl-lg-2 pr-0">
|
||||
<v-btn
|
||||
small
|
||||
size="small"
|
||||
block
|
||||
color="yellow darken-3"
|
||||
color="yellow-darken-3"
|
||||
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Clear)"
|
||||
>
|
||||
Clear All Points
|
||||
|
||||
@@ -3,14 +3,13 @@ import PvSelect from "@/components/common/pv-select.vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { TargetModel } from "@/types/PipelineTypes";
|
||||
import PvSlider from "@/components/common/pv-slider.vue";
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
import { computed } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { useDisplay } from "vuetify";
|
||||
const { mdAndDown } = useDisplay();
|
||||
|
||||
const interactiveCols = computed(() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
? 9
|
||||
: 8
|
||||
mdAndDown.value && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode) ? 9 : 8
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -30,7 +29,9 @@ const interactiveCols = computed(() =>
|
||||
{ name: '2025 Algae (16.25in)', value: TargetModel.ReefscapeAlgae }
|
||||
]"
|
||||
:select-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ targetModel: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ targetModel: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cornerDetectionAccuracyPercentage"
|
||||
@@ -39,7 +40,7 @@ const interactiveCols = computed(() =>
|
||||
label="Contour simplification Percentage"
|
||||
:min="0"
|
||||
:max="100"
|
||||
@input="
|
||||
@update:modelValue="
|
||||
(value) =>
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting({ cornerDetectionAccuracyPercentage: value }, false)
|
||||
"
|
||||
|
||||
@@ -34,8 +34,8 @@ const resetCurrentBuffer = () => {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-row align="start" class="pb-4">
|
||||
<v-simple-table dense class="pt-2 pb-12">
|
||||
<v-row class="pb-4">
|
||||
<v-table density="compact" class="pt-2 pb-12 pl-3 pr-3">
|
||||
<template #default>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -44,24 +44,24 @@ const resetCurrentBuffer = () => {
|
||||
currentPipelineSettings.pipelineType === PipelineType.AprilTag ||
|
||||
currentPipelineSettings.pipelineType === PipelineType.Aruco
|
||||
"
|
||||
class="text-center white--text"
|
||||
class="text-center text-white"
|
||||
>
|
||||
Fiducial ID
|
||||
</th>
|
||||
<template v-if="currentPipelineSettings.pipelineType === PipelineType.ObjectDetection">
|
||||
<th class="text-center white--text">Class</th>
|
||||
<th class="text-center white--text">Confidence</th>
|
||||
<th class="text-center text-white">Class</th>
|
||||
<th class="text-center text-white">Confidence</th>
|
||||
</template>
|
||||
<template v-if="!useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled">
|
||||
<th class="text-center white--text">Pitch θ°</th>
|
||||
<th class="text-center white--text">Yaw θ°</th>
|
||||
<th class="text-center white--text">Skew θ°</th>
|
||||
<th class="text-center white--text">Area %</th>
|
||||
<th class="text-center text-white">Pitch θ°</th>
|
||||
<th class="text-center text-white">Yaw θ°</th>
|
||||
<th class="text-center text-white">Skew θ°</th>
|
||||
<th class="text-center text-white">Area %</th>
|
||||
</template>
|
||||
<template v-else>
|
||||
<th class="text-center white--text">X meters</th>
|
||||
<th class="text-center white--text">Y meters</th>
|
||||
<th class="text-center white--text">Z Angle θ°</th>
|
||||
<th class="text-center text-white">X meters</th>
|
||||
<th class="text-center text-white">Y meters</th>
|
||||
<th class="text-center text-white">Z Angle θ°</th>
|
||||
</template>
|
||||
<template
|
||||
v-if="
|
||||
@@ -70,7 +70,7 @@ const resetCurrentBuffer = () => {
|
||||
useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled
|
||||
"
|
||||
>
|
||||
<th class="text-center white--text">Ambiguity Ratio</th>
|
||||
<th class="text-center text-white">Ambiguity Ratio</th>
|
||||
</template>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -78,7 +78,7 @@ const resetCurrentBuffer = () => {
|
||||
<tr
|
||||
v-for="(target, index) in useStateStore().currentPipelineResults?.targets"
|
||||
:key="index"
|
||||
class="white--text"
|
||||
class="text-white"
|
||||
>
|
||||
<td
|
||||
v-if="
|
||||
@@ -91,13 +91,13 @@ const resetCurrentBuffer = () => {
|
||||
</td>
|
||||
<td
|
||||
v-if="currentPipelineSettings.pipelineType === PipelineType.ObjectDetection"
|
||||
class="text-center white--text"
|
||||
class="text-center text-white"
|
||||
>
|
||||
{{ useStateStore().currentPipelineResults?.classNames[target.classId] }}
|
||||
</td>
|
||||
<td
|
||||
v-if="currentPipelineSettings.pipelineType === PipelineType.ObjectDetection"
|
||||
class="text-center white--text"
|
||||
class="text-center text-white"
|
||||
>
|
||||
{{ target.confidence.toFixed(2) }}
|
||||
</td>
|
||||
@@ -126,7 +126,7 @@ const resetCurrentBuffer = () => {
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-table>
|
||||
</v-row>
|
||||
<v-container
|
||||
v-if="
|
||||
@@ -136,122 +136,123 @@ const resetCurrentBuffer = () => {
|
||||
useCameraSettingsStore().isCurrentVideoFormatCalibrated &&
|
||||
useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled
|
||||
"
|
||||
class="pl-3 pr-3"
|
||||
>
|
||||
<v-row class="pb-4 white--text">
|
||||
<v-row class="pb-4 text-white">
|
||||
<v-card-subtitle class="ma-0 pa-0 pb-4" style="font-size: 16px"
|
||||
>Multi-tag pose, field-to-camera</v-card-subtitle
|
||||
>
|
||||
<v-simple-table dense>
|
||||
<v-table density="compact">
|
||||
<template #default>
|
||||
<thead>
|
||||
<tr class="white--text">
|
||||
<th class="text-center white--text">X meters</th>
|
||||
<th class="text-center white--text">Y meters</th>
|
||||
<th class="text-center white--text">Z meters</th>
|
||||
<th class="text-center white--text">X Angle θ°</th>
|
||||
<th class="text-center white--text">Y Angle θ°</th>
|
||||
<th class="text-center white--text">Z Angle θ°</th>
|
||||
<th class="text-center white--text">Tags</th>
|
||||
<tr class="text-white">
|
||||
<th class="text-center text-white">X meters</th>
|
||||
<th class="text-center text-white">Y meters</th>
|
||||
<th class="text-center text-white">Z meters</th>
|
||||
<th class="text-center text-white">X Angle θ°</th>
|
||||
<th class="text-center text-white">Y Angle θ°</th>
|
||||
<th class="text-center text-white">Z Angle θ°</th>
|
||||
<th class="text-center text-white">Tags</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody v-show="useStateStore().currentPipelineResults?.multitagResult">
|
||||
<tr>
|
||||
<td class="text-center white--text">
|
||||
<td class="text-center text-white">
|
||||
{{ useStateStore().currentPipelineResults?.multitagResult?.bestTransform.x.toFixed(3) }} m
|
||||
</td>
|
||||
<td class="text-center white--text">
|
||||
<td class="text-center text-white">
|
||||
{{ useStateStore().currentPipelineResults?.multitagResult?.bestTransform.y.toFixed(3) }} m
|
||||
</td>
|
||||
<td class="text-center white--text">
|
||||
<td class="text-center text-white">
|
||||
{{ useStateStore().currentPipelineResults?.multitagResult?.bestTransform.z.toFixed(3) }} m
|
||||
</td>
|
||||
<td class="text-center white--text">
|
||||
<td class="text-center text-white">
|
||||
{{
|
||||
toDeg(useStateStore().currentPipelineResults?.multitagResult?.bestTransform.angle_x || 0).toFixed(
|
||||
2
|
||||
)
|
||||
}}°
|
||||
</td>
|
||||
<td class="text-center white--text">
|
||||
<td class="text-center text-white">
|
||||
{{
|
||||
toDeg(useStateStore().currentPipelineResults?.multitagResult?.bestTransform.angle_y || 0).toFixed(
|
||||
2
|
||||
)
|
||||
}}°
|
||||
</td>
|
||||
<td class="text-center white--text">
|
||||
<td class="text-center text-white">
|
||||
{{
|
||||
toDeg(useStateStore().currentPipelineResults?.multitagResult?.bestTransform.angle_z || 0).toFixed(
|
||||
2
|
||||
)
|
||||
}}°
|
||||
</td>
|
||||
<td class="text-center white--text">
|
||||
<td class="text-center text-white">
|
||||
{{ useStateStore().currentPipelineResults?.multitagResult?.fiducialIDsUsed }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-table>
|
||||
</v-row>
|
||||
<v-row class="pb-4 white--text" style="display: flex; flex-direction: column">
|
||||
<v-row class="pb-4 text-white" style="display: flex; flex-direction: column">
|
||||
<v-card-subtitle class="ma-0 pa-0 pb-4 pr-4" style="font-size: 16px"
|
||||
>Multi-tag pose standard deviation over the last
|
||||
{{ useStateStore().currentMultitagBuffer?.length || "NaN" }}/100 samples
|
||||
</v-card-subtitle>
|
||||
<v-btn color="secondary" class="mb-4 mt-1" style="width: min-content" depressed @click="resetCurrentBuffer"
|
||||
<v-btn color="secondary" class="mb-4 mt-1" style="width: min-content" variant="flat" @click="resetCurrentBuffer"
|
||||
>Reset Samples</v-btn
|
||||
>
|
||||
<v-simple-table dense>
|
||||
<v-table density="compact">
|
||||
<template #default>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center white--text">X meters</th>
|
||||
<th class="text-center white--text">Y meters</th>
|
||||
<th class="text-center white--text">Z meters</th>
|
||||
<th class="text-center white--text">X Angle θ°</th>
|
||||
<th class="text-center white--text">Y Angle θ°</th>
|
||||
<th class="text-center white--text">Z Angle θ°</th>
|
||||
<th class="text-center text-white">X meters</th>
|
||||
<th class="text-center text-white">Y meters</th>
|
||||
<th class="text-center text-white">Z meters</th>
|
||||
<th class="text-center text-white">X Angle θ°</th>
|
||||
<th class="text-center text-white">Y Angle θ°</th>
|
||||
<th class="text-center text-white">Z Angle θ°</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody v-show="useStateStore().currentPipelineResults?.multitagResult">
|
||||
<tr>
|
||||
<td class="text-center white--text">
|
||||
<td class="text-center text-white">
|
||||
{{
|
||||
calculateStdDev(useStateStore().currentMultitagBuffer?.map((v) => v.bestTransform.x) || []).toFixed(
|
||||
5
|
||||
)
|
||||
}} m
|
||||
</td>
|
||||
<td class="text-center white--text">
|
||||
<td class="text-center text-white">
|
||||
{{
|
||||
calculateStdDev(useStateStore().currentMultitagBuffer?.map((v) => v.bestTransform.y) || []).toFixed(
|
||||
5
|
||||
)
|
||||
}} m
|
||||
</td>
|
||||
<td class="text-center white--text">
|
||||
<td class="text-center text-white">
|
||||
{{
|
||||
calculateStdDev(useStateStore().currentMultitagBuffer?.map((v) => v.bestTransform.z) || []).toFixed(
|
||||
5
|
||||
)
|
||||
}} m
|
||||
</td>
|
||||
<td class="text-center white--text">
|
||||
<td class="text-center text-white">
|
||||
{{
|
||||
calculateStdDev(
|
||||
useStateStore().currentMultitagBuffer?.map((v) => toDeg(v.bestTransform.angle_x)) || []
|
||||
).toFixed(5)
|
||||
}}°
|
||||
</td>
|
||||
<td class="text-center white--text">
|
||||
<td class="text-center text-white">
|
||||
{{
|
||||
calculateStdDev(
|
||||
useStateStore().currentMultitagBuffer?.map((v) => toDeg(v.bestTransform.angle_y)) || []
|
||||
).toFixed(5)
|
||||
}}°
|
||||
</td>
|
||||
<td class="text-center white--text">
|
||||
<td class="text-center text-white">
|
||||
{{
|
||||
calculateStdDev(
|
||||
useStateStore().currentMultitagBuffer?.map((v) => toDeg(v.bestTransform.angle_z)) || []
|
||||
@@ -261,14 +262,14 @@ const resetCurrentBuffer = () => {
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-table>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.v-data-table {
|
||||
.v-table {
|
||||
background-color: #006492 !important;
|
||||
width: 100%;
|
||||
font-size: 1rem !important;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { computed, getCurrentInstance, onBeforeUnmount, onMounted } from "vue";
|
||||
import { computed, onBeforeUnmount, onMounted } from "vue";
|
||||
import PvRangeSlider from "@/components/common/pv-range-slider.vue";
|
||||
import PvSwitch from "@/components/common/pv-switch.vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { ColorPicker, type HSV } from "@/lib/ColorPicker";
|
||||
import { useDisplay } from "vuetify";
|
||||
|
||||
const averageHue = computed<number>(() => {
|
||||
const isHueInverted = useCameraSettingsStore().currentPipelineSettings.hueInverted;
|
||||
@@ -123,12 +124,10 @@ onBeforeUnmount(() => {
|
||||
|
||||
cameraStream.removeEventListener("click", handleStreamClick);
|
||||
});
|
||||
const { mdAndDown } = useDisplay();
|
||||
|
||||
const interactiveCols = computed(() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
? 9
|
||||
: 8
|
||||
mdAndDown.value && (!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode) ? 9 : 8
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -144,7 +143,7 @@ const interactiveCols = computed(() =>
|
||||
:max="180"
|
||||
:slider-cols="interactiveCols"
|
||||
:inverted="useCameraSettingsStore().currentPipelineSettings.hueInverted"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hsvHue: value }, false)"
|
||||
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hsvHue: value }, false)"
|
||||
/>
|
||||
<pv-range-slider
|
||||
id="sat-slider"
|
||||
@@ -155,7 +154,9 @@ const interactiveCols = computed(() =>
|
||||
:min="0"
|
||||
:max="255"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hsvSaturation: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hsvSaturation: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-range-slider
|
||||
id="value-slider"
|
||||
@@ -166,53 +167,55 @@ const interactiveCols = computed(() =>
|
||||
:min="0"
|
||||
:max="255"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hsvValue: value }, false)"
|
||||
@update:modelValue="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hsvValue: value }, false)"
|
||||
/>
|
||||
<pv-switch
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.hueInverted"
|
||||
label="Invert Hue"
|
||||
:switch-cols="interactiveCols"
|
||||
tooltip="Selects the hue range outside of the hue slider bounds instead of inside"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hueInverted: value }, false)"
|
||||
@update:modelValue="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hueInverted: value }, false)
|
||||
"
|
||||
/>
|
||||
<div>
|
||||
<div class="white--text pt-3">Color Picker</div>
|
||||
<div class="text-white pt-3">Color Picker</div>
|
||||
<div class="d-flex pt-3">
|
||||
<template v-if="!useStateStore().colorPickingMode">
|
||||
<v-col cols="4" class="pl-0 pr-2">
|
||||
<v-btn
|
||||
small
|
||||
size="small"
|
||||
block
|
||||
color="accent"
|
||||
class="black--text"
|
||||
class="text-black"
|
||||
@click="enableColorPicking(useCameraSettingsStore().currentPipelineSettings.hueInverted ? 2 : 3)"
|
||||
>
|
||||
<v-icon left> mdi-minus </v-icon>
|
||||
<v-icon start> mdi-minus </v-icon>
|
||||
Shrink Range
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="4" class="pl-0 pr-0">
|
||||
<v-btn color="accent" class="black--text" small block @click="enableColorPicking(1)">
|
||||
<v-icon left> mdi-plus-minus </v-icon>
|
||||
<v-btn color="accent" class="text-black" size="small" block @click="enableColorPicking(1)">
|
||||
<v-icon start> mdi-plus-minus </v-icon>
|
||||
{{ useCameraSettingsStore().currentPipelineSettings.hueInverted ? "Exclude" : "Set to" }} Average
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="4" class="pl-2 pr-0">
|
||||
<v-btn
|
||||
small
|
||||
size="small"
|
||||
block
|
||||
color="accent"
|
||||
class="black--text"
|
||||
class="text-black"
|
||||
@click="enableColorPicking(useCameraSettingsStore().currentPipelineSettings.hueInverted ? 3 : 2)"
|
||||
>
|
||||
<v-icon left> mdi-plus </v-icon>
|
||||
<v-icon start> mdi-plus </v-icon>
|
||||
Expand Range
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-card-text class="pa-0 pt-3 pb-3">
|
||||
<v-btn block color="accent" class="black--text" small @click="disableColorPicking"> Cancel </v-btn>
|
||||
<v-btn block color="accent" class="text-black" size="small" @click="disableColorPicking"> Cancel </v-btn>
|
||||
</v-card-text>
|
||||
</template>
|
||||
</div>
|
||||
@@ -224,32 +227,32 @@ const interactiveCols = computed(() =>
|
||||
.threshold-modifiers {
|
||||
--averageHue: 0;
|
||||
}
|
||||
#hue-slider >>> .v-slider {
|
||||
#hue-slider:deep(.v-slider__container) {
|
||||
background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
|
||||
border-radius: 10px;
|
||||
/* prettier-ignore */
|
||||
box-shadow: 0 0 5px #333, inset 0 0 3px #333;
|
||||
}
|
||||
#sat-slider >>> .v-slider {
|
||||
#sat-slider:deep(.v-slider__container) {
|
||||
background: linear-gradient(to right, #fff 0%, hsl(var(--averageHue), 100%, 50%) 100%);
|
||||
border-radius: 10px;
|
||||
/* prettier-ignore */
|
||||
box-shadow: 0 0 5px #333, inset 0 0 3px #333;
|
||||
}
|
||||
#value-slider >>> .v-slider {
|
||||
#value-slider:deep(.v-slider__container) {
|
||||
background: linear-gradient(to right, #000 0%, hsl(var(--averageHue), 100%, 50%) 100%);
|
||||
border-radius: 10px;
|
||||
/* prettier-ignore */
|
||||
box-shadow: 0 0 5px #333, inset 0 0 3px #333;
|
||||
}
|
||||
>>> .v-slider__thumb {
|
||||
:deep(.v-slider__thumb) {
|
||||
outline: black solid thin;
|
||||
}
|
||||
.normal-slider >>> .v-slider__track-fill {
|
||||
.normal-slider:deep(.v-slider__track-fill) {
|
||||
outline: black solid thin;
|
||||
}
|
||||
|
||||
.inverted-slider >>> .v-slider__track-background {
|
||||
.inverted-slider:deep(.v-slider__track-background) {
|
||||
outline: black solid thin;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -17,14 +17,14 @@ const quaternionToEuler = (rot_quat: Quaternion): { x: number; y: number; z: num
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card dark style="background-color: #006492">
|
||||
<v-card style="background-color: #006492">
|
||||
<v-card-title class="pa-6">AprilTag Field Layout</v-card-title>
|
||||
<v-card-text class="pa-6 pt-0">
|
||||
<p>Field width: {{ useSettingsStore().currentFieldLayout.field.width.toFixed(2) }} meters</p>
|
||||
<p>Field length: {{ useSettingsStore().currentFieldLayout.field.length.toFixed(2) }} meters</p>
|
||||
|
||||
<!-- Simple table height must be set here and in the CSS for the fixed-header to work -->
|
||||
<v-simple-table fixed-header height="100%" dense dark>
|
||||
<v-table fixed-header height="100%" density="compact" dark>
|
||||
<template #default>
|
||||
<thead style="font-size: 1.25rem">
|
||||
<tr>
|
||||
@@ -47,13 +47,13 @@ const quaternionToEuler = (rot_quat: Quaternion): { x: number; y: number; z: num
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-table>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.v-data-table {
|
||||
.v-table {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { inject, ref } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import PvSelect from "@/components/common/pv-select.vue";
|
||||
import PvInput from "@/components/common/pv-input.vue";
|
||||
import axios from "axios";
|
||||
|
||||
const restartProgram = () => {
|
||||
@@ -140,10 +141,10 @@ enum ImportType {
|
||||
ApriltagFieldLayout
|
||||
}
|
||||
const showImportDialog = ref(false);
|
||||
const importType = ref<ImportType | number>(-1);
|
||||
const importType = ref<ImportType | undefined>(undefined);
|
||||
const importFile = ref<File | null>(null);
|
||||
const handleSettingsImport = () => {
|
||||
if (importType.value === -1 || importFile.value === null) return;
|
||||
if (importType.value === undefined || importFile.value === null) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("data", importFile.value);
|
||||
@@ -198,7 +199,7 @@ const handleSettingsImport = () => {
|
||||
});
|
||||
|
||||
showImportDialog.value = false;
|
||||
importType.value = -1;
|
||||
importType.value = undefined;
|
||||
importFile.value = null;
|
||||
};
|
||||
|
||||
@@ -237,25 +238,25 @@ const nukePhotonConfigDirectory = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card dark class="mb-3" style="background-color: #006492">
|
||||
<v-card class="mb-3" style="background-color: #006492">
|
||||
<v-card-title class="pa-6">Device Control</v-card-title>
|
||||
<div class="pa-6 pt-0">
|
||||
<v-row>
|
||||
<v-col cols="12" lg="4" md="6">
|
||||
<v-btn color="error" @click="restartProgram">
|
||||
<v-icon left class="open-icon"> mdi-restart </v-icon>
|
||||
<v-icon start class="open-icon"> mdi-restart </v-icon>
|
||||
<span class="open-label">Restart PhotonVision</span>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" lg="4" md="6">
|
||||
<v-btn color="error" @click="restartDevice">
|
||||
<v-icon left class="open-icon"> mdi-restart-alert </v-icon>
|
||||
<v-icon start class="open-icon"> mdi-restart-alert </v-icon>
|
||||
<span class="open-label">Restart Device</span>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" lg="4">
|
||||
<v-btn color="secondary" @click="openOfflineUpdatePrompt">
|
||||
<v-icon left class="open-icon"> mdi-upload </v-icon>
|
||||
<v-icon start class="open-icon"> mdi-upload </v-icon>
|
||||
<span class="open-label">Offline Update</span>
|
||||
</v-btn>
|
||||
<input ref="offlineUpdate" type="file" accept=".jar" style="display: none" @change="handleOfflineUpdate" />
|
||||
@@ -265,15 +266,15 @@ const nukePhotonConfigDirectory = () => {
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-btn color="secondary" @click="() => (showImportDialog = true)">
|
||||
<v-icon left class="open-icon"> mdi-import </v-icon>
|
||||
<v-icon start class="open-icon"> mdi-import </v-icon>
|
||||
<span class="open-label">Import Settings</span>
|
||||
</v-btn>
|
||||
<v-dialog
|
||||
v-model="showImportDialog"
|
||||
width="600"
|
||||
@input="
|
||||
@update:modelValue="
|
||||
() => {
|
||||
importType = -1;
|
||||
importType = undefined;
|
||||
importFile = null;
|
||||
}
|
||||
"
|
||||
@@ -301,18 +302,14 @@ const nukePhotonConfigDirectory = () => {
|
||||
<v-row class="mt-6 ml-4 mr-8">
|
||||
<v-file-input
|
||||
v-model="importFile"
|
||||
:disabled="importType === -1"
|
||||
:error-messages="importType === -1 ? 'Settings type not selected' : ''"
|
||||
:disabled="importType === undefined"
|
||||
:error-messages="importType === undefined ? 'Settings type not selected' : ''"
|
||||
:accept="importType === ImportType.AllSettings ? '.zip' : '.json'"
|
||||
/>
|
||||
</v-row>
|
||||
<v-row
|
||||
class="mt-12 ml-8 mr-8 mb-1"
|
||||
style="display: flex; align-items: center; justify-content: center"
|
||||
align="center"
|
||||
>
|
||||
<v-row class="mt-12 ml-8 mr-8 mb-1" style="display: flex; align-items: center; justify-content: center">
|
||||
<v-btn color="secondary" :disabled="importFile === null" @click="handleSettingsImport">
|
||||
<v-icon left class="open-icon"> mdi-import </v-icon>
|
||||
<v-icon start class="open-icon"> mdi-import </v-icon>
|
||||
<span class="open-label">Import Settings</span>
|
||||
</v-btn>
|
||||
</v-row>
|
||||
@@ -322,7 +319,7 @@ const nukePhotonConfigDirectory = () => {
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-btn color="secondary" @click="openExportSettingsPrompt">
|
||||
<v-icon left class="open-icon"> mdi-export </v-icon>
|
||||
<v-icon start class="open-icon"> mdi-export </v-icon>
|
||||
<span class="open-label">Export Settings</span>
|
||||
</v-btn>
|
||||
<a
|
||||
@@ -335,7 +332,7 @@ const nukePhotonConfigDirectory = () => {
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-btn color="secondary" @click="openExportLogsPrompt">
|
||||
<v-icon left class="open-icon"> mdi-download </v-icon>
|
||||
<v-icon start class="open-icon"> mdi-download </v-icon>
|
||||
<span class="open-label">Download logs</span>
|
||||
|
||||
<!-- Special hidden link that gets 'clicked' when the user exports journalctl logs -->
|
||||
@@ -350,7 +347,7 @@ const nukePhotonConfigDirectory = () => {
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-btn color="secondary" @click="useStateStore().showLogModal = true">
|
||||
<v-icon left class="open-icon"> mdi-eye </v-icon>
|
||||
<v-icon start class="open-icon"> mdi-eye </v-icon>
|
||||
<span class="open-label">View program logs</span>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
@@ -359,10 +356,10 @@ const nukePhotonConfigDirectory = () => {
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-btn color="error" @click="() => (showFactoryReset = true)">
|
||||
<v-icon left class="open-icon"> mdi-skull-crossbones </v-icon>
|
||||
<v-icon start class="open-icon"> mdi-skull-crossbones </v-icon>
|
||||
<span class="open-icon">
|
||||
{{
|
||||
$vuetify.breakpoint.mdAndUp
|
||||
$vuetify.display.mdAndUp
|
||||
? "Factory Reset PhotonVision and delete EVERYTHING"
|
||||
: "Factory Reset PhotonVision"
|
||||
}}
|
||||
@@ -373,22 +370,22 @@ const nukePhotonConfigDirectory = () => {
|
||||
</div>
|
||||
|
||||
<v-dialog v-model="showFactoryReset" width="800" dark>
|
||||
<v-card dark color="primary" class="pa-3" flat>
|
||||
<v-card color="primary" class="pa-3" flat>
|
||||
<v-card-title style="justify-content: center" class="pb-6">
|
||||
<span class="open-label">
|
||||
<v-icon right color="error" class="open-icon ma-1">mdi-nuke</v-icon>
|
||||
<v-icon end color="error" class="open-icon ma-1">mdi-nuke</v-icon>
|
||||
Factory Reset PhotonVision
|
||||
<v-icon right color="error" class="open-icon ma-1">mdi-nuke</v-icon>
|
||||
<v-icon end color="error" class="open-icon ma-1">mdi-nuke</v-icon>
|
||||
</span>
|
||||
</v-card-title>
|
||||
<v-card-text class="pt-3">
|
||||
<v-row class="align-center white--text">
|
||||
<v-row class="align-center text-white">
|
||||
<v-col cols="12" md="6">
|
||||
<span class="mt-3"> This will delete ALL OF YOUR SETTINGS and restart PhotonVision. </span>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-btn color="secondary" style="float: right" @click="openExportSettingsPrompt">
|
||||
<v-icon left class="open-icon"> mdi-export </v-icon>
|
||||
<v-icon start class="open-icon"> mdi-export </v-icon>
|
||||
<span class="open-label">Backup Settings</span>
|
||||
<a
|
||||
ref="exportSettings"
|
||||
@@ -415,11 +412,9 @@ const nukePhotonConfigDirectory = () => {
|
||||
:disabled="yesDeleteMySettingsText.toLowerCase() !== expected.toLowerCase()"
|
||||
@click="nukePhotonConfigDirectory"
|
||||
>
|
||||
<v-icon left class="open-icon"> mdi-trash-can-outline </v-icon>
|
||||
<v-icon start class="open-icon"> mdi-trash-can-outline </v-icon>
|
||||
<span class="open-label">
|
||||
{{
|
||||
$vuetify.breakpoint.mdAndUp ? "Delete everything, I have backed up what I need" : "Delete Everything"
|
||||
}}
|
||||
{{ $vuetify.display.mdAndUp ? "Delete everything, I have backed up what I need" : "Delete Everything" }}
|
||||
</span>
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card dark class="mb-3 pr-6 pb-3" style="background-color: #006492">
|
||||
<v-card class="mb-3 pr-6 pb-3" style="background-color: #006492">
|
||||
<v-card-title>LED Control</v-card-title>
|
||||
<div class="ml-5">
|
||||
<pv-slider
|
||||
@@ -14,7 +14,7 @@ import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
:slider-cols="12"
|
||||
:min="0"
|
||||
:max="100"
|
||||
@input="(args) => useSettingsStore().changeLEDBrightness(args)"
|
||||
@update:modelValue="(args) => useSettingsStore().changeLEDBrightness(args)"
|
||||
/>
|
||||
</div>
|
||||
</v-card>
|
||||
|
||||
@@ -120,17 +120,17 @@ onBeforeMount(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card dark class="mb-3" style="background-color: #006492">
|
||||
<v-card class="mb-3" style="background-color: #006492">
|
||||
<v-card-title class="pl-6" style="display: flex; justify-content: space-between">
|
||||
<span class="pt-2 pb-2">Stats</span>
|
||||
<v-btn text @click="fetchMetrics">
|
||||
<v-icon left class="open-icon">mdi-reload</v-icon>
|
||||
<v-btn variant="text" @click="fetchMetrics">
|
||||
<v-icon start class="open-icon">mdi-reload</v-icon>
|
||||
Last Fetched: {{ metricsLastFetched }}
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-card-text class="pa-6 pt-0 pb-3">
|
||||
<v-card-subtitle class="pa-0" style="font-size: 16px">General Metrics</v-card-subtitle>
|
||||
<v-simple-table class="metrics-table mt-3">
|
||||
<v-table class="metrics-table mt-3">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
@@ -163,11 +163,11 @@ onBeforeMount(() => {
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
</v-table>
|
||||
</v-card-text>
|
||||
<v-card-text class="pa-6 pt-4">
|
||||
<v-card-subtitle class="pa-0 pb-1" style="font-size: 16px">Hardware Metrics</v-card-subtitle>
|
||||
<v-simple-table class="metrics-table mt-3">
|
||||
<v-table class="metrics-table mt-3">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
@@ -201,7 +201,7 @@ onBeforeMount(() => {
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
</v-table>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
@@ -263,7 +263,7 @@ onBeforeMount(() => {
|
||||
text-decoration-color: #ffd843;
|
||||
}
|
||||
|
||||
.v-data-table {
|
||||
.v-table {
|
||||
thead,
|
||||
tbody {
|
||||
background-color: #006492;
|
||||
|
||||
@@ -124,9 +124,14 @@ const saveGeneralSettings = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const currentNetworkInterfaceIndex = computed<number>({
|
||||
get: () => useSettingsStore().networkInterfaceNames.indexOf(useSettingsStore().network.networkManagerIface || ""),
|
||||
set: (v) => (tempSettingsStruct.value.networkManagerIface = useSettingsStore().networkInterfaceNames[v])
|
||||
const currentNetworkInterfaceIndex = computed<number | undefined>({
|
||||
get: () => {
|
||||
const index = useSettingsStore().networkInterfaceNames.indexOf(
|
||||
useSettingsStore().network.networkManagerIface || ""
|
||||
);
|
||||
return index === -1 ? undefined : index;
|
||||
},
|
||||
set: (v) => v && (tempSettingsStruct.value.networkManagerIface = useSettingsStore().networkInterfaceNames[v])
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
@@ -136,7 +141,7 @@ watchEffect(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card dark class="mb-3" style="background-color: #006492">
|
||||
<v-card class="mb-3" style="background-color: #006492">
|
||||
<v-card-title class="pa-6">Global Settings</v-card-title>
|
||||
<div class="pa-6 pt-0">
|
||||
<v-divider class="pb-3" />
|
||||
@@ -157,7 +162,7 @@ watchEffect(() => {
|
||||
<v-banner
|
||||
v-if="!isValidNetworkTablesIP(tempSettingsStruct.ntServerAddress) && !tempSettingsStruct.runNTServer"
|
||||
rounded
|
||||
color="error"
|
||||
bg-color="error"
|
||||
text-color="white"
|
||||
style="margin: 10px 0"
|
||||
icon="mdi-alert-circle-outline"
|
||||
@@ -233,7 +238,7 @@ watchEffect(() => {
|
||||
!useSettingsStore().network.networkingDisabled
|
||||
"
|
||||
rounded
|
||||
color="error"
|
||||
bg-color="error"
|
||||
text-color="white"
|
||||
icon="mdi-information-outline"
|
||||
>
|
||||
@@ -248,7 +253,7 @@ watchEffect(() => {
|
||||
<v-banner
|
||||
v-if="tempSettingsStruct.runNTServer"
|
||||
rounded
|
||||
color="error"
|
||||
bg-color="error"
|
||||
text-color="white"
|
||||
icon="mdi-information-outline"
|
||||
>
|
||||
@@ -265,7 +270,7 @@ watchEffect(() => {
|
||||
<v-banner
|
||||
v-if="tempSettingsStruct.shouldPublishProto"
|
||||
rounded
|
||||
color="error"
|
||||
bg-color="error"
|
||||
text-color="white"
|
||||
icon="mdi-information-outline"
|
||||
>
|
||||
@@ -276,6 +281,7 @@ watchEffect(() => {
|
||||
</v-form>
|
||||
<v-btn
|
||||
color="accent"
|
||||
:variant="!settingsValid || !settingsHaveChanged() ? 'tonal' : 'elevated'"
|
||||
style="color: black; width: 100%"
|
||||
:disabled="!settingsValid || !settingsHaveChanged()"
|
||||
@click="saveGeneralSettings"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from "vue";
|
||||
import { ref, computed, inject } from "vue";
|
||||
import axios from "axios";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
@@ -8,6 +8,28 @@ const showImportDialog = ref(false);
|
||||
const importRKNNFile = ref<File | null>(null);
|
||||
const importLabelsFile = ref<File | null>(null);
|
||||
|
||||
const host = inject<string>("backendHost");
|
||||
|
||||
const areValidFileNames = (weights: string | null, labels: string | null) => {
|
||||
const weightsRegex = /^([a-zA-Z0-9._]+)-(\d+)-(\d+)-(yolov(?:5|8|11)[nsmlx]*)\.rknn$/;
|
||||
const labelsRegex = /^([a-zA-Z0-9._]+)-(\d+)-(\d+)-(yolov(?:5|8|11)[nsmlx]*)-labels\.txt$/;
|
||||
|
||||
if (weights && labels) {
|
||||
const weightsMatch = weights.match(weightsRegex);
|
||||
const labelsMatch = labels.match(labelsRegex);
|
||||
|
||||
if (weightsMatch && labelsMatch) {
|
||||
return (
|
||||
weightsMatch[1] === labelsMatch[1] &&
|
||||
weightsMatch[2] === labelsMatch[2] &&
|
||||
weightsMatch[3] === labelsMatch[3] &&
|
||||
weightsMatch[4] === labelsMatch[4]
|
||||
);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// TODO gray out the button when model is uploading
|
||||
const handleImport = async () => {
|
||||
if (importRKNNFile.value === null || importLabelsFile.value === null) return;
|
||||
@@ -65,19 +87,19 @@ const supportedModels = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card dark class="mb-3" style="background-color: #006492">
|
||||
<v-card class="mb-3" style="background-color: #006492">
|
||||
<v-card-title class="pa-6">Object Detection</v-card-title>
|
||||
<div class="pa-6 pt-0">
|
||||
<v-row>
|
||||
<v-col cols="12 ">
|
||||
<v-btn color="secondary" class="justify-center" @click="() => (showImportDialog = true)">
|
||||
<v-icon left class="open-icon"> mdi-import </v-icon>
|
||||
<v-icon start class="open-icon"> mdi-import </v-icon>
|
||||
<span class="open-label">Import New Model</span>
|
||||
</v-btn>
|
||||
<v-dialog
|
||||
v-model="showImportDialog"
|
||||
width="600"
|
||||
@input="
|
||||
@update:modelValue="
|
||||
() => {
|
||||
importRKNNFile = null;
|
||||
importLabelsFile = null;
|
||||
@@ -87,30 +109,28 @@ const supportedModels = computed(() => {
|
||||
<v-card color="primary" dark>
|
||||
<v-card-title>Import New Object Detection Model</v-card-title>
|
||||
<v-card-text>
|
||||
Upload a new object detection model to this device that can be used in a pipeline. Naming convention
|
||||
should be <code>name-verticalResolution-horizontalResolution-yolovXXX</code>. The
|
||||
<code>name</code> should only include alphanumeric characters, periods, and underscores. Additionally,
|
||||
the labels file ought to have the same name as the RKNN file, with <code>-labels</code> appended to the
|
||||
end. For example, if the RKNN file is named <code>note-640-640-yolov5s.rknn</code>, the labels file
|
||||
should be named <code>note-640-640-yolov5s-labels.txt</code>. Note that ONLY 640x640 YOLOv5, YOLOv8, and
|
||||
YOLOv11 models trained and converted to `.rknn` format for RK3588 CPUs are currently supported!
|
||||
Upload a new object detection model to this device that can be used in a pipeline. Note that ONLY
|
||||
640x640 YOLOv5, YOLOv8, and YOLOv11 models trained and converted to `.rknn` format for RK3588 CPUs are
|
||||
currently supported! See [the documentation]({{
|
||||
host
|
||||
}}/docs/objectDetection/about-object-detection.html) for more details.
|
||||
<v-row class="mt-6 ml-4 mr-8">
|
||||
<v-file-input v-model="importRKNNFile" label="RKNN File" accept=".rknn" />
|
||||
</v-row>
|
||||
<v-row class="mt-6 ml-4 mr-8">
|
||||
<v-file-input v-model="importLabelsFile" label="Labels File" accept=".txt" />
|
||||
</v-row>
|
||||
<v-row
|
||||
class="mt-12 ml-8 mr-8 mb-1"
|
||||
style="display: flex; align-items: center; justify-content: center"
|
||||
align="center"
|
||||
>
|
||||
<v-row class="mt-12 ml-8 mr-8 mb-1" style="display: flex; align-items: center; justify-content: center">
|
||||
<v-btn
|
||||
color="secondary"
|
||||
:disabled="importRKNNFile === null || importLabelsFile === null"
|
||||
:disabled="
|
||||
importRKNNFile === null ||
|
||||
importLabelsFile === null ||
|
||||
!areValidFileNames(importRKNNFile.name, importLabelsFile.name)
|
||||
"
|
||||
@click="handleImport"
|
||||
>
|
||||
<v-icon left class="open-icon"> mdi-import </v-icon>
|
||||
<v-icon start class="open-icon"> mdi-import </v-icon>
|
||||
<span class="open-label">Import Object Detection Model</span>
|
||||
</v-btn>
|
||||
</v-row>
|
||||
@@ -121,7 +141,7 @@ const supportedModels = computed(() => {
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-simple-table fixed-header height="100%" dense dark>
|
||||
<v-table fixed-header height="100%" density="compact" dark>
|
||||
<thead style="font-size: 1.25rem">
|
||||
<tr>
|
||||
<th class="text-left">Available Models</th>
|
||||
@@ -132,7 +152,7 @@ const supportedModels = computed(() => {
|
||||
<td>{{ model }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
</v-table>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
@@ -151,7 +171,7 @@ const supportedModels = computed(() => {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.v-data-table {
|
||||
.v-table {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Vue from "vue";
|
||||
import { h, createApp } from "vue";
|
||||
import App from "@/App.vue";
|
||||
|
||||
import { createPinia, PiniaVuePlugin } from "pinia";
|
||||
import { createPinia } from "pinia";
|
||||
import router from "@/router";
|
||||
import vuetify from "@/plugins/vuetify";
|
||||
import axios from "axios";
|
||||
@@ -29,9 +29,9 @@ switch (runtimeMode as PhotonClientRuntimeMode) {
|
||||
axios.defaults.baseURL = `http://${backendHost}/api`;
|
||||
|
||||
// Handle Plugins
|
||||
Vue.use(PiniaVuePlugin);
|
||||
const pinia = createPinia();
|
||||
|
||||
new Vue({
|
||||
const app = createApp({
|
||||
router,
|
||||
vuetify,
|
||||
pinia: createPinia(),
|
||||
@@ -39,5 +39,9 @@ new Vue({
|
||||
backendHost: backendHost,
|
||||
backendHostname: backendHostname
|
||||
},
|
||||
render: (h) => h(App)
|
||||
}).$mount("#app");
|
||||
render: () => h(App)
|
||||
});
|
||||
app.use(pinia);
|
||||
app.use(vuetify);
|
||||
app.use(router);
|
||||
app.mount("#app");
|
||||
|
||||
@@ -1,41 +1,53 @@
|
||||
import Vue from "vue";
|
||||
import Vuetify from "vuetify";
|
||||
import "vuetify/dist/vuetify.min.css";
|
||||
import "vuetify/styles";
|
||||
import "@mdi/font/css/materialdesignicons.css";
|
||||
import type { VuetifyThemeVariant } from "vuetify/types/services/theme";
|
||||
import type { ThemeDefinition } from "vuetify/lib/composables/theme";
|
||||
import { createVuetify } from "vuetify";
|
||||
|
||||
Vue.use(Vuetify);
|
||||
|
||||
const darkTheme: VuetifyThemeVariant = Object.freeze({
|
||||
primary: "#006492",
|
||||
secondary: "#39A4D5",
|
||||
accent: "#FFD843",
|
||||
background: "#232C37",
|
||||
const commonColors = {
|
||||
error: "#b80000",
|
||||
info: "#2196F3",
|
||||
success: "#4CAF50",
|
||||
warning: "#FFC107"
|
||||
});
|
||||
};
|
||||
|
||||
const lightTheme: VuetifyThemeVariant = Object.freeze({
|
||||
primary: "#006492",
|
||||
secondary: "#39A4D5",
|
||||
accent: "#FFD843",
|
||||
background: "#232C37",
|
||||
error: "#b80000",
|
||||
info: "#2196F3",
|
||||
success: "#4CAF50",
|
||||
warning: "#FFC107"
|
||||
});
|
||||
const DarkTheme: ThemeDefinition = {
|
||||
dark: true,
|
||||
colors: {
|
||||
primary: "#006492",
|
||||
secondary: "#39A4D5",
|
||||
accent: "#FFD843",
|
||||
background: "#232C37",
|
||||
...commonColors
|
||||
}
|
||||
};
|
||||
|
||||
export default new Vuetify({
|
||||
const LightTheme: ThemeDefinition = {
|
||||
dark: false,
|
||||
colors: {
|
||||
background: "#232C37",
|
||||
primary: "#006492",
|
||||
surface: "#006492",
|
||||
secondary: "#39A4D5",
|
||||
"surface-variant": "#358AB0",
|
||||
accent: "#FFD843",
|
||||
"surface-light": "#FFD843",
|
||||
...commonColors
|
||||
},
|
||||
variables: {
|
||||
"medium-emphasis-opacity": 1,
|
||||
"high-emphasis-opacity": 1
|
||||
}
|
||||
};
|
||||
|
||||
export default createVuetify({
|
||||
theme: {
|
||||
defaultTheme: "LightTheme",
|
||||
themes: {
|
||||
light: lightTheme,
|
||||
dark: darkTheme
|
||||
LightTheme: LightTheme,
|
||||
DarkTheme: DarkTheme
|
||||
}
|
||||
},
|
||||
breakpoint: {
|
||||
display: {
|
||||
thresholds: {
|
||||
md: 1460,
|
||||
lg: 2000
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Vue from "vue";
|
||||
import VueRouter from "vue-router";
|
||||
import { createRouter, createWebHashHistory } from "vue-router";
|
||||
|
||||
import DashboardView from "@/views/DashboardView.vue";
|
||||
import CameraSettingsView from "@/views/CameraSettingsView.vue";
|
||||
@@ -8,12 +7,10 @@ import DocsView from "@/views/DocsView.vue";
|
||||
import NotFoundView from "@/views/NotFoundView.vue";
|
||||
import CameraMatchingView from "@/views/CameraMatchingView.vue";
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
const router = new VueRouter({
|
||||
const router = createRouter({
|
||||
// Using HTML5 History Mode is problematic with Javalin because each route is treated as a server endpoint which causes Javalin to return a 404 error before being redirected to the UI.
|
||||
// mode: "history",
|
||||
base: import.meta.env.BASE_URL,
|
||||
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: "/",
|
||||
@@ -45,7 +42,7 @@ const router = new VueRouter({
|
||||
component: DocsView
|
||||
},
|
||||
{
|
||||
path: "*",
|
||||
path: "/:pathMatch(.*)",
|
||||
name: "NotFound",
|
||||
component: NotFoundView
|
||||
}
|
||||
|
||||
@@ -155,17 +155,16 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
* Update the configurable camera settings.
|
||||
*
|
||||
* @param data camera settings to save.
|
||||
* @param cameraUniqueNamendex the unique name of the camera.
|
||||
* @param cameraUniqueName the unique name of the camera.
|
||||
*/
|
||||
updateCameraSettings(
|
||||
data: CameraSettingsChangeRequest,
|
||||
cameraUniqueName: String = useStateStore().currentCameraUniqueName
|
||||
cameraUniqueName: string = useStateStore().currentCameraUniqueName
|
||||
) {
|
||||
// The camera settings endpoint doesn't actually require all data, instead, it needs key data such as the FOV
|
||||
const payload = {
|
||||
settings: {
|
||||
...data
|
||||
},
|
||||
fov: data.fov,
|
||||
quirksToChange: data.quirksToChange,
|
||||
cameraUniqueName: cameraUniqueName
|
||||
};
|
||||
return axios.post("/settings/camera", payload);
|
||||
@@ -175,12 +174,12 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
*
|
||||
* @param newPipelineName the name of the new pipeline.
|
||||
* @param pipelineType the type of the new pipeline. Cannot be {@link WebsocketPipelineType.Calib3d} or {@link WebsocketPipelineType.DriverMode}.
|
||||
* @param cameraUniqueNamendex the unique name of the camera.
|
||||
* @param cameraUniqueName the unique name of the camera.
|
||||
*/
|
||||
createNewPipeline(
|
||||
newPipelineName: string,
|
||||
pipelineType: Exclude<WebsocketPipelineType, WebsocketPipelineType.Calib3d | WebsocketPipelineType.DriverMode>,
|
||||
cameraUniqueName: String = useStateStore().currentCameraUniqueName
|
||||
cameraUniqueName: string = useStateStore().currentCameraUniqueName
|
||||
) {
|
||||
const payload = {
|
||||
addNewPipeline: [newPipelineName, pipelineType],
|
||||
@@ -193,7 +192,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
*
|
||||
* @param settings settings to modify. The type of the settings should match the currently selected pipeline type.
|
||||
* @param updateStore whether or not to update the store. This is useful if the input field already models the store reference.
|
||||
* @param cameraUniqueNamendex the unique name of the camera.
|
||||
* @param cameraUniqueName the unique name of the camera.
|
||||
*/
|
||||
changeCurrentPipelineSetting(
|
||||
settings: ActiveConfigurablePipelineSettings,
|
||||
@@ -224,7 +223,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
*
|
||||
* @param newName the new nickname for the camera.
|
||||
* @param updateStore whether or not to update the store. This is useful if the input field already models the store reference.
|
||||
* @param cameraUniqueNamendex the unique name of the camera.
|
||||
* @param cameraUniqueName the unique name of the camera.
|
||||
*/
|
||||
changeCurrentPipelineNickname(
|
||||
newName: string,
|
||||
@@ -244,7 +243,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
* Modify the Pipeline type of the currently selected pipeline of the provided camera. This overwrites the current pipeline's settings when the backend resets the current pipeline settings.
|
||||
*
|
||||
* @param type the pipeline type to set. Cannot be {@link WebsocketPipelineType.Calib3d} or {@link WebsocketPipelineType.DriverMode}.
|
||||
* @param cameraUniqueNamendex the unique name of the camera.
|
||||
* @param cameraUniqueName the unique name of the camera.
|
||||
*/
|
||||
changeCurrentPipelineType(
|
||||
type: Exclude<WebsocketPipelineType, WebsocketPipelineType.Calib3d | WebsocketPipelineType.DriverMode>,
|
||||
@@ -261,7 +260,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
*
|
||||
* @param index pipeline index to set.
|
||||
* @param updateStore whether or not to update the store. This is useful if the input field already models the store reference.
|
||||
* @param cameraUniqueNamendex the unique name of the camera.
|
||||
* @param cameraUniqueName the unique name of the camera.
|
||||
*/
|
||||
changeCurrentPipelineIndex(
|
||||
index: number,
|
||||
@@ -293,7 +292,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
/**
|
||||
* Change the currently selected pipeline of the provided camera.
|
||||
*
|
||||
* @param cameraUniqueNamendex the unique name of the camera.
|
||||
* @param cameraUniqueName the unique name of the camera.
|
||||
*/
|
||||
deleteCurrentPipeline(cameraUniqueName: string = useStateStore().currentCameraUniqueName) {
|
||||
const payload = {
|
||||
@@ -306,7 +305,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
* Duplicate the pipeline at the provided index.
|
||||
*
|
||||
* @param pipelineIndex index of the pipeline to duplicate.
|
||||
* @param cameraUniqueNamendex the unique name of the camera.
|
||||
* @param cameraUniqueName the unique name of the camera.
|
||||
*/
|
||||
duplicatePipeline(pipelineIndex: number, cameraUniqueName: string = useStateStore().currentCameraUniqueName) {
|
||||
const payload = {
|
||||
@@ -335,7 +334,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
*
|
||||
* @param newName the new nickname of the camera.
|
||||
* @param updateStore whether or not to update the store. This is useful if the input field already models the store reference.
|
||||
* @param cameraUniqueNamendex the unique name of the camera.
|
||||
* @param cameraUniqueName the unique name of the camera.
|
||||
* @return HTTP request promise to the backend
|
||||
*/
|
||||
changeCameraNickname(
|
||||
@@ -356,7 +355,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
* Start the 3D calibration process for the provided camera.
|
||||
*
|
||||
* @param calibrationInitData initialization calibration data.
|
||||
* @param cameraUniqueNamendex the unique name of the camera.
|
||||
* @param cameraUniqueName the unique name of the camera.
|
||||
*/
|
||||
startPnPCalibration(
|
||||
calibrationInitData: {
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
type UiCameraConfiguration
|
||||
} from "@/types/SettingTypes";
|
||||
import { getResolutionString } from "@/lib/PhotonUtils";
|
||||
import PhotonCameraStream from "@/components/app/photon-camera-stream.vue";
|
||||
import PvInput from "@/components/common/pv-input.vue";
|
||||
import PvCameraInfoCard from "@/components/common/pv-camera-info-card.vue";
|
||||
import axios from "axios";
|
||||
import PvCameraMatchCard from "@/components/common/pv-camera-match-card.vue";
|
||||
@@ -23,36 +25,106 @@ const activatingModule = ref(false);
|
||||
const activateModule = (moduleUniqueName: string) => {
|
||||
if (activatingModule.value) return;
|
||||
activatingModule.value = true;
|
||||
const url = new URL(`http://${host}/api/utils/activateMatchedCamera`);
|
||||
url.searchParams.set("cameraUniqueName", moduleUniqueName);
|
||||
|
||||
fetch(url.toString(), {
|
||||
method: "POST"
|
||||
}).finally(() => (activatingModule.value = false));
|
||||
axios
|
||||
.post("/utils/activateMatchedCamera", { cameraUniqueName: moduleUniqueName })
|
||||
.then(() => {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Camera activated successfully",
|
||||
color: "success"
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "The backend is unable to fulfil the request to activate this camera.",
|
||||
color: "error"
|
||||
});
|
||||
} else if (error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Error while trying to process the request! The backend didn't respond.",
|
||||
color: "error"
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "An error occurred while trying to process the request.",
|
||||
color: "error"
|
||||
});
|
||||
}
|
||||
})
|
||||
.finally(() => (activatingModule.value = false));
|
||||
};
|
||||
|
||||
const assigningCamera = ref(false);
|
||||
const assignCamera = (cameraInfo: PVCameraInfo) => {
|
||||
if (assigningCamera.value) return;
|
||||
assigningCamera.value = true;
|
||||
const url = new URL(`http://${host}/api/utils/assignUnmatchedCamera`);
|
||||
url.searchParams.set("cameraInfo", JSON.stringify(cameraInfo));
|
||||
|
||||
fetch(url.toString(), {
|
||||
method: "POST"
|
||||
}).finally(() => (assigningCamera.value = false));
|
||||
const payload = {
|
||||
cameraInfo: cameraInfo
|
||||
};
|
||||
|
||||
axios
|
||||
.post("/utils/assignUnmatchedCamera", payload)
|
||||
.then(() => {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Unmatched camera assigned successfully",
|
||||
color: "success"
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "The backend is unable to fulfil the request to assign this unmatched camera.",
|
||||
color: "error"
|
||||
});
|
||||
} else if (error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Error while trying to process the request! The backend didn't respond.",
|
||||
color: "error"
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "An error occurred while trying to process the request.",
|
||||
color: "error"
|
||||
});
|
||||
}
|
||||
})
|
||||
.finally(() => (assigningCamera.value = false));
|
||||
};
|
||||
|
||||
const deactivatingModule = ref(false);
|
||||
const deactivateModule = (cameraUniqueName: string) => {
|
||||
if (deactivatingModule.value) return;
|
||||
deactivatingModule.value = true;
|
||||
const url = new URL(`http://${host}/api/utils/unassignCamera`);
|
||||
url.searchParams.set("cameraUniqueName", cameraUniqueName);
|
||||
|
||||
fetch(url.toString(), {
|
||||
method: "POST"
|
||||
}).finally(() => (deactivatingModule.value = false));
|
||||
axios
|
||||
.post("/utils/unassignCamera", { cameraUniqueName: cameraUniqueName })
|
||||
.then(() => {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Camera deactivated successfully",
|
||||
color: "success"
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "The backend is unable to fulfil the request to deactivate this camera.",
|
||||
color: "error"
|
||||
});
|
||||
} else if (error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Error while trying to process the request! The backend didn't respond.",
|
||||
color: "error"
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "An error occurred while trying to process the request.",
|
||||
color: "error"
|
||||
});
|
||||
}
|
||||
})
|
||||
.finally(() => (deactivatingModule.value = false));
|
||||
};
|
||||
|
||||
const deletingCamera = ref(false);
|
||||
@@ -208,7 +280,7 @@ const openExportSettingsPrompt = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="pa-5">
|
||||
<div class="pa-3">
|
||||
<v-row>
|
||||
<!-- Active modules -->
|
||||
<v-col
|
||||
@@ -218,7 +290,7 @@ const openExportSettingsPrompt = () => {
|
||||
sm="6"
|
||||
lg="4"
|
||||
>
|
||||
<v-card dark color="primary">
|
||||
<v-card color="primary">
|
||||
<v-card-title>{{ cameraInfoFor(module.matchedCameraInfo).name }}</v-card-title>
|
||||
<v-card-subtitle v-if="!cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath)" class="pb-2"
|
||||
>Status: <span class="inactive-status">Disconnected</span></v-card-subtitle
|
||||
@@ -233,7 +305,7 @@ const openExportSettingsPrompt = () => {
|
||||
>
|
||||
<v-card-subtitle v-else class="pb-2">Status: <span class="mismatch-status">Mismatch</span></v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-simple-table dark dense>
|
||||
<v-table density="compact">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Streams:</td>
|
||||
@@ -271,7 +343,7 @@ const openExportSettingsPrompt = () => {
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
</v-table>
|
||||
<div
|
||||
v-if="cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath)"
|
||||
:id="`stream-container-${index}`"
|
||||
@@ -303,7 +375,7 @@ const openExportSettingsPrompt = () => {
|
||||
</v-col>
|
||||
<v-col cols="6" md="5" class="pr-0">
|
||||
<v-btn
|
||||
class="black--text"
|
||||
class="text-black"
|
||||
color="accent"
|
||||
style="width: 100%"
|
||||
:loading="deactivatingModule"
|
||||
@@ -324,11 +396,11 @@ const openExportSettingsPrompt = () => {
|
||||
|
||||
<!-- Disabled modules -->
|
||||
<v-col v-for="module in disabledVisionModules" :key="`disabled-${module.uniqueName}`" cols="12" sm="6" lg="4">
|
||||
<v-card dark color="primary">
|
||||
<v-card color="primary">
|
||||
<v-card-title>{{ module.nickname }}</v-card-title>
|
||||
<v-card-subtitle class="pb-2">Status: <span class="inactive-status">Deactivated</span></v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-simple-table dense>
|
||||
<v-table density="compact">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
@@ -354,7 +426,7 @@ const openExportSettingsPrompt = () => {
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
</v-table>
|
||||
</v-card-text>
|
||||
<v-card-text class="pt-0">
|
||||
<v-row>
|
||||
@@ -374,7 +446,7 @@ const openExportSettingsPrompt = () => {
|
||||
</v-col>
|
||||
<v-col cols="6" md="5" class="pr-0">
|
||||
<v-btn
|
||||
class="black--text"
|
||||
class="text-black"
|
||||
color="accent"
|
||||
style="width: 100%"
|
||||
:loading="activatingModule"
|
||||
@@ -395,7 +467,7 @@ const openExportSettingsPrompt = () => {
|
||||
|
||||
<!-- Unassigned cameras -->
|
||||
<v-col v-for="(camera, index) in unmatchedCameras" :key="index" cols="12" sm="6" lg="4">
|
||||
<v-card dark color="primary">
|
||||
<v-card color="primary">
|
||||
<v-card-title class="pb-2">
|
||||
<span v-if="camera.PVUsbCameraInfo">USB Camera:</span>
|
||||
<span v-else-if="camera.PVCSICameraInfo">CSI Camera:</span>
|
||||
@@ -416,7 +488,7 @@ const openExportSettingsPrompt = () => {
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-btn
|
||||
class="black--text"
|
||||
class="text-black"
|
||||
color="accent"
|
||||
style="width: 100%"
|
||||
:loading="assigningCamera"
|
||||
@@ -448,10 +520,10 @@ const openExportSettingsPrompt = () => {
|
||||
|
||||
<!-- Camera details modal -->
|
||||
<v-dialog v-model="viewingDetails" max-width="800">
|
||||
<v-card v-if="viewingCamera[0] !== null" dark flat color="primary">
|
||||
<v-card v-if="viewingCamera[0] !== null" flat color="primary">
|
||||
<v-card-title class="d-flex justify-space-between">
|
||||
<span>{{ cameraInfoFor(viewingCamera[0])?.name ?? cameraInfoFor(viewingCamera[0])?.baseName }}</span>
|
||||
<v-btn text @click="setCameraView(null, null)">
|
||||
<v-btn variant="text" @click="setCameraView(null, null)">
|
||||
<v-icon>mdi-close-thick</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
@@ -472,17 +544,17 @@ const openExportSettingsPrompt = () => {
|
||||
</v-dialog>
|
||||
|
||||
<!-- Camera delete modal -->
|
||||
<v-dialog v-model="viewingDeleteCamera" dark width="800">
|
||||
<v-card v-if="cameraToDelete !== null" dark class="dialog-container pa-3 pb-2" color="primary" flat>
|
||||
<v-dialog v-model="viewingDeleteCamera" width="800">
|
||||
<v-card v-if="cameraToDelete !== null" class="dialog-container pa-3 pb-2" color="primary" flat>
|
||||
<v-card-title> Delete {{ cameraToDelete.nickname }}? </v-card-title>
|
||||
<v-card-text>
|
||||
<v-row class="align-center pt-6">
|
||||
<v-col cols="12" md="6">
|
||||
<span class="white--text"> This will delete ALL OF YOUR SETTINGS and restart PhotonVision. </span>
|
||||
<span class="text-white"> This will delete ALL OF YOUR SETTINGS and restart PhotonVision. </span>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-btn color="secondary" block @click="openExportSettingsPrompt">
|
||||
<v-icon left class="open-icon"> mdi-export </v-icon>
|
||||
<v-icon start class="open-icon"> mdi-export </v-icon>
|
||||
<span class="open-label">Backup Settings</span>
|
||||
<a
|
||||
ref="exportSettings"
|
||||
@@ -511,7 +583,7 @@ const openExportSettingsPrompt = () => {
|
||||
:loading="deletingCamera"
|
||||
@click="deleteThisCamera(cameraToDelete.uniqueName)"
|
||||
>
|
||||
<v-icon left class="open-icon"> mdi-trash-can-outline </v-icon>
|
||||
<v-icon start class="open-icon"> mdi-trash-can-outline </v-icon>
|
||||
<span class="open-label">DELETE (UNRECOVERABLE)</span>
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
@@ -521,7 +593,11 @@ const openExportSettingsPrompt = () => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.v-data-table {
|
||||
.v-card-title {
|
||||
text-wrap-mode: wrap !important;
|
||||
}
|
||||
|
||||
.v-table {
|
||||
background-color: #006492 !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ const arducamWarningShown = computed<boolean>(() => {
|
||||
>Arducam Camera Detected! Please configure the camera model in the <a href="#/cameras">Cameras tab</a>!
|
||||
</span>
|
||||
</v-banner>
|
||||
<v-row no-gutters align="center" justify="center">
|
||||
<v-row no-gutters>
|
||||
<v-col cols="12" class="pb-3 pr-lg-3" lg="8" align-self="stretch">
|
||||
<CamerasCard v-model="cameraViewType" />
|
||||
</v-col>
|
||||
@@ -88,7 +88,7 @@ const arducamWarningShown = computed<boolean>(() => {
|
||||
|
||||
<!-- TODO - not sure this belongs here -->
|
||||
<v-dialog v-if="warningShown" v-model="warningShown" :persistent="false" max-width="800" dark>
|
||||
<v-card dark flat color="primary">
|
||||
<v-card flat color="primary">
|
||||
<v-card-title>Setup some cameras to get started!</v-card-title>
|
||||
<v-card-text>
|
||||
No cameras activated - head to the <a href="#/cameraConfigs">Camera matching tab</a> to set some up!
|
||||
|
||||
@@ -3,7 +3,7 @@ const devMode = process.env.NODE_ENV === "development";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="overflow: hidden; height: 100%; width: 100%">
|
||||
<div style="overflow: hidden; height: 100vh; width: 100%">
|
||||
<div v-if="devMode" style="width: 100%; height: 100%; padding: 16px">
|
||||
<span style="color: white; font-weight: bold">
|
||||
PhotonClient is in development mode so the documentation page will not load. Please recompile in production mode
|
||||
|
||||
@@ -1,32 +1,22 @@
|
||||
import { fileURLToPath, URL } from "node:url";
|
||||
|
||||
import { defineConfig } from "vite";
|
||||
import Vue2 from "@vitejs/plugin-vue2";
|
||||
import Components from "unplugin-vue-components/vite";
|
||||
import { VuetifyResolver } from "unplugin-vue-components/resolvers";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import vuetify from "vite-plugin-vuetify";
|
||||
|
||||
export default defineConfig({
|
||||
base: "./",
|
||||
plugins: [
|
||||
Vue2(),
|
||||
Components({
|
||||
resolvers: [VuetifyResolver()],
|
||||
dts: true,
|
||||
transformer: "vue2",
|
||||
types: [
|
||||
{
|
||||
from: "vue-router",
|
||||
names: ["RouterLink", "RouterView"]
|
||||
}
|
||||
],
|
||||
version: 2.7
|
||||
vue(),
|
||||
vuetify({
|
||||
styles: {
|
||||
configFile: "src/assets/styles/settings.scss"
|
||||
}
|
||||
})
|
||||
],
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
sass: {
|
||||
additionalData: ["@import \"@/assets/styles/variables.scss\"", ""].join("\n")
|
||||
}
|
||||
sass: {}
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
@@ -35,6 +25,9 @@ export default defineConfig({
|
||||
}
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
external: ["html2canvas", "dompurify", "canvg"]
|
||||
},
|
||||
sourcemap: true
|
||||
}
|
||||
});
|
||||
|
||||
@@ -26,12 +26,6 @@ nativeConfig.dependencies.add wpilibTools.deps.wpilib("hal")
|
||||
nativeConfig.dependencies.add wpilibTools.deps.wpilibOpenCv("frc" + openCVYear, wpi.versions.opencvVersion.get())
|
||||
|
||||
dependencies {
|
||||
// JOGL stuff (currently we only distribute for aarch64, which is Pi 4)
|
||||
implementation "org.jogamp.gluegen:gluegen-rt:$joglVersion"
|
||||
implementation "org.jogamp.jogl:jogl-all:$joglVersion"
|
||||
implementation "org.jogamp.gluegen:gluegen-rt:$joglVersion:natives-linux-aarch64"
|
||||
implementation "org.jogamp.jogl:jogl-all:$joglVersion:natives-linux-aarch64"
|
||||
|
||||
// Zip
|
||||
implementation 'org.zeroturnaround:zt-zip:1.14'
|
||||
|
||||
|
||||
@@ -27,16 +27,19 @@ package org.photonvision.common.configuration;
|
||||
public final class DatabaseSchema {
|
||||
public static final String[] migrations = {
|
||||
// #1 - initial schema
|
||||
"CREATE TABLE IF NOT EXISTS global (\n"
|
||||
+ " filename TINYTEXT PRIMARY KEY,\n"
|
||||
+ " contents mediumtext NOT NULL\n"
|
||||
+ ");"
|
||||
+ "CREATE TABLE IF NOT EXISTS cameras (\n"
|
||||
+ " unique_name TINYTEXT PRIMARY KEY,\n"
|
||||
+ " config_json text NOT NULL,\n"
|
||||
+ " drivermode_json text NOT NULL,\n"
|
||||
+ " pipeline_jsons mediumtext NOT NULL\n"
|
||||
+ ");",
|
||||
// spotless:off
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS global (
|
||||
filename TINYTEXT PRIMARY KEY,
|
||||
contents mediumtext NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS cameras (
|
||||
unique_name TINYTEXT PRIMARY KEY,
|
||||
config_json text NOT NULL,
|
||||
drivermode_json text NOT NULL,
|
||||
pipeline_jsons mediumtext NOT NULL
|
||||
);""",
|
||||
// spotless:on
|
||||
// #2 - add column otherpaths_json
|
||||
"ALTER TABLE cameras ADD COLUMN otherpaths_json TEXT NOT NULL DEFAULT '[]';",
|
||||
// add future migrations here
|
||||
|
||||
@@ -19,113 +19,59 @@ package org.photonvision.common.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class HardwareConfig {
|
||||
public final String deviceName;
|
||||
public final String deviceLogoPath;
|
||||
public final String supportURL;
|
||||
public record HardwareConfig(
|
||||
String deviceName,
|
||||
String deviceLogoPath,
|
||||
String supportURL,
|
||||
// LED control
|
||||
|
||||
// LED control
|
||||
public final ArrayList<Integer> ledPins;
|
||||
public final String ledSetCommand;
|
||||
public final boolean ledsCanDim;
|
||||
public final ArrayList<Integer> ledBrightnessRange;
|
||||
public final String ledDimCommand;
|
||||
public final String ledBlinkCommand;
|
||||
public final ArrayList<Integer> statusRGBPins;
|
||||
ArrayList<Integer> ledPins,
|
||||
String ledSetCommand,
|
||||
boolean ledsCanDim,
|
||||
ArrayList<Integer> ledBrightnessRange,
|
||||
String ledDimCommand,
|
||||
String ledBlinkCommand,
|
||||
ArrayList<Integer> statusRGBPins,
|
||||
// Metrics
|
||||
|
||||
// Metrics
|
||||
public final String cpuTempCommand;
|
||||
public final String cpuMemoryCommand;
|
||||
public final String cpuUtilCommand;
|
||||
public final String cpuThrottleReasonCmd;
|
||||
public final String cpuUptimeCommand;
|
||||
public final String gpuMemoryCommand;
|
||||
public final String ramUtilCommand;
|
||||
public final String gpuMemUsageCommand;
|
||||
public final String diskUsageCommand;
|
||||
|
||||
// Device stuff
|
||||
public final String restartHardwareCommand;
|
||||
public final double vendorFOV; // -1 for unmanaged
|
||||
public final List<Integer> blacklistedResIndices; // this happens before the defaults are applied
|
||||
String cpuTempCommand,
|
||||
String cpuMemoryCommand,
|
||||
String cpuUtilCommand,
|
||||
String cpuThrottleReasonCmd,
|
||||
String cpuUptimeCommand,
|
||||
String gpuMemoryCommand,
|
||||
String ramUtilCommand,
|
||||
String gpuMemUsageCommand,
|
||||
String diskUsageCommand,
|
||||
// Device stuff
|
||||
String restartHardwareCommand,
|
||||
double vendorFOV) { // -1 for unmanaged
|
||||
|
||||
public HardwareConfig() {
|
||||
deviceName = "";
|
||||
deviceLogoPath = "";
|
||||
supportURL = "";
|
||||
ledPins = new ArrayList<>();
|
||||
ledSetCommand = "";
|
||||
ledsCanDim = false;
|
||||
ledBrightnessRange = new ArrayList<>();
|
||||
statusRGBPins = new ArrayList<>();
|
||||
ledDimCommand = "";
|
||||
|
||||
cpuTempCommand = "";
|
||||
cpuMemoryCommand = "";
|
||||
cpuUtilCommand = "";
|
||||
cpuThrottleReasonCmd = "";
|
||||
cpuUptimeCommand = "";
|
||||
gpuMemoryCommand = "";
|
||||
ramUtilCommand = "";
|
||||
ledBlinkCommand = "";
|
||||
gpuMemUsageCommand = "";
|
||||
diskUsageCommand = "";
|
||||
|
||||
restartHardwareCommand = "";
|
||||
vendorFOV = -1;
|
||||
blacklistedResIndices = Collections.emptyList();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public HardwareConfig(
|
||||
String deviceName,
|
||||
String deviceLogoPath,
|
||||
String supportURL,
|
||||
ArrayList<Integer> ledPins,
|
||||
String ledSetCommand,
|
||||
boolean ledsCanDim,
|
||||
ArrayList<Integer> ledBrightnessRange,
|
||||
String ledDimCommand,
|
||||
String ledBlinkCommand,
|
||||
ArrayList<Integer> statusRGBPins,
|
||||
String cpuTempCommand,
|
||||
String cpuMemoryCommand,
|
||||
String cpuUtilCommand,
|
||||
String cpuThrottleReasonCmd,
|
||||
String cpuUptimeCommand,
|
||||
String gpuMemoryCommand,
|
||||
String ramUtilCommand,
|
||||
String gpuMemUsageCommand,
|
||||
String diskUsageCommand,
|
||||
String restartHardwareCommand,
|
||||
double vendorFOV,
|
||||
List<Integer> blacklistedResIndices) {
|
||||
this.deviceName = deviceName;
|
||||
this.deviceLogoPath = deviceLogoPath;
|
||||
this.supportURL = supportURL;
|
||||
this.ledPins = ledPins;
|
||||
this.ledSetCommand = ledSetCommand;
|
||||
this.ledsCanDim = ledsCanDim;
|
||||
this.ledBrightnessRange = ledBrightnessRange;
|
||||
this.ledDimCommand = ledDimCommand;
|
||||
this.ledBlinkCommand = ledBlinkCommand;
|
||||
this.statusRGBPins = statusRGBPins;
|
||||
this.cpuTempCommand = cpuTempCommand;
|
||||
this.cpuMemoryCommand = cpuMemoryCommand;
|
||||
this.cpuUtilCommand = cpuUtilCommand;
|
||||
this.cpuThrottleReasonCmd = cpuThrottleReasonCmd;
|
||||
this.cpuUptimeCommand = cpuUptimeCommand;
|
||||
this.gpuMemoryCommand = gpuMemoryCommand;
|
||||
this.ramUtilCommand = ramUtilCommand;
|
||||
this.gpuMemUsageCommand = gpuMemUsageCommand;
|
||||
this.diskUsageCommand = diskUsageCommand;
|
||||
this.restartHardwareCommand = restartHardwareCommand;
|
||||
this.vendorFOV = vendorFOV;
|
||||
this.blacklistedResIndices = blacklistedResIndices;
|
||||
this(
|
||||
"", // deviceName
|
||||
"", // deviceLogoPath
|
||||
"", // supportURL
|
||||
new ArrayList<>(), // ledPins
|
||||
"", // ledSetCommand
|
||||
false, // ledsCanDim
|
||||
new ArrayList<>(), // ledBrightnessRange
|
||||
"", // ledDimCommand
|
||||
"", // ledBlinkCommand
|
||||
new ArrayList<>(), // statusRGBPins
|
||||
"", // cpuTempCommand
|
||||
"", // cpuMemoryCommand
|
||||
"", // cpuUtilCommand
|
||||
"", // cpuThrottleReasonCmd
|
||||
"", // cpuUptimeCommand
|
||||
"", // gpuMemoryCommand
|
||||
"", // ramUtilCommand
|
||||
"", // gpuMemUsageCommand
|
||||
"", // diskUsageCommand
|
||||
"", // restartHardwareCommand
|
||||
-1); // vendorFOV
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,53 +96,4 @@ public class HardwareConfig {
|
||||
|| gpuMemUsageCommand != ""
|
||||
|| diskUsageCommand != "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "HardwareConfig [deviceName="
|
||||
+ deviceName
|
||||
+ ", deviceLogoPath="
|
||||
+ deviceLogoPath
|
||||
+ ", supportURL="
|
||||
+ supportURL
|
||||
+ ", ledPins="
|
||||
+ ledPins
|
||||
+ ", ledSetCommand="
|
||||
+ ledSetCommand
|
||||
+ ", ledsCanDim="
|
||||
+ ledsCanDim
|
||||
+ ", ledBrightnessRange="
|
||||
+ ledBrightnessRange
|
||||
+ ", ledDimCommand="
|
||||
+ ledDimCommand
|
||||
+ ", ledBlinkCommand="
|
||||
+ ledBlinkCommand
|
||||
+ ", statusRGBPins="
|
||||
+ statusRGBPins
|
||||
+ ", cpuTempCommand="
|
||||
+ cpuTempCommand
|
||||
+ ", cpuMemoryCommand="
|
||||
+ cpuMemoryCommand
|
||||
+ ", cpuUtilCommand="
|
||||
+ cpuUtilCommand
|
||||
+ ", cpuThrottleReasonCmd="
|
||||
+ cpuThrottleReasonCmd
|
||||
+ ", cpuUptimeCommand="
|
||||
+ cpuUptimeCommand
|
||||
+ ", gpuMemoryCommand="
|
||||
+ gpuMemoryCommand
|
||||
+ ", ramUtilCommand="
|
||||
+ ramUtilCommand
|
||||
+ ", gpuMemUsageCommand="
|
||||
+ gpuMemUsageCommand
|
||||
+ ", diskUsageCommand="
|
||||
+ diskUsageCommand
|
||||
+ ", restartHardwareCommand="
|
||||
+ restartHardwareCommand
|
||||
+ ", vendorFOV="
|
||||
+ vendorFOV
|
||||
+ ", blacklistedResIndices="
|
||||
+ blacklistedResIndices
|
||||
+ "]";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.TemporalAccessor;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.util.file.FileUtils;
|
||||
@@ -280,9 +279,7 @@ class LegacyConfigProvider extends ConfigProvider {
|
||||
HashMap<String, CameraConfiguration> loadedConfigurations = new HashMap<>();
|
||||
try {
|
||||
var subdirectories =
|
||||
Files.list(camerasFolder.toPath())
|
||||
.filter(f -> f.toFile().isDirectory())
|
||||
.collect(Collectors.toList());
|
||||
Files.list(camerasFolder.toPath()).filter(f -> f.toFile().isDirectory()).toList();
|
||||
|
||||
for (var subdir : subdirectories) {
|
||||
var cameraConfigPath = Path.of(subdir.toString(), "config.json");
|
||||
@@ -348,7 +345,7 @@ class LegacyConfigProvider extends ConfigProvider {
|
||||
return null;
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList())
|
||||
.toList()
|
||||
: Collections.emptyList();
|
||||
|
||||
loadedConfig.driveModeSettings = driverMode;
|
||||
|
||||
@@ -53,9 +53,9 @@ public class PathManager {
|
||||
|
||||
public static final String LOG_PREFIX = "photonvision-";
|
||||
public static final String LOG_EXT = ".log";
|
||||
public static final String LOG_DATE_TIME_FORMAT = "yyyy-M-d_hh-mm-ss";
|
||||
public static final String LOG_DATE_TIME_FORMAT = "yyyy-MM-dd_HH-mm-ss";
|
||||
|
||||
public String taToLogFname(TemporalAccessor date) {
|
||||
public static String taToLogFname(TemporalAccessor date) {
|
||||
var dateString = DateTimeFormatter.ofPattern(LOG_DATE_TIME_FORMAT).format(date);
|
||||
return LOG_PREFIX + dateString + LOG_EXT;
|
||||
}
|
||||
|
||||