Compare commits

...

21 Commits

Author SHA1 Message Date
samfreund
ac8cccaa2f lint 2026-03-31 22:38:08 -05:00
samfreund
98fee3bd1f CI changes 2026-03-31 22:38:04 -05:00
samfreund
d7bac45e76 quarky, don't run away! 2026-03-31 19:27:48 -05:00
Matt Morley
9883008ed3 more generic quotes 2026-03-31 18:43:18 -05:00
Matt Morley
01b8b8ccb3 Mini-quarky spawn 2026-03-31 18:43:13 -05:00
Matt Morley
fd3d9f6ccc Mini-quarkies that bounce of edges 2026-03-31 18:43:10 -05:00
Matt Morley
d7f0e17dda Even more phrases 2026-03-31 18:43:06 -05:00
Chris Gerth
966071ae2d bad timeouts, more phrases 2026-03-31 18:43:03 -05:00
Chris Gerth
502ae644a4 look ma no inter 2026-03-31 18:42:57 -05:00
Jade
515a1a3d78 Fix comments in OutputStream pipeline (#2415) 2026-03-28 15:13:47 -05:00
Sam Freund
03ffcb1215 Upgrade website dependencies (#2414) 2026-03-27 14:31:18 -04:00
Sam Freund
131098bfdd Add AE quirk to OV2311 (#2411) 2026-03-27 10:52:04 -05:00
Chris Gerth
b5277e5f4c Add Community Contribution Guidelines (#2405)
## Description

Leadership team identified gaps in how we introduce new developers to
the community.

Additional docs for onboarding new developers, helping clarify roles and
what counts as a "good" PR.

## Meta

Merge checklist:
- [ X] Pull Request title is [short, imperative
summary](https://cbea.ms/git-commit/) of proposed changes
- [ X] The description documents the _what_ and _why_, including events
that led to this PR
- [ ] If this PR changes behavior or adds a feature, user documentation
is updated
- [ ] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [ ] If this PR touches configuration, this is backwards compatible
with all settings going back to the previous seasons's last release
(seasons end after champs ends)
- [ ] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [ ] If this PR addresses a bug, a regression test for it is added
- [ ] If this PR adds a dependency, the license has been checked for
compatibility and steps taken to follow it
2026-03-27 02:32:17 +00:00
Jesse Kane
52f1f7726f Fix strong reference in Cleaners (#2404) 2026-03-26 18:55:06 +00:00
Sam Freund
e19534da47 Remove old camera quirk aliases (#2412) 2026-03-26 14:22:58 -04:00
Gold856
488646e755 Upgrade to Vite 8 (#2408) 2026-03-26 04:39:38 +00:00
Jade
a8a0024321 Add nix files to .gitignore (#2409)
I assume based off discord comments a PR won't be accepted for a
shell.nix or equivalent flake solution so we should gitignore this so
people don't accidentally add them to PRs
2026-03-25 23:04:38 -05:00
Sam Freund
032deba775 refactor dark mode checks (#2407)
The current method for checking light vs. dark mode is to compare the
name of the theme against a hardcoded string. This PR uses a dark mode
boolean. This change is for verbosity and so that we're not reliant on
theme name. Additionally, we change some references to colors to the
global theme, instead of indexing the list of themes.
2026-03-24 17:49:56 -05:00
Gold856
7b240a027a Add more linting rules (#2406)
As a precursor to #2394, add a bunch of linting rules to try and catch
more mistakes/potential code errors/unnecessary code. Add a bunch of
rules from https://eslint.vuejs.org/rules/ in the "uncategorized"
section that seem useful to have.
2026-03-23 18:41:11 -04:00
Gold856
d4bfe643d5 Clean up C++ headers (#2402) 2026-03-19 06:10:04 +00:00
Stéphane Dalton
12446a6c44 Populate classId and confidence level for object detection in Java simulation (#2372)
## Description

compute confidence level based on target area in total image size and
populate classId and confidence level in Java (while building the
PhotonTrackedTarget)

## Changes
- Add new VisionTargetSim constructor for object detection
- If class ID specified but confidence = -1, estimate based on total
area

---------

Co-authored-by: Matt Morley <matthew.morley.ca@gmail.com>
2026-03-19 03:42:37 +00:00
107 changed files with 2319 additions and 1694 deletions

View File

@@ -248,11 +248,11 @@ jobs:
- run: git fetch --tags --force
- run: ./gradlew photon-targeting:build photon-lib:build
name: Build with Gradle
- run: ./gradlew photon-lib:publish photon-targeting:publish
name: Publish
env:
ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
if: github.event_name == 'push' && github.repository_owner == 'photonvision'
# - run: ./gradlew photon-lib:publish photon-targeting:publish
# name: Publish
# env:
# ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
# if: github.event_name == 'push' && github.repository_owner == 'photonvision'
# Copy artifacts to build/outputs/maven
- run: ./gradlew photon-lib:publish photon-targeting:publish -PcopyOfflineArtifacts
- uses: actions/upload-artifact@v6
@@ -289,11 +289,11 @@ jobs:
- name: Build PhotonLib
# We don't need to run tests, since we specify only non-native platforms
run: ./gradlew photon-targeting:build photon-lib:build ${{ matrix.build-options }} -x test
- name: Publish
run: ./gradlew photon-lib:publish photon-targeting:publish ${{ matrix.build-options }}
env:
ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
if: github.event_name == 'push' && github.repository_owner == 'photonvision'
# - name: Publish
# run: ./gradlew photon-lib:publish photon-targeting:publish ${{ matrix.build-options }}
# env:
# ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
# if: github.event_name == 'push' && github.repository_owner == 'photonvision'
# Copy artifacts to build/outputs/maven
- run: ./gradlew photon-lib:publish photon-targeting:publish -PcopyOfflineArtifacts ${{ matrix.build-options }}
- uses: actions/upload-artifact@v6
@@ -664,12 +664,9 @@ jobs:
pattern: image-*
- run: find
# Push to dev release
- uses: pyTooling/Actions/releaser@r6
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag: 'Dev'
rm: true
snapshots: false
files: |
**/*.xz
@@ -677,13 +674,13 @@ jobs:
**/*win*.jar
**/photonlib*.json
**/photonlib*.zip
if: github.event_name == 'push'
- name: Create Vendor JSON Repo PR
uses: wpilibsuite/vendor-json-repo/.github/actions/add_vendordep@HEAD
with:
repo: PhotonVision/vendor-json-repo
token: ${{ secrets.VENDOR_JSON_REPO_PUSH_TOKEN }}
vendordep_file: ${{ github.workspace }}/photonlib-${{ github.ref_name }}.json
pr_title: Update photonlib to ${{ github.ref_name }}
pr_branch: photonlib-${{ github.ref_name }}
if: github.repository == 'PhotonVision/photonvision' && startsWith(github.ref, 'refs/tags/v')
if: startsWith(github.ref, 'refs/tags/v')
# - name: Create Vendor JSON Repo PR
# uses: wpilibsuite/vendor-json-repo/.github/actions/add_vendordep@HEAD
# with:
# repo: PhotonVision/vendor-json-repo
# token: ${{ secrets.VENDOR_JSON_REPO_PUSH_TOKEN }}
# vendordep_file: ${{ github.workspace }}/photonlib-${{ github.ref_name }}.json
# pr_title: Update photonlib to ${{ github.ref_name }}
# pr_branch: photonlib-${{ github.ref_name }}
# if: github.repository == 'PhotonVision/photonvision' && startsWith(github.ref, 'refs/tags/v')

View File

@@ -84,22 +84,22 @@ jobs:
with:
pattern: docs-*
- run: find .
- name: Publish Docs To Development
if: github.ref == 'refs/heads/main'
uses: up9cloud/action-rsync@v1.4
env:
HOST: ${{ secrets.WEBMASTER_SSH_HOST }}
USER: ${{ secrets.WEBMASTER_SSH_USERNAME }}
KEY: ${{secrets.WEBMASTER_SSH_KEY}}
TARGET: /var/www/html/photonvision-docs/development/
- name: Publish Docs To Release
if: startsWith(github.ref, 'refs/tags/v')
uses: up9cloud/action-rsync@v1.4
env:
HOST: ${{ secrets.WEBMASTER_SSH_HOST }}
USER: ${{ secrets.WEBMASTER_SSH_USERNAME }}
KEY: ${{ secrets.WEBMASTER_SSH_KEY }}
TARGET: /var/www/html/photonvision-docs/release/
# - name: Publish Docs To Development
# if: github.ref == 'refs/heads/main'
# uses: up9cloud/action-rsync@v1.4
# env:
# HOST: ${{ secrets.WEBMASTER_SSH_HOST }}
# USER: ${{ secrets.WEBMASTER_SSH_USERNAME }}
# KEY: ${{secrets.WEBMASTER_SSH_KEY}}
# TARGET: /var/www/html/photonvision-docs/development/
# - name: Publish Docs To Release
# if: startsWith(github.ref, 'refs/tags/v')
# uses: up9cloud/action-rsync@v1.4
# env:
# HOST: ${{ secrets.WEBMASTER_SSH_HOST }}
# USER: ${{ secrets.WEBMASTER_SSH_USERNAME }}
# KEY: ${{ secrets.WEBMASTER_SSH_KEY }}
# TARGET: /var/www/html/photonvision-docs/release/
publish_demo:
name: Publish PhotonClient Demo
@@ -111,7 +111,6 @@ jobs:
name: built-demo
- run: find .
- name: Publish demo
if: github.ref == 'refs/heads/main'
uses: up9cloud/action-rsync@v1.4
env:
HOST: ${{ secrets.WEBMASTER_SSH_HOST }}

View File

@@ -123,23 +123,23 @@ jobs:
./run.sh $folder
done
deploy:
needs: [test-py, build-python-examples]
runs-on: ubuntu-24.04
# Only upload on tags
if: startsWith(github.ref, 'refs/tags/v')
# deploy:
# needs: [test-py, build-python-examples]
# runs-on: ubuntu-24.04
# # Only upload on tags
# if: startsWith(github.ref, 'refs/tags/v')
steps:
- name: Download artifacts
uses: actions/download-artifact@v6
with:
name: dist
path: dist/
# steps:
# - name: Download artifacts
# uses: actions/download-artifact@v6
# with:
# name: dist
# path: dist/
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: ./dist/
# - name: Publish package distributions to PyPI
# uses: pypa/gh-action-pypi-publish@release/v1
# with:
# packages-dir: ./dist/
permissions:
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
# permissions:
# id-token: write # IMPORTANT: this permission is mandatory for trusted publishing

5
.gitignore vendored
View File

@@ -156,3 +156,8 @@ photon-client/playwright-report/
photon-client/blob-report/
photon-client/playwright/.cache/
photon-client/playwright/.auth/
# Nix files
shell.nix
flake.nix
flake.lock

View File

@@ -0,0 +1,68 @@
# Welcome!
First and foremost, welcome to PhotonVision Development! We're pumped that you're interested to jump in, and help out!
Like most things in FIRST, PhotonVision is reliant on the hard work of dedicated volunteers. It doesn't exist without you. You're joining in with a strong tradition of open-source software development.
This page talks a bit about how we develop PhotonVision, as a community. It applies to all repos and community aspects for PhotonVision.
## Getting Started
The very first thing we'd recommend - get your computer set up to be able to build and run PhotonVision. [Docs for that are here](building-photon.md).
The two best ways to figure out what to do first:
1. Take a look at [the main PhotonVision Repo's Issues](https://github.com/PhotonVision/photonvision/issues) - especially those marked `bug` or `good first issue`!
2. Connect on [the Discord Server](https://discord.gg/wYxTwym) - Introduce yourself, and talk about what you're interested in!
From there - assign yourself to an issue if you intend to work it. Create a Fork in github, and make and test your changes. Once passing builds, meeting your expectations, and passing CI, open a pull request back to the main repo.
## Submitting A PR
Pull Requests are the mechanism used to ensure we only merge high-quality, reviewed, concrete, and organized changes to the codebase.
Things that peer reviewers will look for include:
* All CI checks are passing on the server.
* Documentation - does the PR match the issue? Is the description detailed?
* Cohesiveness - does the PR express a singular, related set of changes?
* Architecture - is the code consistent with other code around it? Is it maintainable for the long term?
* Testing - have unit tests been added as appropriate? Has the change been tested on real hardware?
Work as you can to clean up any changes requested. Once all changes are addressed, the contain should get merged, and included in the next release! Horary!
## General Developer Interaction
The main guiding principle: remember that we're all volunteers. While promptness is always appreciated (and occasionally required as release deadlines approach), it's important to remember each individual is only contributing when their personal schedule allows. Expect delays, prefer asynchronous communication, and be polite with reminders.
While most of the community members are either FIRST robotics mentors or students, the PhotonVision development team is primarily focused on delivering high quality software. Mentorship can and does occur, but is not the primary goal. Members are expected to do their learning fairly independently.
Seek to build trust in the quality of your work. Think carefully on your opinions before asking others to think about them too.
Bias toward action, and productionizable code. Limit the number of active PR's to help keep focus.
Finally, be sure to embody the ethos of Gracious Professionalism in all your actions, on all platforms in the project. See more in our [code of conduct](https://github.com/PhotonVision/.github/blob/738dfcb792fdbfc2e8408c0135e389179fc483c0/codeofcoduct.md)
### AI Usage
Coding assistants driven by Large Language Models ("AI") are extremely powerful tools, and have been used on more than one occasion to accelerate development.
PhotonVision still maintains a fundamental philosophy that the human submitting the pull request is responsible for the code and its behavior, regardless of the tools used to create it.
These tools can also generate a large volume of code changes, very rapidly. The above guidelines on PR quality still apply - large, undirected, or overly-scoped PR's are likely to be ignored, regardless of tooling used to generate them.
### Violations
We're thankful that we've rarely experienced major issues in our community. In all cases, the project leads shall have the final decision making authority when dealing with violations of these guidelines.
## Yearly Development Cycle
PhotonVision's Development Cycle follows the same general flow as the FRC Build Season. Larger experiments get done over the summer, fall focuses on testing and "production-readiness", build season focuses on keeping all teams running smoothly.
The actual priorities also shift as developers have more or time to commit, or express interest in specific direction.
PR's are absolutely always welcome. Just note depending on the scope and size, they may not be reviewed or merged immediately.
## Project Governance
This project is jointly lead by Matt and Banks, who may be contacted on Discord. They serve as a "Benevolent leader for life" role, coordinating and approving architecture as needed, and curating the team of developers who have Pull Request review and merge responsibilities.

View File

@@ -1,6 +1,7 @@
# Contributing to PhotonVision Projects
```{toctree}
guidelines
building-photon
building-docs
linting

View File

@@ -95,6 +95,27 @@ These `TargetModel` are paired with a target pose to create a `VisionTargetSim`.
The pose of a `VisionTargetSim` object can be updated to simulate moving targets. Note, however, that this will break latency simulation for that target.
:::
To use simulated object detection, you must provide an objDetClassId (zero-indexed class ID) and confidence value. When you set objDetConf to -1, the simulation computes confidence based on the area of the target in the camera's field of view. To simulate a object detection model with one class (fuel, index 0) and specify confidence, you'd write:
```{eval-rst}
.. tab-set-code::
.. code-block:: java
// arbitrary position on field
final var targetPose = new Pose3d(new Translation3d(2, 0, 0), new Rotation3d());
// Class id, zero-indexed
final int classId = 0;
// Confidence, between 0 and 1.
final float conf = 0.67f;
// 6 inch diameter ball
final TargetModel ballModel = new TargetModel(Units.inchesToMeters(6));
final var ballTargetSim = new VisionTargetSim(targetPose, ballModel, classId, conf);
// Add this vision target to the vision system simulation to make it visible
visionSim.addVisionTargets(visionTarget);
```
For convenience, an `AprilTagFieldLayout` can also be added to automatically create a target for each of its AprilTags.
```{eval-rst}

View File

@@ -4,7 +4,7 @@ import { defineConfigWithVueTs, vueTsConfigs } from "@vue/eslint-config-typescri
import skipFormattingConfig from "@vue/eslint-config-prettier/skip-formatting";
export default defineConfigWithVueTs(
pluginVue.configs["flat/recommended"],
pluginVue.configs["flat/recommended-error"],
vueTsConfigs.recommended,
skipFormattingConfig,
{
@@ -26,10 +26,24 @@ export default defineConfigWithVueTs(
semi: ["error", "always"],
"eol-last": "error",
eqeqeq: "error",
"no-useless-concat": "error",
"object-curly-spacing": ["error", "always"],
"quote-props": ["error", "as-needed"],
"no-case-declarations": "off",
"vue/eqeqeq": "error",
"vue/no-useless-concat": "error",
"vue/no-constant-condition": "error",
"vue/no-empty-pattern": "error",
"vue/no-undef-directives": "error",
"vue/no-undef-properties": "error",
"vue/no-unused-properties": "error",
"vue/no-unused-refs": "error",
"vue/no-use-v-else-with-v-for": "error",
"vue/no-useless-mustaches": "error",
"vue/no-useless-v-bind": "error",
"vue/require-default-prop": "off",
"vue/v-for-delimiter-style": "error",
"vue/v-on-event-hyphenation": "off",
"@typescript-eslint/no-explicit-any": "off",
"vue/valid-v-slot": ["error", { allowModifiers: true }]

View File

@@ -41,11 +41,11 @@
"@vue/eslint-config-typescript": "^14.5.0",
"@vue/tsconfig": "^0.7.0",
"eslint": "^9.31.0",
"eslint-plugin-vue": "^10.3.0",
"eslint-plugin-vue": "^10.7.0",
"prettier": "^3.6.2",
"sass": "^1.89.2",
"typescript": "^5.8.3",
"vite": "^7.0.5",
"vite": "^8.0.2",
"vite-plugin-vuetify": "^2.1.1"
}
}

View File

@@ -74,7 +74,7 @@ export default defineConfig({
/* Run your local dev server before starting the tests */
webServer: {
command: process.platform == "win32" ? "" : "./" + "gradlew run",
command: process.platform === "win32" ? "" : "./gradlew run",
url: "http://localhost:5800",
timeout: 300 * 1000,
reuseExistingServer: !process.env.CI,

File diff suppressed because it is too large Load Diff

View File

@@ -75,6 +75,36 @@ onBeforeMount(() => {
<photon-log-view />
<photon-error-snackbar />
</v-app>
<!-- Quarky overlay -->
<div class="quarky-overlay">
<div id="quarkyContainer" class="quarky-container" style="left: calc(100vw - 550px); top: calc(100vh - 550px)">
<img id="quarkyImage" src="" alt="Quarky" />
<div
id="quarkySpeechBubble"
style="
display: none;
position: absolute;
left: 50%;
top: 0;
transform: translateX(-50%);
min-width: 120px;
max-width: 320px;
padding: 16px 24px;
background: #fff;
color: #222;
border-radius: 16px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
font-size: 1.2em;
font-family: sans-serif;
opacity: 0;
transition: opacity 0.7s;
pointer-events: none;
z-index: 10;
"
></div>
</div>
</div>
</template>
<style lang="scss">
@@ -117,4 +147,33 @@ onBeforeMount(() => {
div.v-layout {
overflow: unset !important;
}
/* Overlay container for Quarky */
.quarky-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1000;
}
/* Quarky animation container */
.quarky-container {
position: absolute;
width: 500px;
height: 500px;
background-color: transparent;
transition:
left 1s cubic-bezier(0.42, 0, 0.58, 1),
top 1s cubic-bezier(0.42, 0, 0.58, 1);
}
.quarky-container img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

@@ -50,8 +50,8 @@ const drawTargets = async (targets: PhotonTarget[]) => {
return;
}
if (theme.global.name.value === "LightTheme") scene.background = new Color(0xa9a9a9);
else scene.background = new Color(0x000000);
if (theme.global.current.value.dark) scene.background = new Color(0x000000);
else scene.background = new Color(0xa9a9a9);
scene.remove(...previousTargets);
previousTargets = [];
@@ -158,8 +158,8 @@ onMounted(async () => {
if (!canvas) return;
renderer = new WebGLRenderer({ canvas: canvas });
if (theme.global.name.value === "LightTheme") scene.background = new Color(0xa9a9a9);
else scene.background = new Color(0x000000);
if (theme.global.current.value.dark) scene.background = new Color(0x000000);
else scene.background = new Color(0xa9a9a9);
onWindowResize();
window.addEventListener("resize", onWindowResize);
@@ -234,7 +234,7 @@ watchEffect(() => {
<v-btn
style="width: 100%"
color="buttonActive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="resetCamFirstPerson"
>
First Person
@@ -244,7 +244,7 @@ watchEffect(() => {
<v-btn
style="width: 100%"
color="buttonActive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="resetCamThirdPerson"
>
Third Person

View File

@@ -208,8 +208,8 @@ onMounted(async () => {
const ambientLight = new AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
if (theme.global.name.value === "LightTheme") scene.background = new Color(0xa9a9a9);
else scene.background = new Color(0x000000);
if (theme.global.current.value.dark) scene.background = new Color(0x000000);
else scene.background = new Color(0xa9a9a9);
// Initialize a stable aspect ratio so subsequent resize events derive
// height from width, avoiding layout feedback during continuous resizing
@@ -347,7 +347,7 @@ watch(
<v-btn
style="width: 100%"
color="buttonActive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="resetCamFirstPerson"
>
First Person
@@ -357,7 +357,7 @@ watch(
<v-btn
style="width: 100%"
color="buttonActive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="resetCamThirdPerson"
>
Third Person

View File

@@ -50,7 +50,7 @@ const containerStyle = computed<StyleValue>(() => {
});
const overlayStyle = computed<StyleValue>(() => {
if (useStateStore().colorPickingMode || streamSrc.value == emptyStreamSrc) {
if (useStateStore().colorPickingMode || streamSrc.value === emptyStreamSrc) {
return { display: "none" };
} else {
return {};

View File

@@ -14,7 +14,7 @@ import { useStateStore } from "@/stores/StateStore";
{{ useStateStore().snackbarData.message }}
</p>
<v-progress-linear
v-if="useStateStore().snackbarData.progressBar != -1"
v-if="useStateStore().snackbarData.progressBar !== -1"
v-model="useStateStore().snackbarData.progressBar"
height="15"
:color="useStateStore().snackbarData.progressBarColor"

View File

@@ -38,7 +38,7 @@ const renderCompact = computed<boolean>(() => compact.value || !mdAndUp.value);
<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" prepend-icon="mdi-camera">
<v-list-item link to="/cameras" prepend-icon="mdi-camera">
<v-list-item-title>Camera</v-list-item-title>
</v-list-item>
<v-list-item
@@ -73,7 +73,7 @@ const renderCompact = computed<boolean>(() => compact.value || !mdAndUp.value);
</v-list-item>
<v-list-item
link
:prepend-icon="theme.global.name.value === 'LightTheme' ? 'mdi-white-balance-sunny' : 'mdi-weather-night'"
:prepend-icon="theme.global.current.value.dark ? 'mdi-weather-night' : 'mdi-white-balance-sunny'"
@click="() => toggleTheme(theme)"
>
<v-list-item-title>Theme</v-list-item-title>

View File

@@ -28,7 +28,7 @@ const getUniqueVideoFormatsByResolution = (): VideoFormat[] => {
if (useCameraSettingsStore().currentCameraSettings.validVideoFormats.length === 0) return uniqueResolutions;
useCameraSettingsStore().currentCameraSettings.validVideoFormats.forEach((format) => {
const index = uniqueResolutions.findIndex((v) => resolutionsAreEqual(v.resolution, format.resolution));
const contains = index != -1;
const contains = index !== -1;
let skip = false;
if (contains && format.fps > uniqueResolutions[index].fps) {
uniqueResolutions.splice(index, 1);
@@ -132,7 +132,7 @@ const downloadCalibBoard = async () => {
const yPos = chessboardStartY + squareY * squareSizeIn.value;
// Only draw the odd squares to create the chessboard pattern
if (squareY % 2 != squareX % 2) {
if (squareY % 2 !== squareX % 2) {
doc.rect(xPos, yPos, squareSizeIn.value, squareSizeIn.value, "F");
}
}
@@ -293,7 +293,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
<v-card-subtitle v-if="!isCalibrating" class="pl-0 pb-3 pt-4 opacity-100"
>Configure New Calibration</v-card-subtitle
>
<v-form ref="form" v-model="settingsValid">
<v-form v-model="settingsValid">
<pv-select
v-model="uniqueVideoResolutionString"
label="Resolution"
@@ -470,7 +470,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
closable
density="compact"
class="mb-5"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
:color="useSettingsStore().general.mrCalWorking ? 'buttonPassive' : 'error'"
:icon="useSettingsStore().general.mrCalWorking ? 'mdi-check' : 'mdi-close'"
:text="
@@ -481,7 +481,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
/>
<div v-if="isCalibrating" class="d-flex justify-center align-center pb-5">
<v-chip
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
label
:color="useStateStore().calibrationData.hasEnoughImages ? 'buttonPassive' : 'light-grey'"
>
@@ -494,7 +494,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
color="buttonPassive"
size="small"
block
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
:disabled="!settingsValid"
@click="downloadCalibBoard"
>
@@ -509,7 +509,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
density="compact"
text="Too many corners. Finish calibration now!"
icon="mdi-alert-circle-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
/>
<div class="d-flex pt-5">
<v-col cols="6" class="pa-0 pr-2">
@@ -517,7 +517,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
size="small"
block
color="buttonActive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
:disabled="!settingsValid || tooManyPoints"
@click="isCalibrating ? useCameraSettingsStore().takeCalibrationSnapshot() : startCalibration()"
>
@@ -531,7 +531,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
<v-btn
size="small"
block
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
:color="useStateStore().calibrationData.hasEnoughImages ? 'buttonActive' : 'error'"
:disabled="!isCalibrating || !settingsValid"
@click="endCalibration"

View File

@@ -40,8 +40,8 @@ const importCalibration = async () => {
const data = await parseJsonFile<CameraCalibrationResult>(uploadedJson);
if (
data.resolution.height != props.videoFormat.resolution.height ||
data.resolution.width != props.videoFormat.resolution.width
data.resolution.height !== props.videoFormat.resolution.height ||
data.resolution.width !== props.videoFormat.resolution.width
) {
useStateStore().showSnackbarMessage({
color: "error",
@@ -121,7 +121,7 @@ const viewingImg = ref(0);
<v-btn
color="buttonPassive"
style="width: 100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="openUploadPhotonCalibJsonPrompt"
>
<v-icon start size="large"> mdi-import</v-icon>
@@ -140,7 +140,7 @@ const viewingImg = ref(0);
color="buttonPassive"
:disabled="!currentCalibrationCoeffs"
style="width: 100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="openExportCalibrationPrompt"
>
<v-icon start size="large">mdi-export</v-icon>
@@ -318,7 +318,7 @@ const viewingImg = ref(0);
color="primary"
text="The selected video format has not been calibrated."
icon="mdi-alert-circle-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
/>
</div>
<Suspense v-else-if="tab === 'details'">
@@ -341,7 +341,7 @@ const viewingImg = ref(0);
<pv-delete-modal
v-model="confirmRemoveDialog.show"
:width="500"
:title="'Delete Calibration'"
title="Delete Calibration"
:description="`Are you sure you want to delete the calibration for '${confirmRemoveDialog.vf.resolution.width}x${confirmRemoveDialog.vf.resolution.height}'? This action cannot be undone.`"
:on-confirm="() => removeCalibration(confirmRemoveDialog.vf)"
/>

View File

@@ -99,7 +99,7 @@ const expanded = ref([]);
<v-card-text class="pt-0">
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'tonal'"
@click="fetchSnapshots"
>
<v-icon start class="open-icon" size="large"> mdi-folder </v-icon>
@@ -115,7 +115,7 @@ const expanded = ref([]);
density="compact"
text="There are currently no saved snapshots."
icon="mdi-information-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
/>
</v-card-text>
<v-card-text v-else class="pt-0">
@@ -125,7 +125,7 @@ const expanded = ref([]);
density="compact"
text="Snapshot timestamps depend on when the coprocessor was last connected to the internet."
icon="mdi-information-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
/>
<v-data-table
v-model:expanded="expanded"

View File

@@ -66,10 +66,10 @@ const settingsHaveChanged = (): boolean => {
const b = useCameraSettingsStore().currentCameraSettings;
for (const q in ValidQuirks) {
if (a.quirksToChange[q] != b.cameraQuirks.quirks[q]) return true;
if (a.quirksToChange[q] !== b.cameraQuirks.quirks[q]) return true;
}
return a.fov != b.fov.value;
return a.fov !== b.fov.value;
};
const resetTempSettingsStruct = () => {
@@ -179,7 +179,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
size="small"
color="buttonActive"
:disabled="!settingsHaveChanged()"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="saveCameraSettings"
>
<v-icon start size="large"> mdi-content-save </v-icon>
@@ -191,7 +191,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
block
size="small"
color="error"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="() => (showDeleteCamera = true)"
>
<v-icon start size="large"> mdi-trash-can-outline </v-icon>

View File

@@ -103,7 +103,7 @@ const fpsTooLow = computed<boolean>(() => {
<v-btn
color="buttonPassive"
class="fill"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
:disabled="
useCameraSettingsStore().isDriverMode ||
useCameraSettingsStore().isCalibrationMode ||
@@ -116,7 +116,7 @@ const fpsTooLow = computed<boolean>(() => {
<v-btn
color="buttonPassive"
class="fill"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
:disabled="
useCameraSettingsStore().isDriverMode ||
useCameraSettingsStore().isCalibrationMode ||

View File

@@ -49,7 +49,7 @@ const confirmationText = ref("");
color="buttonActive"
style="float: right"
width="100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="onBackup"
>
<v-icon start class="open-icon" size="large"> mdi-export </v-icon>
@@ -65,7 +65,7 @@ const confirmationText = ref("");
? confirmationText.toLowerCase() !== expectedConfirmationText.toLowerCase()
: false
"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="
onConfirm();
confirmationText = '';

View File

@@ -407,14 +407,14 @@ const wrappedCameras = computed<SelectItem[]>(() =>
<v-card-actions class="pr-5 pt-10px pb-5">
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="cancelPipelineCreation"
>
Cancel
</v-btn>
<v-btn
color="buttonActive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
:disabled="checkPipelineName(newPipelineName) !== true"
@click="createNewPipeline"
>
@@ -441,7 +441,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
<v-card-actions class="pa-5 pt-0">
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
class="text-black"
@click="cancelChangePipelineType"
>
@@ -449,7 +449,7 @@ const wrappedCameras = computed<SelectItem[]>(() =>
</v-btn>
<v-btn
color="buttonActive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="confirmChangePipelineType"
>
Confirm

View File

@@ -122,14 +122,14 @@ const onBeforeTabUpdate = () => {
density="compact"
text="Camera is not connected. Please check your connection and try again."
icon="mdi-alert-circle-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
/>
</template>
<template v-else>
<v-col
v-for="(tabGroupData, tabGroupIndex) in tabGroups"
:key="tabGroupIndex"
:cols="tabGroupIndex == 1 && useCameraSettingsStore().currentPipelineSettings.doMultiTarget ? 7 : ''"
:cols="tabGroupIndex === 1 && useCameraSettingsStore().currentPipelineSettings.doMultiTarget ? 7 : ''"
:class="tabGroupIndex !== tabGroups.length - 1 && 'pr-3'"
@vue:before-update="onBeforeTabUpdate"
>

View File

@@ -35,7 +35,7 @@ const processingMode = computed<number>({
<v-btn
color="buttonPassive"
:disabled="!useCameraSettingsStore().hasConnected"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
class="w-50"
>
<template #prepend>
@@ -48,10 +48,10 @@ const processingMode = computed<number>({
:disabled="
!useCameraSettingsStore().hasConnected ||
!useCameraSettingsStore().isCurrentVideoFormatCalibrated ||
useCameraSettingsStore().currentPipelineSettings.pipelineType == PipelineType.ObjectDetection ||
useCameraSettingsStore().currentPipelineSettings.pipelineType == PipelineType.ColoredShape
useCameraSettingsStore().currentPipelineSettings.pipelineType === PipelineType.ObjectDetection ||
useCameraSettingsStore().currentPipelineSettings.pipelineType === PipelineType.ColoredShape
"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
class="w-50"
>
<template #prepend>
@@ -69,7 +69,7 @@ const processingMode = computed<number>({
<v-btn
color="buttonPassive"
class="fill w-50"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
>
<v-icon start class="mode-btn-icon" size="large">mdi-import</v-icon>
<span class="mode-btn-label">Raw</span>
@@ -77,7 +77,7 @@ const processingMode = computed<number>({
<v-btn
color="buttonPassive"
class="fill w-50"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
>
<v-icon start class="mode-btn-icon" size="large">mdi-export</v-icon>
<span class="mode-btn-label">Processed</span>

View File

@@ -168,7 +168,7 @@ const interactiveCols = computed(() =>
block
color="primary"
class="text-black"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Single)"
>
Take Point
@@ -179,7 +179,7 @@ const interactiveCols = computed(() =>
size="small"
block
color="error"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Clear)"
>
Clear All Points
@@ -196,7 +196,7 @@ const interactiveCols = computed(() =>
block
color="primary"
class="text-black"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.DualFirst)"
>
Take First Point
@@ -208,7 +208,7 @@ const interactiveCols = computed(() =>
block
color="primary"
class="text-black"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.DualSecond)"
>
Take Second Point
@@ -219,7 +219,7 @@ const interactiveCols = computed(() =>
size="small"
block
color="error"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Clear)"
>
Clear All Points

View File

@@ -207,7 +207,7 @@ const resetCurrentBuffer = () => {
color="buttonActive"
class="mb-4 mt-1"
style="width: min-content"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="resetCurrentBuffer"
>Reset Samples</v-btn
>

View File

@@ -191,7 +191,7 @@ const interactiveCols = computed(() =>
block
color="primary"
class="text-black"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="enableColorPicking(useCameraSettingsStore().currentPipelineSettings.hueInverted ? 2 : 3)"
>
<v-icon start size="large"> mdi-minus </v-icon>
@@ -204,7 +204,7 @@ const interactiveCols = computed(() =>
class="text-black"
size="small"
block
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="enableColorPicking(1)"
>
<v-icon start size="large"> mdi-plus-minus </v-icon>
@@ -217,7 +217,7 @@ const interactiveCols = computed(() =>
block
color="primary"
class="text-black"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="enableColorPicking(useCameraSettingsStore().currentPipelineSettings.hueInverted ? 3 : 2)"
>
<v-icon start size="large"> mdi-plus </v-icon>
@@ -232,7 +232,7 @@ const interactiveCols = computed(() =>
color="primary"
class="text-black"
size="small"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="disableColorPicking"
>
Cancel

View File

@@ -312,7 +312,7 @@ watch(metricsHistorySnapshot, () => {
<v-col>
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="useStateStore().showLogModal = true"
>
<v-icon start class="open-icon" size="large"> mdi-eye </v-icon>
@@ -322,7 +322,7 @@ watch(metricsHistorySnapshot, () => {
<v-col>
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="openExportLogsPrompt"
>
<v-icon start class="open-icon" size="large"> mdi-download </v-icon>
@@ -345,7 +345,7 @@ watch(metricsHistorySnapshot, () => {
<v-col>
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="() => (showImportDialog = true)"
>
<v-icon start class="open-icon" size="large"> mdi-import </v-icon>
@@ -355,7 +355,7 @@ watch(metricsHistorySnapshot, () => {
<v-col>
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="openExportSettingsPrompt"
>
<v-icon start class="open-icon" size="large"> mdi-export </v-icon>
@@ -369,7 +369,7 @@ watch(metricsHistorySnapshot, () => {
<v-col cols="12" sm="6"
><v-btn
color="buttonActive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="restartProgram"
>
<v-icon start class="open-icon" size="large"> mdi-restart </v-icon>
@@ -379,7 +379,7 @@ watch(metricsHistorySnapshot, () => {
<v-col cols="12" sm="6">
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="openOfflineUpdatePrompt"
>
<v-icon start class="open-icon" size="large"> mdi-upload </v-icon>
@@ -400,7 +400,7 @@ watch(metricsHistorySnapshot, () => {
<v-col cols="12" sm="6">
<v-btn
color="buttonActive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="restartDevice"
>
<v-icon start class="open-icon" size="large"> mdi-restart-alert </v-icon>
@@ -410,7 +410,7 @@ watch(metricsHistorySnapshot, () => {
<v-col cols="12" sm="6">
<v-btn
color="error"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="() => (showFactoryReset = true)"
>
<v-icon start class="open-icon" size="large"> mdi-trash-can-outline </v-icon>
@@ -453,7 +453,7 @@ watch(metricsHistorySnapshot, () => {
<v-card-text class="pt-0 flex-0-0 pb-2">
<div class="d-flex justify-space-between pb-3 pt-3">
<span>CPU Temperature</span>
<span>{{ cpuTempData.at(-1)?.value == -1 ? "--- " : Math.round(cpuTempData.at(-1)?.value ?? 0) }}°C</span>
<span>{{ cpuTempData.at(-1)?.value === -1 ? "--- " : Math.round(cpuTempData.at(-1)?.value ?? 0) }}°C</span>
</div>
<Suspense>
<!-- Allows us to import echarts when it's actually needed -->
@@ -470,7 +470,10 @@ watch(metricsHistorySnapshot, () => {
tooltip="Measured rate for this coprocessor ONLY. This FMS limit is for ALL robot communication. If you are experiencing bandwidth issues while under this limit, check other sources."
/>
<span
>{{ networkUsageData.at(-1)?.value == -1 ? "---" : networkUsageData.at(-1)?.value.toFixed(3) }} Mb/s</span
>{{
networkUsageData.at(-1)?.value === -1 ? "---" : networkUsageData.at(-1)?.value.toFixed(3)
}}
Mb/s</span
>
</div>
<Suspense>
@@ -529,7 +532,7 @@ watch(metricsHistorySnapshot, () => {
<v-btn
color="primary"
:disabled="importFile === null"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="handleSettingsImport"
>
<v-icon start class="open-icon"> mdi-import </v-icon>
@@ -552,7 +555,7 @@ watch(metricsHistorySnapshot, () => {
<v-btn
color="buttonActive"
width="100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="
offlineUpdateDialog.show = false;
handleOfflineUpdate(offlineUpdate.files[0]);

View File

@@ -185,7 +185,7 @@ watchEffect(() => {
</v-card-title>
<div class="pa-5 pt-0">
<v-card-title class="pl-0 pt-0 pb-10px">Networking</v-card-title>
<v-form ref="form" v-model="settingsValid">
<v-form v-model="settingsValid">
<pv-input
v-model="tempSettingsStruct.ntServerAddress"
label="Team Number/NetworkTables Server Address"
@@ -205,7 +205,7 @@ watchEffect(() => {
density="compact"
text="The NetworkTables Server Address is not set or is invalid. NetworkTables is unable to connect."
icon="mdi-alert-circle-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
/>
<pv-radio
v-show="!useSettingsStore().network.networkingDisabled"
@@ -279,7 +279,7 @@ watchEffect(() => {
density="compact"
text="Cannot detect any wired connections! Send program logs to the developers for help."
icon="mdi-alert-circle-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
/>
<pv-switch
v-model="tempSettingsStruct.runNTServer"
@@ -293,7 +293,7 @@ watchEffect(() => {
density="compact"
text="This mode is intended for debugging and should be off for proper usage. PhotonLib will NOT work!"
icon="mdi-information-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
/>
<v-card-title class="pl-0 pt-3 pb-10px">Miscellaneous</v-card-title>
<pv-switch
@@ -308,13 +308,13 @@ watchEffect(() => {
density="compact"
text="This mode is intended for debugging and may reduce performance; it should be off for field use."
icon="mdi-information-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
/>
</v-form>
<v-btn
color="primary"
class="mt-3"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
style="color: black; width: 100%"
:disabled="!settingsValid || !settingsHaveChanged()"
@click="saveGeneralSettings"
@@ -377,7 +377,7 @@ watchEffect(() => {
</v-card-text>
<v-card-actions class="pa-5 pt-0">
<v-btn
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
color="buttonPassive"
class="text-black"
@click="showThemeConfig = false"
@@ -385,7 +385,7 @@ watchEffect(() => {
Close
</v-btn>
<v-btn
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
color="buttonActive"
class="text-black"
@click="

View File

@@ -53,9 +53,9 @@ const getOptions = (data: ChartData[] = []) => {
return `${tooltip}</div>`;
},
backgroundColor: theme.themes.value[theme.global.name.value].colors.background,
backgroundColor: theme.global.current.value.colors.background,
textStyle: {
color: theme.themes.value[theme.global.name.value].colors.onBackground
color: theme.global.current.value.colors.onBackground
},
axisPointer: {
animation: false
@@ -149,10 +149,9 @@ const getSeries = (data: ChartData[] = []) => {
: null,
lineStyle: {
width: 1.5,
color:
theme.global.name.value === "LightTheme"
? theme.themes.value[theme.global.name.value].colors.primary
: `rgb(${color.r}, ${color.g}, ${color.b})`
color: theme.global.current.value.dark
? `rgb(${color.r}, ${color.g}, ${color.b})`
: theme.global.current.value.colors.primary
},
areaStyle: {
color: {
@@ -164,17 +163,15 @@ const getSeries = (data: ChartData[] = []) => {
colorStops: [
{
offset: 0,
color:
theme.global.name.value === "LightTheme"
? `${theme.themes.value[theme.global.name.value].colors.primary}40`
: `rgba(${color.r}, ${color.g}, ${color.b}, 0.15)`
color: theme.global.current.value.dark
? `rgba(${color.r}, ${color.g}, ${color.b}, 0.15)`
: `${theme.global.current.value.colors.primary}40`
},
{
offset: 1,
color:
theme.global.name.value === "LightTheme"
? `${theme.themes.value[theme.global.name.value].colors.primary}40`
: `rgba(${color.r}, ${color.g}, ${color.b}, 0.15)`
color: theme.global.current.value.dark
? `rgba(${color.r}, ${color.g}, ${color.b}, 0.15)`
: `${theme.global.current.value.colors.primary}40`
}
]
}

View File

@@ -166,7 +166,7 @@ const handleBulkImport = async () => {
<v-btn
color="buttonActive"
class="justify-center"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="() => (showImportDialog = true)"
>
<v-icon start class="open-icon"> mdi-import </v-icon>
@@ -245,7 +245,7 @@ const handleBulkImport = async () => {
importHeight === null ||
importVersion === null
"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="handleImport()"
>
<v-icon start class="open-icon" size="large"> mdi-import </v-icon>
@@ -260,7 +260,7 @@ const handleBulkImport = async () => {
<v-btn
color="buttonActive"
class="justify-center"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="() => (showBulkImportDialog = true)"
>
<v-icon start class="open-icon"> mdi-import </v-icon>
@@ -278,7 +278,7 @@ const handleBulkImport = async () => {
color="buttonActive"
width="100%"
:disabled="importFile === null"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="handleBulkImport()"
>
<v-icon start class="open-icon" size="large"> mdi-import </v-icon>
@@ -292,7 +292,7 @@ const handleBulkImport = async () => {
<v-col cols="12" sm="6">
<v-btn
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="openExportPrompt"
>
<v-icon start class="open-icon"> mdi-export </v-icon>
@@ -309,7 +309,7 @@ const handleBulkImport = async () => {
<v-col cols="12" sm="6">
<v-btn
color="error"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="() => (showNukeDialog = true)"
>
<v-icon left class="open-icon"> mdi-trash </v-icon>
@@ -339,7 +339,7 @@ const handleBulkImport = async () => {
small
color="error"
title="Delete Model"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="() => (confirmDeleteDialog = { show: true, model })"
>
<v-icon size="large">mdi-trash-can-outline</v-icon>
@@ -351,7 +351,7 @@ const handleBulkImport = async () => {
small
color="buttonActive"
title="Rename Model"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="() => (showRenameDialog = { show: true, model, newName: '' })"
>
<v-icon size="large">mdi-pencil</v-icon>
@@ -362,7 +362,7 @@ const handleBulkImport = async () => {
icon
small
color="buttonPassive"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="() => (showInfo = { show: true, model })"
>
<v-icon size="large">mdi-information</v-icon>
@@ -391,13 +391,13 @@ const handleBulkImport = async () => {
</div>
<v-card-actions class="pt-5 pb-0 pr-0" style="justify-content: flex-end">
<v-btn
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
color="error"
@click="showRenameDialog.show = false"
>Cancel</v-btn
>
<v-btn
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
color="buttonActive"
@click="renameModel(showRenameDialog.model, showRenameDialog.newName)"
>Rename</v-btn
@@ -413,7 +413,7 @@ const handleBulkImport = async () => {
<v-btn
color="buttonPassive"
width="100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="openExportIndividualModelPrompt"
>
<v-icon left class="open-icon" size="large"> mdi-export </v-icon>
@@ -446,8 +446,8 @@ const handleBulkImport = async () => {
:on-backup="openExportPrompt"
:on-confirm="nukeModels"
title="Delete and Reset All Object Detection Models"
:description="'This will delete ALL object detection models and re-extract the default object detection models. This action cannot be undone.'"
:expected-confirmation-text="'Delete Models'"
description="This will delete ALL object detection models and re-extract the default object detection models. This action cannot be undone."
expected-confirmation-text="Delete Models"
delete-text="Delete all models"
/>
</v-card>

View File

@@ -2,7 +2,7 @@ import { type ThemeInstance } from "vuetify";
import { LightTheme, DarkTheme } from "@/plugins/vuetify";
export const resetTheme = (theme: ThemeInstance) => {
const themeType = theme.global.name.value === "LightTheme" ? "light" : "dark";
const themeType = theme.global.current.value.dark ? "dark" : "light";
localStorage.removeItem(`${themeType}-background`);
localStorage.removeItem(`${themeType}-primary`);
localStorage.removeItem(`${themeType}-secondary`);
@@ -12,13 +12,13 @@ export const resetTheme = (theme: ThemeInstance) => {
};
export const getThemeColor = (theme: ThemeInstance, color: string): string => {
const themeType = theme.global.name.value === "LightTheme" ? "light" : "dark";
const defaultTheme = theme.global.name.value === "LightTheme" ? LightTheme : DarkTheme;
const themeType = theme.global.current.value.dark ? "dark" : "light";
const defaultTheme = theme.global.current.value.dark ? DarkTheme : LightTheme;
return localStorage.getItem(`${themeType}-${color}`) ?? defaultTheme.colors![color]!;
};
export const setThemeColor = (theme: ThemeInstance, color: string, value: string | null) => {
const themeType = theme.global.name.value === "LightTheme" ? "light" : "dark";
const themeType = theme.global.current.value.dark ? "dark" : "light";
if (value) localStorage.setItem(`${themeType}-${color}`, value);
else localStorage.removeItem(`${themeType}-${color}`);
@@ -38,7 +38,7 @@ export const restoreThemeConfig = (theme: ThemeInstance) => {
if (storedTheme) theme.global.name.value = storedTheme;
// Restore custom theme colors
const themeType = theme.global.name.value === "LightTheme" ? "light" : "dark";
const themeType = theme.global.current.value.dark ? "dark" : "light";
const defaultTheme = theme.global.name.value === "LightTheme" ? LightTheme : DarkTheme;
const customBackground = localStorage.getItem(`${themeType}-background`);
@@ -47,7 +47,7 @@ export const restoreThemeConfig = (theme: ThemeInstance) => {
const customSurface = localStorage.getItem(`${themeType}-surface`);
theme.themes.value[theme.global.name.value].colors.background = customBackground ?? defaultTheme.colors!.background!;
theme.themes.value[theme.global.name.value].colors.sidebar = theme.themes.value[theme.global.name.value].dark
theme.themes.value[theme.global.name.value].colors.sidebar = theme.global.current.value.dark
? (customBackground ?? defaultTheme.colors!.sidebar!)
: (customSurface ?? defaultTheme.colors!.sidebar!);

View File

@@ -0,0 +1,452 @@
import IDLEGIF from "@/assets/images/idle.gif";
import GROWGIF from "@/assets/images/grow.gif";
import BLINKGIF from "@/assets/images/blink.gif";
import WAVEGIF from "@/assets/images/wave.gif";
import SPEAKGIF from "@/assets/images/speak.gif";
import SHRINKGIF from "@/assets/images/shrink.gif";
import POINTGIF from "@/assets/images/point.gif";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
const ANIMATIONS = {
idle: IDLEGIF,
grow: GROWGIF,
blink: BLINKGIF,
wave: WAVEGIF,
speak: SPEAKGIF,
shrink: SHRINKGIF,
point: POINTGIF
};
// Extended animation sequence
const SEQUENCE = [
{ animation: "grow", loops: 1 },
{ animation: "idle", loops: 2 },
{ animation: "blink", loops: 1 },
{ animation: "idle", loops: 1 },
{ animation: "speak", loops: 1 },
{ animation: "idle", loops: 1 },
{ animation: "point", loops: 1 },
{ animation: "idle", loops: 1 },
{ animation: "wave", loops: 1 },
{ animation: "idle", loops: 1 }
];
let quarkyImage;
let quarkyContainer;
let speechBubble;
let currentSequenceIndex = 0;
let currentLoopCount = 0;
let isMovingDemo = false;
let hasPlayedGrow = false;
// Speech bubble text (configurable)
let quarkySpeechText = "Hello from Quarky!";
// Turbo-encabulator style nonsense phrases
const quarkyPhrases = [
"Reverse phase oscillation detected!",
"Initializing hyperflux capacitor...",
"Reticulating splines in progress.",
"Quantum entanglement buffer overflowzomg",
"Did you remember the turbo-encabulator?",
"Engaging magnetic flux inverter.",
"Calibrating photon resonance field.",
"Deploying recursive feedback loop.",
"wow.",
"I applaud your pseudo-random bitstream.",
"Rebooting quantum foam stabilizer.",
"Analyzing subspace harmonics.",
"Transmitting encrypted flux packets.",
"Verifying entropic phase alignment.",
"Reconfiguring nano-particle array.",
"pew pew. pew pew.",
"I can't parse the synthetic logic matrix.",
"Don't forget to make the holographic interface.",
"You should generate more stochastic resonance.",
"Greetings",
"You look like you need some help!",
"Set this slider to 25",
"Set this slider to 67. HAHA 67!!!!",
"That's a horrible choice!",
"Fun is a core value! Is that a fun choice?",
"If your grandma saw that choice, would she be proud?",
"Chute Door?",
"Yes, Chute Door!",
"That's a bold strategy, Cotton.",
"asdflkjaslkdflklnf2222",
"00110101? That's just gibberish!",
"Three is my favorite number too!",
"Robots should not quit, but yours did!",
"Dont forget to disable auto-exposure! Or enable it. I'm not sure. ",
"Have you glued your lenses to keep them in focus?",
"Dont put spaces in your camera names — it makes the robot very sad",
"Upgrade to Photon Pro for gtsam support 👍",
"Did you forget to take off the lense covers? Its dark in here…"
];
// State-specific humorous phrases
const cameraNeedsSetupPhrases = [
"These cameras are just standing there... menacingly",
"Are your cameras plugged in? Trick question -- they aren't!",
"Have you hot-glued your USB cameras?"
];
const backendNotConnectedPhrases = [
"Um, is this thing even on?",
"Anyone home? Bulldozer? Bulldozer?",
"Have you tried turning the NI™ RoboRIO™ off and on again?"
];
const ntDisconnectedPhrases = [
"NetworkTables? More like Network'(; DROP TABLE websockets;--",
"Robots shouldn't quit, but I sure can't talk to yours!",
"Are you an OM5P? Because I can't talk to you over the LAN!",
"I'm a sentient subatomic particle, not a networking engineer."
];
/**
* Get list of applicable phrase categories based on current UI state
*/
function getApplicablePhraseLists() {
const cameraStore = useCameraSettingsStore();
const stateStore = useStateStore();
// Build list of applicable phrase categories
const applicableLists = [quarkyPhrases];
// Add state-specific categories (additive, not replacing)
if (cameraStore?.needsCameraConfiguration) {
applicableLists.push(cameraNeedsSetupPhrases);
}
if (!stateStore?.backendConnected) {
applicableLists.push(backendNotConnectedPhrases);
}
if (!stateStore?.ntConnectionStatus?.connected) {
applicableLists.push(ntDisconnectedPhrases);
}
return applicableLists;
}
/**
* Pick a random phrase from applicable categories
*/
function pickRandomPhrase() {
const applicableLists = getApplicablePhraseLists();
const randomList = applicableLists[Math.floor(Math.random() * applicableLists.length)];
return randomList[Math.floor(Math.random() * randomList.length)];
}
/**
* Get the duration of an animation in milliseconds
*/
function getAnimationDuration(animation) {
if (!animation) return 500; // Default 0.5s for empty state
// Animation durations (in seconds) based on quarky_generator.py
const durations = {
idle: 0.5,
grow: 2.0,
blink: 0.3,
wave: 1.8,
speak: 2.0,
shrink: 2.0,
point: 1.5
};
return (durations[animation] || 1.0) * 1000; // Convert to ms
}
/**
* Play an animation
*/
function playAnimation(animation) {
if (!animation) {
quarkyImage.src = "";
quarkyImage.style.display = "none";
return;
}
quarkyImage.style.display = "block";
quarkyImage.src = ANIMATIONS[animation];
if (animation === "speak") {
// Pick random phrase from applicable categories
quarkySpeechText = pickRandomPhrase();
speechBubble.textContent = quarkySpeechText;
speechBubble.style.display = "block";
speechBubble.style.opacity = 1;
setTimeout(() => {
speechBubble.style.opacity = 0;
setTimeout(() => {
speechBubble.style.display = "none";
}, 700);
}, 3000);
}
}
// Mini Quarky management
let miniQuarkies = [];
const MAX_MINI_QUARKIES = 20;
const MINI_QUARKY_SIZE = 120;
const MINI_QUARKY_Z_INDEX = 999;
const MINI_QUARKY_VELOCITY_MULTIPLIER = 12;
const MINI_QUARKY_VELOCITY_CENTER = 0.5;
const DIRECTION_CHANGE_PROBABILITY = 0.02;
const DIRECTION_CHANGE_VELOCITY_MULTIPLIER = 4;
const MINI_QUARKY_ANIMATION_INTERVAL_MS = 50;
const MINI_QUARKY_SPAWN_BASE_DELAY_MS = 4000;
const MINI_QUARKY_SPAWN_DELAY_RANGE_MS = 8000;
// Random movement every few cycles
let mouseX = window.innerWidth / 2;
let mouseY = window.innerHeight / 2;
let mouseMoving = false;
let mouseMoveTimeout = null;
/**
* Clamp Quarky's position to stay within the viewport, accounting for the sidebar
*/
function clampQuarkyPosition(x, y) {
const rect = quarkyContainer.getBoundingClientRect();
// Get sidebar width (account for both expanded and compact modes)
const sidebar = document.querySelector(".v-navigation-drawer");
const sidebarWidth = sidebar ? sidebar.offsetWidth : 0;
// Clamp to viewport, starting after the sidebar
const clampedX = Math.max(sidebarWidth, Math.min(x, window.innerWidth - rect.width));
const clampedY = Math.max(0, Math.min(y, window.innerHeight - rect.height));
return { x: clampedX, y: clampedY };
}
function mouseMoveHandler(e) {
mouseX = e.clientX + window.scrollX;
mouseY = e.clientY + window.scrollY;
mouseMoving = true;
clearTimeout(mouseMoveTimeout);
// After 1.5s of no movement, return Quarky to home and resume nonsense
mouseMoveTimeout = setTimeout(() => {
mouseMoving = false;
quarkyContainer.style.left = "calc(100vw - 550px)";
quarkyContainer.style.top = "calc(100vh - 550px)";
isMovingDemo = false;
playNextAnimation();
}, 1500);
// Actively track mouse: update Quarky's position every mouse move (clamped to viewport)
const clamped = clampQuarkyPosition(mouseX, mouseY);
quarkyContainer.style.left = `${clamped.x}px`;
quarkyContainer.style.top = `${clamped.y}px`;
// Immediately trigger Quarky to point at cursor
if (!isMovingDemo) {
isMovingDemo = true;
playAnimation("point");
setTimeout(() => {
playAnimation("idle");
}, getAnimationDuration("point"));
}
}
/**
* Advance to the next animation in the sequence
*/
function playNextAnimation() {
if (isMovingDemo) return;
let currentStep = SEQUENCE[currentSequenceIndex];
// On loop, skip grow after first time
if (hasPlayedGrow && currentSequenceIndex === 0 && currentStep.animation === "grow") {
currentSequenceIndex = 1;
currentStep = SEQUENCE[currentSequenceIndex];
}
// If mouse is moving, don't do normal cycle
if (mouseMoving) {
// Quarky will point at cursor via mousemove handler
return;
}
// Show speech bubble before speak
if (currentStep.animation === "speak") {
quarkySpeechText = pickRandomPhrase();
speechBubble.textContent = quarkySpeechText;
speechBubble.style.display = "block";
speechBubble.style.opacity = 1;
setTimeout(() => {
speechBubble.style.opacity = 0;
setTimeout(() => {
speechBubble.style.display = "none";
}, 700);
}, 3000);
}
// Fade out speech bubble after speak (during idle)
if (SEQUENCE[currentSequenceIndex - 1]?.animation === "speak" && currentStep.animation === "idle") {
// Speech bubble already has its own timeout from above
}
// Always return to corner when idle
quarkyContainer.style.left = "calc(100vw - 550px)";
quarkyContainer.style.top = "calc(100vh - 550px)";
// Play the animation
playAnimation(currentStep.animation);
// Calculate duration and schedule next animation
const duration = getAnimationDuration(currentStep.animation);
setTimeout(() => {
currentLoopCount++;
// Check if we've completed all loops for this step
if (currentLoopCount >= currentStep.loops) {
// Move to next step
currentLoopCount = 0;
currentSequenceIndex++;
// Loop back to start if we've completed the sequence
if (currentSequenceIndex >= SEQUENCE.length) {
currentSequenceIndex = 0;
hasPlayedGrow = true;
}
}
// Play the next animation
playNextAnimation();
}, duration);
}
function clickToPoint(e) {
// Ignore clicks on the button
if (e.target.id === "moveDemoBtn") return;
isMovingDemo = true;
const clickX = e.clientX + window.scrollX;
const clickY = e.clientY + window.scrollY;
const clamped = clampQuarkyPosition(clickX, clickY);
quarkyContainer.style.left = `${clamped.x}px`;
quarkyContainer.style.top = `${clamped.y}px`;
setTimeout(() => {
playAnimation("point");
setTimeout(() => {
playAnimation("idle");
quarkyContainer.style.left = "calc(100vw - 550px)";
quarkyContainer.style.top = "calc(100vh - 550px)";
setTimeout(() => {
isMovingDemo = false;
playNextAnimation();
}, 1000);
}, getAnimationDuration("point"));
}, 1000);
}
/**
* Spawn a mini Quarky that moves randomly around the screen
*/
function spawnMiniQuarky() {
console.log("SPAWNING A QUARKY");
// If at max, don't spawn
if (miniQuarkies.length >= MAX_MINI_QUARKIES) {
return;
}
const miniContainer = document.createElement("div");
miniContainer.style.position = "fixed";
miniContainer.style.width = `${MINI_QUARKY_SIZE}px`;
miniContainer.style.height = `${MINI_QUARKY_SIZE}px`;
miniContainer.style.pointerEvents = "none";
miniContainer.style.zIndex = MINI_QUARKY_Z_INDEX.toString();
// Spawn from main quarky's actual position
const rect = quarkyContainer.getBoundingClientRect();
const startX = rect.left + window.scrollX + rect.width / 2;
const startY = rect.top + window.scrollY + rect.height / 2;
miniContainer.style.left = startX + "px";
miniContainer.style.top = startY + "px";
const miniImage = document.createElement("img");
miniImage.src = ANIMATIONS["idle"];
miniImage.style.width = "100%";
miniImage.style.height = "100%";
miniImage.style.objectFit = "contain";
miniContainer.appendChild(miniImage);
document.body.appendChild(miniContainer);
const miniQuarky = {
container: miniContainer,
image: miniImage,
x: startX,
y: startY,
vx: (Math.random() - MINI_QUARKY_VELOCITY_CENTER) * MINI_QUARKY_VELOCITY_MULTIPLIER,
vy: (Math.random() - MINI_QUARKY_VELOCITY_CENTER) * MINI_QUARKY_VELOCITY_MULTIPLIER,
animationInterval: null
};
miniQuarkies.push(miniQuarky);
// Start movement loop
miniQuarky.animationInterval = setInterval(() => {
miniQuarky.x += miniQuarky.vx;
miniQuarky.y += miniQuarky.vy;
// Bounce off edges
if (miniQuarky.x <= 0 || miniQuarky.x >= window.innerWidth - MINI_QUARKY_SIZE) {
miniQuarky.vx *= -1;
miniQuarky.x = Math.max(0, Math.min(miniQuarky.x, window.innerWidth - MINI_QUARKY_SIZE));
}
if (miniQuarky.y <= 0 || miniQuarky.y >= window.innerHeight - MINI_QUARKY_SIZE) {
miniQuarky.vy *= -1;
miniQuarky.y = Math.max(0, Math.min(miniQuarky.y, window.innerHeight - MINI_QUARKY_SIZE));
}
// Occasionally change direction randomly
if (Math.random() < DIRECTION_CHANGE_PROBABILITY) {
miniQuarky.vx = (Math.random() - MINI_QUARKY_VELOCITY_CENTER) * DIRECTION_CHANGE_VELOCITY_MULTIPLIER;
miniQuarky.vy = (Math.random() - MINI_QUARKY_VELOCITY_CENTER) * DIRECTION_CHANGE_VELOCITY_MULTIPLIER;
}
miniContainer.style.left = miniQuarky.x + "px";
miniContainer.style.top = miniQuarky.y + "px";
}, MINI_QUARKY_ANIMATION_INTERVAL_MS);
}
/**
* Clean up all mini Quarkies
*/
function cleanupMiniQuarkies() {
// eslint-disable-line @typescript-eslint/no-unused-vars
miniQuarkies.forEach((mini) => {
clearInterval(mini.animationInterval);
mini.container.remove();
});
miniQuarkies = [];
}
export default function setup() {
quarkyImage = document.getElementById("quarkyImage");
quarkyContainer = document.getElementById("quarkyContainer");
speechBubble = document.getElementById("quarkySpeechBubble");
// Start the animation sequence
playNextAnimation();
//Install mouse move handler
window.addEventListener("mousemove", mouseMoveHandler);
// Click-to-point feature installation
window.addEventListener("click", (e) => {
clickToPoint(e);
});
// Spawn mini Quarkies on a random timer
function scheduleNextMiniQuarkySpawn() {
const delayMs = MINI_QUARKY_SPAWN_BASE_DELAY_MS + Math.random() * MINI_QUARKY_SPAWN_DELAY_RANGE_MS;
setTimeout(() => {
spawnMiniQuarky();
scheduleNextMiniQuarkySpawn();
}, delayMs);
}
scheduleNextMiniQuarkySpawn();
}

View File

@@ -6,6 +6,8 @@ import router from "@/router";
import vuetify from "@/plugins/vuetify";
import axios from "axios";
import setup from "@/lib/quarky.js";
type PhotonClientRuntimeMode = "production" | "development" | "local-network-development";
const runtimeMode: PhotonClientRuntimeMode = process.env.NODE_ENV as PhotonClientRuntimeMode;
@@ -45,3 +47,4 @@ app.use(pinia);
app.use(vuetify);
app.use(router);
app.mount("#app");
setup();

View File

@@ -71,10 +71,10 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
return this.currentCameraSettings.currentPipelineIndex === WebsocketPipelineType.DriverMode;
},
isCalibrationMode(): boolean {
return this.currentCameraSettings.currentPipelineIndex == WebsocketPipelineType.Calib3d;
return this.currentCameraSettings.currentPipelineIndex === WebsocketPipelineType.Calib3d;
},
isFocusMode(): boolean {
return this.currentCameraSettings.currentPipelineIndex == WebsocketPipelineType.FocusCamera;
return this.currentCameraSettings.currentPipelineIndex === WebsocketPipelineType.FocusCamera;
},
isCSICamera(): boolean {
return this.currentCameraSettings.isCSICamera;

View File

@@ -232,7 +232,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
<v-btn
color="buttonPassive"
style="width: 100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="
setCameraView(
module.matchedCameraInfo,
@@ -248,7 +248,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
class="text-black"
color="buttonActive"
style="width: 100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
:loading="deactivatingModule"
@click="deactivateModule(module.uniqueName)"
>
@@ -261,7 +261,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
color="error"
style="width: 100%"
:loading="module.uniqueName === deletingCamera"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="
() =>
(confirmDeleteDialog = {
@@ -326,7 +326,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
<v-btn
color="buttonPassive"
style="width: 100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="
setCameraView(
module.matchedCameraInfo,
@@ -342,7 +342,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
class="text-black"
color="buttonActive"
style="width: 100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
:loading="activatingModule"
@click="activateModule(module.uniqueName)"
>
@@ -355,7 +355,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
color="error"
style="width: 100%"
:loading="module.uniqueName === deletingCamera"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="
() =>
(confirmDeleteDialog = {
@@ -393,7 +393,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
<v-btn
color="buttonPassive"
style="width: 100%"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="setCameraView(camera, false)"
>
<span>Details</span>
@@ -405,7 +405,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
color="buttonActive"
style="width: 100%"
:loading="assigningCamera"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'outlined'"
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
@click="assignCamera(camera)"
>
Activate
@@ -457,7 +457,7 @@ const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
density="compact"
text="A different camera may have been connected to this device! Compare the following information carefully."
icon="mdi-information-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
/>
<PvCameraMatchCard :saved="viewingCamera[0]" :current="getMatchedDevice(viewingCamera[0])" />
</v-card-text>

View File

@@ -92,7 +92,7 @@ const showCameraSetupDialog = ref(useCameraSettingsStore().needsCameraConfigurat
color="error"
density="compact"
icon="mdi-alert-circle-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
>
<span>
Arducam camera detected! Please configure the camera model in the <a href="#/cameras">Camera tab</a>!
@@ -104,7 +104,7 @@ const showCameraSetupDialog = ref(useCameraSettingsStore().needsCameraConfigurat
color="error"
density="compact"
icon="mdi-alert-circle-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
>
<span>
Conflicting hostname detected! Please change the hostname in the <a href="#/settings">Settings tab</a>!
@@ -116,7 +116,7 @@ const showCameraSetupDialog = ref(useCameraSettingsStore().needsCameraConfigurat
color="error"
density="compact"
icon="mdi-alert-circle-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
>
<span
>One or more cameras have an FPS limit set! This may cause performance issues. Check your logs for more
@@ -129,7 +129,7 @@ const showCameraSetupDialog = ref(useCameraSettingsStore().needsCameraConfigurat
color="error"
density="compact"
icon="mdi-alert-circle-outline"
:variant="theme.global.name.value === 'LightTheme' ? 'elevated' : 'tonal'"
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
>
<span
>Conflicting camera name(s) detected! Please change the name(s) of

View File

@@ -25,7 +25,7 @@ export default defineConfig({
}
},
build: {
rollupOptions: {
rolldownOptions: {
external: ["html2canvas", "dompurify", "canvg"]
},
sourcemap: true

View File

@@ -17,8 +17,6 @@
package org.photonvision.vision.camera;
import com.fasterxml.jackson.annotation.JsonAlias;
public enum CameraQuirk {
/** Camera settable for controllable image gain */
Gain,
@@ -29,7 +27,6 @@ public enum CameraQuirk {
/** Cap at 100FPS for high-bandwidth cameras */
FPSCap100,
/** Separate red/blue gain controls available */
@JsonAlias("AWBGain") // remove after https://github.com/PhotonVision/photonvision/issues/1488
AwbRedBlueGain,
/** Will not work with photonvision - Logitech C270 at least */
CompletelyBroken,
@@ -43,11 +40,8 @@ public enum CameraQuirk {
* Camera is an arducam USB ov9281 which has a funky exposure issue where it is defined in v4l as
* 1-5000 instead of 1-75
*/
@JsonAlias("ArduOV9281") // remove after https://github.com/PhotonVision/photonvision/issues/1488
ArduOV9281Controls,
/** Dummy quirk to tell OV2311 from OV9281 */
// from 2024.3.1
@JsonAlias("ArduOV2311") // remove after https://github.com/PhotonVision/photonvision/issues/1488
ArduOV2311Controls,
ArduOV9782Controls,
/**

View File

@@ -170,10 +170,12 @@ public class GenericUSBCameraSettables extends VisionSourceSettables {
@Override
public void setAutoExposure(boolean cameraAutoExposure) {
if ((configuration.cameraQuirks.hasQuirk(CameraQuirk.ArduOV9281Controls)
|| configuration.cameraQuirks.hasQuirk(CameraQuirk.ArduOV9782Controls))
|| configuration.cameraQuirks.hasQuirk(CameraQuirk.ArduOV9782Controls)
|| configuration.cameraQuirks.hasQuirk(CameraQuirk.ArduOV2311Controls))
&& !cameraAutoExposure) {
// OV9281 and OV9782 on Linux seems to sometimes ignore our exposure requests on first boot if
// we're in manual mode. Poking the camera into and out of auto exposure seems to fix it.
// OV9281, OV9782, and OV2311 on Linux seems to sometimes ignore our exposure requests on
// first boot if we're in manual mode. Poking the camera into and out of auto exposure seems
// to fix it.
try {
setAutoExposureImpl(false);
Thread.sleep(2000);

View File

@@ -19,8 +19,8 @@ package org.photonvision.vision.objects;
import java.awt.Color;
import java.lang.ref.Cleaner;
import java.lang.ref.Cleaner.Cleanable;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import org.opencv.core.Mat;
import org.opencv.core.Size;
import org.photonvision.common.logging.LogGroup;
@@ -36,8 +36,11 @@ public class RknnObjectDetector implements ObjectDetector {
/** Cleaner instance to release the detector when it goes out of scope */
private final Cleaner cleaner = Cleaner.create();
/** Atomic boolean to ensure that the native object can only be released once. */
private AtomicBoolean released = new AtomicBoolean(false);
private final Cleanable cleanable;
private static Runnable cleanupAction(long ptr) {
return () -> RknnJNI.destroy(ptr);
}
/** Pointer to the native object */
private final long objPointer;
@@ -80,7 +83,7 @@ public class RknnObjectDetector implements ObjectDetector {
logger.debug("Created detector for model " + model.modelFile.getName());
// Register the cleaner to release the detector when it goes out of scope
cleaner.register(this, this::release);
cleanable = cleaner.register(this, cleanupAction(objPointer));
}
/**
@@ -137,17 +140,6 @@ public class RknnObjectDetector implements ObjectDetector {
/** Thread-safe method to release the detector. */
@Override
public void release() {
// Checks if the atomic is 'false', and if so, sets it to 'true'
if (released.compareAndSet(false, true)) {
if (objPointer <= 0) {
logger.error(
"Detector is not initialized, and so it can't be released! Model: "
+ model.modelFile.getName());
return;
}
RknnJNI.destroy(objPointer);
logger.debug("Released detector for model " + model.modelFile.getName());
}
cleanable.clean();
}
}

View File

@@ -19,8 +19,8 @@ package org.photonvision.vision.objects;
import java.awt.Color;
import java.lang.ref.Cleaner;
import java.lang.ref.Cleaner.Cleanable;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import org.opencv.core.Mat;
import org.opencv.core.Size;
import org.photonvision.common.logging.LogGroup;
@@ -36,8 +36,11 @@ public class RubikObjectDetector implements ObjectDetector {
/** Cleaner instance to release the detector when it goes out of scope */
private final Cleaner cleaner = Cleaner.create();
/** Atomic boolean to ensure that the native object can only be released once. */
private AtomicBoolean released = new AtomicBoolean(false);
private final Cleanable cleanable;
private static Runnable cleanupAction(long ptr) {
return () -> RubikJNI.destroy(ptr);
}
/** Pointer to the native object */
private final long ptr;
@@ -88,7 +91,7 @@ public class RubikObjectDetector implements ObjectDetector {
logger.debug("Created detector for model " + model.modelFile.getName());
// Register the cleaner to release the detector when it goes out of scope
cleaner.register(this, this::release);
cleanable = cleaner.register(this, cleanupAction(ptr));
}
/**
@@ -146,17 +149,7 @@ public class RubikObjectDetector implements ObjectDetector {
/** Thread-safe method to release the detector. */
@Override
public void release() {
// Checks if the atomic is 'false', and if so, sets it to 'true'
if (released.compareAndSet(false, true)) {
if (!isValid()) {
logger.error(
"Detector is not initialized, and so it can't be released! Model: "
+ model.modelFile.getName());
return;
}
RubikJNI.destroy(ptr);
logger.debug("Released detector for model " + model.modelFile.getName());
}
cleanable.clean();
}
private boolean isValid() {

View File

@@ -150,7 +150,7 @@ public class OutputStreamPipeline {
if (!(settings instanceof AprilTagPipelineSettings)
&& !(settings instanceof ArucoPipelineSettings)
&& !(settings instanceof Calibration3dPipelineSettings)) {
// If we're processing anything other than Apriltags..
// If we're processing anything other than AprilTags or Aruco targets
var draw2dCrosshairResultOnOutput = draw2dCrosshairPipe.run(Pair.of(outMat, targetsToDraw));
sumPipeNanosElapsed += pipeProfileNanos[4] = draw2dCrosshairResultOnOutput.nanosElapsed;
@@ -203,7 +203,7 @@ public class OutputStreamPipeline {
}
} else if (settings instanceof ArucoPipelineSettings) {
if (settings.solvePNPEnabled) {
// Draw 3d Apriltag markers (camera is calibrated and running in 3d mode)
// Draw 3d aruco markers (camera is calibrated and running in 3d mode)
pipeProfileNanos[5] = 0;
pipeProfileNanos[6] = 0;
@@ -213,7 +213,7 @@ public class OutputStreamPipeline {
pipeProfileNanos[8] = 0;
} else {
// Draw 2d apriltag markers
// Draw 2d aruco markers
var draw2dTargetsOnInput = draw2dArucoPipe.run(Pair.of(outMat, targetsToDraw));
sumPipeNanosElapsed += pipeProfileNanos[5] = draw2dTargetsOnInput.nanosElapsed;

View File

@@ -368,6 +368,15 @@ class PhotonCameraSim:
noisyTargetCorners,
)
# Compute object detection confidence if this is an obj det target
classId = tgt.objDetClassId
conf = tgt.objDetConf
if classId >= 0 and conf < 0:
# Simulate confidence using sqrt-scaled area for a more realistic
# curve. Raw areaPercent/100 is tiny for most targets; sqrt scaling
# gives reasonable values even for small-but-visible objects.
conf = max(0.0, min(1.0, math.sqrt(areaPercent / 100.0) * 2.0))
smallVec: list[TargetCorner] = []
for corner in minAreaRectPts:
smallVec.append(TargetCorner(corner[0], corner[1]))
@@ -381,6 +390,8 @@ class PhotonCameraSim:
area=areaPercent,
skew=math.degrees(centerRot.X()),
fiducialId=tgt.fiducialId,
objDetectId=classId,
objDetectConf=conf,
detectedCorners=cornersFloat,
minAreaRectCorners=smallVec,
bestCameraToTarget=pnpSim.best if pnpSim else Transform3d(),

View File

@@ -1,4 +1,5 @@
import math
from typing import overload
from wpimath.geometry import Pose3d, Translation3d
@@ -8,19 +9,78 @@ from ..estimation.targetModel import TargetModel
class VisionTargetSim:
"""Describes a vision target located somewhere on the field that your vision system can detect."""
def __init__(self, pose: Pose3d, model: TargetModel, id: int = -1):
"""Describes a fiducial tag located somewhere on the field that your vision system can detect.
@overload
def __init__(self, pose: Pose3d, model: TargetModel) -> None:
"""
Describes a retro-reflective/colored shape vision target located somewhere on the field that
your vision system can detect.
:param pose: Pose3d of the target in field-relative coordinates
:param model: TargetModel which describes the shape of the target
"""
...
@overload
def __init__(self, pose: Pose3d, model: TargetModel, id: int) -> None:
"""
Describes a fiducial tag located somewhere on the field that your vision system can detect.
:param pose: Pose3d of the tag in field-relative coordinates
:param model: TargetModel which describes the shape of the target(tag)
:param model: TargetModel which describes the geometry of the target (tag)
:param id: The ID of this fiducial tag
"""
...
@overload
def __init__(
self, pose: Pose3d, model: TargetModel, objDetClassId: int, objDetConf: float
) -> None:
"""
Describes an object-detection vision target located somewhere on the field that your vision
system can detect.
:param pose: Pose3d of the target in field-relative coordinates
:param model: TargetModel which describes the shape of the target
:param objDetClassId: The object detection class ID, or -1 to exclude from object detection
:param objDetConf: The object detection confidence, or -1.0 to compute from target area
in the camera's field of view
"""
...
def __init__(
self,
pose: Pose3d,
model: TargetModel,
*args,
**kwargs,
):
if kwargs:
raise TypeError(
f"VisionTargetSim does not accept keyword arguments: {list(kwargs.keys())}"
)
self.pose: Pose3d = pose
self.model: TargetModel = model
self.fiducialId: int = id
self.objDetClassId: int = -1
self.objDetConf: float = -1.0
if len(args) == 0:
# VisionTargetSim(pose, model)
self.fiducialId: int = -1
self.objDetClassId: int = -1
self.objDetConf: float = -1.0
elif len(args) == 1:
# VisionTargetSim(pose, model, id)
self.fiducialId = args[0]
self.objDetClassId = -1
self.objDetConf = -1.0
elif len(args) == 2:
# VisionTargetSim(pose, model, objDetClassId, objDetConf)
self.fiducialId = -1
self.objDetClassId = args[0]
self.objDetConf = args[1]
else:
raise ValueError(
f"VisionTargetSim takes 2-4 arguments, got {2 + len(args)}"
)
def __lt__(self, right) -> bool:
return self.pose.translation().norm() < right.pose.translation().norm()

View File

@@ -525,6 +525,16 @@ public class PhotonCameraSim implements AutoCloseable {
.get();
}
// If object detection (user classId valid) but conf wasn't provided, estimate
int classId = tgt.objDetClassId;
float conf = tgt.objDetConf;
if (classId >= 0 && conf < 0) {
// Simulate confidence using sqrt-scaled area for a more realistic
// curve. Raw areaPercent/100 is tiny for most targets; sqrt scaling
// gives reasonable values even for small-but-visible objects.
conf = (float) MathUtil.clamp(Math.sqrt(areaPercent / 100.0) * 2.0, 0.0, 1.0);
}
detectableTgts.add(
new PhotonTrackedTarget(
-Math.toDegrees(centerRot.getZ()),
@@ -532,8 +542,8 @@ public class PhotonCameraSim implements AutoCloseable {
areaPercent,
Math.toDegrees(centerRot.getX()),
tgt.fiducialID,
-1,
-1,
classId,
conf,
pnpSim.best,
pnpSim.alt,
pnpSim.ambiguity,

View File

@@ -36,16 +36,21 @@ public class VisionTargetSim {
public final int fiducialID;
/** The object detection class ID, or -1 if not applicable. */
public final int objDetClassId;
/** The object detection confidence, or -1 if not applicable. */
public final float objDetConf;
/**
* Describes a vision target located somewhere on the field that your vision system can detect.
* Describes a retro-reflective/colored shape vision target located somewhere on the field that
* your vision system can detect.
*
* @param pose Pose3d of the tag in field-relative coordinates
* @param model TargetModel which describes the geometry of the target
*/
public VisionTargetSim(Pose3d pose, TargetModel model) {
this.pose = pose;
this.model = model;
this.fiducialID = -1;
this(pose, model, -1, -1, -1);
}
/**
@@ -56,9 +61,33 @@ public class VisionTargetSim {
* @param id The ID of this fiducial tag
*/
public VisionTargetSim(Pose3d pose, TargetModel model, int id) {
this(pose, model, id, -1, -1);
}
/**
* Describes an object-detection vision target located somewhere on the field that your vision
* system can detect. Class ID is the (zero-indexed) index of the object's class ID in the list of
* all classes. Confidence can be specified, or pass -1 to estimate confidence based on 2 *
* sqrt(target area / total image area)
*
* @param pose Pose3d of the target in field-relative coordinates
* @param model TargetModel which describes the geometry of the target
* @param objDetClassId The object detection class ID, if -1 it will not be detected by object
* detection
* @param objDetConf The object detection confidence, or -1 in which case the simulation will
* compute a confidence based on the area of the target in the camera's field of view
*/
public VisionTargetSim(Pose3d pose, TargetModel model, int objDetClassId, float objDetConf) {
this(pose, model, -1, objDetClassId, objDetConf);
}
private VisionTargetSim(
Pose3d pose, TargetModel model, int id, int objDetClassId, float objDetConf) {
this.pose = pose;
this.model = model;
this.fiducialID = id;
this.objDetClassId = objDetClassId;
this.objDetConf = objDetConf;
}
/**
@@ -97,7 +126,11 @@ public class VisionTargetSim {
return model;
}
/** This target's vertices offset from its field pose. */
/**
* This target's vertices offset from its field pose.
*
* @return A vector of Translation3d representing the vertices of the target
*/
public List<Translation3d> getFieldVertices() {
return model.getFieldVertices(pose);
}

View File

@@ -35,11 +35,10 @@
#include <hal/FRCUsageReporting.h>
#include <net/TimeSyncServer.h>
#include <opencv2/core.hpp>
#include <opencv2/core/mat.hpp>
#include <opencv2/core/utility.hpp>
#include <wpi/json.h>
#include "PhotonVersion.h"
#include "opencv2/core/utility.hpp"
#include "photon/dataflow/structures/Packet.h"
static constexpr units::second_t WARN_DEBOUNCE_SEC = 5_s;

View File

@@ -26,7 +26,6 @@
#include <limits>
#include <optional>
#include <span>
#include <utility>
#include <vector>

View File

@@ -32,6 +32,11 @@
#include <frc/apriltag/AprilTagFieldLayout.h>
#include <frc/apriltag/AprilTagFields.h>
#include "photon/estimation/CameraTargetRelation.h"
#include "photon/estimation/RotTrlTransform3d.h"
#include "photon/estimation/VisionEstimation.h"
#include "photon/simulation/VideoSimUtil.h"
namespace photon {
PhotonCameraSim::PhotonCameraSim(PhotonCamera* camera)
: PhotonCameraSim(camera, photon::SimCameraProperties::PERFECT_90DEG(),
@@ -214,12 +219,23 @@ PhotonPipelineResult PhotonCameraSim::Process(
}
std::optional<photon::PnpResult> pnpSim = std::nullopt;
if (tgt.fiducialId >= 0 && tgt.GetFieldVertices().size() == 4) {
if (tgt.GetFiducialId() >= 0 && tgt.GetFieldVertices().size() == 4) {
pnpSim = OpenCVHelp::SolvePNP_Square(
prop.GetIntrinsics(), prop.GetDistCoeffs(),
tgt.GetModel().GetVertices(), noisyTargetCorners);
}
// Compute object detection confidence if this is an obj det target
int classId = tgt.GetObjDetClassId();
float conf = tgt.GetObjDetConf();
if (classId >= 0 && conf < 0) {
// Simulate confidence using sqrt-scaled area for a more realistic
// curve. Raw areaPercent/100 is tiny for most targets; sqrt scaling
// gives reasonable values even for small-but-visible objects.
conf = static_cast<float>(
std::clamp(std::sqrt(areaPercent / 100.0) * 2.0, 0.0, 1.0));
}
std::vector<std::pair<float, float>> tempCorners =
OpenCVHelp::PointsToCorners(minAreaRectPts);
std::vector<TargetCorner> smallVec;
@@ -236,8 +252,8 @@ PhotonPipelineResult PhotonCameraSim::Process(
detectableTgts.emplace_back(
-centerRot.Z().convert<units::degrees>().to<double>(),
-centerRot.Y().convert<units::degrees>().to<double>(), areaPercent,
centerRot.X().convert<units::degrees>().to<double>(), tgt.fiducialId,
tgt.objDetClassId, tgt.objDetConf,
centerRot.X().convert<units::degrees>().to<double>(),
tgt.GetFiducialId(), classId, conf,
pnpSim ? pnpSim->best : frc::Transform3d{},
pnpSim ? pnpSim->alt : frc::Transform3d{},
pnpSim ? pnpSim->ambiguity : -1, smallVec, cornersDouble);
@@ -254,8 +270,8 @@ PhotonPipelineResult PhotonCameraSim::Process(
VisionTargetSim tgt = pair.first;
std::vector<cv::Point2f> corners = pair.second;
if (tgt.fiducialId > 0) {
VideoSimUtil::Warp165h5TagImage(tgt.fiducialId, corners, true,
if (tgt.GetFiducialId() > 0) {
VideoSimUtil::Warp165h5TagImage(tgt.GetFiducialId(), corners, true,
videoSimFrameRaw);
} else if (!tgt.GetModel().GetIsSpherical()) {
std::vector<cv::Point2f> contour = corners;

View File

@@ -28,6 +28,8 @@
#include <utility>
#include <vector>
#include <frc/Errors.h>
using namespace photon;
void SimCameraProperties::SetCalibration(int width, int height,

View File

@@ -31,7 +31,6 @@
#include <frc/Alert.h>
#include <networktables/BooleanTopic.h>
#include <networktables/DoubleArrayTopic.h>
#include <networktables/DoubleTopic.h>
#include <networktables/IntegerTopic.h>
#include <networktables/MultiSubscriber.h>
#include <networktables/NetworkTable.h>

View File

@@ -29,7 +29,7 @@
#include <frc/geometry/Rotation3d.h>
#include <frc/geometry/Transform3d.h>
#include <frc/interpolation/TimeInterpolatableBuffer.h>
#include <opencv2/core/mat.hpp>
#include <wpi/SmallVector.h>
#include "photon/PhotonCamera.h"
#include "photon/targeting/PhotonPipelineResult.h"

View File

@@ -26,11 +26,7 @@
#include <cmath>
#include "photon/dataflow/structures/Packet.h"
#include "photon/targeting/MultiTargetPNPResult.h"
#include "photon/targeting/PhotonPipelineResult.h"
#include "photon/targeting/PhotonTrackedTarget.h"
#include "photon/targeting/PnpResult.h"
namespace photon {

View File

@@ -32,12 +32,6 @@
#include <units/length.h>
#include <units/math.h>
#include "photon/dataflow/structures/Packet.h"
#include "photon/targeting/MultiTargetPNPResult.h"
#include "photon/targeting/PhotonPipelineResult.h"
#include "photon/targeting/PhotonTrackedTarget.h"
#include "photon/targeting/PnpResult.h"
namespace photon {
class PhotonUtils {
public:

View File

@@ -24,23 +24,15 @@
#pragma once
#include <algorithm>
#include <limits>
#include <string>
#include <utility>
#include <vector>
#include <cameraserver/CameraServer.h>
#include <frc/Timer.h>
#include <frc/apriltag/AprilTagFieldLayout.h>
#include <frc/apriltag/AprilTagFields.h>
#include <photon/PhotonCamera.h>
#include <photon/PhotonTargetSortMode.h>
#include <photon/estimation/CameraTargetRelation.h>
#include <photon/estimation/VisionEstimation.h>
#include <photon/networktables/NTTopicSet.h>
#include <photon/simulation/SimCameraProperties.h>
#include <photon/simulation/VideoSimUtil.h>
#include <photon/simulation/VisionTargetSim.h>
#include <units/math.h>
#include <wpi/timestamp.h>

View File

@@ -24,15 +24,12 @@
#pragma once
#include <algorithm>
#include <random>
#include <string>
#include <utility>
#include <vector>
#include <Eigen/Core>
#include <frc/Errors.h>
#include <frc/MathUtil.h>
#include <frc/geometry/Rotation2d.h>
#include <frc/geometry/Translation3d.h>
#include <photon/estimation/OpenCVHelp.h>

View File

@@ -38,8 +38,7 @@
#include <opencv2/objdetect.hpp>
#include <units/length.h>
#include "SimCameraProperties.h"
#include "photon/estimation/RotTrlTransform3d.h"
#include "photon/simulation/SimCameraProperties.h"
namespace mathutil {
template <typename T>

View File

@@ -346,11 +346,14 @@ class VisionSystemSim {
const std::vector<VisionTargetSim>& targets) {
std::vector<VisionTargetSim> removedList;
for (auto& entry : targetSets) {
for (auto target : entry.second) {
auto it = std::find(targets.begin(), targets.end(), target);
if (it != targets.end()) {
removedList.emplace_back(target);
entry.second.erase(it);
auto& vec = entry.second;
auto it = vec.begin();
while (it != vec.end()) {
if (std::find(targets.begin(), targets.end(), *it) != targets.end()) {
removedList.emplace_back(*it);
it = vec.erase(it);
} else {
++it;
}
}
}

View File

@@ -31,23 +31,120 @@
#include "photon/estimation/TargetModel.h"
namespace photon {
/** Describes a vision target located somewhere on the field that your vision
* system can detect. */
class VisionTargetSim {
public:
/**
* Describes a retro-reflective/colored shape vision target located somewhere
* on the field that your vision system can detect.
*
* @param pose Pose3d of the tag in field-relative coordinates
* @param model TargetModel which describes the geometry of the target
*/
VisionTargetSim(const frc::Pose3d& pose, const TargetModel& model)
: fiducialId(-1), pose(pose), model(model) {}
: fiducialId(-1),
objDetClassId(-1),
objDetConf(-1),
pose(pose),
model(model) {}
/**
* Describes a fiducial tag located somewhere on the field that your vision
* system can detect.
*
* @param pose Pose3d of the tag in field-relative coordinates
* @param model TargetModel which describes the geometry of the target(tag)
* @param id The ID of this fiducial tag
*/
VisionTargetSim(const frc::Pose3d& pose, const TargetModel& model, int id)
: fiducialId(id), pose(pose), model(model) {}
: fiducialId(id),
objDetClassId(-1),
objDetConf(-1),
pose(pose),
model(model) {}
/**
* Describes an object-detection vision target located somewhere on the field
* that your vision system can detect. Class ID is the (zero-indexed) index of
* the object's class ID in the list of all classes. Confidence can be
* specified, or pass -1 to estimate confidence based on 2 * sqrt(target area
* / total image area)
*
* @param pose Pose3d of the target in field-relative coordinates
* @param model TargetModel which describes the geometry of the target
* @param objDetClassId The object detection class ID, if -1 it will not be
* detected by object detection
* @param objDetConf The object detection confidence, or -1 in which case the
* simulation will compute a confidence based on the area of the target in the
* camera's field of view
*/
VisionTargetSim(const frc::Pose3d& pose, const TargetModel& model,
int objDetClassId, float objDetConf)
: fiducialId(-1),
objDetClassId(objDetClassId),
objDetConf(objDetConf),
pose(pose),
model(model) {}
/**
* Sets the pose of this target on the field.
*
* @param newPose The pose in field-relative coordinates
*/
void SetPose(const frc::Pose3d& newPose) { pose = newPose; }
/**
* Sets the model describing this target's geometry.
*
* @param newModel The model of the target
*/
void SetModel(const TargetModel& newModel) { model = newModel; }
/**
* Returns the pose of this target on the field.
*
* @return The pose in field-relative coordinates
*/
frc::Pose3d GetPose() const { return pose; }
/**
* Returns the model describing this target's geometry.
*
* @return The model of the target
*/
TargetModel GetModel() const { return model; }
/**
* Returns the fiducial ID of this target, or -1 if not a fiducial target.
*
* @return The fiducial ID
*/
int GetFiducialId() const { return fiducialId; }
/**
* Returns the object detection class ID of this target, or -1 if not an
* object detection target.
*
* @return The object detection class ID
*/
int GetObjDetClassId() const { return objDetClassId; }
/**
* Returns the object detection confidence of this target, or -1 if
* confidence is estimated from target area or is not an object.
*
* @return The object detection confidence
*/
float GetObjDetConf() const { return objDetConf; }
/**
* This target's vertices offset from its field pose.
* @return A vector of Translation3d representing the vertices of the target
*/
std::vector<frc::Translation3d> GetFieldVertices() const {
return model.GetFieldVertices(pose);
}
int fiducialId;
int objDetClassId = -1;
float objDetConf = -1;
bool operator<(const VisionTargetSim& right) const {
return pose.Translation().Norm() < right.pose.Translation().Norm();
@@ -70,6 +167,9 @@ class VisionTargetSim {
}
private:
int fiducialId;
int objDetClassId;
float objDetConf;
frc::Pose3d pose;
TargetModel model;
};

View File

@@ -601,7 +601,49 @@ class VisionSystemSimTest {
robotPose = new Pose2d(-2, -2, Rotation2d.fromDegrees(30));
visionSysSim.update(robotPose);
ambiguity = waitForSequenceNumber(camera, 2).getBestTarget().getPoseAmbiguity();
var target2 = waitForSequenceNumber(camera, 2).getBestTarget();
ambiguity = target2.getPoseAmbiguity();
assertTrue(0 < ambiguity && ambiguity < 0.2, "Tag ambiguity expected to be low");
// and prove that object detection class id/conf are -1 when we look at a tag
assertEquals(-1, target2.objDetectId);
assertEquals(-1, target2.objDetectConf);
}
@Test
public void testObjectDetection() {
var visionSysSim = new VisionSystemSim("Test");
var camera = new PhotonCamera(inst, "camera");
var cameraSim = new PhotonCameraSim(camera);
visionSysSim.addCamera(cameraSim, new Transform3d());
cameraSim.prop.setCalibration(640, 480, Rotation2d.fromDegrees(80));
cameraSim.setMinTargetAreaPixels(20.0);
final var targetPose = new Pose3d(new Translation3d(2, 0, 0), new Rotation3d(0, 0, Math.PI));
final int classId = 3;
final float conf = 0.67f;
final TargetModel ballModel = new TargetModel(Units.inchesToMeters(6));
final var ballTarget = new VisionTargetSim(targetPose, ballModel, classId, conf);
visionSysSim.addVisionTargets(ballTarget);
var robotPose = Pose2d.kZero;
visionSysSim.update(robotPose);
var target1 = waitForSequenceNumber(camera, 1).getBestTarget();
assertEquals(classId, target1.objDetectId);
assertEquals(conf, target1.objDetectConf);
assertEquals(-1, target1.fiducialId);
// much around with the target to force PhotonCameraSim::process calculate conf
visionSysSim.removeVisionTargets(ballTarget);
final float conf2 = -1;
final var ballTarget2 = new VisionTargetSim(targetPose, ballModel, classId, conf2);
visionSysSim.addVisionTargets(ballTarget2);
visionSysSim.update(robotPose);
var target2 = waitForSequenceNumber(camera, 2).getBestTarget();
assertEquals(classId, target2.objDetectId);
// 2 * sqrt(area pixels) at this particular pose
assertEquals(0.131, target2.objDetectConf, 0.01);
assertEquals(-1, target2.fiducialId);
}
}

View File

@@ -24,7 +24,6 @@
#include "photon/PhotonPoseEstimator.h"
#include <map>
#include <utility>
#include <vector>

View File

@@ -22,12 +22,11 @@
* SOFTWARE.
*/
#include <iostream>
#include <gtest/gtest.h>
#include <wpi/print.h>
#include "PhotonVersion.h"
TEST(VersionTest, PrintVersion) {
std::cout << photon::PhotonVersion::versionString << std::endl;
wpi::println("{}", photon::PhotonVersion::versionString);
}

View File

@@ -24,8 +24,6 @@
#include "photon/simulation/VisionSystemSim.h"
#include <chrono>
#include <thread>
#include <tuple>
#include <vector>
@@ -33,6 +31,7 @@
#include <wpi/deprecated.h>
#include "photon/PhotonUtils.h"
#include "photon/estimation/VisionEstimation.h"
// Ignore GetLatestResult warnings
WPI_IGNORE_DEPRECATED

View File

@@ -17,20 +17,14 @@
#include "net/TimeSyncClient.h"
#include <atomic>
#include <chrono>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <iostream>
#include <mutex>
#include <thread>
#include <Eigen/Core>
#include <wpi/Logger.h>
#include <wpi/print.h>
#include <wpi/struct/Struct.h>
#include <wpinet/UDPClient.h>
#include <wpinet/uv/util.h>
#include "ntcore_cpp.h"

View File

@@ -17,22 +17,18 @@
#include "net/TimeSyncServer.h"
#include <atomic>
#include <chrono>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <iostream>
#include <mutex>
#include <thread>
#include <ntcore_cpp.h>
#include <wpi/Logger.h>
#include <wpi/print.h>
#include <wpi/struct/Struct.h>
#include <wpinet/UDPClient.h>
#include <wpinet/uv/util.h>
#include "ntcore_cpp.h"
#include "net/TimeSyncStructs.h"
static void ServerLoggerFunc(unsigned int level, const char* file,
unsigned int line, const char* msg) {

View File

@@ -17,11 +17,8 @@
#include "photon/constrained_solvepnp/wrap/casadi_wrapper.h"
#include <chrono>
#include <cstdio>
#include <iostream>
#include <optional>
#include <vector>
#include <Eigen/Cholesky>
#include <Eigen/Core>
@@ -192,10 +189,10 @@ constrained_solvepnp::do_optimization(
fmt::println("{} tags", nTags);
// fmt::println("nstate {}", nState);
std::cout << "robot2camera:\n" << robot2camera << std::endl;
std::cout << "x guess:\n" << x_guess << std::endl;
std::cout << "field2pt:\n" << field2points << std::endl;
std::cout << "observations:\n" << point_observations << std::endl;
fmt::println("robot2camera:\n{}", robot2camera);
fmt::println("x guess:\n{}", x_guess);
fmt::println("field2pt:\n{}", field2points);
fmt::println("observations:\n{}", point_observations);
fmt::println("---------^^^^^^^^---------");
}
@@ -252,7 +249,7 @@ constrained_solvepnp::do_optimization(
auto H_ldlt = H.ldlt();
if (H_ldlt.info() != Eigen::Success) {
std::cerr << "LDLT decomp failed! H=" << std::endl << H << std::endl;
fmt::println(stderr, "LDLT decomp failed! H=\n{}", H);
return wpi::unexpected{slp::ExitStatus::LOCALLY_INFEASIBLE};
}
@@ -276,7 +273,7 @@ constrained_solvepnp::do_optimization(
H_ldlt = H_reg.ldlt();
if (H_ldlt.info() != Eigen::Success) {
std::cerr << "LDLT decomp failed! H=" << std::endl << H << std::endl;
fmt::println(stderr, "LDLT decomp failed! H=\n{}", H);
return wpi::unexpected{slp::ExitStatus::LOCALLY_INFEASIBLE};
}

View File

@@ -17,13 +17,10 @@
#include "photon/estimation/VisionEstimation.h"
#include <iostream>
#include <utility>
#include <vector>
#include "photon/constrained_solvepnp/wrap/casadi_wrapper.h"
#include "photon/estimation/OpenCVHelp.h"
#include "photon/targeting/MultiTargetPNPResult.h"
namespace photon {
namespace VisionEstimation {

View File

@@ -17,14 +17,4 @@
#include "photon/targeting/PhotonTrackedTarget.h"
#include <algorithm>
#include <iostream>
#include <utility>
#include <vector>
#include <frc/geometry/Translation2d.h>
#include <wpi/SmallVector.h>
static constexpr const uint8_t MAX_CORNERS = 8;
namespace photon {} // namespace photon

View File

@@ -17,31 +17,21 @@
#pragma once
#include <atomic>
#include <chrono>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <functional>
#include <iostream>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <frc/filter/MedianFilter.h>
#include <wpi/Logger.h>
#include <wpi/print.h>
#include <wpi/static_circular_buffer.h>
#include <wpi/struct/Struct.h>
#include <wpinet/EventLoopRunner.h>
#include <wpinet/UDPClient.h>
#include <wpinet/uv/Buffer.h>
#include <wpinet/uv/Timer.h>
#include <wpinet/uv/Udp.h>
#include "TimeSyncStructs.h"
#include "ntcore_cpp.h"
namespace wpi {
namespace tsp {

View File

@@ -17,30 +17,18 @@
#pragma once
#include <atomic>
#include <chrono>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <functional>
#include <iostream>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <wpi/Logger.h>
#include <wpi/print.h>
#include <wpi/struct/Struct.h>
#include <wpinet/EventLoopRunner.h>
#include <wpinet/UDPClient.h>
#include <wpinet/uv/Buffer.h>
#include <wpinet/uv/Timer.h>
#include <wpinet/uv/Udp.h>
#include "TimeSyncStructs.h"
#include "ntcore_cpp.h"
namespace wpi {
namespace tsp {

View File

@@ -17,17 +17,13 @@
#pragma once
#include <algorithm>
#include <bit>
#include <cstring>
#include <iostream>
#include <concepts>
#include <cstdint>
#include <optional>
#include <span>
#include <string>
#include <string_view>
#include <vector>
#include <wpi/Demangle.h>
#include <wpi/ct_string.h>
#include <wpi/struct/Struct.h>
namespace photon {

View File

@@ -30,7 +30,6 @@
#define OPENCV_DISABLE_EIGEN_TENSOR_SUPPORT
#include <opencv2/core/eigen.hpp>
#include "photon/targeting/MultiTargetPNPResult.h"
#include "photon/targeting/PnpResult.h"
#include "photon/targeting/TargetCorner.h"

View File

@@ -102,7 +102,5 @@ class NTTopicSet {
cameraDistortionPublisher =
subTable->GetDoubleArrayTopic("cameraDistortion").Publish();
}
private:
};
} // namespace photon

View File

@@ -19,11 +19,6 @@
#include <utility>
#include <frc/geometry/Transform3d.h>
#include <wpi/SmallVector.h>
#include "PnpResult.h"
#include "photon/dataflow/structures/Packet.h"
#include "photon/struct/MultiTargetPNPResultStruct.h"
namespace photon {

View File

@@ -18,16 +18,13 @@
#pragma once
#include <span>
#include <string>
#include <utility>
#include <units/time.h>
#include <wpi/SmallVector.h>
#include "MultiTargetPNPResult.h"
#include "PhotonTrackedTarget.h"
#include "fmt/base.h"
#include "photon/dataflow/structures/Packet.h"
#include "photon/struct/PhotonPipelineResultStruct.h"
namespace photon {

View File

@@ -17,13 +17,10 @@
#pragma once
#include <cstddef>
#include <string>
#include <utility>
#include <vector>
#include <frc/geometry/Transform3d.h>
#include <wpi/SmallVector.h>
#include "photon/struct/PhotonTrackedTargetStruct.h"

View File

@@ -19,9 +19,6 @@
#include <utility>
#include <frc/geometry/Transform3d.h>
#include "photon/dataflow/structures/Packet.h"
#include "photon/struct/PnpResultStruct.h"
namespace photon {

View File

@@ -15,13 +15,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <iostream>
#include <span>
#include <vector>
#include <fmt/core.h>
#include <fmt/ranges.h>
#include "org_photonvision_jni_ConstrainedSolvepnpJni.h"
#include "photon/constrained_solvepnp/wrap/casadi_wrapper.h"
@@ -72,18 +68,6 @@ Java_org_photonvision_jni_ConstrainedSolvepnpJni_do_1optimization
pointObservationsMat(pointObservationsVec.data(), 2,
pointObservationsVec.size() / 2);
#if 0
fmt::println("======================================================");
fmt::println("Got robot2camera raw {}", robot2cameraVec);
fmt::println("Camera cal {} {} {} {}", cameraCal_.fx, cameraCal_.fy,
cameraCal_.cx, cameraCal_.cy);
fmt::println("{} tags", nTags);
std::cout << "robot2camera:\n" << robot2cameraMat << std::endl;
std::cout << "x guess:\n" << xGuessMat << std::endl;
std::cout << "field2pt:\n" << field2pointsMat << std::endl;
std::cout << "observations:\n" << pointObservationsMat << std::endl;
#endif
wpi::expected<constrained_solvepnp::RobotStateMat, slp::ExitStatus> result =
constrained_solvepnp::do_optimization(
headingFree, nTags, cameraCal_, robot2cameraMat, xGuessMat,

View File

@@ -15,8 +15,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <cstdio>
#include <org_photonvision_jni_TimeSyncClient.h>
#include <org_photonvision_jni_TimeSyncServer.h>

View File

@@ -15,12 +15,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <chrono>
#include <cstdio>
#include <iostream>
#include <vector>
#include <frc/fmt/Eigen.h>
#include <gtest/gtest.h>
#include <wpi/print.h>
#include <wpi/timestamp.h>
#include "photon/constrained_solvepnp/wrap/casadi_wrapper.h"
@@ -169,11 +166,10 @@ void print_cost(casadi_real robot_x, casadi_real robot_y,
x_guess, field2points, point_observations, 0, 0);
auto end = wpi::Now();
std::cout << i << "," << static_cast<bool>(x_out) << "," << end - start
<< std::endl;
std::cout << "Solution:"
<< x_out.value_or(constrained_solvepnp::RobotStateMat::Identity())
<< std::endl;
wpi::println("{},{},{}", i, static_cast<bool>(x_out), end - start);
wpi::println(
"Solution: {}",
x_out.value_or(constrained_solvepnp::RobotStateMat::Identity()));
// std::cout << "iter "
// << i
// // << "\nGuess:\n" << x_guess << "\n Optimized ->\n"

View File

@@ -19,6 +19,7 @@
#include <hal/HAL.h>
#include <net/TimeSyncClient.h>
#include <net/TimeSyncServer.h>
#include <wpi/print.h>
TEST(TimeSyncProtocolTest, Smoketest) {
using namespace wpi::tsp;

View File

@@ -24,8 +24,6 @@
#include "Robot.h"
#include <iostream>
#include <frc/simulation/BatterySim.h>
#include <frc/simulation/RoboRioSim.h>
#include <photon/PhotonUtils.h>

View File

@@ -24,7 +24,6 @@
#include "subsystems/SwerveDrive.h"
#include <iostream>
#include <string>
#include <frc/TimedRobot.h>

View File

@@ -24,8 +24,6 @@
#include "subsystems/SwerveDriveSim.h"
#include <iostream>
#include <frc/RobotController.h>
#include <frc/system/Discretization.h>

View File

@@ -24,7 +24,6 @@
#include "subsystems/SwerveModule.h"
#include <iostream>
#include <string>
#include <frc/MathUtil.h>

View File

@@ -24,8 +24,6 @@
#include "Robot.h"
#include <iostream>
#include <frc/simulation/BatterySim.h>
#include <frc/simulation/RoboRioSim.h>

View File

@@ -24,7 +24,6 @@
#include "subsystems/SwerveDrive.h"
#include <iostream>
#include <string>
#include <frc/TimedRobot.h>

View File

@@ -24,8 +24,6 @@
#include "subsystems/SwerveDriveSim.h"
#include <iostream>
#include <frc/RobotController.h>
#include <frc/system/Discretization.h>

View File

@@ -24,7 +24,6 @@
#include "subsystems/SwerveModule.h"
#include <iostream>
#include <string>
#include <frc/MathUtil.h>

View File

@@ -24,8 +24,6 @@
#include "Robot.h"
#include <iostream>
#include <frc/simulation/BatterySim.h>
#include <frc/simulation/RoboRioSim.h>

Some files were not shown because too many files have changed in this diff Show More