Skip to content

Commit 46609fd

Browse files
committed
Implement heading profile in follower
1 parent e611068 commit 46609fd

File tree

3 files changed

+77
-30
lines changed

3 files changed

+77
-30
lines changed

TeamCode/src/main/kotlin/pioneer/Constants.kt

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,25 +99,33 @@ object Follower {
9999
/** The maximum drive velocity in cm per second. */
100100
const val MAX_DRIVE_VELOCITY = 100.0
101101

102+
102103
/** The maximum drive acceleration in cm per second squared. */
103104
const val MAX_DRIVE_ACCELERATION = 50.0
104105

106+
/** The maximum angular velocity in rad per second. */
107+
const val MAX_ANGULAR_VELOCITY = 0.0
108+
109+
/** The maximum angular acceleration in rad per second squared. */
110+
const val MAX_ANGULAR_ACCELERATION = 0.0
111+
105112
/** The maximum centripetal acceleration that the robot can handle in cm/s^2. */
106113
const val MAX_CENTRIPETAL_ACCELERATION = (70.0 * 70.0) / 25.0
107114

108115
// X-axis PID coefficients for the trajectory follower
109116
@JvmField var X_KP = 0.0
110-
111117
@JvmField var X_KI = 0.0
112-
113118
@JvmField var X_KD = 0.0
114119

115120
// Y-axis PID coefficients for the trajectory follower
116121
@JvmField var Y_KP = 0.0
117-
118122
@JvmField var Y_KI = 0.0
119-
120123
@JvmField var Y_KD = 0.0
124+
125+
// Theta PID coefficients for the trajectory follower
126+
@JvmField var THETA_KP = 0.0
127+
@JvmField var THETA_KI = 0.0
128+
@JvmField var THETA_KD = 0.0
121129
}
122130

123131
object Camera {

TeamCode/src/main/kotlin/pioneer/helpers/MathUtils.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package pioneer.helpers
22

33
import kotlin.math.PI
4+
import kotlin.math.cos
5+
import kotlin.math.sin
46

57
object MathUtils {
68
/**
@@ -33,4 +35,20 @@ object MathUtils {
3335
val step = (end - start) / (num - 1)
3436
return List(num) { i -> start + i * step }
3537
}
38+
39+
/**
40+
* Rotates a 2D vector by a given heading angle.
41+
* @param x X component of the vector
42+
* @param y Y component of the vector
43+
* @param heading Angle in radians to rotate the vector
44+
* @return Pair of rotated (x, y) components
45+
*/
46+
fun rotateVector(x: Double, y: Double, heading: Double): Pair<Double, Double> {
47+
val cos = cos(heading)
48+
val sin = sin(heading)
49+
return Pair(
50+
x * cos - y * sin,
51+
x * sin + y * cos
52+
)
53+
}
3654
}

TeamCode/src/main/kotlin/pioneer/pathing/follower/Follower.kt

Lines changed: 47 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package pioneer.pathing.follower
33
import com.qualcomm.robotcore.util.ElapsedTime
44
import pioneer.hardware.MecanumBase
55
import pioneer.helpers.FileLogger
6+
import pioneer.helpers.MathUtils
67
import pioneer.helpers.PIDController
78
import pioneer.helpers.Pose
89
import 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

Comments
 (0)