public interface SpeedSensor {
/**
* Gets the current speed.
*
* @return the speed
*/
public double getSpeed();
}
Motors
From Strongback’s perspective, a motor is a device that can be set to operate at a controllable speed. This is intentionally a very abstract concept. Each motor object represents a physical motor and its motor controller. For example, a CIM motor and Talon SRX controller devices can be modeled in Strongback as a single Motor
object.
This section goes into more detail about the Strongback interfaces that are related to motors.
SpeedSensor
The org.strongback.components.SpeedSensor
interface represents a simple sensor that can detect speed:
There is only one abstract method, so SpeedSensor
is a functional interface. Although FRC doesn’t normally have sensors that directly measure speed, it still is an independent concept worth having. Besides, you might come up with your own speed sensor implementations: for exmaple, your robot might calculate the (approximate) speed by measuring rotations per second on an encoder, and you could do this with a custom implementation of SpeedSensor
.
SpeedController
The org.strongback.components.SpeedController
interface can control (or set) the speed of a device:
public interface SpeedController {
/**
* Sets the speed.
*
* @param speed the new speed as a double
* @return this object to allow chaining of methods; never null
*/
public SpeedController setSpeed(double speed);
}
There is only one abstract method, so SpeedController
is a functional interface. Again, this interface may not be terribly useful on its own, but it still is an independent concept worth having.
Stoppable
The org.strongback.components.Stoppable
interface represents any device that can be stopped, and is defined with a very simple functional interface:
public interface Stoppable {
/**
* Stops this component.
*/
public void stop();
}
Motor
The org.strongback.components.Motor
interface extends SpeedSensor
, SpeedController
, and Stoppable
(although it overrides many methods to downcast the return type or provide documentation):
public interface Motor extends SpeedSensor, SpeedController, Stoppable {
/**
* Gets the current speed.
*
* @return the speed, will be between -1.0 and 1.0 inclusive
*/
@Override
public double getSpeed();
/**
* Sets the speed of this motor.
*
* @param speed the new speed as a double, clamped to -1.0 to 1.0 inclusive
* @return this object to allow chaining of methods; never null
*/
@Override
public Motor setSpeed(double speed);
/**
* Stops this motor. Same as calling `setSpeed(0.0)`.
*/
@Override
default public void stop() {
setSpeed(0.0);
}
public enum Direction {
FORWARD, REVERSE, STOPPED
}
/**
* Gets the current direction of this motor, which can be
* Direction.FORWARD, Direction.REVERSE, or Direction.STOPPED.
*
* @return the direction of this motor; never null
*/
default public Direction getDirection() {...}
/**
* Create a new motor that inverts this motor.
*
* @return the new inverted motor; never null
*/
default Motor invert() {...}
/**
* Create a new motor instance that is actually composed
* of two other motors that will be controlled identically.
* This is useful when multiple motors are controlled in
* the same way, such as on one side of a TankDrive.
*
* @param motor1 the first motor, and the motor from which
* the speed is read; may not be null
* @param motor2 the second motor; may not be null
* @return the composite motor; never null
*/
static Motor compose(Motor motor1,
Motor motor2) {...}
/**
* Create a new motor instance that is actually composed
* of three other motors that will be controlled identically.
* This is useful when multiple motors are controlled in
* the same way, such as on one side of a TankDrive.
*
* @param motor1 the first motor, and the motor from which
* the speed is read; may not be null
* @param motor2 the second motor; may not be null
* @param motor3 the third motor; may not be null
* @return the composite motor; never null
*/
static Motor compose(Motor motor1,
Motor motor2,
Motor motor3) {...}
/**
* Create a new motor instance that inverts the speed sent
* to and read from another motor. This is useful on
* TankDrive, where all motors on one side are physically
* inverted compared to the motors on the other side.
*
* @param motor the motor to invert; may not be null
* @return the inverted motor; never null
*/
static Motor invert(Motor motor) {...}
}
The Motor
interface is pretty straightforward with its getSpeed()
, setSpeed(double)
, and stop()
methods. It also defines a getDirection()
method that returns Direction.FORWARD
when its speed is positive, Direction.REVERSE
when its speed is negative, and Direction.STOPPED
when its speed is 0.
Interestingly, the Motor
interface also provides a number of static factory methods that make it very easy to compose multiple Motor
instances together so they can be treated as a single Motor
instance. This is very common on some types of chassis, where multiple motors are ganged together on a single gearbox. For example, this fragment shows how to create the Motor
objects for a tank-style chassis that has 2 motors on each side:
Motor leftFront = ...
Motor leftRear = ...
Motor rightFront = ...
Motor rightRear = ...
Motor left = Motor.compose(leftFront,leftRear);
Motor right = Motor.compose(rightFront,rightRear);
DriveTrain drive = TankDrive.create(left, right.invert());
We’ll see later on how your robot can obtain the four Motor
objects that represent the four pairs of hardware motors and motor controller devices. But you can see how easy it is to make left
control both the leftFront
and leftRear
motors. The code is concise and very easy to read and understand.
The example also shows the use of the invert()
method, which creates a new Motor
whose speed is the negation of the speed of the Motor
on which the method is called. (For convenience, there is also a static form of the same method.)
LimitedMotor
The org.strongback.components.LimitedMotor
interface is a specialization of Motor
. It represents a motor that moves a component that is limited on the two extremes, and that should automatically stop when the Switch
sensor on either side is triggered. A LimitedMotor
also tracks the position of the component in relation to the two limits.
public interface LimitedMotor extends Motor {
/**
* The possible positions for a limited motor.
*/
public enum Position {
/**
* The motor is at the forward direction limit.
**/
FORWARD_LIMIT,
/**
* The motor is at the reverse direction limit.
**/
REVERSE_LIMIT,
/**
* The motor is between the forward and reverse limits,
* but the exact position is unknown.
**/
UNKNOWN
}
@Override
public LimitedMotor setSpeed(double speed);
/**
* Get the switch that signals when this motor reaches its limit
* in the forward direction.
*
* @return the forward direction limit switch; never null
*/
public Switch getForwardLimitSwitch();
/**
* Get the switch that signals when this motor reaches its limit
* in the reverse direction.
*
* @return the reverse direction limit switch; never null
*/
public Switch getReverseLimitSwitch();
/**
* Tests if this limited motor is at the high limit. This is equivalent
* to calling `getForwardLimitSwitch().isTriggered()`.
*
* @return true if this motor is at the forward limit; or false otherwise
*/
default public boolean isAtForwardLimit() {
return getForwardLimitSwitch().isTriggered();
}
/**
* Tests if this limited motor is at the low limit. This is equivalent
* to calling `getReverseLimitSwitch().isTriggered()`.
*
* @return true if this motor is at the reverse limit; or false otherwise
*/
default public boolean isAtReverseLimit() {
return getReverseLimitSwitch().isTriggered();
}
/**
* Moves this limited towards the forward limit. This method should
* be called once per loop until the movement is completed.
*
* @param speed the speed at which the underlying motor should spin
* in the forward direction
* @return true if the motor remains moving, or false if it has
* reached the forward limit
*/
default public boolean forward(double speed) {...}
/**
* Moves this {@link LimitedMotor} towards the reverse limit.
* This method should be called once per loop until the movement
* is completed.
*
* @param speed the speed at which the underlying motor should spin
* in the reverse direction
* @return true if the motor remains moving, or false if it has
* reached the forward limit
*/
default public boolean reverse(double speed) {...}
/**
* Gets the current position of this limited motor. Can be
* HIGH, LOW, or UNKNOWN.
*
* @return the current position of this motor
*/
default public Position getPosition() {...}
/**
* Create a limited motor around the given motor and switches.
*
* @param motor the {@link Motor} being limited; may not be null
* @param forwardSwitch the {@link Switch} that signals the motor
* reached its limit in the forward direction, or null if
* there is no limit switch
* @param reverseSwitch the {@link Switch} that signals the motor
* reached its limit in the reverse direction, or null if
* there is no limit switch
* @return the limited motor; never null
* @throws IllegalArgumentException if the `motor` parameter is null
*/
public static LimitedMotor create(Motor motor,
Switch forwardSwitch,
Switch reverseSwitch) {...}
}
The important thing to understand is that a LimitedMotor
doesn’t check the limit switches in a background thread. Instead, it only does this when forward
or reverse
methods are called. Therefore, these methods should be called in the run loop of the robot, and when one of these method invocations detects that a limit has been reached it will automatically stop()
itself.
This class works very well in the commmand framework. Here, you define a command that tells the motor to move forward (or reverse) at certain speed, and that stops as soon as isAtForwardLimit()
(or isAtReverseLimit()
) returns true. Here’s a very concise way to do that:
Motor motor = ...
Switch forwardLimit = ...
Switch reverseLimit = ...
LimitedMotor limitedMotor = LimitedMotor.create(motor,forwardLimit,reverseLimit);
// Begin moving forward at 1/2 speed until limit is reached ...
Strongback.submit(Command.create(()->limitedMotor.forward(0.5));
We’ll see more examples like this in the section on the commmand framework. But note that the limitedMotor.forward(0.5)
tells the LimitedMotor
object to move forward at half-speed, and since it returns a boolean as to whether it is still moving, this lambda is sufficient for the Command
to know how long to keep running.
TalonSRX
The org.strongback.components.TalonSRX
interface is a specialization of LimitedMotor
. The Talon SRX is an advanced motor controller that can be wired with external limit switches to automatically stop the forward and reverse directions when the limit switches are triggered. It also has a built-in current sensor and position (angle) sensor. All of this is handled within the Talon SRX hardware.
public interface TalonSRX extends LimitedMotor {
@Override
public TalonSRX setSpeed(double speed);
/**
* Get the angle sensor (encoder) hooked up to the Talon SRX
* motor controller.
*
* @return the angle sensor; never null, but if not hooked up
* the sensor will always return a meaningless value
*/
public AngleSensor getAngleSensor();
/**
* Get the Talon SRX's current sensor.
*
* @return the current sensor; never null
*/
public CurrentSensor getCurrentSensor();
}
Obtaining hardware motors
Strongback provides a org.strongback.hardware.Hardware
class with static factory methods for obtaining various motors based upon the kind of physical motor controller device:
public class Hardware {
...
/**
* Factory method for different kinds of motors.
*/
public static final class Motors {
/**
* Create a motor driven by a Talon speed controller on the
* specified channel. The speed output is limited to [-1.0,1.0]
* inclusive.
*
* @param channel the channel the controller is connected to
* @return a motor on the specified channel
*/
public static Motor talon(int channel) {...}
/**
* Create a motor driven by a Talon speed controller on the
* specified channel, with a custom speed limiting function.
*
* @param channel the channel the controller is connected to
* @param speedLimiter function that will be used to limit
* the speed; may not be null
* @return a motor on the specified channel
*/
public static Motor talon(int channel,
DoubleToDoubleFunction speedLimiter) {...}
/**
* Create a motor driven by a Jaguar speed controller on the
* specified channel. The speed output is limited to [-1.0,1.0]
* inclusive.
*
* @param channel the channel the controller is connected to
* @return a motor on the specified channel
*/
public static Motor jaguar(int channel) {...}
/**
* Create a motor driven by a Jaguar speed controller on the
* specified channel, with a custom speed limiting function.
*
* @param channel the channel the controller is connected to
* @param speedLimiter function that will be used to limit
* the speed; may not be null
* @return a motor on the specified channel
*/
public static Motor jaguar(int channel,
DoubleToDoubleFunction speedLimiter) {...}
/**
* Create a motor driven by a Victor speed controller on the
* specified channel. The speed output is limited to [-1.0,1.0]
* inclusive.
*
* @param channel the channel the controller is connected to
* @return a motor on the specified channel
*/
public static Motor victor(int channel) {...}
/**
* Create a motor driven by a Victor speed controller on the
* specified channel, with a custom speed limiting function.
*
* @param channel the channel the controller is connected to
* @param speedLimiter function that will be used to limit
* the speed; may not be null
* @return a motor on the specified channel
*/
public static Motor victor(int channel,
DoubleToDoubleFunction speedLimiter) {...}
/**
* Creates a TalonSRX motor controlled by a Talon SRX with
* built-in current sensor and position (angle) sensor.
* The WPILib's CANTalon object passed into this method should
* be already configured by the calling code.
*
* @param talon the already configured WPILib CANTalon instance;
* may not be null
* @param pulsesPerDegree the number of encoder pulses per
* degree of revolution of the final shaft
* @return a {@link TalonSRX} motor; never null
*/
public static TalonSRX talonSRX(CANTalon talon,
double pulsesPerDegree) {...}
}
...
}
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 TankDrive drive;
@Override
public void robotInit() {
...
Motor left = Motor.compose(Hardware.Motors.talon(LF_MOTOR_PORT),
Hardware.Motors.talon(LR_MOTOR_PORT));
Motor right = Motor.compose(Hardware.Motors.talon(RF_MOTOR_PORT),
Hardware.Motors.talon(RR_MOTOR_PORT))
.invert();
drive = new TankDrive(left, right);
}
You can then pass the motors around to other components that need them.
Tip
|
Testability tip
You might think that it’s easy for components to just get the motors or 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 injected |
Using motors in tests
Your tests should never use the Hardware class to create motors 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 up the mock motors.
The MockMotor
class is defined as follows:
public class MockMotor implements Motor {
private volatile double speed = 0;
MockMotor(double speed) {
this.speed = speed;
}
@Override
public double getSpeed() {
return speed;
}
@Override
public MockMotor setSpeed(double speed) {
this.speed = speed;
return this;
}
public MockMotor invert() {...}
}
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 few mocks motors in a stopped or running initial state:
MockMotor m1 = Mock.runningMotor(0.5);
MockMotor m2 = Mock.stoppedMotor();
// Pass the motors to components that use them, and verify they behave
// correctly given the two speeds
m2.setSpeed(0.4);
// Check the components are using the motors correctly
Strongback also provides a MockTalonSRX
, and too these can be constructed using the Mock
static factory methods. However, there is no mock LimitedMotor
, since a real LimitedMotor
can be used with a MockMotor
object and MockSwitches
:
MockSwitch forLimit = Mock.notTriggeredSwitch();
MockSwitch revLimit = Mock.notTriggeredSwitch();
MockMotor motor = Mock.runningMotor(0.5);
LimitedMotor limited = LimitedMotor.create(motor,forLimit,revLimit);
// The limited motor is moving forward at 50% speed and neither limit
// is triggered. Test a component using this limited motor ...
// Change the motor speed ..
motor.setSpeed(0.4);
// Check the component correctly uses the limited motor ...
// Stop at the limit ...
forLimit.setTriggered();
// Check the component correctly uses the limited motor ...