2222
2323from components .chassis import ChassisComponent
2424from utilities .caching import HasPerLoopCache , cache_per_loop
25+ from utilities .functions import clamp
2526from utilities .game import APRILTAGS , apriltag_layout
2627from utilities .rev import configure_through_bore_encoder
27- from utilities .scalers import scale_value
2828
2929
3030@wpiutil .wpistruct .make_wpistruct
@@ -37,6 +37,12 @@ class VisibleTag:
3737 range : float
3838
3939
40+ @dataclass
41+ class ServoOffsets :
42+ neutral : Rotation2d
43+ full_range : Rotation2d
44+
45+
4046class VisualLocalizer (HasPerLoopCache ):
4147 """
4248 This localizes the robot from AprilTags on the field,
@@ -49,12 +55,8 @@ class VisualLocalizer(HasPerLoopCache):
4955 # Time since the last target sighting we allow before informing drivers
5056 TIMEOUT = 1.0 # s
5157
52- # Calibration constants for servo and encoder
53- # range between +/-135 deg
54- SERVO_HALF_ANGLE = math .radians (135 )
55-
5658 CAMERA_FOV = math .radians (
57- 65
59+ 68
5860 ) # photon vision says 69.8, but we are being conservative
5961 CAMERA_MAX_RANGE = 4.0 # m
6062
@@ -75,33 +77,46 @@ def __init__(
7577 # The name of the camera in PhotonVision.
7678 name : str ,
7779 # Position of the camera relative to the center of the robot
78- pos : Translation3d ,
79- # The camera rotation at its neutral position (ie centred).
80- rot : Rotation3d ,
80+ turret_pos : Translation3d ,
81+ # The turret rotation at its neutral position (ie centred).
82+ turret_rot : Rotation2d ,
83+ # The camera relative to the turret (ie without servo rotation)
84+ camera_offset : Translation3d ,
85+ # The camera pitch on the mount, relative to horizontal
86+ camera_pitch : float ,
8187 servo_id : int ,
82- servo_offset : Rotation2d ,
88+ servo_offsets : ServoOffsets ,
8389 encoder_id : int ,
8490 encoder_offset : Rotation2d ,
91+ # Encoder rotations at min and max of desired rotation range
92+ rotation_range : tuple [Rotation2d , Rotation2d ],
8593 field : wpilib .Field2d ,
8694 data_log : wpiutil .log .DataLog ,
8795 chassis : ChassisComponent ,
8896 ) -> None :
8997 super ().__init__ ()
9098 self .camera = PhotonCamera (name )
91- self .encoder = wpilib .DutyCycleEncoder (encoder_id , math .tau , 0 )
99+ self .encoder = wpilib .DutyCycleEncoder (encoder_id , math .pi , - math . pi )
92100 configure_through_bore_encoder (self .encoder )
93101 # Offset of encoder in radians when facing forwards (the desired zero)
94102 # To find this value, manually point the camera forwards and record the encoder value
95103 # This has nothing to do with the servo - do it by hand!!
96104 self .encoder_offset = encoder_offset
97105
98- # To find the servo offset, command the servo to neutral in test mode and record the encoder value
99- self .servo_offset = servo_offset
106+ # To find the servo offsets, command the servo to neutral in test mode and record the encoder value
107+ # Repeat for full range
108+ self .servo_offsets = servo_offsets
109+
110+ relative_rotations = [(r - encoder_offset ) for r in rotation_range ]
111+ self .min_rotation , self .max_rotation = relative_rotations
100112
101113 self .servo = wpilib .Servo (servo_id )
102- self .pos = pos
103- self .robot_to_turret = Transform3d (pos , rot )
104- self .robot_to_turret_2d = Transform2d (pos .toTranslation2d (), rot .toRotation2d ())
114+ self .pos = turret_pos
115+ self .robot_to_turret = Transform3d (turret_pos , Rotation3d (turret_rot ))
116+ self .robot_to_turret_2d = Transform2d (turret_pos .toTranslation2d (), turret_rot )
117+ self .turret_to_camera = Transform3d (
118+ camera_offset , Rotation3d (roll = 0.0 , pitch = camera_pitch , yaw = 0.0 )
119+ )
105120 self .turret_rotation_buffer = TimeInterpolatableRotation2dBuffer (2.0 )
106121
107122 self .last_timestamp = - 1.0
@@ -161,7 +176,8 @@ def visible_tags(self) -> list[VisibleTag]:
161176 distance = turret_to_tag .norm ()
162177 relative_facing = tag .pose .toPose2d ().rotation () - turret_to_tag .angle ()
163178 if (
164- abs (relative_bearing .radians ()) <= self .SERVO_HALF_ANGLE
179+ (relative_bearing - self .min_rotation ).radians () >= 0.0
180+ and (self .max_rotation - relative_bearing ).radians () >= 0.0
165181 and abs (relative_facing .degrees ()) > 100
166182 and distance < self .CAMERA_MAX_RANGE
167183 ):
@@ -176,41 +192,53 @@ def desired_turret_rotation(self) -> Rotation2d:
176192 # Read encoder angle and account for offset
177193 return self .relative_bearing_to_best_cluster ()
178194
195+ @feedback
196+ def desired_servo_rotation (self ) -> Rotation2d :
197+ return self .turret_to_servo (self .desired_turret_rotation ())
198+
179199 def turret_to_servo (self , turret : Rotation2d ) -> Rotation2d :
180- return turret - (self .servo_offset - self .encoder_offset )
200+ return turret - (self .servo_offsets . neutral - self .encoder_offset )
181201
182202 @property
183203 def turret_rotation (self ) -> Rotation2d :
184204 return self .raw_encoder_rotation () - self .encoder_offset
185205
186206 def robot_to_camera (self , timestamp : float ) -> Transform3d :
187- robot_to_turret_rotation = self .robot_to_turret .rotation ()
188207 turret_rotation = self .turret_rotation_buffer .sample (timestamp )
189208 if turret_rotation is None :
190209 return self .robot_to_turret
191- return Transform3d (
192- self .robot_to_turret .translation (),
193- Rotation3d (
194- robot_to_turret_rotation .x ,
195- robot_to_turret_rotation .y ,
196- robot_to_turret_rotation .z + turret_rotation .radians (),
197- ),
210+ r2t = self .robot_to_turret .translation ()
211+ t2c = (
212+ self .turret_to_camera .translation ()
213+ .rotateBy (self .turret_to_camera .rotation ())
214+ .rotateBy (Rotation3d (turret_rotation ))
215+ )
216+ trans = r2t + t2c
217+ rot = (
218+ self .robot_to_turret .rotation ()
219+ .rotateBy (Rotation3d (turret_rotation ))
220+ .rotateBy (self .turret_to_camera .rotation ())
198221 )
222+ return Transform3d (trans , rot )
199223
200224 def zero_servo_ (self ) -> None :
201225 # ONLY CALL THIS IN TEST MODE!
202226 # This is used to put the servo in a neutral position to record the encoder value at that point
203227 self .servo .set (0.5 )
204228
229+ def full_range_servo_ (self ) -> None :
230+ # ONLY CALL THIS IN TEST MODE!
231+ # This is used to put the servo to the full range position to record the encoder value at that point
232+ self .servo .set (1.0 )
233+
205234 def execute (self ) -> None :
235+ desired = self .turret_to_servo (self .desired_turret_rotation ())
236+ neutral = self .servo_offsets .neutral
237+ full = self .servo_offsets .full_range
238+ half_range = full - neutral
239+ delta = desired - neutral
206240 self .servo .set (
207- scale_value (
208- self .turret_to_servo (self .desired_turret_rotation ()).radians (),
209- - self .SERVO_HALF_ANGLE ,
210- self .SERVO_HALF_ANGLE ,
211- 0.0 ,
212- 1.0 ,
213- )
241+ clamp ((delta .radians () / half_range .radians () + 1.0 ) / 2.0 , 0.0 , 1.0 )
214242 )
215243
216244 self .turret_rotation_buffer .addSample (
0 commit comments