Advanced Metal Research
GitHub Contact AMR

Control authority: leases, fences and the jog lane

On this page
  1. The lifecycle in one example
  2. The grant
  3. The fence
  4. Lease timing
  5. What ends authority
  6. Idempotent retries
  7. The jog lane
  8. One controller, many clients
  9. Related pages

A RosieOS cell has exactly one controller at a time. The pendant, the offline programming server, the motion servers, rtctl and your own code all ask rt-control for authority the same way, and they exclude each other. Authority is a grant: a session token plus a generation number (together, the fence), held under a lease that expires unless you renew it.

This page explains the model. The exact calls are in the rt-control HTTP API.

Warning

A grant lets you energise the drives and move the robot. The lease and the software Stop are not safety functions: the hardware E-stop is the only emergency stop, and RosieOS has no software E-stop. Keep the E-stop within reach, and read the safety model before you arm a real cell.

The lifecycle in one example#

This Go program acquires a grant, keeps it alive in the background, enables and arms the cell, and then gives authority back. Every mutating call carries the fence automatically.

authority.gogo
package main

import (
	"context"
	"log"
	"time"

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

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	c, err := control.Dial("/run/rosie-rt-core/control.sock")
	if err != nil {
		log.Fatal(err)
	}
	defer c.Close()

	d, err := c.Describe(ctx) // also measures the round trip used to size the lease
	if err != nil {
		log.Fatal(err)
	}
	g, err := c.Acquire(ctx, "my-app", control.Binding{
		PairID: "cell-a", Revision: 1, // the pair rt-control was started with
		ConfigurationSHA256: d.ConfigurationSHA256, MachineSHA256: d.MachineSHA256,
	})
	if err != nil {
		log.Fatal(err) // e.g. control_already_owned
	}
	log.Printf("session generation=%d lease=%dms", g.Generation, g.LeaseMS)

	renewals, err := c.StartRenewal(ctx, 0) // 0 = every third of the lease
	if err != nil {
		log.Fatal(err)
	}
	go func() {
		for r := range renewals {
			if r.Err != nil {
				log.Printf("renewal stopped: %v", r.Err) // authority is gone; stop producing motion
			}
		}
	}()

	if _, err := c.Enable(ctx, uint32(1)<<len(d.Axes)-1); err != nil {
		log.Print(err)
	}
	if _, err := c.Arm(ctx); err != nil {
		log.Print(err)
	}

	// ... motion ...

	if _, err := c.Stop(ctx); err != nil { // inhibits and bumps the generation
		log.Print(err)
	}
	if _, err := c.Release(ctx); err != nil { // stops renewal, then releases
		log.Print(err)
	}
}

The Go SDK page covers these calls in detail. The C++ client has the same shape.

The grant#

acquire returns a Grant:

FieldMeaning
sessionA fresh, unpredictable token of 64 lowercase hex characters. Empty after release or expiry.
generationA uint64 that increases on every Acquire and every Stop.
lease_msThe effective lease: min(requested_lease_ms, cell ceiling). It stays the same for the life of the session.
deadline_host_nsWhen the lease runs out, in the cell host's CLOCK_MONOTONIC ns.
stoppingtrue while a Stop is draining. Renewal still succeeds, but grants no motion.
controller, bindingYour label and the binding you acquired with.

acquire succeeds only when nobody else holds authority and your binding matches the deployment: the pair ID and revision rt-control was started with, and the machine digest from Describe. If you send machine_sha256 it must match exactly, and there is no fallback to configuration_sha256. A mismatch is authority_binding_mismatch. If another session holds authority you get control_already_owned.

The connection itself is authenticated by the transport: file permissions on the local socket, or the client certificate on the remote listener. A session belongs to the principal that acquired it. Using it from another principal, or from the local socket when it was acquired remotely, returns session_principal_mismatch.

The fence#

Every command after acquire carries session and generation. rt-control checks them on every call.

  • Motion needs the exact current generation. Enable, Arm, Home, jog, Prepare and Start are refused with control_session_stale if the generation is not the current one, if the lease has expired, or while a Stop is in flight.
  • Stop increments the generation. A live session gets its session back with the new generation, so any command still in flight under the old one is fenced off. You need a fresh Enable and Arm before moving again.
  • Renew and Stop accept older generations. A non-zero generation that is not newer than the current one, from the same session, may renew (and learns the current fence from the reply) or stop. It can never move.
  • Stop works after expiry. A session that has expired or been revoked can still send Stop, so a client that lost its lease can still inhibit. It cannot regain motion permission.
  • Restarts invalidate sessions. A session from a previous core incarnation gets daemon_restarted. A well-formed session this rt-control process never issued (for example, one from before an adapter restart) gets fence.

