Skip to content

Commit 2041e2b

Browse files
committed
[photon-lib] Invalidate pose cache when setting referencePose
1 parent ad64bfe commit 2041e2b

File tree

5 files changed

+118
-21
lines changed

5 files changed

+118
-21
lines changed

photon-lib/py/buildAndTest.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
cd -- "$(dirname -- "$0")"
5+
16
# Uninstall if it already was installed
27
python3 -m pip uninstall -y photonlibpy
38

49
# Build wheel
10+
python3 -m pip install wheel
511
python3 setup.py bdist_wheel
612

713
# Install whatever wheel was made

photon-lib/py/photonlibpy/photonPoseEstimator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ def _invalidatePoseCache(self) -> None:
229229
self._poseCacheTimestampSeconds = -1.0
230230

231231
def _checkUpdate(self, oldObj, newObj) -> None:
232-
if oldObj != newObj and oldObj is not None and oldObj is not newObj:
232+
if oldObj != newObj:
233233
self._invalidatePoseCache()
234234

235235
def addHeadingData(

photon-lib/py/test/__init__.py

Whitespace-only changes.

photon-lib/py/test/photonPoseEstimator_test.py

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
## along with this program. If not, see <https://www.gnu.org/licenses/>.
1616
###############################################################################
1717

18+
from test import testUtil
19+
1820
import wpimath.units
1921
from photonlibpy import PhotonCamera, PhotonPoseEstimator, PoseStrategy
2022
from photonlibpy.estimation import TargetModel
@@ -191,7 +193,7 @@ def test_pnpDistanceTrigSolve():
191193
assert bestTarget.fiducialId == 0
192194
assert result.ntReceiveTimestampMicros > 0
193195
# Make test independent of the FPGA time.
194-
result.ntReceiveTimestampMicros = fakeTimestampSecs * 1e6
196+
result.ntReceiveTimestampMicros = int(fakeTimestampSecs * 1e6)
195197

196198
estimator.addHeadingData(
197199
result.getTimestampSeconds(), realPose.rotation().toRotation2d()
@@ -217,7 +219,7 @@ def test_pnpDistanceTrigSolve():
217219
assert bestTarget.fiducialId == 0
218220
assert result.ntReceiveTimestampMicros > 0
219221
# Make test independent of the FPGA time.
220-
result.ntReceiveTimestampMicros = fakeTimestampSecs * 1e6
222+
result.ntReceiveTimestampMicros = int(fakeTimestampSecs * 1e6)
221223

222224
estimator.addHeadingData(
223225
result.getTimestampSeconds(), realPose.rotation().toRotation2d()
@@ -289,8 +291,36 @@ def test_multiTagOnCoprocStrategy():
289291
def test_cacheIsInvalidated():
290292
aprilTags = fakeAprilTagFieldLayout()
291293
cameraOne = PhotonCameraInjector()
294+
295+
estimator = PhotonPoseEstimator(
296+
aprilTags, PoseStrategy.LOWEST_AMBIGUITY, cameraOne, Transform3d()
297+
)
298+
299+
# Initial state, expect no timestamp.
300+
assertEquals(-1, estimator._poseCacheTimestampSeconds)
301+
302+
# First result is 17s after epoch start.
303+
timestamps = testUtil.PipelineTimestamps(captureTimestampMicros=17_000_000)
304+
latencySecs = timestamps.pipelineLatencySecs()
305+
306+
# No targets, expect empty result
307+
cameraOne.result = PhotonPipelineResult(
308+
timestamps.receiveTimestampMicros(),
309+
metadata=timestamps.toPhotonPipelineMetadata(),
310+
)
311+
estimatedPose = estimator.update()
312+
313+
assert estimatedPose is None
314+
assertEquals(
315+
timestamps.receiveTimestampMicros() * 1e-6 - latencySecs,
316+
estimator._poseCacheTimestampSeconds,
317+
1e-3,
318+
)
319+
320+
# Set actual result
321+
timestamps.incrementTimeMicros(2_500_000)
292322
result = PhotonPipelineResult(
293-
int(20 * 1e6),
323+
timestamps.receiveTimestampMicros(),
294324
[
295325
PhotonTrackedTarget(
296326
3.0,
@@ -315,31 +345,21 @@ def test_cacheIsInvalidated():
315345
0.7,
316346
)
317347
],
318-
metadata=PhotonPipelineMetadata(0, int(2 * 1e3), 0),
319-
)
320-
321-
estimator = PhotonPoseEstimator(
322-
aprilTags, PoseStrategy.LOWEST_AMBIGUITY, cameraOne, Transform3d()
348+
metadata=timestamps.toPhotonPipelineMetadata(),
323349
)
324-
325-
# Empty result, expect empty result
326-
cameraOne.result = PhotonPipelineResult(0)
327-
estimatedPose = estimator.update()
328-
assert estimatedPose is None
329-
330-
# Set actual result
331350
cameraOne.result = result
332351
estimatedPose = estimator.update()
333352
assert estimatedPose is not None
334-
assertEquals(20, estimatedPose.timestampSeconds, 0.01)
335-
assertEquals(20 - 2e-3, estimator._poseCacheTimestampSeconds, 1e-3)
353+
expectedTimestamp = timestamps.receiveTimestampMicros() * 1e-6 - latencySecs
354+
assertEquals(expectedTimestamp, estimatedPose.timestampSeconds, 1e-3)
355+
assertEquals(expectedTimestamp, estimator._poseCacheTimestampSeconds, 1e-3)
336356

337357
# And again -- pose cache should mean this is empty
338358
cameraOne.result = result
339359
estimatedPose = estimator.update()
340360
assert estimatedPose is None
341361
# Expect the old timestamp to still be here
342-
assertEquals(20 - 2e-3, estimator._poseCacheTimestampSeconds, 1e-3)
362+
assertEquals(expectedTimestamp, estimator._poseCacheTimestampSeconds, 1e-3)
343363

344364
# Set new field layout -- right after, the pose cache timestamp should be -1
345365
estimator.fieldTags = AprilTagFieldLayout([AprilTag()], 0, 0)
@@ -350,8 +370,14 @@ def test_cacheIsInvalidated():
350370

351371
assert estimatedPose is not None
352372

353-
assertEquals(20, estimatedPose.timestampSeconds, 0.01)
354-
assertEquals(20 - 2e-3, estimator._poseCacheTimestampSeconds, 1e-3)
373+
assertEquals(expectedTimestamp, estimatedPose.timestampSeconds, 1e-3)
374+
assertEquals(expectedTimestamp, estimator._poseCacheTimestampSeconds, 1e-3)
375+
376+
# Setting a value from None to a non-None should invalidate the cache.
377+
assert estimator.referencePose is None
378+
estimator.referencePose = Pose3d(3, 3, 3, Rotation3d())
379+
380+
assertEquals(-1, estimator._poseCacheTimestampSeconds)
355381

356382

357383
def assertEquals(expected, actual, epsilon=0.0):

photon-lib/py/test/testUtil.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""Test utilities."""
2+
3+
from photonlibpy.targeting import PhotonPipelineMetadata
4+
5+
6+
class InvalidTestDataException(ValueError):
7+
pass
8+
9+
10+
class PipelineTimestamps:
11+
"""Helper class to ensure timestamps are positive."""
12+
13+
def __init__(
14+
self,
15+
*,
16+
captureTimestampMicros: int,
17+
pipelineLatencyMicros=2_000,
18+
receiveLatencyMicros=1_000,
19+
):
20+
if captureTimestampMicros < 0:
21+
raise InvalidTestDataException("captureTimestampMicros cannot be negative")
22+
if pipelineLatencyMicros <= 0:
23+
raise InvalidTestDataException("pipelineLatencyMicros must be positive")
24+
if receiveLatencyMicros < 0:
25+
raise InvalidTestDataException("receiveLatencyMicros cannot be negative")
26+
self._captureTimestampMicros = captureTimestampMicros
27+
self._pipelineLatencyMicros = pipelineLatencyMicros
28+
self._receiveLatencyMicros = receiveLatencyMicros
29+
self._sequenceID = 0
30+
31+
@property
32+
def captureTimestampMicros(self) -> int:
33+
return self._captureTimestampMicros
34+
35+
@captureTimestampMicros.setter
36+
def captureTimestampMicros(self, micros: int) -> None:
37+
if micros < 0:
38+
raise InvalidTestDataException("captureTimestampMicros cannot be negative")
39+
if micros < self._captureTimestampMicros:
40+
raise InvalidTestDataException("time cannot go backwards")
41+
self._captureTimestampMicros = micros
42+
self._sequenceID += 1
43+
44+
@property
45+
def pipelineLatencyMicros(self) -> int:
46+
return self._pipelineLatencyMicros
47+
48+
def pipelineLatencySecs(self) -> float:
49+
return self.pipelineLatencyMicros * 1e-6
50+
51+
def incrementTimeMicros(self, micros: int) -> None:
52+
self.captureTimestampMicros += micros
53+
54+
def publishTimestampMicros(self) -> int:
55+
return self._captureTimestampMicros + self.pipelineLatencyMicros
56+
57+
def receiveTimestampMicros(self) -> int:
58+
return self.publishTimestampMicros() + self._receiveLatencyMicros
59+
60+
def toPhotonPipelineMetadata(self) -> PhotonPipelineMetadata:
61+
return PhotonPipelineMetadata(
62+
captureTimestampMicros=self.captureTimestampMicros,
63+
publishTimestampMicros=self.publishTimestampMicros(),
64+
sequenceID=self._sequenceID,
65+
)

0 commit comments

Comments
 (0)