Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,20 @@ export default defineConfig({
label: "Robot Module",
link: "/robot/",
icon: "puzzle",
items: [{ autogenerate: { directory: "robot" } }],
items: [
{ slug: "robot" },
{ slug: "robot/commands" },
{ slug: "robot/mechanisms" },
{ slug: "robot/nextrobot" },
{ slug: "robot/nextopmode" },
{ slug: "robot/triggers" },
{ slug: "robot/drive-commands" },
{ slug: "robot/project-structure" },
{
label: "Advanced",
items: [{ autogenerate: { directory: "robot/advanced" } }],
},
],
},
{
label: "Hardware Module",
Expand Down
174 changes: 174 additions & 0 deletions src/content/docs/robot/advanced/coroutine-commands.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
---
title: Coroutine Commands
description: Writing a command as sequential Kotlin instead of a start/execute/done state machine.
sidebar:
order: 1
---

import { Aside } from "@astrojs/starlight/components";

A normal [command](/robot/commands/) is a state machine: `start()`, then `execute()` once per loop,
then `done()` decides when it's over.
Anything sequential has to be split across those calls, or built out of nested groups.

A coroutine command lets you write the same thing top to bottom:

```kotlin
val scorePiece = command {
await(arm.toHigh())
wait(0.5)
await(claw.open())
fork(lights.flash())
await(arm.toRest())
}
```

The body runs a slice per loop and suspends at each `await`, `wait`, or `yield`,
so it never blocks the OpMode loop.

<Aside type="caution" title="Kotlin only">
Coroutine commands use Kotlin `suspend` functions and can't be written from
Java. Java code can still schedule and compose the resulting `Command` like
any other.
</Aside>

## Creating one

`command { }` is a top-level function, and `Mechanism.coroutine { }` is the same thing with
`requiring(this)` already applied:

```kotlin
class Arm : Mechanism {
val motor = NextMotor("armMotor")

fun scorePiece() = coroutine {
await(toHigh())
wait(0.5)
await(toRest())
}
}
```

Both return a `CoroutineCommandBuilder`, which is itself a `Command`.
You can schedule it, bind it to a [trigger](/robot/triggers/), or drop it into an Ivy group without
calling `build()`:

```kotlin
command {
await(arm.toHigh())
await(claw.open())
}.requiring(arm, claw).setPriority(1).schedule()
```

`requiring`, `setPriority`, `setInterruptedBehavior`, `setConflictBehavior`, and
`setBlockedBehavior` work the same as on Ivy's `CommandBuilder`.
A builder makes a fresh coroutine every time it starts, so one builder can be scheduled repeatedly.

## What the body can do

Inside the braces you're in a `CommandScope`:

| Function | Suspends until... |
| --------------------------- | ------------------------------------------------------- |
| `yield()` | the next loop iteration |
| `wait(seconds)` | at least that many seconds have passed |
| `waitUntil { condition }` | the condition is true, checked once per loop |
| `await(command)` | that command is done |
| `awaitAll(vararg commands)` | every one of those commands is done |
| `awaitAny(vararg commands)` | any one of them is done; returns the winner |
| `fork(command)` | nothing — it starts the command and returns immediately |

`awaitAll` and `awaitAny` also accept a `Collection<Command>`.
`awaitAny` ends the winner naturally and interrupts the rest, and throws if you pass it no commands.

### Doing work every loop

The other functions all wait for something. `yield()` is the one you use when the body itself has
work to do on each iteration: write a loop, do one iteration's worth of work, and `yield()` at the
bottom.

```kotlin
fun ramp() = coroutine {
var power = 0.0
while (power < 1.0) {
power += 0.02
motor.throttle = power
yield()
}
motor.throttle = 1.0
}
```

Each `yield()` gives the loop back to the OpMode, and the next `execute()` picks up on the line
after it, so this ramp takes fifty loop iterations rather than fifty iterations of a `while` loop
in one.

The same shape works for anything that has to keep running while it waits.
Here the arm holds its target with a controller until it's close enough, then stops:

```kotlin
fun goTo(target: Double) = coroutine {
controller.target = target
while (abs(motor.currentPosition - target) > 10.0) {
motor.throttle = controller.calculate(motor.currentPosition)
yield()
}
motor.throttle = 0.0
}
```

## Inline commands and requirements

Commands you `await` or `fork` are driven by the coroutine, not by the `Scheduler`.
They never reach the scheduler, so **their requirements are not checked for conflicts**.
List every mechanism the body touches on the enclosing command:

```kotlin
command {
await(arm.toHigh())
await(claw.open())
}.requiring(arm, claw)
```

To hand a command to the scheduler instead, so it competes for requirements normally,
call `schedule()` on it rather than `await`ing it.

Forked commands keep running after the line that started them.
The coroutine command isn't done until the body has returned _and_ every forked command has finished.

## Cancellation

Interrupting a coroutine command resumes the body with a `CancellationException`.
That means `finally` blocks run, and any command you were `await`ing or had `fork`ed is ended with
`INTERRUPTED`:

```kotlin
fun intakeUntilLoaded() = coroutine {
try {
fork(run())
waitUntil { sensor.isWithinDistance(2.0) }
} finally {
motor.throttle = 0.0
}
}
```

If the command's `InterruptedBehavior` is `SUSPEND`, the body isn't unwound.
The continuation is kept, and the coroutine picks up where it left off when the scheduler resumes it.

## Rules of the body

Everything between two suspension points runs in a single loop iteration, so the usual rule applies:
don't write anything blocking.
A loop with no suspension point in it hangs the robot.

```kotlin
// hangs: nothing yields
while (!sensor.isWithinDistance(2.0)) { }

// correct
waitUntil { sensor.isWithinDistance(2.0) }
```

`Thread.sleep` has the same problem.
Use `wait(seconds)`.
10 changes: 9 additions & 1 deletion src/content/docs/robot/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ sidebar:
order: 1
---

import { Tabs, TabItem } from "@astrojs/starlight/components";
import { Aside, Tabs, TabItem } from "@astrojs/starlight/components";

NextFTC v2 has no command framework of its own.
The `robot` module uses [Ivy](https://pedropathing.com/docs/ivy),
Expand Down Expand Up @@ -57,6 +57,14 @@ so something has to cancel it.
That makes `infinite` the right choice for anything fed by gamepad input,
where the value changes every loop.

<Aside type="tip" title="Sequencing Commands in Kotlin">

Anything with steps in it, like raise the arm, wait, then open the claw, can be written as a
[coroutine command](/robot/advanced/coroutine-commands/) instead of a nest of sequential and parallel groups.
This command is only available in Kotlin, but it can call Java methods and use Java classes.

</Aside>

## Starting, checking, and cancelling

Three `Command` methods from Ivy come up outside of triggers:
Expand Down
5 changes: 5 additions & 0 deletions src/content/docs/robot/mechanisms.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ interface Mechanism {
val defaultCommand: Command get() = infinite {}
fun instant(action: Runnable): CommandBuilder
fun infinite(action: Runnable): CommandBuilder
fun coroutine(body: suspend CommandScope.() -> Unit): CoroutineCommandBuilder
}
```

Expand Down Expand Up @@ -52,6 +53,10 @@ Two commands can never drive the same hardware at the same time.
`instant` runs its action once.
`infinite` re-runs its action every loop until something cancels it.

Kotlin has a third: `coroutine { }` builds a
[coroutine command](/robot/advanced/coroutine-commands/) requiring this mechanism,
for anything with steps in it.

## `defaultCommand`

The command that runs whenever nothing else has claimed the mechanism.
Expand Down