2024-09-14 23:10:02 -05:00
###############################################################################
## 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/>.
###############################################################################
2023-12-16 12:32:49 -06:00
from enum import Enum
2024-08-04 14:23:46 -04:00
from typing import List
2024-11-10 13:42:16 +08:00
2025-07-01 11:08:23 +08:00
import hal
2023-12-16 12:32:49 -06:00
import ntcore
2024-08-31 13:44:19 -04:00
# magical import to make serde stuff work
import photonlibpy . generated # noqa
2024-11-10 13:42:16 +08:00
import wpilib
from wpilib import RobotController , Timer
from . packet import Packet
from . targeting . photonPipelineResult import PhotonPipelineResult
2025-01-03 12:23:27 -06:00
from . timesync . timeSyncServer import inst
2024-11-11 09:16:02 +11:00
from . version import PHOTONLIB_VERSION # type: ignore[import-untyped]
2023-12-16 12:32:49 -06:00
class VisionLEDMode ( Enum ) :
kDefault = - 1
kOff = 0
kOn = 1
kBlink = 2
2024-01-06 06:17:06 -06:00
_lastVersionTimeCheck = 0.0
2023-12-16 12:32:49 -06:00
_VERSION_CHECK_ENABLED = True
def setVersionCheckEnabled ( enabled : bool ) :
2023-12-17 07:27:41 -06:00
global _VERSION_CHECK_ENABLED
2023-12-16 12:32:49 -06:00
_VERSION_CHECK_ENABLED = enabled
class PhotonCamera :
2025-07-01 11:08:23 +08:00
instance_count = 1
2023-12-16 12:32:49 -06:00
def __init__ ( self , cameraName : str ) :
2024-11-16 13:01:33 +11:00
""" Constructs a PhotonCamera from the name of the camera.
: param cameraName : The nickname of the camera ( found in the PhotonVision UI ) .
"""
2023-12-16 12:32:49 -06:00
instance = ntcore . NetworkTableInstance . getDefault ( )
2024-01-06 06:17:06 -06:00
self . _name = cameraName
2023-12-16 12:32:49 -06:00
self . _tableName = " photonvision "
photonvision_root_table = instance . getTable ( self . _tableName )
2024-01-06 06:17:06 -06:00
self . _cameraTable = photonvision_root_table . getSubTable ( cameraName )
self . _path = self . _cameraTable . getPath ( )
self . _rawBytesEntry = self . _cameraTable . getRawTopic ( " rawBytes " ) . subscribe (
2024-10-20 22:21:24 -07:00
f " photonstruct:PhotonPipelineResult: { PhotonPipelineResult . photonStruct . MESSAGE_VERSION } " ,
bytes ( [ ] ) ,
ntcore . PubSubOptions ( periodic = 0.01 , sendAll = True ) ,
2023-12-16 12:32:49 -06:00
)
2024-01-06 06:17:06 -06:00
self . _driverModePublisher = self . _cameraTable . getBooleanTopic (
2023-12-16 12:32:49 -06:00
" driverModeRequest "
) . publish ( )
2024-01-06 06:17:06 -06:00
self . _driverModeSubscriber = self . _cameraTable . getBooleanTopic (
2023-12-16 12:32:49 -06:00
" driverMode "
) . subscribe ( False )
2025-12-29 23:01:10 -06:00
self . _fpsLimitPublisher = self . _cameraTable . getIntegerTopic (
" fpsLimitRequest "
) . publish ( )
self . _fpsLimitSubscriber = self . _cameraTable . getIntegerTopic (
" fpsLimit "
) . subscribe ( - 1 )
2024-01-06 06:17:06 -06:00
self . _inputSaveImgEntry = self . _cameraTable . getIntegerTopic (
2023-12-16 12:32:49 -06:00
" inputSaveImgCmd "
) . getEntry ( 0 )
2024-01-06 06:17:06 -06:00
self . _outputSaveImgEntry = self . _cameraTable . getIntegerTopic (
2023-12-16 12:32:49 -06:00
" outputSaveImgCmd "
) . getEntry ( 0 )
2024-01-06 06:17:06 -06:00
self . _pipelineIndexRequest = self . _cameraTable . getIntegerTopic (
2023-12-16 12:32:49 -06:00
" pipelineIndexRequest "
) . publish ( )
2024-01-06 06:17:06 -06:00
self . _pipelineIndexState = self . _cameraTable . getIntegerTopic (
2023-12-16 12:32:49 -06:00
" pipelineIndexState "
) . subscribe ( 0 )
2024-01-06 06:17:06 -06:00
self . _heartbeatEntry = self . _cameraTable . getIntegerTopic ( " heartbeat " ) . subscribe (
2023-12-16 12:32:49 -06:00
- 1
)
2024-01-06 06:17:06 -06:00
self . _ledModeRequest = photonvision_root_table . getIntegerTopic (
2023-12-16 12:32:49 -06:00
" ledModeRequest "
) . publish ( )
2024-01-06 06:17:06 -06:00
self . _ledModeState = photonvision_root_table . getIntegerTopic (
2023-12-16 12:32:49 -06:00
" ledModeState "
) . subscribe ( - 1 )
self . versionEntry = photonvision_root_table . getStringTopic ( " version " ) . subscribe (
" "
)
# Existing is enough to make this multisubscriber do its thing
self . topicNameSubscriber = ntcore . MultiSubscriber (
instance , [ " /photonvision/ " ] , ntcore . PubSubOptions ( topicsOnly = True )
)
2024-01-06 06:17:06 -06:00
self . _prevHeartbeat = 0
self . _prevHeartbeatChangeTime = Timer . getFPGATimestamp ( )
2023-12-16 12:32:49 -06:00
2025-01-03 12:23:27 -06:00
# Start the time sync server
inst . start ( )
2025-07-01 11:08:23 +08:00
# Usage reporting
hal . report (
hal . tResourceType . kResourceType_PhotonCamera . value ,
PhotonCamera . instance_count ,
)
PhotonCamera . instance_count + = 1
2024-08-04 14:23:46 -04:00
def getAllUnreadResults ( self ) - > List [ PhotonPipelineResult ] :
"""
The list of pipeline results sent by PhotonVision since the last call to getAllUnreadResults ( ) .
Calling this function clears the internal FIFO queue , and multiple calls to
getAllUnreadResults ( ) will return different ( potentially empty ) result arrays . Be careful to
call this exactly ONCE per loop of your robot code ! FIFO depth is limited to 20 changes , so
make sure to call this frequently enough to avoid old results being discarded , too !
"""
self . _versionCheck ( )
changes = self . _rawBytesEntry . readQueue ( )
ret = [ ]
for change in changes :
byteList = change . value
timestamp = change . time
if len ( byteList ) < 1 :
pass
else :
newResult = PhotonPipelineResult ( )
pkt = Packet ( byteList )
2024-08-31 13:44:19 -04:00
newResult = PhotonPipelineResult . photonStruct . unpack ( pkt )
2024-08-04 14:23:46 -04:00
# NT4 allows us to correct the timestamp based on when the message was sent
2024-11-01 23:32:38 -07:00
newResult . ntReceiveTimestampMicros = timestamp
2024-08-04 14:23:46 -04:00
ret . append ( newResult )
return ret
2023-12-16 12:32:49 -06:00
def getLatestResult ( self ) - > PhotonPipelineResult :
2024-11-16 13:01:33 +11:00
""" Returns the latest pipeline result. This is simply the most recent result Received via NT.
Calling this multiple times will always return the most recent result .
Replaced by : meth : ` . getAllUnreadResults ` over getLatestResult , as this function can miss
results , or provide duplicate ones !
TODO implement the thing that will take this ones place . . .
"""
2023-12-16 12:32:49 -06:00
self . _versionCheck ( )
2024-05-10 14:04:34 -04:00
now = RobotController . getFPGATime ( )
2024-01-06 06:17:06 -06:00
packetWithTimestamp = self . _rawBytesEntry . getAtomic ( )
2023-12-16 12:32:49 -06:00
byteList = packetWithTimestamp . value
2024-08-31 13:44:19 -04:00
packetWithTimestamp . time
2023-12-16 12:32:49 -06:00
if len ( byteList ) < 1 :
2024-08-31 13:44:19 -04:00
return PhotonPipelineResult ( )
2023-12-16 12:32:49 -06:00
else :
2024-02-02 14:17:53 -06:00
pkt = Packet ( byteList )
2024-08-31 13:44:19 -04:00
retVal = PhotonPipelineResult . photonStruct . unpack ( pkt )
2024-05-10 14:04:34 -04:00
# We don't trust NT4 time, hack around
2024-08-31 13:44:19 -04:00
retVal . ntReceiveTimestampMicros = now
2023-12-16 12:32:49 -06:00
return retVal
def getDriverMode ( self ) - > bool :
2024-11-16 13:01:33 +11:00
""" Returns whether the camera is in driver mode.
: returns : Whether the camera is in driver mode .
"""
2024-01-06 06:17:06 -06:00
return self . _driverModeSubscriber . get ( )
2023-12-16 12:32:49 -06:00
def setDriverMode ( self , driverMode : bool ) - > None :
2024-11-16 13:01:33 +11:00
""" Toggles driver mode.
: param driverMode : Whether to set driver mode .
"""
2024-01-06 06:17:06 -06:00
self . _driverModePublisher . set ( driverMode )
2023-12-16 12:32:49 -06:00
2025-12-29 23:01:10 -06:00
def getFPSLimit ( self ) - > int :
""" Returns the current FPS limit set on the camera.
: returns : The current FPS limit .
"""
return self . _fpsLimitSubscriber . get ( )
def setFPSLimit ( self , fpsLimit : int ) - > None :
""" Sets the FPS limit on the camera.
: param fpsLimit : The FPS limit to set . Set to - 1 for unlimited FPS .
"""
self . _fpsLimitPublisher . set ( fpsLimit )
2023-12-16 12:32:49 -06:00
def takeInputSnapshot ( self ) - > None :
2024-11-16 13:01:33 +11:00
""" Request the camera to save a new image file from the input camera stream with overlays. Images
take up space in the filesystem of the PhotonCamera . Calling it frequently will fill up disk
space and eventually cause the system to stop working . Clear out images in
/ opt / photonvision / photonvision_config / imgSaves frequently to prevent issues .
"""
2024-01-06 06:17:06 -06:00
self . _inputSaveImgEntry . set ( self . _inputSaveImgEntry . get ( ) + 1 )
2023-12-16 12:32:49 -06:00
def takeOutputSnapshot ( self ) - > None :
2024-11-16 13:01:33 +11:00
""" Request the camera to save a new image file from the output stream with overlays. Images take
up space in the filesystem of the PhotonCamera . Calling it frequently will fill up disk space
and eventually cause the system to stop working . Clear out images in
/ opt / photonvision / photonvision_config / imgSaves frequently to prevent issues .
"""
2024-01-06 06:17:06 -06:00
self . _outputSaveImgEntry . set ( self . _outputSaveImgEntry . get ( ) + 1 )
2023-12-16 12:32:49 -06:00
def getPipelineIndex ( self ) - > int :
2024-11-16 13:01:33 +11:00
""" Returns the active pipeline index.
: returns : The active pipeline index .
"""
2024-01-06 06:17:06 -06:00
return self . _pipelineIndexState . get ( 0 )
2023-12-16 12:32:49 -06:00
def setPipelineIndex ( self , index : int ) - > None :
2024-11-16 13:01:33 +11:00
""" Allows the user to select the active pipeline index.
: param index : The active pipeline index .
"""
2024-01-06 06:17:06 -06:00
self . _pipelineIndexRequest . set ( index )
2023-12-16 12:32:49 -06:00
def getLEDMode ( self ) - > VisionLEDMode :
2024-11-16 13:01:33 +11:00
""" Returns the current LED mode.
: returns : The current LED mode .
"""
2024-01-06 06:17:06 -06:00
mode = self . _ledModeState . get ( )
2023-12-16 12:32:49 -06:00
return VisionLEDMode ( mode )
def setLEDMode ( self , led : VisionLEDMode ) - > None :
2024-11-16 13:01:33 +11:00
""" Sets the LED mode.
: param led : The mode to set to .
"""
2024-01-06 06:17:06 -06:00
self . _ledModeRequest . set ( led . value )
2023-12-16 12:32:49 -06:00
def getName ( self ) - > str :
2024-11-16 13:01:33 +11:00
""" Returns the name of the camera. This will return the same value that was given to the
constructor as cameraName .
: returns : The name of the camera .
"""
2024-01-06 06:17:06 -06:00
return self . _name
2023-12-16 12:32:49 -06:00
def isConnected ( self ) - > bool :
2024-11-16 13:01:33 +11:00
""" Returns whether the camera is connected and actively returning new data. Connection status is
debounced .
: returns : True if the camera is actively sending frame data , false otherwise .
"""
2024-01-06 06:17:06 -06:00
curHeartbeat = self . _heartbeatEntry . get ( )
2023-12-16 12:32:49 -06:00
now = Timer . getFPGATimestamp ( )
2024-01-06 06:17:06 -06:00
if curHeartbeat != self . _prevHeartbeat :
self . _prevHeartbeat = curHeartbeat
self . _prevHeartbeatChangeTime = now
2023-12-16 12:32:49 -06:00
2024-01-06 06:17:06 -06:00
return ( now - self . _prevHeartbeatChangeTime ) < 0.5
2023-12-16 12:32:49 -06:00
def _versionCheck ( self ) - > None :
2024-01-06 06:17:06 -06:00
global _lastVersionTimeCheck
2023-12-17 07:27:41 -06:00
2023-12-16 12:32:49 -06:00
if not _VERSION_CHECK_ENABLED :
return
2024-01-06 06:17:06 -06:00
if ( Timer . getFPGATimestamp ( ) - _lastVersionTimeCheck ) < 5.0 :
2023-12-16 12:32:49 -06:00
return
2024-01-06 06:17:06 -06:00
_lastVersionTimeCheck = Timer . getFPGATimestamp ( )
2023-12-17 07:27:41 -06:00
2024-11-16 13:01:33 +11:00
# Heartbeat entry is assumed to always be present. If it's not present, we
# assume that a camera with that name was never connected in the first place.
2024-01-06 06:17:06 -06:00
if not self . _heartbeatEntry . exists ( ) :
2023-12-16 12:32:49 -06:00
cameraNames = (
2024-01-06 06:17:06 -06:00
self . _cameraTable . getInstance ( ) . getTable ( self . _tableName ) . getSubTables ( )
2023-12-16 12:32:49 -06:00
)
2024-05-10 14:58:18 -04:00
# Look for only cameras with rawBytes entry that exists
cameraNames = list (
filter (
lambda it : self . _cameraTable . getSubTable ( it )
. getEntry ( " rawBytes " )
. exists ( ) ,
cameraNames ,
)
)
2023-12-16 12:32:49 -06:00
if len ( cameraNames ) == 0 :
wpilib . reportError (
" Could not find any PhotonVision coprocessors on NetworkTables. Double check that PhotonVision is running, and that your camera is connected! " ,
False ,
)
else :
wpilib . reportError (
2024-01-06 06:17:06 -06:00
f " PhotonVision coprocessor at path { self . _path } not found in Network Tables. Double check that your camera names match! Only the following camera names were found: { ' ' . join ( cameraNames ) } " ,
2023-12-16 12:32:49 -06:00
True ,
)
2024-11-16 13:01:33 +11:00
# Check for connection status. Warn if disconnected.
2023-12-16 12:32:49 -06:00
elif not self . isConnected ( ) :
wpilib . reportWarning (
2024-01-06 06:17:06 -06:00
f " PhotonVision coprocessor at path { self . _path } is not sending new data. " ,
2023-12-16 12:32:49 -06:00
True ,
)
versionString = self . versionEntry . get ( defaultValue = " " )
2024-10-20 22:21:24 -07:00
2024-11-16 13:01:33 +11:00
# Check mdef UUID
localUUID = PhotonPipelineResult . photonStruct . MESSAGE_VERSION
2024-11-19 13:28:07 +11:00
remoteUUID = self . _rawBytesEntry . getTopic ( ) . getProperty ( " message_uuid " )
2024-09-22 22:27:13 -04:00
2024-11-19 13:28:07 +11:00
if remoteUUID is None :
2024-09-22 22:27:13 -04:00
wpilib . reportWarning (
f " PhotonVision coprocessor at path { self . _path } has not reported a message interface UUID - is your coprocessor ' s camera started? " ,
True ,
)
2024-11-19 13:28:07 +11:00
else :
# ntcore hands us a JSON string with leading/trailing quotes - remove those
remoteUUID = str ( remoteUUID ) . replace ( ' " ' , " " )
if localUUID != remoteUUID :
# Verified version mismatch
bfw = """
\n \n \n
>> >> >> >> >> >> >> >> >> >> >> >> >> >> >> >> >> >> >> >> >> >> >
>> > ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !
>> >
>> > You are running an incompatible version
>> > of PhotonVision on your coprocessor !
>> >
>> > This is neither tested nor supported .
>> > You MUST update PhotonVision ,
>> > PhotonLib , or both .
>> >
>> > Your code will now crash .
>> > We hope your day gets better .
>> >
>> > ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !
>> >> >> >> >> >> >> >> >> >> >> >> >> >> >> >> >> >> >> >> >> >> >
\n \n
"""
wpilib . reportWarning ( bfw )
errText = f " Photonlibpy version { PHOTONLIB_VERSION } (With message UUID { localUUID } ) does not match coprocessor version { versionString } (with message UUID { remoteUUID } ). Please install photonlibpy version { versionString } , or update your coprocessor to { PHOTONLIB_VERSION } . "
wpilib . reportError ( errText , True )
raise Exception ( errText )