Skip to content

Commit 011918e

Browse files
committed
Update arm simulation example
- Fixes #79
1 parent 2a0954d commit 011918e

File tree

4 files changed

+133
-75
lines changed

4 files changed

+133
-75
lines changed

arm-simulation/constants.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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 math
6+
7+
from wpimath import units
8+
9+
10+
class Constants:
11+
kMotorPort = 0
12+
kEncoderAChannel = 0
13+
kEncoderBChannel = 1
14+
kJoystickPort = 0
15+
16+
kArmPositionKey = "ArmPosition"
17+
kArmPKey = "ArmP"
18+
19+
# The P gain for the PID controller that drives this arm.
20+
kDefaultArmKp = 50.0
21+
kDefaultArmSetpointDegrees = 75.0
22+
23+
# distance per pulse = (angle per revolution) / (pulses per revolution)
24+
# = (2 * PI rads) / (4096 pulses)
25+
kArmEncoderDistPerPulse = 2.0 * math.pi / 4096
26+
27+
kArmReduction = 200
28+
kArmMass = 8.0 # Kilograms
29+
kArmLength = units.inchesToMeters(30)
30+
kMinAngleRads = units.degreesToRadians(-75)
31+
kMaxAngleRads = units.degreesToRadians(255)

arm-simulation/physics.py

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
if typing.TYPE_CHECKING:
2525
from robot import MyRobot
2626

27+
from constants import Constants
28+
2729

2830
class PhysicsEngine:
2931
"""
@@ -43,34 +45,39 @@ def __init__(self, physics_controller: PhysicsInterface, robot: "MyRobot"):
4345
self.armGearbox = wpimath.system.plant.DCMotor.vex775Pro(2)
4446

4547
# Simulation classes help us simulate what's going on, including gravity.
46-
# This sim represents an arm with 2 775s, a 600:1 reduction, a mass of 5kg,
47-
# 30in overall arm length, range of motion in [-75, 255] degrees, and noise
48-
# with a standard deviation of 1 encoder tick.
48+
# This arm sim represents an arm that can travel from -75 degrees (rotated down front)
49+
# to 255 degrees (rotated down in the back).
4950
self.armSim = wpilib.simulation.SingleJointedArmSim(
5051
self.armGearbox,
51-
600.0,
52-
wpilib.simulation.SingleJointedArmSim.estimateMOI(0.762, 5),
53-
0.762,
54-
math.radians(-75),
55-
math.radians(255),
52+
Constants.kArmReduction,
53+
wpilib.simulation.SingleJointedArmSim.estimateMOI(
54+
Constants.kArmLength, Constants.kArmMass
55+
),
56+
Constants.kArmLength,
57+
Constants.kMinAngleRads,
58+
Constants.kMaxAngleRads,
5659
True,
57-
0,
60+
# Add noise with a std-dev of 1 tick
61+
Constants.kArmEncoderDistPerPulse,
5862
)
59-
self.encoderSim = wpilib.simulation.EncoderSim(robot.encoder)
60-
self.motorSim = wpilib.simulation.PWMSim(robot.motor.getChannel())
63+
self.encoderSim = wpilib.simulation.EncoderSim(robot.arm.encoder)
64+
self.motorSim = wpilib.simulation.PWMSim(robot.arm.motor.getChannel())
6165

62-
# Create a Mechanism2d display of an Arm
66+
# Create a Mechanism2d display of an Arm with a fixed ArmTower and moving Arm.
6367
self.mech2d = wpilib.Mechanism2d(60, 60)
64-
self.armBase = self.mech2d.getRoot("ArmBase", 30, 30)
65-
self.armTower = self.armBase.appendLigament(
66-
"Arm Tower", 30, -90, 6, wpilib.Color8Bit(wpilib.Color.kBlue)
67-
)
68-
self.arm = self.armBase.appendLigament(
69-
"Arm", 30, self.armSim.getAngle(), 6, wpilib.Color8Bit(wpilib.Color.kYellow)
68+
self.armPivot = self.mech2d.getRoot("ArmPivot", 30, 30)
69+
self.armTower = self.armPivot.appendLigament("Arm Tower", 30, -90)
70+
self.arm = self.armPivot.appendLigament(
71+
"Arm",
72+
30,
73+
math.degrees(self.armSim.getAngle()),
74+
6,
75+
wpilib.Color8Bit(wpilib.Color.kYellow),
7076
)
7177

7278
# Put Mechanism to SmartDashboard
7379
wpilib.SmartDashboard.putData("Arm Sim", self.mech2d)
80+
self.armTower.setColor(wpilib.Color8Bit(wpilib.Color.kBlue))
7481

7582
def update_sim(self, now: float, tm_diff: float) -> None:
7683
"""
@@ -94,9 +101,9 @@ def update_sim(self, now: float, tm_diff: float) -> None:
94101
# voltage
95102
self.encoderSim.setDistance(self.armSim.getAngle())
96103
# SimBattery estimates loaded battery voltage
97-
# wpilib.simulation.RoboRioSim.setVInVoltage(
98-
# wpilib.simulation.BatterySim
99-
# )
104+
wpilib.simulation.RoboRioSim.setVInVoltage(
105+
wpilib.simulation.BatterySim.calculate([self.armSim.getCurrentDraw()])
106+
)
100107

