Angle Sensors

A hardware angle sensor is any device that (surprise!) measures the angle of something relative to something else. Example angle sensors include encoders and potentiometers. Gyroscopes, which directly measure angular velocity (or angular acceleration) can also be used as an angle sensor, although doing so is often very inaccurate because the gyroscope must typically calculate angular displacement by integrating angular velocity.

This section describes Strongback’s multiple interfaces that represent physical angle sensor devices.

AngleSensor

The org.strongback.components.AngleSensor interface represents a simple angle sensor that returns the angular displacement relative to some _zero point.

public interface AngleSensor extends Zeroable {

    /**
     * Gets the angular displacement in continuous degrees.
     *
     * @return the positive or negative angular displacement
     */
    public double getAngle();

    /**
     * Change the output so that the current value is considered to be 0
     * @return this object to allow chaining of methods; never null
     */
    @Override
    public AngleSensor zero();

    /**
     * Create an angle sensor around the given function that returns the angle.
     *
     * @param angleSupplier the function that returns the angle; may not be null
     * @return the angle sensor
     */
    public static AngleSensor create(DoubleSupplier angleSupplier) {...}

    /**
     * Creates a new angle sensor that inverts the specified AngleSensor
     * object, so that negative angles become positive angles.
     *
     * @param sensor the {@link AngleSensor} to invert
     * @return an AngleSensor that reads the opposite of the original sensor
     */
    public static AngleSensor invert(AngleSensor sensor) {...}

}

The getAngle() method returns the angular displacement in continuous degrees, meaning the value can be any positive or negative number. Negative angles are assumed to be counter-clockwise and positive values clockwise.

The zero() method is inherited from the Zeroable interface, and it can be used to set the angle to which subsequent angles will be relative.

The create(DoubleSupplier) static factory method will create an AngleSensor from any java.util.function.DoubleSupplier. The DoubleSupplier is a functional interface added in Java 8, and it has a single method that takes no parameters and returns a double value. Therefore, create(DoubleSupplier) can be used to create an AngleSensor from any object, lambda, or method reference that satisfies the same signature (no parameters and returns a double). For example, the following fragment creates an AngleSensor that always returns the value 180:

AngleSensor angle = AngleSensor.create(()->180);

Or, given some imaginary class Foo that has a method that takes no parameters and returns a double:

public class Foo {
    ...
    public double bar() {...}
    ...
}

the following fragment shows how to create an AngleSensor that always uses the Foo.bar() method to compute the angle, using a method reference to the bar() method on the object foo:

Foo foo = ...
AngleSensor angle = AngleSensor.create(foo::bar);

Finally, the AngleSensor also defines the invert(AngleSensor) method, which can be used to return a new AngleSensor that always inverts the angle returned by another:

AngleSensor actual = ...
AngleSensor inverted = AngleSensor.invert(actual);

This is far easier than always having to multiply the resulting angle by -1!

Compass

The org.strongback.components.Compass is an angle sensor that determines the angular displacement in continous degrees. A Compass is an AngleSensor with an additional method that returns the relative heading, which is an angle that is always in the range [0,360). Like AngleSensor, it can be zeroed to set the angle at which subsequent angles and headings are based.

public interface Compass extends AngleSensor {

    /**
     * Gets the angular displacement of in degrees in the range [0, 360).
     *
     * @return the heading of this {@link Compass}
     */
    public default double getHeading() {...}

    /**
     * Create a angle sensor for the given function that returns the angle.
     *
     * @param angleSupplier the function that returns the angle; may not be null
     * @return the angle sensor
     */
    public static Compass create(DoubleSupplier angleSupplier) {...}
}

The getHeading() method is a default method that is implemented in terms of AngleSensor.getAngle() to always return a value greater or equal to 0 but less than 360.

Compass also defines a create(DoubleSupplier) method that works the same way as AngleSensor.create(DoubleSupplier) except that it returns a Compass object instead. This is useful to create a Compass backed by custom functionality.

Gyroscope

The org.strongback.components.Gyroscope interface represents a device that measures angular velocity (in degrees per second) about a single axis. A gyroscope can indirectly determine angular displacement by integrating velocity with respect to time, which is why it extends Compass. Negative values are assumed to be counter-clockwise and positive values are clockwise. Like AngleSensor, it can be zeroed to set the angle at which subsequent angles and headings are based.

