Skip to content

h_drive.hpp

hDrive() — the H-drive (tank base + one transverse strafe wheel), as a MatrixKinematics preset (chunk C3; the hybrid backend §13 #15: a holonomic LINEAR drive is a coefficient table, exactly as xDrive() is).

This header declares 1 type (4 members) and 1 free function.

Extracted from include/shulib/kinematics/h_drive.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 HDriveConfig

struct HDriveConfig

The H-drive's geometry + capability inputs. Lengths are REQUIRED (no invented-by-default geometry); the two ratios default to the same-hardware strafe wheel with the registered provisional derate (header note).

struct, declared at include/shulib/kinematics/h_drive.hpp:83.

HDriveConfig::trackWidth

units::Length trackWidth{}

Lateral distance between the left and right drive-wheel contact lines (> 0).

field, declared at include/shulib/kinematics/h_drive.hpp:85.

HDriveConfig::strafeWheelOffset

units::Length strafeWheelOffset{}

The strafe wheel's +FORWARD body coordinate, SIGNED (negative = mounted aft of centre). 0 = exactly on centre (orthogonal table — the historical exact path; still one preset). Finite.

field, declared at include/shulib/kinematics/h_drive.hpp:89.

HDriveConfig::strafeSpeedRatio

double strafeSpeedRatio = 1.0

Strafe-wheel top surface speed / drive-wheel top surface speed — the derivable kinematic ceiling (gearing × wheel radius). > 0, finite.

field, declared at include/shulib/kinematics/h_drive.hpp:92.

HDriveConfig::strafeTractionDerate

double strafeTractionDerate = 0.35

Sustained fraction of that ceiling the strafe wheel holds under real load on foam. In [0, 1]. PROVISIONAL (A4: HA-54) — sysid-measured at R5.

field, declared at include/shulib/kinematics/h_drive.hpp:95.

hDrive

[[nodiscard]] inline MatrixKinematics hDrive(const HDriveConfig& cfg)

Build the H-drive preset. Preconditions red-on-failure (see HDriveConfig). Wheel order: 0 = left, 1 = right, 2 = strafe (header).

free function, declared at include/shulib/kinematics/h_drive.hpp:100.

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 — 69 lines, click to expand
 hDrive() — the H-drive (tank base + one transverse strafe wheel), as a
 MatrixKinematics preset (chunk C3; the hybrid backend §13 #15: a holonomic
 LINEAR drive is a coefficient table, exactly as xDrive() is). This is the
 15″ H-bot's drivetrain — the second robot that must run the SAME motion code
 as the 24″ X-bot, unmodified (the M2 Definition of Done).

 Geometry — body frame per F1 (frame.hpp): +X = forward, +Y = left, ω CCW+.
 Canonical wheel order (3 kinematic wheels; wheel_speeds.hpp's "H=3"):
   wheel 0: LEFT drive line   — rolls along +X at body y = +trackWidth/2
   wheel 1: RIGHT drive line  — rolls along +X at body y = −trackWidth/2
   wheel 2: STRAFE wheel      — rolls along +Y at body x = strafeWheelOffset

 Rows from rigid-body kinematics (v_point = v_body + ω × r, projected on the
 wheel's powered-roll direction — the same derivation drive_plant.hpp uses for
 tracking wheels, independently):
   left   = vx − ω·(trackWidth/2)      → [1, 0, −trackWidth/2]
   right  = vx + ω·(trackWidth/2)      → [1, 0, +trackWidth/2]
   strafe = vy + ω·strafeWheelOffset   → [0, 1,  strafeWheelOffset]
 The left/right rows are TankKinematics' closed form verbatim (pinned by test);
 strafeWheelOffset is the wheel's +FORWARD body coordinate, SIGNED — the same
 convention as a Lateral tracking wheel (drive_plant.hpp TrackingWheelSpec).

 ── Why this drive is the reason the pseudo-inverse exists ──────────────────────────
 The v and turn columns have inner product v·t = strafeWheelOffset: an
 OFF-CENTRE strafe wheel (the physical norm — the centre of a 15″ chassis is
 occupied) makes the table non-orthogonal, which the pre-C3 forward() rejected.
 With the strafe wheel EXACTLY on centre the table is orthogonal and
 MatrixKinematics runs its historical exact path — both cases are one preset,
 zero special cases downstream. The 3×3 table is square full-rank (relDet =
 2w²/(2w²+a²) ≈ 0.94 for the H-bot stand-in — nowhere near the conditioning
 floor), so forward() is an EXACT inverse, not a least-squares approximation.

 ── Strafe authority: derived ceiling × registered derate ───────────────────────────
 F5 semantics (kinematics.hpp, C1 D11 — confirmed here at C3): strafeAuthority()
 is the sustainable |body vy| as a FRACTION OF THE DRIVE'S LINEAR SPEED BUDGET;
 the motion layer clamps |vy| ≤ authority·maxLinearSpeed. For the H-drive:

   authority = strafeSpeedRatio × strafeTractionDerate

   * strafeSpeedRatio — the DERIVABLE kinematic ceiling: the strafe wheel's top
     surface speed over the drive wheels' (motor free speed × gearing × wheel
     radius, each side). 1.0 when the strafe wheel shares the drive's cartridge
     and wheel size (the H-bot stand-in).
   * strafeTractionDerate — the part NO geometry can supply: one lightly-loaded
     omni pushing the whole robot's mass across foam sustains only a fraction
     of its free surface speed (traction + load, sysid-measured at R5).
     PROVISIONAL (A4: HA-54); default 0.35 = the master plan's locked
     "HDrive ≈ 0.35 sysid-measured default, not a hardcoded constant".

 Sensible limits, pinned by test: derate 0 ⇒ authority 0 (a tank with a dead
 strafe wheel — StrafeTo times out honestly, C1 D12); ratio·derate = 1 ⇒ a
 fully-symmetric drive (no extra constraint, like the X-drive); the wheel's
 POSITION does not enter authority (placement changes the ω coupling, not the
 sustainable lateral speed).

 ── Known simplifications, stated (not hidden) ──────────────────────────────────────
   * Combined strafe+rotation makes the strafe wheel serve vy + ω·a; the scalar
     authority deliberately reflects SUSTAINED PURE-STRAFE capability (the F5
     scalar cannot carry a coupled budget). Over-budget combinations are handled
     downstream by desaturate() direction-preservingly, as everywhere else.
   * v1 assumes the shared MotionConfig::maxWheelSpeed budget fits all three
     wheels (true at strafeSpeedRatio ≈ 1). A strafe wheel geared much slower
     than the drive would want a per-wheel budget — an R5/Frontier refinement,
     flagged for F6 rather than silently absorbed.

 That the BUILT 15″ H-bot matches any configured geometry is A4-registered
 (HA-55, the C3 stand-ins; HA-17's preset-geometry claim applies here too);
 R3 measures, R5 sysids the derate.

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.