photonlibpy 2025.0.0b8__py3-none-any.whl → 2026.1.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.
photonlibpy/__init__.py CHANGED
@@ -1,21 +1,35 @@
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
- ###############################################################################
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
+ #
17
24
 
18
- from .estimatedRobotPose import EstimatedRobotPose # noqa
19
- from .packet import Packet # noqa
20
- from .photonCamera import PhotonCamera # noqa
21
- from .photonPoseEstimator import PhotonPoseEstimator, PoseStrategy # noqa
25
+ from .estimatedRobotPose import EstimatedRobotPose
26
+ from .packet import Packet
27
+ from .photonCamera import PhotonCamera
28
+ from .photonPoseEstimator import PhotonPoseEstimator
29
+
30
+ __all__ = (
31
+ "EstimatedRobotPose",
32
+ "Packet",
33
+ "PhotonCamera",
34
+ "PhotonPoseEstimator",
35
+ )
@@ -16,15 +16,11 @@
16
16
  ###############################################################################
17
17
 
18
18
  from dataclasses import dataclass
19
- from typing import TYPE_CHECKING
20
19
 
21
20
  from wpimath.geometry import Pose3d
22
21
 
23
22
  from .targeting.photonTrackedTarget import PhotonTrackedTarget
24
23
 
25
- if TYPE_CHECKING:
26
- from .photonPoseEstimator import PoseStrategy
27
-
28
24
 
29
25
  @dataclass
30
26
  class EstimatedRobotPose:
@@ -38,6 +34,3 @@ class EstimatedRobotPose:
38
34
 
39
35
  targetsUsed: list[PhotonTrackedTarget]
40
36
  """A list of the targets used to compute this pose"""
41
-
42
- strategy: "PoseStrategy"
43
- """The strategy actually used to produce this pose"""
@@ -1,19 +1,26 @@
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
- ###############################################################################
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
+ #
17
24
 
18
25
  ###############################################################################
19
26
  ## THIS FILE WAS AUTO-GENERATED BY ./photon-serde/generate_messages.py.
@@ -1,19 +1,26 @@
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
- ###############################################################################
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
+ #
17
24
 
18
25
  ###############################################################################
19
26
  ## THIS FILE WAS AUTO-GENERATED BY ./photon-serde/generate_messages.py.
@@ -1,19 +1,26 @@
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
- ###############################################################################
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
+ #
17
24
 
18
25
  ###############################################################################
19
26
  ## THIS FILE WAS AUTO-GENERATED BY ./photon-serde/generate_messages.py.
@@ -1,19 +1,26 @@
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
- ###############################################################################
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
+ #
17
24
 
18
25
  ###############################################################################
19
26
  ## THIS FILE WAS AUTO-GENERATED BY ./photon-serde/generate_messages.py.
@@ -1,19 +1,26 @@
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
- ###############################################################################
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
+ #
17
24
 
18
25
  ###############################################################################
19
26
  ## THIS FILE WAS AUTO-GENERATED BY ./photon-serde/generate_messages.py.
@@ -1,19 +1,26 @@
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
- ###############################################################################
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
+ #
17
24
 
18
25
  ###############################################################################
19
26
  ## THIS FILE WAS AUTO-GENERATED BY ./photon-serde/generate_messages.py.
@@ -47,6 +47,13 @@ class NTTopicSet:
47
47
 
48
48
  self.driverModeSubscriber.getTopic().publish().setDefault(False)
49
49
 
50
+ self.fpsLimitPublisher = self.subTable.getIntegerTopic("fpsLimit").publish()
51
+ self.fpsLimitSubscriber = self.subTable.getIntegerTopic(
52
+ "fpsLimitRequest"
53
+ ).subscribe(-1)
54
+
55
+ self.fpsLimitSubscriber.getTopic().publish().setDefault(-1)
56
+
50
57
  self.latencyMillisEntry = self.subTable.getDoubleTopic(
51
58
  "latencyMillis"
52
59
  ).publish()
@@ -18,6 +18,7 @@
18
18
  from enum import Enum
19
19
  from typing import List
20
20
 
21
+ import hal
21
22
  import ntcore
22
23
 
23
24
  # magical import to make serde stuff work
@@ -27,6 +28,7 @@ from wpilib import RobotController, Timer
27
28
 
28
29
  from .packet import Packet
29
30
  from .targeting.photonPipelineResult import PhotonPipelineResult
31
+ from .timesync.timeSyncServer import inst
30
32
  from .version import PHOTONLIB_VERSION # type: ignore[import-untyped]
31
33
 
32
34
 
@@ -47,6 +49,8 @@ def setVersionCheckEnabled(enabled: bool):
47
49
 
48
50
 
49
51
  class PhotonCamera:
52
+ instance_count = 1
53
+
50
54
  def __init__(self, cameraName: str):
51
55
  """Constructs a PhotonCamera from the name of the camera.
52
56
 
@@ -70,6 +74,12 @@ class PhotonCamera:
70
74
  self._driverModeSubscriber = self._cameraTable.getBooleanTopic(
71
75
  "driverMode"
72
76
  ).subscribe(False)
77
+ self._fpsLimitPublisher = self._cameraTable.getIntegerTopic(
78
+ "fpsLimitRequest"
79
+ ).publish()
80
+ self._fpsLimitSubscriber = self._cameraTable.getIntegerTopic(
81
+ "fpsLimit"
82
+ ).subscribe(-1)
73
83
  self._inputSaveImgEntry = self._cameraTable.getIntegerTopic(
74
84
  "inputSaveImgCmd"
75
85
  ).getEntry(0)
@@ -104,6 +114,16 @@ class PhotonCamera:
104
114
  self._prevHeartbeat = 0
105
115
  self._prevHeartbeatChangeTime = Timer.getFPGATimestamp()
106
116
 
117
+ # Start the time sync server
118
+ inst.start()
119
+
120
+ # Usage reporting
121
+ hal.report(
122
+ hal.tResourceType.kResourceType_PhotonCamera.value,
123
+ PhotonCamera.instance_count,
124
+ )
125
+ PhotonCamera.instance_count += 1
126
+
107
127
  def getAllUnreadResults(self) -> List[PhotonPipelineResult]:
108
128
  """
109
129
  The list of pipeline results sent by PhotonVision since the last call to getAllUnreadResults().
@@ -176,6 +196,22 @@ class PhotonCamera:
176
196
 
