step.motion.auto_tune

Auto-tune PID controllers via system identification and iterative optimization.

The full pipeline lives in C++ (raccoon.autotune.AutoTuner) so phase-to-phase state coherence is guaranteed: every phase reads its inputs from the live IMotor / Drive / UnifiedMotionPidConfig objects and writes its results back to those same objects before returning. The Python layer below is a thin orchestrator that adds UI confirmations and YAML persistence.

Phases run in this dependency order:

Phase 1 - Velocity LPF alpha (per-motor IIR filter)

Tunes the filter that downstream velocity feedback depends on, so every subsequent phase sees clean velocity estimates.

Phase 2 - Static friction (kS, PWM percent)

Sweep per motor to find the duty cycle that overcomes static friction.

Phase 3 - Firmware velocity PID (STM32 MAV-mode inner loop)

BEMF step-response identification per motor, CHR gains pushed to the STM32.

Phase 4 - Encoder calibration (ticks_to_rad)

IMU-vs-odometry sweep scales each motor’s ticks_to_rad and re-publishes the kinematics config to the STM32.

Phase 5 - Drive characterization (max velocity, accel, decel)

Raw-PWM trials measure the physical limits of every axis.

Phase 6 - Velocity controller tuning (outer chassis loop)

Step-response identification + CHR gains, ISE-validated accept/revert.

Phase 7 - Motion controller tuning (distance / heading PID)

Hooke-Jeeves coordinate descent on real LinearMotion / TurnMotion trials.

Phase 8 - Tolerance derivation (pure math)

Reads back motion-trial residuals and updates distance/angle tolerances.

Step classes:

AutoTune - full pipeline (all phases) AutoTuneVelLpf - Phase 1 only AutoTuneStaticFriction - Phase 2 only AutoTuneFirmwarePid - Phase 3 only AutoTuneVelocity - Phase 6 only AutoTuneMotion - Phase 7 only

Classes

AutoTuneProgressScreen

Live checklist of the auto-tune pipeline.

AutoTuneVelLpf

Tune the IIR velocity-filter alpha per motor (Phase 1).

AutoTuneStaticFriction

Measure per-motor static-friction threshold kS in PWM percent (Phase 2).

AutoTuneBemfVelocity

Calibrate per-motor ticks_to_rad (BEMF→rad) against the calibration board.

AutoTuneFirmwarePid

Tune per-motor STM32 MAV-mode velocity PID via BEMF step response (Phase 3).

AutoTuneVelocity

Calibrate the MCU chassis velocity-command gain per axis (Phase 6).

AutoTuneMotion

Tune motion PID controllers via iterative real-world optimization (Phase 7).

AutoTune

Run the full auto-tune pipeline.

Module Contents

class step.motion.auto_tune.AutoTuneProgressScreen(phases: list[tuple[str, str]])

Bases: raccoon.ui.UIScreen[None]

Live checklist of the auto-tune pipeline.

Shows every active phase with a status glyph so the operator can follow the pipeline’s progress on the robot screen. Reused across all phases of a single AutoTune run — the orchestrator mutates this object via mark() and re-displays it for each phase, so the checklist persists.

title = 'Auto-Tune'
status: dict[str, str]
detail: str = ''
motion_axis: str = ''
motion_control_state: str = 'hidden'
motion_toggle_callback = None
mark(key: str, status: str, detail: str = '') None

Update a phase’s status (pending/running/done/failed/skipped).

set_motion_control_state(state: str, *, axis: str = '', detail: str | None = None, callback=None) None
build() raccoon.ui.Widget

Build the screen layout.

Called on every render. Return a Widget tree describing what to display.

async on_motion_toggle_click() None
async on_motion_toggle_button_press() None
class step.motion.auto_tune.AutoTuneVelLpf(persist: bool = True)

Bases: step.Step

Tune the IIR velocity-filter alpha per motor (Phase 1).

Collects raw BEMF samples at a steady velocity, replays them through IIR low-pass filters with varying alpha, and applies the alpha that minimises a weighted noise+lag score. Runs first in the full pipeline because every downstream phase relies on the same velocity-feedback filter.

Parameters:

persist – Write tuned vel_lpf_alpha values to raccoon.project.yml. Default True.

Example:

from raccoon.step.motion import auto_tune_vel_lpf

auto_tune_vel_lpf()
persist = True
class step.motion.auto_tune.AutoTuneStaticFriction(persist: bool = True)

Bases: step.Step

Measure per-motor static-friction threshold kS in PWM percent (Phase 2).

For each drive motor the PWM is swept from a low starting percentage upward in both directions. The first PWM level where the median BEMF exceeds a motion threshold is recorded as kS.

Parameters:

persist – Reserved for future YAML persistence. Currently logs only.

Example:

from raccoon.step.motion import auto_tune_static_friction

