Shooter Encoder?

What type of encoder do you use for a shooter? We are planning to use a CIM or miniCIM attached to a 1/2" HEX to power our shooter. REV has a through bore encoder but they are out of stock. I also found a mount from VEX for the SRX Mag encoder that attaches to a 1/2" shaft but I’m unsure how that would work since the shaft would not have a magnet on the end for the encoder. What would you all suggest?


I am assuming this is for a Flywheel, (as opposed to a turret or hood)

You don’t care about initial position (zeroing), direction, or crazy high pulsed per rev. So there are a lot of relative encoder setups that will probably work.

This is a bit out of my area, but I am sure additional help will be along shortly with actual part number options :slight_smile:

edit: with Magnet encoders: concentricity between the magnet and shaft isn’t nearly is important as everything being properly on axis (magnet axis and shaft axis being collinear) . Fortunately 1/2" thunderhex already has a guide-hole. (iirc, somebody please confirm this though)

Most people seem to just use the integrated encoder on NEO and Falcon motors these days. However, the most common outboard encoders used in FRC are the SRX mag encoder, the CANcoder, the MA3 absolute encoder, and the AMT10X quadrature encoder. Of those four I would only recommend against the MA3, as it’s rated for only 100RPM.

Regarding mounting: The most common approach I’ve used for magnetic encoders is to bore a 0.25" hole in the end of a hex shaft with a lathe, and then just glue the magnet in. If you want something more temporary, you can instead band-saw cut the area around the hole, then clamp around it with a shaft collar.


If you’re talking about the Hex Bore Mag Encoder Housing:

It’ll work as-is without modification.
The encoder is mounted off to the side and geared 1:1 to the hex shaft, with the magnet inside the housing.
The housing also works with both SRX Mag Encoders and CANCoders, so you’ve got some options based on what controller you’re using.


Thanks for that info. It wasn’t clear in the description. Seems like that may be the easiest way to go since the REV encoders are not in stock.

1 Like

3512 used an AMT103V (one of the ones @ClayTownRo suggested) for our 2020 shooter geared down 4:1:

We were really happy with it’s performance as a shooter encoder and will probably use it again for our 2022 shooter.


I’d recommend just using the integrated encoder on a brushless motor, and using a brushless motor instead of a CIM/miniCIM. Less weight (albeit very little), less to install and wire, less to design, one less point of failure. Will be a lot simpler / less work overall


I agree that would be ideal; however, we don’t have any brushless motors in our possession currently. We may look at getting a couple of the REV NEOs, but the Falcons are out of stock and are more than double the price of a NEO.

That’s a good-looking shooter, although I’m a bit skeptical with having the motor leads that close to a spinning belt.

They were property routed and secured once installed on the actual robot.

I wouldn’t make that blanket statement for flywheels driven by NEOs. The NEO’s built-in encoder has an 8-tap moving average filter applied with 32 ms between samples. This adds (8 - 1) / 2 * 32 ms = 112 ms of delay to encoder measurements (see for the delay equation derivations), and the filtering isn’t user overridable. 3512’s 2020 flywheel model had Kv = 0.0088813 V/(rad/s) and Ka = 0.0045649 V/(rad/s^2). Here’s a simulation of a feedforward + LQR (feedback controller) tuned for no delay, and what the optimal feedback gain is taking that delay into account.

Notice that based on the control input plot, the optimal feedback in the second case does essentially nothing, so shot recovery is going to take much longer. This is the behavior 3512 saw when we were initially testing on-motor-controller PID with the NEO in early 2020.

I’d suggest plugging an external encoder into the Spark Max data port, since the filtering for that can be overridden (the defaults still introduce 81.5 ms of delay from a 100 ms backward finite difference and a 64-tap moving average with 1 ms per sample).

Since alternate encoder support didn’t exist in the Spark Max firmware at the time, we wired a quadrature encoder to the roboRIO instead, and ran feedback on the roboRIO at 200 Hz. Here’s what our controller response ended up looking like (5 ms sample period, 0 ms delay).


Here’s the script I used to generate the plots, for completeness:

#!/usr/bin/env python3

import control as ct
import frccontrol as fct
import matplotlib.pyplot as plt
import math
import numpy as np
import scipy as sp

plt.rc("text", usetex=True)

DT = 0.001
DELAY = 0.112

