Skip to content

Commit

Permalink
fixed some bugs when testing the odometry on real robot
Browse files Browse the repository at this point in the history
now it's working, and also works for replay
  • Loading branch information
catr1xLiu committed Jul 4, 2024
1 parent f988fb4 commit 5266e5e
Show file tree
Hide file tree
Showing 9 changed files with 160 additions and 137 deletions.
1 change: 1 addition & 0 deletions networktables.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
2 changes: 0 additions & 2 deletions src/main/java/frc/robot/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

package frc.robot;

import frc.robot.subsystems.drive.OdometryThread;

/**
* The Constants class provides a convenient place for teams to hold robot-wide numerical or boolean
* constants. This class should not be used for any other purpose. All constants should be declared
Expand Down
25 changes: 15 additions & 10 deletions src/main/java/frc/robot/subsystems/drive/Drive.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,15 @@
import edu.wpi.first.wpilibj.DriverStation;
import edu.wpi.first.wpilibj.DriverStation.Alliance;
import edu.wpi.first.wpilibj2.command.SubsystemBase;
import frc.robot.Constants;
import frc.robot.Robot;
import frc.robot.utils.LocalADStarAK;

import org.littletonrobotics.junction.AutoLogOutput;
import org.littletonrobotics.junction.Logger;

import java.util.Arrays;

public class Drive extends SubsystemBase {
private static final double MAX_LINEAR_SPEED = Units.feetToMeters(14.5);
private static final double TRACK_WIDTH_X = Units.inchesToMeters(25.0);
Expand All @@ -51,6 +55,8 @@ public class Drive extends SubsystemBase {
private SwerveDrivePoseEstimator poseEstimator =
new SwerveDrivePoseEstimator(kinematics, rawGyroRotation, lastModulePositions, new Pose2d());

private final OdometryThread odometryThread;
private final OdometryThread.OdometryThreadInputs odometryThreadInputs;
public Drive(
GyroIO gyroIO,
ModuleIO flModuleIO,
Expand All @@ -63,8 +69,9 @@ public Drive(
modules[2] = new Module(blModuleIO, 2);
modules[3] = new Module(brModuleIO, 3);

// Start threads (no-op for each if no signals have been created)
OdometryThread.getInstance().start();
this.odometryThread = OdometryThread.createInstance();
this.odometryThreadInputs = new OdometryThread.OdometryThreadInputs();
this.odometryThread.start();

// Configure AutoBuilder for PathPlanner
AutoBuilder.configureHolonomic(
Expand All @@ -87,7 +94,8 @@ MAX_LINEAR_SPEED, DRIVE_BASE_RADIUS, new ReplanningConfig()),
}

public void periodic() {
OdometryThread.fetchOdometryDataSincePreviousRobotPeriod();
odometryThread.updateInputs(odometryThreadInputs);
Logger.processInputs("Drive/OdometryThread", odometryThreadInputs);
gyroIO.updateInputs(gyroInputs);
Logger.processInputs("Drive/Gyro", gyroInputs);
for (var module : modules)
Expand All @@ -101,14 +109,11 @@ public void periodic() {
}
// Log empty setpoint states when disabled
if (DriverStation.isDisabled()) {
Logger.recordOutput("SwerveStates/Setpoints", new SwerveModuleState[]{});
Logger.recordOutput("SwerveStates/SetpointsOptimized", new SwerveModuleState[]{});
// Logger.recordOutput("SwerveStates/Setpoints");
// Logger.recordOutput("SwerveStates/SetpointsOptimized");
}

// Update odometry
double[] sampleTimestamps = new double[0]; // TODO
int sampleCount = sampleTimestamps.length;
for (int i = 0; i < sampleCount; i++) {
for (int i = 0; i < odometryThreadInputs.measurementTimeStamps.length; i++) {
// Read wheel positions and deltas from each module
SwerveModulePosition[] modulePositions = new SwerveModulePosition[4];
SwerveModulePosition[] moduleDeltas = new SwerveModulePosition[4];
Expand All @@ -133,7 +138,7 @@ public void periodic() {
}

// Apply update
poseEstimator.updateWithTime(sampleTimestamps[i], rawGyroRotation, modulePositions);
poseEstimator.updateWithTime(odometryThreadInputs.measurementTimeStamps[i], rawGyroRotation, modulePositions);
}
}

Expand Down
3 changes: 1 addition & 2 deletions src/main/java/frc/robot/subsystems/drive/GyroIOPigeon2.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,12 @@
public class GyroIOPigeon2 implements GyroIO {
private final Pigeon2 pigeon = new Pigeon2(0, Constants.ChassisConfigs.DEFAULT_CHASSIS_CANIVORE);
private final StatusSignal<Double> yaw = pigeon.getYaw();
private final OdometryThread.OdometryInput yawPositionInput;
private final OdometryThreadReal.OdometryDoubleInput yawPositionInput;
private final StatusSignal<Double> yawVelocity = pigeon.getAngularVelocityZWorld();

public GyroIOPigeon2() {
pigeon.getConfigurator().apply(new Pigeon2Configuration());
pigeon.getConfigurator().setYaw(0.0);
yaw.setUpdateFrequency(Module.ODOMETRY_FREQUENCY);
yawVelocity.setUpdateFrequency(100.0);
pigeon.optimizeBusUtilization();
yawPositionInput = OdometryThread.registerSignalInput(pigeon.getYaw());
Expand Down
11 changes: 2 additions & 9 deletions src/main/java/frc/robot/subsystems/drive/Module.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,8 @@ public Module(ModuleIO io, int index) {
setBrakeMode(true);
}

/**
* Update inputs without running the rest of the periodic logic. This is useful since these
* updates need to be properly thread-locked.
*/
public void updateInputs() {
io.updateInputs(inputs);
}

