Compare commits

...

10 Commits

47 changed files with 1766 additions and 1 deletions

View File

@ -0,0 +1,33 @@
# 10 - Basic Drivetrain
## Actual robot code! What does it do?
Well, to start off with we're going to look at getting the robot's drive base to move. This is by far the most common thing you'll have to deal with when developing robot code, and will be something you have to do every year.
Drive base code like what's in this example should be something you can do in your sleep, and if you're a rookie team or still a relatively new team, this is probably how your robot program is going to start out looking. There are other types of drive bases though that you may encounter or want to use.
## Wait, there's more than one type of drive base?
Yes, speaking very generally, at the time of writing this, there were three main types seen in the competition space. There are of course, exceptions to this, but you'll generally see the following:
- Differential Drive - These drivebases usually have some combination of 4, 6, or 8 wheels, and in the 6 or 8 wheel configurations, usually the center wheels will be slightly lower to the ground than the front and back wheels. This is probably the simplest robot drive base to build, as it uses simple, high traction wheels, chain or belts, and simple gearboxes to function. It's also probably the simplest to write code for. This is probably the drive base you'll be building if you're a rookie team.
- Mecanum Drive - These drivebases aren't super common anymore but they are still around. The use 4 wheels, one at each corner of the drivebase. The wheels themselves are special because they have rollers ([See Here](https://www.adafruit.com/product/4679?gclid=Cj0KCQiAwJWdBhCYARIsAJc4idBJFVX-rSFgrx250d4-8Viu1bxuqatre4KCdgVIoPXs6OSyRL9_On8aAlQXEALw_wcB)) with different spin orientations (left or right). These different spin orientations, when implemented properly, allows the robot to move laterally both forward/backward as well as left/right, while still maintaining the ability to rotate about the robot's center. Mecanum is somewhat more difficult to build, and to program, when compared to Differential Drive, but not so significantly that it's inaccessible to teams who've only been in the game a year or two.
- Swerve Drive - Swerve drive is complicated, but it's becoming very quickly a mainstay of the FIRST community. Essentially, at each corner of your robot you have a swerve module, the module has 2 motors, and an Encoder. One motor makes the wheel spin, as a wheel should, but the other motor, in tandem with the encoder, rotates the wheel so it can spin in 360 degrees. Because each module spins independent of the other. You get all the benefits of mechanum drive, without losing the pushing power of Differential Drive. Builting a swerve drive base is not for the faint of heart, it can be a complicated project, both mechanically and programmatically. Not recommended for beginners.
## So what do I need to get my drive base up and running in code?
Not much really. In robotInit(), you should be setting up a few things.
- Motor Controllers
- For Differential Drive, you'll usually have 2 motors in one gearbox on each side of the robot, so 4 motors total
- Motor Controller Groups
- You'll need one of these for each side of your drive base, a left side and a right side, the Motor Controller Groups help you to control both motors on a single side together.
- A Differential Drive
- You'll need just one of these, it'll take the Motor Controller Groups you make and manipulate them so your drive base acts as a single cohesive unit
- An XBox Controller (or similar control mechanism, like a Joystick)
- You'll need one of these to start, but probably two for competition.
Once all that is set up you just need one line of code in teleopPeriodic to make your robot move. There are two options for this:
- arcadeDrive(UpDown, LeftRight) - This is like what you might use if you are playing an arcade game, a single joystick on your controller moves the robot forward and back, left and right.
- tankDrive(LeftUpDown, RightUpDown) - This is like, well, controlling a tank, you use two joysticks, but only the forward and backward axis of each. Varying how much you push one or the other forward or back, you can make the robot drive forward, turn left or right, or pivot about its center.
You can find more detail in the comments of the code.
## Why do we "import ctre"?
Some things that we use in the example use the CTRE (Cross the Road Electronics) library. Specifically, WPI_VictorSPX, some motor controllers (among other features) can't be used without importing a separate module that contains the classes we need to build the objects we want to represent. Similar to how we import wpilib, and wpilib.drive, which contains most of the things we're using like XboxController, DifferentialDrive, MotorControllerGroup, etc, we need to import ctre to gain access to other elements that we want to use as well.
For much of the examples to come, you'll see the three imports you see now, and not much else, but be aware that there are others out there that you may need as you begin to develop your own robot programs.

View File

@ -0,0 +1,34 @@
import wpilib
import wpilib.drive
import ctre
class MyRobot(wpilib.TimedRobot):
def robotInit(self):
self.leftFront = ctre.WPI_VictorSPX(0)
self.leftRear = ctre.WPI_VictorSPX(1)
self.rightFront = ctre.WPI_VictorSPX(2)
self.rightRear = ctre.WPI_VictorSPX(3)
self.left = wpilib.MotorControllerGroup(self.leftFront, self.leftRear)
self.right = wpilib.MotorControllerGroup(self.rightFront, self.rightRear)
self.right.setInverted(True)
self.drivetrain = wpilib.drive.DifferentialDrive(self.left, self.right)
self.primary = wpilib.XboxController(0)
def autonomousInit(self):
pass
def autonomousPeriodic(self):
pass
def teleopInit(self):
pass
def teleopPeriodic(self):
self.drivetrain.arcadeDrive(self.primary.getLeftY(), self.primary.getLeftX())
if __name__ == "__main__":
wpilib.run(MyRobot)

View File

@ -0,0 +1,7 @@
# 11 - Timer Based Autonomous - Challenge
Create an autonomous that completes the following steps
- Forward for 2 Seconds
- Right for .5 Seconds
- Forward for 1 Second
- Left for .75 Seconds
- Backward for 2 Seconds

View File

@ -0,0 +1,48 @@
import wpilib
import wpilib.drive
import ctre
class MyRobot(wpilib.TimedRobot):
def robotInit(self):
self.leftFront = ctre.WPI_VictorSPX(0)
self.leftRear = ctre.WPI_VictorSPX(1)
self.rightFront = ctre.WPI_VictorSPX(2)
self.rightRear = ctre.WPI_VictorSPX(3)
self.left = wpilib.MotorControllerGroup(self.leftFront, self.leftRear)
self.right = wpilib.MotorControllerGroup(self.rightFront, self.rightRear)
self.right.setInverted(True)
self.drivetrain = wpilib.drive.DifferentialDrive(self.left, self.right)
self.primary = wpilib.XboxController(0)
self.autoTimer = wpilib.Timer()
def autonomousInit(self):
self.autoTimer.reset()
self.autoTimer.start()
def autonomousPeriodic(self):
if self.autoTimer.get() < 2:
self.drivetrain.tankDrive(.5, .5)
elif self.autoTimer.get() >= 2 and self.autoTimer.get() < 2.5:
self.drivetrain.tankDrive(.5, -.5)
elif self.autoTimer.get() >= 2.5 and self.autoTimer.get() < 3.5:
self.drivetrain.tankDrive(.5, .5)
elif self.autoTimer.get() >= 3.5 and self.autoTimer.get() < 4.25:
self.drivetrain.tankDrive(-.5, .5)
elif self.autoTimer.get() >= 4.25 and self.autoTimer.get() < 6.25:
self.drivetrain.tankDrive(-.5, -.5)
else:
self.drivetrain.tankDrive(0, 0)
def teleopInit(self):
self.autoTimer.stop()
def teleopPeriodic(self):
self.drivetrain.arcadeDrive(self.primary.getLeftY(), self.primary.getLeftX())
if __name__ == "__main__":
wpilib.run(MyRobot)

View File

@ -0,0 +1,20 @@
# 11 - Timer Based Autonomous
## Why should I care about making my robot do things on its own?
Because generally, the beginning 15 seconds of the match have a decent number of points up for grabs, and having an interesting and consistent autonomous will make your team more likely to be picked for playoffs, and open the door to a couple of different awards.
On top of that, autonomy is everything in industry, understanding how to use sensors and environmental data to make decisions in software is crucial in many different programming fields, not just robotics exclusively.
## So how do I make my robot do things on its own?
There's a lot of different options, but we're going to start off small and simple, and work our way up. Timers are a good place to start. Essentially, we base our autonomous code on the 15 second timeline we have to get work done, before we switch to teleoperated.
We can observe how much time has passed by asking the timer, and then use if statements to perform certain actions during different spans of time.
Maybe while the timer is less than 2 seconds, we drive forward, maybe when the timer is greater than or equal to 2 seconds, but less than three seconds, we turn left, and so on. We ask questions about how much time has passed, to determine what the next action should be.
Timers are very simple to implement, but they do have there drawbacks, but if you're looking for a quick autonomous to perform one or two simple actions, timers can work well.
## So wait, what are the drawbacks of timers?
There are really two big ones that will influence your robot's autonomous behavior.
- Environmental obstacles - Because you're not really using a sensor, just basing what you're doing on time, you can't react to changes in the environment that may influence how your robot works. Maybe the field carpet isn't completely flat in spots, maybe testing on cement and playing on carpet causes the robot to do different things, maybe there's another robot in the way. Timers can't really help you with this.
- Battery - Motors don't always behave exactly as you would expect them to. It depends on a lot of factors, but two of the bigger ones are the voltage and current available to the motor when it's time to do work. If the battery your using is older or newer, or more or less charged than what you were last testing with, your autonomous may behave differently because the motors aren't getting as far or going further because of differences in the amount of power being applied.

View File

@ -0,0 +1,46 @@
import wpilib
import wpilib.drive
import ctre
class MyRobot(wpilib.TimedRobot):
def robotInit(self):
self.leftFront = ctre.WPI_VictorSPX(0)
self.leftRear = ctre.WPI_VictorSPX(1)
self.rightFront = ctre.WPI_VictorSPX(2)
self.rightRear = ctre.WPI_VictorSPX(3)
self.left = wpilib.MotorControllerGroup(self.leftFront, self.leftRear)
self.right = wpilib.MotorControllerGroup(self.rightFront, self.rightRear)
self.right.setInverted(True)
self.drivetrain = wpilib.drive.DifferentialDrive(self.left, self.right)
self.primary = wpilib.XboxController(0)
self.autoTimer = wpilib.Timer()
def autonomousInit(self):
self.autoTimer.reset()
self.autoTimer.start()
def autonomousPeriodic(self):
if self.autoTimer.get() < 2:
self.drivetrain.tankDrive(.5, .5)
elif self.autoTimer.get() >= 2 and self.autoTimer.get() < 3:
self.drivetrain.tankDrive(.5, -.5)
elif self.autoTimer.get() >= 3 and self.autoTimer.get() < 4:
self.drivetrain.tankDrive(-.5, .5)
elif self.autoTimer.get() >= 4 and self.autoTimer.get() < 6:
self.drivetrain.tankDrive(-.5, -.5)
else:
self.drivetrain.tankDrive(0, 0)
def teleopInit(self):
self.autoTimer.stop()
def teleopPeriodic(self):
self.drivetrain.arcadeDrive(self.primary.getLeftY(), self.primary.getLeftX())
if __name__ == "__main__":
wpilib.run(MyRobot)

View File

