# Your first motion (simulation)

> Start the simulated core directly, then acquire control, enable, arm, jog one joint, stop and release from a short Go program using the rt-core SDK.

URL: https://advancedmetalresearch.com/docs/get-started/first-motion
Section: RosieOS docs / Get started
Last updated: 2026-10-10

In this tutorial you drive the simulated core from your own code. You start `rosie-rt-core-sim` and `rt-control` by hand, then run a Go program that goes through the full control sequence:

1. Acquire control.
2. Enable and arm the axes.
3. Jog J1 at 0.01 rad/s for one second.
4. Stop and release.

> [!WARNING] **Energised motion.** This example refuses to run against anything but the simulator it starts. The same calls move a real robot when they are pointed at a real cell's socket. RosieOS has no software E-stop: keep the hardware E-stop within reach and clear the cell before you arm. Jog, moves and Home are checked against joint limits only, not against collisions. See the [safety model](https://advancedmetalresearch.com/docs/get-started/safety-model).

You need the [toolchain](https://advancedmetalresearch.com/docs/get-started/installation) and a clone of the repository. Run every command from `rt-core/`.

## 1. Build and set up a scratch directory

```bash
cd rt-core
export GOFLAGS=-mod=mod
export TMPDIR=/dev/shm/rt-core/first-motion
mkdir -p "$TMPDIR" build/first-motion-runtime
make control sim
```

`TMPDIR` holds the sockets, so it must be on a Linux filesystem. `/dev/shm` works on Linux and in WSL.

## 2. Start the simulated core

Compile the nine-axis simulation machine. `rtctl compile` prints the directory it wrote, and the directory name is the configuration digest:

```bash
compiled=$(build/rtctl compile --config config/machines/simulation/simulation-program.json --out build/first-motion-config)
export SHA=${compiled##*/}
echo "$SHA"
```

The compiler also prints warnings about missing collision watchdogs and legacy flat axes. They are expected for this simulation fixture, which is not a hardware recipe.

Start the core. `rtctl run` recompiles, then replaces itself with `rosie-rt-core-sim`:

```bash
(cd build/first-motion-runtime && exec ../rtctl run --backend simulation \
  --config ../../config/machines/simulation/simulation-program.json \
  --out ../first-motion-config --socket "$TMPDIR/ipc.sock") >build/first-motion-core.log 2>&1 &
core_pid=$!
until test -S "$TMPDIR/ipc.sock"; do sleep 0.05; done
```

Start the public control API on the core's socket, with a pair binding of your choosing:

```bash
build/rt-control --core-socket "$TMPDIR/ipc.sock" --socket "$TMPDIR/control.sock" \
  --configuration-sha256 "$SHA" --backend simulation \
  --pair-id first-motion --pair-revision 1 >build/first-motion-control.log 2>&1 &
control_pid=$!
until build/rtctl describe --socket "$TMPDIR/control.sock" --json >/dev/null 2>&1; do sleep 0.05; done
```

`rt-control` also creates the jog socket, `$TMPDIR/jog.sock`, next to `control.sock`.

## 3. Write the program

Save this as `build/first-motion.go`. It is inside the `rosieos/rt-core` module, so the SDK imports resolve without extra setup.

rt-core/build/first-motion.go:

```go
package main

import (
	"context"
	"errors"
	"fmt"
	"os"
	"time"

	"rosieos/rt-core/ipcclient"
	"rosieos/rt-core/sdk/control"
)

// Simulation-only inputs: all nine axes, J1 at 0.01 rad/s for one second,
// a 10 ms update cadence and a 100 ms deadline on each jog update.
const (
	mask     = 0x1ff // J1..J9
	velocity = 0.01  // rad/s
	duration = time.Second
	cadence  = 10 * time.Millisecond
	inputAge = 100 * time.Millisecond
)

func main() {
	if err := run(); err != nil {
		fmt.Fprintln(os.Stderr, "first-motion:", err)
		os.Exit(1)
	}
}

func run() error {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	c, err := control.Dial(control.UnixPath(os.Getenv("TMPDIR") + "/control.sock"))
	if err != nil {
		return err
	}
	defer c.Close()

	// Refuse anything but the simulator started above.
	d, err := c.Describe(ctx)
	if err != nil {
		return err
	}
	if d.Backend != "simulation" || d.ConfigurationSHA256 != os.Getenv("SHA") {
		return errors.New("not the simulator this example started")
	}

	// 1. Acquire: one controller at a time, bound to this pair and configuration.
	g, err := c.Acquire(ctx, "first-motion", control.Binding{
		PairID: "first-motion", Revision: 1, ConfigurationSHA256: d.ConfigurationSHA256,
	})
	if err != nil {
		return err
	}
	released := false
	defer func() { // on any failure, inhibit and give up control
		if !released {
			c.Stop(context.Background())
			c.Release(context.Background())
		}
	}()
	fmt.Printf("acquire generation=%d\n", g.Generation)

	// Keep the lease alive: interval 0 renews every third of the lease.
	renewals, err := c.StartRenewal(ctx, 0)
	if err != nil {
		return err
	}

	// 2. Enable and arm, then wait for every axis to report Operation Enabled.
	e, err := c.Enable(ctx, mask)
	if err != nil {
		return err
	}
	fmt.Printf("enable sequence=%d\n", e.Sequence)
	a, err := c.Arm(ctx)
	if err != nil {
		return err
	}
	fmt.Printf("arm sequence=%d\n", a.Sequence)

	tick := time.NewTicker(cadence)
	defer tick.Stop()
	for {
		s, err := c.Status(ctx)
		if err != nil {
			return err
		}
		if ipcclient.AllOperationEnabled(s.Core) {
			break
		}
		select {
		case <-tick.C:
		case <-ctx.Done():
			return ctx.Err()
		}
	}
	s, err := c.Status(ctx)
	if err != nil {
		return err
	}
	before := s.Core.Axes[0].PositionCounts

	// 3. Jog J1. Each datagram carries its own deadline; if updates stop,
	// the core ramps the axis to a hold.
	jog, err := c.PrepareJogSession(ctx, mask)
	if err != nil {
		return err
	}
	velocities := make([]float64, len(d.Axes))
	velocities[0] = velocity
	for end := time.Now().Add(duration); time.Now().Before(end); {
		origin := ipcclient.HostMonotonicNS()
		if err := jog.UpdateAt(velocities, origin, origin+uint64(inputAge)); err != nil {
			return err
		}
		select {
		case renewal, ok := <-renewals:
			if !ok {
				return errors.New("lease renewal ended")
			}
			if renewal.Err != nil {
				return renewal.Err
			}
		case <-tick.C:
		case <-ctx.Done():
			return ctx.Err()
		}
	}
	if err := jog.End(ctx); err != nil {
		return err
	}
	s, err = c.Status(ctx)
	if err != nil {
		return err
	}
	fmt.Printf("jog requested_ns=%d delta_counts=%d\n", duration, s.Core.Axes[0].PositionCounts-before)

	// 4. Stop inhibits outputs and bumps the fence; Release gives up the session.
	stopped, err := c.Stop(ctx)
	if err != nil {
		return err
	}
	fmt.Printf("stop generation=%d\n", stopped.Generation)
	r, err := c.Release(ctx)
	if err != nil {
		return err
	}
	released = true
	fmt.Printf("release session_empty=%t\n", r.Session == "")
	return nil
}
```

## 4. Run it

```bash
go run build/first-motion.go
```

The output looks like this. The count delta depends on timing:

```text
acquire generation=1
enable sequence=1
arm sequence=2
jog requested_ns=1000000000 delta_counts=207
stop generation=2
release session_empty=true
```

What happened, step by step:

| Step | Call | Effect |
|---|---|---|
| Acquire | `Acquire(ctx, controller, Binding)` | Returns a fence (session and generation 1) under a lease of at most 500 ms. |
| Renew | `StartRenewal(ctx, 0)` | Renews at a third of the lease. If renewal fails, the channel reports it and the lease lapses. |
| Enable, Arm | `Enable(ctx, mask)`, `Arm(ctx)` | Requests CiA402 enable on J1 to J9, then arms. Motion is permitted only while armed. |
| Jog | `PrepareJogSession`, `UpdateAt`, `End` | Opens a jog generation, sends 224-byte datagrams on `jog.sock`, then ends the jog. |
| Stop | `Stop(ctx)` | Inhibits outputs, retires handles and raises the generation to 2. |
| Release | `Release(ctx)` | Gives up the session. |

This simulation machine has no Home requirement. A real cell, and the dev-stack simulation, require Home on every axis before the motion start gate opens. Call `c.Home(ctx, mask)` after enabling and before arming, and wait for Home to become valid in status.

## 5. Look at what the core recorded

Observations don't need control:

```bash
build/rtctl status --socket "$TMPDIR/control.sock" --json | head -c 600; echo
build/rtctl events --socket "$TMPDIR/control.sock" --after 0
```

The event log shows the grant and handle transitions of your run. Stop the processes when you are done:

```bash
kill -TERM "$control_pid" "$core_pid"
wait "$control_pid" "$core_pid"
```

## Why not rtctl?

`rtctl` has `acquire`, `enable`, `arm`, `stop` and `release` commands, but each one is a single request, and it never renews the lease. On the `lan` profile the lease lasts at most 500 ms, so it lapses between one shell command and the next, and the core inhibits outputs. `rtctl` has no `jog` command either. Use `rtctl` to observe and to recover, and use an SDK client (Go, C++) for anything that moves.

## Next steps

- Do the same from a browser: [Quickstart](https://advancedmetalresearch.com/docs/get-started/quickstart), step 5.
- Learn the lease, fence and jog-lane rules: [Control authority](https://advancedmetalresearch.com/docs/concepts/control-authority).
- The full SDK: [Go SDK](https://advancedmetalresearch.com/docs/apis/go-sdk). Every operation: [rt-control HTTP API](https://advancedmetalresearch.com/docs/apis/rt-control-http).

## Sources

Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS):

- `rt-core/tools/rtctl/command.go:16-60`
- `rt-core/tools/rtctl/run.go:13-40`
- `rt-core/tools/rtctl/control.go:22-26,48-110`
- `rt-core/cmd/rt-control/main.go:24-66`
- `rt-core/config/machines/simulation/simulation-program.json`
- `rt-core/sdk/control/client.go:78-90`
- `rt-core/sdk/control/operations.go:14,82,94-113,126`
- `rt-core/sdk/control/renewal.go:25-50`
- `rt-core/sdk/control/jog.go:23,87-130,138,175`
- `rt-core/ipcclient/client_linux.go:1166`
- `rt-core/ipcclient/application_grant_linux.go:69`
- `rt-core/adapters/rosie/control/api_generated.go:381-390,462,512`
- `rt-core/adapters/rosie/control/http.go:190-243`
- `rt-core/protocol/control.json (records.local_jog_update)`
- `rt-core/include/motion_readiness.hpp:97-130; recorded output: rt-core/README.md:269-276 (README cited for the sample run only)`