auto_tune_static_friction()
persist = True
class step.motion.auto_tune.AutoTuneBemfVelocity(persist: bool = True, pwm_min_percent: int = 30, pwm_max_percent: int = 90, pwm_steps: int = 6, sweeps: int = 3)

Bases: step.Step

Calibrate per-motor ticks_to_rad (BEMF→rad) against the calibration board.

Fully automatic. Drives the chassis straight forward at a sweep of open-loop PWM levels (back-and-forth, staying near the start) and, for each level, compares the ground-truth distance travelled — read from the external calibration board’s optical-flow + IMU odometry — against the accumulated BEMF ticks per motor. From that it derives, per motor, ticks_to_rad = (distance / wheel_radius) / Δticks.

Crucially it does not assume the ADC-BEMF↔velocity relationship is linear: it computes the per-motor coefficient of variation of ticks_to_rad across the speed range plus an ω-vs-BEMF linear fit (slope/intercept/R²) and reports whether a single scale actually holds. If the relationship is clearly curved or offset, that is logged as a warning rather than silently persisting a misleading single value.

Prerequisites:
  • The calibration board must be connected. This step temporarily requests calibration-board odometry for the tune and restores the previous preference afterward; the tuner aborts if the board still is not the active source.

  • Roughly 1 m of clear runway forward/back.

Parameters:
  • persist – Write tuned ticks_to_rad per motor to raccoon.project.yml. Default True.

  • pwm_min_percent – Lowest PWM level in the sweep (percent). Default 30.

  • pwm_max_percent – Highest PWM level in the sweep (percent). Default 90.

  • pwm_steps – Number of evenly spaced PWM levels. Default 6.

  • sweeps – Number of full sweeps to run; points from all sweeps are pooled into one per-motor fit. More sweeps stabilise the extrapolated bemf_offset (the ω=0 intercept is noise-sensitive). Default 3.

Returns:

AutoTuneBemfVelocity step.

Example:

from raccoon.step.motion import auto_tune_bemf_velocity

auto_tune_bemf_velocity()
persist = True
pwm_min_percent = 30
pwm_max_percent = 90
pwm_steps = 6
sweeps = 3
class step.motion.auto_tune.AutoTuneFirmwarePid(persist: bool = True, max_bemf_speeds: dict[int, int] | None = None, csv_dir: str | None = '/tmp/auto_tune')

Bases: step.Step

Tune per-motor STM32 MAV-mode velocity PID via BEMF step response (Phase 3).

For each drive motor: record a BEMF step response, fit a FOPDT plant, derive CHR PID gains, push them to the firmware. Gains are accepted only when the tuned ISE is strictly smaller than the baseline ISE.

Parameters:
  • persist – Unused — gains are applied directly to firmware state.

  • max_bemf_speeds – Optional {port: max_bemf_speed} map. If unset, the C++ side runs a brief power sweep to estimate it.

  • csv_dir – When set, every step-response sample is dumped to a CSV under this directory (one per motor + phase) plus a summary CSV with the plant fit and gains. Default "/tmp/auto_tune".

Example:

from raccoon.step.motion import auto_tune_firmware_pid

auto_tune_firmware_pid()
persist = True
max_bemf_speeds = None
csv_dir = '/tmp/auto_tune'
class step.motion.auto_tune.AutoTuneVelocity(axes: list[str] | None = None, persist: bool = True)

Bases: step.Step

Calibrate the MCU chassis velocity-command gain per axis (Phase 6).

With the chassis velocity loop running on the coprocessor (forward kinematics + per-wheel MAV PID), there is no host velocity PID left to tune. What this phase tunes instead is the chassis-level command ACCURACY: it commands a mid-range body velocity, measures the achieved velocity against external ground truth (the calib board), and folds a per-axis correction gain into the STM32 forward-kinematics matrix so commanded == achieved. This compensates drivetrain efficiency the ideal geometry ignores (most notably mecanum roller slip, where wheels track their BEMF setpoint correctly yet the chassis travels less than predicted). The candidate gain is validated by re-measuring and accepted only if the effective gain moved closer to 1.0.

Prerequisites: Phase 5 (drive characterization) should have run first so a max-velocity-per-axis is known. Phases 1–4 (LPF, static friction, firmware MAV PID, ticks_to_rad) should also have run so the inner loop is stable. A calib board must be connected for the external measurement.

Parameters:
  • axes – Velocity axes to tune ("vx", "vy", "wz"). Default auto-detects from kinematics.

  • persist – Write accepted gains to raccoon.project.yml (robot.drive.kinematics.velocity_command_gain). Default True.

Example:

from raccoon.step.motion import auto_tune_velocity

auto_tune_velocity()
auto_tune_velocity(axes=["vx"])
axes = None
persist = True
class step.motion.auto_tune.AutoTuneMotion(axes: list[str] | None = None, persist: bool = True)

Bases: step.Step

Tune motion PID controllers via iterative real-world optimization (Phase 7).

