Only this pageAll pages
Powered by GitBook
1 of 41

Thrifty Nova

Software Resources

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Electrical Resources

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Mechanical

Loading...

Software Releases

Loading...

Thrifty Config

Loading...

Configure Controller Settings

There are many options to configuring on the Thrifty Nova. These can be configured in the constructor using a variety of configuration setters. These setters can be chained together to make for a very clean configuration style. The following is an example of this configuration strategy.

motor = new ThriftyNova(7)   // create with CAN ID 7
  .setInverted(true)         // reverse the motor
  .setBrakeMode(true);       // set brake mode

Configuration Error Handling

After you are done setting your configurations you can check for any errors reported from the motor controllers. The motor controller will provide a list of errors encountered while configuring values. Errors are represented by a status enum. The following examples show how to access these error codes, check for certain error codes, and clear the error list.

In this example we get a list of all errors, then iterate through the errors and printing them out.

List<Error> errors = motor.getErrors();
for (Error err : errors) {
  System.out.println(err.toString());
}

You can also check if a given error is present. Here we are checking if configuring brake mode has failed.

if (motor.hasError(Errors.SET_BRake_MODE)) {
  System.out.println("Setting brake mode has failed!");
}

You can also clear this error list. It will repopulate if more errors are encountered

motor.clearErrors();

Motor Type - Minion Setting

Due to differences in motor phase wire colors, the Minion motor must run with a special config settable in Thrifty Config. This will allow teams to run either REV motors or CTRE motors without changing phase wires.

To change this on Thrifty Config:

Thrifty Config has a dropdown allowing users to select between the default REV option (for Neo-type motors) and the CTRE (Minion) mode.

To change this via the API:

Basic Setup

Example Application

You can also change motor types after initialization:

⚠️ Important: Always verify your motor type matches your physical hardware before running your robot.

Maximum Output

The user can constrain the maximum forward and reverse percent output of the motor.

Examples

If you wanted some motor to only be able to apply 25% power in both directions, I could use the following.

If you want some motor to have no constraint in going forward, but only 30% power in reverse, then you could use the second method overload.

You could also specify a limit of 50% power going forward, and not let the motor drive in reverse.

API Details

Set the maximum percent output.

Parameters

  • maxOutput The maximum absolute output in both directions in range 0 to 1.

Parameters

  • maxFwd The maximum forward output in range 0 to 1.

  • maxRev The maximum reverse output in range 0 to 1.

Brake Mode

There are two types of behaviors that the motor can take when the user specifies that it should idle. These modes are called Brake Mode and Coast Mode.

Coast Mode is the most simple behavior, and the default mode. When the motor wants to idle, power is cut from the phases, letting the kinetic energy of the motor disipate through friction until the system comes to rest.

Brake Mode turns the motor into an electric generator, which converts the kinetic energy from the system into electric energy into the circuit. It accomplishes this by shorting the phases together anytime the motor is requested to be idle.

Examples

API Details

Set the brake mode status of the motor to be either in brake mode or not in brake mode.

Parameters:

  • inverted If the motor should be in brake mode or not.

motor.setMaxOutput(0.25); // sets 25% as limit for both directions
motor.setMaxOutput(1, 0.3);
motor.setMaxOutput(0.5, 0);
setMaxOutput(double maxOutput);
setMaxOutput(double maxFwd, double maxRev);
motor.setBrakeMode(false); // keep motor in coast mode 
// ...
motor.setBrakeMode(true);  // switch to brake mode
setBrakeMode(boolean brakeMode);
javaCopy// For a NEO motor (default)
ThriftyNova neoMotor = new ThriftyNova(1);  // CAN ID 1

// For a Minion motor
ThriftyNova minionMotor = new ThriftyNova(2, ThriftyNova.MotorType.MINION);  // CAN ID 2
javaCopypublic class RobotContainer {
    private ThriftyNova flywheel = new ThriftyNova(1);       // NEO for high-speed flywheel
    private ThriftyNova climber = new ThriftyNova(2, ThriftyNova.MotorType.MINION);  // Minion for climbing
    
    public RobotContainer() {
        // Configure flywheel for high speed
        flywheel.setMaxOutput(1.0)
                .setCurrentLimit(CurrentType.STATOR, 40);
                
        // Configure climber for high torque
        climber.setCurrentLimit(CurrentType.STATOR, 60)
               .setBrakeMode(true);
    }
}
motor.setMotorType(ThriftyNova.MotorType.NEO);     // Switch to NEO
motor.setMotorType(ThriftyNova.MotorType.MINION);  // Switch to Minion

Inverting the Motor

One configuration that can be easily changed is the direction the motor turns forward. This can be set using the following method, and by providing a boolean for weather or not the motor should be inverted.

Examples

motor.setInverted(false); // default settting
// ...
motor.setInverted(true);  // swap "forward" and "reverse" 

Method Details

Set the inversion status of the motor to be either inverted or not inverted.

setInverted(boolean inverted);

Parameters:

  • inverted If the motor be inverted or not.

Follower Mode

The Thrifty Nova supports follower mode, allowing one motor controller to mirror the behavior of another. This is useful for mechanisms that need multiple synchronized motors, like a drive train or elevator.

Basic Usage

To make a motor follow another:

// Create two motor controllers
ThriftyNova leader = new ThriftyNova(1);    // CAN ID 1
ThriftyNova follower = new ThriftyNova(2);  // CAN ID 2

// Make motor 2 follow motor 1
follower.follow(1);  // Pass the CAN ID of the leader

Configuration Example

Followers can be configured like any other motor. Here's a typical setup for a drive train:

ThriftyNova leftLeader = new ThriftyNova(1)
    .setBrakeMode(true)
    .setMaxOutput(1.0);

ThriftyNova leftFollower = new ThriftyNova(2)
    .setBrakeMode(true)          // Match leader's brake mode
    .setMaxOutput(1.0)           // Match leader's max output
    .setInverted(false)          // Match leader's inversion
    .follow(leftLeader.getDeviceID());  // Follow left leader

Important Notes

  • The follower will mirror whatever control mode the leader is using (percent output, position, velocity)

  • Follower updates depend on the fault status frame rate - slower rates mean slower follower updates

  • A motor can only follow one leader at a time

  • If you call other control methods (like setPercent()) on a follower, it will stop following

Error Handling

Like other configurations, you can check if setting up follower mode failed:

if (motor.hasError(Error.SET_FOLLOWER_ID)) {
    System.out.println("Failed to set up follower mode!");
}

Current Limiting

The Thrifty Nova provides for the user to define a maximum current, where the motor will not draw any more power once a certain current threshold is crossed on either the stator or supply side.

  • Stator: Uses the total draw of phase a, b, and c from the motor side.

  • Supply: Uses the draw from the supply side

Think about current limits as protective boundaries - kind of like having both a speed limit and a weight limit on a bridge. Your motor controller needs two different types of these limits because of how brushless motors work.

When your motor is running slowly or just starting up, something interesting happens - the windings inside the motor (stator) can actually draw more current than what's coming from your battery. This gets especially important in pushing matches or when your motor is nearly stalled. Imagine your robot is in a head-to-head pushing match - your motors are working super hard but barely moving. In these near-stall conditions, they're pulling tons of current through their windings while hardly drawing anything from the battery. Without both limits in place, you could easily cook your motors without even realizing it. That's why we need both a supply current limit (protecting your battery) and a stator current limit (protecting the motor's insides). This dual protection becomes crucial when you're running at those really low speeds or in tough pushing situations.

