Skip to content

Replace pneumatic intake arm deploy with motorized NEO control - #196

Merged
nlaverdure merged 31 commits into
mainfrom
MotorIntakeDeployer
Aug 15, 2026
Merged

Replace pneumatic intake arm deploy with motorized NEO control#196
nlaverdure merged 31 commits into
mainfrom
MotorIntakeDeployer

Conversation

@nlaverdure

Copy link
Copy Markdown
Member

Summary

  • Replaces the pneumatic (DoubleSolenoid) intake arm deploy mechanism with a motorized one: two NEOs driven off a single shared trapezoidal motion profile, closed-loop on each Spark's relative encoder.
  • Seeds both relative encoders at boot from the left arm's absolute encoder (the only one physically wired), gated behind a discard-then-settle moving-average window so a stale first CAN frame can't seed a bad reference.
  • Adds gravity feedforward (kG * cos(position)), modeled in both the real IO and the arm sim.
  • Bench-calibrated absEncoderOffset/minPosRad/maxPosRad against horizontal, stowed, and deployed hardstops; fixed the left/right motor inversion (was driving the relative encoders backwards) and removed a phantom absolute-encoder read on the right arm (never physically wired).
  • Adds IntakeArmIOSimSpark, replacing the old open-loop IntakeArmIOSim, so sim exercises the same seeding and profiling code path as real hardware.

Test plan

  • Compiles clean (gradlew compileJava)
  • Sim boots and runs Intake.periodic() without exception under simulateJava
  • First teleop run on the real robot: arm cycles cleanly and repeatedly between stowed and deployed, holding at both ends with no stall current
  • Right arm shows noticeably more overshoot (~9-13°) than the left when reaching each end — confirmed to be mechanical backlash on that side, not a control bug; worth keeping an eye on given soft limits currently have little measured margin past them
  • armSeedOutOfRangeAlert still fires on boot (true stowed sits slightly past the calibrated maxPosRad) — functional (seed clamps safely), but calibration could be tightened further

🤖 Generated with Claude Code

DylanTaylor29 and others added 30 commits April 2, 2026 19:18
Constants.java's UPPER_SNAKE_CASE rename (PR #192, merged from main) landed
after the motorized intake arm's CAN ports and current-limit/voltage constants
were written against the old names, so they never got renamed. Update the two
Spark IO files to match.
Intake now owns one TrapezoidProfile and advances it every periodic()
cycle; both arm IOs independently track the resulting setpoint via
their own closed-loop control. Commands (stop/deployArm/retractArm)
only move the goal state, never call setPosition() directly.
Only the left arm's Spark has an absolute encoder wired up, so
self-seeding the relative encoder inside each IntakeArmIOSpark instance
was wrong for the right side. IntakeArmIO.resetEncoder() now takes a
target position; Intake reads the left arm's absolute encoder once it
reports connected and uses that single reading to seed both arms'
relative encoders and reset the trapezoidal profile's goal/setpoint.
Intake.isDeployed() and the commented-out initializeIntakeArmCommand()
(stale from before the left/right arm split) had no callers.
IntakeArmIO.setOpenLoop()/configureSoftLimits() were only reachable
from that dead command, so drop them from the interface and both
implementations. Also removes a stray no-op semicolon in
RollerIOSpark.setOpenLoop().
Commands could move armGoal before the boot-time seed completed (or if
it never completes on a sensor fault), and the pre-seed hold-still
behavior only worked because minPosRad happened to equal the relative
encoder's power-on zero. Gate profile advancement and setPosition()
on armSeeded explicitly so the arm holds under brake mode instead of
closing a position loop against an unseeded, false-zero encoder.

