Skip to content

Commit eb186d2

Browse files
committed
Bang Bang controller
1 parent 24702c0 commit eb186d2

9 files changed

Lines changed: 157 additions & 53 deletions

File tree

.kotlin/sessions/kotlin-compiler-4566496875608200174.salive

Whitespace-only changes.

nextcontrol/src/main/kotlin/dev/nextftc/nextcontrol/builder/ElementBuilders.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,28 +19,28 @@ class FeedbackElementBuilder {
1919
apply { feedbackElement = feedback }
2020

2121
fun posPid(coefficients: PIDCoefficients) =
22-
custom(PIDElement(PIDType.POSITION, coefficients))
22+
custom(PIDElement(FeedbackType.POSITION, coefficients))
2323

2424
@JvmOverloads
2525
fun posPid(kP: Double, kI: Double = 0.0, kD: Double = 0.0) =
2626
posPid(PIDCoefficients(kP, kI, kD))
2727

2828
fun velPid(coefficients: PIDCoefficients) =
29-
custom(PIDElement(PIDType.VELOCITY, coefficients))
29+
custom(PIDElement(FeedbackType.VELOCITY, coefficients))
3030

3131
@JvmOverloads
3232
fun velPid(kP: Double, kI: Double = 0.0, kD: Double = 0.0) =
3333
velPid(PIDCoefficients(kP, kI, kD))
3434

3535
fun posSquID(coefficients: PIDCoefficients) =
36-
custom(SquIDElement(PIDType.POSITION, coefficients))
36+
custom(SquIDElement(FeedbackType.POSITION, coefficients))
3737

3838
@JvmOverloads
3939
fun posSquID(kP: Double, kI: Double = 0.0, kD: Double = 0.0) =
4040
posSquID(PIDCoefficients(kP, kI, kD))
4141

4242
fun velSquID(coefficients: PIDCoefficients) =
43-
custom(SquIDElement(PIDType.VELOCITY, coefficients))
43+
custom(SquIDElement(FeedbackType.VELOCITY, coefficients))
4444

4545
@JvmOverloads
4646
fun velSquID(kP: Double, kI: Double = 0.0, kD: Double = 0.0) =
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* NextFTC: a user-friendly control library for FIRST Tech Challenge
3+
* Copyright (C) 2025 Rowan McAlpin
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
package dev.nextftc.nextcontrol.feedback
20+
21+
import dev.nextftc.nextcontrol.KineticState
22+
import kotlin.math.sign
23+
24+
/**
25+
* A [FeedbackElement] that is a Bang Bang controller (when error is positive, output 1, when error is negative, output
26+
* -1, when error is 0, output 0.
27+
*
28+
* @param feedbackType The type of component this controller operates on
29+
*
30+
* @author rowan-mcalpin
31+
*/
32+
class BangBangElement(
33+
private val feedbackType: FeedbackType,
34+
) : FeedbackElement {
35+
36+
override fun calculate(error: KineticState): Double = when (feedbackType) {
37+
FeedbackType.POSITION -> sign(error.position)
38+
FeedbackType.VELOCITY -> sign(error.velocity)
39+
}
40+
}

nextcontrol/src/main/kotlin/dev/nextftc/nextcontrol/feedback/FeedbackElement.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,13 @@ fun interface FeedbackElement {
4242
* Resets this element
4343
*/
4444
fun reset() { }
45-
}
45+
}
46+
47+
/**
48+
* Which component a FeedbackElement operates on (position or velocity)
49+
*
50+
* @author Zach.Waffle, rowan-mcalpin
51+
*/
52+
enum class FeedbackType {
53+
POSITION, VELOCITY
54+
}

nextcontrol/src/main/kotlin/dev/nextftc/nextcontrol/feedback/PIDElement.kt

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ package dev.nextftc.nextcontrol.feedback
2020

