photonlibpy 2025.3.1__py3-none-any.whl → 2026.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,384 @@
1
+ ###############################################################################
2
+ ## Copyright (C) Photon Vision.
3
+ ###############################################################################
4
+ ## This program is free software: you can redistribute it and/or modify
5
+ ## it under the terms of the GNU General Public License as published by
6
+ ## the Free Software Foundation, either version 3 of the License, or
7
+ ## (at your option) any later version.
8
+ ##
9
+ ## This program is distributed in the hope that it will be useful,
10
+ ## but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ ## GNU General Public License for more details.
13
+ ##
14
+ ## You should have received a copy of the GNU General Public License
15
+ ## along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ ###############################################################################
17
+
18
+ from test import testUtil
19
+
20
+ import wpimath.units
21
+ from photonlibpy import PhotonCamera, PhotonPoseEstimator, PoseStrategy
22
+ from photonlibpy.estimation import TargetModel
23
+ from photonlibpy.simulation import PhotonCameraSim, SimCameraProperties, VisionTargetSim
24
+ from photonlibpy.targeting import (
25
+ PhotonPipelineMetadata,
26
+ PhotonTrackedTarget,
27
+ TargetCorner,
28
+ )
29
+ from photonlibpy.targeting.multiTargetPNPResult import MultiTargetPNPResult, PnpResult
30
+ from photonlibpy.targeting.photonPipelineResult import PhotonPipelineResult
31
+ from robotpy_apriltag import AprilTag, AprilTagFieldLayout
32
+ from wpimath.geometry import Pose3d, Rotation3d, Transform3d, Translation3d
33
+
34
+
35
+ class PhotonCameraInjector(PhotonCamera):
36
+ result: PhotonPipelineResult
37
+
38
+ def __init__(self, cameraName="camera"):
39
+ super().__init__(cameraName)
40
+
41
+ def getLatestResult(self) -> PhotonPipelineResult:
42
+ return self.result
43
+
44
+
45
+ def fakeAprilTagFieldLayout() -> AprilTagFieldLayout:
46
+ tagList = []
47
+ tagPoses = (
48
+ Pose3d(3, 3, 3, Rotation3d()),
49
+ Pose3d(5, 5, 5, Rotation3d()),
50
+ )
51
+ for id_, pose in enumerate(tagPoses):
52
+ aprilTag = AprilTag()
53
+ aprilTag.ID = id_
54
+ aprilTag.pose = pose
55
+ tagList.append(aprilTag)
56
+
57
+ fieldLength = 54 / 3.281 # 54 ft -> meters
58
+ fieldWidth = 27 / 3.281 # 24 ft -> meters
59
+
60
+ return AprilTagFieldLayout(tagList, fieldLength, fieldWidth)
61
+
62
+
63
+ def test_lowestAmbiguityStrategy():
64
+ aprilTags = fakeAprilTagFieldLayout()
65
+ cameraOne = PhotonCameraInjector()
66
+ cameraOne.result = PhotonPipelineResult(
67
+ int(11 * 1e6),
68
+ [
69
+ PhotonTrackedTarget(
70
+ 3.0,
71
+ -4.0,
72
+ 9.0,
73
+ 4.0,
74
+ 0,
75
+ Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
76
+ Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
77
+ [
78
+ TargetCorner(1, 2),
79
+ TargetCorner(3, 4),
80
+ TargetCorner(5, 6),
81
+ TargetCorner(7, 8),
82
+ ],
83
+ [
84
+ TargetCorner(1, 2),
85
+ TargetCorner(3, 4),
86
+ TargetCorner(5, 6),
87
+ TargetCorner(7, 8),
88
+ ],
89
+ 0.7,
90
+ ),
91
+ PhotonTrackedTarget(
92
+ 3.0,
93
+ -4.0,
94
+ 9.1,
95
+ 6.7,
96
+ 1,
97
+ Transform3d(Translation3d(4, 2, 3), Rotation3d(0, 0, 0)),
98
+ Transform3d(Translation3d(4, 2, 3), Rotation3d(1, 5, 3)),
99
+ [
100
+ TargetCorner(1, 2),
101
+ TargetCorner(3, 4),
102
+ TargetCorner(5, 6),
103
+ TargetCorner(7, 8),
104
+ ],
105
+ [
106
+ TargetCorner(1, 2),
107
+ TargetCorner(3, 4),
108
+ TargetCorner(5, 6),
109
+ TargetCorner(7, 8),
110
+ ],
111
+ 0.3,
112
+ ),
113
+ PhotonTrackedTarget(
114
+ 9.0,
115
+ -2.0,
116
+ 19.0,
117
+ 3.0,
118
+ 0,
119
+ Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
120
+ Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
121
+ [
122
+ TargetCorner(1, 2),
123
+ TargetCorner(3, 4),
124
+ TargetCorner(5, 6),
125
+ TargetCorner(7, 8),
126
+ ],
127
+ [
128
+ TargetCorner(1, 2),
129
+ TargetCorner(3, 4),
130
+ TargetCorner(5, 6),
131
+ TargetCorner(7, 8),
132
+ ],
133
+ 0.4,
134
+ ),
135
+ ],
136
+ metadata=PhotonPipelineMetadata(0, int(2 * 1e3), 0),
137
+ multitagResult=None,
138
+ )
139
+
140
+ estimator = PhotonPoseEstimator(
141
+ aprilTags, PoseStrategy.LOWEST_AMBIGUITY, cameraOne, Transform3d()
142
+ )
143
+
144
+ estimatedPose = estimator.update()
145
+
146
+ assert estimatedPose is not None
147
+
148
+ pose = estimatedPose.estimatedPose
149
+
150
+ assertEquals(11 - 0.002, estimatedPose.timestampSeconds, 1e-3)
151
+ assertEquals(1, pose.x, 0.01)
152
+ assertEquals(3, pose.y, 0.01)
153
+ assertEquals(2, pose.z, 0.01)
154
+
155
+
156
+ def test_pnpDistanceTrigSolve():
157
+ aprilTags = fakeAprilTagFieldLayout()
158
+ cameraOne = PhotonCameraInjector()
159
+ latencySecs: wpimath.units.seconds = 1
160
+ fakeTimestampSecs: wpimath.units.seconds = 9 + latencySecs
161
+
162
+ cameraOneSim = PhotonCameraSim(cameraOne, SimCameraProperties.PERFECT_90DEG())
163
+ simTargets = [
164
+ VisionTargetSim(tag.pose, TargetModel.AprilTag36h11(), tag.ID)
165
+ for tag in aprilTags.getTags()
166
+ ]
167
+
168
+ # Compound Rolled + Pitched + Yaw
169
+ compoundTestTransform = Transform3d(
170
+ -wpimath.units.inchesToMeters(12),
171
+ -wpimath.units.inchesToMeters(11),
172
+ 3,
173
+ Rotation3d(
174
+ wpimath.units.degreesToRadians(37),
175
+ wpimath.units.degreesToRadians(6),
176
+ wpimath.units.degreesToRadians(60),
177
+ ),
178
+ )
179
+
180
+ estimator = PhotonPoseEstimator(
181
+ aprilTags,
182
+ PoseStrategy.PNP_DISTANCE_TRIG_SOLVE,
183
+ cameraOne,
184
+ compoundTestTransform,
185
+ )
186
+
187
+ realPose = Pose3d(7.3, 4.42, 0, Rotation3d(0, 0, 2.197)) # Pose to compare with
188
+ result = cameraOneSim.process(
189
+ latencySecs, realPose.transformBy(estimator.robotToCamera), simTargets
190
+ )
191
+ bestTarget = result.getBestTarget()
192
+ assert bestTarget is not None
193
+ assert bestTarget.fiducialId == 0
194
+ assert result.ntReceiveTimestampMicros > 0
195
+ # Make test independent of the FPGA time.
196
+ result.ntReceiveTimestampMicros = int(fakeTimestampSecs * 1e6)
197
+
198
+ estimator.addHeadingData(
199
+ result.getTimestampSeconds(), realPose.rotation().toRotation2d()
200
+ )
201
+ estimatedRobotPose = estimator.update(result)
202
+
203
+ assert estimatedRobotPose is not None
204
+ pose = estimatedRobotPose.estimatedPose
205
+ assertEquals(realPose.x, pose.x, 0.01)
206
+ assertEquals(realPose.y, pose.y, 0.01)
207
+ assertEquals(0.0, pose.z, 0.01)
208
+
209
+ # Straight on
210
+ fakeTimestampSecs += 60
211
+ straightOnTestTransform = Transform3d(0, 0, 3, Rotation3d())
212
+ estimator.robotToCamera = straightOnTestTransform
213
+ realPose = Pose3d(4.81, 2.38, 0, Rotation3d(0, 0, 2.818)) # Pose to compare with
214
+ result = cameraOneSim.process(
215
+ latencySecs, realPose.transformBy(estimator.robotToCamera), simTargets
216
+ )
217
+ bestTarget = result.getBestTarget()
218
+ assert bestTarget is not None
219
+ assert bestTarget.fiducialId == 0
220
+ assert result.ntReceiveTimestampMicros > 0
221
+ # Make test independent of the FPGA time.
222
+ result.ntReceiveTimestampMicros = int(fakeTimestampSecs * 1e6)
223
+
224
+ estimator.addHeadingData(
225
+ result.getTimestampSeconds(), realPose.rotation().toRotation2d()
226
+ )
227
+ estimatedRobotPose = estimator.update(result)
228
+
229
+ assert estimatedRobotPose is not None
230
+ pose = estimatedRobotPose.estimatedPose
231
+ assertEquals(realPose.x, pose.x, 0.01)
232
+ assertEquals(realPose.y, pose.y, 0.01)
233
+ assertEquals(0.0, pose.z, 0.01)
234
+
235
+
236
+ def test_multiTagOnCoprocStrategy():
237
+ cameraOne = PhotonCameraInjector()
238
+ cameraOne.result = PhotonPipelineResult(
239
+ int(11 * 1e6),
240
+ # There needs to be at least one target present for pose estimation to work
241
+ # Doesn't matter which/how many targets for this test
242
+ [
243
+ PhotonTrackedTarget(
244
+ 3.0,
245
+ -4.0,
246
+ 9.0,
247
+ 4.0,
248
+ 0,
249
+ Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
250
+ Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
251
+ [
252
+ TargetCorner(1, 2),
253
+ TargetCorner(3, 4),
254
+ TargetCorner(5, 6),
255
+ TargetCorner(7, 8),
256
+ ],
257
+ [
258
+ TargetCorner(1, 2),
259
+ TargetCorner(3, 4),
260
+ TargetCorner(5, 6),
261
+ TargetCorner(7, 8),
262
+ ],
263
+ 0.7,
264
+ )
265
+ ],
266
+ metadata=PhotonPipelineMetadata(0, int(2 * 1e3), 0),
267
+ multitagResult=MultiTargetPNPResult(
268
+ PnpResult(Transform3d(1, 3, 2, Rotation3d()))
269
+ ),
270
+ )
271
+
272
+ estimator = PhotonPoseEstimator(
273
+ AprilTagFieldLayout(),
274
+ PoseStrategy.MULTI_TAG_PNP_ON_COPROCESSOR,
275
+ cameraOne,
276
+ Transform3d(),
277
+ )
278
+
279
+ estimatedPose = estimator.update()
280
+
281
+ assert estimatedPose is not None
282
+
283
+ pose = estimatedPose.estimatedPose
284
+
285
+ assertEquals(11 - 2e-3, estimatedPose.timestampSeconds, 1e-3)
286
+ assertEquals(1, pose.x, 0.01)
287
+ assertEquals(3, pose.y, 0.01)
288
+ assertEquals(2, pose.z, 0.01)
289
+
290
+
291
+ def test_cacheIsInvalidated():
292
+ aprilTags = fakeAprilTagFieldLayout()
293
+ 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)
322
+ result = PhotonPipelineResult(
323
+ timestamps.receiveTimestampMicros(),
324
+ [
325
+ PhotonTrackedTarget(
326
+ 3.0,
327
+ -4.0,
328
+ 9.0,
329
+ 4.0,
330
+ 0,
331
+ Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
332
+ Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
333
+ [
334
+ TargetCorner(1, 2),
335
+ TargetCorner(3, 4),
336
+ TargetCorner(5, 6),
337
+ TargetCorner(7, 8),
338
+ ],
339
+ [
340
+ TargetCorner(1, 2),
341
+ TargetCorner(3, 4),
342
+ TargetCorner(5, 6),
343
+ TargetCorner(7, 8),
344
+ ],
345
+ 0.7,
346
+ )
347
+ ],
348
+ metadata=timestamps.toPhotonPipelineMetadata(),
349
+ )
350
+ cameraOne.result = result
351
+ estimatedPose = estimator.update()
352
+ assert estimatedPose is not None
353
+ expectedTimestamp = timestamps.receiveTimestampMicros() * 1e-6 - latencySecs
354
+ assertEquals(expectedTimestamp, estimatedPose.timestampSeconds, 1e-3)
355
+ assertEquals(expectedTimestamp, estimator._poseCacheTimestampSeconds, 1e-3)
356
+
357
+ # And again -- pose cache should mean this is empty
358
+ cameraOne.result = result
359
+ estimatedPose = estimator.update()
360
+ assert estimatedPose is None
361
+ # Expect the old timestamp to still be here
362
+ assertEquals(expectedTimestamp, estimator._poseCacheTimestampSeconds, 1e-3)
363
+
364
+ # Set new field layout -- right after, the pose cache timestamp should be -1
365
+ estimator.fieldTags = AprilTagFieldLayout([AprilTag()], 0, 0)
366
+ assertEquals(-1, estimator._poseCacheTimestampSeconds)
367
+ # Update should cache the current timestamp (20) again
368
+ cameraOne.result = result
369
+ estimatedPose = estimator.update()
370
+
371
+ assert estimatedPose is not None
372
+
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)
381
+
382
+
383
+ def assertEquals(expected, actual, epsilon=0.0):
384
+ assert abs(expected - actual) <= epsilon
@@ -0,0 +1,42 @@
1
+ ###############################################################################
2
+ ## Copyright (C) Photon Vision.
3
+ ###############################################################################
4
+ ## This program is free software: you can redistribute it and/or modify
5
+ ## it under the terms of the GNU General Public License as published by
6
+ ## the Free Software Foundation, either version 3 of the License, or
7
+ ## (at your option) any later version.
8
+ ##
9
+ ## This program is distributed in the hope that it will be useful,
10
+ ## but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ ## GNU General Public License for more details.
13
+ ##
14
+ ## You should have received a copy of the GNU General Public License
15
+ ## along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ ###############################################################################
17
+
18
+ from time import sleep
19
+
20
+ import ntcore
21
+ from photonlibpy import PhotonCamera
22
+ from photonlibpy.photonCamera import setVersionCheckEnabled
23
+
24
+
25
+ def test_roundTrip():
26
+ ntcore.NetworkTableInstance.getDefault().stopServer()
27
+ ntcore.NetworkTableInstance.getDefault().setServer("localhost")
28
+ ntcore.NetworkTableInstance.getDefault().startClient4("meme")
29
+
30
+ camera = PhotonCamera("WPI2024")
31
+
32
+ setVersionCheckEnabled(False)
33
+
34
+ for i in range(5):
35
+ sleep(0.1)
36
+ result = camera.getLatestResult()
37
+ print(result)
38
+ print(camera._rawBytesEntry.getTopic().getProperties())
39
+
40
+
41
+ if __name__ == "__main__":
42
+ test_roundTrip()
@@ -0,0 +1,99 @@
1
+ import math
2
+
3
+ import numpy as np
4
+ import pytest
5
+ from photonlibpy.estimation import RotTrlTransform3d
6
+ from photonlibpy.simulation import SimCameraProperties
7
+ from wpimath.geometry import Rotation2d, Translation3d
8
+
9
+
10
+ @pytest.fixture(autouse=True)
11
+ def scp() -> SimCameraProperties:
12
+ props = SimCameraProperties()
13
+ props.setCalibrationFromFOV(1000, 1000, fovDiag=Rotation2d(math.radians(90.0)))
14
+ return props
15
+
16
+
17
+ def test_GetPixelYaw(scp) -> None:
18
+ rot = scp.getPixelYaw(scp.getResWidth() / 2)
19
+ assert rot.degrees() == pytest.approx(0.0, abs=1.0)
20
+ rot = scp.getPixelYaw(0.0)
21
+ # FOV is square
22
+ assert rot.degrees() == pytest.approx(45.0 / math.sqrt(2.0), abs=5.0)
23
+ rot = scp.getPixelYaw(scp.getResWidth())
24
+ assert rot.degrees() == pytest.approx(-45.0 / math.sqrt(2.0), abs=5.0)
25
+
26
+
27
+ def test_GetPixelPitch(scp) -> None:
28
+ rot = scp.getPixelPitch(scp.getResHeight() / 2)
29
+ assert rot.degrees() == pytest.approx(0.0, abs=1.0)
30
+ rot = scp.getPixelPitch(0.0)
31
+ # FOV is square
32
+ assert rot.degrees() == pytest.approx(-45.0 / math.sqrt(2.0), abs=5.0)
33
+ rot = scp.getPixelPitch(scp.getResHeight())
34
+ assert rot.degrees() == pytest.approx(45.0 / math.sqrt(2.0), abs=5.0)
35
+
36
+
37
+ def test_GetPixelRot(scp) -> None:
38
+ rot = scp.getPixelRot(np.array([scp.getResWidth() / 2.0, scp.getResHeight() / 2.0]))
39
+ assert rot.x_degrees == pytest.approx(0.0, abs=5)
40
+ assert rot.y_degrees == pytest.approx(0.0, abs=5)
41
+ assert rot.z_degrees == pytest.approx(0.0, abs=5)
42
+ rot = scp.getPixelRot(np.array([0.0, 0.0]))
43
+ assert rot.x_degrees == pytest.approx(0.0, abs=5)
44
+ assert rot.y_degrees == pytest.approx(-45.0 / math.sqrt(2.0), abs=5)
45
+ assert rot.z_degrees == pytest.approx(45.0 / math.sqrt(2.0), abs=5)
46
+ rot = scp.getPixelRot(np.array([scp.getResWidth(), scp.getResHeight()]))
47
+ assert rot.x_degrees == pytest.approx(0.0, abs=5)
48
+ assert rot.y_degrees == pytest.approx(45.0 / math.sqrt(2.0), abs=5)
49
+ assert rot.z_degrees == pytest.approx(-45.0 / math.sqrt(2.0), abs=5)
50
+
51
+
52
+ def test_GetCorrectedPixelRot(scp) -> None:
53
+ rot = scp.getCorrectedPixelRot(
54
+ np.array([scp.getResWidth() / 2.0, scp.getResHeight() / 2.0])
55
+ )
56
+ assert rot.x_degrees == pytest.approx(0.0, abs=5)
57
+ assert rot.y_degrees == pytest.approx(0.0, abs=5)
58
+ assert rot.z_degrees == pytest.approx(0.0, abs=5)
59
+ rot = scp.getCorrectedPixelRot(np.array([0.0, 0.0]))
60
+ assert rot.x_degrees == pytest.approx(0.0, abs=5)
61
+ assert rot.y_degrees == pytest.approx(-45.0 / math.sqrt(2.0), abs=5)
62
+ assert rot.z_degrees == pytest.approx(45.0 / math.sqrt(2.0), abs=5)
63
+ rot = scp.getCorrectedPixelRot(np.array([scp.getResWidth(), scp.getResHeight()]))
64
+ assert rot.x_degrees == pytest.approx(0.0, abs=5)
65
+ assert rot.y_degrees == pytest.approx(45.0 / math.sqrt(2.0), abs=5)
66
+ assert rot.z_degrees == pytest.approx(-45.0 / math.sqrt(2.0), abs=5)
67
+
68
+
69
+ def test_GetVisibleLine(scp) -> None:
70
+ camRt = RotTrlTransform3d()
71
+ a = Translation3d()
72
+ b = Translation3d()
73
+ retval = scp.getVisibleLine(camRt, a, b)
74
+ assert retval == (None, None)
75
+
76
+ a = Translation3d(-5.0, -0.1, 0)
77
+ b = Translation3d(5.0, 0.1, 0)
78
+ retval = scp.getVisibleLine(camRt, a, b)
79
+ assert retval == (0.5, 0.5)
80
+
81
+
82
+ def test_EstPixelNoise(scp) -> None:
83
+ with pytest.raises(Exception):
84
+ scp.test_EstPixelNoise(np.array([0, 0]))
85
+ with pytest.raises(Exception):
86
+ scp.test_EstPixelNoise(np.array([[0], [0]]))
87
+
88
+ pts = np.array([[[0, 0]], [[0, 0]]])
89
+
90
+ # No noise parameters set
91
+ noisy = scp.estPixelNoise(pts)
92
+ for n, p in zip(noisy, pts):
93
+ assert n.all() == p.all()
94
+
95
+ # Noise parameters set
96
+ scp.setCalibError(1.0, 1.0)
97
+ noisy = scp.estPixelNoise(pts)
98
+ for n, p in zip(noisy, pts):
99
+ assert n.any() != p.any()
test/testUtil.py ADDED
@@ -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
+ )