Skip to content

Commit 7276f80

Browse files
authored
Merge pull request #387 from thedropbears/state-space-arm
2 parents d3adb82 + f086099 commit 7276f80

File tree

4 files changed

+125
-16
lines changed

4 files changed

+125
-16
lines changed

components/intake.py

Lines changed: 105 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import math
22

3+
import numpy as np
34
import wpilib
45
from magicbot import feedback, tunable
56
from phoenix5 import ControlMode, TalonSRX
@@ -8,7 +9,12 @@
89
SparkMaxConfig,
910
)
1011
from wpilib import DutyCycleEncoder
11-
from wpimath.controller import ArmFeedforward, PIDController
12+
from wpimath.controller import (
13+
LinearQuadraticRegulator_2_1,
14+
)
15+
from wpimath.estimator import KalmanFilter_2_1_2
16+
from wpimath.system import LinearSystemLoop_2_1_2
17+
from wpimath.system.plant import DCMotor, LinearSystemId
1218
from wpimath.trajectory import TrapezoidProfile
1319

1420
from ids import DioChannel, SparkId, TalonId
@@ -24,6 +30,7 @@ class IntakeComponent:
2430
DEPLOYED_ANGLE_LOWER = 3.391598 - ARM_ENCODER_OFFSET
2531
DEPLOYED_ANGLE_UPPER = 3.891598 - ARM_ENCODER_OFFSET
2632
RETRACTED_ANGLE = 4.592024 - ARM_ENCODER_OFFSET
33+
ARM_MOI = 0.181717788
2734

2835
gear_ratio = 4.0 * 5.0 * (48.0 / 40.0)
2936

@@ -46,12 +53,6 @@ def __init__(self, intake_mech_root: wpilib.MechanismRoot2d) -> None:
4653
spark_config.inverted(False)
4754
spark_config.setIdleMode(SparkMaxConfig.IdleMode.kBrake)
4855

