Compare commits

...

12 Commits

Author SHA1 Message Date
Craig Schardt
6a8d638853 Fix typehint problem with ndarrays (#1631)
mypy was inferring the wrong type for ndarrays in photonCameraSim.py.
This fixes the problem by adding typehints using numpy.typing.NDArray.
2024-12-09 05:21:37 +00:00
Craig Schardt
782929b006 Fix "Manage Device Networking" toggle being disabled incorrectly (#1620)
This fixes a bug introduced in #1592 that caused the Manage Device
Networking toggle to be disabled for systems where PhotonVision is
managing the network.

There is still a problem with the toggle defaulting to "off" and not
staying in the "on" position after settings are saved. I need help from
someone who understands the frontend to figure out why it keeps getting
set back to "off".

---------

Co-authored-by: Matt <matthew.morley.ca@gmail.com>
Co-authored-by: Cameron (3539) <theforgelover@gmail.com>
2024-12-07 04:30:41 +00:00
Craig Schardt
4997ad9115 Fix formating errors that are in master (#1627)
A few files with format mistakes were merged into master and they cause
spotless and wpiformat to fail. This PR fixes those files.
2024-12-06 22:21:05 +00:00
Matt
857a30d980 Pull image version from metadata file (#1599)
Closes #1554


![image](https://github.com/user-attachments/assets/fa51c0a3-a25e-4112-8ef2-990404c746d6)
2024-12-05 02:01:48 +00:00
Julius
a40e69cca0 Update docs to suggest JDK 17 (#1611)
#1609 
#1604
2024-12-04 03:27:10 +00:00
William Toth
e069a79a32 Check for exposure setting validity before accessing. (#1618)
If the exposure property for the generic USB camera settable was not
valid for one camera in the list, then the thread would crash/hang and
no cameras would show up in the list
2024-12-01 23:59:00 +00:00
Joseph Farkas
d9dfe15bfe Set loaded to false when JNI loading fails (#1614)
#1613
2024-12-01 16:19:31 +00:00
James Ward
1dbd2e5990 [python] Correct time units (#1605) 2024-11-28 16:12:52 +00:00
Matt
7e9da4133d (Mostly) allow reloading during calibration (#1593) 2024-11-26 03:25:50 +00:00
Gold856
163b5c9c81 Remove unused JNI artifacts (#1603)
Also removes unnecessary `_M_` prefix from artifacts.
2024-11-23 16:54:00 +00:00
Max Midgley
c6a3638a2f More obvious industrial SD card recommendation (#1601)
It wasn't hard to not notice or skip over the recommendation, or just
not take it seriously. So I added some banners to direct people to read
the quick start guide, and a banner to heavily suggest using an
industrial SD card, since they're just the best thing available right
now.
2024-11-23 07:39:42 +00:00
Jordan McMichael
44f78cb03e [Examples] Limit minimum battery voltage in sim to 0.1V (#1600)
Occasionally, the sim projects are capable of simulating current draw of
over 600A, which triggers a condition in
`BatterySim::calculateDefaultBatteryLoadedVoltage` that limits the
minimum measured battery voltage to 0V (to prevent it from going
negative).

When battery voltage measures 0, this causes NaN values to propagate
through the drivetrain model, making sim inoperable. Specifically, [this
is the
line](https://github.com/PhotonVision/photonvision/blob/master/photonlib-java-examples/aimandrange/src/main/java/frc/robot/subsystems/drivetrain/SwerveDriveSim.java#L452)
that causes the initial NaN values in simulation.

This PR is posed as a patch to ensure that simulation doesn't break.
2024-11-22 00:21:27 +00:00
26 changed files with 218 additions and 144 deletions

View File

@@ -8,14 +8,14 @@ You do not need to install PhotonVision on a Windows PC in order to access the w
## Installing Java
PhotonVision requires a JDK installed and on the system path. JDK 11 is needed (different versions will not work). If you don't have JDK 11 already, run the following to install it:
PhotonVision requires a JDK installed and on the system path. JDK 17 is needed (different versions will not work). If you don't have JDK 17 already, run the following to install it:
```
$ sudo apt-get install openjdk-11-jdk
$ sudo apt-get install openjdk-17-jdk
```
:::{warning}
Using a JDK other than JDK11 will cause issues when running PhotonVision and is not supported.
Using a JDK other than JDK17 will cause issues when running PhotonVision and is not supported.
:::
## Downloading the Latest Stable Release of PhotonVision

View File

@@ -5,17 +5,17 @@ Due to current [cscore](https://github.com/wpilibsuite/allwpilib/tree/main/cscor
:::
:::{note}
You do not need to install PhotonVision on a Windows PC in order to access the webdashboard (assuming you are using an external coprocessor like a Raspberry Pi).
You do not need to install PhotonVision on a Mac in order to access the webdashboard (assuming you are using an external coprocessor like a Raspberry Pi).
:::
VERY Limited macOS support is available.
## Installing Java
PhotonVision requires a JDK installed and on the system path. JDK 11 is needed (different versions will not work). You may already have this if you have installed WPILib. If not, [download and install it from here](https://adoptium.net/temurin/releases?version=11).
PhotonVision requires a JDK installed and on the system path. JDK 17 is needed (different versions will not work). You may already have this if you have installed WPILib 2025+. If not, [download and install it from here](https://adoptium.net/temurin/releases?version=17).
:::{warning}
Using a JDK other than JDK11 will cause issues when running PhotonVision and is not supported.
Using a JDK other than JDK17 will cause issues when running PhotonVision and is not supported.
:::
## Downloading the Latest Stable Release of PhotonVision

View File

@@ -12,10 +12,14 @@ Bonjour provides more stable networking when using Windows PCs. Install [Bonjour
## Installing Java
PhotonVision requires a JDK installed and on the system path. **JDK 11 is needed** (different versions will not work). You may already have this if you have installed WPILib, but ensure that running `java -version` shows JDK 11. If not, [download and install it from here](https://adoptium.net/temurin/releases?version=11) and ensure that the new JDK is being used.
PhotonVision requires a JDK installed and on the system path. **JDK 17 is needed. Windows Users must use the JDK that ships with WPILib.** [Download and install it from here.](https://github.com/wpilibsuite/allwpilib/releases/tag/v2025.1.1-beta-2) Either ensure the only Java on your PATH is the WPILIB Java or specify it to gradle with `-Dorg.gradle.java.home=C:\Users\Public\wpilib\2025\jdk`:
```
> ./gradlew run "-Dorg.gradle.java.home=C:\Users\Public\wpilib\2025\jdk"
```
:::{warning}
Using a JDK other than JDK11 will cause issues when running PhotonVision and is not supported.
Using a JDK other than WPILIB's JDK17 will cause issues when running PhotonVision and is not supported.
:::
## Downloading the Latest Stable Release of PhotonVision

View File

@@ -1,5 +1,10 @@
# Selecting Hardware
:::{note}
It is highly recommended that you read the {ref}`quick start guide<docs/quick-start/common-setups:Common Hardware Setups>`, and use the hardware recommended there that
is not touched on here.
:::
In order to use PhotonVision, you need a coprocessor and a camera. Other than the recommended hardware found in the {ref}`quick start guide<docs/quick-start/common-setups:Common Hardware Setups>`, this page will help you select hardware that should work for photonvision even though it is not supported/recommended.
## Choosing a Coprocessor

View File

@@ -13,6 +13,11 @@ The Orange Pi 5 is the only currently supported device for object detection.
## SD Cards
:::{important}
It is highly recommended that you use an industrial micro SD card, as they offer far greater protection against corruption from improper shutdowns, like most cards
face every time the robot is turned off.
:::
- 8GB or larger micro SD card
- Many teams have found that an industrial micro sd card are much more stable in competition. One example is the SanDisk industrial 16GB micro SD card.

View File

@@ -157,7 +157,10 @@ const downloadCalibBoard = () => {
doc.save(`calibrationTarget-${CalibrationBoardTypes[boardType.value]}.pdf`);
};
const isCalibrating = ref(false);
const isCalibrating = computed(
() => useCameraSettingsStore().currentCameraSettings.currentPipelineIndex === WebsocketPipelineType.Calib3d
);
const startCalibration = () => {
useCameraSettingsStore().startPnPCalibration({
squareSizeIn: squareSizeIn.value,
@@ -170,13 +173,15 @@ const startCalibration = () => {
});
// The Start PnP method already handles updating the backend so only a store update is required
useCameraSettingsStore().currentCameraSettings.currentPipelineIndex = WebsocketPipelineType.Calib3d;
isCalibrating.value = true;
// isCalibrating.value = true;
calibCanceled.value = false;
};
const showCalibEndDialog = ref(false);
const calibCanceled = ref(false);
const calibSuccess = ref<boolean | undefined>(undefined);
const endCalibration = () => {
calibSuccess.value = undefined;
if (!useStateStore().calibrationData.hasEnoughImages) {
calibCanceled.value = true;
}
@@ -192,7 +197,8 @@ const endCalibration = () => {
calibSuccess.value = false;
})
.finally(() => {
isCalibrating.value = false;
// isCalibrating.value = false;
// backend deals with this for us
});
};
@@ -245,6 +251,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
<v-row style="display: flex; flex-direction: column" class="mt-4">
<v-card-subtitle v-show="!isCalibrating" class="pl-3 pa-0 ma-0"> Configure New Calibration</v-card-subtitle>
<v-form ref="form" v-model="settingsValid" class="pl-4 mb-10 pr-5">
<!-- TODO: the default videoFormatIndex is 0, but the list of unique video mode indexes might not include 0. getUniqueVideoResolutionStrings indexing is also different from the normal video mode indexing -->
<pv-select
v-model="useStateStore().calibrationData.videoFormatIndex"
label="Resolution"
@@ -492,10 +499,12 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
process.</v-card-text
>
</template>
<template v-else-if="isCalibrating">
<!-- No result reported yet -->
<template v-else-if="calibSuccess === undefined">
<v-progress-circular indeterminate :size="70" :width="8" color="accent" />
<v-card-text>Camera is being calibrated. This process may take several minutes...</v-card-text>
</template>
<!-- Got positive result -->
<template v-else-if="calibSuccess">
<v-icon color="green" size="70"> mdi-check-bold </v-icon>
<v-card-text>

View File

@@ -21,12 +21,8 @@ import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.HashMap;
import java.util.Map;
import org.photonvision.common.hardware.Platform;
import org.photonvision.common.networking.NetworkMode;
import org.photonvision.common.util.file.JacksonUtils;
public class NetworkConfig {
// Can be an integer team number, or an IP address
@@ -104,17 +100,6 @@ public class NetworkConfig {
config.matchCamerasOnlyByPath);
}
public Map<String, Object> toHashMap() {
try {
var ret = new ObjectMapper().convertValue(this, JacksonUtils.UIMap.class);
ret.put("canManage", this.deviceCanManageNetwork());
return ret;
} catch (Exception e) {
e.printStackTrace();
return new HashMap<>();
}
}
@JsonIgnore
public String getPhysicalInterfaceName() {
return this.networkManagerIface;
@@ -125,18 +110,12 @@ public class NetworkConfig {
return "\"" + networkManagerIface + "\"";
}
@JsonIgnore
public boolean shouldManage() {
return this.shouldManage;
}
@JsonIgnore
public void setShouldManage(boolean shouldManage) {
this.shouldManage = shouldManage && this.deviceCanManageNetwork();
}
@JsonIgnore
private boolean deviceCanManageNetwork() {
protected boolean deviceCanManageNetwork() {
return Platform.isLinux();
}

View File

@@ -27,8 +27,10 @@ public class UINetConfig extends NetworkConfig {
super(config);
this.networkInterfaceNames = networkInterfaceNames;
this.networkingDisabled = networkingDisabled;
this.canManage = this.deviceCanManageNetwork();
}
public List<NMDeviceInfo> networkInterfaceNames;
public boolean networkingDisabled;
public boolean canManage;
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.hardware;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
/**
* Our blessed images inject the current version via this build workflow:
* https://github.com/PhotonVision/photon-image-modifier/blob/2e5ddb6b599df0be921c12c8dbe7b939ecd7f615/.github/workflows/main.yml#L67
*
* <p>This class provides a convienent abstraction around this
*/
public class OsImageVersion {
private static final Logger logger = new Logger(OsImageVersion.class, LogGroup.General);
private static Path imageVersionFile = Path.of("/opt/photonvision/image-version");
public static final Optional<String> IMAGE_VERSION = getImageVersion();
private static Optional<String> getImageVersion() {
if (!imageVersionFile.toFile().exists()) {
logger.warn(
"Photon cannot locate base OS image version metadata at " + imageVersionFile.toString());
return Optional.empty();
}
try {
return Optional.of(Files.readString(imageVersionFile).strip());
} catch (IOException e) {
logger.error("Couldn't read image-version file", e);
}
return Optional.empty();
}
}

View File

@@ -113,7 +113,7 @@ public class NetworkManager {
}
public void reinitialize() {
initialize(ConfigManager.getInstance().getConfig().getNetworkConfig().shouldManage());
initialize(ConfigManager.getInstance().getConfig().getNetworkConfig().shouldManage);
DataChangeService.getInstance()
.publishEvent(

View File

@@ -69,7 +69,8 @@ public abstract class PhotonJNICommon {
logger.error("Couldn't load shared object " + libraryName, e);
e.printStackTrace();
// logger.error(System.getProperty("java.library.path"));
break;
instance.setLoaded(false);
return;
}
}
instance.setLoaded(true);

View File

@@ -95,9 +95,11 @@ public class GenericUSBCameraSettables extends VisionSourceSettables {
// first.
var autoExpProp = findProperty("exposure_auto", "auto_exposure");
exposureAbsProp = expProp.get();
this.minExposure = exposureAbsProp.getMin();
this.maxExposure = exposureAbsProp.getMax();
if (expProp.isPresent()) {
exposureAbsProp = expProp.get();
this.minExposure = exposureAbsProp.getMin();
this.maxExposure = exposureAbsProp.getMax();
}
if (autoExpProp.isPresent()) {
autoExposureProp = autoExpProp.get();
@@ -184,7 +186,7 @@ public class GenericUSBCameraSettables extends VisionSourceSettables {
softSet("auto_exposure_bias", 12);
softSet("iso_sensitivity_auto", 1);
softSet("iso_sensitivity", 1); // Manual ISO adjustment by default
autoExposureProp.set(PROP_AUTO_EXPOSURE_ENABLED);
if (autoExposureProp != null) autoExposureProp.set(PROP_AUTO_EXPOSURE_ENABLED);
}
}

View File

@@ -17,10 +17,11 @@ class NTTopicSet:
different for sim vs. real camera
"""
def __init__(self, tableName: str, cameraName: str) -> None:
instance = nt.NetworkTableInstance.getDefault()
photonvision_root_table = instance.getTable(tableName)
self.subTable = photonvision_root_table.getSubTable(cameraName)
def __init__(
self,
ntSubTable: nt.NetworkTable,
) -> None:
self.subTable = ntSubTable
def updateEntries(self) -> None:
options = nt.PubSubOptions()

View File

@@ -60,11 +60,10 @@ class PhotonCameraSim:
self.videoSimRawEnabled: bool = False
self.videoSimWireframeEnabled: bool = False
self.videoSimWireframeResolution: float = 0.1
self.videoSimProcEnabled: bool = (
False # TODO switch this back to default True when the functionality is enabled
)
# TODO switch this back to default True when the functionality is enabled
self.videoSimProcEnabled: bool = False
self.heartbeatCounter: int = 0
self.nextNtEntryTime = int(wpilib.Timer.getFPGATimestamp() * 1e6)
self.nextNtEntryTime = wpilib.Timer.getFPGATimestamp()
self.tagLayout = AprilTagFieldLayout.loadField(AprilTagField.k2024Crescendo)
self.cam = camera
@@ -95,7 +94,7 @@ class PhotonCameraSim:
(self.prop.getResWidth(), self.prop.getResHeight())
)
self.ts = NTTopicSet("photonvision", self.cam.getName())
self.ts = NTTopicSet(self.cam._cameraTable)
self.ts.updateEntries()
# Handle this last explicitly for this function signature because the other constructor is called in the initialiser list
@@ -173,20 +172,20 @@ class PhotonCameraSim:
def consumeNextEntryTime(self) -> float | None:
"""Determine if this camera should process a new frame based on performance metrics and the time
since the last update. This returns an Optional which is either empty if no update should occur
or a Long of the timestamp in microseconds of when the frame which should be received by NT. If
or a float of the timestamp in seconds of when the frame which should be received by NT. If
a timestamp is returned, the last frame update time becomes that timestamp.
:returns: Optional long which is empty while blocked or the NT entry timestamp in microseconds if
:returns: Optional float which is empty while blocked or the NT entry timestamp in seconds if
ready
"""
# check if this camera is ready for another frame update
now = int(wpilib.Timer.getFPGATimestamp() * 1e6)
timestamp = 0
now = wpilib.Timer.getFPGATimestamp()
timestamp = 0.0
iter = 0
# prepare next latest update
while now >= self.nextNtEntryTime:
timestamp = int(self.nextNtEntryTime)
frameTime = int(self.prop.estSecUntilNextFrame() * 1e6)
timestamp = self.nextNtEntryTime
frameTime = self.prop.estSecUntilNextFrame()
self.nextNtEntryTime += frameTime
# if frame time is very small, avoid blocking
@@ -432,7 +431,9 @@ class PhotonCameraSim:
)
def submitProcessedFrame(
self, result: PhotonPipelineResult, receiveTimestamp: float | None
self,
result: PhotonPipelineResult,
receiveTimestamp_us: float | None = None,
):
"""Simulate one processed frame of vision data, putting one result to NT. Image capture timestamp
overrides :meth:`.PhotonPipelineResult.getTimestampSeconds` for more
@@ -441,44 +442,45 @@ class PhotonCameraSim:
:param result: The pipeline result to submit
:param receiveTimestamp: The (sim) timestamp when this result was read by NT in microseconds. If not passed image capture time is assumed be (current time - latency)
"""
if receiveTimestamp is None:
receiveTimestamp = wpilib.Timer.getFPGATimestamp() * 1e6
receiveTimestamp = int(receiveTimestamp)
if receiveTimestamp_us is None:
receiveTimestamp_us = wpilib.Timer.getFPGATimestamp() * 1e6
receiveTimestamp_us = int(receiveTimestamp_us)
self.ts.latencyMillisEntry.set(result.getLatencyMillis(), receiveTimestamp)
self.ts.latencyMillisEntry.set(result.getLatencyMillis(), receiveTimestamp_us)
newPacket = PhotonPipelineResult.photonStruct.pack(result)
self.ts.rawBytesEntry.set(newPacket.getData(), receiveTimestamp)
self.ts.rawBytesEntry.set(newPacket.getData(), receiveTimestamp_us)
hasTargets = result.hasTargets()
self.ts.hasTargetEntry.set(hasTargets, receiveTimestamp)
self.ts.hasTargetEntry.set(hasTargets, receiveTimestamp_us)
if not hasTargets:
self.ts.targetPitchEntry.set(0.0, receiveTimestamp)
self.ts.targetYawEntry.set(0.0, receiveTimestamp)
self.ts.targetAreaEntry.set(0.0, receiveTimestamp)
self.ts.targetPoseEntry.set(Transform3d(), receiveTimestamp)
self.ts.targetSkewEntry.set(0.0, receiveTimestamp)
self.ts.targetPitchEntry.set(0.0, receiveTimestamp_us)
self.ts.targetYawEntry.set(0.0, receiveTimestamp_us)
self.ts.targetAreaEntry.set(0.0, receiveTimestamp_us)
self.ts.targetPoseEntry.set(Transform3d(), receiveTimestamp_us)
self.ts.targetSkewEntry.set(0.0, receiveTimestamp_us)
else:
bestTarget = result.getBestTarget()
assert bestTarget
self.ts.targetPitchEntry.set(bestTarget.getPitch(), receiveTimestamp)
self.ts.targetYawEntry.set(bestTarget.getYaw(), receiveTimestamp)
self.ts.targetAreaEntry.set(bestTarget.getArea(), receiveTimestamp)
self.ts.targetSkewEntry.set(bestTarget.getSkew(), receiveTimestamp)
self.ts.targetPitchEntry.set(bestTarget.getPitch(), receiveTimestamp_us)
self.ts.targetYawEntry.set(bestTarget.getYaw(), receiveTimestamp_us)
self.ts.targetAreaEntry.set(bestTarget.getArea(), receiveTimestamp_us)
self.ts.targetSkewEntry.set(bestTarget.getSkew(), receiveTimestamp_us)
self.ts.targetPoseEntry.set(
bestTarget.getBestCameraToTarget(), receiveTimestamp
bestTarget.getBestCameraToTarget(), receiveTimestamp_us
)
intrinsics = self.prop.getIntrinsics()
intrinsicsView = intrinsics.flatten().tolist()
self.ts.cameraIntrinsicsPublisher.set(intrinsicsView, receiveTimestamp)
intrinsics = self.prop.getIntrinsics()
intrinsicsView = intrinsics.flatten().tolist()
self.ts.cameraIntrinsicsPublisher.set(intrinsicsView, receiveTimestamp_us)
distortion = self.prop.getDistCoeffs()
distortionView = distortion.flatten().tolist()
self.ts.cameraDistortionPublisher.set(distortionView, receiveTimestamp)
distortion = self.prop.getDistCoeffs()
distortionView = distortion.flatten().tolist()
self.ts.cameraDistortionPublisher.set(distortionView, receiveTimestamp_us)
self.ts.heartbeatPublisher.set(self.heartbeatCounter, receiveTimestamp)
self.ts.heartbeatPublisher.set(self.heartbeatCounter, receiveTimestamp_us)
self.heartbeatCounter += 1
self.ts.subTable.getInstance().flush()
self.ts.subTable.getInstance().flush()

View File

@@ -4,6 +4,7 @@ import typing
import cv2 as cv
import numpy as np
import numpy.typing as npt
from wpimath.geometry import Rotation2d, Rotation3d, Translation3d
from wpimath.units import hertz, seconds
@@ -31,8 +32,8 @@ class SimCameraProperties:
"""Default constructor which is the same as {@link #PERFECT_90DEG}"""
self.resWidth: int = -1
self.resHeight: int = -1
self.camIntrinsics: np.ndarray = np.zeros((3, 3)) # [3,3]
self.distCoeffs: np.ndarray = np.zeros((8, 1)) # [8,1]
self.camIntrinsics: npt.NDArray[np.floating] = np.zeros((3, 3)) # [3,3]
self.distCoeffs: npt.NDArray[np.floating] = np.zeros((8, 1)) # [8,1]
self.avgErrorPx: float = 0.0
self.errorStdDevPx: float = 0.0
self.frameSpeed: seconds = 0.0
@@ -80,7 +81,6 @@ class SimCameraProperties:
newCamIntrinsics: np.ndarray,
newDistCoeffs: np.ndarray,
) -> None:
self.resWidth = width
self.resHeight = height
self.camIntrinsics = newCamIntrinsics
@@ -173,10 +173,10 @@ class SimCameraProperties:
def getAspectRatio(self) -> float:
return 1.0 * self.resWidth / self.resHeight
def getIntrinsics(self) -> np.ndarray:
def getIntrinsics(self) -> npt.NDArray[np.floating]:
return self.camIntrinsics
def getDistCoeffs(self) -> np.ndarray:
def getDistCoeffs(self) -> npt.NDArray[np.floating]:
return self.distCoeffs
def getFPS(self) -> hertz:
@@ -355,7 +355,6 @@ class SimCameraProperties:
# find intersections
for i, normal in enumerate(self.viewplanes):
# // we want to know the value of t when the line intercepts this plane
# // parametrized: v = t * ab + a, where v lies on the plane
# // we can find the projection of a onto the plane normal
@@ -467,7 +466,7 @@ class SimCameraProperties:
def estSecUntilNextFrame(self) -> seconds:
"""
:returns: Estimate how long until the next frame should be processed in milliseconds
:returns: Estimate how long until the next frame should be processed in seconds
"""
# // exceptional processing latency blocks the next frame
return self.frameSpeed + max(0.0, self.estLatency() - self.frameSpeed)

View File

@@ -305,7 +305,7 @@ class VisionSystemSim:
timestampNt = optTimestamp
latency = camSim.prop.estLatency()
# the image capture timestamp in seconds of this result
timestampCapture = timestampNt * 1.0e-6 - latency
timestampCapture = timestampNt - latency
# use camera pose from the image capture timestamp
lateRobotPose = self.getRobotPose(timestampCapture)
@@ -318,7 +318,8 @@ class VisionSystemSim:
# process a PhotonPipelineResult with visible targets
camResult = camSim.process(latency, lateCameraPose, allTargets)
# publish this info to NT at estimated timestamp of receive
camSim.submitProcessedFrame(camResult, timestampNt)
# needs a timestamp in microseconds
camSim.submitProcessedFrame(camResult, timestampNt * 1.0e6)
# display debug results
for tgt in camResult.getTargets():
trf = tgt.getBestCameraToTarget()

View File

@@ -1,9 +1,8 @@
import math
import ntcore as nt
import pytest
from photonlibpy.estimation import TargetModel, VisionEstimation
from photonlibpy.photonCamera import PhotonCamera, setVersionCheckEnabled
from photonlibpy.photonCamera import PhotonCamera
from photonlibpy.simulation import PhotonCameraSim, VisionSystemSim, VisionTargetSim
from robotpy_apriltag import AprilTag, AprilTagFieldLayout
from wpimath.geometry import (
@@ -18,12 +17,6 @@ from wpimath.geometry import (
from wpimath.units import feetToMeters, meters
@pytest.fixture(autouse=True)
def setupCommon() -> None:
nt.NetworkTableInstance.getDefault().startServer()
setVersionCheckEnabled(False)
def test_VisibilityCupidShuffle() -> None:
targetPose = Pose3d(Translation3d(15.98, 0.0, 2.0), Rotation3d(0, 0, math.pi))
@@ -32,6 +25,8 @@ def test_VisibilityCupidShuffle() -> None:
cameraSim = PhotonCameraSim(camera)
visionSysSim.addCamera(cameraSim, Transform3d())
# Set massive FPS so timing isn't an issue
cameraSim.prop.setFPS(1e6)
cameraSim.prop.setCalibrationFromFOV(640, 480, fovDiag=Rotation2d.fromDegrees(80.0))
visionSysSim.addVisionTargets(
@@ -93,6 +88,8 @@ def test_NotVisibleVert1() -> None:
cameraSim = PhotonCameraSim(camera)
visionSysSim.addCamera(cameraSim, Transform3d())
# Set massive FPS so timing isn't an issue
cameraSim.prop.setFPS(1e6)
cameraSim.prop.setCalibrationFromFOV(640, 480, fovDiag=Rotation2d.fromDegrees(80.0))
visionSysSim.addVisionTargets(
@@ -128,6 +125,8 @@ def test_NotVisibleVert2() -> None:
cameraSim = PhotonCameraSim(camera)
visionSysSim.addCamera(cameraSim, robotToCamera)
# Set massive FPS so timing isn't an issue
cameraSim.prop.setFPS(1e6)
cameraSim.prop.setCalibrationFromFOV(
4774, 4774, fovDiag=Rotation2d.fromDegrees(80.0)
)
@@ -156,6 +155,8 @@ def test_NotVisibleTargetSize() -> None:
cameraSim = PhotonCameraSim(camera)
visionSysSim.addCamera(cameraSim, Transform3d())
# Set massive FPS so timing isn't an issue
cameraSim.prop.setFPS(1e6)
cameraSim.prop.setCalibrationFromFOV(640, 480, fovDiag=Rotation2d.fromDegrees(80.0))
cameraSim.setMinTargetAreaPixels(20.0)
visionSysSim.addVisionTargets(
@@ -183,6 +184,8 @@ def test_NotVisibleTooFarLeds() -> None:
cameraSim = PhotonCameraSim(camera)
visionSysSim.addCamera(cameraSim, Transform3d())
# Set massive FPS so timing isn't an issue
cameraSim.prop.setFPS(1e6)
cameraSim.prop.setCalibrationFromFOV(640, 480, fovDiag=Rotation2d.fromDegrees(80.0))
cameraSim.setMinTargetAreaPixels(1.0)
cameraSim.setMaxSightRange(10.0)
@@ -216,6 +219,9 @@ def test_YawAngles(expected_yaw) -> None:
cameraSim = PhotonCameraSim(camera)
visionSysSim.addCamera(cameraSim, Transform3d())
# Set massive FPS so timing isn't an issue
cameraSim.prop.setFPS(1e6)
cameraSim.prop.setCalibrationFromFOV(640, 480, fovDiag=Rotation2d.fromDegrees(80.0))
cameraSim.setMinTargetAreaPixels(0.0)
visionSysSim.addVisionTargets(
@@ -250,6 +256,9 @@ def test_PitchAngles(expected_pitch) -> None:
camera = PhotonCamera("camera")
cameraSim = PhotonCameraSim(camera)
visionSysSim.addCamera(cameraSim, Transform3d())
# Set massive FPS so timing isn't an issue
cameraSim.prop.setFPS(1e6)
cameraSim.prop.setCalibrationFromFOV(
640, 480, fovDiag=Rotation2d.fromDegrees(120.0)
)
@@ -316,8 +325,10 @@ def test_distanceCalc(distParam, pitchParam, heightParam) -> None:
)
camera = PhotonCamera("camera")
cameraSim = PhotonCameraSim(camera)
visionSysSim.addCamera(cameraSim, Transform3d())
# Set massive FPS so timing isn't an issue
cameraSim.prop.setFPS(1e6)
cameraSim.prop.setCalibrationFromFOV(
640, 480, fovDiag=Rotation2d.fromDegrees(160.0)
)
@@ -354,6 +365,9 @@ def test_MultipleTargets() -> None:
camera = PhotonCamera("camera")
cameraSim = PhotonCameraSim(camera)
visionSysSim.addCamera(cameraSim, Transform3d())
# Set massive FPS so timing isn't an issue
cameraSim.prop.setFPS(1e6)
cameraSim.prop.setCalibrationFromFOV(640, 480, fovDiag=Rotation2d.fromDegrees(80.0))
cameraSim.setMinTargetAreaPixels(20.0)
@@ -451,6 +465,9 @@ def test_PoseEstimation() -> None:
camera = PhotonCamera("camera")
cameraSim = PhotonCameraSim(camera)
visionSysSim.addCamera(cameraSim, Transform3d())
# Set massive FPS so timing isn't an issue
cameraSim.prop.setFPS(1e6)
cameraSim.prop.setCalibrationFromFOV(640, 480, fovDiag=Rotation2d.fromDegrees(90.0))
cameraSim.setMinTargetAreaPixels(20.0)
@@ -525,6 +542,9 @@ def test_PoseEstimationRotated() -> None:
camera = PhotonCamera("camera")
cameraSim = PhotonCameraSim(camera)
visionSysSim.addCamera(cameraSim, robotToCamera)
# Set massive FPS so timing isn't an issue
cameraSim.prop.setFPS(1e6)
cameraSim.prop.setCalibrationFromFOV(640, 480, fovDiag=Rotation2d.fromDegrees(90.0))
cameraSim.setMinTargetAreaPixels(20.0)

View File

@@ -31,6 +31,7 @@ import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.configuration.NeuralNetworkModelManager;
import org.photonvision.common.dataflow.networktables.NetworkTablesManager;
import org.photonvision.common.hardware.HardwareManager;
import org.photonvision.common.hardware.OsImageVersion;
import org.photonvision.common.hardware.PiVersion;
import org.photonvision.common.hardware.Platform;
import org.photonvision.common.logging.KernelLogLogger;
@@ -353,10 +354,14 @@ public class Main {
logger.info(
"Starting PhotonVision version "
+ PhotonVersion.versionString
+ " on "
+ " on platform "
+ Platform.getPlatformName()
+ (Platform.isRaspberryPi() ? (" (Pi " + PiVersion.getPiVersion() + ")") : ""));
if (OsImageVersion.IMAGE_VERSION.isPresent()) {
logger.info("PhotonVision image version: " + OsImageVersion.IMAGE_VERSION.get());
}
try {
if (!handleArgs(args)) {
System.exit(1);

View File

@@ -125,7 +125,9 @@ void Robot::SimulationPeriodic() {
units::ampere_t totalCurrent = drivetrain.GetCurrentDraw();
units::volt_t loadedBattVolts =
frc::sim::BatterySim::Calculate({totalCurrent});
frc::sim::RoboRioSim::SetVInVoltage(loadedBattVolts);
// Using max(0.1, voltage) here isn't a *physically correct* solution,
// but it avoids problems with battery voltage measuring 0.
frc::sim::RoboRioSim::SetVInVoltage(units::math::max(0.1_V, loadedBattVolts));
}
#ifndef RUNNING_FRC_TESTS

View File

@@ -115,7 +115,9 @@ void Robot::SimulationPeriodic() {
units::ampere_t totalCurrent = drivetrain.GetCurrentDraw();
units::volt_t loadedBattVolts =
frc::sim::BatterySim::Calculate({totalCurrent});
frc::sim::RoboRioSim::SetVInVoltage(loadedBattVolts);
// Using max(0.1, voltage) here isn't a *physically correct* solution,
// but it avoids problems with battery voltage measuring 0.
frc::sim::RoboRioSim::SetVInVoltage(units::math::max(0.1_V, loadedBattVolts));
}
#ifndef RUNNING_FRC_TESTS

View File

@@ -105,7 +105,10 @@ void Robot::SimulationPeriodic() {
units::ampere_t totalCurrent = drivetrain.GetCurrentDraw();
units::volt_t loadedBattVolts =
frc::sim::BatterySim::Calculate({totalCurrent});
frc::sim::RoboRioSim::SetVInVoltage(loadedBattVolts);
// Using max(0.1, voltage) here isn't a *physically correct* solution,
// but it avoids problems with battery voltage measuring 0.
frc::sim::RoboRioSim::SetVInVoltage(units::math::max(0.1_V, loadedBattVolts));
}
#ifndef RUNNING_FRC_TESTS

View File

@@ -147,8 +147,12 @@ public class Robot extends TimedRobot {
debugField.getObject("EstimatedRobotModules").setPoses(drivetrain.getModulePoses());
// Calculate battery voltage sag due to current draw
RoboRioSim.setVInVoltage(
BatterySim.calculateDefaultBatteryLoadedVoltage(drivetrain.getCurrentDraw()));
var batteryVoltage =
BatterySim.calculateDefaultBatteryLoadedVoltage(drivetrain.getCurrentDraw());
// Using max(0.1, voltage) here isn't a *physically correct* solution,
// but it avoids problems with battery voltage measuring 0.
RoboRioSim.setVInVoltage(Math.max(0.1, batteryVoltage));
}
public void resetPose() {

View File

@@ -129,8 +129,12 @@ public class Robot extends TimedRobot {
debugField.getObject("EstimatedRobotModules").setPoses(drivetrain.getModulePoses());
// Calculate battery voltage sag due to current draw
RoboRioSim.setVInVoltage(
BatterySim.calculateDefaultBatteryLoadedVoltage(drivetrain.getCurrentDraw()));
var batteryVoltage =
BatterySim.calculateDefaultBatteryLoadedVoltage(drivetrain.getCurrentDraw());
// Using max(0.1, voltage) here isn't a *physically correct* solution,
// but it avoids problems with battery voltage measuring 0.
RoboRioSim.setVInVoltage(Math.max(0.1, batteryVoltage));
}
public void resetPose() {

View File

@@ -127,8 +127,12 @@ public class Robot extends TimedRobot {
gpLauncher.simulationPeriodic();
// Calculate battery voltage sag due to current draw
RoboRioSim.setVInVoltage(
BatterySim.calculateDefaultBatteryLoadedVoltage(drivetrain.getCurrentDraw()));
var batteryVoltage =
BatterySim.calculateDefaultBatteryLoadedVoltage(drivetrain.getCurrentDraw());
// Using max(0.1, voltage) here isn't a *physically correct* solution,
// but it avoids problems with battery voltage measuring 0.
RoboRioSim.setVInVoltage(Math.max(0.1, batteryVoltage));
}
public void resetPose() {

View File

@@ -110,7 +110,7 @@ ext.createComponentZipTasks = { components, names, base, type, project, func ->
description = 'Creates component archive for platform ' + key
destinationDirectory = outputsFolder
archiveClassifier = key
archiveBaseName = '_M_' + base
archiveBaseName = base
duplicatesStrategy = 'exclude'
from(licenseFile) {

View File

@@ -7,9 +7,6 @@ def baseArtifactId = nativeName
def artifactGroupId = 'org.photonvision'
def zipBaseName = "_GROUP_org_photonvision_${baseArtifactId}_ID_${baseArtifactId}-cpp_CLS"
def jniBaseName = "_GROUP_edu_wpi_first_${nativeName}_ID_${nativeName}-jni_CLS"
def jniCvStaticBaseName = "_GROUP_edu_wpi_first_${nativeName}_ID_${nativeName}-jnicvstatic_CLS"
def licenseFile = ext.licenseFile
// Quick hack to make this name visible to photon-lib for combined
ext.zipBaseName = zipBaseName
@@ -80,29 +77,6 @@ model {
"${nativeName}JNI"
], zipBaseName, Zip, project, includeStandardZipFormat)
// From https://github.com/wpilibsuite/allwpilib/blob/1c220ebc607daa8da7d983b8f17bc40323633cb2/shared/jni/publish.gradle#L80C9-L100C11
def jniTaskList = createComponentZipTasks($.components, ["${nativeName}JNI"], jniBaseName, Jar, project, { task, value ->
value.each { binary ->
if (binary.buildable) {
if (binary instanceof SharedLibraryBinarySpec) {
task.dependsOn binary.tasks.link
def hashFile = new File(binary.sharedLibraryFile.parentFile.absolutePath, "${binary.component.baseName}.hash")
task.outputs.file(hashFile)
task.inputs.file(binary.sharedLibraryFile)
task.from(hashFile) {
into nativeUtils.getPlatformPath(binary)
}
task.doFirst {
hashFile.text = MessageDigest.getInstance("MD5").digest(binary.sharedLibraryFile.bytes).encodeHex().toString()
}
task.from(binary.sharedLibraryFile) {
into nativeUtils.getPlatformPath(binary)
}
}
}
}
})
publications {
cpp(MavenPublication) {
cppTaskList.each {
@@ -115,15 +89,6 @@ model {
groupId artifactGroupId
version pubVersion
}
jni(MavenPublication) {
jniTaskList.each {
artifact it
}
artifactId = "${baseArtifactId}-jni"
groupId artifactGroupId
version pubVersion
}
}
repositories {