package org.firstinspires.ftc.teamcode.util; import com.acmerobotics.roadrunner.util.NanoClock; import com.qualcomm.robotcore.hardware.DcMotorEx; import com.qualcomm.robotcore.hardware.DcMotorSimple; /** * Wraps a motor instance to provide corrected velocity counts and allow reversing independently of the corresponding * slot's motor direction */ public class Encoder { private final static int CPS_STEP = 0x10000; private static double inverseOverflow(double input, double estimate) { // convert to uint16 int real = (int) input & 0xffff; // initial, modulo-based correction: it can recover the remainder of 5 of the upper 16 bits // because the velocity is always a multiple of 20 cps due to Expansion Hub's 50ms measurement window real += ((real % 20) / 4) * CPS_STEP; // estimate-based correction: it finds the nearest multiple of 5 to correct the upper bits by real += Math.round((estimate - real) / (5 * CPS_STEP)) * 5 * CPS_STEP; return real; } public enum Direction { FORWARD(1), REVERSE(-1); private int multiplier; Direction(int multiplier) { this.multiplier = multiplier; } public int getMultiplier() { return multiplier; } } private DcMotorEx motor; private NanoClock clock; private Direction direction; private int lastPosition; private int velocityEstimateIdx; private double[] velocityEstimates; private double lastUpdateTime; public Encoder(DcMotorEx motor, NanoClock clock) { this.motor = motor; this.clock = clock; this.direction = Direction.FORWARD; this.lastPosition = 0; this.velocityEstimates = new double[3]; this.lastUpdateTime = clock.seconds(); } public Encoder(DcMotorEx motor) { this(motor, NanoClock.system()); } public Direction getDirection() { return direction; } private int getMultiplier() { return getDirection().getMultiplier() * (motor.getDirection() == DcMotorSimple.Direction.FORWARD ? 1 : -1); } /** * Allows you to set the direction of the counts and velocity without modifying the motor's direction state * @param direction either reverse or forward depending on if encoder counts should be negated */ public void setDirection(Direction direction) { this.direction = direction; } /** * Gets the position from the underlying motor and adjusts for the set direction. * Additionally, this method updates the velocity estimates used for compensated velocity * * @return encoder position */ public int getCurrentPosition() { int multiplier = getMultiplier(); int currentPosition = motor.getCurrentPosition() * multiplier; if (currentPosition != lastPosition) { double currentTime = clock.seconds(); double dt = currentTime - lastUpdateTime; velocityEstimates[velocityEstimateIdx] = (currentPosition - lastPosition) / dt; velocityEstimateIdx = (velocityEstimateIdx + 1) % 3; lastPosition = currentPosition; lastUpdateTime = currentTime; } return currentPosition; } /** * Gets the velocity directly from the underlying motor and compensates for the direction * See {@link #getCorrectedVelocity} for high (>2^15) counts per second velocities (such as on REV Through Bore) * * @return raw velocity */ public double getRawVelocity() { int multiplier = getMultiplier(); return motor.getVelocity() * multiplier; } /** * Uses velocity estimates gathered in {@link #getCurrentPosition} to estimate the upper bits of velocity * that are lost in overflow due to velocity being transmitted as 16 bits. * CAVEAT: must regularly call {@link #getCurrentPosition} for the compensation to work correctly. * * @return corrected velocity */ public double getCorrectedVelocity() { double median = velocityEstimates[0] > velocityEstimates[1] ? Math.max(velocityEstimates[1], Math.min(velocityEstimates[0], velocityEstimates[2])) : Math.max(velocityEstimates[0], Math.min(velocityEstimates[1], velocityEstimates[2])); return inverseOverflow(getRawVelocity(), median); } }