Skip to content

move_to_pose.hpp

MoveToPose — decoupled per-axis field-pose motion.

This header declares 2 types (12 members).

Extracted from include/shulib/motion/move_to_pose.hpp — this page is that header's documentation, reformatted, so it cannot disagree with the code. Prose about how to think about the API lives in the user guide; worked recipes live in the cookbook; this page is the complete, mechanical list of what exists.

Contents

struct PoseMotionOptions

struct PoseMotionOptions

Internal shaping knobs for the sibling primitives (StrafeTo / HoldPose). Not part of MoveToPose's public construction surface.

struct, declared at include/shulib/motion/move_to_pose.hpp:71.

PoseMotionOptions::captureHeadingAtLive

bool captureHeadingAtLive = false

StrafeTo: hold the first-live heading

field, declared at include/shulib/motion/move_to_pose.hpp:72.

PoseMotionOptions::capturePoseAtLive

bool capturePoseAtLive = false

HoldPose: hold the first-live pose

field, declared at include/shulib/motion/move_to_pose.hpp:73.

PoseMotionOptions::holdFor

double holdFor = 0.0

0 ⇒ hold-mode exit (HoldPose)

field, declared at include/shulib/motion/move_to_pose.hpp:74.

class MoveToPose

class MoveToPose : public IMotion

Drive to a FIELD-frame pose with three INDEPENDENT controllers — field-x, field-y and heading — each closing its own loop every tick and combining into one ChassisSpeeds. The robot therefore translates and rotates simultaneously; nothing in this class sequences a turn before a drive. Arrival needs BOTH criteria at once (translation distance AND heading error), so it composes two SettledUtils and one Watchdog rather than one scalar exit. StrafeTo and HoldPose are this same engine with different capture/exit options. A MoveToPose owns no loop and no thread: the caller ticks it, having updated the Localizer first, until tick() returns something other than Running.

class, declared at include/shulib/motion/move_to_pose.hpp:86.

MoveToPose::MoveToPose

MoveToPose(const MotionDeps& deps, const math::Pose2d& target, const MotionConfig& config = {}, double timeout = 0.0)

Drive to target (FIELD frame). timeout seconds bounds the whole motion INCLUDING any boot wait; 0 selects config.defaultTimeout.

function, declared at include/shulib/motion/move_to_pose.hpp:90.

MoveToPose::start

void start() override

Arm, or fully re-arm: the three PIDs, both settle detectors and the stall check are reset, the watchdog clock restarts, and the state drops back to WaitingForEstimate. Commands no motors. A capture-at-first-live target (StrafeTo's heading, HoldPose's pose) is re-armed too, so a re-started motion captures again from the CURRENT estimate rather than reusing the previous run's. A plain MoveToPose keeps its explicit target.

function, declared at include/shulib/motion/move_to_pose.hpp:99.

MoveToPose::tick

[[nodiscard]] control::ExitReason tick() override