Also clamp the absolute-encoder seed into the soft limit range and
raise an alert when the clamp fires, so a miscalibrated
absEncoderOffset produces a visible warning instead of silently
wedging the arm against a soft limit. Drops the unused kSlot1/kPVel
closed-loop config left over from the removed arm velocity control.
Previously a hardcoded false stub. Now true once the arm is seeded
and both sides' measured position is within STOWED_TOLERANCE_RAD of
minPosRad, mirroring the atGoal()/atSetpoint() pattern ProfiledPID
Controller uses. Gated on armSeeded so it can't report "stowed" off
an unseeded, false-zero encoder reading before boot calibration
completes.
Both arm motors' inverted flags are flipped (also flips each side's
relative encoder direction, since REVLib ties relative-encoder phase
to motor invert in brushless mode) and the absolute encoder is now
configured inverted to match. With the raw range flipped, stowed is
now the top of the [minPosRad, maxPosRad] range and deployed is the
bottom, so Intake now targets the new STOWED_POS_RAD (110°, aliases
maxPosRad) / DEPLOYED_POS_RAD (0°, aliases minPosRad) constants
instead of using minPosRad/maxPosRad directly as go-to targets. Soft
limits and clamping in the IO classes are untouched since they still
bound the same absolute [0°, 110°] range.
motorSim.iterate() already enforces the configured soft limits
(REVLib's SparkSim reads the same motorConfig.softLimit and zeroes
output past it, mirroring firmware behavior), so the manual
position/velocity snap to maxPosRad was duplicate and, unlike the
soft-limit path, discontinuously zeroed velocity instead of letting
the sim physics decelerate naturally.
Position 0 is horizontal in the current coordinate convention, so
gravity torque (and the voltage needed to hold against it) scales
with cos(position) and vanishes at vertical. Adds ArmConstants.kG and
folds kG * cos(rotation) into the feedforward voltage in both the
real and sim IO. kG defaults to 0.0 — not yet measured, so this has
no effect until tuned on the robot.
Position readback doesn't carry the hundreds-of-ms latency REV NEO
velocity measurement does (that's why velocity feedforward stays
commanded, not measured), so kG*cos() now reads the actual sensor
position: relEncoder.getPosition() in the real IO, motorSim.getPosition()
in sim.

Swaps the sim's armSim from a bare DCMotorSim (no gravity term) to
SingleJointedArmSim, which models the same cos(position) gravity
torque as a uniform rod pivoting at one end. Without this, a nonzero
kG in sim would have fought a gravity force that didn't exist; now
sim and real hardware are physically consistent for the same kG.
Adds MOMENT_OF_INERTIA_KG_M2 (carries over the previously-inline 0.004
placeholder) and ARM_LENGTH_METERS (new placeholder, ~16in) — both
unmeasured, flagged for tuning on the robot.
position/velocityMetersPerSec were carried over from the linear
RollerIO naming, but the arm's encoder conversion factors
(encoderPositionFactor = 2π/reduction, encoderVelocityFactor =
2π/(60·reduction)) produce radians and rad/s, not meters/s — confirmed
by every consumer (STOWED_POS_RAD, STOWED_TOLERANCE_RAD, Radians.of(),
RadiansPerSecond.of()) already treating them as such. Renamed to
positionRad/velocityRadPerSec, matching the convention already used by
HoodIO/TurretIO/ModuleIO for the same kind of angular measurement.
SingleJointedArmSim hardcodes the center of mass at length/2. For a
mass concentrated near the roller end rather than a uniform rod, the
value to plug in is 2x the measured pivot-to-mass-concentration
distance, not the raw measurement.
The right arm never had an absolute encoder wired up, so it was reading
noise from a floating input; stop constructing one for it. The boot-time
seed could grab a stale first CAN frame instead of a settled reading, so
gate it behind a discard-then-moving-average settle window. Swap the
left/right motor inversion flags, which had the relative encoders running
backwards, and recalibrate absEncoderOffset/minPosRad/maxPosRad from
bench measurements (five-second holds at stowed, horizontal, and
deployed) now that the sensors read correctly. Update the arm sim to
match: start stowed like the real robot, and report absolutePosition
only where the real IO does.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@nlaverdure
nlaverdure merged commit f3ca52c into main Aug 15, 2026
2 checks passed
@nlaverdure
nlaverdure deleted the MotorIntakeDeployer branch August 15, 2026 20:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants