diff --git a/examples/src/main/java/org/firstinspires/ftc/teamcode/SensorReduxAnalogCanandgyro.java b/examples/src/main/java/org/firstinspires/ftc/teamcode/SensorReduxAnalogCanandgyro.java index 157a059..b13dac5 100644 --- a/examples/src/main/java/org/firstinspires/ftc/teamcode/SensorReduxAnalogCanandgyro.java +++ b/examples/src/main/java/org/firstinspires/ftc/teamcode/SensorReduxAnalogCanandgyro.java @@ -26,11 +26,9 @@ package org.firstinspires.ftc.teamcode; -import com.qualcomm.robotcore.eventloop.opmode.Disabled; import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; import com.qualcomm.robotcore.eventloop.opmode.TeleOp; import com.reduxrobotics.ftc.sensors.canandgyro.ReduxAnalogCanandgyro; - import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit; @@ -41,9 +39,9 @@ *

It assumes that the sensor is configured with a name of "canandgyro". * *

The theory of operation is that the Canandgyro will output its current absolute yaw angle - * through the analog port as a voltage between 0 to 3.3 volts. On boot it will assume a starting + * through the analog port as a voltage between 0 and 3.3 volts. On boot, it will assume a starting * value of 1.65 volts to represent a yaw of 0 degrees. Rotating the device clockwise will raise the - * voltage towards 3.3 volts (positive 180 degrees) and rotating it counter-clockwise will lower + * voltage towards 3.3 volts (positive 180 degrees), and rotating it counter-clockwise will lower * the voltage towards 0 volts (negative 180 degrees). *

