amarcolini / joos

A comprehensive kotlin library designed for FTC.
MIT License
8 stars 2 forks source link

Custom Drive #4

Closed omagarwal25 closed 2 years ago

omagarwal25 commented 2 years ago

How would I go about implementing a custom drivetrain? We have a differential 2 moduled swerve and I was wondering how to create it so that I can use all the odometry and kinematics.

amarcolini commented 2 years ago

Joos does not currently have support for differential swerve out of the box, so you would have to write the odometry and kinematics yourself. It might end up looking something like this:

/**
 * @param trackWidth the distance between the two modules
 */
class DifferentialSwerveDrive(
    private val leftMotorA: Motor, private val leftMotorB: Motor,
    private val rightMotorA: Motor, private val rightMotorB: Motor,
    private val trackWidth: Double,
    private val imu: Imu? = null
) : Drive(), Component {
    /* 
       Note that a motor A corresponds to the motor that spins the same direction as the wheel, 
       and motor B spins the opposite direction.
    */

    /**
     * The gear ratio from one of the motors to the wheel shaft.
     */
    val gearRatio: Double = 1.0

    /**
     * The ticks per rev of each motor.
     */
    val ticksPerRev: Double = 100.0

    /**
     * The radius of each wheel.
     */
    val wheelRadius: Double = 2.0

    /**
     * This is for controlling the orientation of each module. You'll have to tune this.
     */
    val modulePID = PIDCoefficients(4.0, 0.0, 0.1)

    override var localizer: Localizer = DifferentialSwerveLocalizer(
        ::getWheelPositions, ::getWheelVelocities, ::getModuleOrientations,
        trackWidth, this, imu != null
    )

    override val rawExternalHeading: Double = imu.heading ?: 0.0

    private val leftModuleController = PIDFController(modulePID)
    private val rightModuleController = PIDFController(modulePID)

    private val motors = listOf(leftMotorA, leftMotorB, rightMotorA, rightMotorB)

    init {
        leftModuleController.setInputBounds(-PI, PI)
        rightModuleController.setInputBounds(-PI, PI)

        motors.forEach {
            it.distancePerRev = 2 * PI * wheelRadius * gearRatio
            //TODO: tune feedforward for motors
            it.feedforwardCoefficients == FeedforwardCoefficients()
        }
    }

    private var leftVel = 0.0
    private var leftAccel = 0.0
    private var rightVel = 0.0
    private var rightAccel = 0.0

    private fun getWheelPositions(): Pair<Double, Double> =
        (leftMotorA.distance - leftMotorB.distance) * 0.5 to (rightMotorA.distance - rightMotorB.distance) * 0.5

    private fun getWheelVelocities(): Pair<Double, Double> =
        (leftMotorA.distanceVelocity - leftMotorB.distanceVelocity) * 0.5 to (rightMotorA.distanceVelocity - rightMotorB.distanceVelocity) * 0.5

    private fun getModuleOrientations(): Pair<Double, Double> =
        (leftMotorA.currentPosition + leftMotorB.currentPosition) / ticksPerRev * PI to (rightMotorA.currentPosition + rightMotorB) / ticksPerRev * PI

    override fun setDrivePower(drivePower: Pose2d) {
        val leftVector = Vector2d(drivePower.x, drivePower.y + drivePower.heading)
        val rightVector = Vector2d(drivePower.x, drivePower.y - drivePower.heading)

        leftModuleController.setTarget(leftVector.angle())
        rightModuleController.setTarget(rightVector.angle())

        leftVel = leftVector.norm() * leftMotorA.maxRPM
        leftAccel = 0.0
        rightVel = rightVector.norm() * rightMotorA.maxRPM
        rightAccel = 0.0
    }

    override fun update() {
        val moduleOrientations = getModuleOrientations()
        val leftControl = leftModuleController.update(
            moduleOrientations.first
        )
        leftMotorA.setSpeed(leftVel + leftControl, leftAccel)
        leftMotorB.setSpeed(-leftVel + leftControl, -leftAccel)

        val rightControl = rightModuleController.update(
            moduleOrientations.second
        )
        rightMotorA.setSpeed(rightVel + rightControl, rightAccel)
        rightMotorB.setSpeed(-rightVel + rightControl, -rightAccel)

        motors.forEach { it.update() }
    }

    override fun setDriveSignal(driveSignal: DriveSignal) {
        val leftVelVector = Vector2d(driveSignal.vel.x, driveSignal.vel.y + driveSignal.vel.heading)
        val rightVelVector = Vector2d(driveSignal.vel.x, driveSignal.vel.y - driveSignal.vel.heading)

        val leftAccelVector = Vector2d(driveSignal.accel.x, driveSignal.accel.y + driveSignal.accel.heading)
        val rightAccelVector = Vector2d(driveSignal.accel.x, driveSignal.accel.y - driveSignal.accel.heading)

        leftModuleController.setTarget(leftVelVector.angle())
        rightModuleController.setTarget(rightVelVector.angle())

        val divisor = (2 * PI * wheelRadius * gearRatio)
        leftVel = leftVelVector.norm() / divisor
        leftAccel = leftAccelVector.norm() / divisor
        rightVel = rightVelVector.norm() / divisor
        rightAccel = rightAccelVector.norm() / divisor
    }

    override fun getExternalHeadingVelocity(): Double? = imu?.headingVelocity
}

