Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add photonlibpy #1040

Merged
merged 32 commits into from
Dec 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ce84d07
First pass adding a pure-python implementation of photonlib
gerth2 Dec 10, 2023
03bde5c
wip github actions
gerth2 Dec 10, 2023
1a1ef91
more wip github
gerth2 Dec 11, 2023
514c526
one more...
gerth2 Dec 11, 2023
5adcac2
more bugfxes in github workflow
gerth2 Dec 11, 2023
177ac92
another one
gerth2 Dec 11, 2023
bc93c04
another one
gerth2 Dec 11, 2023
6b78555
versioning and dependency tweaks
gerth2 Dec 11, 2023
a27f48c
still working to fix issues
gerth2 Dec 11, 2023
935c434
minor tweak to trigger CI
gerth2 Dec 11, 2023
ddfdfbf
trying even more
gerth2 Dec 11, 2023
a1f1ffd
git on github action is poopy
gerth2 Dec 11, 2023
4be9004
my cat is fuzzy
gerth2 Dec 11, 2023
9364d21
review comments pt 1
gerth2 Dec 12, 2023
d63bb2f
better unit test passing
gerth2 Dec 12, 2023
e14e69d
missed python workflow update
gerth2 Dec 12, 2023
eba7c76
later python?
gerth2 Dec 12, 2023
3fb8b8e
wpiformat, better test infrastructure, derive tag name
gerth2 Dec 13, 2023
e262b79
whatever bro
gerth2 Dec 13, 2023
efa95e4
git describe is not
gerth2 Dec 13, 2023
d591c2c
are ya happy wpiformat. Just be happy.
gerth2 Dec 13, 2023
e38ecdf
un bork index html
gerth2 Dec 13, 2023
816bb5b
added version check
gerth2 Dec 14, 2023
083c9fe
wip better
gerth2 Dec 15, 2023
c582ea2
tweaked name and moved back to tags
gerth2 Dec 15, 2023
4b1af18
ran wpiformat
gerth2 Dec 15, 2023
e87c5d9
Baked more detail into the version without violating pep 440
gerth2 Dec 15, 2023
90f2a4c
i am asking you once again to run wpiformat
gerth2 Dec 15, 2023
9164862
maybe v4 is better
gerth2 Dec 16, 2023
fbfe9f8
Merge branch 'master' into master
mcm001 Dec 16, 2023
5436791
Merge branch 'master' into master
mcm001 Dec 16, 2023
3daf384
Merge branch 'master' into master
mcm001 Dec 16, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .github/workflows/python.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Build and Distribute PhotonLibPy

permissions:
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing

on:
push:
branches: [ master ]
tags:
- 'v*'
pull_request:
branches: [ master ]

jobs:
buildAndDeploy:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v3
with:
sparse-checkout-cone-mode: false
fetch-tags: true
fetch-depth: 99999

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.11

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel pytest

- name: Build wheel
working-directory: ./photon-lib/py
run: |
python setup.py sdist bdist_wheel
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd recommend using pipx run build.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image
pipx is currently being very sad about installing for me.... thoughts on leaving this one for later?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, pipx's docs recommend scoop over pip for installing pipx.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd probably specifically recommend https://github.com/hynek/build-and-inspect-python-package in GitHub Actions though


