1
1
package frc .robot ;
2
2
3
+ import static org .junit .jupiter .api .Assertions .assertDoesNotThrow ;
3
4
import static org .junit .jupiter .api .Assertions .assertEquals ;
4
5
import static org .junit .jupiter .api .Assertions .assertTrue ;
5
6
6
7
import org .carlmontrobotics .libdeepbluesim .WebotsSimulator ;
8
+ import org .carlmontrobotics .libdeepbluesim .WebotsSimulator .SimulationState ;
7
9
import org .junit .jupiter .api .RepeatedTest ;
8
- import org .junit .jupiter .api .Test ;
9
10
import org .junit .jupiter .api .Timeout ;
10
11
import org .junit .jupiter .api .parallel .ResourceLock ;
11
12
12
13
import java .lang .System .Logger ;
13
14
import java .lang .System .Logger .Level ;
15
+ import java .util .ArrayList ;
14
16
import java .util .concurrent .TimeUnit ;
17
+ import java .util .function .BiConsumer ;
18
+ import java .util .function .Consumer ;
15
19
16
20
import edu .wpi .first .math .MathUtil ;
17
21
import edu .wpi .first .math .system .plant .DCMotor ;
@@ -29,7 +33,7 @@ public class MotorControllerTest {
29
33
System .getLogger (MotorControllerTest .class .getName ());
30
34
31
35
// Should be the same as the basicTimeStep in the world's WorldInfo (default to 32ms)
32
- private double simStepSizeSecs = 0.032 ;
36
+ private static final double simStepSizeSecs = 0.032 ;
33
37
34
38
// Derivation of the math used below:
35
39
// Let tau be the torque applied by the motor (Nm).
@@ -87,8 +91,9 @@ public class MotorControllerTest {
87
91
// Rearranging gives:
88
92
// theta(t) = theta_0 + w_f*t + (w_0 - w_f) * t_c * (1-exp(-t/t_c))
89
93
90
- private double computeSpeedRadPerSec (DCMotor gearMotor , double moiKgM2 ,
91
- double throttle , double initialSpeedRadPerSec , double tSecs ) {
94
+ private static double computeSpeedRadPerSec (DCMotor gearMotor ,
95
+ double moiKgM2 , double throttle , double initialSpeedRadPerSec ,
96
+ double tSecs ) {
92
97
double targetSpeedRadPerSec = throttle * gearMotor .nominalVoltageVolts
93
98
* gearMotor .KvRadPerSecPerVolt ;
94
99
double timeConstantSecs = computeTimeConstantSecs (gearMotor , moiKgM2 );
@@ -98,8 +103,8 @@ private double computeSpeedRadPerSec(DCMotor gearMotor, double moiKgM2,
98
103
* Math .exp (-tSecs / timeConstantSecs );
99
104
}
100
105
101
- private double computeAngleRadians (DCMotor gearMotor , double moiKgM2 ,
102
- double throttle , double initialSpeedRadPerSec ,
106
+ private static double computeAngleRadians (DCMotor gearMotor , double moiKgM2 ,
107
+ double throttle , double initialSpeedRadPerSec ,
103
108
double initialAngleRad , double tSecs ) {
104
109
double timeConstantSecs = computeTimeConstantSecs (gearMotor , moiKgM2 );
105
110
double targetSpeedRadPerSec = throttle * gearMotor .nominalVoltageVolts
@@ -111,25 +116,26 @@ private double computeAngleRadians(DCMotor gearMotor, double moiKgM2,
111
116
* (1 - Math .exp (-tSecs / timeConstantSecs ));
112
117
}
113
118
114
- private double computeTimeConstantSecs (DCMotor gearMotor , double moiKgM2 ) {
119
+ private static double computeTimeConstantSecs (DCMotor gearMotor ,
120
+ double moiKgM2 ) {
115
121
// t_c = R*J*k_v/k_T
116
122
return gearMotor .rOhms * moiKgM2 * gearMotor .KvRadPerSecPerVolt
117
123
/ gearMotor .KtNMPerAmp ;
118
124
}
119
125
120
- private double computeCylinderMoiKgM2 (double radiusMeters ,
121
- double heightMeters , double densityKgPerM3 ) {
126
+ private static double computeCylinderMoiKgM2 (double radiusMeters ,
127
+ double heightMeters , double densityKgPerM3 ) {
122
128
double massKg = densityKgPerM3 * Math .PI * radiusMeters * radiusMeters
123
129
* heightMeters ;
124
130
return massKg * radiusMeters * radiusMeters / 2.0 ;
125
131
}
126
132
127
- private double timeOfNextStepSecs (double tSecs ) {
133
+ private static double timeOfNextStepSecs (double tSecs ) {
128
134
return Math .ceil (tSecs / simStepSizeSecs ) * simStepSizeSecs ;
129
135
}
130
136
131
- private double expectedSpeedRadPerSec (DCMotor gearMotor , double moiKgM2 ,
132
- double tSecs ) {
137
+ private static double expectedSpeedRadPerSec (DCMotor gearMotor ,
138
+ double moiKgM2 , double tSecs ) {
133
139
// The robot autonomous code runs the motor at 0.5 for 2 seconds and then stops it (i.e.
134
140
// sets the speed to 0 and lets it stop on it's own).
135
141
double tMotorStopsSecs = timeOfNextStepSecs (2.0 );
@@ -142,8 +148,8 @@ private double expectedSpeedRadPerSec(DCMotor gearMotor, double moiKgM2,
142
148
tSecs - tMotorStopsSecs );
143
149
}
144
150
145
- private double expectedAngleRadians (DCMotor gearMotor , double moiKgM2 ,
146
- double tSecs ) {
151
+ private static double expectedAngleRadians (DCMotor gearMotor ,
152
+ double moiKgM2 , double tSecs ) {
147
153
// The robot autonomous code runs the motor at 0.5 for 2 seconds and then stops it (i.e.
148
154
// sets the speed to 0 and lets it stop on it's own).
149
155
double tMotorStopsSecs = timeOfNextStepSecs (2.0 );
@@ -156,49 +162,13 @@ private double expectedAngleRadians(DCMotor gearMotor, double moiKgM2,
156
162
tSecs - tMotorStopsSecs );
157
163
}
158
164
159
- private void assertShaftCorrectAtSecs (DCMotor gearMotor , double moiKgM2 ,
160
- double actualSpeedRadPerSec , double actualAngleRadians ,
161
- double tSecs ) {
162
- // TODO: Make sim accurate enough that this can be less than 1*simStepSizeSecs
163
- // (https://github.com/DeepBlueRobotics/DeepBlueSim/issues/101)
164
- double jitterSecs = 5 * simStepSizeSecs ;
165
- double expectedEarlierSpeedRadPerSec =
166
- expectedSpeedRadPerSec (gearMotor , moiKgM2 , tSecs - jitterSecs );
167
- double expectedLaterSpeedRadPerSec =
168
- expectedSpeedRadPerSec (gearMotor , moiKgM2 , tSecs + jitterSecs );
169
- LOG .log (Level .DEBUG , "expectedEarlierSpeedRadPerSec = {0}" ,
170
- expectedEarlierSpeedRadPerSec );
171
- LOG .log (Level .DEBUG , "expectedLaterSpeedRadPerSec = {0}" ,
172
- expectedLaterSpeedRadPerSec );
173
- assertEquals (
174
- (expectedEarlierSpeedRadPerSec + expectedLaterSpeedRadPerSec )
175
- / 2 ,
176
- actualSpeedRadPerSec ,
177
- Math .abs (expectedEarlierSpeedRadPerSec
178
- - expectedLaterSpeedRadPerSec ) / 2 ,
179
- "Shaft not close enough to target angular velocity" );
180
- double expectedEarlierAngleRadians =
181
- expectedAngleRadians (gearMotor , moiKgM2 , tSecs - jitterSecs );
182
- double expectedLaterAngleRadians =
183
- expectedAngleRadians (gearMotor , moiKgM2 , tSecs + jitterSecs );
184
- double expectedAngleRadians =
185
- (expectedEarlierAngleRadians + expectedLaterAngleRadians ) / 2 ;
186
- double toleranceRadians = Math .abs (
187
- expectedEarlierAngleRadians - expectedLaterAngleRadians ) / 2 ;
188
- assertTrue (
189
- MathUtil .isNear (expectedAngleRadians , actualAngleRadians ,
190
- toleranceRadians , 0 , 2 * Math .PI ),
191
- "Shaft not close enough to target rotation. Expected %g +/- %g radians, but got %g radians."
192
- .formatted (expectedAngleRadians , toleranceRadians ,
193
- actualAngleRadians ));
194
- }
195
-
196
- private double computeGearing (DCMotor motor , double desiredFreeSpeedRPS ) {
165
+ private static double computeGearing (DCMotor motor ,
166
+ double desiredFreeSpeedRPS ) {
197
167
return motor .freeSpeedRadPerSec / (2 * Math .PI ) / desiredFreeSpeedRPS ;
198
168
}
199
169
200
- private double computeFlywheelThickness (DCMotor gearMotor ,
201
- double flywheelRadiusMeters , double flywheelDensityKgPerM3 ,
170
+ private static double computeFlywheelThickness (DCMotor gearMotor ,
171
+ double flywheelRadiusMeters , double flywheelDensityKgPerM3 ,
202
172
double desiredTimeConstantSecs ) {
203
173
// for a time constant of t_c seconds:
204
174
// t_c = R*J*k_v/k_T
@@ -216,99 +186,156 @@ private static record Measurement(double simTimeSec,
216
186
double speedRadPerSec ) {
217
187
}
218
188
219
- private Measurement m1 , m2 ;
189
+ private static class Model {
190
+ String shaftDefPath ;
191
+ String motorModelName ;
192
+ DCMotor gearMotor ;
193
+ double moiKgM2 ;
194
+ Measurement m1 , m2 ;
220
195
221
- // @Test
222
- @ RepeatedTest (1 )
223
- void testCANMotorRotationInAutonomous () throws Exception {
224
- assertCorrectRotationInAutonomous ("NEO" , "CAN_SHAFT" );
225
- }
196
+ Model (String motorModelName , String shaftDefPath ) throws Exception {
197
+ this .motorModelName = motorModelName ;
198
+ this .shaftDefPath = shaftDefPath ;
199
+ // To ensure we the flywheel doesn't spin or accelerate too fast...
200
+ var desiredFlywheelFreeSpeedRPS = 1.0 ;
201
+ var desiredTimeConstantSecs = 1.0 ;
202
+
203
+ // For this test to pass, the motor and flywheel need to be setup in the world as
204
+ // follows.
205
+ var flywheelRadiusMeters = 0.5 ;
206
+ var flywheelDensityKgPerM3 = 1000.0 ;
207
+ var motor = (DCMotor ) (DCMotor .class
208
+ .getDeclaredMethod ("get" + motorModelName , int .class )
209
+ .invoke (null , 1 ));
210
+ var gearing = computeGearing (motor , desiredFlywheelFreeSpeedRPS );
211
+ gearMotor = motor .withReduction (gearing );
212
+ LOG .log (Level .INFO ,
213
+ "For a free speed of {0} RPS when using a {1}, assuming the gearing is {2}.\n " ,
214
+ desiredFlywheelFreeSpeedRPS , motorModelName , gearing );
215
+ var flywheelThicknessMeters =
216
+ computeFlywheelThickness (gearMotor , flywheelRadiusMeters ,
217
+ flywheelDensityKgPerM3 , desiredTimeConstantSecs );
218
+ LOG .log (Level .INFO ,
219
+ "For a time constant of {0} second when using a {1} with a gearing of {2} and a flywheel with radius {3} meters and density {4} kg/m^3, assuming the flywheel thickness is {5} meters.\n " ,
220
+ desiredTimeConstantSecs , motorModelName , gearing ,
221
+ flywheelRadiusMeters , flywheelDensityKgPerM3 ,
222
+ flywheelThicknessMeters );
223
+
224
+ // With those settings, the flywheel MOI will be:
225
+ moiKgM2 = computeCylinderMoiKgM2 (flywheelRadiusMeters ,
226
+ flywheelThicknessMeters , flywheelDensityKgPerM3 );
227
+ }
226
228
227
- @ Test
228
- void testPWMMotorRotationInAutonomous () throws Exception {
229
- assertCorrectRotationInAutonomous ("MiniCIM" , "PWM_SHAFT" );
229
+ public String toString () {
230
+ return "Model(\" %s\" , \" %s\" )" .formatted (motorModelName ,
231
+ shaftDefPath );
232
+ }
233
+
234
+ private void assertShaftCorrectAtSecs (double actualSpeedRadPerSec ,
235
+ double actualAngleRadians , double tSecs ) {
236
+ // TODO: Make sim accurate enough that this can be less than 1*simStepSizeSecs
237
+ // (https://github.com/DeepBlueRobotics/DeepBlueSim/issues/101)
238
+ double jitterSecs = 2.5 * simStepSizeSecs ;
239
+ double expectedEarlierSpeedRadPerSec = expectedSpeedRadPerSec (
240
+ gearMotor , moiKgM2 , tSecs - jitterSecs );
241
+ double expectedLaterSpeedRadPerSec = expectedSpeedRadPerSec (
242
+ gearMotor , moiKgM2 , tSecs + jitterSecs );
243
+ LOG .log (Level .DEBUG , "expectedEarlierSpeedRadPerSec = {0}" ,
244
+ expectedEarlierSpeedRadPerSec );
245
+ LOG .log (Level .DEBUG , "expectedLaterSpeedRadPerSec = {0}" ,
246
+ expectedLaterSpeedRadPerSec );
247
+ assertEquals (
248
+ (expectedEarlierSpeedRadPerSec
249
+ + expectedLaterSpeedRadPerSec ) / 2 ,
250
+ actualSpeedRadPerSec ,
251
+ Math .abs (expectedEarlierSpeedRadPerSec
252
+ - expectedLaterSpeedRadPerSec ) / 2 ,
253
+ "Shaft not close enough to target angular velocity" );
254
+ double expectedEarlierAngleRadians = expectedAngleRadians (gearMotor ,
255
+ moiKgM2 , tSecs - jitterSecs );
256
+ double expectedLaterAngleRadians = expectedAngleRadians (gearMotor ,
257
+ moiKgM2 , tSecs + jitterSecs );
258
+ double expectedAngleRadians =
259
+ (expectedEarlierAngleRadians + expectedLaterAngleRadians )
260
+ / 2 ;
261
+ double toleranceRadians = Math .abs (
262
+ expectedEarlierAngleRadians - expectedLaterAngleRadians )
263
+ / 2 ;
264
+ assertTrue (
265
+ MathUtil .isNear (expectedAngleRadians , actualAngleRadians ,
266
+ toleranceRadians , 0 , 2 * Math .PI ),
267
+ "Shaft not close enough to target rotation. Expected %g +/- %g radians, but got %g radians."
268
+ .formatted (expectedAngleRadians , toleranceRadians ,
269
+ actualAngleRadians ));
270
+ }
271
+
272
+ private void assertCorrectTimeConstant (double throttle ) {
273
+ var targetSpeedRadPerSec = throttle * gearMotor .nominalVoltageVolts
274
+ * gearMotor .KvRadPerSecPerVolt ;
275
+ // Use the 2 measurements to measure the time constant.
276
+ // w(t) = w_f + (w_0 - w_f) * exp(-t/t_c)
277
+ // Let w'(t) = w(t) - w_f = (w_0 - w_f) * exp(-t/t_c)
278
+ // w'(t1) = (w_0 - w_f) * exp(-t1/t_c)
279
+ // w'(t2) = (w_0 - w_f) * exp(-t2/t_c)
280
+ // w'(t2)/w'(t1) = exp((t1-t2)/t_c)
281
+ // ln(w'(t2)/w'(t1)) = (t1-t2)/t_c
282
+ // t_c = (t1-t2)/ln(w'(t2)/w'(t1))
283
+ var actualTimeConstantSecs = (m1 .simTimeSec () - m2 .simTimeSec ())
284
+ / (Math .log ((m2 .speedRadPerSec () - targetSpeedRadPerSec )
285
+ / (m1 .speedRadPerSec () - targetSpeedRadPerSec )));
286
+ assertEquals (computeTimeConstantSecs (gearMotor , moiKgM2 ),
287
+ actualTimeConstantSecs , 0.02 , "Time constant incorrect" );
288
+ }
230
289
}
231
290
232
- void assertCorrectRotationInAutonomous (String motorModelName ,
233
- String shaftDefPath ) throws Exception {
234
- // To ensure we the flywheel doesn't spin or accelerate too fast...
235
- var desiredFlywheelFreeSpeedRPS = 1.0 ;
236
- var desiredTimeConstantSecs = 1.0 ;
291
+ private ArrayList <Model > models = new ArrayList <>();
237
292
238
- // For this test to pass, the motor and flywheel need to be setup in the world as follows.
239
- var flywheelRadiusMeters = 0.5 ;
240
- var flywheelDensityKgPerM3 = 1000.0 ;
241
- var motor = (DCMotor ) (DCMotor .class
242
- .getDeclaredMethod ("get" + motorModelName , int .class )
243
- .invoke (null , 1 ));
244
- var gearing = computeGearing (motor , desiredFlywheelFreeSpeedRPS );
245
- var gearMotor = motor .withReduction (gearing );
246
- LOG .log (Level .INFO ,
247
- "For a free speed of {0} RPS when using a {1}, assuming the gearing is {2}.\n " ,
248
- desiredFlywheelFreeSpeedRPS , motorModelName , gearing );
249
- var flywheelThicknessMeters =
250
- computeFlywheelThickness (gearMotor , flywheelRadiusMeters ,
251
- flywheelDensityKgPerM3 , desiredTimeConstantSecs );
252
- LOG .log (Level .INFO ,
253
- "For a time constant of {0} second when using a {1} with a gearing of {2} and a flywheel with radius {3} meters and density {4} kg/m^3, assuming the flywheel thickness is {5} meters.\n " ,
254
- desiredTimeConstantSecs , motorModelName , gearing ,
255
- flywheelRadiusMeters , flywheelDensityKgPerM3 ,
256
- flywheelThicknessMeters );
293
+ private Consumer <SimulationState > stateChecker (
294
+ BiConsumer <SimulationState , Model > checker ) {
295
+ return (s ) -> {
296
+ models .forEach ((m ) -> {
297
+ assertDoesNotThrow (() -> checker .accept (s , m ),
298
+ "Check failed for model %s" .formatted (m ));
299
+ });
300
+ };
301
+ }
257
302
258
- // With those settings, the flywheel MOI will be:
259
- var moiKgM2 = computeCylinderMoiKgM2 (flywheelRadiusMeters ,
260
- flywheelThicknessMeters , flywheelDensityKgPerM3 );
303
+ @ RepeatedTest (1 )
304
+ void testCorrectRotationInAutonomous () throws Exception {
305
+ models .add (new Model ("NEO" , "CAN_SHAFT" ));
306
+ models .add (new Model ("MiniCIM" , "PWM_SHAFT" ));
261
307
262
308
try (var manager = new WebotsSimulator (
263
309
"../plugin/controller/src/webotsFolder/dist/worlds/MotorController.wbt" ,
264
310
MotorControllerRobot ::new )) {
265
311
manager .atSec (0.0 , s -> {
266
312
s .enableAutonomous ();
267
- }).setMaxJitterSecs (0 ).everyStep (s -> {
313
+ }).setMaxJitterSecs (0 ).everyStep (stateChecker (( s , m ) -> {
268
314
LOG .log (Level .DEBUG ,
269
315
"robotTime = {0}, simTimeSec = {1}, speedRadPerSec = {2}" ,
270
316
s .getRobotTimeSec (), s .getSimTimeSec (),
271
- s .angularVelocity (shaftDefPath ).getAngle ());
272
- }).atSec (5 * simStepSizeSecs , s -> {
273
- m1 = new Measurement (s .getSimTimeSec (),
274
- s .angularVelocity (shaftDefPath ).getAngle ());
275
- }).atSec (1.0 , s -> {
276
- m2 = new Measurement (s .getSimTimeSec (),
277
- s .angularVelocity (shaftDefPath ).getAngle ());
278
- assertCorrectTimeConstant (gearMotor , moiKgM2 , 0.5 );
279
- assertShaftCorrectAtSecs (gearMotor , moiKgM2 ,
280
- s .angularVelocity (shaftDefPath ).getAngle (),
281
- s .rotation (shaftDefPath ).getZ (), s .getRobotTimeSec ());
282
- }).atSec (2.0 + 5 * simStepSizeSecs , s -> {
283
- m1 = new Measurement (s .getSimTimeSec (),
284
- s .angularVelocity (shaftDefPath ).getAngle ());
285
- }).atSec (3.0 , s -> {
286
- m2 = new Measurement (s .getSimTimeSec (),
287
- s .angularVelocity (shaftDefPath ).getAngle ());
288
- assertCorrectTimeConstant (gearMotor , moiKgM2 , 0.0 );
289
- assertShaftCorrectAtSecs (gearMotor , moiKgM2 ,
290
- s .angularVelocity (shaftDefPath ).getAngle (),
291
- s .rotation (shaftDefPath ).getZ (), s .getRobotTimeSec ());
292
- }).run ();
317
+ s .angularVelocity (m . shaftDefPath ).getAngle ());
318
+ })) .atSec (5 * simStepSizeSecs , stateChecker (( s , m ) -> {
319
+ m . m1 = new Measurement (s .getSimTimeSec (),
320
+ s .angularVelocity (m . shaftDefPath ).getAngle ());
321
+ })) .atSec (1.0 , stateChecker (( s , m ) -> {
322
+ m . m2 = new Measurement (s .getSimTimeSec (),
323
+ s .angularVelocity (m . shaftDefPath ).getAngle ());
324
+ m . assertCorrectTimeConstant (0.5 );
325
+ m . assertShaftCorrectAtSecs (
326
+ s .angularVelocity (m . shaftDefPath ).getAngle (),
327
+ s .rotation (m . shaftDefPath ).getZ (), s .getRobotTimeSec ());
328
+ })) .atSec (2.0 + 5 * simStepSizeSecs , stateChecker (( s , m ) -> {
329
+ m . m1 = new Measurement (s .getSimTimeSec (),
330
+ s .angularVelocity (m . shaftDefPath ).getAngle ());
331
+ })) .atSec (3.0 , stateChecker (( s , m ) -> {
332
+ m . m2 = new Measurement (s .getSimTimeSec (),
333
+ s .angularVelocity (m . shaftDefPath ).getAngle ());
334
+ m . assertCorrectTimeConstant (0.0 );
335
+ m . assertShaftCorrectAtSecs (
336
+ s .angularVelocity (m . shaftDefPath ).getAngle (),
337
+ s .rotation (m . shaftDefPath ).getZ (), s .getRobotTimeSec ());
338
+ })) .run ();
293
339
}
294
340
}
295
-
296
- private void assertCorrectTimeConstant (DCMotor gearMotor , double moiKgM2 ,
297
- double throttle ) {
298
- var targetSpeedRadPerSec = throttle * gearMotor .nominalVoltageVolts
299
- * gearMotor .KvRadPerSecPerVolt ;
300
- // Use the 2 measurements to measure the time constant.
301
- // w(t) = w_f + (w_0 - w_f) * exp(-t/t_c)
302
- // Let w'(t) = w(t) - w_f = (w_0 - w_f) * exp(-t/t_c)
303
- // w'(t1) = (w_0 - w_f) * exp(-t1/t_c)
304
- // w'(t2) = (w_0 - w_f) * exp(-t2/t_c)
305
- // w'(t2)/w'(t1) = exp((t1-t2)/t_c)
306
- // ln(w'(t2)/w'(t1)) = (t1-t2)/t_c
307
- // t_c = (t1-t2)/ln(w'(t2)/w'(t1))
308
- var actualTimeConstantSecs = (m1 .simTimeSec () - m2 .simTimeSec ())
309
- / (Math .log ((m2 .speedRadPerSec () - targetSpeedRadPerSec )
310
- / (m1 .speedRadPerSec () - targetSpeedRadPerSec )));
311
- assertEquals (computeTimeConstantSecs (gearMotor , moiKgM2 ),
312
- actualTimeConstantSecs , 0.02 , "Time constant incorrect" );
313
- }
314
341
}
0 commit comments