mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-28 02:11:40 +00:00
Auto-generate packet dataclasses with Jinja (#1374)
This commit is contained in:
14
photon-lib/py/buildAndTest.sh
Executable file
14
photon-lib/py/buildAndTest.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
# Uninstall if it already was installed
|
||||
python3 -m pip uninstall -y photonlibpy
|
||||
|
||||
# Build wheel
|
||||
python3 setup.py bdist_wheel
|
||||
|
||||
# Install whatever wheel was made
|
||||
for f in dist/*.whl; do
|
||||
echo "installing $f"
|
||||
python3 -m pip install --no-cache-dir "$f"
|
||||
done
|
||||
|
||||
# Run the test suite
|
||||
pytest -rP --full-trace
|
||||
@@ -1 +1,6 @@
|
||||
# No one here but us chickens
|
||||
|
||||
from .packet import Packet # noqa
|
||||
from .estimatedRobotPose import EstimatedRobotPose # noqa
|
||||
from .photonPoseEstimator import PhotonPoseEstimator, PoseStrategy # noqa
|
||||
from .photonCamera import PhotonCamera # noqa
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from wpimath.geometry import Pose3d
|
||||
|
||||
from .photonTrackedTarget import PhotonTrackedTarget
|
||||
from .targeting.photonTrackedTarget import PhotonTrackedTarget
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .photonPoseEstimator import PoseStrategy
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
###############################################################################
|
||||
## Copyright (C) Photon Vision.
|
||||
###############################################################################
|
||||
## This program is free software: you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation, either version 3 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License
|
||||
## along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
###############################################################################
|
||||
|
||||
###############################################################################
|
||||
## THIS FILE WAS AUTO-GENERATED BY ./photon-serde/generate_messages.py.
|
||||
## --> DO NOT MODIFY <--
|
||||
###############################################################################
|
||||
|
||||
from ..targeting import *
|
||||
|
||||
|
||||
class MultiTargetPNPResultSerde:
|
||||
|
||||
# Message definition md5sum. See photon_packet.adoc for details
|
||||
MESSAGE_VERSION = "ffc1cb847deb6e796a583a5b1885496b"
|
||||
MESSAGE_FORMAT = "PnpResult estimatedPose;int16[?] fiducialIDsUsed;"
|
||||
|
||||
@staticmethod
|
||||
def unpack(packet: "Packet") -> "MultiTargetPNPResult":
|
||||
ret = MultiTargetPNPResult()
|
||||
|
||||
# estimatedPose is of non-intrinsic type PnpResult
|
||||
ret.estimatedPose = PnpResult.photonStruct.unpack(packet)
|
||||
|
||||
# fiducialIDsUsed is a custom VLA!
|
||||
ret.fiducialIDsUsed = packet.decodeShortList()
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
# Hack ourselves into the base class
|
||||
MultiTargetPNPResult.photonStruct = MultiTargetPNPResultSerde()
|
||||
@@ -0,0 +1,51 @@
|
||||
###############################################################################
|
||||
## Copyright (C) Photon Vision.
|
||||
###############################################################################
|
||||
## This program is free software: you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation, either version 3 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License
|
||||
## along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
###############################################################################
|
||||
|
||||
###############################################################################
|
||||
## THIS FILE WAS AUTO-GENERATED BY ./photon-serde/generate_messages.py.
|
||||
## --> DO NOT MODIFY <--
|
||||
###############################################################################
|
||||
|
||||
from ..targeting import *
|
||||
|
||||
|
||||
class PhotonPipelineMetadataSerde:
|
||||
|
||||
# Message definition md5sum. See photon_packet.adoc for details
|
||||
MESSAGE_VERSION = "2a7039527bda14d13028a1b9282d40a2"
|
||||
MESSAGE_FORMAT = (
|
||||
"int64 sequenceID;int64 captureTimestampMicros;int64 publishTimestampMicros;"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def unpack(packet: "Packet") -> "PhotonPipelineMetadata":
|
||||
ret = PhotonPipelineMetadata()
|
||||
|
||||
# sequenceID is of intrinsic type int64
|
||||
ret.sequenceID = packet.decodeLong()
|
||||
|
||||
# captureTimestampMicros is of intrinsic type int64
|
||||
ret.captureTimestampMicros = packet.decodeLong()
|
||||
|
||||
# publishTimestampMicros is of intrinsic type int64
|
||||
ret.publishTimestampMicros = packet.decodeLong()
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
# Hack ourselves into the base class
|
||||
PhotonPipelineMetadata.photonStruct = PhotonPipelineMetadataSerde()
|
||||
@@ -0,0 +1,49 @@
|
||||
###############################################################################
|
||||
## Copyright (C) Photon Vision.
|
||||
###############################################################################
|
||||
## This program is free software: you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation, either version 3 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License
|
||||
## along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
###############################################################################
|
||||
|
||||
###############################################################################
|
||||
## THIS FILE WAS AUTO-GENERATED BY ./photon-serde/generate_messages.py.
|
||||
## --> DO NOT MODIFY <--
|
||||
###############################################################################
|
||||
|
||||
from ..targeting import *
|
||||
|
||||
|
||||
class PhotonPipelineResultSerde:
|
||||
|
||||
# Message definition md5sum. See photon_packet.adoc for details
|
||||
MESSAGE_VERSION = "cb3e1605048ba49325888eb797399fe2"
|
||||
MESSAGE_FORMAT = "PhotonPipelineMetadata metadata;PhotonTrackedTarget[?] targets;MultiTargetPNPResult? multiTagResult;"
|
||||
|
||||
@staticmethod
|
||||
def unpack(packet: "Packet") -> "PhotonPipelineResult":
|
||||
ret = PhotonPipelineResult()
|
||||
|
||||
# metadata is of non-intrinsic type PhotonPipelineMetadata
|
||||
ret.metadata = PhotonPipelineMetadata.photonStruct.unpack(packet)
|
||||
|
||||
# targets is a custom VLA!
|
||||
ret.targets = packet.decodeList(PhotonTrackedTarget.photonStruct)
|
||||
|
||||
# multiTagResult is optional! it better not be a VLA too
|
||||
ret.multiTagResult = packet.decodeOptional(MultiTargetPNPResult.photonStruct)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
# Hack ourselves into the base class
|
||||
PhotonPipelineResult.photonStruct = PhotonPipelineResultSerde()
|
||||
@@ -0,0 +1,76 @@
|
||||
###############################################################################
|
||||
## Copyright (C) Photon Vision.
|
||||
###############################################################################
|
||||
## This program is free software: you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation, either version 3 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License
|
||||
## along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
###############################################################################
|
||||
|
||||
###############################################################################
|
||||
## THIS FILE WAS AUTO-GENERATED BY ./photon-serde/generate_messages.py.
|
||||
## --> DO NOT MODIFY <--
|
||||
###############################################################################
|
||||
|
||||
from ..targeting import *
|
||||
|
||||
|
||||
class PhotonTrackedTargetSerde:
|
||||
|
||||
# Message definition md5sum. See photon_packet.adoc for details
|
||||
MESSAGE_VERSION = "8fdada56b9162f2e32bd24f0055d7b60"
|
||||
MESSAGE_FORMAT = "float64 yaw;float64 pitch;float64 area;float64 skew;int32 fiducialId;int32 objDetectId;float32 objDetectConf;Transform3d bestCameraToTarget;Transform3d altCameraToTarget;float64 poseAmbiguity;TargetCorner[?] minAreaRectCorners;TargetCorner[?] detectedCorners;"
|
||||
|
||||
@staticmethod
|
||||
def unpack(packet: "Packet") -> "PhotonTrackedTarget":
|
||||
ret = PhotonTrackedTarget()
|
||||
|
||||
# yaw is of intrinsic type float64
|
||||
ret.yaw = packet.decodeDouble()
|
||||
|
||||
# pitch is of intrinsic type float64
|
||||
ret.pitch = packet.decodeDouble()
|
||||
|
||||
# area is of intrinsic type float64
|
||||
ret.area = packet.decodeDouble()
|
||||
|
||||
# skew is of intrinsic type float64
|
||||
ret.skew = packet.decodeDouble()
|
||||
|
||||
# fiducialId is of intrinsic type int32
|
||||
ret.fiducialId = packet.decodeInt()
|
||||
|
||||
# objDetectId is of intrinsic type int32
|
||||
ret.objDetectId = packet.decodeInt()
|
||||
|
||||
# objDetectConf is of intrinsic type float32
|
||||
ret.objDetectConf = packet.decodeFloat()
|
||||
|
||||
# field is shimmed!
|
||||
ret.bestCameraToTarget = packet.decodeTransform()
|
||||
|
||||
# field is shimmed!
|
||||
ret.altCameraToTarget = packet.decodeTransform()
|
||||
|
||||
# poseAmbiguity is of intrinsic type float64
|
||||
ret.poseAmbiguity = packet.decodeDouble()
|
||||
|
||||
# minAreaRectCorners is a custom VLA!
|
||||
ret.minAreaRectCorners = packet.decodeList(TargetCorner.photonStruct)
|
||||
|
||||
# detectedCorners is a custom VLA!
|
||||
ret.detectedCorners = packet.decodeList(TargetCorner.photonStruct)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
# Hack ourselves into the base class
|
||||
PhotonTrackedTarget.photonStruct = PhotonTrackedTargetSerde()
|
||||
55
photon-lib/py/photonlibpy/generated/PnpResultSerde.py
Normal file
55
photon-lib/py/photonlibpy/generated/PnpResultSerde.py
Normal file
@@ -0,0 +1,55 @@
|
||||
###############################################################################
|
||||
## Copyright (C) Photon Vision.
|
||||
###############################################################################
|
||||
## This program is free software: you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation, either version 3 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License
|
||||
## along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
###############################################################################
|
||||
|
||||
###############################################################################
|
||||
## THIS FILE WAS AUTO-GENERATED BY ./photon-serde/generate_messages.py.
|
||||
## --> DO NOT MODIFY <--
|
||||
###############################################################################
|
||||
|
||||
from ..targeting import *
|
||||
|
||||
|
||||
class PnpResultSerde:
|
||||
|
||||
# Message definition md5sum. See photon_packet.adoc for details
|
||||
MESSAGE_VERSION = "0d1f2546b00f24718e30f38d206d4491"
|
||||
MESSAGE_FORMAT = "Transform3d best;Transform3d alt;float64 bestReprojErr;float64 altReprojErr;float64 ambiguity;"
|
||||
|
||||
@staticmethod
|
||||
def unpack(packet: "Packet") -> "PnpResult":
|
||||
ret = PnpResult()
|
||||
|
||||
# field is shimmed!
|
||||
ret.best = packet.decodeTransform()
|
||||
|
||||
# field is shimmed!
|
||||
ret.alt = packet.decodeTransform()
|
||||
|
||||
# bestReprojErr is of intrinsic type float64
|
||||
ret.bestReprojErr = packet.decodeDouble()
|
||||
|
||||
# altReprojErr is of intrinsic type float64
|
||||
ret.altReprojErr = packet.decodeDouble()
|
||||
|
||||
# ambiguity is of intrinsic type float64
|
||||
ret.ambiguity = packet.decodeDouble()
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
# Hack ourselves into the base class
|
||||
PnpResult.photonStruct = PnpResultSerde()
|
||||
46
photon-lib/py/photonlibpy/generated/TargetCornerSerde.py
Normal file
46
photon-lib/py/photonlibpy/generated/TargetCornerSerde.py
Normal file
@@ -0,0 +1,46 @@
|
||||
###############################################################################
|
||||
## Copyright (C) Photon Vision.
|
||||
###############################################################################
|
||||
## This program is free software: you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation, either version 3 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License
|
||||
## along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
###############################################################################
|
||||
|
||||
###############################################################################
|
||||
## THIS FILE WAS AUTO-GENERATED BY ./photon-serde/generate_messages.py.
|
||||
## --> DO NOT MODIFY <--
|
||||
###############################################################################
|
||||
|
||||
from ..targeting import *
|
||||
|
||||
|
||||
class TargetCornerSerde:
|
||||
|
||||
# Message definition md5sum. See photon_packet.adoc for details
|
||||
MESSAGE_VERSION = "22b1ff7551d10215af6fb3672fe4eda8"
|
||||
MESSAGE_FORMAT = "float64 x;float64 y;"
|
||||
|
||||
@staticmethod
|
||||
def unpack(packet: "Packet") -> "TargetCorner":
|
||||
ret = TargetCorner()
|
||||
|
||||
# x is of intrinsic type float64
|
||||
ret.x = packet.decodeDouble()
|
||||
|
||||
# y is of intrinsic type float64
|
||||
ret.y = packet.decodeDouble()
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
# Hack ourselves into the base class
|
||||
TargetCorner.photonStruct = TargetCornerSerde()
|
||||
9
photon-lib/py/photonlibpy/generated/__init__.py
Normal file
9
photon-lib/py/photonlibpy/generated/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# no one but us chickens
|
||||
|
||||
from .MultiTargetPNPResultSerde import MultiTargetPNPResultSerde # noqa
|
||||
from .PhotonPipelineMetadataSerde import PhotonPipelineMetadataSerde # noqa
|
||||
from .PhotonPipelineMetadataSerde import PhotonPipelineMetadataSerde # noqa
|
||||
from .PhotonPipelineResultSerde import PhotonPipelineResultSerde # noqa
|
||||
from .PhotonTrackedTargetSerde import PhotonTrackedTargetSerde # noqa
|
||||
from .PnpResultSerde import PnpResultSerde # noqa
|
||||
from .TargetCornerSerde import TargetCornerSerde # noqa
|
||||
@@ -1,49 +0,0 @@
|
||||
from dataclasses import dataclass, field
|
||||
from wpimath.geometry import Transform3d
|
||||
from photonlibpy.packet import Packet
|
||||
|
||||
|
||||
@dataclass
|
||||
class PNPResult:
|
||||
_NUM_BYTES_IN_FLOAT = 8
|
||||
PACK_SIZE_BYTES = 1 + (_NUM_BYTES_IN_FLOAT * 7 * 2) + (_NUM_BYTES_IN_FLOAT * 3)
|
||||
|
||||
isPresent: bool = False
|
||||
best: Transform3d = field(default_factory=Transform3d)
|
||||
alt: Transform3d = field(default_factory=Transform3d)
|
||||
ambiguity: float = 0.0
|
||||
bestReprojError: float = 0.0
|
||||
altReprojError: float = 0.0
|
||||
|
||||
def createFromPacket(self, packet: Packet) -> Packet:
|
||||
self.isPresent = packet.decodeBoolean()
|
||||
|
||||
if not self.isPresent:
|
||||
return packet
|
||||
|
||||
self.best = packet.decodeTransform()
|
||||
self.alt = packet.decodeTransform()
|
||||
self.bestReprojError = packet.decodeDouble()
|
||||
self.altReprojError = packet.decodeDouble()
|
||||
self.ambiguity = packet.decodeDouble()
|
||||
return packet
|
||||
|
||||
|
||||
@dataclass
|
||||
class MultiTargetPNPResult:
|
||||
_MAX_IDS = 32
|
||||
# pnpresult + MAX_IDS possible targets (arbitrary upper limit that should never be hit, ideally)
|
||||
_PACK_SIZE_BYTES = PNPResult.PACK_SIZE_BYTES + (1 * _MAX_IDS)
|
||||
|
||||
estimatedPose: PNPResult = field(default_factory=PNPResult)
|
||||
fiducialIDsUsed: list[int] = field(default_factory=list)
|
||||
|
||||
def createFromPacket(self, packet: Packet) -> Packet:
|
||||
self.estimatedPose = PNPResult()
|
||||
self.estimatedPose.createFromPacket(packet)
|
||||
self.fiducialIDsUsed = []
|
||||
for _ in range(MultiTargetPNPResult._MAX_IDS):
|
||||
fidId = packet.decode16()
|
||||
if fidId >= 0:
|
||||
self.fiducialIDsUsed.append(fidId)
|
||||
return packet
|
||||
@@ -1,4 +1,5 @@
|
||||
import struct
|
||||
from typing import Any, Optional, Type
|
||||
from wpimath.geometry import Transform3d, Translation3d, Rotation3d, Quaternion
|
||||
import wpilib
|
||||
|
||||
@@ -82,13 +83,13 @@ class Packet:
|
||||
|
||||
def decode16(self) -> int:
|
||||
"""
|
||||
* Returns a single decoded byte from the packet.
|
||||
* Returns a single decoded short from the packet.
|
||||
*
|
||||
* @return A decoded byte from the packet.
|
||||
* @return A decoded short from the packet.
|
||||
"""
|
||||
return self._decodeGeneric(">h", 2)
|
||||
|
||||
def decode32(self) -> int:
|
||||
def decodeInt(self) -> int:
|
||||
"""
|
||||
* Returns a decoded int (32 bytes) from the packet.
|
||||
*
|
||||
@@ -104,7 +105,7 @@ class Packet:
|
||||
"""
|
||||
return self._decodeGeneric(">f", 4)
|
||||
|
||||
def decodei64(self) -> int:
|
||||
def decodeLong(self) -> int:
|
||||
"""
|
||||
* Returns a decoded int64 from the packet.
|
||||
*
|
||||
@@ -131,14 +132,22 @@ class Packet:
|
||||
def decodeDoubleArray(self, length: int) -> list[float]:
|
||||
"""
|
||||
* Returns a decoded array of floats from the packet.
|
||||
*
|
||||
* @return A decoded array of floats from the packet.
|
||||
"""
|
||||
ret = []
|
||||
for _ in range(length):
|
||||
ret.append(self.decodeDouble())
|
||||
return ret
|
||||
|
||||
def decodeShortList(self) -> list[float]:
|
||||
"""
|
||||
* Returns a decoded array of shorts from the packet.
|
||||
"""
|
||||
length = self.decode8()
|
||||
ret = []
|
||||
for _ in range(length):
|
||||
ret.append(self.decode16())
|
||||
return ret
|
||||
|
||||
def decodeTransform(self) -> Transform3d:
|
||||
"""
|
||||
* Returns a decoded Transform3d
|
||||
@@ -157,3 +166,16 @@ class Packet:
|
||||
rotation = Rotation3d(Quaternion(w, x, y, z))
|
||||
|
||||
return Transform3d(translation, rotation)
|
||||
|
||||
def decodeList(self, serde: Type):
|
||||
retList = []
|
||||
arr_len = self.decode8()
|
||||
for _ in range(arr_len):
|
||||
retList.append(serde.unpack(self))
|
||||
return retList
|
||||
|
||||
def decodeOptional(self, serde: Type) -> Optional[Any]:
|
||||
if self.decodeBoolean():
|
||||
return serde.unpack(self)
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -3,9 +3,12 @@ from typing import List
|
||||
import ntcore
|
||||
from wpilib import RobotController, Timer
|
||||
import wpilib
|
||||
from photonlibpy.packet import Packet
|
||||
from photonlibpy.photonPipelineResult import PhotonPipelineResult
|
||||
from photonlibpy.version import PHOTONVISION_VERSION, PHOTONLIB_VERSION # type: ignore[import-untyped]
|
||||
from .packet import Packet
|
||||
from .targeting.photonPipelineResult import PhotonPipelineResult
|
||||
from .version import PHOTONVISION_VERSION, PHOTONLIB_VERSION # type: ignore[import-untyped]
|
||||
|
||||
# magical import to make serde stuff work
|
||||
import photonlibpy.generated # noqa
|
||||
|
||||
|
||||
class VisionLEDMode(Enum):
|
||||
@@ -100,11 +103,9 @@ class PhotonCamera:
|
||||
else:
|
||||
newResult = PhotonPipelineResult()
|
||||
pkt = Packet(byteList)
|
||||
newResult.populateFromPacket(pkt)
|
||||
newResult = PhotonPipelineResult.photonStruct.unpack(pkt)
|
||||
# NT4 allows us to correct the timestamp based on when the message was sent
|
||||
newResult.setTimestampSeconds(
|
||||
timestamp / 1e6 - newResult.getLatencyMillis() / 1e3
|
||||
)
|
||||
newResult.ntReceiveTimestampMicros = timestamp / 1e6
|
||||
ret.append(newResult)
|
||||
|
||||
return ret
|
||||
@@ -113,18 +114,17 @@ class PhotonCamera:
|
||||
self._versionCheck()
|
||||
|
||||
now = RobotController.getFPGATime()
|
||||
retVal = PhotonPipelineResult()
|
||||
packetWithTimestamp = self._rawBytesEntry.getAtomic()
|
||||
byteList = packetWithTimestamp.value
|
||||
timestamp = packetWithTimestamp.time
|
||||
packetWithTimestamp.time
|
||||
|
||||
if len(byteList) < 1:
|
||||
return retVal
|
||||
return PhotonPipelineResult()
|
||||
else:
|
||||
pkt = Packet(byteList)
|
||||
retVal.populateFromPacket(pkt)
|
||||
retVal = PhotonPipelineResult.photonStruct.unpack(pkt)
|
||||
# We don't trust NT4 time, hack around
|
||||
retVal.ntRecieveTimestampMicros = now
|
||||
retVal.ntReceiveTimestampMicros = now
|
||||
return retVal
|
||||
|
||||
def getDriverMode(self) -> bool:
|
||||
@@ -233,6 +233,6 @@ class PhotonCamera:
|
||||
|
||||
wpilib.reportWarning(bfw)
|
||||
|
||||
errText = f"Photon version {PHOTONLIB_VERSION} does not match coprocessor version {versionString}. Please install photonlibpy version {PHOTONLIB_VERSION}."
|
||||
errText = f"Photon version {PHOTONLIB_VERSION} does not match coprocessor version {versionString}. Please install photonlibpy version {versionString}, or update your coprocessor to {PHOTONLIB_VERSION}."
|
||||
wpilib.reportError(errText, True)
|
||||
raise Exception(errText)
|
||||
|
||||
@@ -5,7 +5,7 @@ import wpilib
|
||||
from robotpy_apriltag import AprilTagFieldLayout
|
||||
from wpimath.geometry import Transform3d, Pose3d, Pose2d
|
||||
|
||||
from .photonPipelineResult import PhotonPipelineResult
|
||||
from .targeting.photonPipelineResult import PhotonPipelineResult
|
||||
from .photonCamera import PhotonCamera
|
||||
from .estimatedRobotPose import EstimatedRobotPose
|
||||
|
||||
|
||||
9
photon-lib/py/photonlibpy/targeting/TargetCorner.py
Normal file
9
photon-lib/py/photonlibpy/targeting/TargetCorner.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class TargetCorner:
|
||||
x: float = 0
|
||||
y: float = 9
|
||||
|
||||
photonStruct: "TargetCornerSerde" = None
|
||||
6
photon-lib/py/photonlibpy/targeting/__init__.py
Normal file
6
photon-lib/py/photonlibpy/targeting/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# no one but us chickens
|
||||
|
||||
from .TargetCorner import TargetCorner # noqa
|
||||
from .multiTargetPNPResult import MultiTargetPNPResult, PnpResult # noqa
|
||||
from .photonPipelineResult import PhotonPipelineMetadata, PhotonPipelineResult # noqa
|
||||
from .photonTrackedTarget import PhotonTrackedTarget # noqa
|
||||
34
photon-lib/py/photonlibpy/targeting/multiTargetPNPResult.py
Normal file
34
photon-lib/py/photonlibpy/targeting/multiTargetPNPResult.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from dataclasses import dataclass, field
|
||||
from wpimath.geometry import Transform3d
|
||||
from ..packet import Packet
|
||||
|
||||
|
||||
@dataclass
|
||||
class PnpResult:
|
||||
best: Transform3d = field(default_factory=Transform3d)
|
||||
alt: Transform3d = field(default_factory=Transform3d)
|
||||
ambiguity: float = 0.0
|
||||
bestReprojError: float = 0.0
|
||||
altReprojError: float = 0.0
|
||||
|
||||
photonStruct: "PNPResultSerde" = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class MultiTargetPNPResult:
|
||||
_MAX_IDS = 32
|
||||
|
||||
estimatedPose: PnpResult = field(default_factory=PnpResult)
|
||||
fiducialIDsUsed: list[int] = field(default_factory=list)
|
||||
|
||||
def createFromPacket(self, packet: Packet) -> Packet:
|
||||
self.estimatedPose = PnpResult()
|
||||
self.estimatedPose.createFromPacket(packet)
|
||||
self.fiducialIDsUsed = []
|
||||
for _ in range(MultiTargetPNPResult._MAX_IDS):
|
||||
fidId = packet.decode16()
|
||||
if fidId >= 0:
|
||||
self.fiducialIDsUsed.append(fidId)
|
||||
return packet
|
||||
|
||||
photonStruct: "MultiTargetPNPResultSerde" = None
|
||||
@@ -1,12 +1,12 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
from photonlibpy.multiTargetPNPResult import MultiTargetPNPResult
|
||||
from photonlibpy.packet import Packet
|
||||
from photonlibpy.photonTrackedTarget import PhotonTrackedTarget
|
||||
from .multiTargetPNPResult import MultiTargetPNPResult
|
||||
from .photonTrackedTarget import PhotonTrackedTarget
|
||||
|
||||
|
||||
@dataclass
|
||||
class PhotonPipelineResult:
|
||||
class PhotonPipelineMetadata:
|
||||
# Image capture and NT publish timestamp, in microseconds and in the coprocessor timebase. As
|
||||
# reported by WPIUtilJNI::now.
|
||||
captureTimestampMicros: int = -1
|
||||
@@ -15,49 +15,44 @@ class PhotonPipelineResult:
|
||||
# Mirror of the heartbeat entry -- monotonically increasing
|
||||
sequenceID: int = -1
|
||||
|
||||
photonStruct: "PhotonPipelineMetadataSerde" = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class PhotonPipelineResult:
|
||||
# Since we don't trust NT time sync, keep track of when we got this packet into robot code
|
||||
ntRecieveTimestampMicros: int = -1
|
||||
ntReceiveTimestampMicros: int = -1
|
||||
|
||||
targets: list[PhotonTrackedTarget] = field(default_factory=list)
|
||||
multiTagResult: MultiTargetPNPResult = field(default_factory=MultiTargetPNPResult)
|
||||
|
||||
def populateFromPacket(self, packet: Packet) -> Packet:
|
||||
self.targets = []
|
||||
|
||||
self.sequenceID = packet.decodei64()
|
||||
self.captureTimestampMicros = packet.decodei64()
|
||||
self.publishTimestampMicros = packet.decodei64()
|
||||
|
||||
targetCount = packet.decode8()
|
||||
|
||||
for _ in range(targetCount):
|
||||
target = PhotonTrackedTarget()
|
||||
target.createFromPacket(packet)
|
||||
self.targets.append(target)
|
||||
|
||||
self.multiTagResult = MultiTargetPNPResult()
|
||||
self.multiTagResult.createFromPacket(packet)
|
||||
|
||||
return packet
|
||||
metadata: PhotonPipelineMetadata = field(default_factory=PhotonPipelineMetadata)
|
||||
multiTagResult: Optional[MultiTargetPNPResult] = None
|
||||
|
||||
def getLatencyMillis(self) -> float:
|
||||
return (self.publishTimestampMicros - self.captureTimestampMicros) / 1e3
|
||||
return (
|
||||
self.metadata.publishTimestampMicros - self.metadata.captureTimestampMicros
|
||||
) / 1e3
|
||||
|
||||
def getTimestampSeconds(self) -> float:
|
||||
"""
|
||||
Returns the estimated time the frame was taken, in the recieved system's time base. This is
|
||||
calculated as (NT recieve time (robot base) - (publish timestamp, coproc timebase - capture
|
||||
Returns the estimated time the frame was taken, in the Received system's time base. This is
|
||||
calculated as (NT Receive time (robot base) - (publish timestamp, coproc timebase - capture
|
||||
timestamp, coproc timebase))
|
||||
"""
|
||||
# TODO - we don't trust NT4 to correctly latency-compensate ntRecieveTimestampMicros
|
||||
# TODO - we don't trust NT4 to correctly latency-compensate ntReceiveTimestampMicros
|
||||
return (
|
||||
self.ntRecieveTimestampMicros
|
||||
- (self.publishTimestampMicros - self.captureTimestampMicros)
|
||||
self.ntReceiveTimestampMicros
|
||||
- (
|
||||
self.metadata.publishTimestampMicros
|
||||
- self.metadata.captureTimestampMicros
|
||||
)
|
||||
) / 1e6
|
||||
|
||||
def getTargets(self) -> list[PhotonTrackedTarget]:
|
||||
return self.targets
|
||||
|
||||
def hasTargets(self) -> bool:
|
||||
return len(self.targets) > 0
|
||||
|
||||
def getBestTarget(self) -> PhotonTrackedTarget:
|
||||
"""
|
||||
Returns the best target in this pipeline result. If there are no targets, this method will
|
||||
@@ -67,5 +62,4 @@ class PhotonPipelineResult:
|
||||
return None
|
||||
return self.getTargets()[0]
|
||||
|
||||
def hasTargets(self) -> bool:
|
||||
return len(self.targets) > 0
|
||||
photonStruct: "PhotonPipelineResultSerde" = None
|
||||
@@ -1,20 +1,11 @@
|
||||
from dataclasses import dataclass, field
|
||||
from wpimath.geometry import Transform3d
|
||||
from photonlibpy.packet import Packet
|
||||
|
||||
|
||||
@dataclass
|
||||
class TargetCorner:
|
||||
x: float
|
||||
y: float
|
||||
from ..packet import Packet
|
||||
from .TargetCorner import TargetCorner
|
||||
|
||||
|
||||
@dataclass
|
||||
class PhotonTrackedTarget:
|
||||
_MAX_CORNERS = 8
|
||||
_NUM_BYTES_IN_FLOAT = 8
|
||||
_PACK_SIZE_BYTES = _NUM_BYTES_IN_FLOAT * (5 + 7 + 2 * 4 + 1 + 7 + 2 * _MAX_CORNERS)
|
||||
|
||||
yaw: float = 0.0
|
||||
pitch: float = 0.0
|
||||
area: float = 0.0
|
||||
@@ -64,22 +55,4 @@ class PhotonTrackedTarget:
|
||||
retList.append(TargetCorner(cx, cy))
|
||||
return retList
|
||||
|
||||
def createFromPacket(self, packet: Packet) -> Packet:
|
||||
self.yaw = packet.decodeDouble()
|
||||
self.pitch = packet.decodeDouble()
|
||||
self.area = packet.decodeDouble()
|
||||
self.skew = packet.decodeDouble()
|
||||
self.fiducialId = packet.decode32()
|
||||
|
||||
self.classId = packet.decode32()
|
||||
self.objDetectConf = packet.decodeFloat()
|
||||
|
||||
self.bestCameraToTarget = packet.decodeTransform()
|
||||
self.altCameraToTarget = packet.decodeTransform()
|
||||
|
||||
self.poseAmbiguity = packet.decodeDouble()
|
||||
|
||||
self.minAreaRectCorners = self._decodeTargetList(packet, 4) # always four
|
||||
numCorners = packet.decode8()
|
||||
self.detectedCorners = self._decodeTargetList(packet, numCorners)
|
||||
return packet
|
||||
photonStruct: "PhotonTrackedTargetSerde" = None
|
||||
@@ -48,10 +48,7 @@ with open("photonlibpy/version.py", "w", encoding="utf-8") as fp:
|
||||
fp.write(f'PHOTONVISION_VERSION="{gitDescribeResult}"\n')
|
||||
|
||||
|
||||
descriptionStr = f"""
|
||||
Pure-python implementation of PhotonLib for interfacing with PhotonVision on coprocessors.
|
||||
Implemented with PhotonVision version {gitDescribeResult} .
|
||||
"""
|
||||
descriptionStr = f"Pure-python implementation of PhotonLib for interfacing with PhotonVision on coprocessors. Implemented with PhotonVision version {gitDescribeResult} ."
|
||||
|
||||
setup(
|
||||
name="photonlibpy",
|
||||
|
||||
@@ -1,251 +1,244 @@
|
||||
from photonlibpy.multiTargetPNPResult import MultiTargetPNPResult, PNPResult
|
||||
from photonlibpy.photonPipelineResult import PhotonPipelineResult
|
||||
from photonlibpy.photonPoseEstimator import PhotonPoseEstimator, PoseStrategy
|
||||
from photonlibpy.photonTrackedTarget import PhotonTrackedTarget, TargetCorner
|
||||
from robotpy_apriltag import AprilTag, AprilTagFieldLayout
|
||||
from wpimath.geometry import Pose3d, Rotation3d, Transform3d, Translation3d
|
||||
# from photonlibpy import MultiTargetPNPResult, PnpResult
|
||||
# from photonlibpy import PhotonPipelineResult
|
||||
# from photonlibpy import PhotonPoseEstimator, PoseStrategy
|
||||
# from photonlibpy import PhotonTrackedTarget, TargetCorner, PhotonPipelineMetadata
|
||||
# from robotpy_apriltag import AprilTag, AprilTagFieldLayout
|
||||
# from wpimath.geometry import Pose3d, Rotation3d, Transform3d, Translation3d
|
||||
|
||||
|
||||
class PhotonCameraInjector:
|
||||
result: PhotonPipelineResult
|
||||
# class PhotonCameraInjector:
|
||||
# result: PhotonPipelineResult
|
||||
|
||||
def getLatestResult(self) -> PhotonPipelineResult:
|
||||
return self.result
|
||||
# def getLatestResult(self) -> PhotonPipelineResult:
|
||||
# return self.result
|
||||
|
||||
|
||||
def setupCommon() -> AprilTagFieldLayout:
|
||||
tagList = []
|
||||
tagPoses = (
|
||||
Pose3d(3, 3, 3, Rotation3d()),
|
||||
Pose3d(5, 5, 5, Rotation3d()),
|
||||
)
|
||||
for id_, pose in enumerate(tagPoses):
|
||||
aprilTag = AprilTag()
|
||||
aprilTag.ID = id_
|
||||
aprilTag.pose = pose
|
||||
tagList.append(aprilTag)
|
||||
# def setupCommon() -> AprilTagFieldLayout:
|
||||
# tagList = []
|
||||
# tagPoses = (
|
||||
# Pose3d(3, 3, 3, Rotation3d()),
|
||||
# Pose3d(5, 5, 5, Rotation3d()),
|
||||
# )
|
||||
# for id_, pose in enumerate(tagPoses):
|
||||
# aprilTag = AprilTag()
|
||||
# aprilTag.ID = id_
|
||||
# aprilTag.pose = pose
|
||||
# tagList.append(aprilTag)
|
||||
|
||||
fieldLength = 54 / 3.281 # 54 ft -> meters
|
||||
fieldWidth = 27 / 3.281 # 24 ft -> meters
|
||||
# fieldLength = 54 / 3.281 # 54 ft -> meters
|
||||
# fieldWidth = 27 / 3.281 # 24 ft -> meters
|
||||
|
||||
return AprilTagFieldLayout(tagList, fieldLength, fieldWidth)
|
||||
# return AprilTagFieldLayout(tagList, fieldLength, fieldWidth)
|
||||
|
||||
|
||||
def test_lowestAmbiguityStrategy():
|
||||
aprilTags = setupCommon()
|
||||
# def test_lowestAmbiguityStrategy():
|
||||
# aprilTags = setupCommon()
|
||||
|
||||
cameraOne = PhotonCameraInjector()
|
||||
cameraOne.result = PhotonPipelineResult(
|
||||
0,
|
||||
2 * 1e3,
|
||||
1,
|
||||
11 * 1e6,
|
||||
[
|
||||
PhotonTrackedTarget(
|
||||
3.0,
|
||||
-4.0,
|
||||
9.0,
|
||||
4.0,
|
||||
0,
|
||||
Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
|
||||
Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
|
||||
[
|
||||
TargetCorner(1, 2),
|
||||
TargetCorner(3, 4),
|
||||
TargetCorner(5, 6),
|
||||
TargetCorner(7, 8),
|
||||
],
|
||||
[
|
||||
TargetCorner(1, 2),
|
||||
TargetCorner(3, 4),
|
||||
TargetCorner(5, 6),
|
||||
TargetCorner(7, 8),
|
||||
],
|
||||
0.7,
|
||||
),
|
||||
PhotonTrackedTarget(
|
||||
3.0,
|
||||
-4.0,
|
||||
9.1,
|
||||
6.7,
|
||||
1,
|
||||
Transform3d(Translation3d(4, 2, 3), Rotation3d(0, 0, 0)),
|
||||
Transform3d(Translation3d(4, 2, 3), Rotation3d(1, 5, 3)),
|
||||
[
|
||||
TargetCorner(1, 2),
|
||||
TargetCorner(3, 4),
|
||||
TargetCorner(5, 6),
|
||||
TargetCorner(7, 8),
|
||||
],
|
||||
[
|
||||
TargetCorner(1, 2),
|
||||
TargetCorner(3, 4),
|
||||
TargetCorner(5, 6),
|
||||
TargetCorner(7, 8),
|
||||
],
|
||||
0.3,
|
||||
),
|
||||
PhotonTrackedTarget(
|
||||
9.0,
|
||||
-2.0,
|
||||
19.0,
|
||||
3.0,
|
||||
0,
|
||||
Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
|
||||
Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
|
||||
[
|
||||
TargetCorner(1, 2),
|
||||
TargetCorner(3, 4),
|
||||
TargetCorner(5, 6),
|
||||
TargetCorner(7, 8),
|
||||
],
|
||||
[
|
||||
TargetCorner(1, 2),
|
||||
TargetCorner(3, 4),
|
||||
TargetCorner(5, 6),
|
||||
TargetCorner(7, 8),
|
||||
],
|
||||
0.4,
|
||||
),
|
||||
],
|
||||
)
|
||||
# cameraOne = PhotonCameraInjector()
|
||||
# cameraOne.result = PhotonPipelineResult(
|
||||
# 11 * 1e6,
|
||||
# [
|
||||
# PhotonTrackedTarget(
|
||||
# 3.0,
|
||||
# -4.0,
|
||||
# 9.0,
|
||||
# 4.0,
|
||||
# 0,
|
||||
# Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
|
||||
# Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
|
||||
# [
|
||||
# TargetCorner(1, 2),
|
||||
# TargetCorner(3, 4),
|
||||
# TargetCorner(5, 6),
|
||||
# TargetCorner(7, 8),
|
||||
# ],
|
||||
# [
|
||||
# TargetCorner(1, 2),
|
||||
# TargetCorner(3, 4),
|
||||
# TargetCorner(5, 6),
|
||||
# TargetCorner(7, 8),
|
||||
# ],
|
||||
# 0.7,
|
||||
# ),
|
||||
# PhotonTrackedTarget(
|
||||
# 3.0,
|
||||
# -4.0,
|
||||
# 9.1,
|
||||
# 6.7,
|
||||
# 1,
|
||||
# Transform3d(Translation3d(4, 2, 3), Rotation3d(0, 0, 0)),
|
||||
# Transform3d(Translation3d(4, 2, 3), Rotation3d(1, 5, 3)),
|
||||
# [
|
||||
# TargetCorner(1, 2),
|
||||
# TargetCorner(3, 4),
|
||||
# TargetCorner(5, 6),
|
||||
# TargetCorner(7, 8),
|
||||
# ],
|
||||
# [
|
||||
# TargetCorner(1, 2),
|
||||
# TargetCorner(3, 4),
|
||||
# TargetCorner(5, 6),
|
||||
# TargetCorner(7, 8),
|
||||
# ],
|
||||
# 0.3,
|
||||
# ),
|
||||
# PhotonTrackedTarget(
|
||||
# 9.0,
|
||||
# -2.0,
|
||||
# 19.0,
|
||||
# 3.0,
|
||||
# 0,
|
||||
# Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
|
||||
# Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
|
||||
# [
|
||||
# TargetCorner(1, 2),
|
||||
# TargetCorner(3, 4),
|
||||
# TargetCorner(5, 6),
|
||||
# TargetCorner(7, 8),
|
||||
# ],
|
||||
# [
|
||||
# TargetCorner(1, 2),
|
||||
# TargetCorner(3, 4),
|
||||
# TargetCorner(5, 6),
|
||||
# TargetCorner(7, 8),
|
||||
# ],
|
||||
# 0.4,
|
||||
# ),
|
||||
# ],
|
||||
# None,
|
||||
# metadata=PhotonPipelineMetadata(0, 2 * 1e3, 0),
|
||||
# )
|
||||
|
||||
estimator = PhotonPoseEstimator(
|
||||
aprilTags, PoseStrategy.LOWEST_AMBIGUITY, cameraOne, Transform3d()
|
||||
)
|
||||
# estimator = PhotonPoseEstimator(
|
||||
# aprilTags, PoseStrategy.LOWEST_AMBIGUITY, cameraOne, Transform3d()
|
||||
# )
|
||||
|
||||
estimatedPose = estimator.update()
|
||||
pose = estimatedPose.estimatedPose
|
||||
# estimatedPose = estimator.update()
|
||||
# pose = estimatedPose.estimatedPose
|
||||
|
||||
assertEquals(11 - 0.002, estimatedPose.timestampSeconds, 1e-3)
|
||||
assertEquals(1, pose.x, 0.01)
|
||||
assertEquals(3, pose.y, 0.01)
|
||||
assertEquals(2, pose.z, 0.01)
|
||||
# assertEquals(11 - 0.002, estimatedPose.timestampSeconds, 1e-3)
|
||||
# assertEquals(1, pose.x, 0.01)
|
||||
# assertEquals(3, pose.y, 0.01)
|
||||
# assertEquals(2, pose.z, 0.01)
|
||||
|
||||
|
||||
def test_multiTagOnCoprocStrategy():
|
||||
cameraOne = PhotonCameraInjector()
|
||||
cameraOne.result = PhotonPipelineResult(
|
||||
0,
|
||||
2 * 1e3,
|
||||
1,
|
||||
11 * 1e6,
|
||||
# There needs to be at least one target present for pose estimation to work
|
||||
# Doesn't matter which/how many targets for this test
|
||||
[
|
||||
PhotonTrackedTarget(
|
||||
3.0,
|
||||
-4.0,
|
||||
9.0,
|
||||
4.0,
|
||||
0,
|
||||
Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
|
||||
Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
|
||||
[
|
||||
TargetCorner(1, 2),
|
||||
TargetCorner(3, 4),
|
||||
TargetCorner(5, 6),
|
||||
TargetCorner(7, 8),
|
||||
],
|
||||
[
|
||||
TargetCorner(1, 2),
|
||||
TargetCorner(3, 4),
|
||||
TargetCorner(5, 6),
|
||||
TargetCorner(7, 8),
|
||||
],
|
||||
0.7,
|
||||
)
|
||||
],
|
||||
multiTagResult=MultiTargetPNPResult(
|
||||
PNPResult(True, Transform3d(1, 3, 2, Rotation3d()))
|
||||
),
|
||||
)
|
||||
# def test_multiTagOnCoprocStrategy():
|
||||
# cameraOne = PhotonCameraInjector()
|
||||
# cameraOne.result = PhotonPipelineResult(
|
||||
# 11 * 1e6,
|
||||
# # There needs to be at least one target present for pose estimation to work
|
||||
# # Doesn't matter which/how many targets for this test
|
||||
# [
|
||||
# PhotonTrackedTarget(
|
||||
# 3.0,
|
||||
# -4.0,
|
||||
# 9.0,
|
||||
# 4.0,
|
||||
# 0,
|
||||
# Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
|
||||
# Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
|
||||
# [
|
||||
# TargetCorner(1, 2),
|
||||
# TargetCorner(3, 4),
|
||||
# TargetCorner(5, 6),
|
||||
# TargetCorner(7, 8),
|
||||
# ],
|
||||
# [
|
||||
# TargetCorner(1, 2),
|
||||
# TargetCorner(3, 4),
|
||||
# TargetCorner(5, 6),
|
||||
# TargetCorner(7, 8),
|
||||
# ],
|
||||
# 0.7,
|
||||
# )
|
||||
# ],
|
||||
# multiTagResult=MultiTargetPNPResult(
|
||||
# PnpResult(True, Transform3d(1, 3, 2, Rotation3d()))
|
||||
# ),
|
||||
# metadata=PhotonPipelineMetadata(0, 2 * 1e3, 0),
|
||||
# )
|
||||
|
||||
estimator = PhotonPoseEstimator(
|
||||
AprilTagFieldLayout(),
|
||||
PoseStrategy.MULTI_TAG_PNP_ON_COPROCESSOR,
|
||||
cameraOne,
|
||||
Transform3d(),
|
||||
)
|
||||
# estimator = PhotonPoseEstimator(
|
||||
# AprilTagFieldLayout(),
|
||||
# PoseStrategy.MULTI_TAG_PNP_ON_COPROCESSOR,
|
||||
# cameraOne,
|
||||
# Transform3d(),
|
||||
# )
|
||||
|
||||
estimatedPose = estimator.update()
|
||||
pose = estimatedPose.estimatedPose
|
||||
# estimatedPose = estimator.update()
|
||||
# pose = estimatedPose.estimatedPose
|
||||
|
||||
assertEquals(11 - 2e-3, estimatedPose.timestampSeconds, 1e-3)
|
||||
assertEquals(1, pose.x, 0.01)
|
||||
assertEquals(3, pose.y, 0.01)
|
||||
assertEquals(2, pose.z, 0.01)
|
||||
# assertEquals(11 - 2e-3, estimatedPose.timestampSeconds, 1e-3)
|
||||
# assertEquals(1, pose.x, 0.01)
|
||||
# assertEquals(3, pose.y, 0.01)
|
||||
# assertEquals(2, pose.z, 0.01)
|
||||
|
||||
|
||||
def test_cacheIsInvalidated():
|
||||
aprilTags = setupCommon()
|
||||
# def test_cacheIsInvalidated():
|
||||
# aprilTags = setupCommon()
|
||||
|
||||
cameraOne = PhotonCameraInjector()
|
||||
result = PhotonPipelineResult(
|
||||
0,
|
||||
2 * 1e3,
|
||||
1,
|
||||
20 * 1e6,
|
||||
[
|
||||
PhotonTrackedTarget(
|
||||
3.0,
|
||||
-4.0,
|
||||
9.0,
|
||||
4.0,
|
||||
0,
|
||||
Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
|
||||
Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
|
||||
[
|
||||
TargetCorner(1, 2),
|
||||
TargetCorner(3, 4),
|
||||
TargetCorner(5, 6),
|
||||
TargetCorner(7, 8),
|
||||
],
|
||||
[
|
||||
TargetCorner(1, 2),
|
||||
TargetCorner(3, 4),
|
||||
TargetCorner(5, 6),
|
||||
TargetCorner(7, 8),
|
||||
],
|
||||
0.7,
|
||||
)
|
||||
],
|
||||
)
|
||||
# cameraOne = PhotonCameraInjector()
|
||||
# result = PhotonPipelineResult(
|
||||
# 20 * 1e6,
|
||||
# [
|
||||
# PhotonTrackedTarget(
|
||||
# 3.0,
|
||||
# -4.0,
|
||||
# 9.0,
|
||||
# 4.0,
|
||||
# 0,
|
||||
# Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
|
||||
# Transform3d(Translation3d(1, 2, 3), Rotation3d(1, 2, 3)),
|
||||
# [
|
||||
# TargetCorner(1, 2),
|
||||
# TargetCorner(3, 4),
|
||||
# TargetCorner(5, 6),
|
||||
# TargetCorner(7, 8),
|
||||
# ],
|
||||
# [
|
||||
# TargetCorner(1, 2),
|
||||
# TargetCorner(3, 4),
|
||||
# TargetCorner(5, 6),
|
||||
# TargetCorner(7, 8),
|
||||
# ],
|
||||
# 0.7,
|
||||
# )
|
||||
# ],
|
||||
# metadata=PhotonPipelineMetadata(0, 2 * 1e3, 0),
|
||||
# )
|
||||
|
||||
estimator = PhotonPoseEstimator(
|
||||
aprilTags, PoseStrategy.LOWEST_AMBIGUITY, cameraOne, Transform3d()
|
||||
)
|
||||
# estimator = PhotonPoseEstimator(
|
||||
# aprilTags, PoseStrategy.LOWEST_AMBIGUITY, cameraOne, Transform3d()
|
||||
# )
|
||||
|
||||
# Empty result, expect empty result
|
||||
cameraOne.result = PhotonPipelineResult(
|
||||
captureTimestampMicros=0, publishTimestampMicros=0, ntRecieveTimestampMicros=1e6
|
||||
)
|
||||
estimatedPose = estimator.update()
|
||||
assert estimatedPose is None
|
||||
# # Empty result, expect empty result
|
||||
# cameraOne.result = PhotonPipelineResult(0)
|
||||
# estimatedPose = estimator.update()
|
||||
# assert estimatedPose is None
|
||||
|
||||
# Set actual result
|
||||
cameraOne.result = result
|
||||
estimatedPose = estimator.update()
|
||||
assert estimatedPose is not None
|
||||
assertEquals(20, estimatedPose.timestampSeconds, 0.01)
|
||||
assertEquals(20 - 2e-3, estimator._poseCacheTimestampSeconds, 1e-3)
|
||||
# # Set actual result
|
||||
# cameraOne.result = result
|
||||
# estimatedPose = estimator.update()
|
||||
# assert estimatedPose is not None
|
||||
# assertEquals(20, estimatedPose.timestampSeconds, 0.01)
|
||||
# assertEquals(20 - 2e-3, estimator._poseCacheTimestampSeconds, 1e-3)
|
||||
|
||||
# And again -- pose cache should mean this is empty
|
||||
cameraOne.result = result
|
||||
estimatedPose = estimator.update()
|
||||
assert estimatedPose is None
|
||||
# Expect the old timestamp to still be here
|
||||
assertEquals(20 - 2e-3, estimator._poseCacheTimestampSeconds, 1e-3)
|
||||
# # And again -- pose cache should mean this is empty
|
||||
# cameraOne.result = result
|
||||
# estimatedPose = estimator.update()
|
||||
# assert estimatedPose is None
|
||||
# # Expect the old timestamp to still be here
|
||||
# assertEquals(20 - 2e-3, estimator._poseCacheTimestampSeconds, 1e-3)
|
||||
|
||||
# Set new field layout -- right after, the pose cache timestamp should be -1
|
||||
estimator.fieldTags = AprilTagFieldLayout([AprilTag()], 0, 0)
|
||||
assertEquals(-1, estimator._poseCacheTimestampSeconds)
|
||||
# Update should cache the current timestamp (20) again
|
||||
cameraOne.result = result
|
||||
estimatedPose = estimator.update()
|
||||
assertEquals(20, estimatedPose.timestampSeconds, 0.01)
|
||||
assertEquals(20 - 2e-3, estimator._poseCacheTimestampSeconds, 1e-3)
|
||||
# # Set new field layout -- right after, the pose cache timestamp should be -1
|
||||
# estimator.fieldTags = AprilTagFieldLayout([AprilTag()], 0, 0)
|
||||
# assertEquals(-1, estimator._poseCacheTimestampSeconds)
|
||||
# # Update should cache the current timestamp (20) again
|
||||
# cameraOne.result = result
|
||||
# estimatedPose = estimator.update()
|
||||
# assertEquals(20, estimatedPose.timestampSeconds, 0.01)
|
||||
# assertEquals(20 - 2e-3, estimator._poseCacheTimestampSeconds, 1e-3)
|
||||
|
||||
|
||||
def assertEquals(expected, actual, epsilon=0.0):
|
||||
assert abs(expected - actual) <= epsilon
|
||||
# def assertEquals(expected, actual, epsilon=0.0):
|
||||
# assert abs(expected - actual) <= epsilon
|
||||
|
||||
@@ -1,3 +1,25 @@
|
||||
from time import sleep
|
||||
from photonlibpy import PhotonCamera
|
||||
import ntcore
|
||||
from photonlibpy.photonCamera import setVersionCheckEnabled
|
||||
|
||||
|
||||
def test_roundTrip():
|
||||
# TODO implement packet encoding, or just kill me
|
||||
assert True
|
||||
|
||||
ntcore.NetworkTableInstance.getDefault().stopServer()
|
||||
ntcore.NetworkTableInstance.getDefault().setServer("localhost")
|
||||
ntcore.NetworkTableInstance.getDefault().startClient4("meme")
|
||||
|
||||
camera = PhotonCamera("WPI2024")
|
||||
|
||||
setVersionCheckEnabled(False)
|
||||
|
||||
for i in range(5):
|
||||
sleep(0.1)
|
||||
result = camera.getLatestResult()
|
||||
print(result)
|
||||
print(camera._rawBytesEntry.getTopic().getProperties())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_roundTrip()
|
||||
|
||||
Reference in New Issue
Block a user