Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 ValueCurrent ValueOutput
falsefalsefalse
falsetruetrue (rising edge!)
truetruefalse
truefalsefalse

How FTrig works:

Previous ValueCurrent ValueOutput
truetruefalse
truefalsetrue (falling edge!)
falsefalsefalse
falsetruefalse

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:

InputDurationTimer StateOutput (q)Elapsed (et)
falseanyResetfalse0
true3sCountingfalse0..3s
true (after 3s)3sDonetrue3s
false (any time)anyResetfalse0

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;
    }
}
}