Advanced Metal Research
GitHub Contact AMR

Go SDK

On this page
  1. Example: acquire, jog, stop, release
  2. Add the module
  3. Connect
  4. Authority
  5. Commands
  6. Jog
  7. Observe
  8. Errors
  9. Types
  10. Related pages

Package rosieos/rt-core/sdk/control is the Go client for rt-control. It is what the offline programming server and the v4 pendant tooling use. It keeps separate connections for lifecycle, urgent (Stop, Release), bulk upload and renewal traffic, so a large upload can never delay a Stop or a renewal. It fills in the fence and a request_id on every command, and it returns refusals as a typed *control.Rejected.

The package builds on Linux only (//go:build linux).

Warning

The example below enables, arms and jogs the robot. Run it against the simulated core (rosie-rt-core-sim) first. On real hardware, keep the hardware E-stop within reach: it is the only emergency stop, and RosieOS has no software E-stop. See the safety model.

Example: acquire, jog, stop, release#

This program is the simulation example from the rt-core README. It acquires, renews in the background, enables and arms all axes, waits for every drive to reach Operation Enabled, jogs axis 0 at 0.01 rad/s for one second, and then stops and releases. Start the simulated core and rt-control first (see Run everything in simulation), then set TMPDIR to the directory holding control.sock and SHA to the compiled configuration digest.

jog.gogo
package main

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

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

func must[T any](v T, err error) T { if err != nil { panic(err) }; return v }
func check(err error) { if err != nil { panic(err) } }

func main() {
	// Simulation inputs: nine axes, 0.01 rad/s, 1 s of input at a 10 ms cadence,
	// 100 ms input lifetime, 10 s overall budget.
	const mask, velocity, duration, cadence, age, budget = 511, 0.01, time.Second, 10 * time.Millisecond, 100 * time.Millisecond, 10 * time.Second
	ctx, cancel := context.WithTimeout(context.Background(), budget)
	defer cancel()

	c := must(control.Dial(control.UnixPath(os.Getenv("TMPDIR") + "/control.sock")))
	defer c.Close()
	d := must(c.Describe(ctx))
	if d.Backend != "simulation" || d.ConfigurationSHA256 != os.Getenv("SHA") {
		panic("simulation identity mismatch")
	}
	g := must(c.Acquire(ctx, "readme", control.Binding{PairID: "readme", Revision: 1, ConfigurationSHA256: d.ConfigurationSHA256}))
	released := false
	defer func() {
		if !released {
			c.Stop(context.Background())
			c.Release(context.Background())
		}
	}()
	fmt.Printf("acquire generation=%d\n", g.Generation)
	renewals := must(c.StartRenewal(ctx, 0))

	fmt.Printf("enable sequence=%d\n", must(c.Enable(ctx, mask)).Sequence)
	fmt.Printf("arm sequence=%d\n", must(c.Arm(ctx)).Sequence)
	tick := time.NewTicker(cadence)
	defer tick.Stop()
	for !ipcclient.AllOperationEnabled(must(c.Status(ctx)).Core) {
		select {
		case <-tick.C:
		case <-ctx.Done():
			panic(ctx.Err())
		}
	}

	before := must(c.Status(ctx)).Core.Axes[0].PositionCounts
	jog := must(c.PrepareJogSession(ctx, mask))
	velocities := make([]float64, len(d.Axes))
	velocities[0] = velocity
	for end := time.Now().Add(duration); time.Now().Before(end); {
		origin := ipcclient.HostMonotonicNS()
		check(jog.UpdateAt(velocities, origin, origin+uint64(age)))
		select {
		case r, ok := <-renewals:
			if !ok {
				panic("renewal ended")
			}
			check(r.Err)
		case <-tick.C:
		case <-ctx.Done():
			panic(ctx.Err())
		}
	}
	check(jog.End(ctx))

	delta := must(c.Status(ctx)).Core.Axes[0].PositionCounts - before
	fmt.Printf("jog requested_ns=%d delta_counts=%d\n", duration, delta)
	fmt.Printf("stop generation=%d\n", must(c.Stop(ctx)).Generation)
	fmt.Printf("release session_empty=%t\n", must(c.Release(ctx)).Session == "")
	released = true
}
Output recorded in the rt-core READMEText
acquire generation=1
enable sequence=1
arm sequence=2
jog requested_ns=1000000000 delta_counts=207
stop generation=2
release session_empty=true

sdk/control/example_motion_server_test.go is a longer runnable example: it Homes, uploads an .rdt program, starts it and observes completion against the simulator. make test-go runs it.

Add the module#

The module path is rosieos/rt-core, which is not a fetchable URL. Point a replace directive at your checkout of the repository, as the offline programming server does:

go.modText
require rosieos/rt-core v0.0.0

replace rosieos/rt-core => ../RosieOS/rt-core

The module declares go 1.24 with toolchain go1.26.2.

Connect#

FunctionUse
control.Dial(path UnixPath) (*Client, error)The local socket, for example control.Dial("/run/rosie-rt-core/control.sock"). Local jog sessions dial jog.sock in the same directory.
control.DialTLS(addr string, config *tls.Config) (*Client, error)The remote listener. config must have RootCAs and a client certificate, and must not set InsecureSkipVerify. TLS 1.3 is forced. See Remote access.
(*Client).Close() errorCloses connections and stops renewal. It does not release authority: call Release first.

Neither constructor connects: the first call does, bounded by its context.

Authority#

MethodReturnsNotes
Acquire(ctx, controller string, binding Binding)Grant, errorMeasures Describe if needed and requests a lease sized to the round trip.
AcquireLease(ctx, controller, binding, requested time.Duration)Grant, errorAs Acquire, with a minimum requested lease. It never goes below the measured requirement.
Renew(ctx)Grant, errorOne renewal.
StartRenewal(ctx, interval time.Duration)<-chan Renewal, errorStarts one background renewer. interval 0 means a third of the lease, and larger values are refused. It refuses to start (control_session_stale) when the lease cannot cover three round trips plus the interval. The channel holds only the latest Renewal{Grant, Err, StartedHostMonotonicNS}; any error ends renewal.
StopRenewal()—Stops and joins the renewer. It does not release.
Stop(ctx)Grant, errorUses the urgent connection. The reply carries the new generation.
Release(ctx)Grant, errorStops renewal, then releases. Never replayed automatically.
Grant(), Fence()Grant, FenceThe client's current authority.
SetFence(f Fence)—Adopts a fence obtained elsewhere, before concurrent use.
Timing()LeaseTimingMeasured round trip, effective lease, renewal interval and call budget.
PrepareAcquisition(ctx)AcquisitionSnapshot, errorWarms remote connections and reads Status and events before Acquire. Grants nothing.

TimingForLease(lease, roundTrip) and RequestedLease(roundTrip, ceiling) are the helpers behind the lease sizing. See Control authority.

Commands#

Each method sends one rt-control operation with the current fence and a fresh request_id.

MethodOperationResult
Enable(ctx, mask uint32)enableResponse (.Sequence)
Arm(ctx)armResponse
Home(ctx, mask uint32)homeResponse
RestoreAnchor(ctx, mask uint32)restore_anchorRecoveryStatus
ResetFault(ctx, mask uint32)reset_faultResponse; the client also forgets the retired fence
IOArm(ctx), IODisarm(ctx)io_arm, io_disarmResponse
PrepareTrajectory(ctx, mask uint32, points []Point)prepare_trajectoryResponse (.Handle)
StartTrajectory(ctx, handle uint64)start_trajectoryResponse
DiscardTrajectory(ctx, handle uint64)discard_trajectoryResponse
PrepareProgram(ctx, blob []byte)prepare_programProgram
StartProgram(ctx, identity Identity)start_programResponse
BeginJog(ctx, mask, clock string, originNS, deadlineNS uint64)begin_jogResponse (.Handle is the jog generation)
EndJog(ctx, generation uint64)end_jogResponse
Jog(ctx, mask, velocity []float64, timeoutMS uint32)jog (test only)Response

There are no convenience methods for halt, recovery_status or mark_telemetry. Send them with Command:

resp, err := c.Command(ctx, &control.Request{Operation: "halt"})

Command(ctx, *Request) fills in schema, the fence and a random request_id the first time, and writes them back into the request. To retry a lost reply, call Command again with the same *Request and a fresh context; the adapter then replays the first outcome instead of running the command twice. Never share one request between goroutines or change it between attempts. NewRequestID() returns a random 32-character hex ID if you want to set your own.

PrepareProgram sends the .rdt bytes with the session and generation headers. Uploads are not deduplicated and never retried: if one fails uncertainly, inspect Describe and Status, or Stop, before uploading again.

Jog#

APIUse
PrepareJogSession(ctx, mask)Begin a local jog session without an input sample; the first input's allowance is the cell's input-age ceiling.
NewJogSession(ctx, mask, deadlineNS), NewJogSessionAt(ctx, mask, originNS, deadlineNS)Begin with a first input captured now or at originNS.
(*JogSession).UpdateAt(velocities, originNS, deadlineNS), Update(velocities, deadlineNS)Send one datagram. Capture times must increase.
(*JogSession).End(ctx)Close the socket, then end the jog generation.
(*JogSession).Generation()The jog generation.
NewRemoteJogSession(ctx, mask, durationNS, sourceNow)The WebSocket lane on a DialTLS client; see remote jog.
BuildJogFrame(...)Encode one 224-byte local_jog_update frame yourself.

Times are host CLOCK_MONOTONIC nanoseconds; ipcclient.HostMonotonicNS() reads that clock. A deadline may be at most the cell's max_jog_input_age_ns after its origin. Update never blocks: when the socket is congested it returns ErrJogWouldBlock, and you should drop that sample and send a fresh one. Local datagrams get no reply, so read refusals from JogIngress.

Observe#

MethodReturns
Describe(ctx)Description. Also records the round trip and the cell's lease and jog ceilings.
Status(ctx)Status (the schema's ProcessStatus)
JogClock(ctx), JogStatus(ctx), JogIngress(ctx)JogClock, JogObservation, JogIngressObservation
Events(after), EventsContext(ctx, after)EventBatch, validated for order and loss
StreamEvents(ctx, after, func(Event) error)Runs until the context ends or the handler returns an error
TelemetryBatches(ctx, after)One TelemetryBatch (Header, Records)
TelemetryStream(ctx, after, func(TelemetryBatch) error)Follows the stream; returns on an incarnation change
TelemetryPeek(ctx, after)The header and first record only
Resource(ctx, sha)io.ReadCloser, returned only after the size and ETag match the digest

See Events and telemetry for cursor handling.

Errors#

g, err := c.Acquire(ctx, "my-app", binding)
var rejected *control.Rejected
switch {
case errors.As(err, &rejected):
	// A 409 refusal. rejected.Reason is the reason code; NativeResult,
	// NativeJogResult and ResponseData carry any structured evidence.
	log.Printf("refused: %s", rejected.Reason)
case errors.Is(err, control.ErrConnectionUnsent):
	// The request never left this process. Safe to retry.
case errors.Is(err, control.ErrTransport):
	// The outcome is unknown. Stop, then reconcile Status before acquiring again.
case err != nil:
	log.Fatal(err)
}
_ = g
ErrorMeaning
*RejectedHTTP 409. Reason is a reason code. NativeResult and NativeJogResult are the native receipts; ResponseData keeps data (for example limit_violation) without losing integer precision.
*LeaseTimingRejectedThe lease cannot cover the measured round trip. Wraps a *Rejected with reason control_session_stale and reports RoundTrip and Ceiling.
ErrTransportThe connection failed. The outcome may be unknown.
ErrConnectionUnsentThe request was not sent.
ErrRequestTimeoutThe request budget expired (joined with context.DeadlineExceeded).
ErrProtocolA malformed reply, or an unexpected status such as 413 or 404.
ErrClosedThe client is closed.
ErrJogWouldBlockA jog datagram was not sent. Replace it with fresh input.

The client retries a request at most once, after a closed or stale connection, reusing the same request_id. It never retries a release or an .rdt upload whose outcome is uncertain.

Types#

The package re-exports the contract types, so you rarely need the adapter package directly: Request, Fence, Binding, Grant, Identity, Program, Execution, HandleRecord, ProcessMarker, Description, RecoveryStatus, CapabilityInfo, Point, JogClock, JogObservation, JogIngressObservation, JogIngressRefusal, Response (the raw envelope, with Data as json.RawMessage), Status, Event, EventBatch, EventsDropped, TelemetryBatch, ResourceInfo, RobotDescription, DriveDescription and DriveIdentity. Their fields are in the types reference. The capability states are exported as CapabilityStateImplemented, CapabilityStateInterim, CapabilityStateTestOnly and CapabilityStateUnimplemented. The reason codes are constants in the generated adapter package rosieos/rt-core/adapters/rosie/control, for example ReasonControlSessionStale.