Log per-module drive/turn voltage and current for sim identification - #10
Open
AdamEXu wants to merge 1 commit into
Open
Log per-module drive/turn voltage and current for sim identification#10AdamEXu wants to merge 1 commit into
AdamEXu wants to merge 1 commit into
Conversation
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) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
We're calibrating a MuJoCo simulator of this robot (PWRDrive) so drive policies developed in sim transfer to the real thing. From the 2026-08-08 practice logs we could identify the closed-loop drive response — a 78 ms first-order lag with ~0 transport delay, and a speed-dependent acceleration droop of a = 7.34 − 1.36·v (m/s²).
What we can't identify is the open-loop model underneath: motor torque constant, effective gear/efficiency losses, and the traction limit. Those need applied voltage and current per module, and nothing in the current logs records them (
Wheels/<port>/*has requested/current speed and angle only). Without them the sim has to assume the published Kraken X60 torque curve rather than measure ours, which is exactly the kind of assumption that shows up as sim-to-real drift under hard acceleration.What this logs
Ten new
Wheels/<port>/*AdvantageKit outputs per module, added to the existinglogEverything(...)thatdrive()already calls:driveAppliedVolts/turnAppliedVoltsgetMotorVoltage()driveStatorAmps/turnStatorAmpsgetStatorCurrent()driveSupplyAmps/turnSupplyAmpsgetSupplyCurrent()driveVoltsTimestamp/driveAmpsTimestamp/turnVoltsTimestamp/turnAmpsTimestampgetTimestamp().getTime()No behaviour change:
Logger.recordOutputcalls only, all inside the existinglogEverything. No call sites were added or moved.Time alignment — the part that needs scrutiny
Verified in
wpiapi-java-26.1.0-sources.jar(CoreTalonFX.java), the three signals do not share a status frame:getMotorVoltagePRO_MotorOutput_MotorVoltagegetStatorCurrentPRO_SupplyAndTemp_StatorCurrentgetSupplyCurrentPRO_SupplyAndTemp_SupplyCurrentSo voltage arrives on one frame at 100 Hz and current on another at 50 Hz, and a 50 Hz robot loop samples both with drifting phase. The (V, I) pair used to fit motor torque can therefore be misaligned by up to a frame — material against the 78 ms time constant we're trying to identify, not ignorable noise.
Bumping the rate doesn't fix this; the two frames are still independent. Instead:
getX(false)overloads) and refreshed together withBaseStatusSignal.refreshAll(...)once perlogEverythingcall, so every logged value comes from one coherent read;*VoltsTimestampand*AmpsTimestamp— so the offline fit can resample onto a common time base instead of assuming the pair was sampled together.Two timestamps per motor is the minimum that's actually correct: stator and supply share a frame (one timestamp covers both), voltage does not.
getTimestamp()returns the most accurate source available; on the roboRIO bus that'sSystem(receive time), which is what we need for relative alignment.CAN bus load
Measured from the same logs,
SystemStats/CANBus/Utilizationon this robot is already mean 0.69, max 1.00, so this needed a real number rather than a guess.getMotorVoltage()is free. MotorOutput already ships at 100 Hz by default on CAN 2.0 — those frames are on the bus whether or not we read them. Adding the 8 voltage signals costs zero extra bandwidth, and the rate is left alone.Only SupplyAndTemp is raised, 4 Hz → 50 Hz. Because stator and supply share that frame,
setUpdateFrequencyForAllis called with only the stator signal per motor — passing supply as well would be a no-op (highest frequency wins for a shared frame). There's a comment in the code saying so, since the omission looks like a bug otherwise.Arithmetic, with assumptions stated so you can check it:
new TalonFX(id)with no bus name →"rio"; a CANivore would make this moot).+5.4 percentage points while enabled — definitive, not a range, because the shared-frame question is settled by the SPN table above. On a bus already averaging 69% that's still not something to leave on by accident, hence:
The switch
Default is
false. With the flag off: no signals are cached, no update frequency is changed, no extra frames hit the bus, and noLogger.recordOutputcalls execute — the robot behaves exactly as it does today. This is now literally true; nothing in this PR runs unconditionally.Because
kLogMotorTelemetryis a compile-time constant, javac eliminates the dead branches outright. Disassembling the built class with the flag off:The only trace left is six unused private fields. (This is also why the constants stay
static finalrather than becoming per-variant instance fields onINSTANCE— instance fields would defeat the folding and make the flag a real runtime cost on a saturated bus.) They sit with the other file-level statics at the top ofSwerveConstants, alongsidekRobotMaxSpeed/kShootingSpeedMultiplier.kMotorTelemetryHzis exposed so the rate can be dropped (25 Hz still resolves a 78 ms time constant fine) if bus headroom turns out tighter than the estimate.How to verify
Compilation: clean. This branch is based on
comp-sfde, which compiles with zero errors. This branch also compiles with zero errors and the same 8 pre-existing deprecation warnings (all inWheelMoverSpark, untouched here):(
-x buildDynamicDepsonly because that task rebuilds the vendoredautobahn_clientfrom source and 6 of its tests fail oncomp-sfdealready — unrelated, and it fails identically on the clean base.)The two changed files were also compiled directly against the real Gradle
compileClasspath(Phoenix 6 26.1.0, AdvantageKit, WPILib 2026) as a second check, and produced clean.classoutput.On the robot:
kLogMotorTelemetry = true, deploy, drive.SystemStats/CANBus/Utilization— expect roughly +5 points. Materially more than that means the frame arithmetic above is wrong andkMotorTelemetryHzshould drop to 25.Wheels/<port>/driveStatorAmpsupdates at ~50 Hz rather than stepping at 4 Hz, which proves the frequency call took effect.driveVoltsTimestampanddriveAmpsTimestampdiffer per sample — if they're identical every loop, the timestamps aren't carrying frame-level information and the alignment story needs rechecking.falsebefore any event.🤖 Generated with Claude Code
Supersedes #8, which GitHub auto-closed when its branch was renamed from
claude/tofeat/. Same commit, same review.