177
197
  self._driverModePublisher.set(driverMode)
178
198
 
199
+ def getFPSLimit(self) -> int:
200
+ """Returns the current FPS limit set on the camera.
201
+
202
+ :returns: The current FPS limit.
203
+ """
204
+
205
+ return self._fpsLimitSubscriber.get()
206
+
207
+ def setFPSLimit(self, fpsLimit: int) -> None:
208
+ """Sets the FPS limit on the camera.
209
+
210
+ :param fpsLimit: The FPS limit to set. Set to -1 for unlimited FPS.
211
+ """
212
+
213
+ self._fpsLimitPublisher.set(fpsLimit)
214
+
179
215
  def takeInputSnapshot(self) -> None:
180
216
  """Request the camera to save a new image file from the input camera stream with overlays. Images
181
217
  take up space in the filesystem of the PhotonCamera. Calling it frequently will fill up disk
@@ -15,52 +15,30 @@
15
15
  ## along with this program. If not, see <https://www.gnu.org/licenses/>.
16
16
  ###############################################################################
17
17
 
18
- import enum
19
18
  from typing import Optional
20
19
 
20
+ import hal
21
21
  import wpilib
22
+ import wpimath.units
22
23
  from robotpy_apriltag import AprilTagFieldLayout
23
- from wpimath.geometry import Pose2d, Pose3d, Transform3d
24
+ from wpimath.geometry import (
25
+ Pose2d,
26
+ Pose3d,
27
+ Rotation2d,
28
+ Rotation3d,
29
+ Transform3d,
30
+ Translation2d,
31
+ Translation3d,
32
+ )
33
+ from wpimath.interpolation import TimeInterpolatableRotation2dBuffer
24
34
 
25
35
  from .estimatedRobotPose import EstimatedRobotPose
26
- from .photonCamera import PhotonCamera
27
36
  from .targeting.photonPipelineResult import PhotonPipelineResult
28
37
 
29
38
 
30
- class PoseStrategy(enum.Enum):
31
- """
32
- Position estimation strategies that can be used by the PhotonPoseEstimator class.
33
- """
34
-
35
- LOWEST_AMBIGUITY = enum.auto()
36
- """Choose the Pose with the lowest ambiguity."""
37
-
38
- CLOSEST_TO_CAMERA_HEIGHT = enum.auto()
39
- """Choose the Pose which is closest to the camera height."""
40
-
41
- CLOSEST_TO_REFERENCE_POSE = enum.auto()
42
- """Choose the Pose which is closest to a set Reference position."""
43
-
44
- CLOSEST_TO_LAST_POSE = enum.auto()
45
- """Choose the Pose which is closest to the last pose calculated."""
46
-
47
- AVERAGE_BEST_TARGETS = enum.auto()
48
- """Return the average of the best target poses using ambiguity as weight."""
49
-
50
- MULTI_TAG_PNP_ON_COPROCESSOR = enum.auto()
51
- """
52
- Use all visible tags to compute a single pose estimate on coprocessor.
53
- This option needs to be enabled on the PhotonVision web UI as well.
54
- """
55
-
56
- MULTI_TAG_PNP_ON_RIO = enum.auto()
57
- """
58
- Use all visible tags to compute a single pose estimate.
59
- This runs on the RoboRIO, and can take a lot of time.
60
- """
61
-
62
-
63
39
  class PhotonPoseEstimator:
40
+ instance_count = 1
41
+
64
42
  """
65
43
  The PhotonPoseEstimator class filters or combines readings from all the AprilTags visible at a
66
44
  given timestamp on the field to produce a single robot in field pose, using the strategy set
@@ -70,8 +48,6 @@ class PhotonPoseEstimator:
70
48
  def __init__(
71
49
  self,
72
50
  fieldTags: AprilTagFieldLayout,
73
- strategy: PoseStrategy,
74
- camera: PhotonCamera,
75
51
  robotToCamera: Transform3d,
76
52
  ):
77
53
  """Create a new PhotonPoseEstimator.
@@ -80,23 +56,21 @@ class PhotonPoseEstimator:
80
56
  with respect to the FIRST field using the Field Coordinate System.
81
57
  Note that setting the origin of this layout object will affect the
82
58
  results from this class.
83
- :param strategy: The strategy it should use to determine the best pose.
84
- :param camera: PhotonCamera
85
59
  :param robotToCamera: Transform3d from the center of the robot to the camera mount position (i.e.,
86
60
  robot ➔ camera) in the Robot Coordinate System.
87
61
  """
88
62
  self._fieldTags = fieldTags
89
- self._primaryStrategy = strategy
90
- self._camera = camera
91
63
  self.robotToCamera = robotToCamera
92
64
 
93
- self._multiTagFallbackStrategy = PoseStrategy.LOWEST_AMBIGUITY
94
65
  self._reportedErrors: set[int] = set()
95
- self._poseCacheTimestampSeconds = -1.0
96
- self._lastPose: Optional[Pose3d] = None
97
- self._referencePose: Optional[Pose3d] = None
66
+ self._headingBuffer = TimeInterpolatableRotation2dBuffer(1)
98
67
 
99
- # TODO: Implement HAL reporting
68
+ # Usage reporting
69
+ hal.report(
70
+ hal.tResourceType.kResourceType_PhotonPoseEstimator.value,
71
+ PhotonPoseEstimator.instance_count,
72
+ )
73
+ PhotonPoseEstimator.instance_count += 1
100
74
 
101
75
  @property
102
76
  def fieldTags(self) -> AprilTagFieldLayout:
@@ -116,160 +90,120 @@ class PhotonPoseEstimator:
116
90
 
117
91
  :param fieldTags: the AprilTagFieldLayout
118
92
  """
119
- self._checkUpdate(self._fieldTags, fieldTags)
120
93
  self._fieldTags = fieldTags
121
94
 
122
- @property
123
- def primaryStrategy(self) -> PoseStrategy:
124
- """Get the Position Estimation Strategy being used by the Position Estimator.
125
-
126
- :returns: the strategy
95
+ def addHeadingData(
96
+ self, timestampSeconds: wpimath.units.seconds, heading: Rotation2d | Rotation3d
97
+ ) -> None:
127
98
  """
128
- return self._primaryStrategy
129
-
130
- @primaryStrategy.setter
131
- def primaryStrategy(self, strategy: PoseStrategy):
132
- """Set the Position Estimation Strategy used by the Position Estimator.
99
+ Add robot heading data to buffer. Must be called periodically for the **PNP_DISTANCE_TRIG_SOLVE** strategy.
133
100
 
134
- :param strategy: the strategy to set
101
+ :param timestampSeconds :timestamp of the robot heading data
102
+ :param heading: field-relative robot heading at given timestamp
135
103
  """
136
- self._checkUpdate(self._primaryStrategy, strategy)
137
- self._primaryStrategy = strategy
104
+ if isinstance(heading, Rotation3d):
105
+ heading = heading.toRotation2d()
106
+ self._headingBuffer.addSample(timestampSeconds, heading)
138
107
 
139
- @property
140
- def multiTagFallbackStrategy(self) -> PoseStrategy:
141
- return self._multiTagFallbackStrategy
142
-
143
- @multiTagFallbackStrategy.setter
144
- def multiTagFallbackStrategy(self, strategy: PoseStrategy):
145
- """Set the Position Estimation Strategy used in multi-tag mode when only one tag can be seen. Must
146
- NOT be MULTI_TAG_PNP
147
-
148
- :param strategy: the strategy to set
108
+ def resetHeadingData(
109
+ self, timestampSeconds: wpimath.units.seconds, heading: Rotation2d | Rotation3d
110
+ ) -> None:
149
111
  """
150
- self._checkUpdate(self._multiTagFallbackStrategy, strategy)
151
- if (
152
- strategy is PoseStrategy.MULTI_TAG_PNP_ON_COPROCESSOR
153
- or strategy is PoseStrategy.MULTI_TAG_PNP_ON_RIO
154
- ):
155
- wpilib.reportWarning(
156
- "Fallback cannot be set to MULTI_TAG_PNP! Setting to lowest ambiguity",
157
- False,
158
- )
159
- strategy = PoseStrategy.LOWEST_AMBIGUITY
160
- self._multiTagFallbackStrategy = strategy
112
+ Clears all heading data in the buffer, and adds a new seed. Useful for preventing estimates
113
+ from utilizing heading data provided prior to a pose or rotation reset.
161
114
 
162
- @property
163
- def referencePose(self) -> Optional[Pose3d]:
164
- """Return the reference position that is being used by the estimator.
165
-
166
- :returns: the referencePose
115
+ :param timestampSeconds: timestamp of the robot heading data
116
+ :param heading: field-relative robot heading at given timestamp
167
117
  """
168
- return self._referencePose
169
-
170
- @referencePose.setter
171
- def referencePose(self, referencePose: Pose3d | Pose2d):
172
- """Update the stored reference pose for use when using the **CLOSEST_TO_REFERENCE_POSE**
173
- strategy.
118
+ self._headingBuffer.clear()
119
+ self.addHeadingData(timestampSeconds, heading)
174
120
 
175
- :param referencePose: the referencePose to set
121
+ def _shouldEstimate(self, cameraResult: PhotonPipelineResult) -> bool:
176
122
  """
177
- if isinstance(referencePose, Pose2d):
178
- referencePose = Pose3d(referencePose)
179
- self._checkUpdate(self._referencePose, referencePose)
180
- self._referencePose = referencePose
123
+ :param cameraResult: A pipeline result from the camera.
181
124
 
182
- @property
183
- def lastPose(self) -> Optional[Pose3d]:
184
- return self._lastPose
185
-
186
- @lastPose.setter
187
- def lastPose(self, lastPose: Pose3d | Pose2d):
188
- """Update the stored last pose. Useful for setting the initial estimate when using the
189
- **CLOSEST_TO_LAST_POSE** strategy.
190
-
191
- :param lastPose: the lastPose to set
125
+ :returns: Whether or not estimation should be done.
192
126
  """
193
- if isinstance(lastPose, Pose2d):
194
- lastPose = Pose3d(lastPose)
195
- self._checkUpdate(self._lastPose, lastPose)
196
- self._lastPose = lastPose
197
-
198
- def _invalidatePoseCache(self) -> None:
199
- self._poseCacheTimestampSeconds = -1.0
127
+ if cameraResult.getTimestampSeconds() < 0:
128
+ return False
200
129
 
201
- def _checkUpdate(self, oldObj, newObj) -> None:
202
- if oldObj != newObj and oldObj is not None and oldObj is not newObj:
203
- self._invalidatePoseCache()
130
+ # If no targets seen, trivial case -- can't do estimation
131
+ return len(cameraResult.targets) > 0
204
132
 
205
- def update(
206
- self, cameraResult: Optional[PhotonPipelineResult] = None
133
+ def estimatePnpDistanceTrigSolvePose(
134
+ self, result: PhotonPipelineResult
207
135
  ) -> Optional[EstimatedRobotPose]:
208
136
  """
209
- Updates the estimated position of the robot. Returns empty if:
210
137
 
211
- - The timestamp of the provided pipeline result is the same as in the previous call to
212
- ``update()``.
138
+ Return the estimated position of the robot by using distance data from best visible tag to
139
+ compute a Pose. This runs on the RoboRIO in order to access the robot's yaw heading, and MUST
140
+ have addHeadingData called every frame so heading data is up-to-date.
213
141
 
214
- - No targets were found in the pipeline results.
142
+ Yields a Pose2d in estimatedRobotPose (0 for z, roll, pitch)
215
143
 
216
- :param cameraResult: The latest pipeline result from the camera
144
+ https://www.chiefdelphi.com/t/frc-6328-mechanical-advantage-2025-build-thread/477314/98
217
145
 
218
- :returns: an :class:`EstimatedRobotPose` with an estimated pose, timestamp, and targets used to
219
- create the estimate.
220
- """
221
- if not cameraResult:
222
- if not self._camera:
223
- wpilib.reportError("[PhotonPoseEstimator] Missing camera!", False)
224
- return None
225
- cameraResult = self._camera.getLatestResult()
226
146
 
227
- if cameraResult.getTimestampSeconds() < 0:
228
- return None
229
-
230
- # If the pose cache timestamp was set, and the result is from the same
231
- # timestamp, return an
232
- # empty result
147
+ :param result: A pipeline result from the camera.
148
+ :returns: An :class:`EstimatedRobotPose` with an estimated pose, timestamp, and targets used
149
+ to create the estimate.
150
+ """
233
151
  if (
234
- self._poseCacheTimestampSeconds > 0.0
235
- and abs(
236
- self._poseCacheTimestampSeconds - cameraResult.getTimestampSeconds()
237
- )
238
- < 1e-6
152
+ not self._shouldEstimate(result)
153
+ or (bestTarget := result.getBestTarget()) is None
239
154
  ):
