11import math
22
3+ import numpy as np
34import wpilib
45from magicbot import feedback , tunable
56from phoenix5 import ControlMode , TalonSRX
89 SparkMaxConfig ,
910)
1011from 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
1218from wpimath .trajectory import TrapezoidProfile
1319
1420from 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¤tLimit=%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
0 commit comments