101108
# Update the mechanism arm angle based on the simulated arm angle
102109
# -> setAngle takes degrees, getAngle returns radians... >_>

arm-simulation/robot.py

Lines changed: 16 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -4,69 +4,31 @@
44
# Open Source Software; you can modify and/or share it under the terms of
55
# the WPILib BSD license file in the root directory of this project.
66

7-
import math
87
import wpilib
9-
import wpimath.controller
108

9+
from subsytems.arm import Arm
10+
from constants import Constants
1111

12-
class MyRobot(wpilib.TimedRobot):
13-
kMotorPort = 0
14-
kEncoderAChannel = 0
15-
kEncoderBChannel = 1
16-
kJoystickPort = 0
17-
18-
kArmPositionKey = "ArmPosition"
19-
kArmPKey = "ArmP"
20-
21-
# distance per pulse = (angle per revolution) / (pulses per revolution)
22-
# = (2 * PI rads) / (4096 pulses)
23-
kArmEncoderDistPerPulse = 2.0 * math.pi / 4096.0
24-
25-
def robotInit(self) -> None:
26-
# The P gain for the PID controller that drives this arm.
27-
self.kArmKp = 50.0
2812

29-
self.armPosition = 75.0
30-
31-
# standard classes for controlling our arm
32-
self.controller = wpimath.controller.PIDController(self.kArmKp, 0, 0)
33-
self.encoder = wpilib.Encoder(self.kEncoderAChannel, self.kEncoderBChannel)
34-
self.motor = wpilib.PWMSparkMax(self.kMotorPort)
35-
self.joystick = wpilib.Joystick(self.kJoystickPort)
36-
37-
self.encoder.setDistancePerPulse(self.kArmEncoderDistPerPulse)
38-
39-
# Set the Arm position setpoint and P constant to Preferences if the keys
40-
# don't already exist
41-
if not wpilib.Preferences.containsKey(self.kArmPositionKey):
42-
wpilib.Preferences.setDouble(self.kArmPositionKey, self.armPosition)
43-
if not wpilib.Preferences.containsKey(self.kArmPKey):
44-
wpilib.Preferences.setDouble(self.kArmPKey, self.kArmKp)
13+
class MyRobot(wpilib.TimedRobot):
14+
def robotInit(self):
15+
self.arm = Arm()
16+
self.joystick = wpilib.Joystick(Constants.kJoystickPort)
4517

