@@ -3,6 +3,7 @@ package pioneer.pathing.follower
33import com.qualcomm.robotcore.util.ElapsedTime
44import pioneer.hardware.MecanumBase
55import pioneer.helpers.FileLogger
6+ import pioneer.helpers.MathUtils
67import pioneer.helpers.PIDController
78import pioneer.helpers.Pose
89import pioneer.localization.Localizer
@@ -18,6 +19,8 @@ class Follower(
1819 private val mecanumBase : MecanumBase ,
1920) {
2021 var motionProfile: MotionProfile ? = null
22+ var headingProfile: MotionProfile ? = null
23+
2124 private var elapsedTime: ElapsedTime = ElapsedTime ()
2225 private var xPID =
2326 PIDController (
@@ -32,6 +35,13 @@ class Follower(
3235 kd = FollowerConstants .Y_KD ,
3336 )
3437
38+ private var headingPID =
39+ PIDController (
40+ kp = FollowerConstants .THETA_KP ,
41+ ki = FollowerConstants .THETA_KI ,
42+ kd = FollowerConstants .THETA_KD ,
43+ )
44+
3545 var path: Path ? = null
3646 set(value) {
3747 field = value
@@ -61,17 +71,15 @@ class Follower(
6171 }
6272
6373 fun update (dt : Double ) {
64- if (motionProfile == null || path == null ) {
74+ if (motionProfile == null || headingProfile == null || path == null ) {
6575// FileLogger.error("Follower", "No path or motion profile set")
6676 return
6777 }
6878 // Get the time since the follower started
6979 val t = elapsedTime.seconds().coerceAtMost(motionProfile!! .duration())
7080 // Get the target state from the motion profile
7181 val targetState = motionProfile!! [t]
72-
73- FileLogger .debug(" Follower" , " Target Velocity: " + targetState.v)
74- FileLogger .debug(" Follower" , " Target Acceleration " + targetState.a)
82+ val headingTarget = headingProfile!! [t]
7583
7684 // Calculate the parameter t for the path based on the target state
7785 val pathT = path!! .getTFromLength(targetState.x)
@@ -92,34 +100,37 @@ class Follower(
92100 vy = tangent.y * targetState.v,
93101 ax = targetState.a * tangent.x + targetState.v.pow(2 ) * curvature * - tangent.y,
94102 ay = targetState.a * tangent.y + targetState.v.pow(2 ) * curvature * tangent.x,
95- theta = localizer.pose.theta,
103+ theta = headingTarget.x,
104+ omega = headingTarget.v,
105+ alpha = headingTarget.a
96106 )
97107
98- // Calculate the position error and convert to robot-centric coordinates
99- val positionError =
108+ // Calculate the error and convert to robot-centric coordinates
109+ val error =
100110 Pose (
101111 x = targetPose.x - localizer.pose.x,
102112 y = targetPose.y - localizer.pose.y,
113+ theta = MathUtils .normalizeRadians(headingTarget.x - localizer.pose.theta)
103114 )
104115
105116 // Calculate the PID outputs
106- val xCorrection = xPID.update(positionError.x, dt)
107- val yCorrection = yPID.update(positionError.y, dt)
117+ val xCorrection = xPID.update(error.x, dt)
118+ val yCorrection = yPID.update(error.y, dt)
119+ val turnCorrection = headingPID.update(error.theta, dt)
108120
109121 // Apply corrections to velocity directly
110122 // Rotate to convert to robot-centric coordinates
111- val correctedPose =
112- targetPose
113- .copy(
114- vx = targetPose.vx + xCorrection,
115- vy = targetPose.vy + yCorrection,
116- ).rotate(- localizer.pose.theta)
117-
118- FileLogger .debug(" Follower" , " Target pose: $targetPose " )
119- FileLogger .debug(" Follower" , " Corrected pose: $correctedPose " )
123+ val (vxRobot, vyRobot) = MathUtils .rotateVector(
124+ x = targetPose.vx + xCorrection,
125+ y = targetPose.vy + yCorrection,
126+ heading = - localizer.pose.theta
127+ )
120128
121- // TODO: Heading interpolation
122- // TODO: Add heading error correction
129+ val correctedPose = targetPose.copy(
130+ vx = vxRobot,
131+ vy = vyRobot,
132+ omega = targetPose.omega + turnCorrection
133+ )
123134
124135 mecanumBase.setDriveVA(correctedPose)
125136 }
@@ -129,15 +140,17 @@ class Follower(
129140 elapsedTime.reset()
130141 // Reset the PID controllers
131142 xPID.reset()
132- yPID.reset()
143+ yPID.reset()
144+ headingPID.reset()
133145 }
134146
135147 private fun reset () {
136148 // Recalculate the motion profile when the path is set
137- calculateMotionProfile()
149+ motionProfile = calculateMotionProfile()
150+ headingProfile = calculateHeadingProfile()
138151 }
139152
140- private fun calculateMotionProfile () {
153+ private fun calculateMotionProfile (): MotionProfile ? {
141154 if (path != null ) {
142155 val totalDistance = path!! .getLength()
143156 val startState = MotionState (0.0 , 0.0 , 0.0 )
@@ -157,15 +170,23 @@ class Follower(
157170 // Constant acceleration constraint
158171 FollowerConstants .MAX_DRIVE_ACCELERATION
159172 }
160- motionProfile =
161- MotionProfileGenerator .generateMotionProfile(
173+ return MotionProfileGenerator .generateMotionProfile(
162174 startState,
163175 endState,
164176 velocityConstraint,
165177 accelerationConstraint,
166178 )
167179 } else {
168- motionProfile = null
180+ return null
169181 }
170182 }
183+
184+ private fun calculateHeadingProfile (): MotionProfile ? {
185+ return if (path != null ) MotionProfileGenerator .generateMotionProfile(
186+ MotionState (localizer.pose.theta, 0.0 , 0.0 ),
187+ MotionState (path!! .endPose.theta, 0.0 , 0.0 ),
188+ { FollowerConstants .MAX_ANGULAR_VELOCITY },
189+ { FollowerConstants .MAX_ANGULAR_ACCELERATION },
190+ ) else null
191+ }
171192}
0 commit comments