Skip to content

Commit 7a4ea3d

Browse files
authored
Assert that version checking won't throw on startup (#1512)
# Overview Previously if the coproc came up later, getProperty would return the string literal "null", which made us print the BFW. Add tests to make sure that we don't do that anymore by rebooting a sim coproc + robot in a combination of different orders.
1 parent 5e1a939 commit 7a4ea3d

File tree

13 files changed

+305
-67
lines changed

13 files changed

+305
-67
lines changed

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

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,7 @@
3232
import edu.wpi.first.math.numbers.*;
3333
import edu.wpi.first.networktables.BooleanPublisher;
3434
import edu.wpi.first.networktables.BooleanSubscriber;
35-
import edu.wpi.first.networktables.DoubleArrayPublisher;
3635
import edu.wpi.first.networktables.DoubleArraySubscriber;
37-
import edu.wpi.first.networktables.DoublePublisher;
3836
import edu.wpi.first.networktables.IntegerEntry;
3937
import edu.wpi.first.networktables.IntegerPublisher;
4038
import edu.wpi.first.networktables.IntegerSubscriber;
@@ -64,13 +62,6 @@ public class PhotonCamera implements AutoCloseable {
6462
PacketSubscriber<PhotonPipelineResult> resultSubscriber;
6563
BooleanPublisher driverModePublisher;
6664
BooleanSubscriber driverModeSubscriber;
67-
DoublePublisher latencyMillisEntry;
68-
BooleanPublisher hasTargetEntry;
69-
DoublePublisher targetPitchEntry;
70-
DoublePublisher targetYawEntry;
71-
DoublePublisher targetAreaEntry;
72-
DoubleArrayPublisher targetPoseEntry;
73-
DoublePublisher targetSkewEntry;
7465
StringSubscriber versionEntry;
7566
IntegerEntry inputSaveImgEntry, outputSaveImgEntry;
7667
IntegerPublisher pipelineIndexRequest, ledModeRequest;
@@ -86,13 +77,6 @@ public void close() {
8677
resultSubscriber.close();
8778
driverModePublisher.close();
8879
driverModeSubscriber.close();
89-
latencyMillisEntry.close();
90-
hasTargetEntry.close();
91-
targetPitchEntry.close();
92-
targetYawEntry.close();
93-
targetAreaEntry.close();
94-
targetPoseEntry.close();
95-
targetSkewEntry.close();
9680
versionEntry.close();
9781
inputSaveImgEntry.close();
9882
outputSaveImgEntry.close();
@@ -111,13 +95,13 @@ public void close() {
11195

11296
private static boolean VERSION_CHECK_ENABLED = true;
11397
private static long VERSION_CHECK_INTERVAL = 5;
114-
private double lastVersionCheckTime = 0;
98+
double lastVersionCheckTime = 0;
11599

116100
private long prevHeartbeatValue = -1;
117101
private double prevHeartbeatChangeTime = 0;
118102
private static final double HEARTBEAT_DEBOUNCE_SEC = 0.5;
119103

120-
private double prevTimeSyncWarnTime = 0;
104+
double prevTimeSyncWarnTime = 0;
121105
private static final double WARN_DEBOUNCE_SEC = 5;
122106

123107
public static void setVersionCheckEnabled(boolean enabled) {
@@ -396,7 +380,7 @@ public final NetworkTable getCameraTable() {
396380
return cameraTable;
397381
}
398382

399-
private void verifyVersion() {
383+
void verifyVersion() {
400384
if (!VERSION_CHECK_ENABLED) return;
401385

402386
if ((Timer.getFPGATimestamp() - lastVersionCheckTime) < VERSION_CHECK_INTERVAL) return;
@@ -433,7 +417,7 @@ private void verifyVersion() {
433417
// Check for connection status. Warn if disconnected.
434418
else if (!isConnected()) {
435419
DriverStation.reportWarning(
436-
"PhotonVision coprocessor at path " + path + " is not sending new data.", true);
420+
"PhotonVision coprocessor at path " + path + " is not sending new data.", false);
437421
}
438422

439423
String versionString = versionEntry.get("");
@@ -448,7 +432,7 @@ else if (!isConnected()) {
448432
"PhotonVision coprocessor at path "
449433
+ path
450434
+ " has not reported a message interface UUID - is your coprocessor's camera started?",
451-
true);
435+
false);
452436
} else if (!local_uuid.equals(remote_uuid)) {
453437
// Error on a verified version mismatch
454438
// But stay silent otherwise

photon-lib/src/main/java/org/photonvision/simulation/PhotonCameraSim.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
public class PhotonCameraSim implements AutoCloseable {
6969
private final PhotonCamera cam;
7070

71-
NTTopicSet ts = new NTTopicSet();
71+
protected NTTopicSet ts = new NTTopicSet();
7272
private long heartbeatCounter = 1;
7373

7474
/** This simulated camera's {@link SimCameraProperties} */

photon-lib/src/test/java/org/photonvision/PhotonCameraTest.java

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,30 @@
2424

2525
package org.photonvision;
2626

27+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
28+
import static org.photonvision.UnitTestUtils.waitForCondition;
29+
import static org.photonvision.UnitTestUtils.waitForSequenceNumber;
30+
2731
import edu.wpi.first.hal.HAL;
32+
import edu.wpi.first.math.geometry.Rotation2d;
2833
import edu.wpi.first.networktables.NetworkTableInstance;
2934
import edu.wpi.first.networktables.NetworkTablesJNI;
3035
import edu.wpi.first.wpilibj.Timer;
3136
import java.io.IOException;
37+
import java.util.stream.Stream;
38+
import org.junit.jupiter.api.AfterEach;
3239
import org.junit.jupiter.api.Assertions;
3340
import org.junit.jupiter.api.BeforeAll;
41+
import org.junit.jupiter.api.BeforeEach;
3442
import org.junit.jupiter.api.Test;
43+
import org.junit.jupiter.params.ParameterizedTest;
44+
import org.junit.jupiter.params.provider.Arguments;
45+
import org.junit.jupiter.params.provider.MethodSource;
3546
import org.photonvision.common.dataflow.structures.Packet;
3647
import org.photonvision.jni.PhotonTargetingJniLoader;
48+
import org.photonvision.jni.TimeSyncClient;
3749
import org.photonvision.jni.WpilibLoader;
50+
import org.photonvision.simulation.PhotonCameraSim;
3851
import org.photonvision.targeting.PhotonPipelineResult;
3952

4053
class PhotonCameraTest {
@@ -43,6 +56,16 @@ public static void load_wpilib() {
4356
WpilibLoader.loadLibraries();
4457
}
4558

59+
@BeforeEach
60+
public void setup() {
61+
HAL.initialize(500, 0);
62+
}
63+
64+
@AfterEach
65+
public void teardown() {
66+
HAL.shutdown();
67+
}
68+
4669
@Test
4770
public void testEmpty() {
4871
Assertions.assertDoesNotThrow(
@@ -92,4 +115,119 @@ public void testTimeSyncServerWithPhotonCamera() throws InterruptedException, IO
92115

93116
HAL.shutdown();
94117
}
118+
119+
private static Stream<Arguments> testNtOffsets() {
120+
return Stream.of(
121+
// various initializaiton orders
122+
Arguments.of(1, 10, 30, 30),
123+
Arguments.of(10, 2, 30, 30),
124+
Arguments.of(10, 10, 30, 30),
125+
// Reboot just the robot
126+
Arguments.of(1, 1, 10, 30),
127+
// Reboot just the coproc
128+
Arguments.of(1, 1, 30, 10));
129+
}
130+
131+
/**
132+
* Try starting client before server and vice-versa, making sure that we never fail the version
133+
* check
134+
*/
135+
@ParameterizedTest
136+
@MethodSource("testNtOffsets")
137+
public void testRestartingRobotAndCoproc(
138+
int robotStart, int coprocStart, int robotRestart, int coprocRestart) throws Throwable {
139+
var robotNt = NetworkTableInstance.create();
140+
var coprocNt = NetworkTableInstance.create();
141+
142+
robotNt.addLogger(10, 255, (it) -> System.out.println("ROBOT: " + it.logMessage.message));
143+
coprocNt.addLogger(10, 255, (it) -> System.out.println("CLIENT: " + it.logMessage.message));
144+
145+
TimeSyncClient tspClient = null;
146+
147+
var robotCamera = new PhotonCamera(robotNt, "MY_CAMERA");
148+
149+
// apparently need a PhotonCamera to hand down
150+
var fakePhotonCoprocCam = new PhotonCamera(coprocNt, "MY_CAMERA");
151+
var coprocSim = new PhotonCameraSim(fakePhotonCoprocCam);
152+
coprocSim.prop.setCalibration(640, 480, Rotation2d.fromDegrees(90));
153+
coprocSim.prop.setFPS(30);
154+
coprocSim.setMinTargetAreaPixels(20.0);
155+
156+
for (int i = 0; i < 20; i++) {
157+
int seq = i + 1;
158+
159+
if (i == coprocRestart) {
160+
System.out.println("Restarting coprocessor NT client");
161+
162+
fakePhotonCoprocCam.close();
163+
coprocNt.close();
164+
coprocNt = NetworkTableInstance.create();
165+
166+
coprocNt.addLogger(10, 255, (it) -> System.out.println("CLIENT: " + it.logMessage.message));
167+
168+
fakePhotonCoprocCam = new PhotonCamera(coprocNt, "MY_CAMERA");
169+
coprocSim = new PhotonCameraSim(fakePhotonCoprocCam);
170+
coprocSim.prop.setCalibration(640, 480, Rotation2d.fromDegrees(90));
171+
coprocSim.prop.setFPS(30);
172+
coprocSim.setMinTargetAreaPixels(20.0);
173+
}
174+
if (i == robotRestart) {
175+
System.out.println("Restarting robot NT server");
176+
177+
robotNt.close();
178+
robotNt = NetworkTableInstance.create();
179+
robotNt.addLogger(10, 255, (it) -> System.out.println("ROBOT: " + it.logMessage.message));
180+
robotCamera = new PhotonCamera(robotNt, "MY_CAMERA");
181+
}
182+
183+
if (i == coprocStart || i == coprocRestart) {
184+
coprocNt.setServer("127.0.0.1", 5940);
185+
coprocNt.startClient4("testClient");
186+
187+
// PhotonCamera makes a server by default - connect to it
188+
tspClient = new TimeSyncClient("127.0.0.1", 5810, 0.5);
189+
}
190+
191+
if (i == robotStart || i == robotRestart) {
192+
robotNt.startServer("networktables_random.json", "", 5941, 5940);
193+
}
194+
195+
Thread.sleep(100);
196+
197+
if (i == Math.max(coprocStart, robotStart)) {
198+
final var c = coprocNt;
199+
final var r = robotNt;
200+
waitForCondition("Coproc connection", () -> c.getConnections().length == 1);
201+
waitForCondition("Rio connection", () -> r.getConnections().length == 1);
202+
}
203+
204+
var result1 = new PhotonPipelineResult();
205+
result1.metadata.captureTimestampMicros = seq * 100;
206+
result1.metadata.publishTimestampMicros = seq * 150;
207+
result1.metadata.sequenceID = seq;
208+
if (tspClient != null) {
209+
result1.metadata.timeSinceLastPong = tspClient.getPingMetadata().timeSinceLastPong();
210+
} else {
211+
result1.metadata.timeSinceLastPong = Long.MAX_VALUE;
212+
}
213+
214+
coprocSim.submitProcessedFrame(result1, NetworkTablesJNI.now());
215+
coprocNt.flush();
216+
217+
if (i > robotStart && i > coprocStart) {
218+
var ret = waitForSequenceNumber(robotCamera, seq);
219+
System.out.println(ret);
220+
}
221+
222+
// force verifyVersion to do checks
223+
robotCamera.lastVersionCheckTime = -100;
224+
robotCamera.prevTimeSyncWarnTime = -100;
225+
assertDoesNotThrow(robotCamera::verifyVersion);
226+
}
227+
228+
coprocSim.close();
229+
coprocNt.close();
230+
robotNt.close();
231+
tspClient.stop();
232+
}
95233
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* MIT License
3+
*
4+
* Copyright (c) PhotonVision
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
25+
package org.photonvision;
26+
27+
import static org.junit.jupiter.api.Assertions.assertTrue;
28+
import static org.junit.jupiter.api.Assertions.fail;
29+
30+
import java.util.function.BooleanSupplier;
31+
import org.photonvision.targeting.PhotonPipelineResult;
32+
33+
public class UnitTestUtils {
34+
static void waitForCondition(String name, BooleanSupplier condition) {
35+
// wait up to 1 second for satisfaction
36+
for (int i = 0; i < 100; i++) {
37+
if (condition.getAsBoolean()) {
38+
System.out.println(name + " satisfied on iteration " + i);
39+
return;
40+
}
41+
42+
try {
43+
Thread.sleep(60);
44+
} catch (InterruptedException e) {
45+
e.printStackTrace();
46+
fail(e);
47+
}
48+
}
49+
throw new RuntimeException(name + " was never satisfied");
50+
}
51+
52+
static PhotonPipelineResult waitForSequenceNumber(PhotonCamera camera, int seq) {
53+
assertTrue(camera.heartbeatEntry.getTopic().getHandle() != 0);
54+
55+
System.out.println(
56+
"Waiting for seq=" + seq + " on " + camera.heartbeatEntry.getTopic().getName());
57+
// wait up to 1 second for a new result
58+
for (int i = 0; i < 100; i++) {
59+
var res = camera.getLatestResult();
60+
System.out.println(res);
61+
if (res.metadata.sequenceID == seq) {
62+
return res;
63+
}
64+
65+
try {
66+
Thread.sleep(60);
67+
} catch (InterruptedException e) {
68+
e.printStackTrace();
69+
fail(e);
70+
}
71+
}
72+
throw new RuntimeException("Never saw sequence number " + seq);
73+
}
74+
}

0 commit comments

Comments
 (0)