240
155
  return None
241
156
 
242
- # Remember the timestamp of the current result used
243
- self._poseCacheTimestampSeconds = cameraResult.getTimestampSeconds()
244
-
245
- # If no targets seen, trivial case -- return empty result
246
- if not cameraResult.targets:
157
+ if (
158
+ headingSample := self._headingBuffer.sample(result.getTimestampSeconds())
159
+ ) is None:
247
160
  return None
248
161
 
249
- return self._update(cameraResult, self._primaryStrategy)
162
+ if (tagPose := self._fieldTags.getTagPose(bestTarget.fiducialId)) is None:
163
+ return None
250
164
 
251
- def _update(
252
- self, cameraResult: PhotonPipelineResult, strat: PoseStrategy
253
- ) -> Optional[EstimatedRobotPose]:
254
- if strat is PoseStrategy.LOWEST_AMBIGUITY:
255
- estimatedPose = self._lowestAmbiguityStrategy(cameraResult)
256
- elif strat is PoseStrategy.MULTI_TAG_PNP_ON_COPROCESSOR:
257
- estimatedPose = self._multiTagOnCoprocStrategy(cameraResult)
258
- else:
259
- wpilib.reportError(
260
- "[PhotonPoseEstimator] Unknown Position Estimation Strategy!", False
165
+ camToTagTranslation = (
166
+ Translation3d(
167
+ bestTarget.getBestCameraToTarget().translation().norm(),
168
+ Rotation3d(
169
+ 0,
170
+ -wpimath.units.degreesToRadians(bestTarget.getPitch()),
171
+ -wpimath.units.degreesToRadians(bestTarget.getYaw()),
172
+ ),
261
173
  )
262
- return None
174
+ .rotateBy(self.robotToCamera.rotation())
175
+ .toTranslation2d()
176
+ .rotateBy(headingSample)
177
+ )
263
178
 
264
- if not estimatedPose:
265
- self._lastPose = None
179
+ fieldToCameraTranslation = (
180
+ tagPose.toPose2d().translation() - camToTagTranslation
181
+ )
182
+ camToRobotTranslation: Translation2d = -(
183
+ self.robotToCamera.translation().toTranslation2d()
184
+ )
185
+ camToRobotTranslation = camToRobotTranslation.rotateBy(headingSample)
186
+ robotPose = Pose2d(
187
+ fieldToCameraTranslation + camToRobotTranslation, headingSample
188
+ )
266
189
 
267
- return estimatedPose
190
+ return EstimatedRobotPose(
191
+ Pose3d(robotPose), result.getTimestampSeconds(), result.getTargets()
192
+ )
268
193
 
269
- def _multiTagOnCoprocStrategy(
194
+ def estimateCoprocMultiTagPose(
270
195
  self, result: PhotonPipelineResult
271
196
  ) -> Optional[EstimatedRobotPose]:
272
- if result.multitagResult is not None:
197
+ """
198
+ Return the estimated position of the robot by using all visible tags to compute a single
199
+ pose estimate on coprocessor. This option needs to be enabled on the PhotonVision web UI as
200
+ well.
201
+
202
+ :param result: A pipeline result from the camera.
203
+ :returns: An :class:`EstimatedRobotPose` with an estimated pose, timestamp, and targets used
204
+ to create the estimate.
205
+ """
206
+ if result.multitagResult is not None and self._shouldEstimate(result):
273
207
  best_tf = result.multitagResult.estimatedPose.best
274
208
  best = (
275
209
  Pose3d()
@@ -281,23 +215,22 @@ class PhotonPoseEstimator:
281
215
  best,
282
216
  result.getTimestampSeconds(),
283
217
  result.targets,
284
- PoseStrategy.MULTI_TAG_PNP_ON_COPROCESSOR,
285
218
  )
286
219
  else:
287
- return self._update(result, self._multiTagFallbackStrategy)
220
+ return None
288
221
 
289
- def _lowestAmbiguityStrategy(
222
+ def estimateLowestAmbiguityPose(
290
223
  self, result: PhotonPipelineResult
291
224
  ) -> Optional[EstimatedRobotPose]:
292
225
  """
293
- Return the estimated position of the robot with the lowest position ambiguity from a List of
294
- pipeline results.
295
-
296
- :param result: pipeline result
226
+ Return the estimated position of the robot with the lowest position ambiguity from a pipeline results.
297
227
 
298
- :returns: the estimated position of the robot in the FCS and the estimated timestamp of this
299
- estimation.
228
+ :param result: A pipeline result from the camera.
229
+ :returns: An :class:`EstimatedRobotPose` with an estimated pose, timestamp, and targets used
230
+ to create the estimate.
300
231
  """
232
+ if not self._shouldEstimate(result):
233
+ return None
301
234
  lowestAmbiguityTarget = None
302
235
 
303
236
  lowestAmbiguityScore = 10.0
@@ -328,7 +261,6 @@ class PhotonPoseEstimator:
328
261
  ).transformBy(self.robotToCamera.inverse()),
329
262
  result.getTimestampSeconds(),
330
263
  result.targets,
331
- PoseStrategy.LOWEST_AMBIGUITY,
332
264
  )
333
265
 
334
266
  def _reportFiducialPoseError(self, fiducialId: int) -> None:
photonlibpy/py.typed CHANGED
@@ -1 +0,0 @@
1
-
@@ -3,3 +3,11 @@ from .simCameraProperties import SimCameraProperties
3
3
  from .videoSimUtil import VideoSimUtil
4
4
  from .visionSystemSim import VisionSystemSim
5
5
  from .visionTargetSim import VisionTargetSim
6
+
7
+ __all__ = (
8
+ "PhotonCameraSim",
9
+ "SimCameraProperties",
10
+ "VideoSimUtil",
11
+ "VisionSystemSim",
12
+ "VisionTargetSim",
13
+ )
@@ -36,6 +36,9 @@ class PhotonCameraSim:
36
36
  self,
37
37
  camera: PhotonCamera,
38
38
  props: SimCameraProperties = SimCameraProperties.PERFECT_90DEG(),
39
+ tagLayout: AprilTagFieldLayout = AprilTagFieldLayout.loadField(
40
+ AprilTagField.kDefaultField
41
+ ),
39
42
  minTargetAreaPercent: float | None = None,
40
43
  maxSightRange: meters | None = None,
41
44
  ):