46-
def teleopInit(self) -> None:
47-
# Read Preferences for Arm setpoint and kP on entering Teleop
48-
self.armPosition = wpilib.Preferences.getDouble(
49-
self.kArmPositionKey, self.armPosition
50-
)
51-
if self.kArmKp != wpilib.Preferences.getDouble(self.kArmPKey, self.kArmKp):
52-
self.kArmKp = wpilib.Preferences.getDouble(self.kArmPKey, self.kArmKp)
53-
self.controller.setP(self.kArmKp)
18+
def teleopInit(self):
19+
self.arm.loadPreferences()
5420

55-
def teleopPeriodic(self) -> None:
21+
def teleopPeriodic(self):
5622
if self.joystick.getTrigger():
57-
# Here we run PID control like normal, with a setpoint read from
58-
# preferences in degrees
59-
pidOutput = self.controller.calculate(
60-
self.encoder.getDistance(), math.radians(self.armPosition)
61-
)
62-
self.motor.setVoltage(pidOutput)
23+
# Here, we run PID control like normal.
24+
self.arm.reachSetpoint()
6325
else:
64-
# Otherwise we disable the motor
65-
self.motor.set(0.0)
26+
# Otherwise, we disable the motor.
27+
self.arm.stop()
6628

67-
def disabledInit(self) -> None:
68-
# This just makes sure that our simulation code knows that the motor is off
69-
self.motor.set(0)
29+
def disabledInit(self):
30+
# This just makes sure that our simulation code knows that the motor's off.
31+
self.arm.stop()
7032

7133

7234
if __name__ == "__main__":

arm-simulation/subsytems/arm.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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+
7+
from wpimath.controller import PIDController
8+
from wpimath.system.plant import DCMotor
9+
from wpimath import units
10+
11+
from wpilib.simulation import BatterySim, EncoderSim, RoboRioSim, SingleJointedArmSim
12+
13+
from constants import Constants
14+
15+
16+
class Arm:
17+
def __init__(self):
18+
# The P gain for the PID controller that drives this arm.
19+
self.armKp = Constants.kDefaultArmKp
20+
self.armSetpointDegrees = Constants.kDefaultArmSetpointDegrees
21+
22+
# The arm gearbox represents a gearbox containing two Vex 775pro motors.
23+
self.armGearbox = DCMotor.vex775Pro(2)
24+
25+
# Standard classes for controlling our arm
26+
self.controller = PIDController(self.armKp, 0, 0)
27+
self.encoder = wpilib.Encoder(
28+
Constants.kEncoderAChannel, Constants.kEncoderBChannel
29+
)
30+
self.motor = wpilib.PWMSparkMax(Constants.kMotorPort)
31+
32+
# Subsystem constructor.
33+
self.encoder.setDistancePerPulse(Constants.kArmEncoderDistPerPulse)
34+
35+
# Set the Arm position setpoint and P constant to Preferences if the keys don't already exist
36+
wpilib.Preferences.initDouble(
37+
Constants.kArmPositionKey, self.armSetpointDegrees
38+
)
39+
wpilib.Preferences.initDouble(Constants.kArmPKey, self.armKp)
40+
41+
def loadPreferences(self):
42+
# Read Preferences for Arm setpoint and kP on entering Teleop
43+
self.armSetpointDegrees = wpilib.Preferences.getDouble(
44+
Constants.kArmPositionKey, self.armSetpointDegrees
45+
)
46+
if self.armKp != wpilib.Preferences.getDouble(Constants.kArmPKey, self.armKp):
47+
self.armKp = wpilib.Preferences.getDouble(Constants.kArmPKey, self.armKp)
48+
self.controller.setP(self.armKp)
49+
50+
def reachSetpoint(self):
51+
pidOutput = self.controller.calculate(
52+
self.encoder.getDistance(),
53+
units.degreesToRadians(self.armSetpointDegrees),
54+
)
55+
self.motor.setVoltage(pidOutput)
56+
57+
def stop(self):
58+
self.motor.set(0.0)

0 commit comments

Comments
 (0)