* @@ -51,10 +49,10 @@ * {@link ReduxAnalogCanandgyro} class wraps this raw readng and applies a software-defined zero * offset that can be set on opmode start via * {@link ReduxAnalogCanandgyro#setYaw(double, AngleUnit)} or {@link ReduxAnalogCanandgyro#zero()} - * as during match setup the robot's heading can be assumed to be a specific angle. + * as during match set up the robot's heading can be assumed to be a specific angle. *

* - * Use Android Studio to Copy this Class, and Paste it into your team's code folder with a new name. + * Use Android Studio to Copy this Class and Paste it into your team's code folder with a new name. * Remove or comment out the @Disabled line to add this OpMode to the Driver Station OpMode list */ //@Disabled @@ -69,7 +67,7 @@ public void runOpMode() { // get a reference to our canandgyro analogCanandgyro = hardwareMap.get(ReduxAnalogCanandgyro.class, "canandgyro"); - // Updates the zero point right as the opmode starts and before the robot may move. + // Updates the zero offset right as the opmode starts and before the robot may move. // This is recommended at the start of every opmode to clear the effect of any drift or shift in // robot pose accumulated from before-match setup. analogCanandgyro.zero(); @@ -79,19 +77,19 @@ public void runOpMode() { boolean xPrevPressed = false; boolean yPrevPressed = false; - // while the OpMode is active, loop and read the yaw angle. - // Note we use opModeIsActive() as our loop condition because it is an interruptible method. + // While the OpMode is active, loop and read the yaw angle. + // Note: we use opModeIsActive() as our loop condition because it is an interruptible method. while (opModeIsActive()) { if (!xPrevPressed && gamepad1.x) { - // Using setYaw we can set the offset of the gyro to an absolute angle. + // Using setYaw, we can set the offset of the gyro to an absolute angle. // When the X button is pressed, this will set our current yaw to 30 degrees. analogCanandgyro.setYaw(30, AngleUnit.DEGREES); } xPrevPressed = gamepad1.x; if (!yPrevPressed && gamepad1.y) { - // We can also adjust the angle relative to itself as well. + // We can also adjust the angle relative to itself. // When the Y button is pressed, we offset the zero point by 15 degrees. analogCanandgyro.setYaw(analogCanandgyro.getYaw(AngleUnit.DEGREES) + 15, AngleUnit.DEGREES); } diff --git a/reduxftc/src/main/java/com/reduxrobotics/ftc/sensors/canandgyro/ReduxAnalogCanandgyro.java b/reduxftc/src/main/java/com/reduxrobotics/ftc/sensors/canandgyro/ReduxAnalogCanandgyro.java index 3d088b7..0199c17 100644 --- a/reduxftc/src/main/java/com/reduxrobotics/ftc/sensors/canandgyro/ReduxAnalogCanandgyro.java +++ b/reduxftc/src/main/java/com/reduxrobotics/ftc/sensors/canandgyro/ReduxAnalogCanandgyro.java @@ -25,21 +25,14 @@ */ package com.reduxrobotics.ftc.sensors.canandgyro; -import com.qualcomm.robotcore.hardware.AnalogInputController; -import com.qualcomm.robotcore.hardware.AnalogSensor; -import com.qualcomm.robotcore.hardware.ControlSystem; -import com.qualcomm.robotcore.hardware.HardwareDevice; -import com.qualcomm.robotcore.hardware.OrientationSensor; +import com.qualcomm.robotcore.hardware.*; import com.qualcomm.robotcore.hardware.configuration.annotations.AnalogSensorType; import com.qualcomm.robotcore.hardware.configuration.annotations.DeviceProperties; +import com.qualcomm.robotcore.util.ElapsedTime; import org.firstinspires.ftc.robotcore.external.ExportClassToBlocks; import org.firstinspires.ftc.robotcore.external.ExportToBlocks; -import org.firstinspires.ftc.robotcore.external.navigation.AngleUnit; -import org.firstinspires.ftc.robotcore.external.navigation.AxesOrder; -import org.firstinspires.ftc.robotcore.external.navigation.AxesReference; -import org.firstinspires.ftc.robotcore.external.navigation.Axis; -import org.firstinspires.ftc.robotcore.external.navigation.Orientation; +import org.firstinspires.ftc.robotcore.external.navigation.*; import java.util.HashSet; import java.util.Set; @@ -47,24 +40,33 @@ /** * Device class for a Redux Robotics Canandgyro connected via an analog port. * @author guineawheek@reduxrobotics.com + * @author j5155 ftcjdhs@gmail.com */ @ExportClassToBlocks @AnalogSensorType @DeviceProperties( name = "Redux Canandgyro (Analog)", xmlTag = "ReduxAnalogCanandgyro", - builtIn = false, compatibleControlSystems = {ControlSystem.REV_HUB}, description = "a Redux Robotics Canandgyro connected via the analog port" ) -public class ReduxAnalogCanandgyro implements AnalogSensor, OrientationSensor, HardwareDevice { - private AnalogInputController controller = null; - private int channel = -1; - private double zeroOffset = 0; +public class ReduxAnalogCanandgyro implements AnalogSensor, OrientationSensor, HardwareDevice, IMU { + private final AnalogInputController controller; + private final int channel; + private double zeroOffset; // The canandgyro itself will only output a maximum of MAX_VOLTAGE volts. private static final double MAX_VOLTAGE = 3.3; + private double oldYaw = 0; // degrees + private final ElapsedTime oldYawAge = new ElapsedTime(); + private double filteredYawVel = 0; // degrees/sec + private final ElapsedTime filteredYawVelAge = new ElapsedTime(); + + private static final double lowpassSmoothing = 10.0; // https://phrogz.net/js/framerate-independent-low-pass-filter.html + + + /** * Constructor * @@ -90,13 +92,26 @@ public ReduxAnalogCanandgyro(AnalogInputController controller, int channel) { parameterLabels = { "angleUnit" } ) public double getYaw(AngleUnit unit) { - switch (unit) { - case DEGREES: - return AngleUnit.normalizeDegrees(readZeroedVoltage() / MAX_VOLTAGE * 360.0); - case RADIANS: - return AngleUnit.normalizeRadians(readZeroedVoltage() / MAX_VOLTAGE * (Math.PI * 2)); + double yaw = getRawYaw() - getZeroOffset(AngleUnit.DEGREES); + updateLowpassFilter(yaw); + return unit.fromDegrees(yaw); + } + + /** + * Get yaw directly from the voltage without the zero offset + * @return raw yaw in degrees + */ + private double getRawYaw() { + double rawVoltage = readRawVoltage(); + // deadband detection + if (rawVoltage <= 0.005) { + return 0; + } else if (rawVoltage >= 3.295) { + return 360; + } else { + // corrects for error and nonlinearity in voltage read by the hub + return rawVoltage * 108.95105635 - 179.64903577; } - return 0; } /** @@ -118,13 +133,9 @@ public double getYaw(AngleUnit unit) { parameterLabels = { "newAngle", "angleUnit" } ) public void setYaw(double newAngle, AngleUnit unit) { - if (unit == AngleUnit.RADIANS) { - newAngle = AngleUnit.RADIANS.toDegrees(newAngle); - } else { - newAngle = AngleUnit.normalizeDegrees(newAngle); - } - newAngle *= MAX_VOLTAGE / 360.0; - zeroOffset = readRawVoltage() - newAngle; + newAngle = unit.toDegrees(newAngle); + resetLowpassFilter(newAngle); + setZeroOffset(getRawYaw() - newAngle, AngleUnit.DEGREES); } /** @@ -150,7 +161,7 @@ public void zero() { } /** - * Fetches the zero offset that is subtracted from the analog reading to produce a re-zeroed yaw. + * Fetches the zero offset subtracted from the analog reading to produce a re-zeroed yaw. *

* You can think of {@link #getYaw} being computed as *

@@ -171,15 +182,11 @@ public void zero() {
       parameterLabels = { "angleUnit" }
   )
   public double getZeroOffset(AngleUnit unit) {
-    if (unit == AngleUnit.DEGREES) {
-      return AngleUnit.normalizeDegrees(zeroOffset / MAX_VOLTAGE * 360);
-    } else {
-      return AngleUnit.normalizeRadians(zeroOffset / MAX_VOLTAGE * 2 * Math.PI);
-    }
+      return unit.fromDegrees(zeroOffset);
   }
 
   /**
-   * Sets the zero offset that is subtracted from the analog reading.
+   * Sets the zero offset subtracted from the analog reading.
    * 

* You can think of {@link #getYaw} being computed as *

@@ -199,35 +206,9 @@ public double getZeroOffset(AngleUnit unit) {
       parameterLabels = { "newOffset", "angleUnit" }
   )
   public void setZeroOffset(double newOffset, AngleUnit unit) {
-    if (unit == AngleUnit.DEGREES) {
-      newOffset = AngleUnit.normalizeDegrees(newOffset) / 360;
-    } else {
-      newOffset = AngleUnit.normalizeRadians(newOffset) / (2 * Math.PI);
-    }
-    zeroOffset = newOffset * MAX_VOLTAGE;
-
+    zeroOffset = unit.toDegrees(newOffset);
   }
 
-  /**
-   * Reads the raw voltage output with the zero offset applied.
-   *
-   * @return voltage from [0, 3.3) volts
-   */
-  @ExportToBlocks(
-      heading = "read zeroed voltage",
-      comment = "Read the raw voltage with the zero offset applied.",
-      tooltip = "Read the raw voltage with the zero offset applied."
-  )
-  public double readZeroedVoltage() {
-    double zeroed = readRawVoltage() - zeroOffset;
-    while (zeroed < 0.0) {
-      zeroed += MAX_VOLTAGE;
-    }
-    while (zeroed >= MAX_VOLTAGE) {
-      zeroed -= MAX_VOLTAGE;
-    }
-    return zeroed;
-  }
 
   /**
    * Reads the raw voltage output straight from the control/expansion hub.
@@ -287,4 +268,124 @@ public void resetDeviceConfigurationForOpMode() {
   @Override
   public void close() {
   }
+
+  /**
+   * This does nothing with the Canandgyro; it's only here to allow existing IMU uses to work.
+   */
+  @Override
+  public boolean initialize(Parameters parameters) {
+    return true;
+  }
+
+  /**
+   * Resets the robot's yaw angle to 0
+   */
+  @Override
+  public void resetYaw() {
+    zero();
+  }
+
+  /**
+   * @return A {@link YawPitchRollAngles} object representing the current orientation of the robot
+   * in the Robot Coordinate System, relative to the robot's position the last time that
+   * {@link #resetYaw()} was called, as if the robot was perfectly level at that time.
+   */
+  @Override
+  public YawPitchRollAngles getRobotYawPitchRollAngles() {
+    // Blindly using System.nanoTime() here isn't ideal, but no better option, I think
+    return new YawPitchRollAngles(AngleUnit.DEGREES,getYaw(AngleUnit.DEGREES),0.0,0.0,System.nanoTime());
+  }
+
+  /**
+   * @return An {@link Orientation} object representing the current orientation of the robot
+   * in the Robot Coordinate System, relative to the robot's position the last time
+   * that {@link #resetYaw()} was called, as if the robot was perfectly level at that time.
+   * 

+ * The {@link Orientation} class provides many ways to represent the robot's orientation, + * which is helpful for advanced use cases. Most teams should use {@link #getRobotYawPitchRollAngles()}. + */ + @Override + public Orientation getRobotOrientation(AxesReference reference, AxesOrder order, AngleUnit angleUnit) { + // Blindly using System.nanoTime() here isn't ideal, but no better option, I think + return new Orientation(AxesReference.EXTRINSIC, AxesOrder.ZYX, AngleUnit.DEGREES,(float) getYaw(AngleUnit.DEGREES), 0f , 0f, System.nanoTime()) + .toAxesReference(reference) + .toAxesOrder(order) + .toAngleUnit(angleUnit); + } + + /** + * @return A {@link Quaternion} object representing the current orientation of the robot + * in the Robot Coordinate System, relative to the robot's position the last time + * that {@link #resetYaw()} was called, as if the robot was perfectly level at that time. + *

+ * Quaternions provide an advanced way to access orientation data that will work well + * for any orientation of the robot, even where other types of orientation data would + * encounter issues such as gimbal lock. + */ + // qy and qw being the same is perfectly fine + // and extracing it into a variable would make this unnecessarily complicated + // also for some reason IntelliJ keeps forgetting this warning exists so also supress RedundantSuppression + @SuppressWarnings({"DuplicateExpressions", "RedundantSuppression"}) + @Override + public Quaternion getRobotOrientationAsQuaternion() { + // is this math right? + double yaw = getYaw(AngleUnit.RADIANS); // this has to be radians + float qx = (float) (Math.cos(yaw/2) - Math.sin(yaw/2)); + float qy = (float) (Math.cos(yaw/2) + Math.sin(yaw/2)); + float qz = (float) (Math.sin(yaw/2) - Math.cos(yaw/2)); + float qw = (float) (Math.cos(yaw/2) + Math.sin(yaw/2)); + return new Quaternion(qw, qx, qy, qz, System.nanoTime()); + } + + /** + * @param unit the unit to return the velocity in + * @return The angular velocity of the robot (how fast it's turning around the three axes) in units per second + */ + @Override + public AngularVelocity getRobotAngularVelocity(AngleUnit unit) { + return new AngularVelocity(unit,0f,0f,(float) getYawVelocity(unit),System.nanoTime()); + } + + /** + * @param unit the unit to return the velocity in + * @return The yaw velocity of the robot in units per second + */ + public double getYawVelocity(AngleUnit unit) { + if (oldYawAge.milliseconds() > 1) { // only read if necessary (is this value big enough?) + getYaw(AngleUnit.DEGREES); // the output value doesn't matter, just doing this to trigger the filter + } + double yawVelocity = filteredYawVel; + return unit.fromDegrees(yawVelocity); + } + + /** + * @param yaw current yaw in degrees + * @return yaw velocity in degrees/sec + */ + private double getRawYawVelocity(double yaw) { + double yawVel = (yaw - oldYaw) / oldYawAge.seconds(); + oldYaw = yaw; + oldYawAge.reset(); + return yawVel; + } + + /** + * Updates the lowpass filter based on an input in degrees + * @param yaw input yaw in degrees + */ + private void updateLowpassFilter(double yaw) { + if (yaw != oldYaw) { // ensure this is a different loop with different bulk read data, + // then update the low-pass filter + //(is milliseconds the right unit?) + filteredYawVel += filteredYawVelAge.milliseconds() * (getRawYawVelocity(yaw) - filteredYawVel) / lowpassSmoothing; + filteredYawVelAge.reset(); + } + } + + private void resetLowpassFilter(double setValue) { + filteredYawVel = 0; + oldYaw = setValue; + filteredYawVelAge.reset(); + oldYawAge.reset(); + } }