For the Neo 550 - since it's one of the smaller motors in the family - you'll want to keep those current limits between 25-30 amps. Yes, your controller might be set to 40 amps by default, but that's too much for these little guys. The full-size Neo motors are different - they can handle brief bursts up to 60 amps, but you'll still want to keep them around 40 amps for normal match use to avoid burning them up.

Examples

To set a limit of 24 amps on stator side.

motor.setMaxCurrent(CurrentType.STATOR, 24);

To set a limit of 60 amps on stator side.

motor.setMaxCurrent(CurrentType.STATOR, 60);

To set a limit of 50 amps on supply side.

motor.setMaxCurrent(CurrentType.SUPPLY, 50);

API Details

Set the max current the motor can draw in amps. Motor speed will be capped to satisfy the max current. Also set what current reading is used for limiting calculations, between:

setMaxCurrent(CurrentType currentType, double maxCurrent)

Parameters:

  • currentType The current reading used for limiting calculations.

  • maxCurrent The max current.

Hard Limits

Hard limits can be configured by connecting limit switches to the Thrifty Nova. Call the enableHardLimits method to enable limit switch hard limits.

Examples

Enable the hard limits.

motor.enableHardLimits(true); 

Disable the hard limits.

motor.enableHardLimits(false); 

API Details

Enable / disable hard limits.

enableHardLimits(boolean enable)

Parameters:

  • enable If hard limits should be enabled.

Hard limits work through physical switches or sensors connected to your motor controller's 10-pin connector. When you hit the switch or trigger the Hall effect sensor at the end of your mechanism's travel, it immediately cuts power to the motor to prevent damage. Think of these as your last line of defense - they're your fail-safe that physically prevents the motor from pushing past its mechanical limits even if your code or soft limits somehow fail. You'll typically wire normally-closed limit switches or Hall effect sensors to these pins, so if a wire gets disconnected, the motor will safely stop instead of potentially running past its limits. These are especially important on mechanisms like elevators or arms where running past the end of travel could cause serious damage.

Ramp Up/Ramp Down

The user can specify the amount of time the motor should take to ramp up to full power and to ramp down to idle. The maximum amount of time is 10 seconds.

Examples

To set the motor to ramp up to 100% from idle over a period of 2s, use the following method.

motor.setRampUp(2);

To set the motor to ramp up to idle from 100% output over a period of 3.75s, use the following method.

motor.setRampDown(3.75);

API Details

Set Ramp Up

Sets the time to ramp up in seconds from 0 to 10. For example, an input of 0.5 will ramp the motor from idle to 100% over the course of 0.5 seconds.

setRampUp(double rampUp)

Parameters:

  • rampUp The ramp up time.

Set Ramp Down

Sets the time to ramp down in seconds from 0 to 10. For example, an input of 0.5 will ramp the motor from 100% to idle over the course of 0.5 seconds.

setRampDown(double rampDown)

Parameters:

  • rampDown The ramp up time.

Subsystem Examples

The following sections contain example implementations for various subsystems using the Thrifty Nova.

Motor Runner Board

Motor Runner Board

