Skip to content

Commit 8d4024b

Browse files
authored
Add alerts for timesync and disconnection (#1799)
1 parent f6736fc commit 8d4024b

File tree

12 files changed

+389
-24
lines changed

12 files changed

+389
-24
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,4 @@ photon-server/src/main/resources/web/*
150150
venv
151151
.venv/*
152152
.venv
153+
networktables.json

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ Note that these are case sensitive!
4242
* linuxathena
4343
- `-PtgtIP`: Specifies where `./gradlew deploy` should try to copy the fat JAR to
4444
- `-Pprofile`: enables JVM profiling
45+
- `-PwithSanitizers`: On Linux, enables `-fsanitize=address,undefined,leak`
4546

4647
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`
4748

photon-lib/src/main/java/org/photonvision/PhotonCamera.java

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
import edu.wpi.first.networktables.NetworkTableInstance;
4242
import edu.wpi.first.networktables.PubSubOption;
4343
import edu.wpi.first.networktables.StringSubscriber;
44+
import edu.wpi.first.wpilibj.Alert;
45+
import edu.wpi.first.wpilibj.Alert.AlertType;
4446
import edu.wpi.first.wpilibj.DriverStation;
4547
import edu.wpi.first.wpilibj.Timer;
4648
import edu.wpi.first.wpilibj.util.WPILibVersion;
@@ -59,6 +61,7 @@
5961
public class PhotonCamera implements AutoCloseable {
6062
private static int InstanceCount = 0;
6163
public static final String kTableName = "photonvision";
64+
private static final String PHOTON_ALERT_GROUP = "PhotonAlerts";
6265

6366
private final NetworkTable cameraTable;
6467
PacketSubscriber<PhotonPipelineResult> resultSubscriber;
@@ -68,7 +71,7 @@ public class PhotonCamera implements AutoCloseable {
6871
IntegerEntry inputSaveImgEntry, outputSaveImgEntry;
6972
IntegerPublisher pipelineIndexRequest, ledModeRequest;
7073
IntegerSubscriber pipelineIndexState, ledModeState;
71-
IntegerSubscriber heartbeatEntry;
74+
IntegerSubscriber heartbeatSubscriber;
7275
DoubleArraySubscriber cameraIntrinsicsSubscriber;
7376
DoubleArraySubscriber cameraDistortionSubscriber;
7477
MultiSubscriber topicNameSubscriber;
@@ -106,6 +109,9 @@ public void close() {
106109
double prevTimeSyncWarnTime = 0;
107110
private static final double WARN_DEBOUNCE_SEC = 5;
108111

112+
private final Alert disconnectAlert;
113+
private final Alert timesyncAlert;
114+
109115
public static void setVersionCheckEnabled(boolean enabled) {
110116
VERSION_CHECK_ENABLED = enabled;
111117
}
@@ -120,6 +126,10 @@ public static void setVersionCheckEnabled(boolean enabled) {
120126
*/
121127
public PhotonCamera(NetworkTableInstance instance, String cameraName) {
122128
name = cameraName;
129+
disconnectAlert =
130+
new Alert(
131+
PHOTON_ALERT_GROUP, "PhotonCamera '" + name + "' is disconnected.", AlertType.kWarning);
132+
timesyncAlert = new Alert(PHOTON_ALERT_GROUP, "", AlertType.kWarning);
123133
rootPhotonTable = instance.getTable(kTableName);
124134
this.cameraTable = rootPhotonTable.getSubTable(cameraName);
125135
path = cameraTable.getPath();
@@ -139,7 +149,7 @@ public PhotonCamera(NetworkTableInstance instance, String cameraName) {
139149
outputSaveImgEntry = cameraTable.getIntegerTopic("outputSaveImgCmd").getEntry(0);
140150
pipelineIndexRequest = cameraTable.getIntegerTopic("pipelineIndexRequest").publish();
141151
pipelineIndexState = cameraTable.getIntegerTopic("pipelineIndexState").subscribe(0);
142-
heartbeatEntry = cameraTable.getIntegerTopic("heartbeat").subscribe(-1);
152+
heartbeatSubscriber = cameraTable.getIntegerTopic("heartbeat").subscribe(-1);
143153
cameraIntrinsicsSubscriber =
144154
cameraTable.getDoubleArrayTopic("cameraIntrinsics").subscribe(null);
145155
cameraDistortionSubscriber =
@@ -249,6 +259,7 @@ public PhotonCamera(String cameraName) {
249259
*/
250260
public List<PhotonPipelineResult> getAllUnreadResults() {
251261
verifyVersion();
262+
updateDisconnectAlert();
252263

253264
List<PhotonPipelineResult> ret = new ArrayList<>();
254265

@@ -274,6 +285,7 @@ public List<PhotonPipelineResult> getAllUnreadResults() {
274285
@Deprecated(since = "2024", forRemoval = true)
275286
public PhotonPipelineResult getLatestResult() {
276287
verifyVersion();
288+
updateDisconnectAlert();
277289

278290
// Grab the latest result. We don't care about the timestamp from NT - the metadata header has
279291
// this, latency compensated by the Time Sync Client
@@ -288,22 +300,34 @@ public PhotonPipelineResult getLatestResult() {
288300
return result;
289301
}
290302

303+
private void updateDisconnectAlert() {
304+
disconnectAlert.set(!isConnected());
305+
}
306+
291307
private void checkTimeSyncOrWarn(PhotonPipelineResult result) {
292308
if (result.metadata.timeSinceLastPong > 5L * 1000000L) {
309+
String warningText =
310+
"PhotonVision coprocessor at path "
311+
+ path
312+
+ " is not connected to the TimeSyncServer? It's been "
313+
+ String.format("%.2f", result.metadata.timeSinceLastPong / 1e6)
314+
+ "s since the coprocessor last heard a pong.";
315+
316+
timesyncAlert.setText(warningText);
317+
timesyncAlert.set(true);
318+
293319
if (Timer.getFPGATimestamp() > (prevTimeSyncWarnTime + WARN_DEBOUNCE_SEC)) {
294320
prevTimeSyncWarnTime = Timer.getFPGATimestamp();
295321

296322
DriverStation.reportWarning(
297-
"PhotonVision coprocessor at path "
298-
+ path
299-
+ " is not connected to the TimeSyncServer? It's been "
300-
+ String.format("%.2f", result.metadata.timeSinceLastPong / 1e6)
301-
+ "s since the coprocessor last heard a pong.\n\nCheck /photonvision/.timesync/{COPROCESSOR_HOSTNAME} for more information.",
323+
warningText
324+
+ "\n\nCheck /photonvision/.timesync/{COPROCESSOR_HOSTNAME} for more information.",
302325
false);
303326
}
304327
} else {
305328
// Got a valid packet, reset the last time
306329
prevTimeSyncWarnTime = 0;
330+
timesyncAlert.set(false);
307331
}
308332
}
309333

@@ -404,9 +428,14 @@ public String getName() {
404428
* @return True if the camera is actively sending frame data, false otherwise.
405429
*/
406430
public boolean isConnected() {
407-
var curHeartbeat = heartbeatEntry.get();
431+
var curHeartbeat = heartbeatSubscriber.get();
408432
var now = Timer.getFPGATimestamp();
409433

434+
if (curHeartbeat < 0) {
435+
// we have never heard from the camera
436+
return false;
437+
}
438+
410439
if (curHeartbeat != prevHeartbeatValue) {
411440
// New heartbeat value from the coprocessor
412441
prevHeartbeatChangeTime = now;
@@ -455,7 +484,7 @@ void verifyVersion() {
455484

456485
// Heartbeat entry is assumed to always be present. If it's not present, we
457486
// assume that a camera with that name was never connected in the first place.
458-
if (!heartbeatEntry.exists()) {
487+
if (!heartbeatSubscriber.exists()) {
459488
var cameraNames = getTablesThatLookLikePhotonCameras();
460489
if (cameraNames.isEmpty()) {
461490
DriverStation.reportError(

photon-lib/src/main/native/cpp/photon/PhotonCamera.cpp

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@
4444
#include "opencv2/core/utility.hpp"
4545
#include "photon/dataflow/structures/Packet.h"
4646

47+
static constexpr units::second_t WARN_DEBOUNCE_SEC = 5_s;
48+
static constexpr units::second_t HEARTBEAT_DEBOUNCE_SEC = 500_ms;
49+
4750
inline void verifyDependencies() {
4851
if (!(std::string_view{GetWPILibVersion()} ==
4952
std::string_view{photon::PhotonVersion::wpilibTargetVersion})) {
@@ -137,6 +140,7 @@ namespace photon {
137140

138141
constexpr const units::second_t VERSION_CHECK_INTERVAL = 5_s;
139142
static const std::vector<std::string_view> PHOTON_PREFIX = {"/photonvision/"};
143+
static const std::string PHOTON_ALERT_GROUP{"PhotonAlerts"};
140144
bool PhotonCamera::VERSION_CHECK_ENABLED = true;
141145

142146
void PhotonCamera::SetVersionCheckEnabled(bool enabled) {
@@ -179,9 +183,16 @@ PhotonCamera::PhotonCamera(nt::NetworkTableInstance instance,
179183
rootTable->GetBooleanTopic("driverMode").Subscribe(false)),
180184
driverModePublisher(
181185
rootTable->GetBooleanTopic("driverModeRequest").Publish()),
186+
heartbeatSubscriber(
187+
rootTable->GetIntegerTopic("heartbeat").Subscribe(-1)),
182188
topicNameSubscriber(instance, PHOTON_PREFIX, {.topicsOnly = true}),
183189
path(rootTable->GetPath()),
184-
cameraName(cameraName) {
190+
cameraName(cameraName),
191+
disconnectAlert(PHOTON_ALERT_GROUP,
192+
std::string{"PhotonCamera '"} + std::string{cameraName} +
193+
"' is disconnected.",
194+
frc::Alert::AlertType::kWarning),
195+
timesyncAlert(PHOTON_ALERT_GROUP, "", frc::Alert::AlertType::kWarning) {
185196
verifyDependencies();
186197
HAL_Report(HALUsageReporting::kResourceType_PhotonCamera, InstanceCount);
187198
InstanceCount++;
@@ -217,6 +228,8 @@ PhotonPipelineResult PhotonCamera::GetLatestResult() {
217228
// Create the new result;
218229
PhotonPipelineResult result = packet.Unpack<PhotonPipelineResult>();
219230

231+
CheckTimeSyncOrWarn(result);
232+
220233
result.SetReceiveTimestamp(now);
221234

222235
return result;
@@ -229,6 +242,7 @@ std::vector<PhotonPipelineResult> PhotonCamera::GetAllUnreadResults() {
229242

230243
// Prints warning if not connected
231244
VerifyVersion();
245+
UpdateDisconnectAlert();
232246

233247
const auto changes = rawBytesEntry.ReadQueue();
234248

@@ -247,6 +261,8 @@ std::vector<PhotonPipelineResult> PhotonCamera::GetAllUnreadResults() {
247261
photon::Packet packet{value.value};
248262
auto result = packet.Unpack<PhotonPipelineResult>();
249263

264+
CheckTimeSyncOrWarn(result);
265+
250266
// TODO: NT4 timestamps are still not to be trusted. But it's the best we
251267
// can do until we can make time sync more reliable.
252268
result.SetReceiveTimestamp(units::microsecond_t(value.time) -
@@ -258,6 +274,37 @@ std::vector<PhotonPipelineResult> PhotonCamera::GetAllUnreadResults() {
258274
return ret;
259275
}
260276

277+
void PhotonCamera::UpdateDisconnectAlert() {
278+
disconnectAlert.Set(!IsConnected());
279+
}
280+
281+
void PhotonCamera::CheckTimeSyncOrWarn(photon::PhotonPipelineResult& result) {
282+
if (result.metadata.timeSinceLastPong > 5L * 1000000L) {
283+
std::string warningText =
284+
"PhotonVision coprocessor at path " + path +
285+
" is not connected to the TimeSyncServer? It's been " +
286+
std::to_string(result.metadata.timeSinceLastPong / 1e6) +
287+
"s since the coprocessor last heard a pong.";
288+
289+
timesyncAlert.SetText(warningText);
290+
timesyncAlert.Set(true);
291+
292+
if (frc::Timer::GetFPGATimestamp() <
293+
(prevTimeSyncWarnTime + WARN_DEBOUNCE_SEC)) {
294+
prevTimeSyncWarnTime = frc::Timer::GetFPGATimestamp();
295+
296+
FRC_ReportWarning(
297+
warningText +
298+
"\n\nCheck /photonvision/.timesync/{{COPROCESSOR_HOSTNAME}} for more "
299+
"information.");
300+
}
301+
} else {
302+
// Got a valid packet, reset the last time
303+
prevTimeSyncWarnTime = 0_s;
304+
timesyncAlert.Set(false);
305+
}
306+
}
307+
261308
void PhotonCamera::SetDriverMode(bool driverMode) {
262309
driverModePublisher.Set(driverMode);
263310
}
@@ -290,6 +337,24 @@ const std::string_view PhotonCamera::GetCameraName() const {
290337
return cameraName;
291338
}
292339

340+
bool PhotonCamera::IsConnected() {
341+
auto currentHeartbeat = heartbeatSubscriber.Get();
342+
auto now = frc::Timer::GetFPGATimestamp();
343+
344+
if (currentHeartbeat < 0) {
345+
// we have never heard from the camera
346+
return false;
347+
}
348+
349+
if (currentHeartbeat != prevHeartbeatValue) {
350+
// New heartbeat value from the coprocessor
351+
prevHeartbeatChangeTime = now;
352+
prevHeartbeatValue = currentHeartbeat;
353+
}
354+
355+
return (now - prevHeartbeatChangeTime) < HEARTBEAT_DEBOUNCE_SEC;
356+
}
357+
293358
std::optional<PhotonCamera::CameraMatrix> PhotonCamera::GetCameraMatrix() {
294359
auto camCoeffs = cameraIntrinsicsSubscriber.Get();
295360
if (camCoeffs.size() == 9) {

photon-lib/src/main/native/cpp/photon/simulation/PhotonCameraSim.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,6 @@ PhotonPipelineResult PhotonCameraSim::Process(
335335
}
336336
}
337337

338-
heartbeatCounter++;
339338
return PhotonPipelineResult{
340339
PhotonPipelineMetadata{heartbeatCounter, 0,
341340
units::microsecond_t{latency}.to<int64_t>(),
@@ -388,6 +387,7 @@ void PhotonCameraSim::SubmitProcessedFrame(const PhotonPipelineResult& result,
388387
ts.cameraDistortionPublisher.Set(distortionView, ReceiveTimestamp);
389388

390389
ts.heartbeatPublisher.Set(heartbeatCounter, ReceiveTimestamp);
390+
heartbeatCounter++;
391391

392392
ts.subTable->GetInstance().Flush();
393393
}

photon-lib/src/main/native/include/photon/PhotonCamera.h

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#include <string>
2929
#include <vector>
3030

31+
#include <frc/Alert.h>
3132
#include <networktables/BooleanTopic.h>
3233
#include <networktables/DoubleArrayTopic.h>
3334
#include <networktables/DoubleTopic.h>
@@ -156,6 +157,14 @@ class PhotonCamera {
156157
*/
157158
const std::string_view GetCameraName() const;
158159

160+
/**
161+
* Returns whether the camera is connected and actively returning new data.
162+
* Connection status is debounced.
163+
*
164+
* @return True if the camera is actively sending frame data, false otherwise.
165+
*/
166+
bool IsConnected();
167+
159168
using CameraMatrix = Eigen::Matrix<double, 3, 3>;
160169
using DistortionMatrix = Eigen::Matrix<double, 8, 1>;
161170

@@ -203,18 +212,31 @@ class PhotonCamera {
203212
nt::BooleanPublisher driverModePublisher;
204213
nt::IntegerSubscriber ledModeSubscriber;
205214

215+
nt::IntegerSubscriber heartbeatSubscriber;
216+
206217
nt::MultiSubscriber topicNameSubscriber;
207218

208219
std::string path;
209220
std::string cameraName;
210221

222+
frc::Alert disconnectAlert;
223+
frc::Alert timesyncAlert;
224+
211225
private:
212226
units::second_t lastVersionCheckTime = 0_s;
213227
static bool VERSION_CHECK_ENABLED;
214228
inline static int InstanceCount = 0;
215229

230+
units::second_t prevTimeSyncWarnTime = 0_s;
231+
232+
int prevHeartbeatValue = -1;
233+
units::second_t prevHeartbeatChangeTime = 0_s;
234+
216235
void VerifyVersion();
217236

237+
void UpdateDisconnectAlert();
238+
void CheckTimeSyncOrWarn(photon::PhotonPipelineResult& result);
239+
218240
std::vector<std::string> tablesThatLookLikePhotonCameras();
219241
};
220242

photon-lib/src/main/native/include/photon/simulation/SimCameraProperties.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,8 +280,8 @@ class SimCameraProperties {
280280
int resHeight;
281281
Eigen::Matrix<double, 3, 3> camIntrinsics;
282282
Eigen::Matrix<double, 8, 1> distCoeffs;
283-
double avgErrorPx;
284-
double errorStdDevPx;
283+
double avgErrorPx{0};
284+
double errorStdDevPx{0};
285285
units::second_t frameSpeed{0};
286286
units::second_t exposureTime{0};
287287
units::second_t avgLatency{0};

0 commit comments

Comments
 (0)