- name: Run Unit Tests
working-directory: ./photon-lib/py
run: |
pip install --no-cache-dir dist/*.whl
pytest


- name: Upload artifacts
uses: actions/upload-artifact@master
with:
name: dist
path: ./photon-lib/py/dist/

- name: Publish package distributions to TestPyPI
# Only upload on tags
if: startsWith(github.ref, 'refs/tags/v')
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages_dir: ./photon-lib/py/dist/

permissions:
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
5 changes: 5 additions & 0 deletions photon-lib/py/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
photonlibpy.egg-info/
dist/
build/
.eggs/
photonlibpy/version.py
14 changes: 14 additions & 0 deletions photon-lib/py/buildAndTest.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
:: Uninstall if it already was installed
pip uninstall -y photonlibpy

:: Build wheel
python setup.py bdist_wheel

:: Install whatever wheel was made
for %%f in (dist/*.whl) do (
echo installing dist/%%f
pip install --no-cache-dir dist/%%f
)

:: Run the test suite
pytest
1 change: 1 addition & 0 deletions photon-lib/py/photonlibpy/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# No one here but us chickens
45 changes: 45 additions & 0 deletions photon-lib/py/photonlibpy/multiTargetPNPResult.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
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)
gerth2 marked this conversation as resolved.
Show resolved Hide resolved
ambiguity: float = 0.0
bestReprojError: float = 0.0
altReprojError: float = 0.0

def createFromPacket(self, packet: Packet) -> Packet:
self.isPresent = packet.decodeBoolean()
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
143 changes: 143 additions & 0 deletions photon-lib/py/photonlibpy/packet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import struct
from wpimath.geometry import Transform3d, Translation3d, Rotation3d, Quaternion
import wpilib


class Packet:
def __init__(self, data: list[int]):
"""
* Constructs an empty packet.
*
* @param self.size The self.size of the packet buffer.
"""
self.packetData = data
self.size = len(data)
self.readPos = 0
self.outOfBytes = False

def clear(self):
"""Clears the packet and resets the read and write positions."""
self.packetData = [0] * self.size
self.readPos = 0
self.outOfBytes = False

def getSize(self):
return self.size

_NO_MORE_BYTES_MESSAGE = """
Photonlib - Ran out of bytes while decoding.
Make sure the version of photonvision on the coprocessor
matches the version of photonlib running in the robot code.
"""

def _getNextByte(self) -> int:
retVal = 0x00

if not self.outOfBytes:
try:
retVal = 0x00FF & self.packetData[self.readPos]
self.readPos += 1
except IndexError:
wpilib.reportError(Packet._NO_MORE_BYTES_MESSAGE, True)
self.outOfBytes = True

return retVal

def getData(self) -> list[int]:
"""
* Returns the packet data.
*
* @return The packet data.
"""
return self.packetData

def setData(self, data: list[int]):
"""
* Sets the packet data.
*
* @param data The packet data.
"""
self.clear()
self.packetData = data
self.size = len(self.packetData)

def _decodeGeneric(self, unpackFormat, numBytes):
# Read ints in from the data buffer
intList = []
for _ in range(numBytes):
intList.append(self._getNextByte())

# Interpret the bytes as a floating point number
value = struct.unpack(unpackFormat, bytes(intList))[0]

return value

def decode8(self) -> int:
"""
* Returns a single decoded byte from the packet.
*
* @return A decoded byte from the packet.
"""
return self._decodeGeneric(">b", 1)

def decode16(self) -> int:
"""
* Returns a single decoded byte from the packet.
*
* @return A decoded byte from the packet.
"""
return self._decodeGeneric(">h", 2)

def decode32(self) -> int:
"""
* Returns a decoded int (32 bytes) from the packet.
*
* @return A decoded int from the packet.
"""
return self._decodeGeneric(">l", 4)

def decodeDouble(self) -> float:
"""
* Returns a decoded double from the packet.
*
* @return A decoded double from the packet.
"""
return self._decodeGeneric(">d", 8)

def decodeBoolean(self) -> bool:
"""
* Returns a decoded boolean from the packet.
*
* @return A decoded boolean from the packet.
"""
return self.decode8() == 1

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 decodeTransform(self) -> Transform3d:
"""
* Returns a decoded Transform3d
*
* @return A decoded Tansform3d from the packet.
"""
x = self.decodeDouble()
y = self.decodeDouble()
z = self.decodeDouble()
translation = Translation3d(x, y, z)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI we implemented getStructTopic, not currently released but it'll be available in the kickoff release. See https://github.com/robotpy/mostrobotpy/blob/main/subprojects/pyntcore/tests/test_struct_topic.py


w = self.decodeDouble()
x = self.decodeDouble()
y = self.decodeDouble()
z = self.decodeDouble()
rotation = Rotation3d(Quaternion(w, x, y, z))

return Transform3d(translation, rotation)
Loading
Loading