49-
self.motion_profile = TrapezoidProfile(TrapezoidProfile.Constraints(4.0, 8.0))
50-
self.pid = PIDController(Kp=5.9679, Ki=0, Kd=0.0)
51-
52-
# CG is at 220mm, 2.9kg
53-
# https://www.reca.lc/arm?armMass=%7B%22s%22%3A2.9%2C%22u%22%3A%22kg%22%7D&comLength=%7B%22s%22%3A0.22%2C%22u%22%3A%22m%22%7D&currentLimit=%7B%22s%22%3A40%2C%22u%22%3A%22A%22%7D&efficiency=90&endAngle=%7B%22s%22%3A90%2C%22u%22%3A%22deg%22%7D&iterationLimit=10000&motor=%7B%22quantity%22%3A1%2C%22name%22%3A%22NEO%22%7D&ratio=%7B%22magnitude%22%3A24%2C%22ratioType%22%3A%22Reduction%22%7D&startAngle=%7B%22s%22%3A15%2C%22u%22%3A%22deg%22%7D
54-
self.arm_ff = ArmFeedforward(kS=0.0, kG=0.86, kV=0.47, kA=0.02)
5556
spark_config.encoder.positionConversionFactor(math.tau * (1 / self.gear_ratio))
5657
spark_config.encoder.velocityConversionFactor(
5758
(1 / 60) * math.tau * (1 / self.gear_ratio)
@@ -65,11 +66,47 @@ def __init__(self, intake_mech_root: wpilib.MechanismRoot2d) -> None:
6566

6667
self.motor_encoder = self.arm_motor.getEncoder()
6768

69+
plant = LinearSystemId.singleJointedArmSystem(
70+
DCMotor.NEO(1), self.ARM_MOI, self.gear_ratio
71+
)
72+
73+
self.observer = KalmanFilter_2_1_2(
74+
plant,
75+
(
76+
0.15,
77+
0.17,
78+
),
79+
(0.005, 0.009),
80+
0.020,
81+
)
82+
83+
self.controller = LinearQuadraticRegulator_2_1(
84+
plant,
85+
(
86+
0.005,
87+
0.02,
88+
),
89+
(2.0,),
90+
0.020,
91+
)
92+
93+
self.loop = LinearSystemLoop_2_1_2(
94+
plant, self.controller, self.observer, 12.0, 0.020
95+
)
96+
97+
self.loop.reset([self.position_observation(), self.velocity_observation()])
98+
self.loop.setNextR([self.position_observation(), self.velocity_observation()])
99+
self.innovation = np.zeros(self.loop.xhat().shape)
100+
101+
self.motion_profile = TrapezoidProfile(TrapezoidProfile.Constraints(8.0, 10.0))
68102
self.desired_state = TrapezoidProfile.State(
69103
IntakeComponent.RETRACTED_ANGLE, 0.0
70104
)
105+
self.tracked_state = self.desired_state
71106
self.last_setpoint_update_time = wpilib.Timer.getFPGATimestamp()
72-
self.initial_state = TrapezoidProfile.State(self.position(), self.velocity())
107+
self.initial_state = TrapezoidProfile.State(
108+
self.position_observation(), self.velocity_observation()
109+
)
73110

74111
def intake(self, upper: bool):
75112
deployed_angle = (
@@ -97,16 +134,68 @@ def retract(self):
97134
def raw_encoder(self) -> float:
98135
return self.encoder.get()
99136

100-
@feedback
101137
def position_degrees(self) -> float:
102138
return math.degrees(self.position())
103139

104-
def position(self):
140+
@feedback
141+
def position(self) -> float:
142+
return self.loop.xhat(0)
143+
144+
def position_observation(self) -> float:
105145
return self.encoder.get() - IntakeComponent.ARM_ENCODER_OFFSET
106146

147+
@feedback
148+
def position_observation_degrees(self) -> float:
149+
return math.degrees(self.position_observation())
150+
107151
def velocity(self) -> float:
152+
return self.loop.xhat(1)
153+
154+
def velocity_observation(self) -> float:
108155
return self.motor_encoder.getVelocity()
109156

157+
@feedback
158+
def next_input(self) -> float:
159+
return self.loop.U(0)
160+
161+
def correct_and_predict(self) -> None:
162+
predicted = self.loop.xhat()
163+
if wpilib.DriverStation.isDisabled():
164+
self.observer.correct(
165+
[0.0], [self.position_observation(), self.velocity_observation()]
166+
)
167+
corrected = self.observer.xhat()
168+
169+
self.observer.predict([0.0], 0.02)
170+
else:
171+
self.loop.correct(
172+
[self.position_observation(), self.velocity_observation()]
173+
)
174+
corrected = self.loop.xhat()
175+
176+
self.loop.predict(0.020)
177+
178+
# Use in-place subtraction to avoid creating a new array.
179+
# `corrected` is no longer used after this point.
180+
innovation = corrected
181+
innovation -= predicted
182+
self.innovation = innovation
183+
184+
# constrain ourselves if we are going to do damage
185+
if (
186+
self.position() > IntakeComponent.RETRACTED_ANGLE
187+
or self.position() < IntakeComponent.DEPLOYED_ANGLE_LOWER
188+
):
189+
self.loop.reset([self.position_observation(), self.velocity_observation()])
190+
191+
@feedback
192+
def next_setpoint(self) -> tuple[float, float]:
193+
return (self.tracked_state.position, self.tracked_state.velocity)
194+
195+
@feedback
196+
def filter_innovation(self) -> tuple[float, float]:
197+
return (self.innovation[0], self.innovation[1])
198+
110199
def _force_retract(self):
111200
self.desired_state = TrapezoidProfile.State(
112201
IntakeComponent.RETRACTED_ANGLE, 0.0
@@ -123,19 +212,20 @@ def execute(self) -> None:
123212

124213
self.desired_output = 0.0
125214

126-
tracked_state = self.motion_profile.calculate(
215+
self.tracked_state = self.motion_profile.calculate(
127216
wpilib.Timer.getFPGATimestamp() - self.last_setpoint_update_time,
128217
self.initial_state,
129218
self.desired_state,
130219
)
131-
ff = self.arm_ff.calculate(tracked_state.position, tracked_state.velocity)
220+
221+
self.loop.setNextR([self.tracked_state.position, self.tracked_state.velocity])
222+
223+
self.correct_and_predict()
132224

133225
if not math.isclose(
134226
self.desired_state.position, self.position(), abs_tol=math.radians(5)
135227
):
136-
self.arm_motor.setVoltage(
137-
self.pid.calculate(self.position(), tracked_state.position) + ff
138-
)
228+
self.arm_motor.setVoltage(self.loop.U(0))
139229
else:
140230
self.arm_motor.setVoltage(0.0)
141231

physics.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ def __init__(self, physics_controller: PhysicsInterface, robot: MyRobot):
183183
SingleJointedArmSim(
184184
intake_arm_gearbox,
185185
IntakeComponent.gear_ratio,
186-
moi=0.035579622,
186+
moi=IntakeComponent.ARM_MOI,
187187
armLength=0.22,
188188
minAngle=IntakeComponent.DEPLOYED_ANGLE_LOWER,
189189
maxAngle=IntakeComponent.RETRACTED_ANGLE,

robot.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,8 @@ def disabledPeriodic(self) -> None:
368368
self.chassis.update_alliance()
369369
self.chassis.update_odometry()
370370

371+
self.intake_component.correct_and_predict()
372+
371373
self.starboard_vision.execute()
372374
self.port_vision.execute()
373375

utilities/state_space.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import numpy as np
2+
from wpimath import units
3+
from wpimath.system import LinearSystem_2_1_1
4+
from wpimath.system.plant import DCMotor
5+
6+
7+
def single_jointed_arm_system(
8+
motor: DCMotor, J: units.kilogram_square_meters, gearing: float
9+
) -> LinearSystem_2_1_1:
10+
A = np.array(
11+
[[0.0, 1.0], [0.0, -(gearing**2) * motor.Kt / (motor.Kv * motor.R * J)]]
12+
)
13+
B = np.array([[0.0], [gearing * motor.Kt / (motor.R * J)]])
14+
C = np.array([1.0, 0.0])
15+
D = np.array([0.0])
16+
17+
return LinearSystem_2_1_1(A, B, C, D)

0 commit comments

Comments
 (0)