2121
import dev.nextftc.nextcontrol.ControlSystem
2222
import dev.nextftc.nextcontrol.KineticState
23-
import dev.nextftc.nextcontrol.TimeUtil
23+
import dev.nextftc.nextcontrol.utils.TimeUtil
2424
import kotlin.math.sign
2525

2626
/**
@@ -36,10 +36,6 @@ data class PIDCoefficients @JvmOverloads constructor(
3636
@JvmField var kD: Double = 0.0
3737
)
3838

39-
enum class PIDType {
40-
POSITION, VELOCITY
41-
}
42-
4339
/**
4440
* Traditional proportional-integral-derivative controller.
4541
*
@@ -107,22 +103,22 @@ internal class PIDController @JvmOverloads constructor(val coefficients: PIDCoef
107103
* @author Zach.Waffle, rowan-mcalpin
108104
*/
109105
class PIDElement @JvmOverloads constructor(
110-
private val pidType: PIDType,
106+
private val pidType: FeedbackType,
111107
coefficients: PIDCoefficients,
112108
resetIntegralOnZeroCrossover: Boolean = true
113109
) : FeedbackElement {
114110

115111
@JvmOverloads
116-
constructor(pidType: PIDType, kP: Double, kI: Double = 0.0, kD: Double = 0.0) :
112+
constructor(pidType: FeedbackType, kP: Double, kI: Double = 0.0, kD: Double = 0.0) :
117113
this(pidType, PIDCoefficients(kP, kI, kD))
118114

119115
private val controller = PIDController(coefficients, resetIntegralOnZeroCrossover)
120116

121117
val coefficients by controller::coefficients
122118

123119
fun calculate(timestamp: Long, error: KineticState) = when (pidType) {
124-
PIDType.POSITION -> controller.calculate(timestamp, error.position, error.velocity)
125-
PIDType.VELOCITY -> controller.calculate(timestamp, error.velocity, error.acceleration)
120+
FeedbackType.POSITION -> controller.calculate(timestamp, error.position, error.velocity)
121+
FeedbackType.VELOCITY -> controller.calculate(timestamp, error.velocity, error.acceleration)
126122
}
127123

128124
override fun calculate(error: KineticState) = calculate(TimeUtil.nanoTime(), error)

nextcontrol/src/main/kotlin/dev/nextftc/nextcontrol/feedback/SquIDElement.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ package dev.nextftc.nextcontrol.feedback
2020

2121
import dev.nextftc.nextcontrol.ControlSystem
2222
import dev.nextftc.nextcontrol.KineticState
23-
import dev.nextftc.nextcontrol.TimeUtil
23+
import dev.nextftc.nextcontrol.utils.TimeUtil
2424
import kotlin.math.abs
2525
import kotlin.math.sign
2626
import kotlin.math.sqrt
@@ -87,21 +87,21 @@ internal class SquIDController(val coefficients: PIDCoefficients) {
8787
* @author Zach.Waffle, rowan-mcalpin
8888
*/
8989
class SquIDElement(
90-
private val pidType: PIDType,
90+
private val pidType: FeedbackType,
9191
coefficients: PIDCoefficients
9292
) : FeedbackElement {
9393

9494
@JvmOverloads
95-
constructor(pidType: PIDType, kP: Double, kI: Double = 0.0, kD: Double = 0.0) :
95+
constructor(pidType: FeedbackType, kP: Double, kI: Double = 0.0, kD: Double = 0.0) :
9696
this(pidType, PIDCoefficients(kP, kI, kD))
9797

9898
private val controller = SquIDController(coefficients)
9999

100100
val coefficients by controller::coefficients
101101

102102
fun calculate(timestamp: Long, error: KineticState) = when (pidType) {
103-
PIDType.POSITION -> controller.calculate(timestamp, error.position, error.velocity)
104-
PIDType.VELOCITY -> controller.calculate(timestamp, error.velocity, error.acceleration)
103+
FeedbackType.POSITION -> controller.calculate(timestamp, error.position, error.velocity)
104+
FeedbackType.VELOCITY -> controller.calculate(timestamp, error.velocity, error.acceleration)
105105
}
106106

107107
override fun calculate(error: KineticState) = this.calculate(TimeUtil.nanoTime(), error)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* NextFTC: a user-friendly control library for FIRST Tech Challenge
3+
* Copyright (C) 2025 Rowan McAlpin
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
package dev.nextftc.nextcontrol.feedback
20+
21+
import dev.nextftc.nextcontrol.KineticState
22+
import junit.framework.TestCase.assertEquals
23+
import org.junit.Test
24+
25+
class BangBangElementTest {
26+
@Test
27+
fun `test bang bang controller on position`() {
28+
val element: BangBangElement = BangBangElement(FeedbackType.POSITION)
29+
30+
// When error is positive, output is positive
31+
var error = KineticState(100.0)
32+
assertEquals(1.0, element.calculate(error), 0.0)
33+
34+
// When error is 0, output is 0
35+
error = KineticState(0.0)
36+
assertEquals(0.0, element.calculate(error), 0.0)
37+
38+
// When error is negative, output is negative
39+
error = KineticState(-100.0)
40+
assertEquals(-1.0, element.calculate(error), 0.0)
41+
}
42+
43+
@Test
44+
fun `test bang bang controller on velocity`() {
45+
val element: BangBangElement = BangBangElement(FeedbackType.VELOCITY)
46+
47+
// When error is positive, output is positive
48+
var error = KineticState(0.0, 100.0)
49+
assertEquals(1.0, element.calculate(error), 0.0)
50+
51+
// When error is 0, output is 0
52+
error = KineticState(0.0, 0.0)
53+
assertEquals(0.0, element.calculate(error), 0.0)
54+
55+
// When error is negative, output is negative
56+
error = KineticState(0.0, -100.0)
57+
assertEquals(-1.0, element.calculate(error), 0.0)
58+
}
59+
}

nextcontrol/src/test/java/dev/nextftc/nextcontrol/feedback/PIDElementTest.kt

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class PIDElementTest {
3838
val expected = PIDCoefficients(kP, kI, kD)
3939

4040
// Act
41-
val pidElement = PIDElement(PIDType.POSITION, kP, kI, kD)
41+
val pidElement = PIDElement(FeedbackType.POSITION, kP, kI, kD)
4242
val actual = pidElement.coefficients
4343

4444
// Assert
@@ -49,8 +49,8 @@ class PIDElementTest {
4949
fun `returns zero when all gains are zero`() {
5050
// Arrange
5151
val coefficients = PIDCoefficients(0.0, 0.0, 0.0)
52-
val positionPID = PIDElement(PIDType.POSITION, coefficients)
53-
val velocityPID = PIDElement(PIDType.VELOCITY, coefficients)
52+
val positionPID = PIDElement(FeedbackType.POSITION, coefficients)
53+
val velocityPID = PIDElement(FeedbackType.VELOCITY, coefficients)
5454
val input = KineticState(10.0, 20.0, 30.0)
5555
val expected = 0.0
5656

@@ -67,8 +67,8 @@ class PIDElementTest {
6767
fun `returns zero when error has always been zero`() {
6868
// Arrange
6969
val coefficients = PIDCoefficients(1.0, 2.0, 3.0)
70-
val positionPID = PIDElement(PIDType.POSITION, coefficients)
71-
val velocityPID = PIDElement(PIDType.VELOCITY, coefficients)
70+
val positionPID = PIDElement(FeedbackType.POSITION, coefficients)
71+
val velocityPID = PIDElement(FeedbackType.VELOCITY, coefficients)
7272
val input = KineticState(0.0, 0.0, 0.0)
7373
val expected = 0.0
7474

@@ -88,8 +88,8 @@ class PIDElementTest {
8888
@Test
8989
fun `returns error when kP is one and kI and kD are zero`() {
9090
// Arrange
91-
val positionPID = PIDElement(PIDType.POSITION, 1.0, 0.0, 0.0)
92-
val velocityPID = PIDElement(PIDType.VELOCITY, 1.0, 0.0, 0.0)
91+
val positionPID = PIDElement(FeedbackType.POSITION, 1.0, 0.0, 0.0)
92+
val velocityPID = PIDElement(FeedbackType.VELOCITY, 1.0, 0.0, 0.0)
9393
val error = 10.0
9494
val positionInput = KineticState(error, 20.0, 30.0)
9595
val velocityInput = KineticState(20.0, error, 30.0)
@@ -107,8 +107,8 @@ class PIDElementTest {
107107
fun `returns sum of all past errors times deltaT when kI is one and kP and kD are zero`() {
108108
// Arrange
109109
val coefficients = PIDCoefficients(0.0, 1.0, 0.0)
110-
val positionPID = PIDElement(PIDType.POSITION, coefficients)
111-
val velocityPID = PIDElement(PIDType.VELOCITY, coefficients)
110+
val positionPID = PIDElement(FeedbackType.POSITION, coefficients)
111+
val velocityPID = PIDElement(FeedbackType.VELOCITY, coefficients)
112112

113113
mockkObject(TimeUtil)
114114

@@ -149,8 +149,8 @@ class PIDElementTest {
149149
fun `returns error of derivative when kD is one and kP and kI are zero`() {
150150
// Arrange
151151
val coefficients = PIDCoefficients(0.0, 0.0, 1.0)
152-
val positionPID = PIDElement(PIDType.POSITION, coefficients)
153-
val velocityPID = PIDElement(PIDType.VELOCITY, coefficients)
152+
val positionPID = PIDElement(FeedbackType.POSITION, coefficients)
153+
val velocityPID = PIDElement(FeedbackType.VELOCITY, coefficients)
154154
val derivativeError = 20.0
155155
val positionPIDError = KineticState(10.0, derivativeError, 30.0)
156156
val velocityPIDError = KineticState(10.0, 30.0, derivativeError)
@@ -172,8 +172,8 @@ class PIDElementTest {
172172
val kI = 6.0
173173
val kD = 7.0
174174
val coefficients = PIDCoefficients(kP, kI, kD)
175-
val positionPID = PIDElement(PIDType.POSITION, coefficients)
176-
val velocityPID = PIDElement(PIDType.VELOCITY, coefficients)
175+
val positionPID = PIDElement(FeedbackType.POSITION, coefficients)
176+
val velocityPID = PIDElement(FeedbackType.VELOCITY, coefficients)
177177

178178
mockkObject(TimeUtil)
179179

@@ -208,8 +208,8 @@ class PIDElementTest {
208208
fun `acceleration error does not affect position pid`() {
209209
// Arrange
210210
val coefficients = PIDCoefficients(1.0, 2.0, 3.0)
211-
val firstController = PIDElement(PIDType.POSITION, coefficients)
212-
val secondController = PIDElement(PIDType.POSITION, coefficients)
211+
val firstController = PIDElement(FeedbackType.POSITION, coefficients)
212+
val secondController = PIDElement(FeedbackType.POSITION, coefficients)
213213
val firstError = KineticState(10.0, 20.0, 30.0)
214214
val secondError = KineticState(10.0, 20.0, 40.0)
215215
mockkObject(TimeUtil)
@@ -229,8 +229,8 @@ class PIDElementTest {
229229
fun `position error does not affect velocity pid`() {
230230
// Arrange
231231
val coefficients = PIDCoefficients(1.0, 2.0, 3.0)
232-
val firstController = PIDElement(PIDType.VELOCITY, coefficients)
233-
val secondController = PIDElement(PIDType.VELOCITY, coefficients)
232+
val firstController = PIDElement(FeedbackType.VELOCITY, coefficients)
233+
val secondController = PIDElement(FeedbackType.VELOCITY, coefficients)
234234
val firstError = KineticState(10.0, 20.0, 30.0)
235235
val secondError = KineticState(15.0, 20.0, 30.0)
236236
mockkObject(TimeUtil)

0 commit comments

Comments
 (0)