Uses Hooke-Jeeves coordinate descent on the distance/heading PID kp & kd via real LinearMotion and TurnMotion trials with constraint-aware scoring.

Prerequisites: velocity controllers tuned (Phase 6) and drive limits characterized (Phase 5).

Parameters:
  • axes – Motion parameters to tune ("distance", "lateral", "heading"). Default auto-detects from kinematics.

  • persist – Write final gains to raccoon.project.yml. Default True.

Example:

from raccoon.step.motion import auto_tune_motion

auto_tune_motion()
auto_tune_motion(axes=["heading"])
axes = None
persist = True
class step.motion.auto_tune.AutoTune(vel_axes: list[str] | None = None, characterize_axes: list[str] | None = None, motion_axes: list[str] | None = None, tune_bemf_velocity: bool = True, tune_vel_lpf: bool = True, tune_static_friction: bool = True, tune_firmware_pid: bool = True, tune_encoder_cal: bool = False, tune_characterize: bool = True, tune_velocity: bool = True, tune_motion: bool = True, tune_tolerances: bool = True, pwm_min_percent: int = 30, pwm_max_percent: int = 90, pwm_steps: int = 6, sweeps: int = 2, characterize_trials: int = 3, characterize_power_percent: int = 100, persist: bool = True, step_confirm: bool = True)

Bases: raccoon.ui.UIStep

Run the full auto-tune pipeline.

Drives the C++ AutoTuner one phase at a time so UI confirmations can pause between phases. Each phase reads its inputs from the live drive / motors / motion-config objects and writes back to those same objects, so state stays coherent without Python having to shuttle calibration values.

Only the phases that are currently validated run by default. The remaining phases are kept but disabled — pass the matching tune_*=True to re-enable.

Default-enabled phases:

  • bemf_velocity — per-motor ticks_to_rad against the calibration board (the big validated win; supersedes the IMU encoder_cal).

  • vel_lpf — per-motor IIR alpha for velocity feedback.

  • static_friction — kS per motor (PWM percent).

  • firmware_pid — STM32 MAV-mode inner velocity loop.

  • characterize — max velocity / accel / decel per axis at 100% PWM, measured against calib-board ground truth (frame-independent straight-line distance).

  • velocity — MCU chassis velocity-command gain per axis: makes commanded body velocity match the calib-board-measured achieved velocity.

  • motion — distance / heading PID via real LinearMotion/TurnMotion trials (Hooke-Jeeves); linear trials return to start between runs.

  • tolerances — distance/angle tolerances derived from motion residuals.

Default-disabled phases (re-enable explicitly):

  • encoder_cal — IMU ticks_to_rad; superseded by bemf_velocity.

Parameters:
  • vel_axes – Override the auto-detected velocity axis list.

  • characterize_axes – Override the auto-detected characterize axis list.

  • motion_axes – Override the auto-detected motion-parameter list.

  • tune_bemf_velocity – Enable BEMF→velocity ticks_to_rad calibration against the calibration board. Default True.

  • tune_vel_lpf – Enable vel LPF alpha tuning. Default True.

  • tune_static_friction – Enable static friction measurement. Default True.

  • tune_firmware_pid – Enable firmware velocity PID tuning. Default True.

  • tune_encoder_cal – Enable IMU encoder calibration. Default False.

  • tune_characterize – Enable drive characterization (max vel/accel/decel per axis vs calib board). Default True.

  • tune_velocity – Enable MCU chassis velocity-command-gain calibration (commanded == achieved body velocity). Default True.

  • tune_motion – Enable motion PID tuning (distance / heading) via real LinearMotion/TurnMotion trials; linear trials return to start after each so the robot stays in place. Default True.

  • tune_tolerances – Enable tolerance derivation from motion residuals. Default True.

  • pwm_min_percent – Lowest PWM level for the bemf_velocity sweep. Default 30.

  • pwm_max_percent – Highest PWM level for the bemf_velocity sweep. Default 90.

  • pwm_steps – Number of bemf_velocity sweep PWM levels. Default 6.

  • sweeps – Number of bemf_velocity sweeps to pool. Default 2.

  • characterize_trials – Number of characterize trials per axis. Default 3.

  • characterize_power_percent – Raw PWM for characterize trials (1–100). Default 100.

  • persist – Write phase results to raccoon.project.yml. Default True.

  • step_confirm – Pause for a button press before every phase. Default True.

characterize_axes = None
vel_axes = None
motion_axes = None
tune_bemf_velocity = True
tune_vel_lpf = True
tune_static_friction = True
tune_firmware_pid = True
tune_encoder_cal = False
tune_characterize = True
tune_velocity = True
tune_motion = True
tune_tolerances = True
pwm_min_percent = 30
pwm_max_percent = 90
pwm_steps = 6
sweeps = 2
characterize_trials = 3
characterize_power_percent = 100
persist = True
step_confirm = True