Austin Schuh | dc1c84a | 2013-02-23 16:33:10 -0800 | [diff] [blame] | 1 | #include <unistd.h> |
| 2 | |
| 3 | #include <memory> |
| 4 | |
| 5 | #include "gtest/gtest.h" |
| 6 | #include "aos/common/queue.h" |
| 7 | #include "aos/common/queue_testutils.h" |
| 8 | #include "frc971/control_loops/wrist_motor.q.h" |
| 9 | #include "frc971/control_loops/wrist.h" |
Austin Schuh | fa03369 | 2013-02-24 01:00:55 -0800 | [diff] [blame] | 10 | #include "frc971/constants.h" |
Austin Schuh | dc1c84a | 2013-02-23 16:33:10 -0800 | [diff] [blame] | 11 | |
| 12 | |
| 13 | using ::aos::time::Time; |
| 14 | |
| 15 | namespace frc971 { |
| 16 | namespace control_loops { |
| 17 | namespace testing { |
| 18 | |
Austin Schuh | fa03369 | 2013-02-24 01:00:55 -0800 | [diff] [blame] | 19 | |
| 20 | // Class which simulates the wrist and sends out queue messages containing the |
| 21 | // position. |
| 22 | class WristMotorSimulation { |
| 23 | public: |
| 24 | // Constructs a motor simulation. initial_position is the inital angle of the |
| 25 | // wrist, which will be treated as 0 by the encoder. |
| 26 | WristMotorSimulation(double initial_position) |
| 27 | : wrist_plant_(new StateFeedbackPlant<2, 1, 1>(MakeWristPlant())), |
| 28 | my_wrist_loop_(".frc971.control_loops.wrist", |
| 29 | 0x1a7b7094, ".frc971.control_loops.wrist.goal", |
| 30 | ".frc971.control_loops.wrist.position", |
| 31 | ".frc971.control_loops.wrist.output", |
| 32 | ".frc971.control_loops.wrist.status") { |
| 33 | Reinitialize(initial_position); |
| 34 | } |
| 35 | |
| 36 | // Resets the plant so that it starts at initial_position. |
| 37 | void Reinitialize(double initial_position) { |
| 38 | initial_position_ = initial_position; |
| 39 | wrist_plant_->X(0, 0) = initial_position_; |
| 40 | wrist_plant_->X(1, 0) = 0.0; |
| 41 | wrist_plant_->Y = wrist_plant_->C * wrist_plant_->X; |
| 42 | last_position_ = wrist_plant_->Y(0, 0); |
| 43 | calibration_value_ = 0.0; |
| 44 | } |
| 45 | |
| 46 | // Returns the absolute angle of the wrist. |
| 47 | double GetAbsolutePosition() const { |
| 48 | return wrist_plant_->Y(0, 0); |
| 49 | } |
| 50 | |
| 51 | // Returns the adjusted angle of the wrist. |
| 52 | double GetPosition() const { |
| 53 | return GetAbsolutePosition() - initial_position_; |
| 54 | } |
| 55 | |
| 56 | // Sends out the position queue messages. |
| 57 | void SendPositionMessage() { |
| 58 | const double angle = GetPosition(); |
| 59 | |
| 60 | double horizontal_hall_effect_start_angle; |
| 61 | ASSERT_TRUE(constants::horizontal_hall_effect_start_angle( |
| 62 | &horizontal_hall_effect_start_angle)); |
| 63 | double horizontal_hall_effect_stop_angle; |
| 64 | ASSERT_TRUE(constants::horizontal_hall_effect_stop_angle( |
| 65 | &horizontal_hall_effect_stop_angle)); |
| 66 | |
| 67 | ::aos::ScopedMessagePtr<control_loops::WristLoop::Position> position = |
| 68 | my_wrist_loop_.position.MakeMessage(); |
| 69 | position->pos = angle; |
| 70 | |
| 71 | // Signal that the hall effect sensor has been triggered if it is within |
| 72 | // the correct range. |
| 73 | double abs_position = GetAbsolutePosition(); |
| 74 | if (abs_position >= horizontal_hall_effect_start_angle && |
| 75 | abs_position <= horizontal_hall_effect_stop_angle) { |
| 76 | position->hall_effect = true; |
| 77 | } else { |
| 78 | position->hall_effect = false; |
| 79 | } |
| 80 | |
| 81 | // Only set calibration if it changed last cycle. Calibration starts out |
| 82 | // with a value of 0. |
| 83 | if ((last_position_ < horizontal_hall_effect_start_angle || |
| 84 | last_position_ > horizontal_hall_effect_stop_angle) && |
| 85 | position->hall_effect) { |
| 86 | calibration_value_ = |
| 87 | horizontal_hall_effect_start_angle - initial_position_; |
| 88 | } |
| 89 | |
| 90 | position->calibration = calibration_value_; |
| 91 | position.Send(); |
| 92 | } |
| 93 | |
| 94 | // Simulates the wrist moving for one timestep. |
| 95 | void Simulate() { |
| 96 | last_position_ = wrist_plant_->Y(0, 0); |
| 97 | EXPECT_TRUE(my_wrist_loop_.output.FetchLatest()); |
| 98 | wrist_plant_->U << my_wrist_loop_.output->voltage; |
| 99 | wrist_plant_->Update(); |
| 100 | |
| 101 | // Assert that we are in the right physical range. |
| 102 | double horizontal_upper_physical_limit; |
| 103 | ASSERT_TRUE(constants::horizontal_upper_physical_limit( |
| 104 | &horizontal_upper_physical_limit)); |
| 105 | double horizontal_lower_physical_limit; |
| 106 | ASSERT_TRUE(constants::horizontal_lower_physical_limit( |
| 107 | &horizontal_lower_physical_limit)); |
| 108 | |
| 109 | EXPECT_GE(horizontal_upper_physical_limit, wrist_plant_->Y(0, 0)); |
| 110 | EXPECT_LE(horizontal_lower_physical_limit, wrist_plant_->Y(0, 0)); |
| 111 | } |
| 112 | |
| 113 | ::std::unique_ptr<StateFeedbackPlant<2, 1, 1>> wrist_plant_; |
| 114 | private: |
| 115 | WristLoop my_wrist_loop_; |
| 116 | double initial_position_; |
| 117 | double last_position_; |
| 118 | double calibration_value_; |
| 119 | }; |
| 120 | |
Austin Schuh | dc1c84a | 2013-02-23 16:33:10 -0800 | [diff] [blame] | 121 | class WristTest : public ::testing::Test { |
| 122 | protected: |
| 123 | ::aos::common::testing::GlobalCoreInstance my_core; |
| 124 | |
| 125 | // Create a new instance of the test queue so that it invalidates the queue |
| 126 | // that it points to. Otherwise, we will have a pointer to shared memory that |
| 127 | // is no longer valid. |
| 128 | WristLoop my_wrist_loop_; |
| 129 | |
Austin Schuh | fa03369 | 2013-02-24 01:00:55 -0800 | [diff] [blame] | 130 | // Create a loop and simulation plant. |
Austin Schuh | dc1c84a | 2013-02-23 16:33:10 -0800 | [diff] [blame] | 131 | WristMotor wrist_motor_; |
Austin Schuh | fa03369 | 2013-02-24 01:00:55 -0800 | [diff] [blame] | 132 | WristMotorSimulation wrist_motor_plant_; |
Austin Schuh | dc1c84a | 2013-02-23 16:33:10 -0800 | [diff] [blame] | 133 | |
| 134 | WristTest() : my_wrist_loop_(".frc971.control_loops.wrist", |
| 135 | 0x1a7b7094, ".frc971.control_loops.wrist.goal", |
| 136 | ".frc971.control_loops.wrist.position", |
| 137 | ".frc971.control_loops.wrist.output", |
| 138 | ".frc971.control_loops.wrist.status"), |
| 139 | wrist_motor_(&my_wrist_loop_), |
Austin Schuh | fa03369 | 2013-02-24 01:00:55 -0800 | [diff] [blame] | 140 | wrist_motor_plant_(0.5) { |
| 141 | // Flush the robot state queue so we can use clean shared memory for this |
Austin Schuh | dc1c84a | 2013-02-23 16:33:10 -0800 | [diff] [blame] | 142 | // test. |
| 143 | ::aos::robot_state.Clear(); |
Austin Schuh | fa03369 | 2013-02-24 01:00:55 -0800 | [diff] [blame] | 144 | SendDSPacket(true); |
| 145 | } |
| 146 | |
| 147 | void SendDSPacket(bool enabled) { |
| 148 | ::aos::robot_state.MakeWithBuilder().enabled(enabled) |
Austin Schuh | dc1c84a | 2013-02-23 16:33:10 -0800 | [diff] [blame] | 149 | .autonomous(false) |
| 150 | .team_id(971).Send(); |
Austin Schuh | fa03369 | 2013-02-24 01:00:55 -0800 | [diff] [blame] | 151 | ::aos::robot_state.FetchLatest(); |
| 152 | } |
| 153 | |
| 154 | void VerifyNearGoal() { |
| 155 | my_wrist_loop_.goal.FetchLatest(); |
| 156 | my_wrist_loop_.position.FetchLatest(); |
| 157 | EXPECT_NEAR(my_wrist_loop_.goal->goal, |
| 158 | wrist_motor_plant_.GetAbsolutePosition(), |
| 159 | 1e-4); |
| 160 | } |
| 161 | |
| 162 | virtual ~WristTest() { |
| 163 | ::aos::robot_state.Clear(); |
Austin Schuh | dc1c84a | 2013-02-23 16:33:10 -0800 | [diff] [blame] | 164 | } |
| 165 | }; |
| 166 | |
Austin Schuh | fa03369 | 2013-02-24 01:00:55 -0800 | [diff] [blame] | 167 | // Tests that the wrist zeros correctly and goes to a position. |
| 168 | TEST_F(WristTest, ZerosCorrectly) { |
| 169 | my_wrist_loop_.goal.MakeWithBuilder().goal(0.1).Send(); |
| 170 | for (int i = 0; i < 400; ++i) { |
| 171 | wrist_motor_plant_.SendPositionMessage(); |
| 172 | wrist_motor_.Iterate(); |
| 173 | wrist_motor_plant_.Simulate(); |
| 174 | SendDSPacket(true); |
| 175 | } |
| 176 | VerifyNearGoal(); |
| 177 | } |
Austin Schuh | dc1c84a | 2013-02-23 16:33:10 -0800 | [diff] [blame] | 178 | |
Austin Schuh | fa03369 | 2013-02-24 01:00:55 -0800 | [diff] [blame] | 179 | // Tests that the wrist zeros correctly starting on the hall effect sensor. |
| 180 | TEST_F(WristTest, ZerosStartingOn) { |
| 181 | wrist_motor_plant_.Reinitialize(90 * M_PI / 180.0); |
| 182 | |
| 183 | my_wrist_loop_.goal.MakeWithBuilder().goal(0.1).Send(); |
| 184 | for (int i = 0; i < 500; ++i) { |
| 185 | wrist_motor_plant_.SendPositionMessage(); |
| 186 | wrist_motor_.Iterate(); |
| 187 | wrist_motor_plant_.Simulate(); |
| 188 | SendDSPacket(true); |
| 189 | } |
| 190 | VerifyNearGoal(); |
| 191 | } |
| 192 | |
| 193 | // Tests that missing positions are correctly handled. |
| 194 | TEST_F(WristTest, HandleMissingPosition) { |
| 195 | my_wrist_loop_.goal.MakeWithBuilder().goal(0.1).Send(); |
| 196 | for (int i = 0; i < 400; ++i) { |
| 197 | if (i % 23) { |
| 198 | wrist_motor_plant_.SendPositionMessage(); |
| 199 | } |
| 200 | wrist_motor_.Iterate(); |
| 201 | wrist_motor_plant_.Simulate(); |
| 202 | SendDSPacket(true); |
| 203 | } |
| 204 | VerifyNearGoal(); |
| 205 | } |
| 206 | |
| 207 | // Tests that loosing the encoder for a second triggers a re-zero. |
| 208 | TEST_F(WristTest, RezeroWithMissingPos) { |
| 209 | my_wrist_loop_.goal.MakeWithBuilder().goal(0.1).Send(); |
| 210 | for (int i = 0; i < 800; ++i) { |
| 211 | // After 3 seconds, simulate the encoder going missing. |
| 212 | // This should trigger a re-zero. To make sure it works, change the goal as |
| 213 | // well. |
| 214 | if (i < 300 || i > 400) { |
| 215 | wrist_motor_plant_.SendPositionMessage(); |
| 216 | } else { |
| 217 | if (i > 310) { |
| 218 | // Should be re-zeroing now. |
| 219 | EXPECT_EQ(WristMotor::UNINITIALIZED, wrist_motor_.state_); |
| 220 | } |
| 221 | my_wrist_loop_.goal.MakeWithBuilder().goal(0.2).Send(); |
| 222 | } |
| 223 | if (i == 430) { |
| 224 | EXPECT_EQ(WristMotor::ZEROING, wrist_motor_.state_); |
| 225 | } |
Austin Schuh | dc1c84a | 2013-02-23 16:33:10 -0800 | [diff] [blame] | 226 | |
| 227 | wrist_motor_.Iterate(); |
Austin Schuh | fa03369 | 2013-02-24 01:00:55 -0800 | [diff] [blame] | 228 | wrist_motor_plant_.Simulate(); |
| 229 | SendDSPacket(true); |
Austin Schuh | dc1c84a | 2013-02-23 16:33:10 -0800 | [diff] [blame] | 230 | } |
Austin Schuh | fa03369 | 2013-02-24 01:00:55 -0800 | [diff] [blame] | 231 | VerifyNearGoal(); |
| 232 | } |
| 233 | |
| 234 | // Tests that disabling while zeroing sends the state machine into the |
| 235 | // uninitialized state. |
| 236 | TEST_F(WristTest, DisableGoesUninitialized) { |
| 237 | my_wrist_loop_.goal.MakeWithBuilder().goal(0.1).Send(); |
| 238 | for (int i = 0; i < 800; ++i) { |
| 239 | wrist_motor_plant_.SendPositionMessage(); |
| 240 | // After 0.5 seconds, disable the robot. |
| 241 | if (i > 50 && i < 200) { |
| 242 | SendDSPacket(false); |
| 243 | if (i > 100) { |
| 244 | // Give the loop a couple cycled to get the message and then verify that |
| 245 | // it is in the correct state. |
| 246 | EXPECT_EQ(WristMotor::UNINITIALIZED, wrist_motor_.state_); |
Austin Schuh | 06ee48e | 2013-03-02 01:47:54 -0800 | [diff] [blame] | 247 | // When disabled, we should be applying 0 power. |
| 248 | EXPECT_TRUE(my_wrist_loop_.output.FetchLatest()); |
| 249 | EXPECT_EQ(0, my_wrist_loop_.output->voltage); |
Austin Schuh | fa03369 | 2013-02-24 01:00:55 -0800 | [diff] [blame] | 250 | } |
| 251 | } else { |
| 252 | SendDSPacket(true); |
| 253 | } |
| 254 | if (i == 202) { |
| 255 | // Verify that we are zeroing after the bot gets enabled again. |
| 256 | EXPECT_EQ(WristMotor::ZEROING, wrist_motor_.state_); |
| 257 | } |
| 258 | |
| 259 | wrist_motor_.Iterate(); |
| 260 | wrist_motor_plant_.Simulate(); |
| 261 | } |
| 262 | VerifyNearGoal(); |
Austin Schuh | dc1c84a | 2013-02-23 16:33:10 -0800 | [diff] [blame] | 263 | } |
| 264 | |
Austin Schuh | 06ee48e | 2013-03-02 01:47:54 -0800 | [diff] [blame] | 265 | // Tests that the wrist can't get too far away from the zeroing position if the |
| 266 | // zeroing position is saturating the goal. |
| 267 | TEST_F(WristTest, NoWindupNegative) { |
| 268 | double saved_zeroing_position = 0; |
| 269 | my_wrist_loop_.goal.MakeWithBuilder().goal(0.1).Send(); |
| 270 | for (int i = 0; i < 500; ++i) { |
| 271 | wrist_motor_plant_.SendPositionMessage(); |
| 272 | if (i == 50) { |
| 273 | EXPECT_EQ(WristMotor::ZEROING, wrist_motor_.state_); |
| 274 | // Move the zeroing position far away and verify that it gets moved back. |
| 275 | saved_zeroing_position = wrist_motor_.zeroing_position_; |
| 276 | wrist_motor_.zeroing_position_ = -100.0; |
| 277 | } else if (i == 51) { |
| 278 | EXPECT_EQ(WristMotor::ZEROING, wrist_motor_.state_); |
| 279 | EXPECT_NEAR(saved_zeroing_position, wrist_motor_.zeroing_position_, 0.4); |
| 280 | } |
| 281 | if (wrist_motor_.state_ != WristMotor::READY) { |
| 282 | if (i == 51) { |
| 283 | // The cycle after we kick the zero position is the only cycle during |
| 284 | // which we should expect to see a high uncapped power during zeroing. |
| 285 | EXPECT_LT(5, ::std::abs(wrist_motor_.loop_->U_uncapped(0, 0))); |
| 286 | } else { |
| 287 | EXPECT_GT(5, ::std::abs(wrist_motor_.loop_->U_uncapped(0, 0))); |
| 288 | } |
| 289 | } |
| 290 | |
| 291 | |
| 292 | wrist_motor_.Iterate(); |
| 293 | wrist_motor_plant_.Simulate(); |
| 294 | SendDSPacket(true); |
| 295 | } |
| 296 | VerifyNearGoal(); |
| 297 | } |
| 298 | |
| 299 | // Tests that the wrist can't get too far away from the zeroing position if the |
| 300 | // zeroing position is saturating the goal. |
| 301 | TEST_F(WristTest, NoWindupPositive) { |
| 302 | double saved_zeroing_position = 0; |
| 303 | my_wrist_loop_.goal.MakeWithBuilder().goal(0.1).Send(); |
| 304 | for (int i = 0; i < 500; ++i) { |
| 305 | wrist_motor_plant_.SendPositionMessage(); |
| 306 | if (i == 50) { |
| 307 | EXPECT_EQ(WristMotor::ZEROING, wrist_motor_.state_); |
| 308 | // Move the zeroing position far away and verify that it gets moved back. |
| 309 | saved_zeroing_position = wrist_motor_.zeroing_position_; |
| 310 | wrist_motor_.zeroing_position_ = 100.0; |
| 311 | } else { |
| 312 | if (i == 51) { |
| 313 | EXPECT_EQ(WristMotor::ZEROING, wrist_motor_.state_); |
| 314 | EXPECT_NEAR(saved_zeroing_position, wrist_motor_.zeroing_position_, 0.4); |
| 315 | } |
| 316 | } |
| 317 | if (wrist_motor_.state_ != WristMotor::READY) { |
| 318 | if (i == 51) { |
| 319 | // The cycle after we kick the zero position is the only cycle during |
| 320 | // which we should expect to see a high uncapped power during zeroing. |
| 321 | EXPECT_LT(5, ::std::abs(wrist_motor_.loop_->U_uncapped(0, 0))); |
| 322 | } else { |
| 323 | EXPECT_GT(5, ::std::abs(wrist_motor_.loop_->U_uncapped(0, 0))); |
| 324 | } |
| 325 | } |
| 326 | |
| 327 | wrist_motor_.Iterate(); |
| 328 | wrist_motor_plant_.Simulate(); |
| 329 | SendDSPacket(true); |
| 330 | } |
| 331 | VerifyNearGoal(); |
| 332 | } |
| 333 | |
Austin Schuh | dc1c84a | 2013-02-23 16:33:10 -0800 | [diff] [blame] | 334 | } // namespace testing |
| 335 | } // namespace control_loops |
| 336 | } // namespace frc971 |