class DifferentialSwerveLocalizer(
    private val wheelPositions: () -> Pair<Double, Double>,
    private val wheelVelocities: () -> Pair<Double, Double>? = { null },
    private val moduleOrientations: () -> Pair<Double, Double>,
    private val trackWidth: Double,
    private val drive: Drive,
    private val useExternalHeading: Boolean = true
) : Localizer {
    private var _poseEstimate: Pose2d = Pose2d()
    override var poseEstimate: Pose2d
        get() = _poseEstimate
        set(value) {
            lastWheelPositions = null
            lastExtHeading = null
            if (useExternalHeading) drive.externalHeading = value.heading
            _poseEstimate = value
        }

    private var lastWheelPositions: Pair<Double, Double>? = null
    private var lastExtHeading: Double? = null

    override var poseVelocity: Pose2d? = null
        private set

    override fun update() {
        val wheelPositions = wheelPositions()
        val moduleOrientations = moduleOrientations()
        val externalHeading = if (useExternalHeading) drive.externalHeading else null
        lastWheelPositions?.let { lastWheelPositions ->
            val leftDelta = wheelPositions.first - lastWheelPositions.first
            val rightDelta = wheelPositions.second - lastWheelPositions.second
            val robotPoseDelta = Pose2d(
                (leftDelta * cos(moduleOrientations.first) + rightDelta * cos(moduleOrientations.second)) * 0.5,
                (leftDelta * sin(moduleOrientations.first) + rightDelta * sin(moduleOrientations.second)) * 0.5,
                lastExtHeading?.let { externalHeading?.minus(it) }
                    ?: ((rightDelta * sin(moduleOrientations.second) - leftDelta * sin(moduleOrientations.first)) / trackWidth)
            )
            _poseEstimate = Kinematics.relativeOdometryUpdate(
                _poseEstimate, robotPoseDelta
            )
        }

        val wheelVelocities = wheelVelocities()
        val extHeadingVel = if (useExternalHeading) drive.getExternalHeadingVelocity() else null
        poseVelocity =
            if (wheelVelocities != null)
                Pose2d(
                    (wheelVelocities.first * cos(moduleOrientations.first) + wheelVelocities.second * cos(
                        moduleOrientations.second
                    )) * 0.5,
                    (wheelVelocities.first * sin(moduleOrientations.first) + wheelVelocities.second * sin(
                        moduleOrientations.second
                    )) * 0.5,
                    extHeadingVel
                        ?: ((wheelVelocities.second * sin(moduleOrientations.second) - wheelVelocities.first * sin(
                            moduleOrientations.first
                        )) / trackWidth)
                )
            else null

        lastWheelPositions = wheelPositions
        lastExtHeading = externalHeading
    }
}

