Created
January 30, 2025 09:28
-
-
Save neilmehra/4ddef174bb8b0f71626c539e5ae178d0 to your computer and use it in GitHub Desktop.
FTC Lift Code
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package com.asiankoala.koawalib.hardware.motor | |
| import com.asiankoala.koawalib.logger.Logger | |
| import com.asiankoala.koawalib.math.epsilonEquals | |
| import com.asiankoala.koawalib.util.Clock | |
| import com.qualcomm.robotcore.util.MovingStatistics | |
| import kotlin.math.abs | |
| import kotlin.math.max | |
| import kotlin.math.sign | |
| // @Suppress("unused") | |
| class KEncoder internal constructor( | |
| private val motor: KMotor, | |
| private val ticksPerUnit: Double, | |
| private val isRevEncoder: Boolean | |
| ) { | |
| private var offset = 0.0 | |
| private var multiplier = 1.0 | |
| private var _pos = 0.0 | |
| private var _vel = 0.0 | |
| private val prevPos = ArrayList<Pair<Double, Double>>() | |
| private val prevVel = ArrayList<Pair<Double, Double>>() | |
| private val velStats = MovingStatistics(LOOK_BEHIND) | |
| val pos get() = (_pos + offset) / ticksPerUnit | |
| val vel get() = velStats.mean / ticksPerUnit | |
| val delta get() = ( | |
| prevPos[prevPos.size - 1].second - | |
| prevPos[prevPos.size - 2].second | |
| ) / ticksPerUnit | |
| val reverse: KEncoder | |
| get() { | |
| multiplier *= -1.0 | |
| Logger.logInfo("encoder associated with ${motor.deviceName} reversed") | |
| return this | |
| } | |
| private fun estimateDerivative(): Pair<Double, Boolean> { | |
| if (prevPos.size < 2) return Pair(0.0, false) | |
| val oldIndex = max(0, prevPos.size - 2) | |
| val oldPosition = prevPos[oldIndex] | |
| val currPosition = prevPos.last() | |
| val dt = (currPosition.first - oldPosition.first) | |
| if (dt epsilonEquals 0.0) throw Exception("failed derivative") | |
| return Pair((currPosition.second - oldPosition.second) / dt, true) | |
| } | |
| private fun attemptVelUpdate() { | |
| val ret = estimateDerivative() | |
| if (!ret.second) return | |
| var estimatedVel = ret.first | |
| if (isRevEncoder) { | |
| estimatedVel = inverseOverflow(motor.rawMotorVelocity * multiplier, _vel) | |
| } | |
| velStats.add(estimatedVel) | |
| } | |
| private fun internalReset() { | |
| _pos = motor.rawMotorPosition * multiplier | |
| prevPos.clear() | |
| prevPos.add(Pair(Clock.seconds, _pos)) | |
| prevPos.add(Pair(Clock.seconds - 1e6, _pos)) | |
| prevVel.clear() | |
| velStats.clear() | |
| _vel = 0.0 | |
| } | |
| fun update() { | |
| val seconds = Clock.seconds | |
| _pos = motor.rawMotorPosition * multiplier | |
| prevPos.add(Pair(seconds, _pos)) | |
| attemptVelUpdate() | |
| prevVel.add(Pair(seconds, _vel)) | |
| } | |
| fun zero(newPosition: Double = 0.0): KEncoder { | |
| internalReset() | |
| offset = newPosition * ticksPerUnit - _pos | |
| return this | |
| } | |
| companion object { | |
| private const val LOOK_BEHIND = 5 | |
| private const val CPS_STEP = 0x10000 | |
| private fun inverseOverflow(input: Double, estimate: Double): Double { | |
| var real = input | |
| while (abs(estimate - real) > CPS_STEP / 2.0) { | |
| real += sign(estimate - real) * CPS_STEP | |
| } | |
| return real | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package com.asiankoala.koawalib.hardware.motor | |
| import com.asiankoala.koawalib.command.commands.LoopCmd | |
| import com.asiankoala.koawalib.control.motor.MotionProfileMotorController | |
| import com.asiankoala.koawalib.control.motor.MotorControlModes | |
| import com.asiankoala.koawalib.control.motor.MotorController | |
| import com.asiankoala.koawalib.control.motor.PositionMotorController | |
| import com.asiankoala.koawalib.control.profile.MotionState | |
| import com.asiankoala.koawalib.hardware.KDevice | |
| import com.asiankoala.koawalib.logger.Logger | |
| import com.asiankoala.koawalib.math.clamp | |
| import com.asiankoala.koawalib.math.d | |
| import com.asiankoala.koawalib.math.epsilonNotEqual | |
| import com.qualcomm.robotcore.hardware.DcMotor | |
| import com.qualcomm.robotcore.hardware.DcMotorEx | |
| import com.qualcomm.robotcore.hardware.DcMotorSimple | |
| import kotlin.math.absoluteValue | |
| import kotlin.math.sign | |
| // @Suppress("unused") | |
| class KMotor internal constructor(name: String) : KDevice<DcMotorEx>(name) { | |
| enum class Priority { HIGH, LOW } | |
| private val cmd = LoopCmd(this::update).withName("$name motor") | |
| private var powerMultiplier = 1.0 | |
| private var lastUpdateIter = 0 | |
| internal var controller: MotorController? = null | |
| var encoder: KEncoder? = null | |
| internal var mode = MotorControlModes.OPEN_LOOP | |
| internal var encoderCreated = false | |
| internal var isVoltageCorrected = false | |
| internal val rawMotorPosition get() = device.currentPosition.d | |
| internal val rawMotorVelocity get() = device.velocity | |
| internal var priority = Priority.HIGH | |
| internal var ks = 0.0 | |
| val pos: Double get() = encoder?.pos ?: throw Exception("queried motor $deviceName's pos without paired encoder") | |
| val vel: Double get() = encoder?.vel ?: throw Exception("queried motor $deviceName's vel without paired encoder") | |
| internal var zeroPowerBehavior: DcMotor.ZeroPowerBehavior = DcMotor.ZeroPowerBehavior.FLOAT | |
| set(value) { | |
| device.zeroPowerBehavior = value | |
| field = value | |
| } | |
| internal var direction: DcMotorSimple.Direction = DcMotorSimple.Direction.FORWARD | |
| set(value) { | |
| powerMultiplier = if (value == DcMotorSimple.Direction.FORWARD) { | |
| 1.0 | |
| } else { | |
| -1.0 | |
| } | |
| field = value | |
| } | |
| var power: Double = 0.0 | |
| set(value) { | |
| val clamped = clamp(value * (if (isVoltageCorrected) VOLTAGE_CONSTANT / lastVoltageRead else 1.0), -1.0, 1.0) * powerMultiplier | |
| if (clamped epsilonNotEqual field && | |
| (clamped == 0.0 || clamped.absoluteValue == 1.0 || (clamped - field).absoluteValue > 0.005) && | |
| (priority == Priority.HIGH || iter - lastUpdateIter > 3) | |
| ) { | |
| field = clamped | |
| device.power = clamped + clamped.sign * ks | |
| lastUpdateIter = iter | |
| } | |
| } | |
| get() = field * powerMultiplier | |
| val setpoint: MotionState get() { | |
| if (controller !is MotionProfileMotorController) throw Exception("controller not motion profile controller") | |
| return (controller as MotionProfileMotorController).setpoint | |
| } | |
| val targetState: MotionState get() { | |
| if (controller !is PositionMotorController) throw Exception("not position controller") | |
| return (controller as PositionMotorController).targetState | |
| } | |
| val currState: MotionState get() = controller?.currentState ?: throw Exception("fuck") | |
| private fun update() { | |
| encoder?.let { enc -> | |
| enc.update() | |
| controller?.let { | |
| it.currentState = MotionState(enc.pos, enc.vel) | |
| it.update() | |
| power = it.output | |
| Logger.put("updating motor controller for $deviceName") | |
| } | |
| } | |
| } | |
| fun setPositionTarget(x: Double) { | |
| controller?.setTargetPosition(x) ?: throw Exception("motor must be position controlled") | |
| Logger.logInfo("set motor $deviceName's position target to $x") | |
| } | |
| fun setVelocityTarget(v: Double) { | |
| controller?.setTargetVelocity(v) ?: throw Exception("motor must be velocity controlled") | |
| } | |
| fun setProfileTarget(x: Double) { | |
| controller?.setProfileTarget(x) ?: throw Exception("motor must be motion profiled") | |
| } | |
| fun isAtTarget() = controller?.isAtTarget() ?: throw Exception("motor must not be open loop") | |
| fun zero(pos: Double = 0.0) { | |
| encoder?.zero(pos) ?: throw Exception("motor must have paired encoder") | |
| } | |
| fun enable() { | |
| power = 0.0 | |
| cmd.schedule() | |
| } | |
| fun disable() { | |
| power = 0.0 | |
| cmd.cancel() | |
| } | |
| init { | |
| device.mode = DcMotor.RunMode.RUN_WITHOUT_ENCODER | |
| device.zeroPowerBehavior = DcMotor.ZeroPowerBehavior.FLOAT | |
| } | |
| companion object { | |
| private var iter = 0 | |
| private var VOLTAGE_CONSTANT = 12.0 | |
| internal var lastVoltageRead = Double.NaN | |
| internal fun updatePriorityIter() { | |
| iter++ | |
| } | |
| @JvmStatic | |
| fun setVoltageConstant(x: Double) { | |
| VOLTAGE_CONSTANT = x | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package asiankoala.ftc2022.sunmi.subsystems | |
| import asiankoala.ftc2022.sunmi.constants.LiftConstants | |
| import com.asiankoala.koawalib.control.controller.PIDGains | |
| import com.asiankoala.koawalib.control.motor.FFGains | |
| import com.asiankoala.koawalib.control.profile.MotionConstraints | |
| import com.asiankoala.koawalib.hardware.motor.EncoderFactory | |
| import com.asiankoala.koawalib.hardware.motor.KMotor | |
| import com.asiankoala.koawalib.hardware.motor.MotorFactory | |
| import com.asiankoala.koawalib.hardware.sensor.KLimitSwitch | |
| import com.asiankoala.koawalib.logger.Logger | |
| import com.asiankoala.koawalib.subsystem.KSubsystem | |
| class Lift(br: KMotor) : KSubsystem() { | |
| private val lt = MotorFactory("lt") | |
| .reverse | |
| .pairEncoder( | |
| br, | |
| EncoderFactory(367.0 / 12.0).reverse.zero() | |
| ) | |
| .withMotionProfileControl( | |
| PIDGains( | |
| LiftConstants.kP, | |
| LiftConstants.kI, | |
| LiftConstants.kD | |
| ), | |
| FFGains(kG = LiftConstants.kG), | |
| MotionConstraints(LiftConstants.vel, LiftConstants.accel, LiftConstants.deccel), | |
| 0.1, | |
| 0.0 | |
| ) | |
| .float | |
| .build() | |
| private val lb = MotorFactory("lb").float.build() | |
| private val rt = MotorFactory("rt").float.build() | |
| private val rb = MotorFactory("rb").float.reverse.build() | |
| private val limit = KLimitSwitch("limit") | |
| private val chainedMotors = listOf(rt, lb, rb) | |
| private val allMotors = listOf(lt, *chainedMotors.toTypedArray()) | |
| var isAttemptingZero = false | |
| var isFailed = false | |
| val pos get() = lt.pos | |
| val vel get() = lt.vel | |
| val setpoint get() = lt.setpoint | |
| val power get() = lt.power | |
| fun setTarget(pos: Double) { | |
| lt.setProfileTarget(pos) | |
| } | |
| fun startAttemptingZero() { | |
| isAttemptingZero = true | |
| } | |
| fun startFailsafeZero() { | |
| allMotors.forEach(KMotor::disable) | |
| allMotors.forEach { it.power = LiftConstants.failsafePow } | |
| isAttemptingZero = true | |
| isFailed = true | |
| } | |
| private fun checkFailsafeZero() { | |
| if(isFailed) { | |
| allMotors.forEach(KMotor::enable) | |
| Logger.logInfo("lift unfailed") | |
| } | |
| } | |
| override fun periodic() { | |
| chainedMotors.forEach { it.power = lt.power } | |
| if(isAttemptingZero && limit.invoke()) { | |
| checkFailsafeZero() | |
| lt.zero() | |
| isAttemptingZero = false | |
| setTarget(LiftConstants.home) | |
| Logger.logInfo("zeroed") | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package com.asiankoala.koawalib.control.motor | |
| import com.acmerobotics.roadrunner.profile.MotionProfile | |
| import com.acmerobotics.roadrunner.profile.MotionProfileGenerator | |
| import com.asiankoala.koawalib.control.controller.Bounds | |
| import com.asiankoala.koawalib.control.controller.PIDGains | |
| import com.asiankoala.koawalib.control.profile.MotionConstraints | |
| import com.asiankoala.koawalib.control.profile.MotionState | |
| import com.asiankoala.koawalib.control.profile.disp.Constraints | |
| import com.asiankoala.koawalib.control.profile.disp.DispState | |
| import com.asiankoala.koawalib.control.profile.disp.Lmao | |
| import com.asiankoala.koawalib.control.profile.disp.OnlineProfile | |
| import com.asiankoala.koawalib.logger.Logger | |
| import com.qualcomm.robotcore.util.ElapsedTime | |
| import kotlin.math.abs | |
| typealias stat = com.acmerobotics.roadrunner.profile.MotionState | |
| internal class MotionProfileMotorController( | |
| pid: PIDGains, | |
| ff: FFGains, | |
| bounds: Bounds, | |
| allowedPositionError: Double, | |
| disabledPosition: DisabledPosition, | |
| private val constraints: MotionConstraints, | |
| ) : PositionMotorController(pid, ff, bounds, allowedPositionError, disabledPosition) { | |
| private var profile: MotionProfile? = null | |
| private val timer = ElapsedTime() | |
| internal var setpoint = MotionState(currentState.x) | |
| private set | |
| override fun setTarget(requestedState: MotionState) { | |
| controller.reset() | |
| timer.reset() | |
| targetState = requestedState | |
| profile = MotionProfileGenerator.generateSimpleMotionProfile( | |
| stat(currentState.x, 0.0), | |
| stat(targetState.x, 0.0), | |
| constraints.maxV, | |
| constraints.accel | |
| ) | |
| // Logger.logInfo("created profile with startState: $currentState and endstate: $targetState") | |
| } | |
| override fun isAtTarget() = profile?.let { timer.seconds() > it.duration() && super.isAtTarget() } ?: false | |
| override fun update() { | |
| profile?.let { p -> setpoint = p[timer.seconds()].let { MotionState(it.x, it.v, it.a) } } | |
| controller.apply { | |
| targetPosition = setpoint.x | |
| targetVelocity = setpoint.v | |
| targetAcceleration = setpoint.a | |
| } | |
| super.update() | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package com.asiankoala.koawalib.control.motor | |
| import com.asiankoala.koawalib.control.controller.Bounds | |
| import com.asiankoala.koawalib.control.controller.PIDFController | |
| import com.asiankoala.koawalib.control.controller.PIDGains | |
| import com.asiankoala.koawalib.control.profile.MotionProfile | |
| import com.asiankoala.koawalib.control.profile.MotionState | |
| import kotlin.math.cos | |
| internal abstract class MotorController( | |
| pidGains: PIDGains, | |
| ffGains: FFGains, | |
| bounds: Bounds = Bounds() | |
| ) { | |
| protected abstract fun setTarget(requestedState: MotionState) | |
| abstract fun isAtTarget(): Boolean | |
| abstract fun update() | |
| protected val controller = PIDFController( | |
| pidGains, | |
| ffGains.kV, | |
| ffGains.kA, | |
| ffGains.kS, | |
| kF = { pos, _ -> ffGains.kG + (ffGains.kCos?.let { it * cos(pos) } ?: 0.0) } | |
| ).apply { | |
| if (bounds.isBounded) { | |
| this.setInputBounds(bounds.lowerBound!!, bounds.upperBound!!) | |
| } | |
| } | |
| var output = 0.0; protected set | |
| var targetState = MotionState(0.0); protected set | |
| var currentState = MotionState(0.0); internal set | |
| fun setProfile(profile: MotionProfile) { | |
| // (this as MotionProfileMotorController).setInternalProfile(profile) | |
| } | |
| fun setProfileTarget(x: Double) { | |
| setTarget(MotionState(x)) | |
| } | |
| fun setTargetPosition(x: Double) { | |
| setTarget(MotionState(x)) | |
| } | |
| fun setTargetVelocity(v: Double) { | |
| setTarget(MotionState(v = v)) | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package com.asiankoala.koawalib.control.controller | |
| import com.asiankoala.koawalib.util.Clock | |
| import kotlin.math.abs | |
| import kotlin.math.sign | |
| // edited version of the PIDFController that got removed in rr 1.0.0 | |
| class PIDFController @JvmOverloads constructor( | |
| private val pid: PIDGains, | |
| private val kV: Double = 0.0, | |
| private val kA: Double = 0.0, | |
| private val kStatic: Double = 0.0, | |
| private val kF: (Double, Double?) -> Double = { _, _ -> 0.0 }, | |
| ) { | |
| private var errorSum: Double = 0.0 | |
| private var lastUpdateTimestamp: Double = Double.NaN | |
| private var inputBounded: Boolean = false | |
| private var minInput: Double = 0.0 | |
| private var maxInput: Double = 0.0 | |
| var targetPosition: Double = 0.0 | |
| var targetVelocity: Double = 0.0 | |
| var targetAcceleration: Double = 0.0 | |
| var lastError: Double = 0.0 | |
| private set | |
| fun setInputBounds(min: Double, max: Double) { | |
| if (min < max) { | |
| inputBounded = true | |
| minInput = min | |
| maxInput = max | |
| } | |
| } | |
| private fun getPositionError(measuredPosition: Double): Double { | |
| var error = targetPosition - measuredPosition | |
| if (inputBounded) { | |
| val inputRange = maxInput - minInput | |
| while (abs(error) > inputRange / 2.0) { | |
| error -= sign(error) * inputRange | |
| } | |
| } | |
| return error | |
| } | |
| /** | |
| * Run a single iteration of the controller. | |
| * | |
| * @param measuredPosition measured position (feedback) | |
| * @param measuredVelocity measured velocity | |
| */ | |
| @JvmOverloads | |
| fun update( | |
| measuredPosition: Double, | |
| measuredVelocity: Double? = null | |
| ): Double { | |
| val currentTimestamp = Clock.seconds | |
| val error = getPositionError(measuredPosition) | |
| return if (lastUpdateTimestamp.isNaN()) { | |
| lastError = error | |
| lastUpdateTimestamp = currentTimestamp | |
| 0.0 | |
| } else { | |
| val dt = currentTimestamp - lastUpdateTimestamp | |
| errorSum += 0.5 * (error + lastError) * dt | |
| val errorDeriv = (error - lastError) / dt | |
| lastError = error | |
| lastUpdateTimestamp = currentTimestamp | |
| val closed = pid.kP * error + pid.kI * errorSum + | |
| pid.kD * (measuredVelocity?.let { targetVelocity - it } ?: errorDeriv) | |
| val open = kV * targetVelocity + kA * targetAcceleration + | |
| kF(measuredPosition, measuredVelocity) + kStatic * sign(targetVelocity) | |
| closed + open | |
| } | |
| } | |
| /** | |
| * Reset the controller's integral sum. | |
| */ | |
| fun reset() { | |
| errorSum = 0.0 | |
| lastError = 0.0 | |
| lastUpdateTimestamp = Double.NaN | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment