2222
2323from components .chassis import ChassisComponent
2424from utilities .caching import HasPerLoopCache , cache_per_loop
25- from utilities .functions import clamp
2625from utilities .game import APRILTAGS , apriltag_layout
2726from utilities .rev import configure_through_bore_encoder
27+ from utilities .scalers import scale_value
2828
2929
3030@wpiutil .wpistruct .make_wpistruct
@@ -37,12 +37,6 @@ class VisibleTag:
3737 range : float
3838
3939
40- @dataclass
41- class ServoOffsets :
42- neutral : Rotation2d
43- full_range : Rotation2d
44-
45-
4640class VisualLocalizer (HasPerLoopCache ):
4741 """
4842 This localizes the robot from AprilTags on the field,
@@ -55,8 +49,12 @@ class VisualLocalizer(HasPerLoopCache):
5549 # Time since the last target sighting we allow before informing drivers
5650 TIMEOUT = 1.0 # s
5751
52+ # Calibration constants for servo and encoder
53+ # range between +/-135 deg
54+ SERVO_HALF_ANGLE = math .radians (135 )
55+
5856 CAMERA_FOV = math .radians (
59- 68
57+ 65
6058 ) # photon vision says 69.8, but we are being conservative
6159 CAMERA_MAX_RANGE = 4.0 # m
6260
@@ -77,46 +75,33 @@ def __init__(
7775 # The name of the camera in PhotonVision.
7876 name : str ,
7977 # Position of the camera relative to the center of the robot
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 ,
78+ pos : Translation3d ,
79+ # The camera rotation at its neutral position (ie centred).
80+ rot : Rotation3d ,
8781 servo_id : int ,
88- servo_offsets : ServoOffsets ,
82+ servo_offset : Rotation2d ,
8983 encoder_id : int ,
9084 encoder_offset : Rotation2d ,
91- # Encoder rotations at min and max of desired rotation range
92- rotation_range : tuple [Rotation2d , Rotation2d ],
9385 field : wpilib .Field2d ,
9486 data_log : wpiutil .log .DataLog ,
9587 chassis : ChassisComponent ,
9688 ) -> None :
9789 super ().__init__ ()
9890 self .camera = PhotonCamera (name )
99- self .encoder = wpilib .DutyCycleEncoder (encoder_id , math .pi , - math . pi )
91+ self .encoder = wpilib .DutyCycleEncoder (encoder_id , math .tau , 0 )
10092 configure_through_bore_encoder (self .encoder )
10193 # Offset of encoder in radians when facing forwards (the desired zero)
10294 # To find this value, manually point the camera forwards and record the encoder value
10395 # This has nothing to do with the servo - do it by hand!!
10496 self .encoder_offset = encoder_offset
10597
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
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
112100
113101 self .servo = wpilib .Servo (servo_id )
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- )
102+ self .pos = pos
103+ self .robot_to_turret = Transform3d (pos , rot )
104+ self .robot_to_turret_2d = Transform2d (pos .toTranslation2d (), rot .toRotation2d ())
120105 self .turret_rotation_buffer = TimeInterpolatableRotation2dBuffer (2.0 )
121106
122107 self .last_timestamp = - 1.0
@@ -176,8 +161,7 @@ def visible_tags(self) -> list[VisibleTag]:
176161 distance = turret_to_tag .norm ()
177162 relative_facing = tag .pose .toPose2d ().rotation () - turret_to_tag .angle ()
178163 if (
179- (relative_bearing - self .min_rotation ).radians () >= 0.0
180- and (self .max_rotation - relative_bearing ).radians () >= 0.0
164+ abs (relative_bearing .radians ()) <= self .SERVO_HALF_ANGLE
181165 and abs (relative_facing .degrees ()) > 100
182166 and distance < self .CAMERA_MAX_RANGE
183167 ):
@@ -192,53 +176,41 @@ def desired_turret_rotation(self) -> Rotation2d:
192176 # Read encoder angle and account for offset
193177 return self .relative_bearing_to_best_cluster ()
194178
195- @feedback
196- def desired_servo_rotation (self ) -> Rotation2d :
197- return self .turret_to_servo (self .desired_turret_rotation ())
198-
199179 def turret_to_servo (self , turret : Rotation2d ) -> Rotation2d :
200- return turret - (self .servo_offsets . neutral - self .encoder_offset )
180+ return turret - (self .servo_offset - self .encoder_offset )
201181
202182 @property
203183 def turret_rotation (self ) -> Rotation2d :
204184 return self .raw_encoder_rotation () - self .encoder_offset
205185
206186 def robot_to_camera (self , timestamp : float ) -> Transform3d :
187+ robot_to_turret_rotation = self .robot_to_turret .rotation ()
207188 turret_rotation = self .turret_rotation_buffer .sample (timestamp )
208189 if turret_rotation is None :
209190 return self .robot_to_turret
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 ())
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+ ),
221198 )
222- return Transform3d (trans , rot )
223199
224200 def zero_servo_ (self ) -> None :
225201 # ONLY CALL THIS IN TEST MODE!
226202 # This is used to put the servo in a neutral position to record the encoder value at that point
227203 self .servo .set (0.5 )
228204
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-
234205 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
240206 self .servo .set (
241- clamp ((delta .radians () / half_range .radians () + 1.0 ) / 2.0 , 0.0 , 1.0 )
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+ )
242214 )
243215
244216 self .turret_rotation_buffer .addSample (
0 commit comments