Skip to content

Commit 5e9f53a

Browse files
authored
Merge pull request #218 from thedropbears/vision-limits
2 parents aa16499 + 9856190 commit 5e9f53a

File tree

3 files changed

+91
-44
lines changed

3 files changed

+91
-44
lines changed

components/vision.py

Lines changed: 61 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@
2222

2323
from components.chassis import ChassisComponent
2424
from utilities.caching import HasPerLoopCache, cache_per_loop
25+
from utilities.functions import clamp
2526
from utilities.game import APRILTAGS, apriltag_layout
2627
from 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+
4046
class 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(

physics.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -232,10 +232,13 @@ def update_sim(self, now: float, tm_diff: float) -> None:
232232
self.vision_encoder_sim.set(
233233
constrain_angle(
234234
(
235-
(2.0 * self.visual_localiser.servo.getPosition() - 1.0)
236-
* self.visual_localiser.SERVO_HALF_ANGLE
237-
)
238-
+ self.visual_localiser.servo_offset.radians()
235+
(
236+
self.visual_localiser.servo_offsets.full_range
237+
- self.visual_localiser.servo_offsets.neutral
238+
)
239+
* (2.0 * self.visual_localiser.servo.getPosition() - 1.0)
240+
+ self.visual_localiser.servo_offsets.neutral
241+
).radians()
239242
)
240243
)
241244

robot.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import math
2+
13
import magicbot
24
import ntcore
35
import wpilib
@@ -15,7 +17,7 @@
1517
from components.intake import IntakeComponent
1618
from components.led_component import LightStrip
1719
from components.shooter import ShooterComponent
18-
from components.vision import VisualLocalizer
20+
from components.vision import ServoOffsets, VisualLocalizer
1921
from components.wrist import WristComponent
2022
from controllers.algae_measurement import AlgaeMeasurement
2123
from controllers.algae_shooter import AlgaeShooter
@@ -92,7 +94,6 @@ def createObjects(self) -> None:
9294
self.vision_name = "ardu_cam"
9395
self.vision_encoder_id = DioChannel.VISION_ENCODER
9496
self.vision_servo_id = PwmChannel.VISION_SERVO
95-
9697
if wpilib.RobotController.getSerialNumber() == RioSerialNumber.TEST_BOT:
9798
self.chassis_swerve_config = SwerveConfig(
9899
drive_ratio=(14.0 / 50.0) * (25.0 / 19.0) * (15.0 / 45.0),
@@ -120,6 +121,14 @@ def createObjects(self) -> None:
120121
self.vision_rot = Rotation3d.fromDegrees(0, -20.0, 0)
121122
self.vision_servo_offset = Rotation2d(3.107)
122123
self.vision_encoder_offset = Rotation2d(3.052)
124+
# TODO set on test bot
125+
self.vision_camera_offset = Translation3d(0.05, 0, 0)
126+
self.vision_camera_pitch = math.radians(-20.0)
127+
self.vision_servo_offsets = ServoOffsets(
128+
neutral=Rotation2d(0.5757), full_range=Rotation2d(1.9234)
129+
)
130+
self.vision_rotation_range = (Rotation2d(1.525), Rotation2d(4.33))
131+
123132
else:
124133
self.chassis_swerve_config = SwerveConfig(
125134
drive_ratio=(14.0 / 50.0) * (27.0 / 17.0) * (15.0 / 45.0),
@@ -143,10 +152,15 @@ def createObjects(self) -> None:
143152
# metres between centre of front and back wheels
144153
self.chassis_wheel_base = 0.517
145154

146-
self.vision_pos = Translation3d(-0.050, -0.300, 0.660)
147-
self.vision_rot = Rotation3d.fromDegrees(0, 10.0, -135.0)
148-
self.vision_servo_offset = Rotation2d(6.235)
149-
self.vision_encoder_offset = Rotation2d(0.183795)
155+
self.vision_turret_pos = Translation3d(-0.030, -0.300, 0.660)
156+
self.vision_turret_rot = Rotation2d.fromDegrees(-90.0)
157+
self.vision_camera_offset = Translation3d(0.021, 0, 0)
158+
self.vision_camera_pitch = math.radians(10.0)
159+
self.vision_encoder_offset = Rotation2d(0.9796)
160+
self.vision_servo_offsets = ServoOffsets(
161+
neutral=Rotation2d(0.044), full_range=Rotation2d(1.37)
162+
)
163+
self.vision_rotation_range = (Rotation2d(4.611), Rotation2d(2.020))
150164

151165
self.coast_button = wpilib.DigitalInput(DioChannel.SWERVE_COAST_SWITCH)
152166
self.coast_button_pressed_event = wpilib.event.BooleanEvent(
@@ -273,8 +287,10 @@ def testPeriodic(self) -> None:
273287
# self.coral_placer_component.execute()
274288
self.shooter_component.execute()
275289
self.injector_component.execute()
276-
if self.gamepad.getStartButton():
290+
if self.gamepad.getLeftStickButton():
277291
self.vision.zero_servo_()
292+
elif self.gamepad.getRightStickButton():
293+
self.vision.full_range_servo_()
278294
else:
279295
self.vision.execute()
280296
self.wrist.execute()

0 commit comments

Comments
 (0)