3 — Timing and teammates¶
Your autonomous period is short and there is another robot on your alliance. These recipes are about both facts.
This page deliberately names no period length. A match's autonomous phase and an Autonomous Coding Skills run are different lengths, and the library holds no default match length by design — the run guard makes you supply both instants yourself. Substitute your own number wherever a budget appears below.
Listings are compiled and run in
test/cookbook_examples_test.cpp, cases cookbook-03a
and cookbook-03b.
Wait for your alliance partner¶
Use this when: your path crosses your partner's and one of you has to go second.
// 1. A fixed beat: sit still, motors idle, while your partner clears out.
const double beforePause = c.rig.h.clock().now().value();
r.pause(750_ms);
Why pause and not hold. They sound alike and do opposite things:
pause(750_ms)leaves the drive exactly as the last step left it — after a settled motion, stopped and unpowered. Time passes; the robot does nothing.hold(750_ms)actively drives to stay where it is, fighting anything that pushes it. Use it when a defender is about to lean on you; do not use it to let time pass.
Watch out for: typing the duration. pause(750) does not compile — durations are typed, so
"750, meaning milliseconds" cannot silently become 750 seconds inside a period measured in
tens of them. Write 750_ms or 0.75_s.
Wait for a condition, with a deadline¶
Use this when: the next step is meaningless until something is true, and continuing without it would be acting on a state the field never reached.
// 2. A condition, with a deadline. If the deadline passes first the chain
// STOPS — waitFor() means "the next step assumes this happened".
r.waitFor([&lane] { return lane.clear(); }, 2_s, "partner-clear");
The condition is yours. shulib knows nothing about your partner, so in the compiled example it is a stand-in struct:
On a real robot that becomes a distance sensor reading, a line sensor, or a button your driver presses.
Why the deadline is required. An unbounded wait is a hang in a costume. The autonomous period is the entire budget; a wait with no deadline can consume all of it and produce nothing.
Why timing out stops the chain. waitFor means "the step after this one assumes the
condition holds". If the deadline passes with the condition still false, running that next step
would be acting out a plan against a world that never arrived. If the timeout is a legitimate
strategy branch rather than a failure, you want the next recipe instead.
Wait, but go anyway¶
Use this when: the thing you are waiting for is nice to have, not required — "give my partner half a second, then go regardless".
// 3. "Wait, but go anyway": that is NOT waitFor. Drop one tier inside a
// then() — waitUntil returns a verdict you deliberately discard.
const double beforeGiveUp = c.rig.h.clock().now().value();
r.then([&c] { (void)c.chassis.waitUntil([] { return false; }, 400_ms); },
"partner-or-not");
Why it is written this way. waitUntil is the full API's wait: it returns whether the
condition became true, and it never stops anything. Wrapping it in a void action makes it a
chain step that always succeeds. The (void) cast is not decoration — waitUntil's result is
marked must-use precisely so you cannot ignore it by accident, and this is the one place you are
ignoring it on purpose.
Watch out for: reaching for this when you actually meant waitFor. "Go anyway" is a real
strategy and a real bug, depending on what the next step assumes. Write down which one you meant
in the step name — the log will show it.
Fit the match window¶
Use this when: your routine is longer than the autonomous period allows, or might become so when one leg goes slowly.
The problem, stated plainly: a Routine has no whole-chain deadline. Every step has its own
timeout, and they are independent. If an early leg burns six seconds fighting a defender, every
later leg still gets its full budget, and the routine can run well past the buzzer.
Since the sequence layer landed, the unconditional part of this has an owner. Wrap the whole auton in the run guard: you supply two instants and the final act, the guard cuts scoring at the first instant, refuses anything started after it, runs your act, and forces everything safe at the second instant no matter what. The stalled-loop case — the one the phased recipe below never solved — is exactly what it is for:
shulib::sequence::RunGuard guard{pacer}; // wraps YOUR tick pacer
Chassis chassis{deps, guard, config}; // the guard IS the pacer
const Pose2d endPose{90_in, -24_in, 0_deg}; // YOUR end position (a fixture here)
const RunGuardReport report = guard.run(
chassis,
{.endActionAt = 12_s, .hardStopAt = 14.5_s}, // YOUR schedule — no defaults exist
[&] {
Routine r{chassis, "skills"};
r.startAt(Pose2d{0_in, 0_in, 0_deg})
.moveTo(Pose2d{300_in, 0_in, 0_deg},
{.timeout = 60_s, .maxLinearSpeed = Velocity{10.0}})
.moveTo(Pose2d{0_in, 24_in, 90_deg}, {.timeout = 6_s});
},
[&] {
Routine end{chassis, "skills/end"};
end.moveTo(endPose, {.timeout = 4_s}).brake({.timeout = 1_s});
return end.ok();
});
if (!report.endActionSucceeded) { /* transcript already says why (SEQ lines) */ }
Both instants are measured from guard.run()'s start, both are required (the library has no
default match length and no default lead time to offer — those are your measurements), and the
guard must be the pacer your Chassis was constructed with, or it is not in the loop at all
(it warns loudly after the fact if so). What "guaranteed" does and does not cover is stated in
full in guide chapter 14 — read it before trusting the
word; the two limits that matter daily are that frozen waits (pause/waitFor) still pay their
own remaining budget past the deadline (guide chapter 9), and
that nothing can preempt a loop that refuses to return — write retries against guard.expired().
What remains yours — and remains this recipe's: what to drop when time is short is
strategy, and the guard has no opinion about it. The phased pattern below still earns its place
for the CONDITIONAL parts of a routine (skip the optional goal if the first one went long); run
it INSIDE guard.run(), use guard.remaining() instead of the hand-rolled clock helper, and
let the guard own the unconditional ending:
RoutineResult budgetedAuton(Chassis& chassis, Intake& intake, Time budget,
Time secondGoalCost) {
const Time started = clockNow(chassis);
Routine r{chassis, "budgeted"};
r.startAt(Pose2d{-48_in, -24_in, 0_deg})
.driveTo(-24_in, -24_in, {.timeout = 8_s, .maxLinearSpeed = Velocity{8.0}})
.then([&intake] { intake.release(); }, "score-1");
// The optional goal runs only if there is time for it AND for the park.
const double spent = (clockNow(chassis) - started).value();
if (r.ok() && spent + secondGoalCost.value() < budget.value()) {
r.driveTo(-24_in, 24_in, {.timeout = 6_s})
.then([&intake] { intake.release(); }, "score-2");
}
// Parking is unconditional, so it lives outside the chain that might stop.
const RoutineResult verdict = r.result();
Routine park{chassis, "budgeted/park"};
park.moveTo(Pose2d{-48_in, -48_in, 0_deg}, {.timeout = 6_s})
.brake({.timeout = 1.5_s});
return verdict;
}
Reading the clock needs one helper, and it reaches past the recipe layer to get it:
Why it is written this way.
secondGoalCostis a parameter, not a constant baked into the library or the recipe. How long your second goal takes is something your team measures on your robot. A number invented here would be a guess wearing the clothes of a measurement.- The park chain is separate. A stopped chain skips everything after the stop, so anything that must happen regardless — parking, retracting a mechanism, ending in a legal position — cannot be the last step of the chain that might stop.
- The comparison uses
.value()on both sides. Both are typed times; comparing the underlying seconds is explicit about the fact that this is arithmetic on durations.
The honest note, updated. clockNow reaches through deps(), the advanced seam
(Chapter 10) — one tier below where a recipe should have to go for
something this ordinary. The gap it papered over is now half-closed, and here is exactly which
half: the unconditional ending no longer needs any of this — the run guard owns it, and
guard.remaining()/guard.expired() replace the clock helper inside a guarded run. The
CONDITIONAL budgeting above (measuring a phase, deciding what to drop) is still yours and still
legitimately reads the clock; inside guard.run() prefer guard.remaining() for it. The
standalone helper remains the honest answer only for code running outside a guard.
Watch out for: trusting per-step timeouts as a budget. They are worst-case bounds on individual motions, not a plan. Six steps with 5-second timeouts is a thirty-second routine in the worst case — which is longer than any autonomous period you will run it in.
Next: 4 — Drivetrains.
Last updated 2026-08-14 — published from the main release branch, commit 6d5dd35. 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.