public void periodic() {
io.updateInputs(inputs);
Logger.processInputs("Drive/Module" + index, inputs);

// On first cycle, reset relative turn encoder
Expand Down Expand Up @@ -99,7 +92,7 @@ public void periodic() {
}

// Calculate positions for odometry
odometryPositions = new SwerveModulePosition[OdometryThread.getOdometryTimeStamps().length];
odometryPositions = new SwerveModulePosition[inputs.odometryDrivePositionsRad.length];
for (int i = 0; i < odometryPositions.length; i++) {
double positionMeters = inputs.odometryDrivePositionsRad[i] * WHEEL_RADIUS;
Rotation2d angle =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,13 @@
import com.revrobotics.CANSparkLowLevel.MotorType;
import com.revrobotics.CANSparkLowLevel.PeriodicFrame;
import com.revrobotics.CANSparkMax;
import com.revrobotics.REVLibError;
import com.revrobotics.RelativeEncoder;
import edu.wpi.first.math.geometry.Rotation2d;
import edu.wpi.first.math.util.Units;
import edu.wpi.first.wpilibj.AnalogInput;
import edu.wpi.first.wpilibj.RobotController;

import java.util.Arrays;
import java.util.OptionalDouble;
import java.util.Queue;

/**
* Module IO implementation for SparkMax drive motor controller, SparkMax turn motor controller (NEO
Expand All @@ -51,8 +48,8 @@ public class ModuleIOSparkMax implements ModuleIO {
private final RelativeEncoder driveEncoder;
private final RelativeEncoder turnRelativeEncoder;
private final AnalogInput turnAbsoluteEncoder;
private final OdometryThread.OdometryInput drivePositionInput;
private final OdometryThread.OdometryInput turnPositionInput;
private final OdometryThreadReal.OdometryDoubleInput drivePositionInput;
private final OdometryThreadReal.OdometryDoubleInput turnPositionInput;

private final boolean isTurnMotorInverted = true;
private final Rotation2d absoluteEncoderOffset;
Expand Down
9 changes: 3 additions & 6 deletions src/main/java/frc/robot/subsystems/drive/ModuleIOTalonFX.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import frc.robot.Constants;

import java.util.Arrays;
import java.util.Queue;

/**
* Module IO implementation for Talon FX drive motor controller, Talon FX turn motor controller, and
Expand All @@ -39,14 +38,14 @@ public class ModuleIOTalonFX implements ModuleIO {
private final CANcoder cancoder;

private final StatusSignal<Double> drivePosition;
private final OdometryThread.OdometryInput drivePositionInput;
private final OdometryThreadReal.OdometryDoubleInput drivePositionInput;
private final StatusSignal<Double> driveVelocity;
private final StatusSignal<Double> driveAppliedVolts;
private final StatusSignal<Double> driveCurrent;

private final StatusSignal<Double> turnAbsolutePosition;
private final StatusSignal<Double> turnPosition;
private final OdometryThread.OdometryInput turnPositionInput;
private final OdometryThreadReal.OdometryDoubleInput turnPositionInput;
private final StatusSignal<Double> turnVelocity;
private final StatusSignal<Double> turnAppliedVolts;
private final StatusSignal<Double> turnCurrent;
Expand Down Expand Up @@ -115,8 +114,6 @@ public ModuleIOTalonFX(int index) {
turnAppliedVolts = turnTalon.getMotorVoltage();
turnCurrent = turnTalon.getSupplyCurrent();

BaseStatusSignal.setUpdateFrequencyForAll(
Module.ODOMETRY_FREQUENCY, drivePosition, turnPosition);
BaseStatusSignal.setUpdateFrequencyForAll(
50.0,
driveVelocity,
Expand Down Expand Up @@ -158,7 +155,7 @@ public void updateInputs(ModuleIOInputs inputs) {
inputs.odometryDrivePositionsRad = Arrays.stream(drivePositionInput.getValuesSincePreviousPeriod())
.mapToDouble((Double value) -> Units.rotationsToRadians(value) / DRIVE_GEAR_RATIO)
.toArray();
inputs.odometryTurnPositions = Arrays.stream(drivePositionInput.getValuesSincePreviousPeriod())
inputs.odometryTurnPositions = Arrays.stream(turnPositionInput.getValuesSincePreviousPeriod())
.map((Double value) -> Rotation2d.fromRotations(value / TURN_GEAR_RATIO))
.toArray(Rotation2d[]::new);
}
Expand Down
156 changes: 53 additions & 103 deletions src/main/java/frc/robot/subsystems/drive/OdometryThread.java
Original file line number Diff line number Diff line change
@@ -1,142 +1,92 @@
// By 5516 Iron Maple https://github.com/Shenzhen-Robotics-Alliance/
package frc.robot.subsystems.drive;

import com.ctre.phoenix6.BaseStatusSignal;
import com.ctre.phoenix6.StatusSignal;
import frc.robot.Constants;
import frc.robot.utils.MapleTimeUtils;
import frc.robot.Robot;
import org.littletonrobotics.junction.LogTable;
import org.littletonrobotics.junction.inputs.LoggableInputs;

import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Supplier;

public class OdometryThread extends Thread {
public static final class OdometryInput {
public interface OdometryThread {
class OdometryThreadInputs implements LoggableInputs {
public double[] measurementTimeStamps = new double[0];

@Override
public void toLog(LogTable table) {
table.put("measurementTimeStamps", measurementTimeStamps);
}

@Override
public void fromLog(LogTable table) {
measurementTimeStamps = table.get("measurementTimeStamps", new double[0]);
}
}

class OdometryDoubleInput {
private final Supplier<Double> supplier;
private final Queue<Double> queue;
private Double[] valuesSincePreviousPeriod = new Double[0];

public OdometryInput(Supplier<Double> signal) {
public OdometryDoubleInput(Supplier<Double> signal) {
this.supplier = signal;
this.queue = new ArrayBlockingQueue<>(Constants.ChassisConfigs.ODOMETRY_CACHE_CAPACITY);
}

public Double[] getValuesSincePreviousPeriod() {
return valuesSincePreviousPeriod;
}
}
private static final List<OdometryInput> registeredInputs = new ArrayList<>();
private static final List<BaseStatusSignal> registeredStatusSignals = new ArrayList<>();
public static OdometryInput registerInput(Supplier<Double> supplier) {
final OdometryInput odometryInput = new OdometryInput(supplier);
registeredInputs.add(odometryInput);
return odometryInput;
}
public static OdometryInput registerSignalInput(StatusSignal<Double> signal) {
signal.setUpdateFrequency(Constants.ChassisConfigs.ODOMETRY_FREQUENCY, Constants.ChassisConfigs.ODOMETRY_WAIT_TIMEOUT_SECONDS);
final OdometryInput odometryInput = new OdometryInput(signal.asSupplier());
registeredStatusSignals.add(signal);
return odometryInput;
}

private static OdometryThread instance = null;
public static OdometryThread getInstance() {
if (instance == null)
instance = new OdometryThread(
registeredInputs.toArray(new OdometryInput[0]),
registeredStatusSignals.toArray(new BaseStatusSignal[0])
);
return instance;
}

private final OdometryInput[] odometryInputs;
private final BaseStatusSignal[] statusSignals;
private final Queue<Double> timeStampsQueue;
private Double[] timeStamps = new Double[0];
private final Lock odometryLock = new ReentrantLock();
public OdometryThread(OdometryInput[] odometryInputs, BaseStatusSignal[] statusSignals) {
this.timeStampsQueue = new ArrayBlockingQueue<>(Constants.ChassisConfigs.ODOMETRY_CACHE_CAPACITY);

this.odometryInputs = odometryInputs;
this.statusSignals = statusSignals;

setName("OdometryThread");
setDaemon(true);
}
public void cacheInputToQueue() {
queue.offer(supplier.get());
}

@Override
public synchronized void start() {
if (odometryInputs.length > 0)
super.start();
public void fetchQueueToArray() {
valuesSincePreviousPeriod = mapQueueToArray(queue);
queue.clear();
}
}

public static Double[] getOdometryTimeStamps() {
if (instance == null || !instance.isAlive())
return new Double[0];
return instance.getTimeStamps();
List<OdometryDoubleInput> registeredInputs = new ArrayList<>();
List<BaseStatusSignal> registeredStatusSignals = new ArrayList<>();
static OdometryThread.OdometryDoubleInput registerSignalInput(StatusSignal<Double> signal) {
signal.setUpdateFrequency(Constants.ChassisConfigs.ODOMETRY_FREQUENCY, Constants.ChassisConfigs.ODOMETRY_WAIT_TIMEOUT_SECONDS);
registeredStatusSignals.add(signal);
return registerInput(signal.asSupplier());
}
private Double[] getTimeStamps() {
return timeStamps;
static OdometryThread.OdometryDoubleInput registerInput(Supplier<Double> supplier) {
final OdometryThread.OdometryDoubleInput odometryDoubleInput = new OdometryThread.OdometryDoubleInput(supplier);
registeredInputs.add(odometryDoubleInput);
return odometryDoubleInput;
}

@Override
public void run() {
while (true) pollInputsInBackEnd();
static double[] mapQueueToDoubleArray(Queue<Double> queue) {
return queue.stream().mapToDouble(value -> value).toArray();
}

private void pollInputsInBackEnd() {
refreshSignalsAndBlockThread();

odometryLock.lock();
timeStampsQueue.offer(estimateAverageTimeStamps());
for(OdometryInput odometryInput:odometryInputs)
odometryInput.queue.offer(odometryInput.supplier.get());
odometryLock.unlock();
}

private void refreshSignalsAndBlockThread() {
switch (Constants.ChassisConfigs.chassisType) {
case REV -> MapleTimeUtils.delay(1.0 / Constants.ChassisConfigs.ODOMETRY_FREQUENCY);
case CTRE_ON_RIO -> {
MapleTimeUtils.delay(1.0 / Constants.ChassisConfigs.ODOMETRY_FREQUENCY);
BaseStatusSignal.refreshAll();
}
case CTRE_ON_CANIVORE -> BaseStatusSignal.waitForAll(Constants.ChassisConfigs.ODOMETRY_WAIT_TIMEOUT_SECONDS, statusSignals);
}
static Double[] mapQueueToArray(Queue<Double> queue) {
return queue.toArray(new Double[0]);
}

private double estimateAverageTimeStamps() {
double currentTime = MapleTimeUtils.getRealTimeSeconds(), totalLatency = 0;
for (BaseStatusSignal signal:statusSignals)
totalLatency += signal.getTimestamp().getLatency();

if (statusSignals.length == 0)
return currentTime;
return currentTime - totalLatency / statusSignals.length;
}
static OdometryThread createInstance() {
if (Robot.CURRENT_ROBOT_MODE == Constants.RobotMode.REAL)
return new OdometryThreadReal(
registeredInputs.toArray(new OdometryThread.OdometryDoubleInput[0]),
registeredStatusSignals.toArray(new BaseStatusSignal[0])
);
return new OdometryThread() {
@Override public void updateInputs(OdometryThreadInputs inputs) {}
@Override public void start() {}
};

public static void fetchOdometryDataSincePreviousRobotPeriod() {
if (instance != null && instance.isAlive())
instance.fetchDataSincePreviousRobotPeriod();
}
private void fetchDataSincePreviousRobotPeriod() {
odometryLock.lock();
this.timeStamps = mapQueueToArray(timeStampsQueue);
timeStampsQueue.clear();

for(OdometryInput odometryInput:odometryInputs) {
odometryInput.valuesSincePreviousPeriod = mapQueueToArray(odometryInput.queue);
odometryInput.queue.clear();
}

odometryLock.unlock();
}
void updateInputs(OdometryThreadInputs inputs);

private static Double[] mapQueueToArray(Queue<Double> queue) {
return queue.toArray(new Double[0]);
}
void start();
}
Loading

0 comments on commit 5266e5e

Please sign in to comment.