Control Program Patterns and Examples
This chapter covers common patterns you will use in nearly every control program. If you have written PLC programs in Structured Text, you will recognize most of these.
State Machines
State machines are the most common pattern in machine control. In AutoCore, you define states as a Rust enum and transition between them in process_tick:
#![allow(unused)]
fn main() {
use autocore_std::{ControlProgram, TickContext};
use crate::gm::GlobalMemory;
#[derive(Debug, Clone, Copy, PartialEq)]
enum MachineState {
Idle,
Homing,
Ready,
Running,
Stopping,
Faulted,
}
pub struct MyControlProgram {
state: MachineState,
prev_state: MachineState,
}
impl MyControlProgram {
pub fn new() -> Self {
Self {
state: MachineState::Idle,
prev_state: MachineState::Idle,
}
}
}
impl ControlProgram for MyControlProgram {
type Memory = GlobalMemory;
fn initialize(&mut self, _mem: &mut Self::Memory) {
log::info!("Machine starting in Idle state");
}
fn process_tick(&mut self, ctx: &mut TickContext<Self::Memory>) {
// Log state changes
if self.state != self.prev_state {
log::info!("State: {:?} -> {:?}", self.prev_state, self.state);
self.prev_state = self.state;
}
match self.state {
MachineState::Idle => {
ctx.gm.status_code = 0;
if ctx.gm.cmd_start {
self.state = MachineState::Homing;
}
}
MachineState::Homing => {
ctx.gm.status_code = 1;
// Perform homing sequence...
// When complete:
self.state = MachineState::Ready;
}
MachineState::Ready => {
ctx.gm.status_code = 2;
if ctx.gm.cmd_run {
self.state = MachineState::Running;
}
}
MachineState::Running => {
ctx.gm.status_code = 3;
// Main production logic here...
if ctx.gm.cmd_stop {
self.state = MachineState::Stopping;
}
if ctx.gm.emergency_stop {
self.state = MachineState::Faulted;
}
}
MachineState::Stopping => {
ctx.gm.status_code = 4;
// Decelerate, finish current operation...
// When stopped:
self.state = MachineState::Idle;
}
MachineState::Faulted => {
ctx.gm.status_code = 99;
ctx.gm.motor_enable = false;
if ctx.gm.cmd_reset {
self.state = MachineState::Idle;
}
}
}
}
}
}
Corresponding variables in project.json:
"variables": { "cmd_start": { "type": "bool", "direction": "command", "description": "Start command from HMI" }, "cmd_run": { "type": "bool", "direction": "command", "description": "Run command from HMI" }, "cmd_stop": { "type": "bool", "direction": "command", "description": "Stop command from HMI" }, "cmd_reset": { "type": "bool", "direction": "command", "description": "Reset faults from HMI" }, "emergency_stop": { "type": "bool", "direction": "input", "description": "Emergency stop input" }, "motor_enable": { "type": "bool", "direction": "output", "description": "Motor enable output" }, "status_code": { "type": "i32", "direction": "status", "description": "Machine state code" } }
Edge Detection (Rising and Falling Triggers)
AutoCore provides function blocks for detecting signal transitions, just like R_TRIG and F_TRIG in IEC 61131-3:
#![allow(unused)]
fn main() {
use autocore_std::fb::{RTrig, FTrig};
pub struct MyControlProgram {
start_trigger: RTrig, // Detects false → true
stop_trigger: FTrig, // Detects true → false
part_counter: u32,
}
impl MyControlProgram {
pub fn new() -> Self {
Self {
start_trigger: RTrig::new(),
stop_trigger: FTrig::new(),
part_counter: 0,
}
}
}
impl ControlProgram for MyControlProgram {
type Memory = GlobalMemory;
fn process_tick(&mut self, ctx: &mut TickContext<Self::Memory>) {
// Rising edge: fires once when start_button goes from false to true
if self.start_trigger.call(ctx.gm.start_button) {
log::info!("Start button pressed!");
// This runs exactly once per button press
}
// Falling edge: fires once when sensor goes from true to false
if self.stop_trigger.call(ctx.gm.part_sensor) {
self.part_counter += 1;
log::info!("Part detected! Count: {}", self.part_counter);
}
ctx.gm.part_count = self.part_counter;
}
}
}
How RTrig works:
| Previous Value | Current Value | Output |
|---|---|---|
false | false | false |
false | true | true (rising edge!) |
true | true | false |
true | false | false |
How FTrig works:
| Previous Value | Current Value | Output |
|---|---|---|
true | true | false |
true | false | true (falling edge!) |
false | false | false |
false | true | false |
Timers
The Ton (Timer On Delay) function block works like TON in IEC 61131-3. The output becomes true after the input has been true for a specified duration:
#![allow(unused)]
fn main() {
use autocore_std::fb::Ton;
use std::time::Duration;
pub struct MyControlProgram {
startup_delay: Ton,
fault_timer: Ton,
}
impl MyControlProgram {
pub fn new() -> Self {
Self {
startup_delay: Ton::new(),
fault_timer: Ton::new(),
}
}
}
impl ControlProgram for MyControlProgram {
type Memory = GlobalMemory;
fn process_tick(&mut self, ctx: &mut TickContext<Self::Memory>) {
// Wait 3 seconds after machine_running becomes true
// before enabling the motor
let delay_done = self.startup_delay.call(
ctx.gm.machine_running,
Duration::from_secs(3),
);
ctx.gm.motor_enable = delay_done;
// If temperature is too high for more than 5 seconds, raise an alarm
let overtemp = ctx.gm.temperature > 80.0;
let alarm = self.fault_timer.call(overtemp, Duration::from_secs(5));
ctx.gm.temperature_alarm = alarm;
// You can also read the elapsed time
if overtemp && !alarm {
log::warn!(
"Temperature high for {:.1}s (alarm at 5.0s)",
self.fault_timer.et.as_secs_f64()
);
}
}
}
}
Ton behavior:
| Input | Duration | Timer State | Output (q) | Elapsed (et) |
|---|---|---|---|---|
false | any | Reset | false | 0 |
true | 3s | Counting | false | 0..3s |
true (after 3s) | 3s | Done | true | 3s |
false (any time) | any | Reset | false | 0 |
Combining Patterns: A Complete Machine Example
Here is a more realistic example that combines state machines, edge detection, and timers to control a simple pick-and-place machine:
project.json variables:
"variables": {
"cmd_start": { "type": "bool", "direction": "command", "description": "Start button" },
"cmd_stop": { "type": "bool", "direction": "command", "description": "Stop button" },
"cmd_reset": { "type": "bool", "direction": "command", "description": "Reset faults" },
"part_present": { "type": "bool", "direction": "input", "description": "Part sensor at pick position" },
"cylinder_extended": { "type": "bool", "direction": "input", "description": "Cylinder extended sensor" },
"cylinder_retracted": { "type": "bool", "direction": "input", "description": "Cylinder retracted sensor" },
"gripper_closed": { "type": "bool", "direction": "input", "description": "Gripper closed sensor" },
"extend_cylinder": { "type": "bool", "direction": "output", "description": "Cylinder extend solenoid" },
"close_gripper": { "type": "bool", "direction": "output", "description": "Gripper close solenoid" },
"conveyor_run": { "type": "bool", "direction": "output", "description": "Conveyor motor" },
"parts_completed": { "type": "u32", "direction": "status", "description": "Total parts completed" },
"machine_state": { "type": "i32", "direction": "status", "description": "Current state code" },
"fault_active": { "type": "bool", "direction": "status", "description": "Fault is active" }
}
control/src/program.rs:
#![allow(unused)]
fn main() {
use autocore_std::{ControlProgram, TickContext};
use autocore_std::fb::{RTrig, Ton};
use std::time::Duration;
use crate::gm::GlobalMemory;
#[derive(Debug, Clone, Copy, PartialEq)]
enum State {
Idle, // 0: Waiting for start
WaitForPart, // 1: Conveyor running, waiting for a part
Extend, // 2: Extending cylinder to pick position
WaitExtended, // 3: Waiting for cylinder to reach
Grip, // 4: Closing gripper
WaitGripped, // 5: Waiting for gripper to close
Retract, // 6: Retracting cylinder
WaitRetracted, // 7: Waiting for cylinder to retract
Release, // 8: Opening gripper to release part
WaitReleased, // 9: Waiting for gripper to open
Fault, // 99: Fault condition
}
pub struct MyControlProgram {
state: State,
prev_state: State,
start_trig: RTrig,
reset_trig: RTrig,
timeout: Ton,
parts_done: u32,
}
impl MyControlProgram {
pub fn new() -> Self {
Self {
state: State::Idle,
prev_state: State::Idle,
start_trig: RTrig::new(),
reset_trig: RTrig::new(),
timeout: Ton::new(),
parts_done: 0,
}
}
fn go_to(&mut self, new_state: State) {
self.state = new_state;
}
fn fault(&mut self, reason: &str) {
log::error!("FAULT: {}", reason);
self.state = State::Fault;
}
}
impl ControlProgram for MyControlProgram {
type Memory = GlobalMemory;
fn initialize(&mut self, _mem: &mut Self::Memory) {
log::info!("Pick-and-place machine initialized");
}
fn process_tick(&mut self, ctx: &mut TickContext<Self::Memory>) {
// Log state transitions
if self.state != self.prev_state {
log::info!("State: {:?} -> {:?}", self.prev_state, self.state);
self.prev_state = self.state;
}
// Edge triggers
let start_pressed = self.start_trig.call(ctx.gm.cmd_start);
let reset_pressed = self.reset_trig.call(ctx.gm.cmd_reset);
// Global timeout: if any wait state takes longer than 10 seconds, fault
let waiting = matches!(
self.state,
State::WaitExtended | State::WaitGripped |
State::WaitRetracted | State::WaitReleased
);
if self.timeout.call(waiting, Duration::from_secs(10)) {
self.fault("Operation timed out");
}
// Global stop
if ctx.gm.cmd_stop && self.state != State::Idle && self.state != State::Fault {
log::info!("Stop requested");
self.go_to(State::Idle);
}
// State machine
match self.state {
State::Idle => {
ctx.gm.extend_cylinder = false;
ctx.gm.close_gripper = false;
ctx.gm.conveyor_run = false;
if start_pressed {
self.go_to(State::WaitForPart);
}
}
State::WaitForPart => {
ctx.gm.conveyor_run = true;
if ctx.gm.part_present {
ctx.gm.conveyor_run = false;
self.go_to(State::Extend);
}
}
State::Extend => {
ctx.gm.extend_cylinder = true;
self.go_to(State::WaitExtended);
}
State::WaitExtended => {
if ctx.gm.cylinder_extended {
self.go_to(State::Grip);
}
}
State::Grip => {
ctx.gm.close_gripper = true;
self.go_to(State::WaitGripped);
}
State::WaitGripped => {
if ctx.gm.gripper_closed {
self.go_to(State::Retract);
}
}
State::Retract => {
ctx.gm.extend_cylinder = false;
self.go_to(State::WaitRetracted);
}
State::WaitRetracted => {
if ctx.gm.cylinder_retracted {
self.go_to(State::Release);
}
}
State::Release => {
ctx.gm.close_gripper = false;
self.go_to(State::WaitReleased);
}
State::WaitReleased => {
if !ctx.gm.gripper_closed {
self.parts_done += 1;
ctx.gm.parts_completed = self.parts_done;
log::info!("Part complete! Total: {}", self.parts_done);
self.go_to(State::WaitForPart);
}
}
State::Fault => {
// Turn off all outputs
ctx.gm.extend_cylinder = false;
ctx.gm.close_gripper = false;
ctx.gm.conveyor_run = false;
if reset_pressed {
log::info!("Fault reset");
self.go_to(State::Idle);
}
}
}
// Update status outputs
ctx.gm.machine_state = match self.state {
State::Idle => 0,
State::WaitForPart => 1,
State::Extend | State::WaitExtended => 2,
State::Grip | State::WaitGripped => 3,
State::Retract | State::WaitRetracted => 4,
State::Release | State::WaitReleased => 5,
State::Fault => 99,
};
ctx.gm.fault_active = self.state == State::Fault;
}
}
}