Handles follow the same rule. A trajectory handle belongs to the generation that prepared it, and Stop, expiry and connection loss retire every handle. A retired handle can never start again.

Lease timing#

The cell's compiled configuration sets two ceilings, and Describe publishes both:

Profile (control.link_profile)Lease ceiling max_grant_lease_nsJog input age max_jog_input_age_ns
lan (the default)500 ms250 ms
internet (starting values, unverified)3 s750 ms

A cell can override either value explicitly, up to 10 s for the lease and 2 s for the input age, and leases are whole milliseconds. The effective lease is the smaller of what you request and the ceiling. If you request nothing, you get 500 ms, still capped by the cell.

Renew at most every third of the effective lease, on a connection of its own, so Status polls and uploads cannot delay it. The Go and C++ clients measure the Describe round trip and refuse to start when the lease cannot cover at least three round trips plus the renewal interval; that refusal is typed control_session_stale (LeaseTimingRejected).

Note

Longer is not safer. A longer lease means the robot keeps its last authority for longer after the operator's link drops. A longer input age lets a stale jog velocity apply for longer before it expires. Both are safety trade-offs, not qualified stop times.

What ends authority#

EventWhat happensWhat you do next
stopOutputs are inhibited immediately, execution, uploads, handles and jog are cancelled, and the generation increments. The session survives if it was still valid.Enable and Arm again, then prepare new motion.
haltA controlled deceleration to an enabled hold. The grant, Enable and Arm are kept; the active trajectory handle and jog generation are retired.Start new motion from the held position, or begin_jog again.
releaseStop, then the session is cleared.Acquire again when you need authority.
Lease expiryrt-control notices within about 20 ms, emits grant_expired, retires every handle, cancels jog and inhibits outputs through the same Stop path.Acquire again, Enable, Arm and upload anew.
reset_faultA submitted reset retires the session, even when the core refuses the reset.Acquire again after recovery.
Core or adapter restartSessions from before the restart are refused.Describe again, then acquire.

Neither a Stop receipt nor a Halt receipt proves the robot is at standstill. Confirm in Status.

Idempotent retries#

Mutating JSON calls can carry a request_id. Within a live session, rt-control keeps the last 256 outcomes: a retry with the same ID and the same payload joins the original request or replays its final reply, so a lost reply never causes a second Start. The same ID with a different payload is request_id_conflict. Release, expiry and restarts end the guarantee, and binary .rdt uploads are never deduplicated. The details are under Idempotent retries.

The jog lane#

Jogging runs on its own lane, separate from the JSON command path, so a stalled HTTP connection cannot hold a velocity in place.

  1. GET /v1/jog/clock returns the host clock's incarnation.
  2. begin_jog reserves jog mode for an axis mask and returns a jog generation. It carries the capture time and an absolute deadline of the first input.
  3. Each velocity update is a 224-byte binary frame: a Unix datagram on jog.sock, or a WebSocket frame on the remote listener. It carries the session, grant generation, jog generation, a strictly increasing sequence, its own capture time and deadline, and the velocities in each axis's unit per second.
  4. end_jog revokes input and the core ramps the jog to a hold.

Four rules keep jog input from outliving its operator:

  • Deadlines are never extended. Renewing the grant does not freshen an input, and a refused frame never refreshes the previous one.
  • Input ages out. A frame's deadline may be at most max_jog_input_age_ns after its capture (250 ms on LAN). When input stops arriving, the core ramps the jog down within each axis's jog acceleration, holds, and publishes jog_expired. If the ramp cannot stay within its bounds, the outputs are inhibited instead.
  • Old sessions cannot come back. After Stop, expiry, a source loss or a Home or configuration epoch change, frames for the old session are refused as wrong_identity, even with fresh timestamps.
  • Only the newest input counts. In a burst of datagrams only the newest valid frame is applied. Producers keep only their latest unsent sample and never replay a backlog.

Remote jog adds a clock calibration exchange, because the pendant's clock is not the cell's. That is covered in Remote access.

One controller, many clients#

Because authority is exclusive, clients on one cell lock each other out while they hold it: a desktop OLP session holding the grant keeps the pendant out, and the other way round. Observation is not exclusive. Anyone who can reach the socket can read Describe, Status, events and telemetry without a lease.