The Units Library is very powerful and useful for robot programming. It is a bit difficult to learn and understand initially but it will help keep your code’s units consistent and avoid conversion errors.
One of the more useful aspects of using the Units Library is being able to define custom units that pertain to your robot code. One example is converting from motor revolutions to distance traveled for the robot drivetrain. If you have a gear box between the motor and the wheel that has a gear ratio of 6.86 to 1 and a wheel diameter of 4 inches then you could define a custom unit type of "meters per rotation" and then create a constant with those units that you can use to multiply desired linear velocities by to get motor angular velocities.
Custom Unit For Drivetrain
// Gear ratio of the drive motors. 6.86 rotations of the drive motor is one rotation of the wheel.
constexpr double kDriveGearRatio = 6.86;
// Compound unit for the meter per revolution constant.
using meters_per_rev = units::compound_unit<units::meters, units::inverse<units::turns>>;
using meters_per_rev_t = units::unit_t<meters_per_rev> meters_per_rev_t;
// The number of meters traveled per rotation of the drive motor
// wheel circumference / gear ratio
constexpr meters_per_rev_t kDriveMetersPerRotation = std::numbers::pi * 4_in / (kDriveGearRatio * 1_tr );
Note that the units library uses "turns" for rotations with the suffix "tr". Also notice that on the last line the wheel diameter is specified in inches but the units library automatically converts inches to meters.
Another example is using the TalonFX smart motor controller library’s Set() function. It requires the position or velocity inputs in very awkward units (See Table 3). The position should be in "encoder tics" and the velocity should be in "encoder tics" per 100 milliseconds. There are 2048 encoder tics per rotation for the TalonFX built-in encoder. Custom units can help with converting from these strange units to more physically meaningful units. You could define a custom angular position unit that is ("tics") and a custom angular velocity unit that is ("tics" / 100_ms).
Custom Unit for Encoder Tics
// Create a unit called "tics" that represents 1/2048th of a revolution
// and make a type qualifier called "tics_t"
using tics = units::unit<std::ratio<1,2048>, units::turns>;
using tics_t = units::unit_t<tics>;
// Create a unit called "tics_per_100ms" that represents (tics / 0.1 seconds)
// and make a type qualifier called "tics_per_100ms_t"
using tics_per_100ms = units::compound_unit<tics, units::inverse<units::deciseconds>>;
using tics_per_100ms_t = units::unit_t<tics_per_100ms>;
// Alternatively "tics_per_100ms" could be defined as:
// using tics_per_100ms = units::compound_unit<tics, units::inverse<
// units::unit<std::ratio<1,10>, units::seconds>>>;
Once these types are defined then the programmer doesn’t need to worry about converting from tics to degrees or from RPM to tics_per_100ms. The units types will do all the conversions automatically. The code below shows how to use these types.
Automatic Units Conversion
tics_t talon_position;
tics_per_100ms_t talon_velocity;
ctre::phoenix::motorcontrol::can::TalonFX talon{2};
// This automatically converts from degrees to tics
talon_position = 45_deg;
// value() returns the position in tics as a double
// which is 256 tics ( 45 * 2048 / 360 )
talon.Set( ctre::phoenix::motorcontrol::ControlMode::Position,
talon_position.value() );
// This automatically converts from RPM to tics_per_100ms.
talon_velocity = 2400_rpm;
// value() returns the velocity in tics_per_100ms as a double
// which is 8192 tics_per_100ms ( 2400 * 2048 / 600 )
talon.Set( ctre::phoenix::motorcontrol::ControlMode::Velocity,
talon_velocity.value() );
// If you need to convert a variable in one unit to another
// without creating a variable you can use:
printf( "Talon Velocity = %7.2f rpm\n",
units::revolutions_per_minute_t(talon_velocity).value() );
// This will print "Talon Velocity = 2400.00 rpm"
This choice could be a bit inconvienent as well since in order to print out the value of the talon_velocity or to send it to the Network Tables (See Section 7) in RPM you must use the syntax units::revolutions_per_minute_t(talon_velocity).value(). A better approach might be to create a class that encapsulates the mechanism that the motor is used in (like a shooter). Then create a member function in that class that sets the velocity of the motor and does the necessary conversion from RPM to tics_per_100ms. So for example if the TalonFX motor was connected to a flywheel that is used to shoot a ball then you might want to create a Shooter class that looks like:
Encapsulating Unit Conversion
class Shooter {
public:
Shooter( const int canId ) : m_talon{canId} {}
void SetVelocity( units::revolutions_per_minute_t rpm ) {
m_talon.Set( ctre::phoenix::motorcontrol::ControlMode::Velocity,
rpm.value() * 2048.0 / 600.0 );
}
void Stop( void ) { m_talon.Set( ctre::phoenix::motorcontrol::TalonFXControlMode::PercentOutput, 0.0); }
private:
ctre::phoenix::motorcontrol::can::TalonFX m_talon;
};
The Shooter class is then used in the main robot program and can be called with whatever angular velocity units you want (RPM, radians per second, etc) and it will convert them to the correct units for the Set() command inside Shooter::SetVelocity().