public interface Gyroscope extends Compass {

    /**
     * Gets the rate of change in angle in degrees per second.
     *
     * @return the angular velocity
     */
    public double getRate();

    /**
     * Create a gyroscope for the given functions that returns the anglular
     * displacement and velocity.
     *
     * @param angleSupplier the function that returns the angle; may not be null
     * @param rateSupplier the function that returns the angular acceleration; may not be null
     * @return the angle sensor
     */
    public static Gyroscope create(DoubleSupplier angleSupplier,
                                   DoubleSupplier rateSupplier) { ... }

}

In addition to the getHeading() and getAngle() methods inherited from Compass, the getRate() method is returns the rate of change in the angle, otherwise known as the angular velocity. The rate can be positive or negative.

Gyroscope also defines a create(DoubleSupplier,DoubleSupplier) static factory method for creating a Gyroscope given one function that returns the angle and another function that returns the rate. This is useful to create a Gyroscope backed by custom functionality.

Obtaining hardware angle sensors

Strongback provides a org.strongback.hardware.Hardware class with static factory methods for common physical hardware angle sensing devices:

public class Hardware {

    /**
     * Factory method for angle sensors.
     */
    public static final class AngleSensors {

        /**
         * Create a Gyroscope that uses a WPILib Gyro on the specified channel.
         *
         * @param channel the channel the gyroscope is plugged into
         * @return the gyroscope; never null
         */
        public static Gyroscope gyroscope(int channel) {...}

        /**
         * Creates a new AngleSensor from an encoder using the specified
         * channels with the specified distance per pulse.
         *
         * @param aChannel the a channel of the encoder
         * @param bChannel the b channel of the encoder
         * @param distancePerPulse the distance the end shaft spins per pulse
         * @return the angle sensor; never null
         */
        public static AngleSensor encoder(int aChannel,
                                          int bChannel,
                                          double distancePerPulse) {...}

        /**
         * Create a new AngleSensor from a potentiometer on the specified
         * channel and with the given scaling. Since no offset is provided,
         * the resulting angle sensor may often be used with a limit switch
         * to know precisely where a mechanism might be located in space.
         * When the limit switch is triggered, the location is , and
         * the angle sensor can be zeroed at that known position. (See
         * potentiometer(int, double, double) when another switch is
         * not used to help determine the location, and instead the
         * zero point is pre-determined by the physical design of the
         * mechanism.)
         *
         * The scale factor multiplies the 0-1 ratiometric value to return
         * the angle in degrees.
         *
         * For example, let's say you have an ideal 10-turn linear potentiometer
         * attached to a motor attached by chains and a 25x gear reduction
         * to an arm. If the potentiometer (attached to the motor shaft)
         * turned its full 3600 degrees, the arm would rotate 144 degrees.
         * Therefore, the 'fullVoltageRangeToInches' scale factor is
         * '144 degrees / 5 V', or 28.8 degrees/volt.
         *
         * @param channel The analog channel this potentiometer is plugged into.
         * @param fullVoltageRangeToDegrees The scaling factor multiplied by the
         *        analog voltage value to obtain the angle in degrees.
         * @return the angle sensor that uses the potentiometer on the given channel;
         *        never null
         */
        public static AngleSensor potentiometer(int channel,
                                                double fullVoltageRangeToDegrees) {...}

        /**
         * Create a new AngleSensor from an analog potentiometer using the specified
         * channel, scaling, and offset. This method is often used when the offset
         * can be hard-coded by measuring the value of the potentiometer at
         * the mechanism's zero-point. On the other hand, if a limit switch is used
         * to always determine the position of the mechanism upon startup, then see
         * potentiometer(int, double).
         *
         * The scale factor multiplies the 0-1 ratiometric value to return the angle
         * in degrees.
         *
         * For example, let's say you have an ideal 10-turn linear potentiometer
         * attached to a motor attached by chains and a 25x gear reduction to an arm.
         * If the potentiometer (attached to the motor shaft) turned its full 3600
         * degrees, the arm would rotate 144 degrees. Therefore, the
         * 'fullVoltageRangeToInches' scale factor is 144 degrees / 5 V, or
         * 28.8 degrees/volt.
         *
         * To prevent the potentiometer from breaking due to minor shifting in
         * alignment of the mechanism, the potentiometer may be installed with the
         * "zero-point" of the mechanism (e.g., arm in this case) a little ways into
         * the potentiometer's range (for example 30 degrees). In this case, the
         * 'offset' value of '30' is determined from the mechanical design.
         *
         * @param channel The analog channel this potentiometer is plugged into.
         * @param fullVoltageRangeToDegrees The scaling factor multiplied by the
         *        analog voltage value to obtain the angle in degrees.
         * @param offsetInDegrees The offset in degrees that the angle sensor will
         *        subtract from the underlying value before returning the angle
         * @return the angle sensor that uses the potentiometer on the given channel;
         *        never null
         */
        public static AngleSensor potentiometer(int channel,
                                                double fullVoltageRangeToDegrees,
                                                double offsetInDegrees) {...}
    }

}

Obviously you should only use this class in code that will only run on the robot, so we recommend centralizing this logic inside one of your top-level robot classes:

public class SimpleRobot extends IterativeRobot {

    private Gyroscope gyro;

    @Override
    public void robotInit() {
        ...
        gyro = Hardware.AngleSensors.gyroscope(3);

        // pass 'gyro' to the objects that need it ...
    }

You can then pass these angle sensor, compasse, or gyroscope objects around to other components that need them.

Tip
Testability tip

You might think that it’s easy for components to just get the gyro field on the SimpleRobot, perhaps via a getter method. However, that embeds knowledge about how to find the sensor inside the component that uses it. This makes it very difficult to test the component off-robot, because it will always try to get a hardware-based sensor from the SimpleRobot object.

Instead, it’s much better to use injection, which is just a fancy way of saying that when a component is created it should be handed all the objects it needs. So if we have a component that requires a sensor, then that component should require the sensor be given to it (usually through the constructor, if possible). The robot class can create this component and pass the sensor to it, while test cases can pass in a mock sensor. (We’ll talk about mocks in the next section.)

The bottom line is that these other components should depend only upon an injected AngleSensor, Compass, or Gyroscope objects, and should never know how to find one. This helps keep these other components testable.

Using angle sensors in tests

Your tests should never use the Hardware class to create angle sensors, compasses, or gyroscopes in your tests. Instead, Strongback provides a series of mock classes that can be used in place of the hardware implementations. These mocks extend the normal interfaces as expected, but they add setter methods that your test code can explicitly set the angles and/or rates on these artificial mock objects.

For example, the MockAngleSensor class is defined as follows:

public class MockAngleSensor extends MockZeroable implements AngleSensor {

    @Override
    public MockAngleSensor zero() {
        super.zero();
        return this;
    }

    @Override
    public double getAngle() {
        return super.getValue();
    }

    /**
     * Set the angle value {@link #getAngle() returned} by this object.
     *
     * @param angle the angle value
     * @return this instance to enable chaining methods; never null
     */
    public MockAngleSensor setAngle(double angle) {
        super.setValue(angle);
        return this;
    }
}

where MockZeroable manages the scalar value and zeroing functionality, and is defined as:

abstract class MockZeroable implements Zeroable {

    private volatile double zero = 0;
    private volatile double value;

    protected double getValue() {
        return value - zero;
    }

    protected void setValue( double value) {
        this.value = value;
    }

    @Override
    public MockZeroable zero() {
        zero = value;
        return this;
    }
}

The MockAngleSensor implements AngleSensor, but rather than using real hardware it simply uses a field to track the current angle. Your tests can change the value of the acceleration at any time using the setAngle(double) method, and it can zero the sensor at any time using the zero() method.

Strongback provides MockCompass and MockGyroscope classes, too.

You can create mock objects using the org.strongback.mock.Mock class' static factory methods. For example, here’s a fragment from a test that creates a mock compass:

MockCompass compass = Mock.compass();
compass.setAngle(721.1);
// pass compass to a component that needs and uses a Compass or AngleSensor
// and verify the component correctly reads the angle and/or heading

results matching ""

    No results matching ""