One control tick, and the only member here that commands a DRIVING voltage — cancel() commands the motors too, into the shared safe state, and is in fact the only member that ever changes a brake mode (this one's stops just write 0 V). Precondition: start() has been called; the loop owner must have advanced the Localizer FIRST, since this reads the estimate as the world at time t. While the estimate is still Uninitialized it commands zero volts and makes no settle progress — but the watchdog keeps running through that wait, so a never-live estimate exits TimedOut instead of hanging. Returns Running until both criteria settle (Settled) or the watchdog fires (TimedOut, MotionTimeout raised); motors are stopped BEFORE the exit record is emitted, so the record stream ends on the true final state. After any non-Running verdict this is a no-op that returns the cached verdict. Emits AT MOST one DebugRecord per call: that cached-verdict path emits nothing, and no path emits unless the sink answers wantsRecord() — the record is built inside hal::emitRecord's lambda, so against a NullSink or any log-only sink it is never populated at all. When one is emitted its commanded field is the FINAL achievable command in the FIELD frame — post-clamp, so this layer's clamping is auditable from the stream.

function, declared at include/shulib/motion/move_to_pose.hpp:132.

MoveToPose::cancel

void cancel() override

The cancel contract (motion.hpp): safe state whenever started, verdict only if still running, Idle untouched, idempotent, never raises.

function, declared at include/shulib/motion/move_to_pose.hpp:245.

MoveToPose::exitReason

[[nodiscard]] control::ExitReason exitReason() const noexcept override

The verdict cached by the last tick() or cancel() — Running until the first exit, then that exit reason for good. Reading it never recomputes anything and never advances the motion; only start() clears it back to Running.

function, declared at include/shulib/motion/move_to_pose.hpp:274.

MoveToPose::state

[[nodiscard]] MotionState state() const noexcept override

The motion-layer state, which is also written into DebugRecord.activeCommandState every tick: Idle before start(), WaitingForEstimate through the boot window, Running while controlling, then the state matching the verdict. Finer-grained than exitReason(), which cannot tell Idle from Running.

function, declared at include/shulib/motion/move_to_pose.hpp:280.

MoveToPose::name

[[nodiscard]] const char* name() const noexcept override

Always the literal "MoveToPose" — the string that identifies this motion in MotionTimeout fault text and in run result lines. The siblings override it with their own names, so a StrafeTo never reports as its base class.

function, declared at include/shulib/motion/move_to_pose.hpp:285.

MoveToPose::target

[[nodiscard]] const math::Pose2d& target() const noexcept

The FIELD-frame target (after any first-live-tick capture).

function, declared at include/shulib/motion/move_to_pose.hpp:288.

MoveToPose::setTarget

void setTarget(const math::Pose2d& target)

Retarget BEFORE start() (rebuilding a motion for a new waypoint). Precondition: not currently running.

function, declared at include/shulib/motion/move_to_pose.hpp:292.

Design commentary, from the header

The header opens with the reasoning behind these shapes. It is reproduced here in full because a reference that only lists signatures teaches nobody why.

The header’s own reasoning — 46 lines, click to expand
 MoveToPose — decoupled per-axis field-pose motion (chunk C1). THE HOLONOMIC
 THESIS, as code: three INDEPENDENT controllers — field-x, field-y, heading —
 each closing its own loop every tick, combined into one ChassisSpeeds. The
 robot translates and rotates SIMULTANEOUSLY and INDEPENDENTLY; there is no
 turn-then-drive sequencing anywhere in this file, and the simultaneity test
 pins that a diagonal move with a heading change happens as ONE motion. (That
 sequencing is precisely LemLib's tank-coupled behaviour this project exists
 to beat; C3's H-drive strafe FALLBACK is the only sanctioned exception, and
 it is telemetry-visible by contract.)

 ── The per-tick pipeline (frames and clamps annotated — the F1/F5 choreography) ────
   1. pose  = Localizer::pose()                                  [FIELD]
   2. errors: ex = tx−px, ey = ty−py (in); eh = heading.errorTo(target)
      — eh is F3's SHORTEST signed error, so the ±180° seam is absorbed
      BEFORE any controller sees a number.                       [FIELD/rad]
   3. three decoupled PIDs → (vx, vy) in/s, ω rad/s              [FIELD]
      heading PID is fed (setpoint 0, measurement −eh): the error the PID
      differentiates is CONTINUOUS near the target (no wrap in its input),
      and D-on-measurement stays live for a future kD.
   4–8. applyCommandPipeline (command_pipeline.hpp — extracted from here at
      C4 so the Chassis facade's drive() shares ONE choreography): |ω| clamp;
      uniform norm cap; fieldToRobot (THE one F1 rotation); body |vy| clamped
      to strafeAuthority()·maxLinearSpeed (the UPSTREAM clamp §13 #5 assigns
      to THIS layer — kinematics never clamps, F5; a meaningful bind flags
      strafeFallbackActive in the record, C3's never-silent contract);
      toWheels → desaturate → Feedforward → compensateForBattery → volts.
   9. OdoStallCheck + HealthMonitor::tick — the A3 containment wiring.
  10. settle/watchdog verdict; one lazy DebugRecord (A1 contract).

 ── Exit logic ──────────────────────────────────────────────────────────────────────
 Arrival needs BOTH a translation criterion (‖(ex,ey)‖) and a heading
 criterion (|eh|) — so this class composes TWO SettledUtils + ONE Watchdog
 rather than the 1-scalar ExitGroup (same tested primitives, 2+1 composition;
 ExitGroup's Settled-beats-simultaneous-TimedOut priority is preserved).
 TurnTo / DriveBrake, whose exit is one scalar, use ExitGroup unchanged.
 On the exit tick the motors are stopped BEFORE the record is emitted, so the
 record stream shows the true final state (zero command, exit state).

 ── Sibling shaping (PoseMotionOptions) ─────────────────────────────────────────────
 StrafeTo and HoldPose are this same engine with capture/exit options — the
 three share one pipeline so a fix lands once. Capture-at-first-LIVE-tick is
 part of the wait-for-live contract (motion.hpp): an estimate-derived target
 must never be read during the boot window.

 Gains/tolerances: MotionConfig — every default provisional until R5 (HA-50/51/52).

Last updated 2026-08-15 — published from the main release branch, commit fa545ff. This site tracks releases, so between them it can lag the repository — that lag is deliberate, and it is what keeps the development log off the public site. For the current state of the code, see the repository.