Skip to content

Commit c044306

Browse files
authored
Merge branch 'master' into dependabot/github_actions/actions/checkout-6
2 parents d19c02a + 46609fd commit c044306

7 files changed

Lines changed: 79 additions & 103 deletions

File tree

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
}

TeamCode/src/main/kotlin/pioneer/pathing/paths/CompoundPath.kt

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ class CompoundPath(
1313
) : Path {
1414
override var startPose: Pose = paths.first().startPose
1515
override var endPose: Pose = paths.last().endPose
16-
override var headingInterpolationMode: Path.HeadingInterpolationMode =
17-
Path.HeadingInterpolationMode.LINEAR
1816

1917
init {
2018
// Validate that the paths are connected end-to-end
@@ -27,12 +25,6 @@ class CompoundPath(
2725

2826
override fun getLength(): Double = paths.sumOf { it.getLength() }
2927

30-
override fun getHeading(t: Double): Double {
31-
val pathIndex = (t * paths.size).toInt().coerceIn(0, paths.size - 1)
32-
val localT = t * paths.size - pathIndex
33-
return paths[pathIndex].getHeading(localT)
34-
}
35-
3628
override fun getPoint(t: Double): Pose {
3729
val pathIndex = (t * paths.size).toInt().coerceIn(0, paths.size - 1)
3830
val localT = t * paths.size - pathIndex

TeamCode/src/main/kotlin/pioneer/pathing/paths/HermitePath.kt

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package pioneer.pathing.paths
22

33
import pioneer.helpers.Polynomial
44
import pioneer.helpers.Pose
5-
import pioneer.pathing.paths.Path.HeadingInterpolationMode
65
import kotlin.math.pow
76

87
/**
@@ -18,9 +17,6 @@ class HermitePath(
1817
startVelocity: Pose = Pose(),
1918
endVelocity: Pose = Pose(),
2019
) : Path {
21-
// Enum for heading interpolation mode
22-
override var headingInterpolationMode: HeadingInterpolationMode = HeadingInterpolationMode.LINEAR
23-
2420
// Hermite basis functions
2521
private val basis00 = Polynomial(arrayOf(1.0, 0.0, -3.0, 2.0))
2622
private val basis01 = Polynomial(arrayOf(0.0, 1.0, -2.0, 1.0))
@@ -61,26 +57,16 @@ class HermitePath(
6157
return compoundPath.getTFromLength(length)
6258
}
6359

64-
override fun getHeading(t: Double): Double {
65-
when (headingInterpolationMode) {
66-
HeadingInterpolationMode.LINEAR -> {
67-
return startPose.theta + (endPose.theta - startPose.theta) * t
68-
}
69-
}
70-
}
71-
7260
override fun getPoint(t: Double): Pose =
7361
Pose(
7462
x = xHermite.eval(t),
7563
y = yHermite.eval(t),
76-
theta = getHeading(t),
7764
)
7865

7966
override fun getPose(t: Double): Pose =
8067
Pose(
8168
x = xHermite.eval(t),
8269
y = yHermite.eval(t),
83-
theta = getHeading(t),
8470
vx = xHermite.nDerEval(t, 1),
8571
vy = yHermite.nDerEval(t, 1),
8672
ax = xHermite.nDerEval(t, 2),
@@ -128,7 +114,7 @@ class HermitePath(
128114
}
129115

130116
fun setTension(tension: Double): Builder {
131-
if (tension < 0.0 || tension > 1.0) {
117+
if (tension !in 0.0..1.0) {
132118
throw IllegalArgumentException("Tension must be in the range [0, 1]")
133119
}
134120
this.t = tension

TeamCode/src/main/kotlin/pioneer/pathing/paths/LinearPath.kt

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package pioneer.pathing.paths
22

3-
import pioneer.helpers.MathUtils
43
import pioneer.helpers.Pose
5-
import pioneer.pathing.paths.Path.HeadingInterpolationMode
64

75
/**
86
* LinearPath class representing a straight line path in 2D space
@@ -15,37 +13,22 @@ class LinearPath(
1513
) : Path {
1614
// Constructor overloads
1715
constructor(startX: Double, startY: Double, endX: Double, endY: Double) : this(Pose(startX, startY), Pose(endX, endY))
18-
19-
// Enum for heading interpolation mode
20-
override var headingInterpolationMode: HeadingInterpolationMode = HeadingInterpolationMode.LINEAR
21-
2216
override fun getLength(): Double = startPose.distanceTo(endPose)
2317

2418
override fun getLengthSoFar(t: Double): Double = getLength() * t
2519

2620
override fun getTFromLength(length: Double): Double = length / getLength()
2721

28-
override fun getHeading(t: Double): Double {
29-
when (headingInterpolationMode) {
30-
HeadingInterpolationMode.LINEAR -> {
31-
val delta = MathUtils.normalizeRadians(endPose.theta - startPose.theta)
32-
return startPose.theta + delta * t
33-
}
34-
}
35-
}
36-
3722
override fun getPoint(t: Double): Pose {
3823
val x = startPose.x + (endPose.x - startPose.x) * t
3924
val y = startPose.y + (endPose.y - startPose.y) * t
40-
val heading = getHeading(t)
41-
return Pose(x, y, heading)
25+
return Pose(x, y)
4226
}
4327

4428
override fun getPose(t: Double): Pose =
4529
Pose(
4630
x = startPose.x + (endPose.x - startPose.x) * t,
4731
y = startPose.y + (endPose.y - startPose.y) * t,
48-
theta = getHeading(t),
4932
vx = (endPose.x - startPose.x) / getLength(),
5033
vy = (endPose.y - startPose.y) / getLength(),
5134
)

TeamCode/src/main/kotlin/pioneer/pathing/paths/Path.kt

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,6 @@ import pioneer.helpers.Pose
66
* Path interface representing a path in 2D space
77
*/
88
interface Path {
9-
/**
10-
* Enum for heading interpolation modes
11-
*/
12-
enum class HeadingInterpolationMode {
13-
LINEAR,
14-
}
15-
16-
/**
17-
* Enum for heading interpolation mode
18-
*/
19-
var headingInterpolationMode: HeadingInterpolationMode
20-
219
/**
2210
* The start and end poses of the path
2311
*/
@@ -54,13 +42,6 @@ interface Path {
5442
*/
5543
fun getTFromLength(length: Double): Double
5644

57-
/**
58-
* Gets the heading at the given parameter t
59-
* @param t The parameter value in the range [0, 1]
60-
* @return The heading at the given parameter
61-
*/
62-
fun getHeading(t: Double): Double
63-
6445
/**
6546
* Gets the point at the given parameter t
6647
* @param t The parameter value in the range [0, 1]
@@ -88,17 +69,4 @@ interface Path {
8869
* @return The closest point on the path to the given position
8970
*/
9071
fun getClosestPoint(position: Pose): Pose = getPoint(getClosestPointT(position))
91-
92-
/**
93-
* Gets the heading goal at the given parameter t based on the interpolation mode
94-
* @param t The parameter value in the range [0, 1]
95-
* @return The heading goal at the given parameter
96-
*/
97-
fun getHeadingGoal(t: Double): Double {
98-
when (headingInterpolationMode) {
99-
HeadingInterpolationMode.LINEAR -> {
100-
return startPose.theta + (endPose.theta - startPose.theta) * t
101-
}
102-
}
103-
}
10472
}

0 commit comments

Comments
 (0)