Advanced Metal Research
GitHub Contact AMR

Your first motion (simulation)

On this page
  1. 1. Build and set up a scratch directory
  2. 2. Start the simulated core
  3. 3. Write the program
  4. 4. Run it
  5. 5. Look at what the core recorded
  6. Why not rtctl?
  7. Next steps

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.

You need the toolchain and a clone of the repository. Run every command from rt-core/.

1. Build and set up a scratch directory#

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:

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:

(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:

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.gogo
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#

go run build/first-motion.go

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

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:

StepCallEffect
AcquireAcquire(ctx, controller, Binding)Returns a fence (session and generation 1) under a lease of at most 500 ms.
RenewStartRenewal(ctx, 0)Renews at a third of the lease. If renewal fails, the channel reports it and the lease lapses.
Enable, ArmEnable(ctx, mask), Arm(ctx)Requests CiA402 enable on J1 to J9, then arms. Motion is permitted only while armed.
JogPrepareJogSession, UpdateAt, EndOpens a jog generation, sends 224-byte datagrams on jog.sock, then ends the jog.
StopStop(ctx)Inhibits outputs, retires handles and raises the generation to 2.
ReleaseRelease(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:

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:

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#