@ -0,0 +1,16 @@
# 12 - Digital and Analog Inputs
## What is a Digital Input?
This is covered in slightly more detail in [8 - Basic Robot Electronics](../8%20-%20Basic%20Robot%20Electronics/), but to reiterate, something that is digital will only ever have two states, on or off, 1 or 0.
Digital Inputs for robots will often times manifest themselves as various forms of switches, like buttons, toggles, etc.
## What is an Analog Input?
Again, covered in slightly more detail in [8 - Basic Robot Electronics](../8%20-%20Basic%20Robot%20Electronics/), but to reiterate, something that is analog can have many different values. For robot programming, this range is from 0 to 1, with every decimal number inbetween a different value that the potentiometer is providing (note that this would be different for other types of Analog Inputs, for this example, just focus on the 0 to 1 range).
Analog Inputs can manifest themselves as a lot of different things. This example shows off an Analog Potentiometer, a Potentiometer being something that turns that produces different analog values the more or less you turn it. Analog potentiometers are used for things like volume knobs and temperature controls.
## How might I use these sorts of things for an Autonomous?
For a drive base exclusively, you probably wouldn't. Analog Potentiometers in particular wouldn't be very useful for just a drive base, but if you had an arm on your robot that you want to go to specific positions, you could use an analog potentiometer for that. You can use Digital Input buttons to detect if your robot has bumped into something, but in general this isn't super safe (for you or the button, depending on speed of travel).
The example I've provided is more to introduce you to some basic sensors before moving into the more complicated stuff. The basics of environmental interaction can be shown with this simple sensors without getting into too much complex code. Just remember that the use case you see here, isn't a common one.

View File

@ -0,0 +1,41 @@
import wpilib
import wpilib.drive
import ctre
class MyRobot(wpilib.TimedRobot):
def robotInit(self):
self.leftFront = ctre.WPI_VictorSPX(0)
self.leftRear = ctre.WPI_VictorSPX(1)
self.rightFront = ctre.WPI_VictorSPX(2)
self.rightRear = ctre.WPI_VictorSPX(3)
self.left = wpilib.MotorControllerGroup(self.leftFront, self.leftRear)
self.right = wpilib.MotorControllerGroup(self.rightFront, self.rightRear)
self.right.setInverted(True)
self.drivetrain = wpilib.drive.DifferentialDrive(self.left, self.right)
self.primary = wpilib.XboxController(0)
self.wallTouchButton = wpilib.DigitalInput(0)
self.autoSpeedControl = wpilib.AnalogPotentiometer(0)
def autonomousInit(self):
pass
def autonomousPeriodic(self):
if not self.wallTouchButton.get():
self.drivetrain.tankDrive(self.autoSpeedControl.get() / 2, self.autoSpeedControl.get() / 2)
else:
self.drivetrain.tankDrive(0, 0)
def teleopInit(self):
pass
def teleopPeriodic(self):
self.drivetrain.arcadeDrive(self.primary.getLeftY(), self.primary.getLeftX())
if __name__ == "__main__":
wpilib.run(MyRobot)

27
13 - Encoders/README.md Normal file
View File

@ -0,0 +1,27 @@
# 13 - Encoders
## Ok, this example looks a little more complicated than the last few...
That's because it is, we're getting further along now that you've seen more of the simple stuff, now we need to address more complicated topics.
## What is an Encoder?
This is covered in slightly more detail in [8 - Basic Robot Electronics](../8%20-%20Basic%20Robot%20Electronics/), but to reiterate, an Encoder is something that can be used to determine:
- The direction a motor/wheel is moving
- The speed at which a motor/wheel is moving
- The distance a motor/wheel has moved
These are all important tidbits of information if you want to go from saying "drive forward 2 seconds" to "drive forward 60 inches".
## How does the encoder know how fast or far it's gone?
Well, we sort of have to tell it. All encoders are, in some way, counting specific events. Hardware internal to the encoder usually "announces" (or pulses) when there is something to count, and then some software (either on the encoder itself or on the RoboRIO) is taking that count of pulses and determining speed over time and distance travelled.
All encoders will "announce" there's something to count a certain number of times within a single rotation of the encoder. We can use that information to determine how far, for instance a wheel attached to the same shaft as the encoder turns for one announcement or pulse of the encoder.
The way we do this is with a specific formula. `math.pi * Wheel Diameter / Pulses Per Revolution`. By doing this, we're basically dividing up the circumference of the wheel into slices that match the pulses (or announcements) the encoder will make in one complete rotation. This will make the information provided by methods (behaviors) like getDistance() and getRate() take on the units of whatever the wheel diameter is. If the wheel diameter provided is in inches, for example, then getDistance() will return inches and getRate will return inches/second
## This seems like a better solution than timers, what's the catch?
While it is a better solution than timers, the way we have this set up doesn't allow for turning, and isn't controlling the drivetrain in a way that will end up being super accurate.
Averaging both sides of the drivetrain together makes for some easy code. But our target is to drive straight 60 inches, what happens if something slows one side of the drivetrain or the other? Let's say that one side of the drivetrain goes 90 inches, and the other goes 30, that's a right turn, not driving straight, this code has no way to correct for that.
The other issue here, is lets say the drive base goes forward perfectly straight. What happens if we drive 65 inches because momentum carried us that far? 65 inches may not work for what we're trying to do. With this code, the robot can't correct for overshoot.
In future examples, we'll see how to solve the above problems.

47
13 - Encoders/robot.py Normal file
View File

@ -0,0 +1,47 @@
import wpilib
import wpilib.drive
import math
import ctre
class MyRobot(wpilib.TimedRobot):
def robotInit(self):
self.leftFront = ctre.WPI_VictorSPX(0)
self.leftRear = ctre.WPI_VictorSPX(1)
self.rightFront = ctre.WPI_VictorSPX(2)
self.rightRear = ctre.WPI_VictorSPX(3)
self.left = wpilib.MotorControllerGroup(self.leftFront, self.leftRear)
self.right = wpilib.MotorControllerGroup(self.rightFront, self.rightRear)
self.right.setInverted(True)
self.drivetrain = wpilib.drive.DifferentialDrive(self.left, self.right)
self.primary = wpilib.XboxController(0)
self.PULSES_PER_REVOLUTION = 512
self.WHEEL_DIAMETER = 6
self.DISTANCE_PER_PULSE = math.pi * self.WHEEL_DIAMETER / self.PULSES_PER_REVOLUTION
self.leftDrivetrainEncoder = wpilib.Encoder(0, 1)
self.leftDrivetrainEncoder.setDistancePerPulse(self.DISTANCE_PER_PULSE)
self.rightDrivetrainEncoder = wpilib.Encoder(2, 3)
self.rightDrivetrainEncoder.setDistancePerPulse(self.DISTANCE_PER_PULSE)
def autonomousInit(self):
self.leftDrivetrainEncoder.reset()
self.rightDrivetrainEncoder.reset()
def autonomousPeriodic(self):
if (self.leftDrivetrainEncoder.getDistance() + self.rightDrivetrainEncoder.getDistance()) / 2 < 60:
self.drivetrain.tankDrive(.5, .5)
else:
self.drivetrain.tankDrive(0, 0)
def teleopInit(self):
pass
def teleopPeriodic(self):
self.drivetrain.arcadeDrive(self.primary.getLeftY(), self.primary.getLeftX())
if __name__ == "__main__":
wpilib.run(MyRobot)

View File

@ -0,0 +1,6 @@
# 14 - Gyroscopes - Challenge
Combine the use of encoders and gyroscopes to do the following
- Drive forward 60 inches
- Rotate to 45 degrees (assume a left turn is negative rotation, right turn is positive rotation)
- Drive forward 20 inches
- Rotate to -90 degrees

View File

@ -0,0 +1,79 @@
import wpilib
import wpilib.drive
import math
import ctre
class MyRobot(wpilib.TimedRobot):
def robotInit(self):
self.leftFront = ctre.WPI_VictorSPX(0)
self.leftRear = ctre.WPI_VictorSPX(1)
self.rightFront = ctre.WPI_VictorSPX(2)
self.rightRear = ctre.WPI_VictorSPX(3)
self.left = wpilib.MotorControllerGroup(self.leftFront, self.leftRear)
self.right = wpilib.MotorControllerGroup(self.rightFront, self.rightRear)
self.right.setInverted(True)
self.drivetrain = wpilib.drive.DifferentialDrive(self.left, self.right)
self.primary = wpilib.XboxController(0)
self.PULSES_PER_REVOLUTION = 512
self.WHEEL_DIAMETER = 6
self.DISTANCE_PER_PULSE = math.pi * self.WHEEL_DIAMETER / self.PULSES_PER_REVOLUTION
self.leftDrivetrainEncoder = wpilib.Encoder(0, 1)
self.leftDrivetrainEncoder.setDistancePerPulse(self.DISTANCE_PER_PULSE)
self.rightDrivetrainEncoder = wpilib.Encoder(2, 3)
self.rightDrivetrainEncoder.setDistancePerPulse(self.DISTANCE_PER_PULSE)
self.gyro = wpilib.ADXRS450_Gyro()
self.gyro.calibrate()
def autonomousInit(self):
self.leftDrivetrainEncoder.reset()
self.rightDrivetrainEncoder.reset()
self.gyro.reset()
self.autoState = "DriveForward60"
def autonomousPeriodic(self):
if self.autoState == "DriveForward60":
if (self.leftDrivetrainEncoder.getDistance() + self.rightDrivetrainEncoder.getDistance()) / 2 < 60:
self.drivetrain.tankDrive(.5, .5)
else:
self.drivetrain.tankDrive(0, 0)
self.gyro.reset()
self.autoState = "Rotate45"
elif self.autoState == "Rotate45":
if self.gyro.getAngle() < 45:
self.drivetrain.tankDrive(.5, -.5)
else:
self.drivetrain.tankDrive(0, 0)
self.leftDrivetrainEncoder.reset()
self.rightDrivetrainEncoder.reset()
self.autoState = "DriveForward20"
elif self.autoState == "DriveForward20":
if (self.leftDrivetrainEncoder.getDistance() + self.rightDrivetrainEncoder.getDistance()) / 2 < 20:
self.drivetrain.tankDrive(.5, .5)
else:
self.drivetrain.tankDrive(0, 0)
self.gyro.reset()
self.autoState = "RotateNegative90"
elif self.autoState == "RotateNegative90":
if self.gyro.getAngle() > -90:
self.drivetrain.tankDrive(-.5, .5)
else:
self.drivetrain.tankDrive(0, 0)
self.autoState = "End"
else:
self.drivetrain.tankDrive(0, 0)
def teleopInit(self):
pass
def teleopPeriodic(self):
self.drivetrain.arcadeDrive(self.primary.getLeftY(), self.primary.getLeftX())
if __name__ == "__main__":
wpilib.run(MyRobot)

22
14 - Gyroscopes/README.md Normal file
View File