class FlywheelTimeDelay(fct.System):
    def __init__(self, dt, latency_comp=False):
        """Flywheel subsystem.

        Keyword arguments:
        dt -- time between model/controller updates
        latency_comp -- True if the controller gain should be latency-compensated
        self.latency_comp = latency_comp

        state_labels = [("Angular velocity", "rad/s")]
        u_labels = [("Voltage", "V")]
        self.set_plot_labels(state_labels, u_labels)

            np.zeros((1, 1)),
            np.zeros((1, 1)),

    def create_model(self, states, inputs):
        Kv = 0.0088813
        Ka = 0.0045649

        A = np.array([[-Kv / Ka]])
        B = np.array([[1.0 / Ka]])
        C = np.array([[1]])
        D = np.array([[0]])

        return, B, C, D)

    def design_controller_observer(self):
        self.design_lqr([50], [12])

        q_vel = 200
        r_vel = (2 * math.pi * 8 / 512) / 0.005
        self.design_kalman_filter([q_vel], [r_vel])

        self.ubuf = []
        for i in range(int(DELAY / DT)):
            self.ubuf.append(np.zeros((1, 1)))

        if self.latency_comp:
            self.K = self.K @ sp.linalg.fractional_matrix_power(
                self.sysd.A - self.sysd.B @ self.K, DELAY / DT

    def update_controller(self, next_r):
        u = self.K @ (self.r - self.x_hat)
        if self.f:
            rdot = (next_r - self.r) / self.dt
            uff = self.Kff @ (rdot - self.f(self.r, np.zeros(self.u.shape)))
            uff = self.Kff @ (next_r - self.sysd.A @ self.r)
        self.r = next_r
        self.u = np.clip(u + uff, self.u_min, self.u_max)
        self.u = self.ubuf.pop(0)

def plot_time_responses(
    system, t, x_rec, ref_rec, u_rec, ndigits, title=None, use_pid_labels=False
    """Plots time-domain responses for a given system with the K_p and K_d
    controller gains included in the legend.

    Keyword arguments:
    time -- list of timesteps corresponding to references
    x_rec -- recording of state estimates from generate_time_responses()
    ref_rec -- recording of references from generate_time_responses()
    u_rec -- recording of inputs from generate_time_responses()
    ndigits -- number of digits after decimal point to include in gains
    title -- title for time-domain plots (default: no title)
    use_pid_labels -- whether to use PID controller or state-space controller
                      labels (output and setpoint vs state and reference)
    subplot_max = system.sysd.nstates + system.sysd.ninputs
    for i in range(system.sysd.nstates):
        plt.subplot(subplot_max, 1, i + 1)
        if system.sysd.nstates + system.sysd.ninputs > 3:
        if use_pid_labels:
            label = "Output"
            label = "State"
        if i == 0:
            label += f" ($K_p = {round(system.K[0, 0], ndigits)}$)"
        elif i == 1:
            label += f" ($K_d = {round(system.K[0, 1], ndigits)}$)"
        plt.plot(t, system.extract_row(x_rec, i), label=label)
        if use_pid_labels:
            label = "Setpoint"
            label = "Reference"
        plt.plot(t, system.extract_row(ref_rec, i), label=label)

    for i in range(system.sysd.ninputs):
        plt.subplot(subplot_max, 1, system.sysd.nstates + i + 1)
        if system.sysd.nstates + system.sysd.ninputs > 3:
        plt.plot(t, system.extract_row(u_rec, i), label="Control effort")
    plt.xlabel("Time (s)")

def main():
    # Set up graphing
    l0 = 0.1
    l1 = l0 + 5.0
    l2 = l1 + 0.1
    t = np.arange(0, l2 + 5.0, DT)

    refs = []

    # Generate references for simulation
    for i in range(len(t)):
        if t[i] < l0:
            r = np.array([[0]])
        elif t[i] < l1:
            r = np.array([[510]])
            r = np.array([[0]])

    flywheel = FlywheelTimeDelay(DT)
    x_rec, ref_rec, u_rec, y_rec = flywheel.generate_time_responses(t, refs)
    plot_time_responses(flywheel, t, x_rec, ref_rec, u_rec, 2)

    flywheel = FlywheelTimeDelay(DT, latency_comp=True)
    x_rec, ref_rec, u_rec, y_rec = flywheel.generate_time_responses(t, refs)
    plot_time_responses(flywheel, t, x_rec, ref_rec, u_rec, 8)

if __name__ == "__main__":

The AMT encoders are cheap, reliable, and easy to install. Dunno why they’re not more popular.

Those points made it especially helpful when we melted a couple before we decided to gear it down. Our flywheel had a top speed of 10k RPM and they’re rated to 15k RPM, but shaft wobble or something may have heated them up. We were still using an aluminum shaft at the time instead of steel, if I recall correctly.

1 Like

Fair. Don’t have the specific code available, just have experience getting the required consistency without additional encoders with both NEOs (225) and Falcons (2481)

1 Like

The flywheel mass plays a huge part. For high-inertia mechanisms like drivetrains or flywheels with higher moments of inertia and less torquey motor configurations, the measurement delay doesn’t affect the overall response as much.

If 3512 were to make another flywheel, they should definitely focus on giving it more moment of inertia to make the controls problem easier.


This topic was automatically closed 365 days after the last reply. New replies are no longer allowed.