Sequence Control
A sequence is a stored series of implement actions that a master can replay
on operator command. Think of a headland turn: lift the hitch, fold the
sections, stop the PTO. Doing that by hand every pass is tedious and
error-prone. Sequence Control (defined in ISO 11783-14) lets one control
function — the master — drive that series step by step, while each
participating implement — the client — actually performs the actions. This
tutorial explains the master/client relationship, the full lifecycle as a state
machine, the messages involved, what machbus supports today versus what it
intentionally rejects, and how to drive it from the session facade.
Why this exists
On a working machine, the same handful of operator actions repeat at every headland or waterway. The standard’s idea is to let the operator perform those actions once and have a master replay them afterward — each time the trigger point is reached — so the machine repeats the operation without the operator re-doing every input. The actions themselves still live in the implements; the master only coordinates when each one fires and who has acknowledged it.
machbus implements the playback half of that picture: a master that drives
a predefined list of steps through ready, active, pause, resume, and abort, and
a client that follows along and reports what it is doing. The recording half —
capturing a fresh sequence from live operator inputs — is a deliberate gap; see
What this supports below.
Mental model
master (SCM) client (SCC)
┌──────────────┐ ┌──────────────┐
│ list of steps│ master status │ executes the │
│ + lifecycle │ ──────────────────► │ client │
│ timers │ │ function │
│ │ ◄────────────────── │ │
└──────────────┘ client status └──────────────┘
│ │
│ "start step 7" │ "ready / playing back /
│ "pause" / "resume" / "abort" │ aborting" + step echo
▼ ▼
Two control functions, two status messages, one shared cadence. The master
broadcasts its lifecycle state and the current step on PGN_SC_MASTER_STATUS.
Each client broadcasts its own state and the step it is acting on via
PGN_SC_CLIENT_STATUS. Neither side commands the other with a one-shot request:
the whole exchange is a pair of periodic status messages that each side reads
and reacts to. Clients are independent of one another — they only watch the
master, never each other.
The master/client relationship
| Role | What it owns | What it broadcasts |
|---|---|---|
| Master (SCM) | The list of steps, the lifecycle, the ready/active timeouts, who has acknowledged. | Its master state, the current step id, and busy flags. |
| Client (SCC) | The actual implement function and whether it is busy. | Its client state, the step it is executing, and a function-error byte. |
Only one master is meant to be active at a time. A client follows whichever
active master it sees and mirrors the sequence state back so the master can tell
that the step landed. The master treats a step as truly active only once enough
clients have acknowledged it — configurable through
SCMasterConfig::required_client_count (a 0 is treated as 1, so a sequence
never starts with no participation).
Lifecycle as a state machine
machbus runs both ends on one unified internal state, sc::SCState, with these
variants: Idle, Ready, Active, Paused, Complete, Error. The master
and client map this onto the wire-level master state (SCMasterState) and
sequence state (SCSequenceState) when they encode a status frame.
add_step()* (client ack ≥ required_count)
┌──────┐ ┌──────────────┐ start() ┌───────┐ ─────────────────► ┌────────┐
│ Idle │──►│ steps loaded│ ────────► │ Ready │ │ Active │
└──────┘ └──────────────┘ └───────┘ ◄───── resume() ── └────────┘
▲ │ │ (Paused) │ │
│ │ │ ready-timeout │ │ pause()
│ │ └──────────────┐ │ ▼
│ │ │ ┌────────┐
│ ▼ │ │ Paused │
│ (master Idle/Inactive seen) ┌───────┐ │ └────────┘
└─────────────────────────────── │ Error │ ◄────────────┘
└───────┘ abort() / timeout /
step_completed() advances the step; client abort
last step ──► ┌──────────┐
│ Complete │
└──────────┘
* steps may only be added while Idle.
The transitions, in words:
- Load and start. You add steps while the master is
Idle(add_steprefuses once you leaveIdle).startrequires at least one step and moves the master toReady, clearing its ready/active timers and its ready/ack client sets. - Ready → Active. The master sits in
Readyadvertising the sequence as ready. As clients report Ready, the master collects their addresses; once it hasrequired_client_countunique clients it transitions toActiveand emits the first step. A client moves itself toReadythe moment it sees an active master advertising a ready sequence. - A step advances. In
Active, the master broadcasts the current step’s wire-visible id. The client executes and acknowledges by playing back that same step number. When the application callsstep_completed(step_id)on the master, the step is marked done, the index advances, and either the next step is emitted or the master transitions toCompleteafter the last one. - Pause / resume. The master may
pauseonly fromActive(→Paused) andresumeonly fromPaused(→Active, re-arming the active timer and clearing the per-step ack set). A client infers pause when an active master drops the sequence back to ready while the client was active, and infers resume when playback returns. - Abort and errors.
abortfrom any live state forcesErrorand makes the very next status carry an Abort sequence state, so the abort is visible on the bus, not just locally. Ready and active timeouts also land inError, as does a client reporting Abort.
Anatomy of the messages
Both status messages are classic 8-byte CAN payloads; machbus rejects shorter
or overlong reassembled buffers rather than prefix-decoding them.
Master status (PGN_SC_MASTER_STATUS) carries:
- byte 0 — a fixed message code (
SC_MSG_CODE_MASTER) that identifies the frame; - byte 1 — the master state (
SCMasterState:Inactive,Active, or the reservedInitialization); - byte 2 — the current sequence/step number, or the
0xFFnot-applicable sentinel when the sequence is merely ready; - byte 3 — the sequence state (
SCSequenceState); - byte 4 — two busy flags in bits 1–2 (non-volatile-memory busy, SCD-parsing
busy); bits 3–8 are unallocated, transmitted as
1and ignored on receive; - bytes 5–7 — reserved, held at
0xFFon transmit and ignored on receive.
Client status (PGN_SC_CLIENT_STATUS) mirrors it: a client message code in
byte 0, the client state (SCClientState: Disabled, Enabled, or reserved
Initialization) in byte 1, the echoed step number in byte 2, the sequence state
in byte 3, and a function-error byte (SCClientFuncError: NoErrors,
NoChange, Changed, NeedsConfirm) in byte 4, with the same reserved tail.
A few rules the decoders enforce, all from sc::types:
- The selected sequence number is a small standard-defined wire range. Library
step ids therefore run
0..=SC_MAX_SEQUENCE_STEP_ID(0x31); higher byte values are reserved, and0xFFis the ready / not-applicable sentinel.add_steprejects a larger id, and rejects duplicate ids in the same sequence. - A Ready sequence state must carry the
0xFFsentinel; a PlayBack state must not. Mismatched combinations are rejected. - The reserved tail bytes must all be
0xFF, or the frame is rejected.
What this supports vs rejects
This is the part to be precise about. SCSequenceState names all of the
standard’s sequence states — Reserved, Ready, Recording,
RecordingCompletion, PlayBack, Abort — so the decoders can recognize them on
the wire. But only Ready, PlayBack, and Abort are supported as active
behavior. When an active master or an enabled client advertises Recording or
RecordingCompletion, machbus treats it as an unsupported sequence state and
rejects the frame; the local state machine does not move. Likewise, the
Initialization master and client states are recognized as names but rejected as
inputs.
The practical consequence: machbus plays back sequences; it does not record
them. There is no API to capture a fresh sequence from live operator inputs.
You define steps yourself (as SequenceStep values) and the master replays them.
This is a current limitation, stated plainly rather than implied — the recording
phase, the SCD object definitions, and the VT object pools that a full
implementation would use are out of scope here.
Doing it with machbus
There are two layers. The pump-style sc::SCMaster / sc::SCClient are codecs
plus state machines: you feed them frames and elapsed time and they hand back
[u8; 8] payloads to transmit. The session facade wires those into the unified
event loop so you do not dispatch frames by hand.
You plug one or both roles into a Session at build time:
#![allow(unused)]
fn main() {
use machbus::session::{Session, EndpointTransport, plugins::ScMaster};
let (ctrl, mut driver) = Session::builder(name, 0x80)
.plug(ScMaster::new(SCMasterConfig::default()))
.spawn(EndpointTransport::new(0, endpoint))?;
ctrl.start()?;
// load steps and start the sequence through fine control:
ctrl.with_mut::<ScMaster, _>(|m| {
m.add_step(SequenceStep { step_id: 7, ..Default::default() })?;
m.start()
})?;
}
You reach the master through ctrl.with_mut::<ScMaster, _>(|m| ...), which
returns the master so you can call add_step, start, pause, resume,
abort, step_completed, and state. A client plugs
ScClient::new(...) and reaches it with ctrl.with_mut::<ScClient, _>(|c| ...),
exposing state, is_busy, set_busy, and report_step_complete.
The session owns the I/O: inbound SC status PGNs are routed in, the master/client
update functions run on each driver.poll()?, and any emitted payloads are
broadcast for you. You react through typed events with ctrl.drain::<ScEvent>(),
described next.
Sequence Control has a natural integration point with section state: after a
master announces a wire-visible step id, you feed that id through a
SectionRouter to toggle implement sections. You drain the master’s
ScEvent::MasterStepStarted { step_id }, bind sections to VT indicator objects,
and map step ids such as 7 and 8 onto section changes. The router
deduplicates (setting the same value twice is a no-op) and skips the VT push when
no VT plugin is present.
Events and responsibilities
The session translates internal master/client events into ScEvent variants you
drain from the queue:
| Event | Side | Meaning |
|---|---|---|
MasterStateChanged { from, to } | master | The master’s lifecycle state moved. |
MasterStepStarted { step_id } | master | A new step was dispatched. |
MasterStepCompleted { step_id } | master | A step was recorded complete. |
MasterSequenceComplete | master | The last step finished. |
MasterTimeout { reason } | master | A ready or active timeout struck. |
MasterClientStatus { source, state } | master | A valid client status arrived. |
ClientStateChanged { from, to } | client | The client’s state moved after a master status or timeout. |
ClientSequenceStart | client | The client observed a new sequence start. |
ClientStepRequest { step_id } | client | The client was asked to execute a step. |
ClientPause / ClientResume | client | The client inferred pause or resume. |
ClientAbort | client | The client aborted or entered error. |
Your responsibilities split cleanly by role. As a master application you load
steps, call start, and — crucially — call step_completed(step_id) once your
logic decides a step is done; the master does not advance on its own. As a
client application you act on ClientStepRequest, set set_busy(true) while
the function is in progress, and call report_step_complete(step_id) to
acknowledge. The one rule the client enforces for you: report_step_complete
only works while the client is Active and only for the step it was actually
asked to run.
Edge cases and failures
- Abort mid-sequence. Calling
abortfromReady,Active, orPausedforcesErrorand emits a visible Abort status immediately. Calling it fromIdle,Complete, orErroris rejected — there is nothing to abort. The standard’s intent is that an abort halts motion; your application is responsible for actually bringing the machine to a safe state when it sees the abort. - Client not ready. If a client never reports Ready (or fewer than
required_client_countclients do), the master never leavesReady. The ready timeout eventually fires and drops it toError. - Client stuck busy. A client that stays busy past its
busy_pause_timeout_mswhile Active or Paused drives itself toError, emits an Abort status, and the master picks that up as a sequence-level abort. - No ack for a step. In
Active, if the required clients do not acknowledge the current step within the active timeout, the master times out toError. An ack only counts when it echoes the current step number. - Unsupported state on the wire. A frame advertising
Recording,RecordingCompletion,Initialization, reserved state bytes, bad busy bits, a wrong message code, a wrong length, or a non-0xFFtail is rejected without moving the state machine. Use thetry_*handlers (try_handle_master_status/try_handle_client_status) when you need the explicit validation error rather than a silent no-op. - Master disappears. A client that sees the master go Inactive/Idle returns
to
Idleand emits an abort event, so a vanished master is not mistaken for a paused one.
Advanced
- Multi-client coordination. Set
required_client_countabove 1 to demand that several distinct implements participate. The master keys on client source address, so a duplicate Ready from one client does not count as a second participant, and every required client must echo the current step before its ack timer is disarmed. - Safe state on abort. The library makes the abort visible; it does not move
hydraulics. Treat
MasterTimeout,ClientAbort, and any transition intoErroras a cue to put your functions in a safe state. See Shortcut Button and safe-state thinking. - Timing and cadence. The status messages are paced: clients hold a minimum
spacing between sends (
min_status_spacing_ms) and defer a status to the nextupdateif a send would come too soon. The master emits on itsstatus_interval_ms. The standard expects a faster cadence in active states than in ready; the timeout and spacing constants insc::typesencode those expectations, andSCMasterConfig/SCClientConfiglet you tune them. - Session facade vs the bare codecs. Use the
ScMaster/ScClientplugins for applications — they route frames, run the update loop, and fan out events. Drop toSCMaster/SCClientdirectly for unit tests or tightly controlled embedded loops where you own every elapsed millisecond.
Validate locally
make test
The session tests run entirely in software: they plug the SC roles into a
Session, bind sections, and apply step ids through the router. The unit tests
under sc::master and
sc::client cover the lifecycle directly — ready-to-active on client ack,
step advance and completion, pause/resume, every timeout path, the abort paths,
the multi-client count, and the rejection of unsupported and malformed frames.
What this proves / does not prove
Proves: the playback lifecycle — ready, active, step advance, pause, resume, complete, and the abort/timeout error paths — behaves deterministically in software, the master/client status messages round-trip, and the malformed and unsupported-state frames are rejected rather than silently accepted.
Does not prove: recording of live operator sequences (not implemented), the SCD object and VT object-pool machinery a full system uses, real-hardware timing, interoperability with a specific third-party master or implement, or any conformance/certification claim. Those still require official standards, real hardware, and interoperability evidence.
See also
- Sequence Control and TIM — how sequence playback relates to TIM (automated control of tractor functions by an implement) and to tractor-implement management.
- Shortcut Button and safe-state thinking — what to do when a sequence aborts or a function must stop.