@ -0,0 +1,22 @@
# 14 - Gyroscopes
## What is a Gyroscope?
This is covered in slightly more detail in [8 - Basic Robot Electronics](../8%20-%20Basic%20Robot%20Electronics/), but to reiterate, a Gyroscope is a device that tells you the (near) instantaneous acceleration at which your robot is turning (often in 3 dimensions). Special software is used to calculate from the acceleration data the rate your turning, and the angle you've turned relative to a starting point (that you can control).
Gyroscopes can take you from saying "turn left for .75 seconds" to "turn to -45 degrees"
## How does a gyroscope know how fast or far it's gone?
It depends a lot on the type of gyroscope, but generally the ones you'll see in FRC use a trick of electrical capacitance (not capacity, two different things).
If you're really interested, you can [learn more about capacitive gyroscope sensing here](https://www.analog.com/en/technical-articles/mems-gyroscope-provides-precision-inertial-sensing.html). Be warned though, it's a dense read, and is far outside the scope of what you need to know if you actually just want to use the gyroscope.
## Another solution that seems better than timers, but I'm guessing there's a catch?
Yep. Definitely still a better solution than timers in many cases, however, gyroscopes alone can't help you determine the speed or distance you have traveled, just how much you've rotated.
Similar to Encoders, this example I've provided here also has the same overshoot problem. We'll see how to fix this next example, but just keep in mind that the way this example is written, you may ask for 45 degrees, and get 46, or 50, or maybe even 55 depending on the conditions around your robot.
Gyroscopes also have another issue, drift. Because of how the rotation is sensed, and the math involved to go from acceleration to rate and angle, a certain amount of drift (that is, a certain amount of rotation is preceived by the underlying code that isn't actually happening) is accumulated over time. It varies from sensor to sensor, even if there the same part from the same manufacturer.
Calling .calibrate() on your Gyro can help mitigate some of this, and for simple to moderately complex processes, this would be enough (and in general is going to be enough for most situations you might want to write code for). But some teams (and perhaps you in the future) will want to do something more complex that requires <i>very</i> accurate gyroscope readings. This is where filters come in. Filters help to wash away the noise while still allowing the actual activity to pass through appropriately.
Filters could be an example all there own, and in the Java training, we covered them very little, because setting up filters isn't usually necessary for beginner to intermediate work. If you're interested in reading more about filters, [refer to the WPILib documentation](https://docs.wpilib.org/en/stable/docs/software/advanced-controls/filters/index.html) for more information. They provide a significant amount of detailed information, as well as graphs made with sample data to give a rough idea about the behavior of each filter type.

40
14 - Gyroscopes/robot.py Normal file
View File

@ -0,0 +1,40 @@
import wpilib
import wpilib.drive
import math
import ctre
class MyRobot(wpilib.TimedRobot):
def robotInit(self):
self.leftFront = ctre.WPI_VictorSPX(0)
self.leftRear = ctre.WPI_VictorSPX(1)
self.rightFront = ctre.WPI_VictorSPX(2)
self.rightRear = ctre.WPI_VictorSPX(3)
self.left = wpilib.MotorControllerGroup(self.leftFront, self.leftRear)
self.right = wpilib.MotorControllerGroup(self.rightFront, self.rightRear)
self.right.setInverted(True)
self.drivetrain = wpilib.drive.DifferentialDrive(self.left, self.right)
self.primary = wpilib.XboxController(0)
self.gyro = wpilib.ADXRS450_Gyro()
self.gyro.calibrate()
def autonomousInit(self):
self.gyro.reset()
def autonomousPeriodic(self):
if self.gyro.getAngle() < 90:
self.drivetrain.tankDrive(.5, -.5)
else:
self.drivetrain.tankDrive(0, 0)
def teleopInit(self):
pass
def teleopPeriodic(self):
self.drivetrain.arcadeDrive(self.primary.getLeftY(), self.primary.getLeftX())
if __name__ == "__main__":
wpilib.run(MyRobot)

View File

@ -0,0 +1,10 @@
# 15 - PID Control - Challenge
Repeat the previous challenge from section 14 using PID Control
Combine the user of encoders and gyroscopes to do the following
- Drive forward 60 inches
- Rotate to 45 degrees (assume a left turn is negative rotation, right turn is positive rotation)
- Drive forward 20 inches
- Rotate to -90 degrees

View File

@ -0,0 +1,161 @@
import wpilib
import wpilib.drive
import wpimath.controller
import math
import ctre
def clamp(x, low, high):
if x < low:
return low
elif x > high:
return high
else:
return x
class MyRobot(wpilib.TimedRobot):
def robotInit(self):
self.leftFront = ctre.WPI_VictorSPX(0)
self.leftRear = ctre.WPI_VictorSPX(1)
self.rightFront = ctre.WPI_VictorSPX(2)
self.rightRear = ctre.WPI_VictorSPX(3)
self.left = wpilib.MotorControllerGroup(self.leftFront, self.leftRear)
self.right = wpilib.MotorControllerGroup(self.rightFront, self.rightRear)
self.right.setInverted(True)
self.drivetrain = wpilib.drive.DifferentialDrive(self.left, self.right)
self.primary = wpilib.XboxController(0)
self.PULSES_PER_REVOLUTION = 512
self.WHEEL_DIAMETER = 6
self.DISTANCE_PER_PULSE = math.pi * self.WHEEL_DIAMETER / self.PULSES_PER_REVOLUTION
self.leftDrivetrainEncoder = wpilib.Encoder(0, 1)
self.leftDrivetrainEncoder.setDistancePerPulse(self.DISTANCE_PER_PULSE)
self.rightDrivetrainEncoder = wpilib.Encoder(2, 3)
self.rightDrivetrainEncoder.setDistancePerPulse(self.DISTANCE_PER_PULSE)
self.gyro = wpilib.ADXRS450_Gyro()
self.gyro.calibrate()
self.ENCODER_PID_P = .1
self.ENCODER_PID_I = 0
self.ENCODER_PID_D = .01
self.ENCODER_PID_TOLERANCE = 1
self.AUTO_ENCODER_SETPOINT = 60
self.GYRO_PID_P = .1
self.GYRO_PID_I = 0
self.GYRO_PID_D = .01
self.GYRO_PID_TOLERANCE = 1
self.AUTO_GYRO_SETPOINT = 90
self.leftDrivetrainController = wpimath.controller.PIDController(
self.ENCODER_PID_P,
self.ENCODER_PID_I,
self.ENCODER_PID_D
)
self.leftDrivetrainController.setTolerance(self.ENCODER_PID_TOLERANCE)
self.leftDrivetrainController.setSetpoint(self.AUTO_ENCODER_SETPOINT)
self.rightDrivetrainController = wpimath.controller.PIDController(
self.ENCODER_PID_P,
self.ENCODER_PID_I,
self.ENCODER_PID_D
)
self.rightDrivetrainController.setTolerance(self.ENCODER_PID_TOLERANCE)
self.rightDrivetrainController.setSetpoint(self.AUTO_ENCODER_SETPOINT)
self.gyroDrivetrainController = wpimath.controller.PIDController(
self.GYRO_PID_P,
self.GYRO_PID_I,
self.GYRO_PID_D
)
self.gyroDrivetrainController.setTolerance(self.GYRO_PID_TOLERANCE)
self.gyroDrivetrainController.setSetpoint(self.AUTO_GYRO_SETPOINT)
def autonomousInit(self):
self.leftDrivetrainEncoder.reset()
self.rightDrivetrainEncoder.reset()
self.autoState = "DriveForward60"
def autonomousPeriodic(self):
if self.autoState == "DriveForward60":
if not self.leftDrivetrainController.atSetpoint() and not self.rightDrivetrainController.atSetpoint():
self.drivetrain.tankDrive(
clamp(
self.leftDrivetrainController.calculate(self.leftDrivetrainEncoder.getDistance(), 60),
-.5,
.5
),
clamp(
self.rightDrivetrainController.calculate(self.rightDrivetrainEncoder.getDistance(), 60),
-.5,
.5
)
)
else:
self.drivetrain.tankDrive(0, 0)
self.gyro.reset()
self.autoState = "Rotate45"
elif self.autoState == "Rotate45":
if not self.gyroDrivetrainController.atSetpoint():
speedValue = clamp(
self.gyroDrivetrainController.calculate(self.gyro.getAngle(), 45),
-.5,
.5
)
self.drivetrain.tankDrive(speedValue, -speedValue)
else:
self.drivetrain.tankDrive(0, 0)
self.leftDrivetrainEncoder.reset()
self.leftDrivetrainController.reset()
self.rightDrivetrainEncoder.reset()
self.rightDrivetrainController.reset()
self.autoState = "DriveForward20"
elif self.autoState == "DriveForward20":
if not self.leftDrivetrainController.atSetpoint() and not self.rightDrivetrainController.atSetpoint():
self.drivetrain.tankDrive(
clamp(
self.leftDrivetrainController.calculate(self.leftDrivetrainEncoder.getDistance(), 20),
-.5,
.5
),
clamp(
self.rightDrivetrainController.calculate(self.rightDrivetrainEncoder.getDistance(), 20),
-.5,
.5
)
)
else:
self.drivetrain.tankDrive(0, 0)
self.gyro.reset()
self.gyroDrivetrainController.reset()
self.autoState = "RotateNegative90"
elif self.autoState == "RotateNegative90":
if not self.gyroDrivetrainController.atSetpoint():
speedValue = clamp(
self.gyroDrivetrainController.calculate(self.gyro.getAngle(), -90),
-.5,
.5
)
self.drivetrain.tankDrive(speedValue, -speedValue)
else:
self.drivetrain.tankDrive(0, 0)
self.autoState = "End"
else:
self.drivetrain.tankDrive(0, 0)
def teleopInit(self):
pass
def teleopPeriodic(self):
self.drivetrain.arcadeDrive(self.primary.getLeftY(), self.primary.getLeftX())
if __name__ == "__main__":
wpilib.run(MyRobot)

View File

@ -0,0 +1,52 @@
# 15 - PID Control
This is going to be an intermediate level topic that is <i>not</i> required for you to have a functional robot for competition. It can be a very powerful solution for managing the interaction between a sensor and a system (like a drivetrain, arm, elevator, shooter, etc.) but it isn't necessarily required for those systems to work.
## What is PID?
PID stands for Proportional, Integral, and Derivative. These three "pieces" are combined together to create a form feedback control for a motor or collection of motors on your robot. Each piece plays a different role, and more often than not you won't see more than 2 of the pieces used at any given time in FRC. The explanation below of each of the pieces, and the additional components needed for functional PID control, is not going to be fully comprehensive, but I'll do what I can to ensure that I cover enough to get you exploring.
First thing to understand is the controller has to have an input. This input is defined by you. It could be the rate or the current angle from a Gyroscope, the rate or the current position of the robot as received from an Encoder, and so on.
The second thing to understand is the setpoint, this is the position or the speed you want the PID controller to help you achieve, the units of the setpoint come from the units of your input. Each component (the P, I, and D) plays a different role in getting to your desired state as represented by the setpoint. Note that each of the mathematical functions represented by the three letters are looking at the difference between the current setpoint, and the most recent input from the sensor into the controller.
The third thing to understand is the tolerance of your controller. PID controllers can be tuned to perform very accurate calculations but there is a limit to how close they can get you to being exactly on target. The tolerance is the amount of error you're ok with before the PID controller says it's at the desired setpoint and stops telling motors to do work. For example, if your setpoint is 60, and the toleraance is +/- 1, then the range of values that is considered "at setpoint" for the controller is 59-61. Tolerance is something you have to evaluate for yourself between the amount of time you want to spend tuning the PID versus how much error you're willing to have left over when the PID controller stops doing work.
The next 3 values will talk about are multipliers for the P, I, and D portions of the control loop. If the value for any of these is 0, then the result of that particularly mathematical process is ignored by the controller, and doesn't influence the overall output provided to motors.
P, or Proportional, is the amount of effort the controller puts in to close the gap between your desired setpoint, and your current input value. Sometimes, you can get away with just having a value for P, and the I and D values can be 0. You will basically always have a value for P.
I, or Integral, is a calculation performed to determine the area under a curve on a graph, but at a single point, rather than the range between two points. The I portion informs the controller of what you've been doing previously, and uses that information to influence what happens next. In FRC, this value is usually ignored, i.e. is 0.
D, or Derivative, is a calculation perform to determine the slope of a line, but at a single point, rather than the range between two points. The D portion informs the controller what might happen or need to happen next, and uses that information to influence the output of the controller. It's not uncommon to see teams have relatively small values for D. To help compensate for an issue called oscillation, which is where the P value is so large that it causes the system to constantly overshoot the setpoint, then overcompensate and overshoot in the opposite direction, overcompensate again and overshoot, and so on. Small amounts of D can suppress these osciallations fairly easily, with ample time spent on tuning, of course.
## Where can I use PID Control?
Lots of different places. PID Control has been available to FRC for a long time which is why I show it here in these examples, it's fairly versatile and can handle the work of a lot of systems. Drivetrains, elevators, arms, shooters, you name it, PID control has probably done it. You're implementation and you're willingness to spend time on finding values for P, I, and D and tuning those values is what will make PID control useful to you.
## How do I find values for P, I, D?
This is where things get tricky. It can be <i>very</i> hard to find values for P, I, and D depending on the system. I'll describe the manual way that can work for many systems, and then mention a more automated mechanism you can use and research on your own.
First, if you're manually tuning your PID controller, assume your robot is never going to drive at full speed when using PID control. Because of the manual effort in testing required to figure out the PID values, a safe speed is half speed in both directions, forward and back. In the example, you'll see that I've written a small function (method/behavior) called clamp. It takes in a value, plus a high/low range, if the value is higher than the high value, the high value is returned, if it's lower than the low value, then low is returned, if the value is between high and low, then the value itself is returned. This makes it so you can constrain your motors to a specific speed range and keep you and your robot safe (or at least safer) from out of control behavior.
For the tuning we'll be doing, you can ignore I (set it to 0), as I mentioned before, I is not often used in FRC and can be very tempermental to tune. Just leave it be.
While tuning, slowly begin increasing P (with I and D set to 0), adding small amounts each time. What constitutes a small amount varies from system to system, use your best judgement, but starting off between .01 and .1 is usually safe. If the system your tuning takes off and seems out of control, go smaller.
Overtime as you increase P, one of two things will happen, either your system will get to the setpoint you want, or it will begin to oscilate. If it gets to your setpoint, your done. Leave I and D at 0 and revel in your accomplishment. If it begins to oscilate, that's when you start adding small values to D. For many systems, adding .001 to .01 to start with is pretty safe. Continue to increase D until the oscillations stop and you're still able to achieve your setpoint. If you get to this point, you're done!
Sometimes increasing D can cause your system to constantly fall short of its setpoint, this is called steady state error. In this situation, you need to, very slowly, increase P, or decrease D, by very small increments, until the problem is solved. Depending on the scenario, you may need to increase P, or you may not. You should be able to go with either option, but make sure you write down the values that got you to the "steady state error" issue, so you can go back if your tinkering doesn't work out.
Now, I mentioned an automated mechanism. As of this moment, I wouldn't call this mechanism a full on replacement for manual tuning. But if you're willing to learn the software, you can get some <i>very</i> close values for P and D that may even work as well as you want them to immediately after running the program.
The software used for automating the calculation of the values is called SysID. It's not new software, but I wouldn't call it "mature" software either. It takes a bit of getting used to to get through it's sometimes errant quirkiness. I'm hoping that through the next several seasons it's ease of use will improve. But for the work that it does, the difficulties and learning curve can be overlooked.
The values that SysID calculates for P and D are estimates, based on SysID actually capturing real world data about the system you want to make a PID Controller for, and the parameters you provide for the calculation beyond the sampled data.
SysID will upload custom code to your robot based on the parameters you specify for what motor controllers to use, what sensors to observe, etc, and then have you run several routines on your robot to identify different characteristics about how the motors behave when certain amounts of voltage are applied. By observing these values, SysID can calculate P and D values that should work in most situations, even at full speed (although your mileage may vary).
SysID is more of an advanced topic, but you can learn more [in the WPILib documentation](https://docs.wpilib.org/en/stable/docs/software/pathplanning/system-identification/introduction.html), note that some portions of this part of the documentation are <i>very</i> dense.
## Is PID Control the only option for controlling motors from sensor input?
Absolutely not, there are several others available to you that are useful for various types of control already available in WPILib. They aren't covered here, because they lean more towards the advanced end of robot programming (at least most of them do). If you want to explore more, you can start here [in the WPILib documentation](https://docs.wpilib.org/en/stable/docs/software/advanced-controls/controllers/index.html).

126
15 - PID Control/robot.py Normal file
View File

@ -0,0 +1,126 @@
import wpilib
import wpilib.drive
import wpimath.controller
import math
import ctre
def clamp(x, low, high):
if x < low:
return low
elif x > high:
return high
else:
return x
class MyRobot(wpilib.TimedRobot):
def robotInit(self):
self.leftFront = ctre.WPI_VictorSPX(0)
self.leftRear = ctre.WPI_VictorSPX(1)
self.rightFront = ctre.WPI_VictorSPX(2)
self.rightRear = ctre.WPI_VictorSPX(3)
self.left = wpilib.MotorControllerGroup(self.leftFront, self.leftRear)
self.right = wpilib.MotorControllerGroup(self.rightFront, self.rightRear)
self.right.setInverted(True)
self.drivetrain = wpilib.drive.DifferentialDrive(self.left, self.right)
self.primary = wpilib.XboxController(0)
self.PULSES_PER_REVOLUTION = 512
self.WHEEL_DIAMETER = 6
self.DISTANCE_PER_PULSE = math.pi * self.WHEEL_DIAMETER / self.PULSES_PER_REVOLUTION
self.leftDrivetrainEncoder = wpilib.Encoder(0, 1)
self.leftDrivetrainEncoder.setDistancePerPulse(self.DISTANCE_PER_PULSE)
self.rightDrivetrainEncoder = wpilib.Encoder(2, 3)
self.rightDrivetrainEncoder.setDistancePerPulse(self.DISTANCE_PER_PULSE)
self.gyro = wpilib.ADXRS450_Gyro()
self.gyro.calibrate()
self.ENCODER_PID_P = .1
self.ENCODER_PID_I = 0
self.ENCODER_PID_D = .01
self.ENCODER_PID_TOLERANCE = 1
self.AUTO_ENCODER_SETPOINT = 60
self.GYRO_PID_P = .1
self.GYRO_PID_I = 0
self.GYRO_PID_D = .01
self.GYRO_PID_TOLERANCE = 1
self.AUTO_GYRO_SETPOINT = 90
self.leftDrivetrainController = wpimath.controller.PIDController(
self.ENCODER_PID_P,
self.ENCODER_PID_I,
self.ENCODER_PID_D
)
self.leftDrivetrainController.setTolerance(self.ENCODER_PID_TOLERANCE)
self.leftDrivetrainController.setSetpoint(self.AUTO_ENCODER_SETPOINT)
self.rightDrivetrainController = wpimath.controller.PIDController(
self.ENCODER_PID_P,
self.ENCODER_PID_I,
self.ENCODER_PID_D
)
self.rightDrivetrainController.setTolerance(self.ENCODER_PID_TOLERANCE)
self.rightDrivetrainController.setSetpoint(self.AUTO_ENCODER_SETPOINT)
self.gyroDrivetrainController = wpimath.controller.PIDController(
self.GYRO_PID_P,
self.GYRO_PID_I,
self.GYRO_PID_D
)
self.gyroDrivetrainController.setTolerance(self.GYRO_PID_TOLERANCE)
self.gyroDrivetrainController.setSetpoint(self.AUTO_GYRO_SETPOINT)
def autonomousInit(self):
self.leftDrivetrainEncoder.reset()
self.rightDrivetrainEncoder.reset()
self.autoState = "Encoder Step"
def autonomousPeriodic(self):
if self.autoState == "Encoder Step":
if not self.leftDrivetrainController.atSetpoint() and not self.rightDrivetrainController.atSetpoint():
self.drivetrain.tankDrive(
clamp(
self.leftDrivetrainController.calculate(self.leftDrivetrainEncoder.getDistance()),
-.5,
.5
),
clamp(
self.rightDrivetrainController.calculate(self.rightDrivetrainEncoder.getDistance()),
-.5,
.5
)
)
else:
self.drivetrain.tankDrive(0, 0)
self.gyro.reset()
self.autoState = "Gyro Step"
elif self.autoState == "Gyro Step":
if not self.gyroDrivetrainController.atSetpoint():
speedValue = clamp(
self.gyroDrivetrainController.calculate(self.gyro.getAngle()),
-.5,
.5
)
self.drivetrain(speedValue, -speedValue)
else:
self.drivetrain.tankDrive(0, 0)
self.autoState = "End Step"
else:
self.drivetrain.tankDrive(0, 0)
def teleopInit(self):
pass
def teleopPeriodic(self):
self.drivetrain.arcadeDrive(self.primary.getLeftY(), self.primary.getLeftX())
if __name__ == "__main__":
wpilib.run(MyRobot)

View File

@ -0,0 +1,116 @@
from enum import Enum
import networktables
class LEDModes(Enum):
PIPELINE_MODE = 0
FORCE_OFF = 1
FORCE_BLINK = 2
FORCE_ON = 3
class CameraModes(Enum):
VISION_PROCESSOR = 0
DRIVER_CAMERA = 1 # THIS OPTION DISABLES VISION PROCESSING!
class Pipelines(Enum):
PIPELINE_0 = 0
PIPELINE_1 = 1
PIPELINE_2 = 2
PIPELINE_3 = 3
PIPELINE_4 = 4
PIPELINE_5 = 5
PIPELINE_6 = 6
PIPELINE_7 = 7
PIPELINE_8 = 8
PIPELINE_9 = 9
class Limelight():
def __init__(self):
self.limelightTable = networktables.NetworkTablesInstance.getDefault().getTable("limelight")
def hasTargets(self):
return self.limelightTable.getEntry("tv").getDouble(0) == 1
def getHorizontalOffset(self):
return self.limelightTable.getEntry("tx").getDouble(0)
def getVerticalOffset(self):
return self.limelightTable.getEntry("ty").getDouble(0)
def getTargetArea(self):
return self.limelightTable.getEntry("ta").getDouble(0)
def setLEDMode(self, mode):
if not isinstance(mode, LEDModes):
raise TypeError("setLEDMode must be passed an LEDModes enumeration value")
self.__setLEDMode(mode.value)
def setCameraMode(self, mode):
if not isinstance(mode, CameraModes):
raise TypeError("setCameraMode must be passed a CameraModes enumeration value")
self.__setCameraMode(mode.value)
def setCurrentPipeline(self, pipeline):
if not isinstance(pipeline, Pipelines):
raise TypeError("setCurrentPipeline must be passed a Pipelines enumeration value")
self.__setCurrentPipeline(pipeline.value)
def getLEDMode(self):
for mode in (LEDModes):
if mode.value == self.__getLEDModeRAW():
return mode
return None
def getCameraMode(self):
for mode in (CameraModes):
if mode.value == self.__getCameraModeRAW():
return mode
return None
def getCurrentPipeline(self):
for mode in (Pipelines):
if mode.value == self.__getCurrentPipelineRAW():
return mode
return None
def __getLEDModeRAW(self):
return self.limelightTable.getEntry("ledMode").getDouble(0)
def __getCameraModeRAW(self):
return self.limelightTable.getEntry("camMode").getDouble(0)
def __getCurrentPipelineRAW(self):
return self.limelightTable.getEntry("getpipe").getDouble(0)
def __setLEDMode(self, mode):
if mode > 3:
mode = 3
elif mode < 0:
mode = 0
self.limelightTable.getEntry("ledMode").setDouble(mode)
def __setCameraMode(self, mode):
if mode > 1:
mode = 1
elif mode < 0:
mode = 0
self.limelightTable.getEntry("camMode").setDouble(mode)
def __setCurrentPipeline(self, pipeline):
if pipeline > 9:
pipeline = 9
elif pipeline < 0:
pipeline = 0
self.limelightTable.getEntry("pipeline").setDouble(pipeline)
# The initSendable portion of the original training has been removed
# from this example for simplicity, it may be restored at a later
# time

View File

@ -0,0 +1,38 @@
# 16 - NetworkTables and Limelight
As we progress through the next several examples, we're going to see some aspects of robot programming that aren't always necessary. Generally speaking most of the things we've covered so far with the exception of the structure and drivetrain code isn't always necessary. It depends a lot on what you're doing. But the next few examples are going to be slightly more "on the fringe" because you may find a reason to use these things, you may not. On top of that, several of the things we're going to talk about have budgetary requirements that your team may not necessarily be able to meet.
None the less, you should know what these things are, just in case you want to use them in the future.
## Ok, I'll start with the first thing in the example name, what's a NetworkTable?
NetworkTables is a software mechanism for sending data between systems over a network. Data is formed by a structure of key/value pairs, sort of like a dictionary, where you have a word (the key) and a definition (the value). These key/value pairs are organization into tables, the tables themselves are values associated with keys further up in the hierarchy.
It's basically a bunch of nested Excel spreadsheets, which is a terrible example, but it's the one I'm going with.
To use NetworkTables, you have to have a NetworkTables server, that NetworkTables clients can connect to. Thankfully, the work of creating a NetworkTables server is managed for you. When you run your robot code on the RoboRIO, a NetworkTables server is automatically created, and begins listening for connections from clients.
There are several different forms of clients in FRC. You could use the NetworkTables libraries to create your own client for accessing data or you could use built in tools like Outline Viewer, Shuffleboard, and the LabVIEW Dashboard to begin interacting with the NetworkTables data.
In this example, we'll be interacting with the NetworkTables libraries directly to create a class that manages interaction between our robot and a device called a Limelight.
## A second new thing, great, what's a Limelight?
I'm pretty sure I talk a little bit about the Limelight in [8 - Basic Robot Electronics](../8%20-%20Basic%20Robot%20Electronics/) but to recap, a Limelight is a camera with LEDs on it that you use for computer vision tracking. It's based on the ever popular Raspberry Pi small board computers and can be configured and used to provide your robot with real time object tracking for things like field goals and score-able game elements.
The Limelight takes a ton of the work required to create a workable vision tracking algorithm and gives you a simple web based user interface to configure specific settings to tune for what you want to track. <b>Many</b> teams rely on the Limelight over manual vision tracking because in the vast majority of cases it performs well enough for what a team wants to accomplish.
The Limelight communicates with the rest of your robot program over Networktables. The software connects to the NetworkTables server running on your RoboRIO and creates a table with a key called "limelight" (this is actually the default, but you can change the table name if you want). By interacting with this table (whether you're retrieving or setting values), you can manipulate the Limelight and retrive information about what it is tracking based on the configuration you set up using the web user interface prior to putting your robot on the competition field.
The Limelight has several important values you can observe to if it's looking at a target, and if it is, where the target is and roughly how large it is (compared to the overall size of the image). These elements are (by key name):
- tv - tv will return a decimal (float) with a value of either 0 or 1. It'll be 0 if there's no target currently in view, or 1 if there is
- tx - tx returns a decimal (float) with a value that is the number of degrees from the left/right center (by default) of the image the target it sees is. The value can be negative or positive up to about 30 degrees (depends on model).
- ty - ty returns a decimal (float) with a value that is the number of degrees from the up/down center (by default) of the image the target it sees is. This value can be negative or positive up to about 27 degrees (depends on model).
- ta - ta returns a percentage (float) with a value that is the area that the current target takes up in the image as a whole. If the target covers more than half the image, then it returns 50.
These values can be a pain to work with directly, especially if you have to work with them repeatedly throught different sections of your code. We are going to look at creating a Limelight class that we can use to interact with our Limelight with, and to give better names to the different elements we are working with.
The Limelight also has settings you can change from your robot code, you'll see explanations of these in the comments, more than likely you won't need to interact with them to get what you want out of the Limelight, but they are there for completeness.
## What else can I do with NetworkTables?
Interacting directly with NetworkTables? Not a lot at this point, you'd have to have some other goal in mind. Indirectly working with NetworkTables however, we'll see that in [18 - CameraServer and Shuffleboard](../18%20-%20CameraServer%20and%20Shuffleboard/)

View File

@ -0,0 +1,82 @@
import wpilib
import wpilib.drive
import wpimath.controller
import ctre
import Limelight
def clamp(x, low, high):
if x < low:
return low
elif x > high:
return high
else:
return x
class MyRobot(wpilib.TimedRobot):
def robotInit(self):
self.leftFront = ctre.WPI_VictorSPX(0)
self.leftRear = ctre.WPI_VictorSPX(1)
self.rightFront = ctre.WPI_VictorSPX(2)
self.rightRear = ctre.WPI_VictorSPX(3)
self.left = wpilib.MotorControllerGroup(self.leftFront, self.leftRear)
self.right = wpilib.MotorControllerGroup(self.rightFront, self.rightRear)
self.right.setInverted(True)
self.drivetrain = wpilib.drive.DifferentialDrive(self.left, self.right)
self.primary = wpilib.XboxController(0)
self.limelight = Limelight.Limelight()
self.PID_P = .1
self.PID_I = 0
self.PID_D = .01
self.PID_TOLERANCE = 1
self.pidController = wpimath.controller.PIDController(
self.PID_P,
self.PID_I,
self.PID_D
)
self.pidController.setTolerance(self.PID_TOLERANCE)
self.useLimelightTargeting = False
def autonomousInit(self):
pass
def autonomousPeriodic(self):
pass
def teleopInit(self):
pass
def teleopPeriodic(self):
if self.primary.getAButtonPressed():
self.useLimelightTargeting = True
self.pidController.reset()
elif self.primary.getAButtonReleased():
self.useLimelightTargeting = False
if self.useLimelightTargeting:
self.getOnTarget(.5)
else:
self.drivetrain.arcadeDrive(self.primary.getLeftY(), self.primary.getLeftX())
def getOnTarget(self, maxSpeed):
if self.limelight.hasTargets():
outputSpeed = clamp(
self.pidController.calculate(
self.limelight.getHorizontalOffset(), 0),
-maxSpeed,
maxSpeed
)
self.drivetrain.tankDrive(outputSpeed, -outputSpeed)
else:
self.drivetrain.tankDrive(maxSpeed, -maxSpeed)
if __name__ == "__main__":
wpilib.run(MyRobot)

18
17 - Pneumatics/README.md Normal file
View File

@ -0,0 +1,18 @@
# 17 - Pneumatics
## P What?
Pneumatics (new-matics) is working with air to have your robot perform various functions. These actions tend to be very linear in nature (up/down, in/out, etc.). Pneumatics provide robots with a consistent motion experience for relatively simple tasks where a motor would require some form of complicated sensing to complete the same task.
<b>Big disclaimer, all robots can be dangerous, but pneumatics tend to add an extra layer of possible danger, be wary of your pneumatics setup and always [refer to the documentation](https://www.firstinspires.org/sites/default/files/uploads/resource_library/frc/technical-resources/frc_pneumatics_manual.pdf) if you're unsure if something is set up correctly. DO NOT charge your system before verifying your pneumatics are setup correctly.</b>
## So how do I go about using Pneumatics?
You need a pneumatics setup first, the documentation in the above disclaimer can help you with that. This is a programming training after all. The parts you'll be most concerned with are:
- What type of cylinders are we using? (Single or Double Action)
- What type of control module are we using? (Pnuematics Control Module (PCM) from CTRE or Pneumatics Control Hub (PCH) from REV)
The type of cylinder is important because it will inform you what class to create an object from when writing your pneumatics oriented code. You may have a mix of single and double, and that's ok, you just need to make sure that you configure the right ports with the right objects.
Speaking of ports, the PCM and PCH devices are sub-boards that help the RoboRIO manage not only your cylinders and how they're directing air, but also your compressor and the amount of stored air you have in your tanks. These devices have a number of ports that can be used to control solenoids (either single or double solenoids, again, depending on the type of cylinder). The ports are numbered, similar to how they are on the RoboRIO and PDP/PDH, you should be able to spot them pretty easily. Note that a double solenoid, like the name implies, will take up two ports, rather than just one.
The PCM/PCH as I mentioned also play a role in managing your compressor and your tank stored air. If you're using the PCM from CTRE, you're limited to Digital Only pressure sensing, meaning that, a digital switch informs the PCM whether or not to turn the compressor on. This isn't a bad thing, but it can cause the PCM to turn the compressor on more often than it really needs to, which can waste battery power. If you've got a PCH from REV though, you can use Analog sensing, and get real time pressure data that will tell you (within a tolerance) the exact amount of air you have stored for use. This way, you can use your better programming judgement to determine when the compressor should come on. For example, if your working pressure (the pressure you actually send to mechanisms) is 60, and your desired standing pressure tank pressure is no less than 75. With digital, the compressor is going to turn on basically every time the storage tank pressure hits 112 PSI or so, whereas with Analog, you could tell the PCH to not turn the compressor on until you hit the 75 or less mark you care about, saving a bunch of start/stop compressor runs that really aren't necessary.

51
17 - Pneumatics/robot.py Normal file
View File

@ -0,0 +1,51 @@
import wpilib
import wpilib.drive
import ctre
class MyRobot(wpilib.TimedRobot):
def robotInit(self):
self.leftFront = ctre.WPI_VictorSPX(0)
self.leftRear = ctre.WPI_VictorSPX(1)
self.rightFront = ctre.WPI_VictorSPX(2)
self.rightRear = ctre.WPI_VictorSPX(3)
self.left = wpilib.MotorControllerGroup(self.leftFront, self.leftRear)
self.right = wpilib.MotorControllerGroup(self.rightFront, self.rightRear)
self.right.setInverted(True)
self.drivetrain = wpilib.drive.DifferentialDrive(self.left, self.right)
self.primary = wpilib.XboxController(0)
self.compressor = wpilib.Compressor(0, wpilib.PneumaticsModuleType.REVPH)
# self.compressor.enableDigital()
self.compressor.enableAnalog(75, 115)
self.singleActionMechanism = wpilib.Solenoid(0, wpilib.PneumaticsModuleType.REVPH, 0)
self.doubleActionMechanism = wpilib.DoubleSolenoid(0, wpilib.PneumaticsModuleType.REVPH, 1, 2)
def autonomousInit(self):
pass
def autonomousPeriodic(self):
pass
def teleopInit(self):
pass
def teleopPeriodic(self):
self.drivetrain.arcadeDrive(self.primary.getLeftY(), self.primary.getLeftX())
if self.primary.getAButtonPressed():
self.singleActionMechanism.set(not self.singleActionMechanism.get())
if self.primary.getBButtonPressed():
if self.doubleActionMechanism.get() == wpilib.DoubleSolenoid.Value.kForward:
self.doubleActionMechanism.set(wpilib.DoubleSolenoid.Value.kReverse)
elif self.doubleActionMechanism.get() == wpilib.DoubleSolenoid.Value.kReverse:
self.doubleActionMechanism.set(wpilib.DoubleSolenoid.Value.kForward)
else:
self.doubleActionMechanism.set(wpilib.DoubleSolenoid.Value.kReverse)
if __name__ == "__main__":
wpilib.run(MyRobot)

View File

@ -0,0 +1 @@
# 18 - CameraServer and Shuffleboard - Challenge

View File

@ -0,0 +1,10 @@
# 18 - CameraServer and Shuffleboard
# <b><i>THIS EXAMPLE IS BROKEN, PLEASE PROCEED TO THE NEXT SECTION AND COME BACK LATER TO VIEW THIS EXAMPLE</i></b>
Notes:
- Example will probably never work on Windows with the current state of the libraries, libraries will need to change in order to get to the point where the example will work on Windows
- The limitation has something to do with OpenCV, and manually compiling OpenCV in a specific way to get the Windows stuff to work
- The development team for RobotPy indicates in the documentation that Windows is only very lightly tested and may or may not work.
- Sooner or later, I will work on getting a Ubuntu 18.04 VM set up with the appropriate configuration to try and get this working.

View File

@ -0,0 +1,38 @@
import wpilib
import wpilib.drive
import wpilib.cameraserver
import ctre
# THIS EXAMPLE IS INCOMPLETE BECAUSE CSCORE SUPPORT ON WINDOWS IS
# NOT WELL TESTED AND IS VERY DIFFICULT TO GET WORKING, PLEASE IGNORE THIS EXAMPLE
# FOR THE TIME BEING
class MyRobot(wpilib.TimedRobot):
def robotInit(self):
self.leftFront = ctre.WPI_VictorSPX(0)
self.leftRear = ctre.WPI_VictorSPX(1)
self.rightFront = ctre.WPI_VictorSPX(2)
self.rightRear = ctre.WPI_VictorSPX(3)
self.left = wpilib.MotorControllerGroup(self.leftFront, self.leftRear)
self.right = wpilib.MotorControllerGroup(self.rightFront, self.rightRear)
self.right.setInverted(True)
self.drivetrain = wpilib.drive.DifferentialDrive(self.left, self.right)
self.primary = wpilib.XboxController(0)
def autonomousInit(self):
pass
def autonomousPeriodic(self):
pass
def teleopInit(self):
pass
def teleopPeriodic(self):
self.drivetrain.arcadeDrive(self.primary.getLeftY(), self.primary.getLeftX())
if __name__ == "__main__":
wpilib.run(MyRobot)

View File

@ -0,0 +1,5 @@
# 19 - AddressableLED - Challenge
Make your own LED animation, be creative, no wrong answers on this challenge, assuming your code runs as you describe in comments.
## A note from the editor
The animation provided as a solution to this challenge is something I wrote several years ago called "plasma", which is based on various other pieces of code I've found over the years to form this weird, liquid type effect. It's somewhat complicated, and I don't necessarily expect you to understand what it does and how, because even I'm not 100% sure about how someone came up with the functional structure to make the plasma (the function is from somewhere with psuedorandom tweaks from me). But after a few read-throughs with comments, you should be able to get the jist of what's going on.

View File

@ -0,0 +1,81 @@
import wpilib
import math
class MyRobot(wpilib.TimedRobot):
def robotInit(self):
self.width = 84
self.height = 56
self.buffer = wpilib.AddressableLED.LEDData(self.width * self.height)
self.leds = wpilib.AddressableLED(0)
self.leds.setLength(self.width * self.height)
self.leds.start()
self.hsvShift = 0
self.plasma = [[0] * self.width] * self.height
for i in range(self.width):
for j in range(self.height):
self.plasma[i][j] = int((128.0 + (128.0 * math.sin(i / 4.0)) + 128.0 + (128.0 * math.sin(j / 4.0))) / 2)
self.palette = [0] * 256
for i in range(len(self.palette)):
self.palette[i] = self.__hsvToColor(self.__map(i, 0, 255, 0, 359), 1, 1)
def robotPeriodic(self):
for i in range(self.width):
for j in range(self.height):
position = (self.plasma[i][j] + self.hsvShift) % 256
self.buffer.setLED((j * self.width) + i, self.palette[position])
self.hsvShift += 1
self.leds.setData(self.buffer)
def autonomousInit(self):
pass
def autonomousPeriodic(self):
pass
def teleopInit(self):
pass
def teleopPeriodic(self):
pass
def __map(self, x, inMin, inMax, outMin, outMax):
return int((x - inMin) * (outMax - outMin) / (inMax - inMin) + outMin)
def __hsvToColor(self, h, s, v):
if s == 0:
return wpilib.Color(wpilib.Color8Bit(int(v * 255.0), int(v * 255.0), int(v * 255.0)))
htemp = h / 60.0
i = int(math.floor(htemp))
f = htemp - i
p = v * (1.0 - s)
q = v * (1.0 - s * f)
t = v * (1.0 - s * (1.0 - f))
if i == 0:
return wpilib.Color(wpilib.Color8Bit(int(v * 255.0), int(t * 255.0), int(p * 255.0)))
elif i == 1:
return wpilib.Color(wpilib.Color8Bit(int(q * 255.0), int(v * 255.0), int(p * 255.0)))
elif i == 2:
return wpilib.Color(wpilib.Color8Bit(int(p * 255.0), int(v * 255.0), int(t * 255.0)))
elif i == 3:
return wpilib.Color(wpilib.Color8Bit(int(p * 255.0), int(q * 255.0), int(v * 255.0)))
elif i == 4:
return wpilib.Color(wpilib.Color8Bit(int(t * 255.0), int(p * 255.0), int(v * 255.0)))
elif i == 5:
return wpilib.Color(wpilib.Color8Bit(int(v * 255.0), int(p * 255.0), int(q * 255.0)))
else:
return wpilib.Color(0, 0, 0)
if __name__ == "__main__":
wpilib.run(MyRobot)

View File

@ -0,0 +1,15 @@
# 19 - AddressableLED
## Are you telling me I can make my robot RGB?
Yes, you can! (with some caveats).
LEDs in FRC should be used tastefully, the LEDs you use should either serve a purpose (like those on the Limelight camera) or be discrete enough that they aren't painful to look at while you're on the playing field.
You're limited to using LEDs from the WS2812 family or similar (most consumers are familar with this type of LED under the name Adafruit NeoPixel). You CANNOT use APA102s (again, more commonly known as Adafruit DotStars) with the libraries shown in this example. With that being said, theoretically, you could use APA102s with the RoboRIO, but it would require extra development effort on your part, again, at least at the time of writing.
You do have a limit on the number of LEDs you can run, it's something like 5700 LEDs though, so that shouldn't be a problem. However, you can only use one continuous strip of LEDs on the RoboRIO at a time, if you want to have separate strips on your robot doing different things, you'll need to make long wire runs and separate the different sections of the singular strip logically, rather than physically. This is easy enough to do, but will require more than just beginner level programming experience if you want to get too complicated.
## When can I start installing the LEDs? Moar RGB moar better.
If your LEDs are going to serve a purpose, like indicating when a shooter wheel is fully spun up or provide extra light for vision tracking, then set those up at the same time you're implementing code for that system. Your drive team will need the opportunity to get used to using the LEDs as reference.
If you're just using LEDs for looks, like underglow or flame animation, it should wait towards the end (if not the very end) of robot construction. LEDs for looks should be an after thought that comes later, and shouldn't ever obstruct getting work done on real mechanisms that are designed to complete game objectives.

View File

@ -0,0 +1,71 @@
import wpilib
import wpilib.drive
import ctre
class MyRobot(wpilib.TimedRobot):
def robotInit(self):
self.leftFront = ctre.WPI_VictorSPX(0)
self.leftRear = ctre.WPI_VictorSPX(1)
self.rightFront = ctre.WPI_VictorSPX(2)
self.rightRear = ctre.WPI_VictorSPX(3)
self.left = wpilib.MotorControllerGroup(self.leftFront, self.leftRear)
self.right = wpilib.MotorControllerGroup(self.rightFront, self.rightRear)
self.right.setInverted(True)
self.drivetrain = wpilib.drive.DifferentialDrive(self.left, self.right)
self.primary = wpilib.XboxController(0)
self.leds = wpilib.AddressableLED(0)
self.leds.setLength(20)
self.buffer = wpilib.AddressableLED.LEDData(20)
self.leds.setData(self.buffer)
self.leds.start()
self.rainbowTimer = wpilib.Timer()
self.rainbowTimer.start()
self.rainbowColorSelection = 0
self.hsvColorSelection = 0
self.RAINBOW = [
wpilib.Color.kRed,
wpilib.Color.kOrange,
wpilib.Color.kYellow,
wpilib.Color.kGreen,
wpilib.Color.kBlue,
wpilib.Color.kPurple]
def robotPeriodic(self):
if self.rainbowTimer.get() > 1:
for i in range(10):
self.buffer.setLED(i, self.RAINBOW[self.rainbowColorSelection % len(self.rainbowColorSelection)])
self.rainbowColorSelection += 1
self.rainbowTimer.reset()
for i in range(10, 20):
self.buffer.setHSV(i, self.hsvColorSelection % 180, 255, 255)
self.hsvColorSelection += 1
self.leds.setData(self.buffer)
def autonomousInit(self):
pass
def autonomousPeriodic(self):
pass
def teleopInit(self):
pass
def teleopPeriodic(self):
self.drivetrain.arcadeDrive(self.primary.getLeftY(), self.primary.getLeftX())
if __name__ == "__main__":
wpilib.run(MyRobot)

View File

@ -1 +1,2 @@
# 2 - Console In and Out - Challenge
Build a program that requests a user's name, and then prints a greeting. Build a program that requests a user's name, and then prints a greeting.

View File

@ -0,0 +1,9 @@
# 2 - Console In and Out
## What is the console?
The answer to this question is "it's a place to send and receive text to and from your program". For any programs that you run from the command line (Command Prompt in Windows, Terminal in most Linux Distributions), the console is the window you see. This window can produce text from your program (Console/Standard Out) or can accept text from the keyboard (Console/Standard In). All programs technically, in some way shape or form, have a console, even if it's not run directly from Command Prompt/Terminal, but we often don't see or interact with it, because we spend most of our time interacting with our programs visually, rather than with text.
## Why is this relevant to Robot Programming, am I going to be typing commands to my robot?
The console out piece is important to robot programming because you can use it as a mechanism to identify where your robot program "is" when it's running. You can use print() to send messages to the Driver's Station computer that you'll be able to see and use to identify issues. For example, you could place several print() statements throughout your code to identify when a "step" completes, when your robot code crashes, you can just look for which print() statement recently displayed in the console, and know roughly where in your code there may be an issue.
You don't really use Console In in Robot Programming, but they go hand in hand, and its good to know how to do both if you want to continue to learn to program in Python.

View File

@ -1 +1,2 @@
# 3 - Variables and Basic Math - Challenge
Build a program that requests three numbers from the user, and performs a math operation on them and prints the result Build a program that requests three numbers from the user, and performs a math operation on them and prints the result

View File

@ -0,0 +1,48 @@
# 3 - Variables and Basic Math
## Are you teaching me Algebra now?
No not really. In Algebra, you might be used to single letter variables, maybe some special characters if you've taken High School Physics already, and you're often trying to solve for one of these hidden variable values, or maybe you have a mix of known and unknown values. Python (and other programming languages) use variables to store information we want to reference in our program at a later time, the variables you'll see in programming are more dynamic that what you might have to deal with in Algebra.
## What can variables store?
This is kind of a loaded question, the answer is a lot of things. For now, for this lesson, we're going to constrain ourselves to just talking about 4 types of variables, they are:
- Strings - This is text, usually represented by surrounding the text in double quotes
- ints (Integers) - These are whole numbers, both negative and positive and 0. You CAN'T have a decimal in an int value
- Booleans (True/False) - These are on/off, true/false values, you'll see booleans used when your code needs to make choices about what to do next
- Floats (Decimals) - These are real numbers, both negative and positive and 0. While there are limitations to what you can have for numbers here, decimal numbers are allowed and exceptionally small fractions are allowed, much smaller than what you'll need to deal with Robot Programming
## How do I create a variable?
In the early examples, creating a variable is as easy as the following
\<Name\> = \<Value\>
Example: fruit = "Apple"
Example: teamNumber = 2648
Example: zuchinniNasty = True
Example: radius = 1.423563
When you want to use a variable in your program, simply type its name
print(fruit)
print("Team Number: " + str(teamNumber))
## How do I know if a variable is a particular type?
For the most part, you don't. Python does it's best to automatically assign variables the appropriate type in the background, in the hopes that you won't have to think about it. A lot of other programming languages don't do this, as it can cause problems when trying to do certain things.
If you are working on a part of your program and you want a value to be a certain way, most of the 4 types we talked about (with the exception of boolean) have a easy way of trying to convert on type to another
str(value) - Will try to convert value to a string
int(value) - Will try to convert value to an integer
float(value) - Will try to convert value to a decimal
Note that, if you try to convert something ridiculous like int("Banana"), you're going to get an error, because it really doesn't make sense to take some text, and turn it into a number.
## Is math any different than what I'm used to?
For the most part no, you have addition, subtraction, multiplication, and division. You also have another operation, modulous, or remainder, this does a division operation, but rather than return the number of times the second number goes into the first, it returns the amount left over that couldn't be accounted for.
(Assume we're working with only integers here)
Example: 5 / 3 = 1, 5 % 3 = 2
Example 8 / 4 = 2, 8 % 4 = 0
Remainder only works on integers, floats should (but may not always) return an error.
## What about power, cosine, sine, tangent, etc?
Those do exist! But they're a bit further into math than we need to get right now. If you want to know more, Google "Python math module". Basically, all of those things exist, but as an extra "component" you need to "import" into your code in order for them to become avaialble to use. There will be more information about importing modules later, as it becomes more important the more complicated the programs you want to create become.

View File

@ -1,3 +1,4 @@
# 4 - If/Else and Logical Operators - Challenge
Build a program that requests the user's age, and return one of the following: Build a program that requests the user's age, and return one of the following:
- <= 18 - You're a youngin' - <= 18 - You're a youngin'
- > 18 AND <= 35 - The early adult years - > 18 AND <= 35 - The early adult years

View File

@ -0,0 +1,147 @@
# 4 - If/Else and Logical Operators
## What's this if/else nonsense?
Sometimes, our programs need to make decisions, we need to do one thing if some condition is true, and something else if it's not. We use if/else statements to ask questions in our program so we can make decisions about what should happen. The format of an if statement starts off simple.
```
if (some condition):
(DO STUFF IF CONDITION IS TRUE)
else:
(DO STUFF IF CONDITION IS FALSE)
```
We can check different things to make different choices, neat. But what do we do if we need to ask a bunch of related questions in our program? That's were elif comes in.
```
if (some condition):
(DO STUFF IF CONDITION IS TRUE)
elif (some other condition):
(DO STUFF IF SOME OTHER CONDITION IS TRUE)
else:
(DO STUFF IF ALL CONDITIONS (QUESTIONS) ARE FALSE)
```
elif allows use to specify another condition and some code to go with it to add another question to ask if the questions that came before it were false. This gives us more options to determine what we're going to do next.
## Do I need to use if/elif/else all the time?
The smallest form of if is this
```
if (some condition):
(DO STUFF IF CONDITION IS TRUE)
```
You don't need else, you don't need elif, unless you want them and it makes sense for what you're trying to do in your program
## Does the indentation matter?
YES! Python is ULTRA PICKY about indentation. In general, when you see a colon (:) in Python, that means that some portion of the code underneath it should be indented one tab more than the current level of indentation. Here's a very bland example (ignore the for loop for the moment, we'll talk about that next lesson)
```
for x in range(10):
if x == 5 or x == 7:
print("Potato")
elif x == 2:
print("Grape")
else:
print("You get nothing")
```
The identation determines something called scope. Scope is essentially defining blocks of code that "live in the same space". Scope can be a complicated topic, so we're going to gloss over it. [W3Schools](https://www.w3schools.com/python/python_scope.asp) does a really good job explaining scope relatively quickly. You can even try the code for yourself right on the webpage!
## So how do I write a condition?
Depends on what you're trying to do. There's lots of different options, but the most common should be relatively familiar to you
- == - Is something equal to something else
- This is not a typo, it has to be two equals signs to check if two values are the same or Python gets confused
- != - Is something not equal to something else
- < - Is the left value less than the right value
- <= - Is the left value less than or equal to the right value
- \> - Is the left value greater than the right value
- \>= - Is the left value greater than or equal to the right value
Sometimes conditions are as simple as, is this less than that, or is this equal to that. Sometimes you need to check multiple things! This is where AND and OR come into play. These are logical operators that can "combine" the results of different questions (like is equal to) into a single true false value.
AND will result in a True value only when both the left and the right side of the AND are True.
Example: `intValue < 100 and intValue > 50` is only True when intValue is any number between 51 and 99
The Truth Table for AND looks like this
<table>
<tr>
<th>Left Side</th>
<th>Right Side</th>
<th>Result</th>
</tr>
<tr>
<td>True</td>
<td>True</td>
<td>True</td>
</tr>
<tr>
<td>True</td>
<td>False</td>
<td>False</td>
</tr>
<tr>
<td>False</td>
<td>True</td>
<td>False</td>
</tr>
<tr>
<td>False</td>
<td>False</td>
<td>False</td>
</tr>
</table>
OR will result in a True value so long as at least one side (the left, the right, or both) is True, OR is only False when both sides are False
Example: `intValue < 0 or intValue > 100` is True when intValue is less than 0 (any negative number) OR intValue is a value of 101 or greater
The Truth Table for OR looks like this
<table>
<tr>
<th>Left Side</th>
<th>Right Side</th>
<th>Result</th>
</tr>
<tr>
<td>True</td>
<td>True</td>
<td>True</td>
</tr>
<tr>
<td>True</td>
<td>False</td>
<td>True</td>
</tr>
<tr>
<td>False</td>
<td>True</td>
<td>True</td>
</tr>
<tr>
<td>False</td>
<td>False</td>
<td>False</td>
</tr>
</table>
One other Logical Operator that doesn't really combine two values is NOT. NOT flips whatever the resulting value is, True becomes False, False becomes True.
Example: `not intValue < 100` is True when intValue has a value of 100 or greater
NOT can be a little weird to think about, adding negation to logic can make things a little fuzzy to think about. An actual Python Programming course would cover this more than I can here, but there is one thing to keep in mind when working with logical operations. Like PEMDAS from math, logical operators also have an order of operations, see the formatted list below
- Parenthesis
- NOT
- AND
- OR
Parenthesis is at the top, whatever you see in parenthesis will happen first, NOT is evaluated next, then AND, and finally OR. That means if you have something like this:
`someValue or someOtherValue and thisValue`
The AND of someOtherValue and ThisValue will happen first, because AND has higher precedence, the result of that will then be combined with OR only after the AND operation has completed. If you wanted OR to come first, you'd have to write it something like this
`(someValue or someOtherValue) and thisValue`

View File

@ -1,5 +1,5 @@
intValue1 = int(input("Enter Int Value 1: ")) intValue1 = int(input("Enter Int Value 1: "))
intValue2 = int(input("enter Int Value 2: ")) intValue2 = int(input("Enter Int Value 2: "))
if intValue1 == intValue2: if intValue1 == intValue2:
print("Int Value 1 equals Int Value 2") print("Int Value 1 equals Int Value 2")

View File

@ -1 +1,2 @@
# 5 - Random Numbers and For Loops - Challenge
Write a program that requests an integer from the user, and prints that many random numbers AND prints hte sum and average of those numbers. Write a program that requests an integer from the user, and prints that many random numbers AND prints hte sum and average of those numbers.

View File

@ -0,0 +1,34 @@
# Random Numbers and For Loops
## What is a For Loop, and why do I care?
There are times where you're going to want to repeat a task or section of code in your program a certain number of times. For Loops take care of this for us. We create a for loop structure like this
```
for indexVariable in range(someNumber):
DO SOME STUFF
```
The indexVariable element stores the current loop iteration number, or index. The range() function creates a sequence of numbers that we can use from 0 to someNumber-1. i.e. If you said range(5), you'd get a list of numbers 0, 1, 2, 3, 4.
## Can you tell me more about range?
There are different types of "ranges" but range() is pretty common to see when first starting out. Range isn't limited to just creating 0..N-1 ranges, you can do more stuff with it.
Lets say you wanted the range to start with 1 instead of 0. You can say range(1, 6), and this will give you a list of numbers 1, 2, 3, 4, 5.
Lets say you wanted to count by 2's instead of by 1. You can say range(0, 5, 2), and this will give you a list of numbers, 0, 2, and 4.
You can even count backwards. range(4, -1, -1) will give you a list of numbers 4, 3, 2, 1 and 0.
You'll have to decide what goes into range() when you are writing code for whatever it is your doing.
## Random numbers, why do I need those?
For Robot Programming, you probably don't. It's pretty rare for a use case to show up where a robot needs to do something based on random input, as it generally isn't very safe to do something like that.
Random makes for easy examples when working with For Loops, which is why it's included here, and in general programming, it's seen a lot of different places, cryptography and securing communications and data especially. For these examples, it just makes for a nice talking point for how loops can be used to repeat a process several times over.
## Why do we "import random"?
Not everything is available to you all at the same time in Python. Some stuff is in seperate packages called "Modules" that extend the functionality of the base language. In order to work with the extra stuff in the Modules we want to use, we have to "announce" with an import statement that we want to use the module.
Some modules (like random) come with Python can don't need to be installed, other modules, like RobotPy and its assoicated pieces, need to be installed, usually through the command line utility pip, before they can be added in your program.
On top of importing separate modules, you can import additional code that exists within your own project. We'll see some of this further down the line in other lessons, but for now, just know that importing becomes much more important as your programs (whether they be for robots or not) become larger and more complex.

View File

@ -1 +1,2 @@
# 6 - While Loops, Break/Continue, and Loop Translation - Challenge
Write a program that generates a random number, and repeatedly asks the user to guess the number, telling them if they're too low, too high, or if they guess correctly. The program should end once they guess the correct number. Write a program that generates a random number, and repeatedly asks the user to guess the number, telling them if they're too low, too high, or if they guess correctly. The program should end once they guess the correct number.

View File

@ -0,0 +1,63 @@
# 6 - While Loops, Break/Continue, and Loop Translation
## I just learned about For Loops, what the heck is a While Loop?
For Loops are great when you "know" the number of times you need to repeat something in Python (or any language). You have a finite number of steps that need to be repeated, so you do those things a certain number of times and then you're done.
While loops, on the other hand, repeat indefinitely until something is no longer true. A While Loop could go on "forever" if you let it (although generally this is a bad thing, more on that later).
While Loops are structured like this:
```
while someConditionIsTrue:
DO STUFF
```
Each time you run through the While Loop, the condition at the top is re-checked to see if it's still true, if it is, the code runs again, and again, and again, until that condition at the top is False, at which point, the while loop exits, and the code is no longer repeated.
## What are we breaking or continuing?
Break and Continue are keywords that allow us to change the behavior of loops in our programs under certain circumstances we define, usually with if statements.
Continue can be used to jump back to the top of the loop. For For Loops, this means going back to the top, incrementing our index value (as defined by our call to range()) and then making sure that we still numbers to get through. For example, if you had a list of numbers 0, 1, 2, 3, 4 and the following code:
```
for x in range(5):
if x == 3:
continue
print(x)
```
The above code would print the following:
- 0
- 1
- 2
- 4
Three never prints, because once we hit that continue statement, we go right back to the top and onto the next number in the list of numbers that was generated by range(5)
Break on the other hand, when you hit a break statement, the loop ends, immediately, regardless of whether or not there were more things to do. Using the same list 0, 1, 2, 3, 4, and slightly different code:
```
for x in range(5):
if x == 3:
break
print(x)
```
We would get
- 0
- 1
- 2
Continue and break can also be used with While Loops, it all depends on what you're trying to accomplish.
## What is Loop Translation?
Loop Translation is the idea that, a For Loop can always be written as a While Loop, and a While Loop can always be written as a For Loop. Loops in general do the same things once you break them down into there component parts and start running them on a CPU, so from that frame of reference it's easy to see how Loop Translation could be possible.
In general though, it's not good to "use the wrong type of loop" if you can help it. For loops are great when you know how many times you want to do something, but are terrible if the number of times you need to do something will change while the loop is running. While loops, on the other hand are great at doing things indefinitely, until some condition is no longer met, but the code needed to make a while loop do a certain number of things a certain number of times can be really ugly.
You as the programmer will need to decide when to use one loop or the other. This comes with experience, and for the most part is easy to pick up once you've made the decision a few times.
## I've seen two loops now, are there other types?
Yes, sort of. Most other types of loops are based on For and While, the most common forms in my opinion are what I've shown you in this lesson and the last one.
There is a form of looping that can be done using something called Recursion. This however, goes way beyond the scope of these lessons, and so it isn't discussed here.

View File

@ -1,3 +1,4 @@
# 7 - Classes and Objects - Challenge
Write a class called Ball with the following Write a class called Ball with the following
- Characteristics - Characteristics
- diameter (float) - diameter (float)

View File

@ -0,0 +1,63 @@
# 7 - Classes and Objects
## Are you just throwing out words now, what are Classes and Objects?
We are now getting into a powerful concept in programming in general that can be a little difficult to comprehend at first. The concept is called Object Oriented Programming, or OOP. Essentially what you're doing, is creating representations of a thing, and that things characteristics and behaviors, in code. The thing might be a real world object, it might not be, but to start out with, we're going to look at it from the context of creating real world objects in code, because that'll make the most sense. On top of that, a lot of the time spent Robot Programming is creating and manipulating representations of real world things in your code.
Classes are templates, blueprints used to define what an object should have for characteristics (how big it is, how tall, how short, how fast, does it have teeth and if so how many, etc.) and behaviors (can it bite, does it dance, can it jump up and down, etc.). You can't have an Object without a class that comes before it, the class gives you the framework to build an Object.
When making a class, the characteristics should be relevant to the Object you're wanting to represent, and the behaviors it needs to convey. Trees don't have teeth, so your class for a Tree shouldn't have anything referencing teeth, of the tree biting something. Likewise, a dog doesn't have leaves, so no need to write a behavior for the dog to make it photosynthesize light into sugar.
Objects are created from classes. You fill in the characteristics you defined in the Class with values, keeping with the tree example, you might have values for the height of the tree, the diameter of the tree, the type of tree. All of those things are characteristics of a specific tree. You could create one tree that's 5 meters tall, .5 meters around, and is an Oak tree, while creating a completely different tree that's 2 meters tall, .15 meters around, and is a Maple tree. You can create many representations of the same type of Object from a single class.
Now, representing trees in code, not so useful for our purposes. But once we get into Robot Programming, you'll have classes that create Objects that represent Motor Controllers, Sensors, the Driver's Station, Cameras, all kinds of stuff, all real world objects represented by structures in code.
## What does a class definition look like?
Let's start off with this small class and talk about how it looks
```
class SomeClass():
def __init__(self, someParameter, someOtherParameter):
self.instanceVariable1 = someParameter
self.instanceVariable2 = someParameter
def doSomething(self):
print(self.instanceVariable1)
print(self.instanceVariable2)
def doSomethingElse(self):
return self.instanceVariable1 + self.instanceVariable2
```
Above, we start off by saying class <i>ClassName</i>(): this just defines what our class name is going to be so we can reference it later.
Next, we have def __init__(self), this is our constructor. It's a special method (a set of code that can be called by name) that is used to build Objects based on the class we create. The names inside the parenthesis are called parameters, these are special variables that are used to pass information into methods from outside the class. Classes are meant to encapsulate the information they contain and hide it away from prying eyes unless it's accessed in some way through a behavior (a method). So parameters allow us to hand information in that wouldn't otherwise be there.
The <i>self</i> parameter is important when building classes, because it stores our characteristics (also called instance variables) that we to contain inside of our created objects. You'll notice that in all methods in our class, not just __init__, self is a parameter. This is how the other methods (behaviors) in our resulting objects will be able to access the instance variables we set up in __init__ for later use.
The other methods (behaviors) <i>doSomething</i> and <i>doSomethingElse</i> are the actions that our created objects will be able to take. You get to define how this behaviors work, and you can have as many or as few as are necessary to make your representation work the way you want it to.
Notice for a moment in <i>doSomethingElse</i> there is a <i>return</i> statement. Return makes it so that the behavior produces a value, you can take that value, much like you would from a method like input() and store it in a variable. We'll see this when we look at creating an object down below.
## So how do I create an Object from a Class?
Well, lets assume for the moment that we defined SomeClass in a file called OtherClasses.py, using that information, lets look at the code below:
```
import OtherClasses
thisIsSomeClass = OtherClasses.SomeClass(1, 2)
thisIsSomeClassAgain = OtherClasses.SomeClass(3, 4)
print(thisIsSomeClass.doSomethingElse())
print(thisIsSomeClassAgain.doSomethingElse())
```
First, we import the OtherClasses file by name, this makes SomeClass available to us to use. Then, we create two variables, thisIsSomeClass and thisIsSomeClassAgain, and create two different representations of SomeClass, the first using the values 1 and 2, the second using 3 and 4.
By doing this, we've created two Objects from the same class that will produce different results when asking each separate object to do something.
The first print statement deals with thisIsSomeClass, which has 1 and 2 for values. We saw above that doSomethingElse returns the sum of the instance variables we created, those instance variables have the values we pass in as parameters when we create an Object using SomeClass, in this case, the values 1 and 2. So when doSomethingElse runs, it will return, in this case, the value of 1 + 2, which is three. So the line print(thisIsSomeClass.doSomethingElse()) will print 3 to the console.
The next line is similar but not the same, because now we're dealing with the Object stored in thisIsSomeClassAgain, that Object was created using 3 and 4, we call doSomethingElse for that Object too, which adds 3 + 4, to get 7. So the line print(thisIsSomeClassAgain.doSomethingElse()) will print 7 to the console.
## This feels like a lot of information, is this a lot of information?
It is, Classes and Objects can do so much, even more than what has been demonstrated in this lesson. It's a hard concept to convey quickly. For Robot Programming, at least at the beginner to low intermediate levels, an in depth understanding of Classes and Objects isn't super important, but it's good to know they exist, because once you decide to start making more complex robot programs, it may make sense to move certain aspects of your code into a class/object type of structure. On top of that, certain styles of Robot Programming require you to create many different classes (and subsequently, many different objects) in order for your robot to work. Take this early introduction to Classes and Objects into memory and hold onto it, even if you couldn't quite make this setup yourself for your own real world object if you wanted to, again, it's still good to have a general idea of what they are, because they will show up in your robot code, even if you don't realize in the moment that's what you're looking at.

View File

@ -0,0 +1,13 @@
# 8 - Basic Robot Electronics
## I want to be a programmer, not an electrician, why is this here?
To be a good programmer (not just robot programmer) you need to have an understanding of what hardware your software has to interact with. Not every system is the same, and so you can't write your programs the same way for every problem you're trying to solve (although it would be nice if it were that way).
Not knowing which Motor Controller is which, not knowing which sensor is which, not knowing what any electronic component is on the robot will set you up for failure if you want to write code that actually works. So I built in some basic information to help give you a general idea of what everything is and how it's important or could be important to your overall robot function.
Now, am I saying that you need to get your hands in and start wiring the robot, no of course not. I did, but that doesn't mean the dual path of electronics and programming is right for you. But it will serve you well to know at least roughly what everything is that way when someone walks up to you and says "the climber motors are using REV Spark MAXs on CAN IDs 3 and 4 and we need to use the built in encoders to know where are climbing hooks are at all times" you won't just look back at them with a confused expression on your face.
This concept of "knowing just enough" applies to everything you'll do as a programmer. You don't write a AAA game without first understanding what engine you're using, what hardware that engine works on, what the target performance is. You don't write a database application without first understanding what the underlying database is going to be, what resources does it have, do any other applications interact with it. You must know the system your coding for, or you won't be able to write effective and efficient code.
## Ok I get it, but, this is just a PDF slideshow...
That I can also fix, while there may be some Java oriented comments in the video, [this video on Youtube](https://www.youtube.com/watch?v=RI5EFQlbrYI) is an hour long discussion from the original Java Programming Training I conducted in 2022. We talk about everything in the slideshow, step by step. Again, it's just a basic overview, so if you have questions, you can direct them to me, or Google is your friend. Don't be in the dark on something when it comes time for you to start writing code if you can help it at all, it'll save you from the terrible feeling of needing to scramble to learn something in a time crunch.

View File

@ -0,0 +1,21 @@
# 9 - Robot Program Structure
## We're finally getting into Robot Programming?
Yes! We had to get through some of the basics before we get into the robots. With that being said, we're going to start off easy and escalate and build out from here. Which means, we need to talk about the general structure of a Robot Program. Starting with the different modes that a robot can be in.
## What are the different Robot Modes?
There are 3 modes that we're going to concern ourselves with.
- Disabled - This is your robot's default state when it first starts up. Disabled just means, well, what the name implies. The robot is disabled, which means no motors, no pneumatics, basically nothing that moves is going to work. LEDs, sensors, and other non-motile systems still can run in the background though.
- Teleoperated - This is the mode your Robot is in where you (or your Drive Team) get's to move the robot with controllers. This is the meat and potatoes of a match at the competition. Most of the match is spent in this mode.
- Autonomous - This is the mode where you're given the opportunity as a programmer to show your stuff. You make the robot automatically complete some aspect or series of aspects of the game that we are playing that year. An Autonomous program can be very simple, or wildly complex, it depends heavily on two things, your proficiency with programming, and how much time you are given with the robot to develop, test, and rework your autonomous program. No human interaction is allowed when the robot is running autonomous, no driver control at all, which means you'll need to use time, sensors, or both to figure out where you are on the field, and complete whatever tasks are laid out before you.
Each mode has two methods, an init method and a periodic method. The init method is called (or used) once when your robot enters a mode. So teleopInit() would get called when Teleoperated starts, autonomousInit() would get called when Autonomous starts, and so on. The periodic method is called (or used) every 20ms (roughly 50 times a second) once the robot has run the associated init method for the mode your robot is in.
There are some special methods that aren't technically a mode. These are the robotInit() and robotPeriodic() methods. The robotInit() method is where you do the setup for your robot. It's kind of like the __init__ method we've seen previously. All of your general setup should happen in robotInit() as it gets called once when your robot first boots up, before it enters any mode. As for robotPeriodic(), this method runs every 20ms (roughly 50 times a second) regardless of what mode your robot is in. You can put code here you want to have run all the time, like LED code, or sensor management code.
## Every 20ms, that seems like a really small amount of time...
It is, you need to ensure that your code fits in this 20ms time period to make sure you don't experience problems controlling your robot. If your code in the periodic methods takes longer than 20ms too many times, something called the watchdog gets mad. When the watchdog doesn't get fed, for safety reasons, it will shut your motors down. Not ideal. Don't worry though, unless your doing something completely insane, it's unlikely normal beginner to intermediate level robot code shouldn't cause any issues.
## So, is this example how all robot programs will start?
Yes...for now. There are other forms of writing robot code that change this up a little bit. But we are nowhere near talking about that. For all of the examples we'll see up through and including example 19 will use this structure as a base.

View File

@ -0,0 +1,20 @@
import wpilib
class MyRobot(wpilib.TimedRobot):
def robotInit(self):
pass
def autonomousInit(self):
pass
def autonomousPeriodic(self):
pass
def teleopInit(self):
pass
def teleopPeriodic(self):
pass
if __name__ == "__main__":
wpilib.run(MyRobot)