Skip to content

Commit 16bece7

Browse files
authored
Add armbot example (#70)
Following #49 In `constants.py` file, the constants are grouped in different classes (I don't know if this style will respect the [RobotPy's porting guide](https://github.com/robotpy/examples/blob/main/CONTRIBUTING.md)). In `robotcontainer.py`, I created a function named `moveArm`, to adapt in a single line (two calls in `configureButtonBindings`).
1 parent c616f00 commit 16bece7

File tree

6 files changed

+402
-0
lines changed

6 files changed

+402
-0
lines changed

commands-v2/armbot/constants.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Copyright (c) FIRST and other WPILib contributors.
2+
# Open Source Software; you can modify and/or share it under the terms of
3+
# the WPILib BSD license file in the root directory of this project.
4+
5+
"""
6+
The constants module is a convenience place for teams to hold robot-wide
7+
numerical or boolean constants. Don't use this for any other purpose!
8+
"""
9+
10+
import math
11+
12+
13+
class DriveConstants:
14+
# The PWM IDs for the drivetrain motor controllers.
15+
kLeftMotor1Port = 0
16+
kLeftMotor2Port = 1
17+
kRightMotor1Port = 2
18+
kRightMotor2Port = 3
19+
20+
# Encoders and their respective motor controllers.
21+
kLeftEncoderPorts = (0, 1)
22+
kRightEncoderPorts = (2, 3)
23+
kLeftEncoderReversed = False
24+
kRightEncoderReversed = True
25+
26+
# Encoder counts per revolution/rotation.
27+
kEncoderCPR = 1024
28+
kWheelDiameterInches = 6
29+
30+
# Assumes the encoders are directly mounted on the wheel shafts
31+
kEncoderDistancePerPulse = (kWheelDiameterInches * math.pi) / kEncoderCPR
32+
33+
34+
class ArmConstants:
35+
# NOTE: Please do NOT use these values on your robot.
36+
kMotorPort = 4
37+
kP = 1
38+
kSVolts = 1
39+
kGVolts = 1
40+
kVVoltSecondPerRad = 0.5
41+
kAVoltSecondSquaredPerRad = 0.1
42+
43+
kMaxVelocityRadPerSecond = 3
44+
kMaxAccelerationRadPerSecSquared = 10
45+
46+
kEncoderPorts = (4, 5)
47+
kEncoderPPR = 256
48+
kEncoderDistancePerPulse = 2.0 * math.pi / kEncoderPPR
49+
50+
# The offset of the arm from the horizontal in its neutral position,
51+
# measured from the horizontal
52+
kArmOffsetRads = 0.5
53+
54+
55+
class AutoConstants:
56+
kAutoTimeoutSeconds = 12
57+
kAutoShootTimeSeconds = 7
58+
59+
60+
class OIConstants:
61+
kDriverControllerPort = 0

commands-v2/armbot/robot.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#!usr/bin/env python3
2+
3+
# Copyright (c) FIRST and other WPILib contributors.
4+
# Open Source Software; you can modify and/or share it under the terms of
5+
# the WPILib BSD license file in the root directory of this project.
6+
7+
import wpilib
8+
import commands2
9+
import typing
10+
11+
from robotcontainer import RobotContainer
12+
13+
14+
class MyRobot(commands2.TimedCommandRobot):
15+
"""
16+
Our default robot class, pass it to wpilib.run
17+
Command v2 robots are encouraged to inherit from TimedCommandRobot, which
18+
has an implementation of robotPeriodic which runs the scheduler for you
19+
"""
20+
21+
autonomousCommand: typing.Optional[commands2.Command] = None
22+
23+
def robotInit(self) -> None:
24+
"""
25+
This function is run when the robot is first started up and should be used for any
26+
initialization code.
27+
"""
28+
29+
# Instantiate our RobotContainer. This will perform all our button bindings, and put our
30+
# autonomous chooser on the dashboard.
31+
self.container = RobotContainer()
32+
33+
def robotPeriodic(self) -> None:
34+
"""This function is called every 20 ms, no matter the mode. Use this for items like diagnostics
35+
that you want ran during disabled, autonomous, teleoperated and test.
36+
37+
This runs after the mode specific periodic functions, but before LiveWindow and
38+
SmartDashboard integrated updating."""
39+
40+
# Runs the Scheduler. This is responsible for polling buttons, adding newly-scheduled
41+
# commands, running already-scheduled commands, removing finished or interrupted commands,
42+
# and running subsystem periodic() methods. This must be called from the robot's periodic
43+
# block in order for anything in the Command-based framework to work.
44+
commands2.CommandScheduler.getInstance().run()
45+
46+
def disabledInit(self) -> None:
47+
"""This function is called once each time the robot enters Disabled mode."""
48+
self.container.disablePIDSubsystems()
49+
50+
def disabledPeriodic(self) -> None:
51+
"""This function is called periodically when disabled"""
52+
pass
53+
54+
def autonomousInit(self) -> None:
55+
"""This autonomous runs the autonomous command selected by your RobotContainer class."""
56+
self.autonomousCommand = self.container.getAutonomousCommand()
57+
58+
if self.autonomousCommand:
59+
self.autonomousCommand.schedule()
60+
61+
def autonomousPeriodic(self) -> None:
62+
"""This function is called periodically during autonomous"""
63+
pass
64+
65+
def teleopInit(self) -> None:
66+
# This makes sure that the autonomous stops running when
67+
# teleop starts running. If you want the autonomous to
68+
# continue until interrupted by another command, remove
69+
# this line or comment it out.
70+
if self.autonomousCommand:
71+
self.autonomousCommand.cancel()
72+
73+
def teleopPeriodic(self) -> None:
74+
"""This function is called periodically during operator control"""
75+
pass
76+
77+
def testInit(self) -> None:
78+
# Cancels all running commands at the start of test mode
79+
commands2.CommandScheduler.getInstance().cancelAll()
80+
81+
82+
if __name__ == "__main__":
83+
wpilib.run(MyRobot)

commands-v2/armbot/robotcontainer.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Copyright (c) FIRST and other WPILib contributors.
2+
# Open Source Software; you can modify and/or share it under the terms of
3+
# the WPILib BSD license file in the root directory of this project.
4+
5+
import commands2
6+
import commands2.button
7+
import commands2.cmd
8+
9+
import constants
10+
11+
from subsystems.armsubsystem import ArmSubsystem
12+
from subsystems.drivesubsystem import DriveSubsystem
13+
14+
15+
class RobotContainer:
16+
"""
17+
This class is where the bulk of the robot should be declared. Since Command-based is a
18+
"declarative" paradigm, very little robot logic should actually be handled in the :class:`.Robot`
19+
periodic methods (other than the scheduler calls). Instead, the structure of the robot (including
20+
subsystems, commands, and button mappings) should be declared here.
21+
"""
22+
23+
def __init__(self) -> None:
24+
# The robot's subsystems
25+
self.robot_drive = DriveSubsystem()
26+
self.robot_arm = ArmSubsystem()
27+
28+
# The driver's controller
29+
self.driver_controller = commands2.button.CommandXboxController(
30+
constants.OIConstants.kDriverControllerPort
31+
)
32+
33+
# Configure the button bindings
34+
self.configureButtonBindings()
35+
36+
# Set the default drive command
37+
# Set the default drive command to split-stick arcade drive
38+
self.robot_drive.setDefaultCommand(
39+
commands2.cmd.run(
40+
# A split-stick arcade command, with forward/backward controlled by the left
41+
# hand, and turning controlled by the right.
42+
lambda: self.robot_drive.arcadeDrive(
43+
-self.driver_controller.getLeftY(),
44+
-self.driver_controller.getRightX(),
45+
),
46+
[self.robot_drive],
47+
)
48+
)
49+
50+
def configureButtonBindings(self) -> None:
51+
"""
52+
Use this method to define your button->command mappings. Buttons can be created by
53+
instantiating a :GenericHID or one of its subclasses (Joystick or XboxController),
54+
and then passing it to a JoystickButton.
55+
"""
56+
57+
# Move the arm to 2 radians above horizontal when the 'A' button is pressed.
58+
self.driver_controller.A().onTrue(
59+
commands2.cmd.run(self.moveArm(2), [self.robot_arm])
60+
)
61+
62+
# Move the arm to neutral position when the 'B' button is pressed
63+
self.driver_controller.B().onTrue(
64+
commands2.cmd.run(
65+
self.moveArm(constants.ArmConstants.kArmOffsetRads), [self.robot_arm]
66+
)
67+
)
68+
69+
# Disable the arm controller when Y is pressed
70+
self.driver_controller.Y().onTrue(
71+
commands2.cmd.runOnce(lambda: self.robot_arm.disable())
72+
)
73+
74+
# Drive at half speed when the bumper is held
75+
self.driver_controller.rightTrigger().onTrue(
76+
commands2.cmd.runOnce(lambda: self.robot_drive.setMaxOutput(0.5))
77+
)
78+
self.driver_controller.rightTrigger().onFalse(
79+
commands2.cmd.runOnce(lambda: self.robot_drive.setMaxOutput(1.0))
80+
)
81+
82+
def disablePIDSubsystems(self) -> None:
83+
"""Disables all ProfiledPIDSubsystem and PIDSubsystem instances.
84+
This should be called on robot disable to prevent integral windup."""
85+
self.robot_arm.disable()
86+
87+
def getAutonomousCommand(self) -> commands2.Command:
88+
"""Use this to pass the autonomous command to the main {@link Robot} class.
89+
90+
:returns: the command to run in autonomous
91+
"""
92+
return commands2.cmd.nothing()
93+
94+
def moveArm(self, radians: int) -> None:
95+
self.robot_arm.setGoal(radians)
96+
self.robot_arm.enable()
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Copyright (c) FIRST and other WPILib contributors.
2+
# Open Source Software; you can modify and/or share it under the terms of
3+
# the WPILib BSD license file in the root directory of this project.
4+
5+
import wpilib
6+
import commands2
7+
import wpimath.controller
8+
import wpimath.trajectory
9+
10+
import constants
11+
12+
13+
class ArmSubsystem(commands2.ProfiledPIDSubsystem):
14+
"""A robot arm subsystem that moves with a motion profile."""
15+
16+
# Create a new ArmSubsystem
17+
def __init__(self) -> None:
18+
super().__init__(
19+
wpimath.controller.ProfiledPIDController(
20+
constants.ArmConstants.kP,
21+
0,
22+
0,
23+
wpimath.trajectory.TrapezoidProfile.Constraints(
24+
constants.ArmConstants.kMaxVelocityRadPerSecond,
25+
constants.ArmConstants.kMaxAccelerationRadPerSecSquared,
26+
),
27+
),
28+
0,
29+
)
30+
31+
self.motor = wpilib.PWMSparkMax(constants.ArmConstants.kMotorPort)
32+
self.encoder = wpilib.Encoder(
33+
constants.ArmConstants.kEncoderPorts[0],
34+
constants.ArmConstants.kEncoderPorts[1],
35+
)
36+
self.feedforward = wpimath.controller.ArmFeedforward(
37+
constants.ArmConstants.kSVolts,
38+
constants.ArmConstants.kGVolts,
39+
constants.ArmConstants.kVVoltSecondPerRad,
40+
constants.ArmConstants.kAVoltSecondSquaredPerRad,
41+
)
42+
43+
self.encoder.setDistancePerPulse(
44+
constants.ArmConstants.kEncoderDistancePerPulse
45+
)
46+
47+
# Start arm at rest in neutral position
48+
self.setGoal(constants.ArmConstants.kArmOffsetRads)
49+
50+
def _useOutput(
51+
self, output: float, setpoint: wpimath.trajectory.TrapezoidProfile.State
52+
) -> None:
53+
# Calculate the feedforward from the setpoint
54+
feedforward = self.feedforward.calculate(setpoint.position, setpoint.velocity)
55+
56+
# Add the feedforward to the PID output to get the motor output
57+
self.motor.setVoltage(output + feedforward)
58+
59+
def _getMeasurement(self) -> float:
60+
return self.encoder.getDistance() + constants.ArmConstants.kArmOffsetRads

0 commit comments

Comments
 (0)