Note that this may or may not work, but should give you an idea of what to do.

omagarwal25 commented 2 years ago

Hey, I appreciate you taking the time to write this amazing code. Although I have been programming for a long time, concepts such as kinematics and odometry are completely new to me. Our team is a rookie FTC team (we technically participated in 2020 but that was remote.) but we have seasoned FRC veterans. So as a challenge we decided to build a Differential Swerve. This is some awesome stuff. We use regular swerve for our FRC robot and the code for that so much easier. I wish you and your team good luck from here.

FTC is mainly a post-season project for us as our season is pretty much over as we got knocked our last weekend in Orlando (We were on Alliance Two). If your team ever does FRC do let me know, I can put you in contact with the team. FTC is actually relatively new to the UK, so it's been a fun challenge.

PS: I see that you team is called the Griffins and that's a fun coincidence as our FRC team (1884) is also called the Griffins

omagarwal25 commented 2 years ago

Also @amarcolini am I meant to be using the GUI package yet? Or do I just use the FTC dashboard? I don't need to do any config for that right? I should just be able to open it up in my browser?

amarcolini commented 2 years ago

The GUI does not currently work properly due to some radians/degrees thing, but after the next release, it should be good to go. If you're familiar with MeepMeep, it works very similar to that. I'll provide some instructions with the next release as well.

As for the kinematics and odometry, there's a great explanation here. It's what I used to derive the differential swerve kinematics.

omagarwal25 commented 2 years ago

What I meant with the GUI is whenever I uncomment the dependency for the GUI I get a ton of duplicate class errors.

amarcolini commented 2 years ago

I'm not sure what would have caused that. How are you importing it?

omagarwal25 commented 2 years ago

I add it to build.dependencies.gradle. Here is what the file looks like

It builds fine without the GUI, but as soon as I add it back in I start to get these errors on build.

