class SwerveModule(object): def __init__(self, name, steer_id, drive_id): """ Performs calculations and bookkeeping for a single swerve module. Args: name (str): A NetworkTables-friendly name for this swerve module. Used for saving and loading configuration data. steer_id (number): The CAN ID for the Talon SRX controlling this module's steering. drive_id (number): The CAN ID for the Talon SRX controlling this module's driving. Attributes: steer_talon (:class:`ctre.cantalon.CANTalon`): The Talon SRX used to actuate this module's steering. drive_talon (:class:`ctre.cantalon.CANTalon`): The Talon SRX used to actuate this module's drive. steer_target (number): The current target steering position for this module, in radians. steer_offset (number): The swerve module's steering zero position. This value can be determined by manually steering a swerve module so that it faces forwards relative to the chassis, and by taking the raw encoder position value (ADC reading); this value is the steer offset. drive_reversed (boolean): Whether or not the drive motor's output is currently reversed. """ self.steer_talon = TalonSRX(steer_id) self.drive_talon = TalonSRX(drive_id) # Configure steering motors to use abs. encoders # and closed-loop control self.steer_talon.configSelectedFeedbackSensor(FeedbackDevice.Analog, 0, 0) # noqa: E501 self.steer_talon.selectProfileSlot(0, 0) self.steer_talon.configAllowableClosedloopError( 0, math.ceil(_acceptable_steer_err), 0) self.drive_talon.configSelectedFeedbackSensor( FeedbackDevice.QuadEncoder, 0, 0) # noqa: E501 self.drive_talon.setQuadraturePosition(0, 0) self.name = name self.steer_target = 0 self.steer_target_native = 0 self.drive_temp_flipped = False self.max_speed = 470 # ticks / 100ms self.max_observed_speed = 0 self.raw_drive_speeds = [] self.load_config_values() def load_config_values(self): """ Load saved configuration values for this module via WPILib's Preferences interface. The key names are derived from the name passed to the constructor. """ self.steer_talon.selectProfileSlot(0, 0) preferences = wpilib.Preferences.getInstance() self.max_speed = preferences.getFloat(self.name + '-Max Wheel Speed', 370) sensor_phase = preferences.getBoolean(self.name + '-Sensor Reverse', False) self.drive_talon.setSensorPhase(sensor_phase) self.steer_offset = preferences.getFloat(self.name + '-offset', 0) if _apply_range_hack: self.steer_min = preferences.getFloat(self.name + '-min', 0) self.steer_max = preferences.getFloat(self.name + '-max', 1024) actual_steer_range = int(self.steer_max - self.steer_min) self.steer_min += int(actual_steer_range * 0.01) self.steer_max -= int(actual_steer_range * 0.01) self.steer_offset -= self.steer_min self.steer_range = int(self.steer_max - self.steer_min) else: self.steer_min = 0 self.steer_max = 1024 self.steer_range = 1024 self.drive_reversed = preferences.getBoolean(self.name + '-reversed', False) self.steer_reversed = preferences.getBoolean( self.name + '-steer-reversed', False) def save_config_values(self): """ Save configuration values for this module via WPILib's Preferences interface. """ preferences = wpilib.Preferences.getInstance() preferences.putFloat(self.name + '-offset', self.steer_offset) preferences.putBoolean(self.name + '-reversed', self.drive_reversed) if _apply_range_hack: preferences.putFloat(self.name + '-min', self.steer_min) preferences.putFloat(self.name + '-max', self.steer_max) def get_steer_angle(self): """ Get the current angular position of the swerve module in radians. """ native_units = self.steer_talon.getSelectedSensorPosition(0) native_units -= self.steer_offset # Position in rotations rotation_pos = native_units / self.steer_range return rotation_pos * 2 * math.pi def set_steer_angle(self, angle_radians): """ Steer the swerve module to the given angle in radians. `angle_radians` should be within :math:`[-2\\pi, 2\\pi]`. This method attempts to find the shortest path to the given steering angle; thus, it may in actuality servo to the position opposite the passed angle and reverse the drive direction. Args: angle_radians (number): The angle to steer towards in radians, where 0 points in the chassis forward direction. """ n_rotations = math.trunc( (self.steer_talon.getSelectedSensorPosition(0) - self.steer_offset) / self.steer_range) current_angle = self.get_steer_angle() adjusted_target = angle_radians + (n_rotations * 2 * math.pi) # Perform angle unwrapping for target angles. # This prevents issues resulting from discontinuities in input angle # ranges. if abs(adjusted_target - current_angle) - math.pi > 0.0005: if adjusted_target > current_angle: adjusted_target -= 2 * math.pi else: adjusted_target += 2 * math.pi # Shortest-path servoing should_reverse_drive = False if abs(adjusted_target - current_angle) - (math.pi / 2) > 0.0005: # shortest path is to move to opposite angle and reverse drive dir if adjusted_target > current_angle: adjusted_target -= math.pi else: # angle_radians < local_angle adjusted_target += math.pi should_reverse_drive = True self.steer_target = adjusted_target # Compute and send actual target to motor controller native_units = (self.steer_target * 512 / math.pi) + self.steer_offset self.steer_talon.set(ControlMode.Position, native_units) self.drive_temp_flipped = should_reverse_drive def set_drive_speed(self, speed, direct=False): """ Drive the swerve module wheels at a given percentage of maximum power or speed. Args: percent_speed (number): The speed to drive the module at, expressed as a percentage of maximum speed. Negative values drive in reverse. """ if self.drive_reversed: speed *= -1 if self.drive_temp_flipped: speed *= -1 self.drive_talon.selectProfileSlot(1, 0) self.drive_talon.config_kF(0, 1023 / self.max_speed, 0) if direct: self.drive_talon.set(ControlMode.Velocity, speed) else: self.drive_talon.set(ControlMode.Velocity, speed * self.max_speed) def set_drive_distance(self, ticks): if self.drive_reversed: ticks *= -1 if self.drive_temp_flipped: ticks *= -1 self.drive_talon.selectProfileSlot(0, 0) self.drive_talon.set(ControlMode.Position, ticks) def reset_drive_position(self): self.drive_talon.setQuadraturePosition(0, 0) def apply_control_values(self, angle_radians, speed, direct=False): """ Set a steering angle and a drive speed simultaneously. Args: angle_radians (number): The desired angle to steer towards. percent_speed (number): The desired percentage speed to drive at. See Also: :func:`~set_drive_speed` and :func:`~set_steer_angle` """ self.set_steer_angle(angle_radians) self.set_drive_speed(speed, direct) def update_smart_dashboard(self): """ Push various pieces of info to the Smart Dashboard. This method calls to NetworkTables (eventually), thus it may be _slow_. As of right now, this displays the current raw absolute encoder reading from the steer Talon, and the current target steer position. """ wpilib.SmartDashboard.putNumber( self.name + ' Position', (self.steer_talon.getSelectedSensorPosition(0) - self.steer_offset) * 180 / 512) wpilib.SmartDashboard.putNumber(self.name + ' ADC', self.steer_talon.getAnalogInRaw()) wpilib.SmartDashboard.putNumber(self.name + ' Target', self.steer_target * 180 / math.pi) wpilib.SmartDashboard.putNumber(self.name + ' Steer Error', self.steer_talon.getClosedLoopError(0)) wpilib.SmartDashboard.putNumber(self.name + ' Drive Error', self.drive_talon.getClosedLoopError(0)) wpilib.SmartDashboard.putNumber( self.name + ' Drive Ticks', self.drive_talon.getQuadraturePosition()) self.raw_drive_speeds.append(self.drive_talon.getQuadratureVelocity()) if len(self.raw_drive_speeds) > 50: self.raw_drive_speeds = self.raw_drive_speeds[-50:] self.cur_drive_spd = np.mean(self.raw_drive_speeds) if abs(self.cur_drive_spd) > abs(self.max_observed_speed): self.max_observed_speed = self.cur_drive_spd wpilib.SmartDashboard.putNumber(self.name + ' Drive Velocity', self.cur_drive_spd) wpilib.SmartDashboard.putNumber(self.name + ' Drive Velocity (Max)', self.max_observed_speed) if wpilib.RobotBase.isReal(): wpilib.SmartDashboard.putNumber( self.name + ' Drive Percent Output', self.drive_talon.getMotorOutputPercent()) wpilib.SmartDashboard.putNumber( self.name + ' Drive Current', self.drive_talon.getOutputCurrent()) wpilib.SmartDashboard.putNumber( self.name + ' Steer Current', self.steer_talon.getOutputCurrent())
class Elevator(FaultableSystem): def __init__(self, mock=False): super().__init__("Elevator") self.talon_master = TalonSRX(4) self.talon_slave = TalonSRX(5) self._state = ElevatorState.HOLDING self.nlogger = Logger("elevator") self.nlogger.add("Time", robot_time.delta_time) self.nlogger.add("Position", self.get_elevator_position) self.nlogger.add("Voltage", self.talon_master.getMotorOutputVoltage) self.nlogger.add("Current", self.talon_master.getOutputCurrent) # self.nlogger.start() dashboard2.add_graph("Elevator Position", self.get_elevator_position) dashboard2.add_graph("Elevator Voltage", self.talon_master.getMotorOutputVoltage) dashboard2.add_graph("Elevator Current", self.talon_master.getOutputCurrent) dashboard2.add_graph("Elevator Current2", self.talon_slave.getOutputCurrent) dashboard2.add_graph("Elevator State", lambda: self._state) if not mock: self.mp_manager = SRXMotionProfileManager(self.talon_master, 1000 // FREQUENCY) self.talon_master.setQuadraturePosition(0, 0) self.talon_slave.follow(self.talon_master) # 1023 units per 12V # This lets us pass in feedforward as voltage self.talon_master.config_kF(MAIN_IDX, 1023/12, 0) self.talon_master.config_kF(EXTENT_IDX, 1023/12, 0) kP = 0.05 * 1023 / 4096 # self.talon_master.config_kP(MAIN_IDX, kP, 0) # self.talon_master.config_kP(EXTENT_IDX, kP, 0) self.talon_master.config_kP(HOLD_MAIN_IDX, .1 * 1023 / 4096, 0) self.talon_master.config_kP(HOLD_EXTENT_IDX, .3 * 1023 / 4096, 0) self.talon_master.configSelectedFeedbackSensor(FeedbackDevice.CTRE_MagEncoder_Relative, 0, 0) self.talon_master.setSensorPhase(True) invert = False self.talon_master.setInverted(invert) self.talon_slave.setInverted(invert) def init_profile(self, new_pos): if self._state != ElevatorState.HOLDING: return False profile, _ = self.gen_profile(self.get_elevator_position(), new_pos) self.mp_manager.init_profile(profile) self.talon_master.configSelectedFeedbackSensor(FeedbackDevice.CTRE_MagEncoder_Relative, 0, 0) return True def start_profile(self): self._state = ElevatorState.MOVING return self.mp_manager.start_profile() def has_finished_profile(self): return self.mp_manager.is_done() def finish_profile(self): self._state = ElevatorState.HOLDING self.hold() def set_power(self, power): self._state = ElevatorState.MANUAL self.talon_master.set(ControlMode.PercentOutput, power) def get_mass(self, pos): """ Mass in lbm :param pos: :return: """ if pos <= CARRIAGE_TRAVEL: return CARRIAGE_WEIGHT return CARRIAGE_WEIGHT + EXTENT_WEIGHT def get_hold_torque(self, pos): """ Torque in lb-in :param pos: :return: """ return self.get_mass(pos) * SPOOL_RADIUS def get_pid_index(self, pos): if pos <= CARRIAGE_TRAVEL: return MAIN_IDX return EXTENT_IDX def calc_ff(self, pos, vel, acc): """ Returns feedforward in volts :param pos: Position of the elevator, in inches :param vel: Desired velocity, in in/s :param acc: Desired acceleration, in in/s^2 :return: Feedforward voltage """ hold_voltage = 12 * self.get_hold_torque(pos) / self.get_stall_torque() vel_maxv = 12 - hold_voltage vel_voltage = vel_maxv * vel / (self.get_max_speed() * (vel_maxv / 12)) acc_maxv = 12 - hold_voltage - vel_voltage acc_voltage = 0 return hold_voltage + vel_voltage + acc_voltage def gen_profile(self, start, end) -> Tuple[List[TalonPoint], int]: # if not 0 <= end <= TOP_EXTENT: # raise ValueError(f"End must be within 0 and {TOP_EXTENT}") talon_points = [] rawmp = MotionProfile(start=start, end=end, cruise_speed=CRUISE_SPEED, acc=ACC, frequency=FREQUENCY) for point in rawmp: last = point == rawmp[-1] talonpt = TalonPoint(position=-self.in_to_native_units(point.position), velocity=self.calc_ff(point.position, point.velocity, point.acc), headingDeg=0, profileSlotSelect0=self.get_pid_index(point.position), profileSlotSelect1=0, isLastPoint=last, zeroPos=False, timeDur=0) talon_points.append(talonpt) return talon_points, FREQUENCY def get_elevator_position(self): """ :return: The elevator's position as measured by the encoder, in inches. 0 is bottom, 70 is top """ return -self.native_to_inches(self.talon_master.getQuadraturePosition()) def get_stall_torque(self): """ Motor stall torque (N-m) * # of motors * gear ratio * 8.851 lb-in/N-m :return: 12V stall torque in lb-in """ return 0.71 * 2 * GEAR_RATIO * 8.851 def get_max_speed(self): """ Motor free speed (rpm) * (1/60) seconds/minute / gear ratio 2*pi*r * RPS = IPS :return: 12V speed of the elevator in in/s """ return 2*math.pi*SPOOL_RADIUS*(18730 * (1/60) / GEAR_RATIO) def in_to_native_units(self, inches): return (inches / (2*math.pi*SPOOL_RADIUS)) * 4096 / TRAVEL_RATIO def native_to_inches(self, native_distance): return (native_distance / 4096) * (2*math.pi*SPOOL_RADIUS) * TRAVEL_RATIO def start_zero_position(self): self.talon_master.selectProfileSlot(MAIN_IDX, 0) self.talon_master.configSelectedFeedbackSensor(FeedbackDevice.CTRE_MagEncoder_Absolute, 0, 0) self.talon_master.set(ControlMode.Position, ZERO_POS) self._state = ElevatorState.ZEROING def is_done_zeroing(self): return self._state == ElevatorState.ZEROING and self.talon_master.getClosedLoopError(0) < ZERO_MAX_ERR def finish_zero_position(self): self._state = ElevatorState.HOLDING self.talon_master.setQuadraturePosition(0, 0) def hold(self): pos = self.get_elevator_position() target = self.in_to_native_units(pos) pid = self.get_pid_index(pos) + 2 self.talon_master.selectProfileSlot(pid, 0) ff = (1023/12) * self.calc_ff(pos, 0, 0) if target == 0: ff = 0 else: ff /= target self.talon_master.config_kF(pid, ff, 0) self.talon_master.set(ControlMode.Position, target) self._state = ElevatorState.HOLDING def move_to(self, target): pos = self.get_elevator_position() target_n = self.in_to_native_units(target) pid = self.get_pid_index(pos) + 2 self.talon_master.selectProfileSlot(pid, 0) ff = (1023/12) * self.calc_ff(pos, 0, 0) if target_n == 0: ff = 0 else: ff /= target_n self.talon_master.config_kF(pid, ff, 0) self.talon_master.set(ControlMode.Position, target_n) self._state = ElevatorState.HOLDING def is_at_top(self): return self.talon_master.isFwdLimitSwitchClosed() def is_at_bottom(self): return self.talon_master.isRevLimitSwitchClosed() def check_continuous_faults(self): """ Check if the system is faulted. For the elevator, we are interested in whether the versa dual-input has failed. :return: """ master_current = self.talon_master.getOutputCurrent() slave_current = self.talon_slave.getOutputCurrent() ref = master_current if ref == 0: ref = slave_current if ref == 0: ref = 1 eps = 1e-1 return (master_current > eps or slave_current > eps) and abs(master_current - slave_current) / ref < 1