The motor runner board allows for reversing (via switch) and powering the motor (via the integrated slide potentiometer. The board sends a signal to the processor to enable it in "Slider Mode", turning off USB and CAN control. We designed this to make prototyping with brushless easier, just wire a ribbon cable to it and away you go!

Here is a link to the OnShape with the printed case included.

Getting Started

To get started using the Thrifty Nova you must use the ThriftyLib.

ThriftyLib only has support for Java at this time. Support for C++ and Python are coming soon.

API Install

To install the ThriftyLib beta, please paste the following link into the WPILIB VSCode:

Basic Device Usage

Before we break it down, here is a template to demonstrate simple ThriftyNova API usage.

Lets break that down...

Each Thrifty Nova is represented by an object. You must create a ThriftyNova instance variable in your subsystem to hold this object.

To actually instantiate the motor we call the ThrifyNova constructor. The constructor takes the CAN ID of the motor as a parameter.

In order to command the motor we use various setters. To set the output percentage of the motor we use the setPercent method. The method takes an argument from -1.0, full reverse, to 1.0, full forward, where 0 is idle.

Wiring the Thrifty Nova

Powering and Wiring the Thrifty Nova

To use the Thrifty Nova motor controller, you need to connect the power, communication, hall sensors, and phase wires. For the larger wires, we recommend Inline Wago Connectors, sold . Follow these steps for proper setup:

  1. Power Lines:

    • Connect the 12 AWG red wire to the positive power supply terminal.

    • Connect the 12 AWG black wire to the negative power supply terminal.

  2. Communication:

    • CAN Connection: Use the 4-pin CAN connector for CAN communication. Ensure it is properly attached.

    • USB Connection: Alternatively, use the USB-C port for USB communication. You will typically use either CAN or USB, not both.

  3. Hall Sensor Cable:

    • Connect the 6-pin JST encoder cable to the . This cable is necessary for reading the motor's hall sensors.

  4. Phase Wires:

    • Connect the three 12 AWG phase wires:

      • Red wire to red motor phase.

      • Black wire to black motor phase.

      • White wire to the white motor phase.

    • These wires should match the corresponding terminals on a NEO motor.

  5. Data Port Connector

    • For wiring external sensors like encoders and limit switches, not needed for initial bring up

    • More info found at

Required Connections:

  • Power: Red and black power lines.

  • Communication: Either the CAN connector or the USB-C port.

  • Hall Sensors: 6-pin JST encoder cable.

  • Phase Wires: Three 12 AWG wires (red, black, and white).

By following these steps, you ensure that your Thrifty Nova is correctly powered and wired for operation.

Software Releases

Software releases will be made available

Simple Elevator Example
Swerve Module Example
Simple Arm Example
public class MySubsystem extends Subsystem {
  private volatile MySubsystem instance; // singleton instance

  private ThriftyNova motor; // motor instance

  private MySubsystem() { // constructor initialization 
    motor = new ThriftyNova(7); // creates motor with CAN ID 7
  }

  public void periodic() { // update method called periodically 
    motor.setPercent(1); // set motor to output full forward 
  }

  public synchronized MySubsystem getInstance() {  // singleton getter
    return instance == null ? instance = new Instance() : instance;
}
private ThriftyNova motor;  
motor = new ThriftyNova(7);
motor.setPercent(1.0);   // 100% forward
motor.setPercent(0.75);  // 75% forward
motor.setPercent(0);     // off
motor.setPercent(-0.25); // 25% reverse
motor.setPercent(-1.0);  // 100% reverse
https://docs.home.thethriftybot.com/ThriftyLib/Latest/ThriftyLib.json
here, along with a Javadoc.
here
Brushless Hall Sensor Connector
Data Port Connector

Setting Encoder Position

You can reset the encoder on the Thrifty Nova to be at some specific value. The current position of the encoder will real as the set value, and all the other readings will be adjusted accordingly. The position needs to be passed in encoder units, so it can be wise to use conversions.

Encoder Types

The Thrifty Nova supports three types of encoders, each with different capabilities:

Encoder Type
Resolution
Position
Velocity
Notes

INTERNAL

42 counts/rev

✓

✓

Built into NEO motors

QUAD

4096 counts/rev

✓

✓

External quadrature encoder

ABS

4096 counts/rev

✓

X

External absolute encoder

Setting Up Encoders

First, tell the motor which encoder type you're using:

javaCopy// Select which encoder to use for feedback
motor.useEncoderType(EncoderType.INTERNAL);  // Built-in NEO encoder
motor.useEncoderType(EncoderType.QUAD);      // External quadrature encoder
motor.useEncoderType(EncoderType.ABS);       // External absolute encoder

Usage Examples

The following example sets the encoder to be currently at the zero position.

myEncoder.setEncoderPosition(0);

And this example sets the current position to read as 21 encoder units.

myEncoder.setEncoderPosition(21);

API Details for setEncoderPosition

Sets the encoder position.

setEncoderPosition(double position)

Parameters:

  • position The position to set the encoder to.

Get Feedback

The Thrifty Nova provides various types of feedback including encoder feedback, and current feedback.

Configuring Encoder Type for Feedback

For encoder feedback to report correctly the user must specify which encoder is being utilized. This can be set using the useEncoderType method.

One encoder type is the encoder inside the BLDC motor attached to the controller.

motor.useEncoderType(EncoderType.INTERNAL);

The encoder type can also be set to an external quadrature encoder.

motor.useEncoderType(EncoderType.QUAD);

In addition, the encoder type can also be set to an external absolute encoder, but currently only for position control and feedback.

motor.useEncoderType(EncoderType.ABS);

The following encoder functionality is work in progress:

  • Direct CAN encoder to motor controller

API Details

Set the encoder type to use for feedback control.

useEncoderType(EncoderType encoderType)

Parameters:

  • encoderType The encoder type to use.

Encoder Feedback Method Examples

You can access encoder feedback for both position and velocity measurements.

Position Feedback

Below is the method for retrieving the position feedback measurement from the encoder. This is returned as a double representing encoder units.

double position = motor.getPosition(); // measure position from the encoder

Velocity Feedback

Below is the method for retrieving the velocity feedback measurement from the encoder. This is returned as a double representing encoder units per second.

double velocity = motor.getVelocity(); // measure velocity from the encoder

Current Feedback

The Thrifty Nova also provides current feedback on both stator and supply side current. The difference is as follows:

  • Stator: Uses the total current draw of phase a, b, and c.

  • Supply: Uses the stator current draw plus the current draw of the controller itself.

Stator Current Feedback

Below is the method for retrieving the stator current feedback measurement from the motor. This is returned as an integer representing amps.

int statorCurrent = motor.getStatorCurrent(); // measure stator current from the Nova

Supply Current Feedback

Below is the method for retrieving the supply current feedback measurement from the motor. This is returned as an integer representing amps.

int supplyCurrent = motor.getSupplyCurrent(); // measure supply current from the Nova

Configure Onboard PID

The Thrifty Nova provides both position and velocity control using an onboard control loop, running on the motor controller at 1kHz. A PID must be configured in order to use position and velocity control functionality. The PID controller has an additional feed-forward term.

There are two separate builtin PID configurations. These can be individually configured, and then the active PID configuration can be selected by Selecting Active PID Configuration.

Configuring the PID Object

Below is how to set individual PID settings on the controller's first PID slot. Note that these can be chained together just like standard configuration functions.

motor.pid0.setP(0.5)    // set the proportional term to 0.5
          .setI(0.24)   // set the integral term to 0.24
          .setD(0.16)   // set the derivative term to 0.16
          .setFF(1.12); // set the feed-forward term to 1.12

Setting the configuration for the second PID slot is as simple as changing pid0 to pid1.

motor.pid1.setP(0.66)   // set the second PID's proportional term
          .setFF(1.19); // set the second PID's feed-forward term

Selecting Active PID Configuration

To select the first PID configuration, pid0, as active, use the following code.

motor.usePIDSlot(PIDSlot.SLOT0);

And to select the second PID configuration, pid1, as active, use the following code

motor.usePIDSlot(PIDSlot.SLOT1);

Use PID API Details

Set the PID slot to use for feedback control.

usePIDSlot(PIDSlot pidSlot)

Parameters:

  • encoderType The PID slot to use.

PID Configuration API Details

Functionality for configuring the Integral Zone is currently in progress on the Thrifty Nova firmware.

Set

Set the PID controller terms from a PIDController object.

set(PIDController pid)

Parameters:

  • pid The PIDController object.

Set P Term

Sets the proportional term for the PID controller.

setP(double p)

Parameters:

  • p The proportional term.

Set I-Term

Sets the integral term for the PID controller.

setI(double i)

Parameters:

  • i The integral term.

Set D-Term

Sets the derivative term for the PID controller.

setD(double d)

Parameters:

  • d The derivative term.

Set FF-Term

Sets the feed forward term for the PID controller.

setFF(double ff)

Parameters:

  • ff The feed forward term.

Factory Default

Sometimes you might need to reset your Thrifty Nova motor controller back to its original settings. The factory reset feature lets you do this with just one line of code!

Basic Usage

motor.factoryReset();

That's it! This command will reset almost all settings back to their factory defaults.

What Gets Reset?

When you perform a factory reset, these settings will return to their default values:

  • Motor inversion (returns to false)

  • Brake mode (returns to false)

  • Maximum output (returns to 100%)

  • Ramp rates (returns to 0.1 seconds)

  • Current limits (returns to 40 amps)

  • PID values (returns to 0)

  • Soft limits

  • Temperature throttling

What Doesn't Get Reset?

Some settings will stay the same even after a factory reset:

  • CAN ID

  • Device name

  • Serial number

Example Code

Here's a complete example showing how you might use factory reset in your robot code:

javaCopypublic class MySubsystem extends Subsystem {
    private ThriftyNova motor;
    
    public MySubsystem() {
        // Create our motor with CAN ID 7
        motor = new ThriftyNova(7);
        
        // Reset to factory defaults
        motor.factoryReset();
        
        // Now set up the configuration we actually want
        setupMotor();
    }
    
    private void setupMotor() {
        motor.setInverted(true)
             .setBrakeMode(true)
             .setMaxOutput(0.8)  // Limit to 80% power
             .setRampUp(0.5);    // Take 0.5 seconds to reach full power
    }
}

When Should You Use Factory Reset?

Factory reset is useful when:

  1. You're setting up a new motor controller

  2. You want to start fresh with default settings

  3. You're troubleshooting motor issues

  4. You're not sure what settings might have been changed before

💡 Tip: It's a good practice to call factoryReset() during your robot's initialization if you want to ensure you're starting with known default values.

⚠️ Important: Remember that after doing a factory reset, you'll need to reconfigure any special settings your robot needs. Don't just reset and forget!

Logging

Network Tables Logging

ThriftyNova controllers automatically support integration with NetworkTables, making it easy to view motor data in real-time using tools like Shuffleboard or Glass.

 * Enables or disables NetworkTables logging for this motor controller.
 * 
 * <p>When enabled, motor data will be automatically published to NetworkTables
 * for viewing in Shuffleboard or other dashboard applications.</p>
 * 
 * @param enabled True to enable logging, false to disable.
 */
public void setNTLogging(boolean enabled) { ... }

Example Usage:

// Enable logging for a specific motor
motor.setNTLogging(true);

// Disable logging to reduce network traffic during competition
motor.setNTLogging(false);

Default Logged Values

When NetworkTables logging is enabled, the following values are automatically published:

// Values published to NetworkTables when setNTLogging(true) is called
// and updateStatusNT() is called either manually or through periodic updates

"position"         // Current position from the selected encoder
"velocity"         // Current velocity from the selected encoder
"voltage"          // Output voltage
"stator_current"   // Stator current in amps
"supply_current"   // Supply current in amps
"temperature"      // Controller temperature in °C
"io"               // IO state as integer (can be decoded with getIOState())
"closed_loop_error" // Error between setpoint and actual position/velocity

Global Status Updates

The ThriftyNova API provides a static method to update all motor controllers' NetworkTables values at once:

/**
 * Updates the network tables for all ThriftyNova motors.
 * This is a convenient way to update all motors at the same time,
 * typically called from robotPeriodic().
 */
public static void updateStatusNTGlobal() { ... }

Example Usage in Robot Code:

@Override
public void robotPeriodic() {
    // Update all ThriftyNova motors' telemetry data
    ThriftyNova.updateStatusNTGlobal();
    
    // Other periodic robot code
    CommandScheduler.getInstance().run();
}

Individual Status Updates

You can also update NetworkTables values for individual motor controllers:

/**
 * Updates NetworkTables entries for this motor controller.
 * This calls all status getters and publishes their values to NetworkTables.
 */
public void updateStatusNT() { ... }

Example Usage:

// In a subsystem's periodic method
@Override
public void periodic() {
    // Update just this motor's telemetry
    shooterMotor.updateStatusNT();
    
    // Other subsystem code
}

Unit Conversions

The Thrifty Nova position and velocity feedback methods return using encoder units. Closed loop control uses these units as well. However oftentimes programmers want to convert to a more user friendly unit. For this we can use the Conversion class.

Creating a Conversion Object

We can create a converter by providing a human readable unit from the PositionUnit or VelocityUnit types, and the EncoderType of the encoder we are using.

 private Conversion converter1 = new Conversion(
   PositionUnit.RADIANS, EncoderType.INTERNAL);
private Conversion converter2 = new Conversion(
   VelocityUnit.ROTATIONS_PER_MIN, EncoderType.INTERNAL);

Available Units

Position Units

Unit
Description
Common Use Cases

RADIANS

Angular position in radians

Mathematical calculations, arms

DEGREES

Angular position in degrees

Human-readable angles, swerve modules

ROTATIONS

Complete rotations

Wheel rotations, continuous mechanisms

Velocity Units

Unit
Description
Common Use Cases

RADIANS_PER_SEC

Angular velocity in radians/s

Mathematical calculations

DEGREES_PER_SEC

Angular velocity in degrees/s

Angular motion control

ROTATIONS_PER_SEC

Rotations per second

High-speed mechanisms

ROTATIONS_PER_MIN

Rotations per minute

Shooters, intakes

Using the Conversion Object

To convert to encoder units for setting a value we use the toMotor method.

double targetPosition = Math.PI / 2; // radians
positionMotor.setPosition( converter1.toMotor(targetPosition) );

double targetVelocity = 2500; // RPMs
velocityMotor.setVelocity( converter2.toMotor(targetVelocity) );

To convert from encoder units for reading a value we use the fromMotor method.

double position = converter1.fromMotor(positionMotor.getPosition());
SmartDashboard.putNumber("Position (rads)", position);

double velocity = converter2.toMotor(velocityMotor.getVelocity());
SmartDashboard.putNumber("Velocity (rpms)", velocity);

To Motor Units (for Setting Values)

javaCopy// Position control examples
Conversion armConverter = new Conversion(PositionUnit.DEGREES, EncoderType.INTERNAL);
double targetAngle = 90.0;  // We want the arm at 90 degrees
motor.setPosition(armConverter.toMotor(targetAngle));

// Velocity control examples
Conversion shooterConverter = new Conversion(VelocityUnit.ROTATIONS_PER_MIN, EncoderType.INTERNAL);
double targetRPM = 3600;  // We want the shooter at 3600 RPM
motor.setVelocity(shooterConverter.toMotor(targetRPM));

From Motor Units (for Reading Values)

javaCopy// Position reading examples
Conversion armConverter = new Conversion(PositionUnit.DEGREES, EncoderType.INTERNAL);
double currentAngle = armConverter.fromMotor(motor.getPosition());
SmartDashboard.putNumber("Arm Angle", currentAngle);

// Velocity reading examples
Conversion shooterConverter = new Conversion(VelocityUnit.ROTATIONS_PER_MIN, EncoderType.INTERNAL);
double currentRPM = shooterConverter.fromMotor(motor.getVelocity());
SmartDashboard.putNumber("Shooter RPM", currentRPM);

Soft Limits

Soft limits are positions on the motor's encoder that the motor will not pass. This allows you to constrain motion between two points, helpful in many mechanisms where overrotation or overextension are dangerous.

Examples

First you must set the soft limits, the following constrains the motor between -18 and 24 encoder ticks.

motor.setSoftLimits(-18, 24); 

This example constrains the motor between 3 and 9 encoder ticks.

motor.setSoftLimits(3, 9); 

Then one must physically enable the soft limits to take effect.

motor.enableSoftLimits(true); 

You can also turn off soft limts, but still leave the limits configured on the motor controller.

motor.enableSoftLimits(false); 

API Details

Set Soft Limits

Sets the soft limits that the motor will not cross if soft limits are enabled.

setSoftLimits(double revLimit, double fwdLimit)

Parameters:

  • revLimit The reverse position the motor will not go past.

  • fwdLimit The forward position the motor will not go past.

Enable Soft Limits

Enable / disable soft limits.

enableSoftLimits(boolean enable)

Parameters:

  • enable If soft limits should be enabled.

Soft limits tell your motor controller how many revolutions your motor can safely turn before it needs to stop. Unlike hard limit switches that physically cut power at the end of travel, soft limits use the motor's built-in encoder to count rotations. You set these in your controller to match your mechanism's actual range of motion - for example, if your arm can only rotate 3.5 times before it hits its mechanical stop, you'd set your soft limit to something like 3.4 rotations. This prevents your mechanism from binding up or damaging itself by trying to push past its mechanical limits. Since these are programmed into the controller itself using encoder counts, they're always enforced - you can't override them, and they'll work consistently every time.

Set Output

The Thrifty Nova provides various ways to set the output of the motor, percent, positional, and velocity control. The following examples highlight various applications of these three control modes.

Percent Output Control

Simplest control mode - directly sets motor power:

javaCopymotor.setPercent(1.0);    // 100% forward
motor.setPercent(0.75);   // 75% forward
motor.setPercent(0);      // Off
motor.setPercent(-0.25);  // 25% reverse
motor.setPercent(-1.0);   // 100% reverse

Position Control

Moves to specific encoder positions using PID control:

javaCopy// Requires configured PID!
motor.setPosition(90);    // Go to position 90
motor.setPosition(-45);   // Go to position -45
motor.setPosition(0);     // Return to zero position

Velocity Control

Maintains specific speeds using PID control:

javaCopy// Requires configured PID!
motor.setVelocity(100);   // 100 units/second forward
motor.setVelocity(-50);   // 50 units/second reverse
motor.setVelocity(0);     // Stop with active control

Control Flow Summary

  1. Configure encoder type with useEncoderType()

  2. (Optional) Set initial position with setEncoderPosition()

  3. Use control methods:

    • setPercent() for direct power control

    • setPosition() for position control (requires PID)

    • setVelocity() for velocity control (requires PID)

  4. Read feedback with:

    • getPosition() for current position

    • getVelocity() for current speed

Important Notes

  • Position and velocity units are always in encoder-native units

  • Use the Conversion class to convert between encoder units and real-world units

  • Position/velocity control require properly configured PID

  • setPosition() sets a target for closed-loop control

  • getPosition() reads the actual measured position

  • Current readings are always in amps

  • All encoder readings are relative to the last setEncoderPosition() call

USB Communications

The USB-C Port is located on the corner of the Thrifty Nova. It provides a USB 2.0 interface and power for the Nova's internal microcontroller. For more info, see Thrifty Config.

Although you can configure your Thrifty Nova using Thrifty Config without connecting the main power, you won’t be able to spin a motor until main power is connected.

Hard Reset

Resetting the Thrifty Nova

Sometimes, you may need to restore your Thrifty Nova controller to its factory firmware. This can be done by following these steps:

  1. Prepare the USB Cable:

    • Make sure you have a USB-C cable ready.

  2. Hold Down the Reset Button:

    • Press and hold the reset button on the Thrifty Nova.

  3. Connect the USB Cable:

    • While holding the reset button, plug the USB-C cable into the Thrifty Nova and then into your computer.

    • The Thrifty Nova will appear on your computer as a removable storage device, just like a flash drive.

  4. Transfer the Reset File:

    • Drag and drop the provided .TBFW file onto the Thrifty Nova "flash drive."

    • The controller will automatically reset the firmware

Why Reset?

  • Starting Fresh: If the controller loses power when flashing firmware, the firmware can be corrupted. Restoring to default will fix this corruption.

Brushless Hall Sensor Connector

The Brushless Hall Sensor Connector is a 6-pin Port. This port is designed to accept the built-in hall-sesnors from legal FRC brushless motors.

The pinout is specified below.

Connector Pin

Pin Type

Pin Function

1

Power

Ground

2

Digital

Hall Sensor C

3

Digital

Hall Sensor B

4

Digital

Hall Sensor A

5

NC

Not Internally Connected

6

Power

+5V

Sensor Hat

Thrifty Nova Sensor Hat Board

The Breakout Board exists to run limit switches, encoders, and analog sensors into the Thrifty Nova. This board breaks the 10 pin Data Port Connector into individual 3 pin connectors which break out 5V, the signal, and ground.

For the digital signals, a normally closed sensor will trigger a green LED when the sensor is opened. This was designed to compliment the Thrifty Hall Effect, so when the red LED on the Thrifty Hall Effect (normally on) turns off at the presence of a magnet, a green LED on this board turns on.

For the analog signal, an analog buffer and a resistor divider are present to gain the full resolution of a 5V signal such as the Thrifty Absolute Magnetic Encoder.

Configure CAN Frequency

Managing CAN bus bandwidth is crucial as you add more devices to your robot. CAN 2.0 has a finite amount of bandwidth available, and each message takes up space on the bus. At 1Mbps (typical for FRC), you can theoretically transmit about 1000 frames per second under perfect conditions, but you'll want to stay well below this to avoid bus congestion.

One key way to reduce unnecessary traffic is to disable polling for sensors you aren't using. If you're not using the internal encoder, set setSensor to zero. Similarly, if you're not using an external encoder, setQuadSensor should be zero. There's no reason to waste bus bandwidth requesting data from sensors that aren't connected!

Remember that control loops (position and velocity PID) run directly on the motor controller itself at 1kHz - you don't need super fast feedback just for the control to work. However, if other mechanisms on your robot are sequencing based on position or velocity values from this motor, you'll want to set appropriate feedback rates to ensure smooth coordination. Just remember - there's rarely a reason to set any feedback faster than 10ms (100Hz).

Similarly, current limiting runs directly on the motor controller - you don't need frequent current feedback just for the limiting to work. If you're not actively monitoring current draw for debugging or logging, you can set this to a much lower frequency (like 0.2s) or even zero. The controller will still protect your motors, but you'll free up valuable bus bandwidth.

When you're first testing a mechanism, it can be helpful to temporarily increase these frequencies for debugging. But once you've verified everything works, dial those frequencies back down for competition. Think about what data you actually need and when. Position feedback for coordinated mechanisms? Keep that at 100Hz. Fault data? That can probably run at 4Hz or even slower.

Remember that every motor controller on your CAN bus adds up. If you've got six motors each sending five different types of frames, that's 30 potential messages fighting for bandwidth! And that's before counting other devices like the PDP/PDH, pneumatics hub, or other sensors. Being smart about your CAN frequencies isn't just good practice - it can mean the difference between smooth operation and weird timing hiccups in the middle of a match.

Remember: All these values represent the period in seconds between transmissions. Smaller numbers mean more frequent updates but more CAN bus utilization.

The Thrifty Nova provides functionality to modify the frequency at which each of the five categories of CAN frames are transmitted at.

These configurations can be set by accessing the motors canFreq configuration object, and by calling the appropriate method by providing the period, or the time between consecutive transmissions, in seconds.

The following example demonstrates how to modify these values.

Specific Frame Configuration Documentation

Set Fault

Set CAN frame frequency for faults.

Parameters:

  • per The period in seconds.

Set Sensor

Set CAN frame frequency for encoder feedback.

Parameters:

  • per The period in seconds.

Set Quad Sensor

Set CAN frame frequency for quadrature encoder feedback.

Parameters:

  • per The period in seconds.

Set Control

Set CAN frame frequency for control commands.

Parameters:

  • per The period in seconds.

Set Current

Set CAN frame frequency for current measurements.

Parameters:

  • per The period in seconds.

Thrifty Config Demo Video

We are actively working on our release for the Thrifty Config app, and we are excited to share more with you soon. In the meantime, here is a sneak preview of the existing functionality.

motor.canFreq           // Access the canFreq object
    .setFault(.25)      // transmit faults every .25s (250ms) (4hz)
    .setSensor(.01)     // transmit encoder feedback every .01s (10ms) (100hz)
    .setQuadSensor(.01) // same for external encoder feedback
    .setControl(.02)    // transmit control frames every .02s (20ms) (50hz)
    .setCurrent(.20);   // transmit current feedback every .2s (200ms) (5hz)
setFault(double per)
setSensor(double per)
setQuadSensor(double per)
setControl(double per)
setCurrent(double per)

LED Color Codes

The following table describes the LED status indicator patterns and their meanings:

Color
Pattern
Frequency
Meaning

Red

Alternating sides

500ms

Fatal fault

Blue

Synchronous flashing

100ms

Encoder not connected or invalid

Yellow

Alternating

500ms

Enabled but output power is 0%

Yellow

Synchronous flashing

500ms

No CAN data detected

Yellow

Solid

N/A

CAN detected but no control frames

Green

Flashing

Variable

Forward motion (flash rate = speed)

Red

Flashing

Variable

Reverse motion (flash rate = speed)

Orange

Synchronous flashing

500ms

Motor halted - high temperature (>90°C)

Orange

Synchronous flashing

50ms

Warning - approaching thermal limit (>75°C)

Note: For direction indicators (Green/Red), a solid light indicates maximum speed in the respective direction (forward for green, reverse for red).

Simple Arm Example

Below is an example of a simple arm subsystem. Visualize the arm from the side as overlaying unit circle, where zeroed arm lies on the +x axis.

public class Arm extends Subsystem {
  // CAN ID = 4
  public static int canId = 4;
  
  // How many inches the elavator tranvels
  // per radian that the motor turns
  public static double gearRatio = 17.06; 
  
  // Thrify Nova driving NEO
  private ThriftyNova pivot;
  
  // From NEO internal encoder units, to radians
  private Conversion converter; 
  
  // Enum which contains setpoint data from arm
  // assume zero position is straight out in front and up is +rotation
  // visualize as unit circle where zeroed arm lies on the +x axis
  enum SetPoint {
    TOP(67),
    MIDDLE(27),
    OTHER_SIDE(254),
    BOTTOM(-33);
    final Rotation2d theta;
    private SetPoint(double deg) { this(Rotation2d.fromDegrees(deg)); }
    private SetPoint(Rotation2d theta) { this.theta = theta; }

  }
  public SetPoint setPoint = SetPoint.BOTTOM;

  // Some constraints to not let the arm rotate past
  private static final Rotation2d LOW_CONSTRAINT = Rotation2d.fromDegrees(-33);
  private static final Rotation2d HIGH_CONSTRAINT = Rotation2d.fromDegrees(330);
  
  // Storing the real angle of the arm 
  private double realTheta = 0;
  
  private Arm() {
  
    converter = new Conversion(PositionUnit.RADIANS, EncoderType.INTERNAL);
    
    pivot = new ThriftyNova(canId) // create with CAN ID
      .setBrakeMode(true) // brake mode
      .setInverted(false) // not inverted 
      .setRampUp(0.25)    // 1/4 second ramp up
      .setRampDown(0.05)  // tiny ramp dowm
      
      .setMaxOutput(1, 0.25)  // full power for forward because the 
                              // system is fighting against gravity
                              // limits power on reverse because the 
                              // system is falling with gravity
                              
      // constrain the motor
      .setSoftLimits(
        converter.toMotor(LOW_CONSTRAINT.getRadians * gearRatio),
        converter.toMotor(HIGH_CONSTRAINT.getRadians * gearRatio)
      ) 
      .enableSoftLimits(true)        // enable the soft limits
      
      .setMaxCurrent(CurrentType.SUPPLY, 50) // set a 50amp current limit
                                             // on supply side
      
      .useEncoderType(EncoderType.INTERNAL) // use internal NEO encoder
      .usePIDSlot(PIDSlot.SLOT0);            // use the first PID slot
      
    // Configure the first PID slot
    pivot.pid0.setP(0.5)
        .setI(0)
        .setD(0)
        .setFF(1.18);

      
    // Iterate through errors and check them
    for (var err : pivot.getErrors()) {
    // The user can handle the errors
      System.err.println("Error", err.toString());
    }
    // Clear errors here
    pivot.clearErrors();
  }
  
  // This method is called periodically
  public void update() {
    // Set the position based on the setpoint
    pivot.setPosition(converter.toMotor(setPoint.pos * gearRatio));
    
    // Get the real position from the encoder
    realTheta = converter.fromMotor(pivot.getPosition()) / gearRatio;
    
    // Log the real position and the current draw
    SmartDashboard.putNumber("Arm Position", realTheta); 
    SmartDashboard.putNumber("Arm Current", pivot.getSupplyCurrent()); 
  }
  
  // Is the arm at the correct setpoint
  public boolean atSetpoint() {
    // Return true if the absolute distance is under the 2 deg tolerance
    return Math.abs(realTheta - setPoint.theta.getDegrees()) < 2; 
  } 
  
  // Begin singleton boilerplate
  private static volatile Arm instance;
  public static synchronized Arm getInstance() { 
    return instance == null ? instance = new Arm() : instance;
  }
  // End singleton boilerplate
}

Mounting Options

The Nova contains 2 tapped 10-32 holes for mounting on grid pattern tubing, 1 inch apart. There are 2 additional 6-32 holes for mounting add-on boards on top or for a lower weight mounting option.

Simple Elevator Example

Below is an example of a simple elevator subsystem. Forward motion pulls the elevator up.

public class Elevator extends Subsystem {
  // CAN ID = 2
  public static int canId = 2;
  
  // How many inches the elavator tranvels
  // per radian that the motor turns
  public static double inchesPerRad = 2.5; 
  // where theta is rotation in radians and x is travel in inches:
  // - theta(x) = x / inchesPerRad
  // - x(theta) = theta * inchesPerRad
  
  // Thrify Nova driving NEO
  private ThriftyNova pivot;
  
  // From NEO internal encoder units, to radians
  private Conversion converter; 
  
  // Enum which contains setpoint data
  enum SetPoint {
    BOTTOM(0), LOW(18), MID(32), HIGH(42);
    final double pos;
    private SetPoint(double pos) { this.pos = pos; }
  }
  public SetPoint setPoint = SetPoint.BOTTOM;
  
  // Storing the real position 
  private double realPosition = 0;
  
  private Elevator() {
  
    converter = new Conversion(PositionUnit.RADIANS, EncoderType.INTERNAL);
    
    pivot = new ThriftyNova(canId) // create with CAN ID
      .setBrakeMode(true) // brake mode
      .setInverted(false) // not inverted 
      .setRampUp(0.25)    // 1/4 second ramp up
      .setRampDown(0.05)  // tiny ramp dowm
      
      .setMaxOutput(1, 0.25)  // full power for forward because the 
                              // system is fighting against gravity
                              // limits power on reverse because the 
                              // system is falling with gravity
                              
      .setSoftLimits(0, 7 * Math.PI) // constrain the motor [0, 4pi]
      .enableSoftLimits(true)        // enable the soft limits
      
      .setMaxCurrent(CurrentType.SUPPLY, 50) // set a 50amp current limit
                                             // on supply side
      
      .useEncoderType(EncoderType.INTERNAL) // use internal NEO encoder
      .usePIDSlot(PIDSlot.SLOT0)            // use the first PID slot
      
      // Configure the first PID slot
      .pid0.setP(0.5) 
      .pid0.setI(0)
      .pid0.setD(0)
      .pid0.setFF(1.2);
      
    // Iterate through errors and check them
    for (var err : pivot.getErrors()) {
    // The user can handle the errors
      System.err.println("Error", err.toString());
    }
    // Clear errors here
    pivot.clearErrors();
  }
  
  // This method is called periodically
  public void update() {
    // Set the position based on the setpoint
    pivot.setPosition(convertor.toMotor(setPoint.pos / inchesPerRad));
    
    // Get the real position from the encoder
   realPosition = convertor.fromMotor(pivot.getPosition()) * inchesPerRad;
    
    // Log the real position and the current draw
    SmartDashboard.putNumber("Elevator Position", realPosition); 
    SmartDashboard.putNumber("Elevator Current", pivot.getCurrent()); 
  }
  
  // Is the elevator at the correct setpoint
  public boolean atSetpoint() {
    // Return true if the absolute distance is under the 1/4 inch tolerance
    return Math.abs(realPosition - setPoint.pos) < 0.25; 
  } 
  
  // Begin singleton boilerplate
  private static volatile Elevator instance;
  public static synchronized Elevator getInstance() { 
    return instance == null ? instance = new Elevator() : instance;
  }
  // End singleton boilerplate
}

Intro to Sensors

Limit Switches

A limit switch is a device that detects the presence, absence, or position of an object by opening or closing an electrical circuit when a physical force is applied to its actuator (like a lever or button).

The Thifty Bot sells a normally closed version of this called the Thrifty Hall Effect, which is a contactless magnet-based limit switch that is omnidirectional with an LED for user feedback. For more info, see the product page here.

Uses:

  • Position Sensing: Detects when the system reaches the end of its range.

  • Safety: Stops motors if the system they control moves into an unsafe position.

  • Protection: Prevents parts from moving too far and getting damaged.


Quadrature Encoders

Quadrature encoders are sensors that measure the position, speed, and direction of a rotating object using two signals that are 90 degrees apart.

How They Work:

  1. Signal Generation: The rotating shaft produces two square waves.

  2. Direction Detection: The order of the signals shows which way the shaft is turning.

  3. Position Counting: Counts the pulses of the signals to determine position.

  4. Resolution: More pulses per revolution mean more accurate measurements.


Absolute Encoders

Absolute encoders provide a unique position value for each shaft angle, ensuring that the exact position is always known, even after a power cycle.

PWM Index Pin Absolute Encoders

These encoders use Pulse Width Modulation (PWM) to convey position information. The position is encoded in the width of the pulses generated by the index pin.

How They Work:

  1. Signal Generation: The encoder generates pulses with varying widths corresponding to the shaft's position.

  2. Position Detection: The width of the PWM signal is measured to determine the exact position of the shaft.

Uses:

  • Precise Positioning: Ideal for applications requiring accurate and repeatable positioning. Think about controlling a swerve module's rotation or an arm's position.


Analog Absolute Encoders

These encoders provide position feedback as a continuous analog signal, typically a voltage or current that varies linearly with the position.

The Thrifty Bot sells one of these, and more info can be found at the product page here.

How They Work:

  1. Signal Generation: The encoder produces a continuous analog signal (in the Nova's case, 0-3.3V) proportional to the shaft position.

  2. Position Detection: The control system reads the analog signal to determine the exact position.

  3. Continuous Feedback: Provides smooth and continuous position information.

Uses:

  • Precise Positioning: Ideal for applications requiring accurate and repeatable positioning. Think about controlling a swerve module's rotation or an arm's position.

Swerve Module Example

Here is an example implementation of a swerve module using Nova. The turn controller is configured with an external absolute encoder. You would create a swerve module by calling the constructor as follows.

Below is the swerve class definition.

// Assume module1 is defined in class scope
module1 = new NovaSwerveModule(1, 4, 77); // drive ID, turn ID, encoder offset.
public class NovaSwerveModule {
  // Constants for drive and turn motors (use your own)
  private static final double MAX_VELOCITY = 4.0; // max velocity
  private static final double GEAR_RATIO = 6.75; // gear ratio drive motor to wheel
  private static final double WHEEL_CIRCUM = .1; // wheel circumference
  
  // Thrify Novas driving NEO
  private ThriftyNova drive;
  private ThriftyNova turn;

  // Unit convertors for drive velocity, drive position, and turn
  private Conversion driveConverter; 
  private Conversion distanceConverter; 
  private Conversion turnConverter; 
  
  // Store the turn encoder offset
  private double turnEncoderOffset;
  
  // Construct swerve module
  private NovaSwerveModule(int driveId, int turnId, double turnEncoderOffset) {
    this.turnEncoderOffset = turnEncoderOffset;
    initializeDriveMotor(driveId);
    initializeTurnMotor(turnId);
  }

  // Construct and configure turn motor
  private void initializeDriveMotor(int driveId) {

    // For drive we need to convert between RPM and internal NEO encoder units
    driveConverter = new Conversion(VelcotiyUnit.ROTATIONS_PER_MIN, EncoderType.INTERNAL);
    distanceConverter = new Conversion(PositionUnit.ROTATIONS, EncoderType.INTERNAL);

    drive = new ThriftyNova(driveId) // create with correct CAN ID
      .setBrakeMode(true) // brake mode
      .setInverted(false) // not inverted 
      .setRampUp(0.1)    // small ramp up used during auto 
      .setRampDown(0.1)  // same for ramp down
      .setMaxOutput(1, 1)  // full power because drive motor
      .setMaxCurrent(CurrentType.SUPPLY, 80) // set a 80amp current limit
                                             // on supply side
      .useEncoderType(EncoderType.INTERNAL) // use internal NEO encoder
      .usePIDSlot(PIDSlot.SLOT0)            // use the first PID slot
      
      // Configure the first PID slot (examples, please tune your own values)
      .pid0.setP(0.5) 
      .pid0.setI(0)
      .pid0.setD(0)
      .pid0.setFF(1.2);
      
    // Iterate through errors and check them
    for (var err : drive.getErrors()) {
    // The user can handle the errors
      System.err.println("Drive Motor Config Error: " + err.toString());
    }
    // Clear errors here
    drive.clearErrors();
  }

  // Construct and configure turn motor
  private void initializeTurnMotor(int turnId) {

    // For drive we need to convert between radians and external absolute encoder units
    turnConverter = new Conversion(PositionUnit.RADIANS, EncoderType.ABS);

    turn = new ThriftyNova(turnId) // create with correct CAN ID
      .setBrakeMode(true) // brake mode
      .setInverted(false) // not inverted 
      .setMaxOutput(1, 1)  // full power because we sometimes want to
                           // change direction quickly
      .setMaxCurrent(CurrentType.SUPPLY, 35) // set a 35amp current limit
                                             // on supply side
      .useEncoderType(EncoderType.ABS)      // use external absolute encoder
      .usePIDSlot(PIDSlot.SLOT0)            // use the first PID slot
      
      // Configure the first PID slot (examples, please tune your own values)
      .pid0.setP(0.5) 
      .pid0.setI(0)
      .pid0.setD(0)
      .pid0.setFF(1.2);
      
    // Iterate through errors and check them
    for (var err : turn.getErrors()) {
    // The user can handle the errors
      System.err.println("Drive Motor Config Error: " + err.toString());
    }
    // Clear errors here
    turn.clearErrors();
  }
  
  
  // This method is called periodically
  public void getState() {
    // Set the position based on the setpoint
    pivot.setPosition(convertor.toMotor(setPoint.pos / inchesPerRad));
    
    // Get the real position from the encoder
    double realPosition = convertor.fromMotor(pivot.getPosition()) * inchesPerRad;
    
    // Log the real position and the current draw
    SmartDashboard.putNumber("Elevator Position", realPosition); 
    SmartDashboard.putNumber("Elevator Current", pivot.getCurrent()); 
  }

  // Get position, required for swerve
  public SwerveModulePosition getPosition() {
    // Get the position of the drive motor and use the distance convertor to get the real position
    double distance = distanceConverter.fromMotor((drive.getPosition() * WHEEL_CIRCUM) / GEAR_RATIO);
    // Return the position object using the distance and rotation
    return new SwerveModulePosition(distance, getRotation());
  }

  // Get rotation, required for swerve
  public Rotation2d getRotation() {
    // Return the current rotation of the swerve rotation
    return Rotation2d.fromRadians(turnConverter.fromMotor(turn.getPosition()) - turnEncoderOffset);
  }

  // Set desired state, required for swerve
  public void setDesiredState(SwerveModuleState state) {
    // Optimize the state
    final SwerveModuleState optimized = SwerveModuleState.optimize(state, getRotation());

    // Output the desired velocity on the drive motor
    if (DriverStation.isAutonomous()) {
      // Calculate the motor velocity input
      double driveVelocity = driveConvertor.toMotor((optimized.speedMetersPerSecond * GEAR_RATIO) / WHEEL_CIRCUM);
      drive.setVelocity(driveVelocity);
    } else {
      // Calculate the motor power input
      drive.setPercent(optimized.speedMetersPerSecond / MAX_VELOCITY);
    }

    // Calculate the turn position input
    double turnPosition = turnConvertor.toMotor(optimized.angle.getRadians());
    // Output the desired angle on the turn motor
    turn.setPosition(turnPosition);
  }
}

IO Signal Management

IO and Signal Management for ThriftyNova

Overview

The ThriftyNova controller provides comprehensive access to input/output signals and status information. These features allow you to monitor limit switches, encoder indexes, and other digital signals to coordinate robot actions effectively.

Understanding IO States

The ThriftyNova tracks six digital input signals that can be accessed individually or as a group. Each represents a different signal available on the controller's inputs.

IO State Methods

Getting the Complete IO State

/**
 * Gets the complete IO state of the motor controller as a boolean array.
 * 
 * 
 * @return An array of boolean values representing the state of each IO signal.
 */
public boolean[] getIOState() { ... }

The boolean array returned by the getIOState method provides the current state of each digital input on the motor controller. Here's a breakdown of each index:

  • Index 0: Represents the state of the forward limit switch (FWD_LIMIT).

  • Index 1: Corresponds to the quadrature encoder index (QUAD_INDEX).

  • Index 2: Indicates the state of the reverse limit switch (REV_LIMIT).

  • Index 3: Reflects the alternate quadrature index (ALT_QUAD_INDEX).

  • Index 4: Signifies the status of the quadrature encoder A signal (QUAD_A).

  • Index 5: Represents the quadrature encoder B signal (QUAD_B).

Example Usage:

boolean[] ioStates = motor.getIOState();
if (ioStates[0]) {
    // Forward limit switch is active
    // Take appropriate action
}

Individual IO Signal Access

/**
 * Gets the forward limit switch state.
 * 
 * <p>This method provides a BooleanSupplier that can be used in conditions or
 * passed to other WPILib components that accept suppliers.</p>
 * 
 * @return A BooleanSupplier that returns true when the forward limit switch is active.
 */
public BooleanSupplier getForwardLimit() { ... }

/**
 * Gets the reverse limit switch state.
 * 
 * @return A BooleanSupplier that returns true when the reverse limit switch is active.
 */
public BooleanSupplier getReverseLimit() { ... }

Example Usage:

// Using in a condition
if (motor.getForwardLimit().getAsBoolean()) {
    // Forward limit is activated
}

// With Command-based framework
new ConditionalCommand(
    new StopCommand(),
    new MoveCommand(),
    motor.getForwardLimit()
);

Analog Input Support

The ThriftyNova includes support for analog input sensors through a 12-bit ADC (Analog-to-Digital Converter). This provides 4096 discrete levels of resolution, making it suitable for a wide range of analog sensors including potentiometers, distance sensors, and analog absolute encoders. The analog input can be used for position feedback or other sensor applications where continuous variable measurement is required.

/**
 * Gets the analog input reading from the sensor connected to the analog input port.
 * 
 * <p>The ThriftyNova uses a 12-bit ADC, providing values from 0-4095.</p>
 * 
 * @return The current analog reading (0-4095)
 */
public int getAnalogInput() { ... }

/**
 * Gets the analog input reading scaled to a position value.
 * 
 * <p>This can be used when an analog sensor (like a potentiometer) is being
 * used as a position feedback device.</p>
 * 
 * @return The position value derived from the analog input
 */
public double getPositionAnalog() { ... }

Encoder Signal Access

The ThriftyNova also exposes encoder signals as digital inputs. These are primarily intended for monitoring the state of the encoder lines, not for position or velocity tracking.

Note: For tracking encoder position and velocity, use the dedicated methods like getPosition() and getVelocity() rather than these boolean suppliers. These methods provide the actual encoder values that should be used for control and feedback.

/**
 * Gets the quadrature encoder index pulse state.
 * 
 * <p>The index pulse is a reference marker that occurs once per revolution in many
 * quadrature encoders. It is generally saved between power cycles on absolute encoders</p>
 * 
 * @return A BooleanSupplier that returns true when the quadrature index pulse is detected.
 */
public BooleanSupplier getQuadIndex() { ... }

/**
 * Gets the alternate quadrature encoder index state.
 * 
 * @return A BooleanSupplier that returns true when the alternate quadrature index is detected.
 */
public BooleanSupplier getAltQuadIndex() { ... }

/**
 * Gets the quadrature encoder A channel state.
 * 
 * @return A BooleanSupplier that returns true when the quadrature A signal is high.
 */
public BooleanSupplier getQuadA() { ... }

/**
 * Gets the quadrature encoder B channel state.
 * 
 * @return A BooleanSupplier that returns true when the quadrature B signal is high.
 */
public BooleanSupplier getQuadB() { ... }

Encoder Position Handling

The ThriftyNova supports multiple types of position feedback:

  • Internal encoder (built into brushless motors like the NEO)

  • Quadrature encoder (external)

  • Absolute encoder (external)

Each type has specific methods to access their position values:

/**
 * Gets the position from the currently selected encoder type.
 * 
 * <p>This method returns position data from whichever encoder type was set
 * with the {@code useEncoderType()} method.</p>
 * 
 * @return The current position in encoder ticks.
 */
public double getPosition() { ... }

/**
 * Gets the internal encoder position value.
 * 
 * <p>For NEO motors, this represents the position of the motor in ticks,
 * regardless of which encoder type is selected for control.</p>
 * 
 * @return The internal encoder position in ticks.
 */
public double getPositionInternal() { ... }

/**
 * Gets the quadrature encoder position.
 * 
 * <p>Returns the position from an external quadrature encoder connected
 * to the ThriftyNova's encoder inputs.</p>
 * 
 * @return The quadrature encoder position in ticks.
 */
public double getPositionQuad() { ... }

/**
 * Gets the absolute encoder position.
 * 
 * <p>Returns the position from an external absolute encoder (like a REV Through Bore Encoder)
 * connected to the ThriftyNova's encoder inputs.</p>
 * 
 * @return The absolute encoder position in ticks.
 */
public double getPositionAbs() { ... }

Best Practices for IO Signal Usage

Integrating IO Signals With WPILib

The ThriftyNova's IO methods return BooleanSuppliers, making them compatible with WPILib's Command-based programming. You can use these directly in conditionals or pass them to commands.

// Stop the motor when the forward limit is reached
new RunCommand(() -> motor.set(0.5))
    .until(motor.getForwardLimit());

// Or use with a trigger
new Trigger(motor.getForwardLimit())
    .onTrue(new InstantCommand(() -> System.out.println("Limit reached!")));

Efficiently Polling IO Signals

Like other status frames, you can control how frequently IO data is sent over the CAN bus. If you're using limit switches for critical safety stops, you'll want a higher polling rate, but if you're just using them for homing sequences, you can use a lower rate.

Consider how your robot uses these signals:

  • For emergency stops or critical limits: 10-20ms (50-100Hz)

  • For position reference or homing: 50-100ms (10-20Hz)

  • For diagnostic information only: 200-250ms (4-5Hz)

Remember that IO states are included in the current frame, so you can control their update rate using motor.canFreq.setCurrent().

// Configure polling rates based on your needs
motor.canFreq
    .setCurrent(0.01)       // Poll internal encoder at 100Hz

Quick Tips for Encoder and IO Usage

  1. For mechanisms with hard stops (like an arm or elevator):

    • Use limit switches connected to ThriftyNova inputs

    • Check limit switch states with getForwardLimit() and getReverseLimit()

    • When a limit is reached, zero your encoder with setEncoderPosition(0)

  2. For tracking mechanisms through full rotations:

    • External encoders often provide more precision than internal encoders

    • Use getQuadIndex() to detect when an encoder passes its index pulse

    • This can be used to know when a full rotation has occurred

  3. Remember that digital inputs can be used for more than just limit switches:

    • Connect beam break sensors to detect game pieces

    • Use proximity sensors for position detection

    • Implement end-of-travel detection for moving mechanisms

10 Pin Data Connector

Pinout of Data Port Connector

Connector Pin

Pin Type

Pin Function

1

Power

+3.3V

2

Power

+5V

3

Analog

Analog Input (0-3.3V)

4

Digital

Forward Limit Switch Input

5

Digital

Encoder B

6

Digital

Multi-function Pin

7

Digital

Encoder A

8

Digital

Reverse Limit Switch Input

9

Digital

Absolute Encoder Index

10

Ground

Ground

For more info on these types of sensors, see Intro to Sensors

Add on Boards:

For ease of wiring, we provide a breakout board, detailed at Sensor Hat Board

To run the motor with no code or computer, use our Motor Runner Board