Duplicate class javafx.util.converter.DefaultStringConverter found in the following modules: jetified-javafx-base-11.0-linux.2-linux (org.openjfx:javafx-base:11.0.2), jetified-javafx-base-11.0-mac.2-mac (org.openjfx:javafx-base:11.0.2) and jetified-javafx-base-11.0-win.2-win (org.openjfx:javafx-base:11.0.2) Duplicate class javafx.util.converter.DoubleStringConverter found in the following modules: jetified-javafx-base-11.0-linux.2-linux (org.openjfx:javafx-base:11.0.2), jetified-javafx-base-11.0-mac.2-mac (org.openjfx:javafx-base:11.0.2) and jetified-javafx-base-11.0-win.2-win (org.openjfx:javafx-base:11.0.2) Duplicate class javafx.util.converter.FloatStringConverter found in the following modules: jetified-javafx-base-11.0-linux.2-linux (org.openjfx:javafx-base:11.0.2), jetified-javafx-base-11.0-mac.2-mac (org.openjfx:javafx-base:11.0.2) and jetified-javafx-base-11.0-win.2-win (org.openjfx:javafx-base:11.0.2) Duplicate class javafx.util.converter.FormatStringConverter found in the following modules: jetified-javafx-base-11.0-linux.2-linux (org.openjfx:javafx-base:11.0.2), jetified-javafx-base-11.0-mac.2-mac (org.openjfx:javafx-base:11.0.2) and jetified-javafx-base-11.0-win.2-win (org.openjfx:javafx-base:11.0.2) Duplicate class javafx.util.converter.IntegerStringConverter found in the following modules: jetified-javafx-base-11.0-linux.2-linux (org.openjfx:javafx-base:11.0.2), jetified-javafx-base-11.0-mac.2-mac (org.openjfx:javafx-base:11.0.2) and jetified-javafx-base-11.0-win.2-win (org.openjfx:javafx-base:11.0.2) Duplicate class javafx.util.converter.LocalDateStringConverter found in the following modules: jetified-javafx-base-11.0-linux.2-linux (org.openjfx:javafx-base:11.0.2), jetified-javafx-base-11.0-mac.2-mac (org.openjfx:javafx-base:11.0.2) and jetified-javafx-base-11.0-win.2-win (org.openjfx:javafx-base:11.0.2) Duplicate class javafx.util.converter.LocalDateTimeStringConverter found in the following modules: jetified-javafx-base-11.0-linux.2-linux (org.openjfx:javafx-base:11.0.2), jetified-javafx-base-11.0-mac.2-mac (org.openjfx:javafx-base:11.0.2) and jetified-javafx-base-11.0-win.2-win (org.openjfx:javafx-base:11.0.2) Duplicate class javafx.util.converter.LocalDateTimeStringConverter$LdtConverter found in the following modules: jetified-javafx-base-11.0-linux.2-linux (org.openjfx:javafx-base:11.0.2), jetified-javafx-base-11.0-mac.2-mac (org.openjfx:javafx-base:11.0.2) and jetified-javafx-base-11.0-win.2-win (org.openjfx:javafx-base:11.0.2) Duplicate class javafx.util.converter.LocalTimeStringConverter found in the following modules: jetified-javafx-base-11.0-linux.2-linux (org.openjfx:javafx-base:11.0.2), jetified-javafx-base-11.0-mac.2-mac (org.openjfx:javafx-base:11.0.2) and jetified-javafx-base-11.0-win.2-win (org.openjfx:javafx-base:11.0.2) Duplicate class javafx.util.converter.LongStringConverter found in the following modules: jetified-javafx-base-11.0-linux.2-linux (org.openjfx:javafx-base:11.0.2), jetified-javafx-base-11.0-mac.2-mac (org.openjfx:javafx-base:11.0.2) and jetified-javafx-base-11.0-win.2-win (org.openjfx:javafx-base:11.0.2) Duplicate class javafx.util.converter.NumberStringConverter found in the following modules: jetified-javafx-base-11.0-linux.2-linux (org.openjfx:javafx-base:11.0.2), jetified-javafx-base-11.0-mac.2-mac (org.openjfx:javafx-base:11.0.2) and jetified-javafx-base-11.0-win.2-win (org.openjfx:javafx-base:11.0.2) Duplicate class javafx.util.converter.PercentageStringConverter found in the following modules: jetified-javafx-base-11.0-linux.2-linux (org.openjfx:javafx-base:11.0.2), jetified-javafx-base-11.0-mac.2-mac (org.openjfx:javafx-base:11.0.2) and jetified-javafx-base-11.0-win.2-win (org.openjfx:javafx-base:11.0.2) Duplicate class javafx.util.converter.ShortStringConverter found in the following modules: jetified-javafx-base-11.0-linux.2-linux (org.openjfx:javafx-base:11.0.2), jetified-javafx-base-11.0-mac.2-mac (org.openjfx:javafx-base:11.0.2) and jetified-javafx-base-11.0-win.2-win (org.openjfx:javafx-base:11.0.2) Duplicate class javafx.util.converter.TimeStringConverter found in the following modules: jetified-javafx-base-11.0-linux.2-linux (org.openjfx:javafx-base:11.0.2), jetified-javafx-base-11.0-mac.2-mac (org.openjfx:javafx-base:11.0.2) and jetified-javafx-base-11.0-win.2-win (org.openjfx:javafx-base:11.0.2)

amarcolini commented 2 years ago

I think I've found the problem. It has to do with one of the GUI's dependencies. It'll hopefully be cleared up in the next release.