@@ -48,7 +51,7 @@ class PhotonCameraSim:
48
51
 
49
52
  :param camera: The camera to be simulated
50
53
  :param prop: Properties of this camera such as FOV and FPS
51
- :param minTargetAreaPercent: The minimum percentage(0 - 100) a detected target must take up of
54
+ :param minTargetAreaPercent: The minimum percentage (0 - 100) a detected target must take up of
52
55
  the camera's image to be processed. Match this with your contour filtering settings in the
53
56
  PhotonVision GUI.
54
57
  :param maxSightRangeMeters: Maximum distance at which the target is illuminated to your camera.
@@ -64,7 +67,7 @@ class PhotonCameraSim:
64
67
  self.videoSimProcEnabled: bool = False
65
68
  self.heartbeatCounter: int = 0
66
69
  self.nextNtEntryTime = wpilib.Timer.getFPGATimestamp()
67
- self.tagLayout = AprilTagFieldLayout.loadField(AprilTagField.k2024Crescendo)
70
+ self.tagLayout = tagLayout
68
71
 
69
72
  self.cam = camera
70
73
  self.prop = props
@@ -203,7 +206,7 @@ class PhotonCameraSim:
203
206
  return None
204
207
 
205
208
  def setMinTargetAreaPercent(self, areaPercent: float) -> None:
206
- """The minimum percentage(0 - 100) a detected target must take up of the camera's image to be
209
+ """The minimum percentage (0 - 100) a detected target must take up of the camera's image to be
207
210
  processed.
208
211
  """
209
212
  self.minTargetAreaPercent = areaPercent
@@ -268,6 +271,9 @@ class PhotonCameraSim:
268
271
  camRt = RotTrlTransform3d.makeRelativeTo(cameraPose)
269
272
 
270
273
  for tgt in targets:
274
+ if len(detectableTgts) >= 50:
275
+ break
276
+
271
277
  # pose isn't visible, skip to next
272
278
  if not self.canSeeTargetPose(cameraPose, tgt):
273
279
  continue
@@ -417,14 +423,15 @@ class PhotonCameraSim:
417
423
 
418
424
  # put this simulated data to NT
419
425
  self.heartbeatCounter += 1
420
- now_micros = wpilib.Timer.getFPGATimestamp() * 1e6
426
+ publishTimestampMicros = wpilib.Timer.getFPGATimestamp() * 1e6
421
427
  return PhotonPipelineResult(
428
+ ntReceiveTimestampMicros=int(publishTimestampMicros + 10),
422
429
  metadata=PhotonPipelineMetadata(
423
- self.heartbeatCounter,
424
- int(now_micros - latency * 1e6),
425
- int(now_micros),
430
+ captureTimestampMicros=int(publishTimestampMicros - latency * 1e6),
431
+ publishTimestampMicros=int(publishTimestampMicros),
432
+ sequenceID=self.heartbeatCounter,
426
433
  # Pretend like we heard a pong recently
427
- int(np.random.uniform(950, 1050)),
434
+ timeSinceLastPong=int(np.random.uniform(950, 1050)),
428
435
  ),
429
436
  targets=detectableTgts,
430
437
  multitagResult=multiTagResults,
@@ -474,11 +481,11 @@ class PhotonCameraSim:
474
481
 
475
482
  intrinsics = self.prop.getIntrinsics()
476
483
  intrinsicsView = intrinsics.flatten().tolist()
477
- self.ts.cameraIntrinsicsPublisher.set(intrinsicsView, receiveTimestamp_us)
484
+ self.ts.cameraIntrinsicsPublisher.set(list(intrinsicsView), receiveTimestamp_us)
478
485
 
479
486
  distortion = self.prop.getDistCoeffs()
480
487
  distortionView = distortion.flatten().tolist()
481
- self.ts.cameraDistortionPublisher.set(distortionView, receiveTimestamp_us)
488
+ self.ts.cameraDistortionPublisher.set(list(distortionView), receiveTimestamp_us)
482
489
 
483
490
  self.ts.heartbeatPublisher.set(self.heartbeatCounter, receiveTimestamp_us)
484
491
  self.heartbeatCounter += 1
@@ -195,7 +195,7 @@ class SimCameraProperties:
195
195
  return self.latencyStdDev
196
196
 
197
197
  def getContourAreaPercent(self, points: np.ndarray) -> float:
198
- """The percentage(0 - 100) of this camera's resolution the contour takes up in pixels of the
198
+ """The percentage (0 - 100) of this camera's resolution the contour takes up in pixels of the
199
199
  image.
200
200
 
201
201
  :param points: Points of the contour
@@ -1,6 +1,13 @@
1
- # no one but us chickens
1
+ from .multiTargetPNPResult import MultiTargetPNPResult, PnpResult
2
+ from .photonPipelineResult import PhotonPipelineMetadata, PhotonPipelineResult
3
+ from .photonTrackedTarget import PhotonTrackedTarget
4
+ from .TargetCorner import TargetCorner
2
5
 
3
- from .multiTargetPNPResult import MultiTargetPNPResult, PnpResult # noqa
4
- from .photonPipelineResult import PhotonPipelineMetadata, PhotonPipelineResult # noqa
5
- from .photonTrackedTarget import PhotonTrackedTarget # noqa
6
- from .TargetCorner import TargetCorner # noqa
6
+ __all__ = (
7
+ "MultiTargetPNPResult",
8
+ "PnpResult",
9
+ "PhotonPipelineMetadata",
10
+ "PhotonPipelineResult",
11
+ "PhotonTrackedTarget",
12
+ "TargetCorner",
13
+ )
@@ -47,13 +47,10 @@ class PhotonPipelineResult:
47
47
  timestamp, coproc timebase))
48
48
  """
49
49
  # TODO - we don't trust NT4 to correctly latency-compensate ntReceiveTimestampMicros
50
- return (
51
- self.ntReceiveTimestampMicros
52
- - (
53
- self.metadata.publishTimestampMicros
54
- - self.metadata.captureTimestampMicros
55
- )
56
- ) / 1e6
50
+ latency = (
51
+ self.metadata.publishTimestampMicros - self.metadata.captureTimestampMicros
52
+ )
53
+ return (self.ntReceiveTimestampMicros - latency) / 1e6
57
54
 
