mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-26 01:51:40 +00:00
Compare commits
10 Commits
v2025.2.1-
...
v2025.3.1-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d4024b8c8 | ||
|
|
f6736fc730 | ||
|
|
889c73ec91 | ||
|
|
8fe53f3b84 | ||
|
|
a3304818d2 | ||
|
|
4057205583 | ||
|
|
7f1936d609 | ||
|
|
f41a472308 | ||
|
|
9589967808 | ||
|
|
311846dc26 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -150,3 +150,4 @@ photon-server/src/main/resources/web/*
|
||||
venv
|
||||
.venv/*
|
||||
.venv
|
||||
networktables.json
|
||||
|
||||
@@ -42,6 +42,7 @@ Note that these are case sensitive!
|
||||
* linuxathena
|
||||
- `-PtgtIP`: Specifies where `./gradlew deploy` should try to copy the fat JAR to
|
||||
- `-Pprofile`: enables JVM profiling
|
||||
- `-PwithSanitizers`: On Linux, enables `-fsanitize=address,undefined,leak`
|
||||
|
||||
If you're cross-compiling, you'll need the wpilib toolchain installed. This can be done via Gradle: for example `./gradlew installArm64Toolchain` or `./gradlew installRoboRioToolchain`
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ Using a JDK other than JDK17 will cause issues when running PhotonVision and is
|
||||
Go to the [GitHub releases page](https://github.com/PhotonVision/photonvision/releases) and download the relevant .jar file for your coprocessor.
|
||||
|
||||
:::{note}
|
||||
If you have an M1/M2 Mac, download the macarm64.jar file.
|
||||
If you have an M Series Mac, download the macarm64.jar file.
|
||||
|
||||
If you have an Intel based Mac, download the macx64.jar file.
|
||||
:::
|
||||
|
||||
@@ -6,6 +6,10 @@ PhotonVision can combine AprilTag detections from multiple simultaneously observ
|
||||
MultiTag requires an accurate field layout JSON to be uploaded! Differences between this layout and the tags' physical location will drive error in the estimated pose output.
|
||||
:::
|
||||
|
||||
:::{warning}
|
||||
For the 2025 Reefscape Season, there are two different field layouts. The first is the [welded field layout](https://github.com/wpilibsuite/allwpilib/blob/main/apriltag/src/main/native/resources/edu/wpi/first/apriltag/2025-reefscape-welded.json), which photonvision ships with. The second is the [Andymark field layout](https://github.com/wpilibsuite/allwpilib/blob/main/apriltag/src/main/native/resources/edu/wpi/first/apriltag/2025-reefscape-andymark.json). It is very important to ensure that you use the correct field layout, both in the [PhotonPoseEstimator](https://docs.photonvision.org/en/latest/docs/programming/photonlib/robot-pose-estimator.html#apriltags-and-photonposeestimator) and on the [coprocessor](https://docs.photonvision.org/en/latest/docs/apriltag-pipelines/multitag.html#updating-the-field-layout).
|
||||
:::
|
||||
|
||||
## Enabling MultiTag
|
||||
|
||||
Ensure that your camera is calibrated and 3D mode is enabled. Navigate to the Output tab and enable "Do Multi-Target Estimation". This enables MultiTag to use the uploaded field layout JSON to calculate your camera's pose in the field. This 3D transform will be shown as an additional table in the "targets" tab, along with the IDs of AprilTags used to compute this transform.
|
||||
@@ -51,7 +55,7 @@ The returned field to camera transform is a transform from the fixed field origi
|
||||
|
||||
## Updating the Field Layout
|
||||
|
||||
PhotonVision ships by default with the [2025 field layout JSON](https://github.com/wpilibsuite/allwpilib/blob/main/apriltag/src/main/native/resources/edu/wpi/first/apriltag/2025-reefscape.json). The layout can be inspected by navigating to the settings tab and scrolling down to the "AprilTag Field Layout" card, as shown below.
|
||||
PhotonVision ships by default with the [2025 welded field layout JSON](https://github.com/wpilibsuite/allwpilib/blob/main/apriltag/src/main/native/resources/edu/wpi/first/apriltag/2025-reefscape-welded.json). The layout can be inspected by navigating to the settings tab and scrolling down to the "AprilTag Field Layout" card, as shown below.
|
||||
|
||||
```{image} images/field-layout.png
|
||||
:alt: The currently saved field layout in the Photon UI
|
||||
|
||||
@@ -51,6 +51,10 @@ We'll next select a resolution to calibrate and populate our pattern spacing, ma
|
||||
:::{warning} Old OpenCV Pattern selector. This should be used in the case that the calibration image is generated from a version of OpenCV before version 4.6.0. This would include targets created by calib.io. If this selector is not set correctly the calibration will be completely invalid. For more info view [this GitHub issue](https://github.com/opencv/opencv_contrib/issues/3291).
|
||||
:::
|
||||
|
||||
:::{note}
|
||||
If you have a [calib.io](https://calib.io/) CharuCo Target you will have to enter the paramaters of your target. For example if your taget says "9x12 | Chceker Size: 30 mm | Marker Size: 22 mm | Dictionary: AruCo DICT 5x5", you would have to set the board type to Dict_5x5_1000, the pattern spacing to 1.1811 in (30 mm converted to inches), the marker size 0.866142 in (22 mm converted to inches), the board width to 12 and the board height to 9. If you chose the wrong tag family the baord wont be detected duting calibration. If you swap the width and height your calibration will have a very high error.
|
||||
:::
|
||||
|
||||
### 4. Take at calibration images from various angles.
|
||||
|
||||
Now, we'll capture images of our board from various angles. It's important to check that the board overlay matches the board in your image. The further the overdrawn points are from the true position of the chessboard corners, the less accurate the final calibration will be. We'll want to capture enough images to cover the whole camera's FOV (with a minimum of 12). Once we've got our images, we'll click "Finish calibration" and wait for the calibration process to complete. If all goes well, the mean error and FOVs will be shown in the table on the right. The FOV should be close to the camera's specified FOV (usually found in a datasheet) usually within + or - 10 degrees. The mean error should also be low, usually less than 1 pixel.
|
||||
|
||||
@@ -44,11 +44,11 @@ Before beginning, it is necessary to install the [rknn-toolkit2](https://github.
|
||||
## Uploading Custom Models
|
||||
|
||||
:::{warning}
|
||||
PhotonVision currently ONLY supports 640x640 Ultralytics YOLOv5, YOLOv8, and YOLO11 models trained and converted to `.rknn` format for RK3588 CPUs! Other models require different post-processing code and will NOT work. The model conversion process is also highly particular. Proceed with care.
|
||||
PhotonVision currently ONLY supports 640x640 Ultralytics YOLOv5, YOLOv8, and YOLOv11 models trained and converted to `.rknn` format for RK3588 CPUs! Other models require different post-processing code and will NOT work. The model conversion process is also highly particular. Proceed with care.
|
||||
:::
|
||||
|
||||
In the settings, under `Device Control`, there's an option to upload a new object detection model. Naming convention
|
||||
should be `name-verticalResolution-horizontalResolution-modelType`. The
|
||||
should be `name-verticalResolution-horizontalResolution-yolovXXX`. The
|
||||
`name` should only include alphanumeric characters, periods, and underscores. Additionally, the labels
|
||||
file ought to have the same name as the RKNN file, with `-labels` appended to the end. For
|
||||
example, if the RKNN file is named `Algae_1.03.2025-640-640-yolov5s.rknn`, the labels file should be
|
||||
|
||||
@@ -32,7 +32,7 @@ The API documentation can be found in here: [Java](https://github.wpilib.org/all
|
||||
|
||||
## Creating a `PhotonPoseEstimator`
|
||||
|
||||
The PhotonPoseEstimator has a constructor that takes an `AprilTagFieldLayout` (see above), `PoseStrategy`, `PhotonCamera`, and `Transform3d`. `PoseStrategy` has six possible values:
|
||||
The PhotonPoseEstimator has a constructor that takes an `AprilTagFieldLayout` (see above), `PoseStrategy`, `PhotonCamera`, and `Transform3d`. `PoseStrategy` has nine possible values:
|
||||
|
||||
- MULTI_TAG_PNP_ON_COPROCESSOR
|
||||
- Calculates a new robot position estimate by combining all visible tag corners. Recommended for all teams as it will be the most accurate.
|
||||
@@ -155,3 +155,7 @@ Updates the stored reference pose when using the CLOSEST_TO_REFERENCE_POSE strat
|
||||
### `setLastPose(Pose3d lastPose)`
|
||||
|
||||
Update the stored last pose. Useful for setting the initial estimate when using the CLOSEST_TO_LAST_POSE strategy.
|
||||
|
||||
### `addHeadingData(double timestampSeconds, Rotation2d heading)`
|
||||
|
||||
Adds robot heading data to be stored in buffer. Must be called periodically with a proper timestamp for the PNP_DISTANCE_TRIG_SOLVE and CONSTRAINED_SOLVEPNP strategies
|
||||
|
||||
@@ -91,3 +91,7 @@ The address in the code above (`photonvision.local`) is the hostname of the copr
|
||||
## Camera Stream Ports
|
||||
|
||||
The camera streams start at 1181 with two ports for each camera (ex. 1181 and 1182 for camera one, 1183 and 1184 for camera two, etc.). The easiest way to identify the port of the camera that you want is by double clicking on the stream, which opens it in a separate page. The port will be listed below the stream.
|
||||
|
||||
:::{warning}
|
||||
If your camera stream isn't sent to the same port as it's originally found on, its stream will not be visible in the UI.
|
||||
:::
|
||||
|
||||
@@ -88,12 +88,12 @@ const supportedModels = computed(() => {
|
||||
<v-card-title>Import New Object Detection Model</v-card-title>
|
||||
<v-card-text>
|
||||
Upload a new object detection model to this device that can be used in a pipeline. Naming convention
|
||||
should be <code>name-verticalResolution-horizontalResolution-modelType</code>. The
|
||||
should be <code>name-verticalResolution-horizontalResolution-yolovXXX</code>. The
|
||||
<code>name</code> should only include alphanumeric characters, periods, and underscores. Additionally,
|
||||
the labels file ought to have the same name as the RKNN file, with <code>-labels</code> appended to the
|
||||
end. For example, if the RKNN file is named <code>note-640-640-yolov5s.rknn</code>, the labels file
|
||||
should be named <code>note-640-640-yolov5s-labels.txt</code>. Note that ONLY 640x640 YOLOv5 & YOLOv8
|
||||
models trained and converted to `.rknn` format for RK3588 CPUs are currently supported!
|
||||
should be named <code>note-640-640-yolov5s-labels.txt</code>. Note that ONLY 640x640 YOLOv5, YOLOv8, and
|
||||
YOLOv11 models trained and converted to `.rknn` format for RK3588 CPUs are currently supported!
|
||||
<v-row class="mt-6 ml-4 mr-8">
|
||||
<v-file-input v-model="importRKNNFile" label="RKNN File" accept=".rknn" />
|
||||
</v-row>
|
||||
|
||||
@@ -41,6 +41,8 @@ import edu.wpi.first.networktables.NetworkTable;
|
||||
import edu.wpi.first.networktables.NetworkTableInstance;
|
||||
import edu.wpi.first.networktables.PubSubOption;
|
||||
import edu.wpi.first.networktables.StringSubscriber;
|
||||
import edu.wpi.first.wpilibj.Alert;
|
||||
import edu.wpi.first.wpilibj.Alert.AlertType;
|
||||
import edu.wpi.first.wpilibj.DriverStation;
|
||||
import edu.wpi.first.wpilibj.Timer;
|
||||
import edu.wpi.first.wpilibj.util.WPILibVersion;
|
||||
@@ -59,6 +61,7 @@ import org.photonvision.timesync.TimeSyncSingleton;
|
||||
public class PhotonCamera implements AutoCloseable {
|
||||
private static int InstanceCount = 0;
|
||||
public static final String kTableName = "photonvision";
|
||||
private static final String PHOTON_ALERT_GROUP = "PhotonAlerts";
|
||||
|
||||
private final NetworkTable cameraTable;
|
||||
PacketSubscriber<PhotonPipelineResult> resultSubscriber;
|
||||
@@ -68,7 +71,7 @@ public class PhotonCamera implements AutoCloseable {
|
||||
IntegerEntry inputSaveImgEntry, outputSaveImgEntry;
|
||||
IntegerPublisher pipelineIndexRequest, ledModeRequest;
|
||||
IntegerSubscriber pipelineIndexState, ledModeState;
|
||||
IntegerSubscriber heartbeatEntry;
|
||||
IntegerSubscriber heartbeatSubscriber;
|
||||
DoubleArraySubscriber cameraIntrinsicsSubscriber;
|
||||
DoubleArraySubscriber cameraDistortionSubscriber;
|
||||
MultiSubscriber topicNameSubscriber;
|
||||
@@ -106,6 +109,9 @@ public class PhotonCamera implements AutoCloseable {
|
||||
double prevTimeSyncWarnTime = 0;
|
||||
private static final double WARN_DEBOUNCE_SEC = 5;
|
||||
|
||||
private final Alert disconnectAlert;
|
||||
private final Alert timesyncAlert;
|
||||
|
||||
public static void setVersionCheckEnabled(boolean enabled) {
|
||||
VERSION_CHECK_ENABLED = enabled;
|
||||
}
|
||||
@@ -120,6 +126,10 @@ public class PhotonCamera implements AutoCloseable {
|
||||
*/
|
||||
public PhotonCamera(NetworkTableInstance instance, String cameraName) {
|
||||
name = cameraName;
|
||||
disconnectAlert =
|
||||
new Alert(
|
||||
PHOTON_ALERT_GROUP, "PhotonCamera '" + name + "' is disconnected.", AlertType.kWarning);
|
||||
timesyncAlert = new Alert(PHOTON_ALERT_GROUP, "", AlertType.kWarning);
|
||||
rootPhotonTable = instance.getTable(kTableName);
|
||||
this.cameraTable = rootPhotonTable.getSubTable(cameraName);
|
||||
path = cameraTable.getPath();
|
||||
@@ -139,7 +149,7 @@ public class PhotonCamera implements AutoCloseable {
|
||||
outputSaveImgEntry = cameraTable.getIntegerTopic("outputSaveImgCmd").getEntry(0);
|
||||
pipelineIndexRequest = cameraTable.getIntegerTopic("pipelineIndexRequest").publish();
|
||||
pipelineIndexState = cameraTable.getIntegerTopic("pipelineIndexState").subscribe(0);
|
||||
heartbeatEntry = cameraTable.getIntegerTopic("heartbeat").subscribe(-1);
|
||||
heartbeatSubscriber = cameraTable.getIntegerTopic("heartbeat").subscribe(-1);
|
||||
cameraIntrinsicsSubscriber =
|
||||
cameraTable.getDoubleArrayTopic("cameraIntrinsics").subscribe(null);
|
||||
cameraDistortionSubscriber =
|
||||
@@ -249,6 +259,7 @@ public class PhotonCamera implements AutoCloseable {
|
||||
*/
|
||||
public List<PhotonPipelineResult> getAllUnreadResults() {
|
||||
verifyVersion();
|
||||
updateDisconnectAlert();
|
||||
|
||||
List<PhotonPipelineResult> ret = new ArrayList<>();
|
||||
|
||||
@@ -274,6 +285,7 @@ public class PhotonCamera implements AutoCloseable {
|
||||
@Deprecated(since = "2024", forRemoval = true)
|
||||
public PhotonPipelineResult getLatestResult() {
|
||||
verifyVersion();
|
||||
updateDisconnectAlert();
|
||||
|
||||
// Grab the latest result. We don't care about the timestamp from NT - the metadata header has
|
||||
// this, latency compensated by the Time Sync Client
|
||||
@@ -288,22 +300,34 @@ public class PhotonCamera implements AutoCloseable {
|
||||
return result;
|
||||
}
|
||||
|
||||
private void updateDisconnectAlert() {
|
||||
disconnectAlert.set(!isConnected());
|
||||
}
|
||||
|
||||
private void checkTimeSyncOrWarn(PhotonPipelineResult result) {
|
||||
if (result.metadata.timeSinceLastPong > 5L * 1000000L) {
|
||||
String warningText =
|
||||
"PhotonVision coprocessor at path "
|
||||
+ path
|
||||
+ " is not connected to the TimeSyncServer? It's been "
|
||||
+ String.format("%.2f", result.metadata.timeSinceLastPong / 1e6)
|
||||
+ "s since the coprocessor last heard a pong.";
|
||||
|
||||
timesyncAlert.setText(warningText);
|
||||
timesyncAlert.set(true);
|
||||
|
||||
if (Timer.getFPGATimestamp() > (prevTimeSyncWarnTime + WARN_DEBOUNCE_SEC)) {
|
||||
prevTimeSyncWarnTime = Timer.getFPGATimestamp();
|
||||
|
||||
DriverStation.reportWarning(
|
||||
"PhotonVision coprocessor at path "
|
||||
+ path
|
||||
+ " is not connected to the TimeSyncServer? It's been "
|
||||
+ String.format("%.2f", result.metadata.timeSinceLastPong / 1e6)
|
||||
+ "s since the coprocessor last heard a pong.\n\nCheck /photonvision/.timesync/{COPROCESSOR_HOSTNAME} for more information.",
|
||||
warningText
|
||||
+ "\n\nCheck /photonvision/.timesync/{COPROCESSOR_HOSTNAME} for more information.",
|
||||
false);
|
||||
}
|
||||
} else {
|
||||
// Got a valid packet, reset the last time
|
||||
prevTimeSyncWarnTime = 0;
|
||||
timesyncAlert.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,9 +428,14 @@ public class PhotonCamera implements AutoCloseable {
|
||||
* @return True if the camera is actively sending frame data, false otherwise.
|
||||
*/
|
||||
public boolean isConnected() {
|
||||
var curHeartbeat = heartbeatEntry.get();
|
||||
var curHeartbeat = heartbeatSubscriber.get();
|
||||
var now = Timer.getFPGATimestamp();
|
||||
|
||||
if (curHeartbeat < 0) {
|
||||
// we have never heard from the camera
|
||||
return false;
|
||||
}
|
||||
|
||||
if (curHeartbeat != prevHeartbeatValue) {
|
||||
// New heartbeat value from the coprocessor
|
||||
prevHeartbeatChangeTime = now;
|
||||
@@ -455,7 +484,7 @@ public class PhotonCamera implements AutoCloseable {
|
||||
|
||||
// Heartbeat entry is assumed to always be present. If it's not present, we
|
||||
// assume that a camera with that name was never connected in the first place.
|
||||
if (!heartbeatEntry.exists()) {
|
||||
if (!heartbeatSubscriber.exists()) {
|
||||
var cameraNames = getTablesThatLookLikePhotonCameras();
|
||||
if (cameraNames.isEmpty()) {
|
||||
DriverStation.reportError(
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
package org.photonvision;
|
||||
|
||||
import edu.wpi.first.apriltag.AprilTagFieldLayout;
|
||||
import edu.wpi.first.cscore.OpenCvLoader;
|
||||
import edu.wpi.first.hal.FRCNetComm.tResourceType;
|
||||
import edu.wpi.first.hal.HAL;
|
||||
import edu.wpi.first.math.Matrix;
|
||||
@@ -159,6 +160,11 @@ public class PhotonPoseEstimator {
|
||||
this.primaryStrategy = strategy;
|
||||
this.robotToCamera = robotToCamera;
|
||||
|
||||
if (strategy == PoseStrategy.MULTI_TAG_PNP_ON_RIO
|
||||
|| strategy == PoseStrategy.CONSTRAINED_SOLVEPNP) {
|
||||
OpenCvLoader.forceStaticLoad();
|
||||
}
|
||||
|
||||
HAL.report(tResourceType.kResourceType_PhotonPoseEstimator, InstanceCount);
|
||||
InstanceCount++;
|
||||
}
|
||||
@@ -231,6 +237,11 @@ public class PhotonPoseEstimator {
|
||||
*/
|
||||
public void setPrimaryStrategy(PoseStrategy strategy) {
|
||||
checkUpdate(this.primaryStrategy, strategy);
|
||||
|
||||
if (strategy == PoseStrategy.MULTI_TAG_PNP_ON_RIO
|
||||
|| strategy == PoseStrategy.CONSTRAINED_SOLVEPNP) {
|
||||
OpenCvLoader.forceStaticLoad();
|
||||
}
|
||||
this.primaryStrategy = strategy;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import edu.wpi.first.apriltag.AprilTagFieldLayout;
|
||||
import edu.wpi.first.apriltag.AprilTagFields;
|
||||
import edu.wpi.first.cameraserver.CameraServer;
|
||||
import edu.wpi.first.cscore.CvSource;
|
||||
import edu.wpi.first.cscore.OpenCvLoader;
|
||||
import edu.wpi.first.cscore.VideoSource.ConnectionStrategy;
|
||||
import edu.wpi.first.math.MathUtil;
|
||||
import edu.wpi.first.math.Pair;
|
||||
@@ -94,7 +95,7 @@ public class PhotonCameraSim implements AutoCloseable {
|
||||
private boolean videoSimProcEnabled = true;
|
||||
|
||||
static {
|
||||
OpenCVHelp.forceLoadOpenCV();
|
||||
OpenCvLoader.forceStaticLoad();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -26,6 +26,7 @@ package org.photonvision.simulation;
|
||||
|
||||
import edu.wpi.first.apriltag.AprilTag;
|
||||
import edu.wpi.first.cscore.CvSource;
|
||||
import edu.wpi.first.cscore.OpenCvLoader;
|
||||
import edu.wpi.first.math.geometry.Pose3d;
|
||||
import edu.wpi.first.math.geometry.Translation3d;
|
||||
import edu.wpi.first.math.util.Units;
|
||||
@@ -62,7 +63,7 @@ public class VideoSimUtil {
|
||||
private static double fieldWidth = 8.0137;
|
||||
|
||||
static {
|
||||
OpenCVHelp.forceLoadOpenCV();
|
||||
OpenCvLoader.forceStaticLoad();
|
||||
|
||||
// create Mats of 10x10 apriltag images
|
||||
for (int i = 0; i < VideoSimUtil.kNumTags36h11; i++) {
|
||||
|
||||
@@ -44,6 +44,9 @@
|
||||
#include "opencv2/core/utility.hpp"
|
||||
#include "photon/dataflow/structures/Packet.h"
|
||||
|
||||
static constexpr units::second_t WARN_DEBOUNCE_SEC = 5_s;
|
||||
static constexpr units::second_t HEARTBEAT_DEBOUNCE_SEC = 500_ms;
|
||||
|
||||
inline void verifyDependencies() {
|
||||
if (!(std::string_view{GetWPILibVersion()} ==
|
||||
std::string_view{photon::PhotonVersion::wpilibTargetVersion})) {
|
||||
@@ -137,6 +140,7 @@ namespace photon {
|
||||
|
||||
constexpr const units::second_t VERSION_CHECK_INTERVAL = 5_s;
|
||||
static const std::vector<std::string_view> PHOTON_PREFIX = {"/photonvision/"};
|
||||
static const std::string PHOTON_ALERT_GROUP{"PhotonAlerts"};
|
||||
bool PhotonCamera::VERSION_CHECK_ENABLED = true;
|
||||
|
||||
void PhotonCamera::SetVersionCheckEnabled(bool enabled) {
|
||||
@@ -179,9 +183,16 @@ PhotonCamera::PhotonCamera(nt::NetworkTableInstance instance,
|
||||
rootTable->GetBooleanTopic("driverMode").Subscribe(false)),
|
||||
driverModePublisher(
|
||||
rootTable->GetBooleanTopic("driverModeRequest").Publish()),
|
||||
heartbeatSubscriber(
|
||||
rootTable->GetIntegerTopic("heartbeat").Subscribe(-1)),
|
||||
topicNameSubscriber(instance, PHOTON_PREFIX, {.topicsOnly = true}),
|
||||
path(rootTable->GetPath()),
|
||||
cameraName(cameraName) {
|
||||
cameraName(cameraName),
|
||||
disconnectAlert(PHOTON_ALERT_GROUP,
|
||||
std::string{"PhotonCamera '"} + std::string{cameraName} +
|
||||
"' is disconnected.",
|
||||
frc::Alert::AlertType::kWarning),
|
||||
timesyncAlert(PHOTON_ALERT_GROUP, "", frc::Alert::AlertType::kWarning) {
|
||||
verifyDependencies();
|
||||
HAL_Report(HALUsageReporting::kResourceType_PhotonCamera, InstanceCount);
|
||||
InstanceCount++;
|
||||
@@ -217,6 +228,8 @@ PhotonPipelineResult PhotonCamera::GetLatestResult() {
|
||||
// Create the new result;
|
||||
PhotonPipelineResult result = packet.Unpack<PhotonPipelineResult>();
|
||||
|
||||
CheckTimeSyncOrWarn(result);
|
||||
|
||||
result.SetReceiveTimestamp(now);
|
||||
|
||||
return result;
|
||||
@@ -229,6 +242,7 @@ std::vector<PhotonPipelineResult> PhotonCamera::GetAllUnreadResults() {
|
||||
|
||||
// Prints warning if not connected
|
||||
VerifyVersion();
|
||||
UpdateDisconnectAlert();
|
||||
|
||||
const auto changes = rawBytesEntry.ReadQueue();
|
||||
|
||||
@@ -247,6 +261,8 @@ std::vector<PhotonPipelineResult> PhotonCamera::GetAllUnreadResults() {
|
||||
photon::Packet packet{value.value};
|
||||
auto result = packet.Unpack<PhotonPipelineResult>();
|
||||
|
||||
CheckTimeSyncOrWarn(result);
|
||||
|
||||
// TODO: NT4 timestamps are still not to be trusted. But it's the best we
|
||||
// can do until we can make time sync more reliable.
|
||||
result.SetReceiveTimestamp(units::microsecond_t(value.time) -
|
||||
@@ -258,6 +274,37 @@ std::vector<PhotonPipelineResult> PhotonCamera::GetAllUnreadResults() {
|
||||
return ret;
|
||||
}
|
||||
|
||||
void PhotonCamera::UpdateDisconnectAlert() {
|
||||
disconnectAlert.Set(!IsConnected());
|
||||
}
|
||||
|
||||
void PhotonCamera::CheckTimeSyncOrWarn(photon::PhotonPipelineResult& result) {
|
||||
if (result.metadata.timeSinceLastPong > 5L * 1000000L) {
|
||||
std::string warningText =
|
||||
"PhotonVision coprocessor at path " + path +
|
||||
" is not connected to the TimeSyncServer? It's been " +
|
||||
std::to_string(result.metadata.timeSinceLastPong / 1e6) +
|
||||
"s since the coprocessor last heard a pong.";
|
||||
|
||||
timesyncAlert.SetText(warningText);
|
||||
timesyncAlert.Set(true);
|
||||
|
||||
if (frc::Timer::GetFPGATimestamp() <
|
||||
(prevTimeSyncWarnTime + WARN_DEBOUNCE_SEC)) {
|
||||
prevTimeSyncWarnTime = frc::Timer::GetFPGATimestamp();
|
||||
|
||||
FRC_ReportWarning(
|
||||
warningText +
|
||||
"\n\nCheck /photonvision/.timesync/{{COPROCESSOR_HOSTNAME}} for more "
|
||||
"information.");
|
||||
}
|
||||
} else {
|
||||
// Got a valid packet, reset the last time
|
||||
prevTimeSyncWarnTime = 0_s;
|
||||
timesyncAlert.Set(false);
|
||||
}
|
||||
}
|
||||
|
||||
void PhotonCamera::SetDriverMode(bool driverMode) {
|
||||
driverModePublisher.Set(driverMode);
|
||||
}
|
||||
@@ -290,6 +337,24 @@ const std::string_view PhotonCamera::GetCameraName() const {
|
||||
return cameraName;
|
||||
}
|
||||
|
||||
bool PhotonCamera::IsConnected() {
|
||||
auto currentHeartbeat = heartbeatSubscriber.Get();
|
||||
auto now = frc::Timer::GetFPGATimestamp();
|
||||
|
||||
if (currentHeartbeat < 0) {
|
||||
// we have never heard from the camera
|
||||
return false;
|
||||
}
|
||||
|
||||
if (currentHeartbeat != prevHeartbeatValue) {
|
||||
// New heartbeat value from the coprocessor
|
||||
prevHeartbeatChangeTime = now;
|
||||
prevHeartbeatValue = currentHeartbeat;
|
||||
}
|
||||
|
||||
return (now - prevHeartbeatChangeTime) < HEARTBEAT_DEBOUNCE_SEC;
|
||||
}
|
||||
|
||||
std::optional<PhotonCamera::CameraMatrix> PhotonCamera::GetCameraMatrix() {
|
||||
auto camCoeffs = cameraIntrinsicsSubscriber.Get();
|
||||
if (camCoeffs.size() == 9) {
|
||||
|
||||
@@ -335,7 +335,6 @@ PhotonPipelineResult PhotonCameraSim::Process(
|
||||
}
|
||||
}
|
||||
|
||||
heartbeatCounter++;
|
||||
return PhotonPipelineResult{
|
||||
PhotonPipelineMetadata{heartbeatCounter, 0,
|
||||
units::microsecond_t{latency}.to<int64_t>(),
|
||||
@@ -388,6 +387,7 @@ void PhotonCameraSim::SubmitProcessedFrame(const PhotonPipelineResult& result,
|
||||
ts.cameraDistortionPublisher.Set(distortionView, ReceiveTimestamp);
|
||||
|
||||
ts.heartbeatPublisher.Set(heartbeatCounter, ReceiveTimestamp);
|
||||
heartbeatCounter++;
|
||||
|
||||
ts.subTable->GetInstance().Flush();
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <frc/Alert.h>
|
||||
#include <networktables/BooleanTopic.h>
|
||||
#include <networktables/DoubleArrayTopic.h>
|
||||
#include <networktables/DoubleTopic.h>
|
||||
@@ -156,6 +157,14 @@ class PhotonCamera {
|
||||
*/
|
||||
const std::string_view GetCameraName() const;
|
||||
|
||||
/**
|
||||
* Returns whether the camera is connected and actively returning new data.
|
||||
* Connection status is debounced.
|
||||
*
|
||||
* @return True if the camera is actively sending frame data, false otherwise.
|
||||
*/
|
||||
bool IsConnected();
|
||||
|
||||
using CameraMatrix = Eigen::Matrix<double, 3, 3>;
|
||||
using DistortionMatrix = Eigen::Matrix<double, 8, 1>;
|
||||
|
||||
@@ -203,18 +212,31 @@ class PhotonCamera {
|
||||
nt::BooleanPublisher driverModePublisher;
|
||||
nt::IntegerSubscriber ledModeSubscriber;
|
||||
|
||||
nt::IntegerSubscriber heartbeatSubscriber;
|
||||
|
||||
nt::MultiSubscriber topicNameSubscriber;
|
||||
|
||||
std::string path;
|
||||
std::string cameraName;
|
||||
|
||||
frc::Alert disconnectAlert;
|
||||
frc::Alert timesyncAlert;
|
||||
|
||||
private:
|
||||
units::second_t lastVersionCheckTime = 0_s;
|
||||
static bool VERSION_CHECK_ENABLED;
|
||||
inline static int InstanceCount = 0;
|
||||
|
||||
units::second_t prevTimeSyncWarnTime = 0_s;
|
||||
|
||||
int prevHeartbeatValue = -1;
|
||||
units::second_t prevHeartbeatChangeTime = 0_s;
|
||||
|
||||
void VerifyVersion();
|
||||
|
||||
void UpdateDisconnectAlert();
|
||||
void CheckTimeSyncOrWarn(photon::PhotonPipelineResult& result);
|
||||
|
||||
std::vector<std::string> tablesThatLookLikePhotonCameras();
|
||||
};
|
||||
|
||||
|
||||
@@ -280,8 +280,8 @@ class SimCameraProperties {
|
||||
int resHeight;
|
||||
Eigen::Matrix<double, 3, 3> camIntrinsics;
|
||||
Eigen::Matrix<double, 8, 1> distCoeffs;
|
||||
double avgErrorPx;
|
||||
double errorStdDevPx;
|
||||
double avgErrorPx{0};
|
||||
double errorStdDevPx{0};
|
||||
units::second_t frameSpeed{0};
|
||||
units::second_t exposureTime{0};
|
||||
units::second_t avgLatency{0};
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
package org.photonvision;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assumptions.assumeTrue;
|
||||
import static org.photonvision.UnitTestUtils.waitForCondition;
|
||||
import static org.photonvision.UnitTestUtils.waitForSequenceNumber;
|
||||
@@ -33,8 +36,14 @@ import edu.wpi.first.hal.HAL;
|
||||
import edu.wpi.first.math.geometry.Rotation2d;
|
||||
import edu.wpi.first.networktables.NetworkTableInstance;
|
||||
import edu.wpi.first.networktables.NetworkTablesJNI;
|
||||
import edu.wpi.first.wpilibj.DataLogManager;
|
||||
import edu.wpi.first.wpilibj.Timer;
|
||||
import edu.wpi.first.wpilibj.simulation.SimHooks;
|
||||
import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
@@ -49,24 +58,38 @@ import org.photonvision.jni.PhotonTargetingJniLoader;
|
||||
import org.photonvision.jni.TimeSyncClient;
|
||||
import org.photonvision.jni.WpilibLoader;
|
||||
import org.photonvision.simulation.PhotonCameraSim;
|
||||
import org.photonvision.targeting.PhotonPipelineMetadata;
|
||||
import org.photonvision.targeting.PhotonPipelineResult;
|
||||
|
||||
class PhotonCameraTest {
|
||||
// A test-scoped, local-only NT instance
|
||||
NetworkTableInstance inst = null;
|
||||
|
||||
@BeforeAll
|
||||
public static void load_wpilib() {
|
||||
WpilibLoader.loadLibraries();
|
||||
|
||||
// See #1574 - test flakey, disabled until we address this
|
||||
assumeTrue(false);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
assertNull(inst);
|
||||
|
||||
HAL.initialize(500, 0);
|
||||
|
||||
inst = NetworkTableInstance.create();
|
||||
inst.stopClient();
|
||||
inst.stopServer();
|
||||
inst.startLocal();
|
||||
SmartDashboard.setNetworkTableInstance(inst);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void teardown() {
|
||||
inst.close();
|
||||
inst = null;
|
||||
|
||||
SimHooks.resumeTiming();
|
||||
|
||||
HAL.shutdown();
|
||||
}
|
||||
|
||||
@@ -87,12 +110,10 @@ class PhotonCameraTest {
|
||||
load_wpilib();
|
||||
PhotonTargetingJniLoader.load();
|
||||
|
||||
HAL.initialize(500, 0);
|
||||
inst.stopClient();
|
||||
inst.startServer();
|
||||
|
||||
NetworkTableInstance.getDefault().stopClient();
|
||||
NetworkTableInstance.getDefault().startServer();
|
||||
|
||||
var camera = new PhotonCamera("Arducam_OV2311_USB_Camera");
|
||||
var camera = new PhotonCamera(inst, "Arducam_OV2311_USB_Camera");
|
||||
PhotonCamera.setVersionCheckEnabled(false);
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
@@ -132,6 +153,38 @@ class PhotonCameraTest {
|
||||
Arguments.of(1, 1, 30, 10));
|
||||
}
|
||||
|
||||
private void configureInstanceDataLoggers(NetworkTableInstance inst, String name) {
|
||||
// Consumer<NetworkTableEvent> printEvent = (NetworkTableEvent e) -> {
|
||||
// if (e.is(Kind.kConnected)) {
|
||||
// System.out.println(name + ": Connected to " + e.connInfo);
|
||||
// }
|
||||
// if (e.is(Kind.kDisconnected)) {
|
||||
// System.out.println(name + ": Disconnected from " + e.connInfo);
|
||||
// }
|
||||
// if (e.is(Kind.kPublish)) {
|
||||
// System.out.println(name + ": Topic published: " + e.topicInfo);
|
||||
// }
|
||||
// if (e.is(Kind.kUnpublish)) {
|
||||
// System.out.println(name + ": Topic removed " + e.topicInfo);
|
||||
// }
|
||||
// if (e.is(Kind.kValueAll)) {
|
||||
// System.out.println(name + ": Value changed " + e.valueData);
|
||||
// }
|
||||
// if (e.is(Kind.kLogMessage)) {
|
||||
// System.out.println(name + ": LOG: " + e.logMessage);
|
||||
// }
|
||||
// };
|
||||
|
||||
// inst.addConnectionListener(true, printEvent);
|
||||
// inst.addListener(new String[]{""}, EnumSet.of(NetworkTableEvent.Kind.kTopic,
|
||||
// NetworkTableEvent.Kind.kValueAll), printEvent);
|
||||
|
||||
var log = DataLogManager.getLog();
|
||||
inst.startEntryDataLog(log, "", name + "_NT:");
|
||||
inst.startConnectionDataLog(log, name + "_NTCONNECTION:");
|
||||
System.out.println(name + ": Started logging to " + DataLogManager.getLogDir());
|
||||
}
|
||||
|
||||
/**
|
||||
* Try starting client before server and vice-versa, making sure that we never fail the version
|
||||
* check
|
||||
@@ -140,9 +193,21 @@ class PhotonCameraTest {
|
||||
@MethodSource("testNtOffsets")
|
||||
public void testRestartingRobotAndCoproc(
|
||||
int robotStart, int coprocStart, int robotRestart, int coprocRestart) throws Throwable {
|
||||
// See #1574 - test flakey, disabled until we address this
|
||||
assumeTrue(false);
|
||||
|
||||
var robotNt = NetworkTableInstance.create();
|
||||
var coprocNt = NetworkTableInstance.create();
|
||||
|
||||
configureInstanceDataLoggers(robotNt, "ROBOT 1");
|
||||
configureInstanceDataLoggers(coprocNt, "COPROC 1");
|
||||
|
||||
// Don't need inst
|
||||
inst.close();
|
||||
|
||||
// rename log
|
||||
DataLogManager.start("logs", "testRestartingRobotAndCoproc.wpilog");
|
||||
|
||||
robotNt.addLogger(10, 255, (it) -> System.out.println("ROBOT: " + it.logMessage.message));
|
||||
coprocNt.addLogger(10, 255, (it) -> System.out.println("CLIENT: " + it.logMessage.message));
|
||||
|
||||
@@ -166,6 +231,7 @@ class PhotonCameraTest {
|
||||
fakePhotonCoprocCam.close();
|
||||
coprocNt.close();
|
||||
coprocNt = NetworkTableInstance.create();
|
||||
configureInstanceDataLoggers(coprocNt, "COPROC 2");
|
||||
|
||||
coprocNt.addLogger(10, 255, (it) -> System.out.println("CLIENT: " + it.logMessage.message));
|
||||
|
||||
@@ -180,6 +246,7 @@ class PhotonCameraTest {
|
||||
|
||||
robotNt.close();
|
||||
robotNt = NetworkTableInstance.create();
|
||||
configureInstanceDataLoggers(robotNt, "ROBOT 2");
|
||||
robotNt.addLogger(10, 255, (it) -> System.out.println("ROBOT: " + it.logMessage.message));
|
||||
robotCamera = new PhotonCamera(robotNt, "MY_CAMERA");
|
||||
}
|
||||
@@ -233,5 +300,94 @@ class PhotonCameraTest {
|
||||
coprocNt.close();
|
||||
robotNt.close();
|
||||
tspClient.stop();
|
||||
|
||||
DataLogManager.stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAlerts() throws InterruptedException {
|
||||
// GIVEN a fresh NT instance
|
||||
|
||||
var cameraName = "foobar";
|
||||
|
||||
// AND a photoncamera that is disconnected
|
||||
var camera = new PhotonCamera(inst, cameraName);
|
||||
assertFalse(camera.isConnected());
|
||||
|
||||
String disconnectedCameraString = "PhotonCamera '" + cameraName + "' is disconnected.";
|
||||
|
||||
// Loop to hit cases past first iteration
|
||||
for (int i = 0; i < 10; i++) {
|
||||
// WHEN we update the camera
|
||||
camera.getAllUnreadResults();
|
||||
|
||||
// AND we tick SmartDashboard
|
||||
SmartDashboard.updateValues();
|
||||
|
||||
// The alert state will be set (hard-coded here)
|
||||
assertTrue(
|
||||
Arrays.stream(SmartDashboard.getStringArray("PhotonAlerts/warnings", new String[0]))
|
||||
.anyMatch(it -> it.equals(disconnectedCameraString)));
|
||||
|
||||
Thread.sleep(20);
|
||||
}
|
||||
|
||||
// GIVEN a simulated camera
|
||||
var sim = new PhotonCameraSim(camera);
|
||||
// AND a result with a timeSinceLastPong in the past
|
||||
PhotonPipelineResult noPongResult =
|
||||
new PhotonPipelineResult(
|
||||
new PhotonPipelineMetadata(
|
||||
1, 2, 3, 10 * 1000000 // 10 seconds -> us since last pong
|
||||
),
|
||||
List.of(),
|
||||
Optional.empty());
|
||||
|
||||
// Loop to hit cases past first iteration
|
||||
for (int i = 0; i < 10; i++) {
|
||||
// AND a PhotonCamera with a "new" result
|
||||
sim.submitProcessedFrame(noPongResult);
|
||||
|
||||
// WHEN we update the camera
|
||||
camera.getAllUnreadResults();
|
||||
|
||||
// AND we tick SmartDashboard
|
||||
SmartDashboard.updateValues();
|
||||
|
||||
// THEN the camera isn't disconnected
|
||||
assertTrue(
|
||||
Arrays.stream(SmartDashboard.getStringArray("PhotonAlerts/warnings", new String[0]))
|
||||
.noneMatch(it -> it.equals(disconnectedCameraString)));
|
||||
// AND the alert string looks like a timesync warning
|
||||
assertTrue(
|
||||
Arrays.stream(SmartDashboard.getStringArray("PhotonAlerts/warnings", new String[0]))
|
||||
.filter(it -> it.contains("is not connected to the TimeSyncServer"))
|
||||
.count()
|
||||
== 1);
|
||||
|
||||
Thread.sleep(20);
|
||||
}
|
||||
|
||||
final double HEARTBEAT_TIMEOUT = 0.5;
|
||||
|
||||
// GIVEN a PhotonCamera provided new results
|
||||
SimHooks.pauseTiming();
|
||||
sim.submitProcessedFrame(noPongResult);
|
||||
camera.getAllUnreadResults();
|
||||
// AND in a connected state
|
||||
assertTrue(camera.isConnected());
|
||||
|
||||
// WHEN we wait the timeout
|
||||
SimHooks.stepTiming(HEARTBEAT_TIMEOUT * 1.5);
|
||||
|
||||
// THEN the camera will not be connected
|
||||
assertFalse(camera.isConnected());
|
||||
|
||||
// WHEN we then provide new results
|
||||
SimHooks.stepTiming(0.02);
|
||||
sim.submitProcessedFrame(noPongResult);
|
||||
camera.getAllUnreadResults();
|
||||
// THEN the camera will not be connected
|
||||
assertTrue(camera.isConnected());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,10 +50,10 @@ public class UnitTestUtils {
|
||||
}
|
||||
|
||||
static PhotonPipelineResult waitForSequenceNumber(PhotonCamera camera, int seq) {
|
||||
assertTrue(camera.heartbeatEntry.getTopic().getHandle() != 0);
|
||||
assertTrue(camera.heartbeatSubscriber.getTopic().getHandle() != 0);
|
||||
|
||||
System.out.println(
|
||||
"Waiting for seq=" + seq + " on " + camera.heartbeatEntry.getTopic().getName());
|
||||
"Waiting for seq=" + seq + " on " + camera.heartbeatSubscriber.getTopic().getName());
|
||||
// wait up to 1 second for a new result
|
||||
for (int i = 0; i < 100; i++) {
|
||||
var res = camera.getLatestResult();
|
||||
|
||||
@@ -33,6 +33,7 @@ import static org.photonvision.UnitTestUtils.waitForSequenceNumber;
|
||||
|
||||
import edu.wpi.first.apriltag.AprilTag;
|
||||
import edu.wpi.first.apriltag.AprilTagFieldLayout;
|
||||
import edu.wpi.first.cscore.OpenCvLoader;
|
||||
import edu.wpi.first.hal.HAL;
|
||||
import edu.wpi.first.math.geometry.Pose2d;
|
||||
import edu.wpi.first.math.geometry.Pose3d;
|
||||
@@ -57,7 +58,6 @@ import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.photonvision.estimation.OpenCVHelp;
|
||||
import org.photonvision.estimation.TargetModel;
|
||||
import org.photonvision.estimation.VisionEstimation;
|
||||
import org.photonvision.jni.PhotonTargetingJniLoader;
|
||||
@@ -84,7 +84,7 @@ class VisionSystemSimTest {
|
||||
fail(e);
|
||||
}
|
||||
|
||||
OpenCVHelp.forceLoadOpenCV();
|
||||
OpenCvLoader.forceStaticLoad();
|
||||
|
||||
// See #1574 - test flakey, disabled until we address this
|
||||
assumeTrue(false);
|
||||
|
||||
@@ -22,12 +22,19 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#include <fmt/ranges.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <hal/HAL.h>
|
||||
#include <net/TimeSyncClient.h>
|
||||
#include <net/TimeSyncServer.h>
|
||||
#include <photon/PhotonCamera.h>
|
||||
#include <photon/simulation/PhotonCameraSim.h>
|
||||
|
||||
#include "photon/PhotonCamera.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <frc/smartdashboard/SmartDashboard.h>
|
||||
#include <networktables/NetworkTableInstance.h>
|
||||
|
||||
TEST(TimeSyncProtocolTest, Smoketest) {
|
||||
using namespace wpi::tsp;
|
||||
@@ -52,3 +59,77 @@ TEST(TimeSyncProtocolTest, Smoketest) {
|
||||
|
||||
client.Stop();
|
||||
}
|
||||
|
||||
TEST(PhotonCameraTest, Alerts) {
|
||||
using frc::SmartDashboard;
|
||||
|
||||
// GIVEN a local-only NT instance
|
||||
auto inst = nt::NetworkTableInstance::GetDefault();
|
||||
inst.StopClient();
|
||||
inst.StopServer();
|
||||
inst.StartLocal();
|
||||
// (We can't create our own instance, SmartDashboard will always use the
|
||||
// default)
|
||||
|
||||
const std::string cameraName = "foobar";
|
||||
|
||||
// AND a PhotonCamera that is disconnected
|
||||
photon::PhotonCamera camera(inst, cameraName);
|
||||
EXPECT_FALSE(camera.IsConnected());
|
||||
std::string disconnectedCameraString =
|
||||
"PhotonCamera '" + cameraName + "' is disconnected.";
|
||||
|
||||
// Loop to hit cases past first iteration
|
||||
for (int i = 0; i < 10; i++) {
|
||||
// WHEN we update the camera
|
||||
camera.GetAllUnreadResults();
|
||||
// AND we tick SmartDashboard
|
||||
SmartDashboard::UpdateValues();
|
||||
|
||||
// The alert state will be set (hard-coded here)
|
||||
auto alerts = SmartDashboard::GetStringArray("PhotonAlerts/warnings", {});
|
||||
EXPECT_TRUE(
|
||||
std::any_of(alerts.begin(), alerts.end(),
|
||||
[&disconnectedCameraString](const std::string& alert) {
|
||||
return alert == disconnectedCameraString;
|
||||
}));
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||
}
|
||||
|
||||
// GIVEN a simulated camera
|
||||
photon::PhotonCameraSim sim(&camera);
|
||||
// AND a result with a timeSinceLastPong in the past
|
||||
photon::PhotonPipelineMetadata metadata{3, 1, 2, 10 * 1000000};
|
||||
photon::PhotonPipelineResult noPongResult{
|
||||
metadata, std::vector<photon::PhotonTrackedTarget>{}, std::nullopt};
|
||||
|
||||
// Loop to hit cases past first iteration
|
||||
for (int i = 0; i < 10; i++) {
|
||||
// AND a PhotonCamera with a "new" result
|
||||
sim.SubmitProcessedFrame(noPongResult);
|
||||
// WHEN we update the camera
|
||||
camera.GetAllUnreadResults();
|
||||
// AND we tick SmartDashboard
|
||||
SmartDashboard::UpdateValues();
|
||||
|
||||
// THEN the camera isn't disconnected
|
||||
auto alerts = SmartDashboard::GetStringArray("PhotonAlerts/warnings", {});
|
||||
fmt::println("{}:{}: saw alerts: {}", __FILE__, __LINE__, alerts);
|
||||
EXPECT_TRUE(
|
||||
std::none_of(alerts.begin(), alerts.end(),
|
||||
[&disconnectedCameraString](const std::string& alert) {
|
||||
return alert == disconnectedCameraString;
|
||||
}));
|
||||
|
||||
// AND the alert string looks like a timesync warning
|
||||
EXPECT_EQ(
|
||||
1, std::count_if(
|
||||
alerts.begin(), alerts.end(), [](const std::string& alert) {
|
||||
return alert.find("is not connected to the TimeSyncServer") !=
|
||||
std::string::npos;
|
||||
}));
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -509,6 +509,10 @@ TEST_F(VisionSystemSimTest, TestPoseEstimationRotated) {
|
||||
|
||||
auto camResults = camera.GetLatestResult();
|
||||
auto targetSpan = camResults.GetTargets();
|
||||
|
||||
// We need to see at least one target
|
||||
ASSERT_GT(targetSpan.size(), static_cast<size_t>(0));
|
||||
|
||||
std::vector<photon::PhotonTrackedTarget> targets;
|
||||
for (photon::PhotonTrackedTarget tar : targetSpan) {
|
||||
targets.push_back(tar);
|
||||
|
||||
@@ -121,7 +121,7 @@ public enum Platform {
|
||||
}
|
||||
|
||||
public static boolean isRK3588() {
|
||||
return Platform.isOrangePi() || Platform.isCoolPi4b();
|
||||
return Platform.isOrangePi() || Platform.isCoolPi4b() || Platform.isRock5C();
|
||||
}
|
||||
|
||||
public static boolean isRaspberryPi() {
|
||||
@@ -217,7 +217,7 @@ public enum Platform {
|
||||
return LINUX_32;
|
||||
} else if (OS_ARCH.equals("aarch64") || OS_ARCH.equals("arm64")) {
|
||||
// TODO - os detection needed?
|
||||
if (isOrangePi()) {
|
||||
if (isRK3588()) {
|
||||
return LINUX_RK3588_64;
|
||||
} else {
|
||||
return LINUX_AARCH64;
|
||||
@@ -243,6 +243,10 @@ public enum Platform {
|
||||
return fileHasText("/proc/device-tree/model", "Orange Pi 5");
|
||||
}
|
||||
|
||||
private static boolean isRock5C() {
|
||||
return fileHasText("/proc/device-tree/model", "ROCK 5C");
|
||||
}
|
||||
|
||||
private static boolean isCoolPi4b() {
|
||||
return fileHasText("/proc/device-tree/model", "CoolPi 4B");
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
package org.photonvision.estimation;
|
||||
|
||||
import edu.wpi.first.cscore.CvSink;
|
||||
import edu.wpi.first.cscore.OpenCvLoader;
|
||||
import edu.wpi.first.math.MatBuilder;
|
||||
import edu.wpi.first.math.Matrix;
|
||||
import edu.wpi.first.math.Nat;
|
||||
@@ -54,14 +54,12 @@ public final class OpenCVHelp {
|
||||
private static final Rotation3d NWU_TO_EDN;
|
||||
private static final Rotation3d EDN_TO_NWU;
|
||||
|
||||
// Creating a cscore object is sufficient to load opencv, per
|
||||
// https://www.chiefdelphi.com/t/unsatisfied-link-error-when-simulating-java-robot-code-using-opencv/426731/4
|
||||
private static CvSink dummySink = null;
|
||||
|
||||
/**
|
||||
* @deprecated Replaced by {@link OpenCvLoader#forceStaticLoad()}
|
||||
*/
|
||||
@Deprecated(since = "2025", forRemoval = true)
|
||||
public static void forceLoadOpenCV() {
|
||||
if (dummySink != null) return;
|
||||
dummySink = new CvSink("ignored");
|
||||
dummySink.close();
|
||||
OpenCvLoader.forceStaticLoad();
|
||||
}
|
||||
|
||||
static {
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.photonvision.estimation;
|
||||
|
||||
import edu.wpi.first.apriltag.AprilTag;
|
||||
import edu.wpi.first.apriltag.AprilTagFieldLayout;
|
||||
import edu.wpi.first.cscore.OpenCvLoader;
|
||||
import edu.wpi.first.math.MatBuilder;
|
||||
import edu.wpi.first.math.Matrix;
|
||||
import edu.wpi.first.math.Nat;
|
||||
@@ -105,6 +106,8 @@ public class VisionEstimation {
|
||||
if (knownTags.isEmpty() || corners.isEmpty() || corners.size() % 4 != 0) {
|
||||
return Optional.empty();
|
||||
}
|
||||
OpenCvLoader.forceStaticLoad();
|
||||
|
||||
Point[] points = OpenCVHelp.cornersToPoints(corners);
|
||||
|
||||
// single-tag pnp
|
||||
@@ -200,6 +203,8 @@ public class VisionEstimation {
|
||||
if (knownTags.isEmpty() || corners.isEmpty() || corners.size() % 4 != 0) {
|
||||
return Optional.empty();
|
||||
}
|
||||
OpenCvLoader.forceStaticLoad();
|
||||
|
||||
Point[] points = OpenCVHelp.cornersToPoints(corners);
|
||||
|
||||
// Undistort
|
||||
|
||||
@@ -17,10 +17,6 @@
|
||||
|
||||
package org.photonvision.jni;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.opencv.core.Core;
|
||||
|
||||
import edu.wpi.first.apriltag.jni.AprilTagJNI;
|
||||
import edu.wpi.first.cscore.CameraServerJNI;
|
||||
import edu.wpi.first.cscore.OpenCvLoader;
|
||||
@@ -30,9 +26,12 @@ import edu.wpi.first.net.WPINetJNI;
|
||||
import edu.wpi.first.networktables.NetworkTablesJNI;
|
||||
import edu.wpi.first.util.CombinedRuntimeLoader;
|
||||
import edu.wpi.first.util.WPIUtilJNI;
|
||||
import java.io.IOException;
|
||||
import org.opencv.core.Core;
|
||||
|
||||
public class WpilibLoader {
|
||||
private static boolean has_loaded = false;
|
||||
|
||||
public static boolean loadLibraries() {
|
||||
if (has_loaded) return true;
|
||||
|
||||
@@ -45,9 +44,11 @@ public class WpilibLoader {
|
||||
WPIMathJNI.Helper.setExtractOnStaticLoad(false);
|
||||
AprilTagJNI.Helper.setExtractOnStaticLoad(false);
|
||||
try {
|
||||
// Need to load wpiutil first before checking if the MSVC runtime is valid
|
||||
CombinedRuntimeLoader.loadLibraries(WpilibLoader.class, "wpiutiljni");
|
||||
WPIUtilJNI.checkMsvcRuntime();
|
||||
CombinedRuntimeLoader.loadLibraries(
|
||||
WpilibLoader.class,
|
||||
"wpiutiljni",
|
||||
"wpimathjni",
|
||||
"ntcorejni",
|
||||
"wpinetjni",
|
||||
@@ -56,7 +57,6 @@ public class WpilibLoader {
|
||||
"apriltagjni");
|
||||
|
||||
CombinedRuntimeLoader.loadLibraries(WpilibLoader.class, Core.NATIVE_LIBRARY_NAME);
|
||||
|
||||
has_loaded = true;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
@@ -31,6 +31,12 @@ model {
|
||||
nativeUtils.usePlatformArguments(it)
|
||||
if (it.toolChain instanceof GccCompatibleToolChain) {
|
||||
it.cppCompiler.args << "-Wno-deprecated-enum-enum-conversion"
|
||||
|
||||
if (project.hasProperty('withSanitizers')) {
|
||||
println("Adding asan/usan/lsan to " + it)
|
||||
it.cppCompiler.args << "-fsanitize=address,undefined,leak" << "-g"
|
||||
it.linker.args << "-fsanitize=address,undefined,leak"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user