From 76491bcacab9fd9222764f57d8ca429c83b517c7 Mon Sep 17 00:00:00 2001 From: Adam Xu Date: Sun, 9 Aug 2026 01:09:30 -0700 Subject: [PATCH 1/2] Log per-module drive/turn voltage and current for sim identification The 2026-08-08 practice logs pin down the drivetrain's closed-loop response (78 ms first-order lag, ~0 transport delay, a = 7.34 - 1.36*v) but not the motor or traction model underneath it, because applied voltage and current are never recorded. Without them a simulator has to assume the published Kraken torque curve instead of measuring ours. Adds driveAppliedVolts/StatorAmps/SupplyAmps and the turn equivalents to WheelMoverTalonFX.logEverything, which drive() already calls. Motor voltage and the current signals live on different TalonFX status frames (MotorOutput vs SupplyAndTemp) published at different rates, so a 50 Hz loop reads each pair up to a frame apart. The signals are cached and refreshed together via BaseStatusSignal.refreshAll, and each frame's timestamp is logged so the offline fit can time-align volts against amps rather than assuming they were sampled together. Raising SupplyAndTemp from 4 Hz to 50 Hz costs bus bandwidth on a bus that already averages 69% and peaks at 100% utilization, so the whole thing is gated behind SwerveConstants.kLogMotorTelemetry, default false. Co-Authored-By: Claude Opus 5 (1M context) --- .../constant/swerve/SwerveConstants.java | 4 ++ .../frc/robot/hardware/WheelMoverTalonFX.java | 43 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/src/main/java/frc/robot/constant/swerve/SwerveConstants.java b/src/main/java/frc/robot/constant/swerve/SwerveConstants.java index a4f1cef..2743963 100644 --- a/src/main/java/frc/robot/constant/swerve/SwerveConstants.java +++ b/src/main/java/frc/robot/constant/swerve/SwerveConstants.java @@ -28,6 +28,10 @@ public final class SwerveConstants { public static final double kShootingSpeedMultiplier = 1.0; + /** Per-module volts/amps logging for offline motor-model identification; adds CAN load, so off for competition. */ + public static final boolean kLogMotorTelemetry = false; + public static final double kMotorTelemetryHz = 50.0; + ////////////////////////////////////// public Translation2d rearLeftTranslation; diff --git a/src/main/java/frc/robot/hardware/WheelMoverTalonFX.java b/src/main/java/frc/robot/hardware/WheelMoverTalonFX.java index d43f8af..c2073ba 100644 --- a/src/main/java/frc/robot/hardware/WheelMoverTalonFX.java +++ b/src/main/java/frc/robot/hardware/WheelMoverTalonFX.java @@ -2,6 +2,7 @@ import org.littletonrobotics.junction.Logger; +import com.ctre.phoenix6.BaseStatusSignal; import com.ctre.phoenix6.configs.CANcoderConfiguration; import com.ctre.phoenix6.configs.ClosedLoopGeneralConfigs; import com.ctre.phoenix6.configs.CurrentLimitsConfigs; @@ -38,6 +39,13 @@ public class WheelMoverTalonFX extends WheelMoverBase { private CANcoder turnCANcoder; + private BaseStatusSignal driveVoltsSignal; + private BaseStatusSignal driveStatorSignal; + private BaseStatusSignal driveSupplySignal; + private BaseStatusSignal turnVoltsSignal; + private BaseStatusSignal turnStatorSignal; + private BaseStatusSignal turnSupplySignal; + public WheelMoverTalonFX( int driveMotorChannel, InvertedValue driveMotorReversed, @@ -123,6 +131,21 @@ public WheelMoverTalonFX( m_turnMotor.setPosition( turnCANcoder.getAbsolutePosition().getValueAsDouble()); + + if (SwerveConstants.kLogMotorTelemetry) { + driveVoltsSignal = m_driveMotor.getMotorVoltage(false); + driveStatorSignal = m_driveMotor.getStatorCurrent(false); + driveSupplySignal = m_driveMotor.getSupplyCurrent(false); + turnVoltsSignal = m_turnMotor.getMotorVoltage(false); + turnStatorSignal = m_turnMotor.getStatorCurrent(false); + turnSupplySignal = m_turnMotor.getSupplyCurrent(false); + + // Stator and supply current share the SupplyAndTemp status frame, so raising the + // rate on stator raises it for both; passing supply too would be a no-op. Motor + // voltage is a separate frame (MotorOutput) already published at 100 Hz. + BaseStatusSignal.setUpdateFrequencyForAll( + SwerveConstants.kMotorTelemetryHz, driveStatorSignal, turnStatorSignal); + } } @Override @@ -238,6 +261,26 @@ private void logEverything(LinearVelocity requestedMps, Angle requestedAngle) { Logger.recordOutput(base + "rawCurrentAngle", rawAngle); + if (SwerveConstants.kLogMotorTelemetry) { + BaseStatusSignal.refreshAll( + driveVoltsSignal, driveStatorSignal, driveSupplySignal, + turnVoltsSignal, turnStatorSignal, turnSupplySignal); + + // Volts and amps arrive on separate status frames at different rates, so within one + // loop the pair can be up to a frame apart -- significant against the 78 ms time + // constant being identified. Log each frame's timestamp so the fit can align them. + Logger.recordOutput(base + "driveAppliedVolts", driveVoltsSignal.getValueAsDouble()); + Logger.recordOutput(base + "driveStatorAmps", driveStatorSignal.getValueAsDouble()); + Logger.recordOutput(base + "driveSupplyAmps", driveSupplySignal.getValueAsDouble()); + Logger.recordOutput(base + "driveVoltsTimestamp", driveVoltsSignal.getTimestamp().getTime()); + Logger.recordOutput(base + "driveAmpsTimestamp", driveStatorSignal.getTimestamp().getTime()); + + Logger.recordOutput(base + "turnAppliedVolts", turnVoltsSignal.getValueAsDouble()); + Logger.recordOutput(base + "turnStatorAmps", turnStatorSignal.getValueAsDouble()); + Logger.recordOutput(base + "turnSupplyAmps", turnSupplySignal.getValueAsDouble()); + Logger.recordOutput(base + "turnVoltsTimestamp", turnVoltsSignal.getTimestamp().getTime()); + Logger.recordOutput(base + "turnAmpsTimestamp", turnStatorSignal.getTimestamp().getTime()); + } } // *********************************************************************************************** From dba26adf70ca5cb422db335b15eb987b3bbce65f Mon Sep 17 00:00:00 2001 From: Adam Xu Date: Sat, 15 Aug 2026 15:21:49 -0700 Subject: [PATCH 2/2] Include wheel velocity in the coherent read and pin volts to the log rate Velocity is the dependent variable of the motor-model fit but was sampled outside the refreshAll block with no timestamp, so (volts, velocity) carried the same cross-frame skew the timestamps were added to remove. Pinning MotorOutput to kMotorTelemetryHz drops it from its 100 Hz default, roughly offsetting the SupplyAndTemp increase on a bus already peaking at full utilization. Nothing else reads MotorOutput signals. Co-Authored-By: Claude Opus 5 (1M context) --- .../constant/swerve/SwerveConstants.java | 2 +- .../frc/robot/hardware/WheelMoverTalonFX.java | 32 +++++++++++-------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/main/java/frc/robot/constant/swerve/SwerveConstants.java b/src/main/java/frc/robot/constant/swerve/SwerveConstants.java index 2743963..10a41e2 100644 --- a/src/main/java/frc/robot/constant/swerve/SwerveConstants.java +++ b/src/main/java/frc/robot/constant/swerve/SwerveConstants.java @@ -28,7 +28,7 @@ public final class SwerveConstants { public static final double kShootingSpeedMultiplier = 1.0; - /** Per-module volts/amps logging for offline motor-model identification; adds CAN load, so off for competition. */ + /** Raises TalonFX status frame rates and costs CAN bandwidth; keep off at competition. */ public static final boolean kLogMotorTelemetry = false; public static final double kMotorTelemetryHz = 50.0; diff --git a/src/main/java/frc/robot/hardware/WheelMoverTalonFX.java b/src/main/java/frc/robot/hardware/WheelMoverTalonFX.java index c2073ba..e6a53aa 100644 --- a/src/main/java/frc/robot/hardware/WheelMoverTalonFX.java +++ b/src/main/java/frc/robot/hardware/WheelMoverTalonFX.java @@ -42,9 +42,11 @@ public class WheelMoverTalonFX extends WheelMoverBase { private BaseStatusSignal driveVoltsSignal; private BaseStatusSignal driveStatorSignal; private BaseStatusSignal driveSupplySignal; + private BaseStatusSignal driveVelocitySignal; private BaseStatusSignal turnVoltsSignal; private BaseStatusSignal turnStatorSignal; private BaseStatusSignal turnSupplySignal; + private BaseStatusSignal turnVelocitySignal; public WheelMoverTalonFX( int driveMotorChannel, @@ -136,15 +138,18 @@ public WheelMoverTalonFX( driveVoltsSignal = m_driveMotor.getMotorVoltage(false); driveStatorSignal = m_driveMotor.getStatorCurrent(false); driveSupplySignal = m_driveMotor.getSupplyCurrent(false); + driveVelocitySignal = m_driveMotor.getVelocity(false); turnVoltsSignal = m_turnMotor.getMotorVoltage(false); turnStatorSignal = m_turnMotor.getStatorCurrent(false); turnSupplySignal = m_turnMotor.getSupplyCurrent(false); + turnVelocitySignal = m_turnMotor.getVelocity(false); - // Stator and supply current share the SupplyAndTemp status frame, so raising the - // rate on stator raises it for both; passing supply too would be a no-op. Motor - // voltage is a separate frame (MotorOutput) already published at 100 Hz. + // Velocity is deliberately absent: it shares a frame with position, which odometry + // reads, and pinning it here would cap that frame at kMotorTelemetryHz. BaseStatusSignal.setUpdateFrequencyForAll( - SwerveConstants.kMotorTelemetryHz, driveStatorSignal, turnStatorSignal); + SwerveConstants.kMotorTelemetryHz, + driveVoltsSignal, driveStatorSignal, driveSupplySignal, + turnVoltsSignal, turnStatorSignal, turnSupplySignal); } } @@ -263,23 +268,24 @@ private void logEverything(LinearVelocity requestedMps, Angle requestedAngle) { if (SwerveConstants.kLogMotorTelemetry) { BaseStatusSignal.refreshAll( - driveVoltsSignal, driveStatorSignal, driveSupplySignal, - turnVoltsSignal, turnStatorSignal, turnSupplySignal); + driveVoltsSignal, driveStatorSignal, driveSupplySignal, driveVelocitySignal, + turnVoltsSignal, turnStatorSignal, turnSupplySignal, turnVelocitySignal); - // Volts and amps arrive on separate status frames at different rates, so within one - // loop the pair can be up to a frame apart -- significant against the 78 ms time - // constant being identified. Log each frame's timestamp so the fit can align them. Logger.recordOutput(base + "driveAppliedVolts", driveVoltsSignal.getValueAsDouble()); Logger.recordOutput(base + "driveStatorAmps", driveStatorSignal.getValueAsDouble()); Logger.recordOutput(base + "driveSupplyAmps", driveSupplySignal.getValueAsDouble()); - Logger.recordOutput(base + "driveVoltsTimestamp", driveVoltsSignal.getTimestamp().getTime()); - Logger.recordOutput(base + "driveAmpsTimestamp", driveStatorSignal.getTimestamp().getTime()); + Logger.recordOutput(base + "driveVelocityRps", driveVelocitySignal.getValueAsDouble()); + Logger.recordOutput(base + "driveVoltsFrameTimestamp", driveVoltsSignal.getTimestamp().getTime()); + Logger.recordOutput(base + "driveAmpsFrameTimestamp", driveStatorSignal.getTimestamp().getTime()); + Logger.recordOutput(base + "driveVelocityFrameTimestamp", driveVelocitySignal.getTimestamp().getTime()); Logger.recordOutput(base + "turnAppliedVolts", turnVoltsSignal.getValueAsDouble()); Logger.recordOutput(base + "turnStatorAmps", turnStatorSignal.getValueAsDouble()); Logger.recordOutput(base + "turnSupplyAmps", turnSupplySignal.getValueAsDouble()); - Logger.recordOutput(base + "turnVoltsTimestamp", turnVoltsSignal.getTimestamp().getTime()); - Logger.recordOutput(base + "turnAmpsTimestamp", turnStatorSignal.getTimestamp().getTime()); + Logger.recordOutput(base + "turnVelocityRps", turnVelocitySignal.getValueAsDouble()); + Logger.recordOutput(base + "turnVoltsFrameTimestamp", turnVoltsSignal.getTimestamp().getTime()); + Logger.recordOutput(base + "turnAmpsFrameTimestamp", turnStatorSignal.getTimestamp().getTime()); + Logger.recordOutput(base + "turnVelocityFrameTimestamp", turnVelocitySignal.getTimestamp().getTime()); } }