> This is the Markdown version of the page. For the complete Lattice Developers documentation index, fetch https://developer.anduril.com/llms.txt. Append .md to any page URL for its clean Markdown.

## Send manual control frames

You can now drive an asset in real time while it executes a manual control task. The new
[`SendManualControlFrame`](/reference/rest/tasks/send-manual-control-frame) API, available in
[preview](/reference/overview/api-lifecycle#preview), sends a single frame of control input, such as a joystick
movement, for a task in `STATUS_EXECUTING`. Call it repeatedly as the operator's input changes. Lattice forwards each
frame to the agent executing the task, which receives it through
[`StreamManualControlFrames`](/reference/rest/tasks/stream-manual-control-frames).

### Manage the control session

Your client owns the control session. Each frame carries the following fields:

* **`sessionEpochMicros`**: A Unix timestamp in microseconds that identifies the session. Generate it once when you take control, and send the same value with every frame.
* **`sequence`**: The frame's position in the session. Increase it with each frame so the agent can detect out-of-order delivery, and reset it to 1 when you start a new session.
* **`specification`**: The control instructions, as a `google.protobuf.Any` in a format specific to the task. Lattice passes it to the agent without inspecting it.

A frame with a higher epoch replaces the current session, which lets another operator take over control. Lattice
rejects frames with a lower epoch as stale with an HTTP `409 Conflict`, which the SDKs surface as a `ConflictError`.
Agents receive the epoch and sequence on each `ManualControlFrame`, so they can ignore frames from a stale session.

### Send a frame

The following example starts a control session and sends its first frame:

**`Python (REST)`**

```python title="Python (REST)"
import time

from anduril import GoogleProtobufAny

# Generate the epoch once per control session and reuse it for every frame.
session_epoch_micros = str(time.time_ns() // 1_000)

client.tasks.send_manual_control_frame(
    task_id=task_id,
    session_epoch_micros=session_epoch_micros,
    sequence="1",  # Increase with each frame.
    specification=GoogleProtobufAny(
        type="type.googleapis.com/org.example.control.v1.JoystickInput",
        pitch=0.25,
        yaw=-0.5,
    ),
)
```

**`Go (REST)`**

```go title="Go (REST)"
// Generate the epoch once per control session and reuse it for every frame.
sessionEpochMicros := strconv.FormatInt(time.Now().UnixMicro(), 10)

_, err := client.Tasks.SendManualControlFrame(ctx, &Lattice.ManualControlFrameRequest{
    TaskID:             taskID,
    SessionEpochMicros: sessionEpochMicros,
    Sequence:           Lattice.String("1"), // Increase with each frame.
    Specification: &Lattice.GoogleProtobufAny{
        Type:            Lattice.String("type.googleapis.com/org.example.control.v1.JoystickInput"),
        ExtraProperties: map[string]interface{}{"pitch": 0.25, "yaw": -0.5},
    },
})
```

**`Java (REST)`**

```java title="Java (REST)"
import com.anduril.resources.tasks.requests.ManualControlFrameRequest;
import com.anduril.types.GoogleProtobufAny;

// Generate the epoch once per control session and reuse it for every frame.
String sessionEpochMicros = String.valueOf(System.currentTimeMillis() * 1_000);

client.tasks().sendManualControlFrame(
    taskId,
    ManualControlFrameRequest.builder()
        .sessionEpochMicros(sessionEpochMicros)
        .sequence("1") // Increase with each frame.
        .specification(GoogleProtobufAny.builder()
            .type("type.googleapis.com/org.example.control.v1.JoystickInput")
            .additionalProperty("pitch", 0.25)
            .additionalProperty("yaw", -0.5)
            .build())
        .build());
```

**`TypeScript (REST)`**

```typescript title="TypeScript (REST)"
// Generate the epoch once per control session and reuse it for every frame.
const sessionEpochMicros = String(Date.now() * 1000);

await client.tasks.sendManualControlFrame({
    taskId,
    sessionEpochMicros,
    sequence: "1", // Increase with each frame.
    specification: {
        "@type": "type.googleapis.com/org.example.control.v1.JoystickInput",
        pitch: 0.25,
        yaw: -0.5,
    },
});
```

**`Rust (REST)`**

```rust title="Rust (REST)"
use anduril_lattice_sdk::prelude::*;
use std::time::{SystemTime, UNIX_EPOCH};

// Generate the epoch once per control session and reuse it for every frame.
let session_epoch_micros = SystemTime::now()
    .duration_since(UNIX_EPOCH)?
    .as_micros()
    .to_string();

let request = ManualControlFrameRequest {
    session_epoch_micros,
    sequence: Some("1".to_string()), // Increase with each frame.
    specification: Some(GoogleProtobufAny {
        r#type: Some("type.googleapis.com/org.example.control.v1.JoystickInput".to_string()),
        extra: HashMap::from([
            ("pitch".to_string(), json!(0.25)),
            ("yaw".to_string(), json!(-0.5)),
        ]),
    }),
};
client.tasks.send_manual_control_frame(&task_id, &request, None).await?;
```

Over gRPC, call the
[`SendManualControlFrame`](/reference/grpc/anduril-taskmanager-v-1/task-manager-api/anduril-taskmanager-v-1-send-manual-control-frame)
RPC with the same fields. It rejects a stale epoch with an `ABORTED` error.

To get started, [install the SDK for REST](/guides/getting-started/set-up#get-the-sdk-for-rest). The API is available
starting in Python `5.4.0`, Go `v5.4.0`, Java `6.4.0`, TypeScript `5.4.0`, and Rust `2.5.0`.