58
55
  def getTargets(self) -> list[PhotonTrackedTarget]:
59
56
  return self.targets
@@ -0,0 +1 @@
1
+ # no one but us chickens
@@ -0,0 +1,94 @@
1
+ import logging
2
+ import socket
3
+ import struct
4
+ import threading
5
+ from typing import Callable, Optional
6
+
7
+ from wpilib import Timer
8
+
9
+
10
+ class TspPing:
11
+ def __init__(self, version: int, message_id: int, client_time: int):
12
+ self.version = version
13
+ self.message_id = message_id
14
+ self.client_time = client_time
15
+
16
+ @staticmethod
17
+ def unpack(data: bytes) -> "TspPing":
18
+ # Unpack using struct.unpack
19
+ version, message_id, client_time = struct.unpack("<BBQ", data)
20
+ return TspPing(version, message_id, client_time)
21
+
22
+ def pack(self) -> bytes:
23
+ # Pack using struct.pack
24
+ return struct.pack("<BBQ", self.version, self.message_id, self.client_time)
25
+
26
+
27
+ class TspPong:
28
+ def __init__(self, ping: "TspPing", server_time: int):
29
+ self.version = ping.version
30
+ self.message_id = 2 # Pong message ID
31
+ self.client_time = ping.client_time
32
+ self.server_time = server_time
33
+
34
+ def pack(self) -> bytes:
35
+ # Pack using struct.pack
36
+ return struct.pack(
37
+ "<BBQQ", self.version, self.message_id, self.client_time, self.server_time
38
+ )
39
+
40
+ @staticmethod
41
+ def unpack(data: bytes) -> "TspPong":
42
+ # Unpack using struct.unpack
43
+ version, message_id, client_time, server_time = struct.unpack("<BBQQ", data)
44
+ ping = TspPing(version, message_id, client_time)
45
+ return TspPong(ping, server_time)
46
+
47
+
48
+ class TimeSyncServer:
49
+ """This class is a python re-write of the UDP time sync server protocol
50
+ which runs on a roboRIO to establish a timebase for all PhotonVision coprocessors.
51
+ """
52
+
53
+ PORT = 5810
54
+
55
+ def __init__(self, time_provider: Optional[Callable[[], int]] = None):
56
+ self.time_provider = time_provider or Timer.getFPGATimestamp
57
+ self._process: Optional[threading.Thread] = None
58
+ self.logger = logging.getLogger("PhotonVision-TimeSyncServer")
59
+
60
+ def _udp_server(self):
61
+ with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as udp_socket:
62
+ udp_socket.bind(("0.0.0.0", self.PORT))
63
+ while True:
64
+ data, addr = udp_socket.recvfrom(1024) # Buffer size of 1024 bytes
65
+
66
+ if len(data) < 10:
67
+ self.logger.error("Too few bytes")
68
+ continue # Ignore incomplete packets
69
+
70
+ ping = TspPing.unpack(data)
71
+ if ping.version != 1 or ping.message_id != 1:
72
+ self.logger.error("Invalid Version/ID")
73
+ continue # Ignore invalid pings
74
+
75
+ server_time = int(self.time_provider() * 1e6) # Convert to microseconds
76
+ pong = TspPong(ping, server_time)
77
+ udp_socket.sendto(pong.pack(), addr)
78
+
79
+ def start(self):
80
+ if self._process is not None and self._process.is_alive():
81
+ return # Nothing to do
82
+
83
+ self._process = threading.Thread(target=self._udp_server, daemon=True)
84
+ self._process.start()
85
+ self.logger.info("Server Started")
86
+
87
+ def stop(self):
88
+ if self._process is not None:
89
+ self._process.join()
90
+ self._process = None
91
+ self.logger.info("Server Stopped")
92
+
93
+
94
+ inst = TimeSyncServer()
photonlibpy/version.py CHANGED
@@ -1,2 +1,2 @@
1
- PHOTONLIB_VERSION="v2025.0.0.beta.8"
2
- PHOTONVISION_VERSION="v2025.0.0-beta-8"
1
+ PHOTONLIB_VERSION="2026.1.1"
2
+ PHOTONVISION_VERSION="v2026.1.1"
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: photonlibpy
3
+ Version: 2026.1.1
4
+ Summary: Pure-python implementation of PhotonLib for interfacing with PhotonVision on coprocessors. Implemented with PhotonVision version v2026.1.1 .
5
+ Home-page: https://photonvision.org
6
+ Author: Photonvision Development Team
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: numpy~=2.3
10
+ Requires-Dist: wpilib<2027,>=2026.2.1
11
+ Requires-Dist: robotpy-wpimath<2027,>=2026.2.1
12
+ Requires-Dist: robotpy-apriltag<2027,>=2026.2.1
13
+ Requires-Dist: robotpy-cscore<2027,>=2026.2.1
14
+ Requires-Dist: pyntcore<2027,>=2026.2.1
15
+ Requires-Dist: opencv-python; platform_machine != "roborio"
16
+ Dynamic: author
17
+ Dynamic: classifier
18
+ Dynamic: description
19
+ Dynamic: description-content-type
20
+ Dynamic: home-page
21
+ Dynamic: requires-dist
22
+ Dynamic: summary
23
+
24
+ A Pure-python implementation of PhotonLib
@@ -0,0 +1,39 @@
1
+ photonlibpy/__init__.py,sha256=zPyxHwPCDSeCz7OeAAtzfn9-TnHNlG2qRyNQ8yPy0yQ,1379
2
+ photonlibpy/estimatedRobotPose.py,sha256=gKKALm4PcMDBzkF7WganGW9srONJQgQjZioHY588AHA,1413
3
+ photonlibpy/packet.py,sha256=5YomViVFwOljL2FGOetWM9FbPc_yCQ15ylzkYlgLIs8,9724
4
+ photonlibpy/photonCamera.py,sha256=chSwxxfCbunlam9t0BaE9deNMxJ6x9KyBPJBA9esir4,13908
5
+ photonlibpy/photonPoseEstimator.py,sha256=t10pyh38FLRMwvqAuz9rnf9MNaE8AEkBFhoYaODW3kI,10387
6
+ photonlibpy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ photonlibpy/version.py,sha256=6_AEn0cUFelka8gVEzXz3CFr_yA8RdW99pRU9O8et4s,62
8
+ photonlibpy/estimation/__init__.py,sha256=pZ-d6fN1DJvT-lRl4FfIos5HAvlzmetIOrGIinrdv7k,223
9
+ photonlibpy/estimation/cameraTargetRelation.py,sha256=i7DPBXtkZve4ToXQscEIe-5F1oGQ1Qmf5QBaE__EeMQ,1158
10
+ photonlibpy/estimation/openCVHelp.py,sha256=O1dV7v7RHSyw7l5L0QXbal6t9K7iyvEG76tG-t4AFVg,12388
11
+ photonlibpy/estimation/rotTrlTransform3d.py,sha256=oRYk4V1XF7guNxefJsCPcBWPIkKZl0HYD_Gs86wwToE,2671
12
+ photonlibpy/estimation/targetModel.py,sha256=fIhkf0-_Sfv-KhYEBnhFWMF4Xu84FfF6B-KUEsuuGjQ,6459
13
+ photonlibpy/estimation/visionEstimation.py,sha256=Q4KWdLCV1H0wJUJCzG7OYNhVmk2jR1iPUmX_PFylLdA,4174
14
+ photonlibpy/generated/MultiTargetPNPResultSerde.py,sha256=GH31IKMyHglymJ5phA2I-xJW3QmI9jEv-hmql6r6RoU,2693
15
+ photonlibpy/generated/PhotonPipelineMetadataSerde.py,sha256=RjYLPVE_Od-N2LfeI0_5wvtYKCAjOhArsNSZOcQShmY,3074
16
+ photonlibpy/generated/PhotonPipelineResultSerde.py,sha256=4TrLO8a61eLSfT_cux3Y4AAgjhDoGOiRCfyBLTL6mBg,3302
17
+ photonlibpy/generated/PhotonTrackedTargetSerde.py,sha256=vTuugrX6EzegQYOBspa5i0YAWXvru4NxaBlNkjmnEz0,4624
18
+ photonlibpy/generated/PnpResultSerde.py,sha256=YHKEGaTHNRvd0NMIpw-cO3hOuSqXnkPxmSi3v3nr5OE,2880
19
+ photonlibpy/generated/TargetCornerSerde.py,sha256=FJ6rPTGlwpp6ORUYU-y09xaZxBFlV1o1pft2uVHJMKQ,2377
20
+ photonlibpy/generated/__init__.py,sha256=mElM8M88---wxTWO-SRqIJ4EfxN0fdIUwZBZ-UIGuRw,428
21
+ photonlibpy/networktables/NTTopicSet.py,sha256=sSZtW23OJVNzpfwc2abx1lFW4RWrGpGeta62pKe18ZY,3241
22
+ photonlibpy/networktables/__init__.py,sha256=o_LxTdyIylAszMy_zhUtTkXHyu6jqxccccj78d44OrI,35
23
+ photonlibpy/simulation/__init__.py,sha256=HazsBMXg1HT8TnyxYO8QI9NXwZOrtuCSytnTdquLBKw,358
24
+ photonlibpy/simulation/photonCameraSim.py,sha256=RojKliUvmgc5UTdSKttvtj1CW7ANZyt11_xYqixamTs,20241
25
+ photonlibpy/simulation/simCameraProperties.py,sha256=qU-3zDpETKcmM23Mi1UvcL1uOE0buBYlblPlZrFtvDs,27196
26
+ photonlibpy/simulation/videoSimUtil.py,sha256=xMuTvJ2Jx9IoQqmAJi_zUm06MdEwhVpIz9OyzYQp0k4,29
27
+ photonlibpy/simulation/visionSystemSim.py,sha256=GmKs0d32WE8B020YEWnj-0dQuCnVv1ScGdcFl1fOsKo,13835
28
+ photonlibpy/simulation/visionTargetSim.py,sha256=FH85fKE4NntowUvssfgZ1KlE-I_3Z-QuAgb2bFqvfdY,2219
29
+ photonlibpy/targeting/TargetCorner.py,sha256=ouKj3E5uD76OZSNHHuSDzKOY65a8HqtcOsuejH-MVsU,276
30
+ photonlibpy/targeting/__init__.py,sha256=OxxkBvBa6sFdjG7T1hO8CwBkRHk6GYdXbVhVgYfW7Gc,402
31
+ photonlibpy/targeting/multiTargetPNPResult.py,sha256=Y9rweHtMzoCZ6mv6F8CutQi2Thq5pHN0ydBWvTCsOwY,806
32
+ photonlibpy/targeting/photonPipelineResult.py,sha256=H_wsIQfdo41xNT32YqfttH6AYQRZhAMR7XDiysMa3lw,2700
33
+ photonlibpy/targeting/photonTrackedTarget.py,sha256=zCoFp32hX-3GmBYEmsYBQieBoMzXtP2F_55_q0zPOXA,1956
34
+ photonlibpy/timesync/__init__.py,sha256=pECFraHRZC2Dl4x4OrmhW8oWkawP68pCb-Y61dZycJs,25
35
+ photonlibpy/timesync/timeSyncServer.py,sha256=0d76uHBS_Jo3etvougdZHapbRrbb31iu-jlf8h9U_QQ,3194
36
+ photonlibpy-2026.1.1.dist-info/METADATA,sha256=KdRf2WnQteiGxvdUy1bQpG0t-__rKPyi1SMDmg-16nM,871
37
+ photonlibpy-2026.1.1.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
38
+ photonlibpy-2026.1.1.dist-info/top_level.txt,sha256=T8Xc6U6he2VjKUAca6zawSkHdUZuLanxYIc4nxw2ctc,12
39
+ photonlibpy-2026.1.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.45.1)
2
+ Generator: setuptools (80.10.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,16 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: photonlibpy
3
- Version: 2025.0.0b8
4
- Summary: Pure-python implementation of PhotonLib for interfacing with PhotonVision on coprocessors. Implemented with PhotonVision version v2025.0.0-beta-8 .
5
- Home-page: https://photonvision.org
6
- Author: Photonvision Development Team
7
- Description-Content-Type: text/markdown
8
- Requires-Dist: numpy~=2.1
9
- Requires-Dist: wpilib<2026,>=2025.0.0b1
10
- Requires-Dist: robotpy-wpimath<2026,>=2025.0.0b1
11
- Requires-Dist: robotpy-apriltag<2026,>=2025.0.0b1
12
- Requires-Dist: robotpy-cscore<2026,>=2025.0.0b1
13
- Requires-Dist: pyntcore<2026,>=2025.0.0b1
14
- Requires-Dist: opencv-python; platform_machine != "roborio"
15
-
16
- A Pure-python implementation of PhotonLib
@@ -1,37 +0,0 @@
1
- photonlibpy/__init__.py,sha256=WW1OGrrcNXwwxaHSZlkxmhH2GYiQIHHxSxGVTJZhZbY,1136
2
- photonlibpy/estimatedRobotPose.py,sha256=X7wF9xdPXGKSVy0MY0qrWZJOEbuZPd721lYp0KXKlP0,1603
3
- photonlibpy/packet.py,sha256=5YomViVFwOljL2FGOetWM9FbPc_yCQ15ylzkYlgLIs8,9724
4
- photonlibpy/photonCamera.py,sha256=ENBHp959it4aLnFtV66rHoAIYCvK4bTflZMrGSCwnZ8,12905
5
- photonlibpy/photonPoseEstimator.py,sha256=2iMqxPFsQHTsq95yv-WCSv1a6wXNcHPqOyMc4Bu6IG0,12584
6
- photonlibpy/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
7
- photonlibpy/version.py,sha256=Gi-5Nspy6P3-YhTRXS3INLKV_X1zGeRsdORosEs-li4,77
8
- photonlibpy/estimation/__init__.py,sha256=pZ-d6fN1DJvT-lRl4FfIos5HAvlzmetIOrGIinrdv7k,223
9
- photonlibpy/estimation/cameraTargetRelation.py,sha256=i7DPBXtkZve4ToXQscEIe-5F1oGQ1Qmf5QBaE__EeMQ,1158
10
- photonlibpy/estimation/openCVHelp.py,sha256=O1dV7v7RHSyw7l5L0QXbal6t9K7iyvEG76tG-t4AFVg,12388
11
- photonlibpy/estimation/rotTrlTransform3d.py,sha256=oRYk4V1XF7guNxefJsCPcBWPIkKZl0HYD_Gs86wwToE,2671
12
- photonlibpy/estimation/targetModel.py,sha256=fIhkf0-_Sfv-KhYEBnhFWMF4Xu84FfF6B-KUEsuuGjQ,6459
13
- photonlibpy/estimation/visionEstimation.py,sha256=Q4KWdLCV1H0wJUJCzG7OYNhVmk2jR1iPUmX_PFylLdA,4174
14
- photonlibpy/generated/MultiTargetPNPResultSerde.py,sha256=CzsCosHUnzxAoSC_sSAqpsXsDp54KW-BClnIXdzfMPc,2506
15
- photonlibpy/generated/PhotonPipelineMetadataSerde.py,sha256=9pq5XUEDEoRowCd7sRnuGV7xtugu5HZCRrnwqZG2xXc,2887
16
- photonlibpy/generated/PhotonPipelineResultSerde.py,sha256=8_YhW1icdhPOx5eAqwKadvnR29NABmhHr6ERAWK8wEo,3115
17
- photonlibpy/generated/PhotonTrackedTargetSerde.py,sha256=-6vKir_ABDVBGbg8ktM48IKm_nBMFBbiyuZLiO_fP9U,4437
18
- photonlibpy/generated/PnpResultSerde.py,sha256=YoTKdQ51oSdxC-7Poy6hunL0-zkMKvP5uedqaHWPudY,2693
19
- photonlibpy/generated/TargetCornerSerde.py,sha256=kziD_rQIwyhzPfgOaDgn-3d87tvtXiAYbBjzu76biYU,2190
20
- photonlibpy/generated/__init__.py,sha256=mElM8M88---wxTWO-SRqIJ4EfxN0fdIUwZBZ-UIGuRw,428
21
- photonlibpy/networktables/NTTopicSet.py,sha256=29wPgXcuqT-u72-YXwSjRHWhECNzU8eDsexcqlA8KQ0,2967
22
- photonlibpy/networktables/__init__.py,sha256=o_LxTdyIylAszMy_zhUtTkXHyu6jqxccccj78d44OrI,35
23
- photonlibpy/simulation/__init__.py,sha256=HKJV02of5d8bOnuI7syLzSYtOYge7XUrHSaLvawh99M,227
24
- photonlibpy/simulation/photonCameraSim.py,sha256=8ELLcOXUtDftV_OYzGG03wigANimJs2SehV-fluWy3k,19907
25
- photonlibpy/simulation/simCameraProperties.py,sha256=ODVxnylF8zw9HZSbw0PzG_OEtUo9ChRo-G_iEgADOCg,27195
26
- photonlibpy/simulation/videoSimUtil.py,sha256=xMuTvJ2Jx9IoQqmAJi_zUm06MdEwhVpIz9OyzYQp0k4,29
27
- photonlibpy/simulation/visionSystemSim.py,sha256=GmKs0d32WE8B020YEWnj-0dQuCnVv1ScGdcFl1fOsKo,13835
28
- photonlibpy/simulation/visionTargetSim.py,sha256=FH85fKE4NntowUvssfgZ1KlE-I_3Z-QuAgb2bFqvfdY,2219
29
- photonlibpy/targeting/TargetCorner.py,sha256=ouKj3E5uD76OZSNHHuSDzKOY65a8HqtcOsuejH-MVsU,276
30
- photonlibpy/targeting/__init__.py,sha256=YzINSpq6A0cjr-yAQcFqHoiYdLGKPFXThlVYlMjY11w,295
31
- photonlibpy/targeting/multiTargetPNPResult.py,sha256=Y9rweHtMzoCZ6mv6F8CutQi2Thq5pHN0ydBWvTCsOwY,806
32
- photonlibpy/targeting/photonPipelineResult.py,sha256=MbaSyHZTJpoKTtLOZztpSGSt9xWWFqhzgwj8medObVA,2732
33
- photonlibpy/targeting/photonTrackedTarget.py,sha256=zCoFp32hX-3GmBYEmsYBQieBoMzXtP2F_55_q0zPOXA,1956
34
- photonlibpy-2025.0.0b8.dist-info/METADATA,sha256=f0Zgv_wfJOSge_dKazRJUnuld9Vb_3oWMLC_AOTeMwI,689
35
- photonlibpy-2025.0.0b8.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
36
- photonlibpy-2025.0.0b8.dist-info/top_level.txt,sha256=T8Xc6U6he2VjKUAca6zawSkHdUZuLanxYIc4nxw2ctc,12
37
- photonlibpy-2025.0.0b8.dist-info/RECORD,,