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

Appendix B: Function Block Reference

AutoCore provides standard function blocks inspired by IEC 61131-3. Import them from autocore_std::fb.

RTrig — Rising Edge Detector

Detects false to true transitions. Equivalent to R_TRIG in IEC 61131-3.

#![allow(unused)]
fn main() {
use autocore_std::fb::RTrig;

let mut trig = RTrig::new();

trig.call(false);  // returns false
trig.call(true);   // returns true  (rising edge detected)
trig.call(true);   // returns false (no transition)
trig.call(false);  // returns false
trig.call(true);   // returns true  (another rising edge)
}

FTrig — Falling Edge Detector

Detects true to false transitions. Equivalent to F_TRIG in IEC 61131-3.

#![allow(unused)]
fn main() {
use autocore_std::fb::FTrig;

let mut trig = FTrig::new();

trig.call(true);   // returns false
trig.call(false);  // returns true  (falling edge detected)
trig.call(false);  // returns false (no transition)
trig.call(true);   // returns false
trig.call(false);  // returns true  (another falling edge)
}

Ton — Timer On Delay

Output becomes true after input has been true for the specified duration. Equivalent to TON in IEC 61131-3.

#![allow(unused)]
fn main() {
use autocore_std::fb::Ton;
use std::time::Duration;

let mut timer = Ton::new();

// In process_tick:
let done = timer.call(input_signal, Duration::from_secs(5));
// done = true after input_signal has been true for 5 seconds continuously
// timer.et = elapsed time
// timer.q = same as the return value (done)

// If input_signal becomes false at any time, the timer resets
}

Fields:

FieldTypeDescription
qboolOutput — true when timer has elapsed
etDurationElapsed time since input became true

BitResetOnDelay — Auto-Reset Timer

Sets output to false after a delay. Useful for pulse outputs.

#![allow(unused)]
fn main() {
use autocore_std::fb::BitResetOnDelay;
use std::time::Duration;

let mut reset = BitResetOnDelay::new(Duration::from_millis(500));

// When you set the bit to true, it automatically resets to false after 500ms
reset.set(); // Output becomes true
// ... 500ms later, in process_tick ...
reset.call(); // Call every cycle to update
// reset.q becomes false after the delay
}

RunningAverage — Online Averaging

Computes a running average of values.

#![allow(unused)]
fn main() {
use autocore_std::fb::RunningAverage;

let mut avg = RunningAverage::new();

avg.add(10.0);
avg.add(20.0);
avg.add(30.0);
let mean = avg.average(); // 20.0
let count = avg.count();  // 3

avg.reset(); // Start over
}

Shutdown — System Shutdown Controller

Initiates or cancels a full system shutdown via IPC. The server delays the actual power-off by 15 seconds, giving the control program (or a human operator) time to cancel. All methods are non-blocking — the block tracks the IPC transaction internally and updates its output flags each scan cycle.

#![allow(unused)]
fn main() {
use autocore_std::fb::Shutdown;

struct MyProgram {
    shutdown: Shutdown,
    shutdown_trigger: RTrig,
    cancel_trigger: RTrig,
}

impl MyProgram {
    fn new() -> Self {
        Self {
            shutdown: Shutdown::new(),
            shutdown_trigger: RTrig::new(),
            cancel_trigger: RTrig::new(),
        }
    }
}
}

In process_tick:

#![allow(unused)]
fn main() {
// Always call once per cycle to poll for server responses
self.shutdown.call(ctx.client);

// Initiate shutdown on rising edge of a button
if self.shutdown_trigger.call(ctx.gm.shutdown_button) {
    self.shutdown.initiate(ctx.client);
}

// Cancel shutdown on rising edge of an abort button
if self.cancel_trigger.call(ctx.gm.abort_button) {
    self.shutdown.cancel(ctx.client);
}

// React to results
if self.shutdown.done {
    log::info!("Server confirmed the command");
}
if self.shutdown.error {
    log::error!("Shutdown error: {}", self.shutdown.error_message);
}
}

Methods:

MethodSignatureDescription
new() -> SelfCreate in idle state
call(&mut self, client: &mut CommandClient)Poll for response — call every scan cycle
initiate(&mut self, client: &mut CommandClient)Send system.full_shutdown. No-op while busy.
cancel(&mut self, client: &mut CommandClient)Send system.cancel_full_shutdown. No-op while busy.
is_initiating(&self) -> boolA shutdown initiation is pending
is_cancelling(&self) -> boolA shutdown cancellation is pending

Output fields:

FieldTypeDescription
busybooltrue while waiting for the server to respond
donebooltrue for one cycle after the server confirms the command
errorbooltrue for one cycle after the server returns an error
error_messageStringError description from the server (empty when no error)

Server broadcasts: When a shutdown is scheduled, the server sends system.shutdown_pending to all connected clients. If cancelled, it sends system.shutdown_cancelled. After the 15-second delay elapses, it sends system.shutdown_executing just before powering off.

Typical flow:

Cycle 1:  initiate()          → busy=true
Cycle 2:  call()              → (waiting for response)
Cycle 3:  call()              → done=true, busy=false  (server accepted)
          ... 15 seconds pass on the server ...
          Server powers off the PC

Cancellation flow:

Cycle 1:  initiate()          → busy=true
Cycle 2:  call()              → done=true  (shutdown scheduled)
Cycle 3:  call()              → done cleared
Cycle 4:  cancel()            → busy=true
Cycle 5:  call()              → done=true  (shutdown cancelled)

ni::DaqCapture — NI DAQ Triggered Capture

Manages the full lifecycle of a triggered DAQ capture: arm the trigger, wait for the hardware event, and retrieve the captured data. All communication is via IPC commands to the autocore-ni module — the control program does not need to interact with capture shared memory directly.

#![allow(unused)]
fn main() {
use autocore_std::fb::ni::DaqCapture;
use autocore_std::fb::RTrig;

struct MyProgram {
    daq: DaqCapture,
    arm_trigger: RTrig,
}

impl MyProgram {
    fn new() -> Self {
        Self {
            daq: DaqCapture::new("ni.impact"),  // matches the DAQ name in NI config
            arm_trigger: RTrig::new(),
        }
    }
}
}

In process_tick:

#![allow(unused)]
fn main() {
// Call every cycle with the execute signal and a timeout (ms).
// Rising edge on the first argument triggers a new capture sequence.
self.daq.call(ctx.gm.arm_request, 5000, ctx.client);

// Check results
if self.daq.error {
    log::error!("Capture error: {}", self.daq.error_message);
}

if !self.daq.busy && !self.daq.error {
    if let Some(data) = &self.daq.data {
        // Capture complete — process the data
        log::info!(
            "Captured {} channels, {} samples/ch at {} Hz",
            data.channel_count, data.actual_samples, data.sample_rate,
        );

        // Access channel data: data.channels[ch_idx][sample_idx]
        let ch0_peak = data.channels[0].iter().cloned().fold(f64::MIN, f64::max);
        log::info!("Channel 0 peak: {:.2}", ch0_peak);

        // Trigger point is at sample index data.pre_trigger_samples
        let trigger_sample = data.pre_trigger_samples;
        log::info!("Trigger at sample {}", trigger_sample);
    }
}
}

Constructor:

MethodSignatureDescription
new(daq_fqdn: &str) -> SelfCreate a new capture FB. daq_fqdn is the FQDN prefix for the DAQ, e.g. "ni.impact".

Call signature:

#![allow(unused)]
fn main() {
pub fn call(&mut self, execute: bool, timeout_ms: u32, client: &mut CommandClient)
}

Call once per cycle. A rising edge on execute starts a new capture sequence. The FB internally handles edge detection, arming, polling for completion, reading the data, and timeout tracking.

Output fields:

FieldTypeDescription
busybooltrue from the rising edge of execute until capture completes or an error/timeout occurs
activebooltrue while the DAQ is armed and waiting for the hardware trigger event
errorbooltrue when an error or timeout occurs. Stays true until the next rising edge of execute.
error_messageStringError description (empty when no error)
dataOption<CaptureData>Some(...) after a successful capture, None otherwise

CaptureData fields:

FieldTypeDescription
channelsVec<Vec<f64>>Sample data per channel. channels[ch_idx][sample_idx].
channel_countusizeNumber of channels in the capture
capture_lengthusizeConfigured post-trigger samples per channel
pre_trigger_samplesusizeConfigured pre-trigger samples per channel
actual_samplesusizeTotal samples per channel actually written (pre + post)
sample_ratef64Sample rate in Hz
timestamp_nsu64UNIX timestamp (nanoseconds) of the trigger event
sequenceu64Monotonically increasing capture sequence number

State machine:

Idle ──(rising edge on execute)──> Arming        (busy=true)
Arming ──(arm confirmed)────────> Waiting        (active=true)
Waiting ──(data ready)──────────> Reading Data
Waiting ──(timeout)─────────────> Idle           (error=true)
Reading Data ──(data received)──> Idle           (data=Some(...))
Reading Data ──(error)──────────> Idle           (error=true)

IPC commands used internally: <daq_fqdn>.arm, <daq_fqdn>.capture_status, <daq_fqdn>.read_capture. These are handled by the autocore-ni module.

Complete example — impact test with force plate:

#![allow(unused)]
fn main() {
use autocore_std::{ControlProgram, TickContext};
use autocore_std::fb::ni::DaqCapture;
use crate::gm::GlobalMemory;

pub struct ImpactTest {
    daq: DaqCapture,
    peak_force: f64,
}

impl ImpactTest {
    pub fn new() -> Self {
        Self {
            daq: DaqCapture::new("ni.impact"),
            peak_force: 0.0,
        }
    }
}

impl ControlProgram for ImpactTest {
    type Memory = GlobalMemory;

    fn process_tick(&mut self, ctx: &mut TickContext<Self::Memory>) {
        // Arm on rising edge of the HMI button, 10 second timeout
        self.daq.call(ctx.gm.arm_request, 10000, ctx.client);

        // Write status to HMI
        ctx.gm.capture_busy = self.daq.busy;
        ctx.gm.capture_active = self.daq.active;
        ctx.gm.capture_error = self.daq.error;

        // Process completed capture
        if !self.daq.busy && !self.daq.error {
            if let Some(data) = &self.daq.data {
                // Sum all force channels to get total force per sample
                let num_samples = data.actual_samples;
                let mut total_force = vec![0.0f64; num_samples];
                for ch in &data.channels {
                    for (i, &v) in ch.iter().enumerate() {
                        total_force[i] += v;
                    }
                }

                // Find peak total force
                self.peak_force = total_force.iter().cloned().fold(f64::MIN, f64::max);
                ctx.gm.peak_force = self.peak_force;

                log::info!("Impact captured: peak force = {:.1} N", self.peak_force);
            }
        }
    }
}
}

Motion

Import motion function blocks from autocore_std::motion.

SeekProbe — Jog to Sensor

Jogs an axis in the negative direction until a sensor triggers, then halts. Inspired by IEC 61131-3 homing patterns. The axis must be enabled and at a position > 0 before executing.

Jog velocity, acceleration, and deceleration are taken from the axis configuration (jog_speed, jog_accel, jog_decel).

use autocore_std::motion::{Axis, AxisConfig, SeekProbe};
use crate::gm::Axis1PpStubs;

struct MyProgram {
    axis: Axis,
    seek: SeekProbe,
    stubs: Axis1PpStubs,
}

impl MyProgram {
    fn new() -> Self {
        let config = AxisConfig::new(12_800)
            .with_user_scale(100.0);  // mm per rev
        Self {
            axis: Axis::new(config, "Axis1"),
            seek: SeekProbe::new(),
            stubs: Axis1PpStubs::default(),
        }
    }
}

In process_tick:

let Self { axis, seek, stubs } = self;

// Build the view and tick the axis first
let mut view = stubs.view(&mut ctx.gm);
axis.tick(&mut view, ctx.client);

// Run the seek probe state machine
seek.call(
    axis,
    &mut view,
    ctx.gm.start_seek,    // execute: rising edge triggers operation
    ctx.gm.ball_sensor,   // sensor: halts axis when true
);

// Check results
if seek.done {
    log::info!("Probe found at position {:.3}", axis.position);
}
if seek.error {
    log::error!("Seek failed: code={}", seek.state.error_code);
}

Methods:

MethodSignatureDescription
new() -> SelfCreate in idle state
call(&mut self, axis, view, execute: bool, sensor: bool)Execute one scan cycle. Rising edge on execute starts the seek.
abort(&mut self, axis, view)Halt immediately and set error code 1
is_busy(&self) -> booltrue while seek operation is in progress

Output fields:

FieldTypeDescription
donebooltrue for one cycle when the probe is found and axis has stopped
errorbooltrue for one cycle when an error occurs
stateStateMachineState machine with index, error_code, error_message

Error codes:

CodeMeaning
1Abort called while motion was active
100Axis position is not > 0 at start
120Axis error or control disabled during motion
200Axis reported error when stopping

State diagram:

┌──────────┐  execute ↑  ┌────────────┐ position>0  ┌────────────────┐
│ 10: Idle │──────────►│ 100: Start │────────────►│ 120: Jogging   │
└──────────┘           └────────────┘             │  (negative)    │
      ▲                      │ pos<=0             └───────┬────────┘
      │                      ▼                   sensor │  │ axis error
      │                 error_code=100                   ▼  ▼
      │                                          ┌────────────────┐
      │◄──── done=true ◄─────────────────────────│ 200: Stopping  │
      │◄──── error=true ◄────────────────────────│                │
      │                                          └────────────────┘
      │◄──── error=true ◄── 250: Motion Error

Complete example — ball detect on a linear slide:

use autocore_std::{ControlProgram, TickContext};
use autocore_std::motion::{Axis, AxisConfig, SeekProbe};
use crate::gm::{GlobalMemory, SlidePpStubs};

pub struct BallDetect {
    axis: Axis,
    seek: SeekProbe,
    stubs: SlidePpStubs,
}

impl BallDetect {
    pub fn new() -> Self {
        let config = AxisConfig::new(12_800)
            .with_user_scale(100.0);  // mm per rev
        Self {
            axis: Axis::new(config, "Slide"),
            seek: SeekProbe::new(),
            stubs: SlidePpStubs::default(),
        }
    }
}

impl ControlProgram for BallDetect {
    type Memory = GlobalMemory;

    fn process_tick(&mut self, ctx: &mut TickContext<Self::Memory>) {
        let Self { axis, seek, stubs } = self;
        let mut view = stubs.view(&mut ctx.gm);
        axis.tick(&mut view, ctx.client);

        seek.call(
            axis,
            &mut view,
            ctx.gm.start_button,
            ctx.gm.proximity_sensor,
        );

        ctx.gm.seek_busy = seek.is_busy();

        if seek.done {
            ctx.gm.probe_position = axis.position;
            log::info!("Ball detected at {:.3} mm", axis.position);
        }
        if seek.error {
            ctx.gm.error_code = seek.state.error_code;
            log::error!("Seek error {}: {}",
                seek.state.error_code,
                seek.state.error_message);
        }

        // Abort on E-stop
        if ctx.gm.estop {
            seek.abort(axis, &mut view);
        }
    }
}

Import Banner device helpers from autocore_std::banner::wls15.

Controls a Banner WLS15P multi-color light strip via IO-Link. Each output field corresponds to a PDO byte that should be linked to the device’s IO-Link process data. Use the preset methods for common animations, or set the fields directly for full control.

use autocore_std::banner::wls15::{Wls15RunMode, Color, ColorIntensity, Speed};

let mut light = Wls15RunMode::new();

// Solid green
light.steady(Color::Green, ColorIntensity::High);

// Red alert — scrolls out from center
light.alert(Color::Red, ColorIntensity::High, Speed::Medium);

// Knight Rider scanner effect
light.knight_rider(Color::Red);

// Breathing pulse
light.pulse(Color::Blue, ColorIntensity::High, Speed::Slow);

// Rainbow spectrum
light.spectrum(Speed::Fast);

// Turn off
light.off();

Preset methods:

MethodSignatureDescription
new() -> SelfCreate with all outputs at zero (off)
off(&mut self)Turn the light off
steady(&mut self, color, intensity)Solid single color
flash(&mut self, color, intensity, speed)Single color flashing
alert(&mut self, color, intensity, speed)Center-scroll alert pattern
knight_rider(&mut self, color)Bouncing scanner with tail
pulse(&mut self, color, intensity, speed)Smooth breathing effect
spectrum(&mut self, speed)Rainbow sweep across the strip

PDO output fields (all u8):

FieldTypeDescription
animationu8Animation mode (see Animation enum)
color1u8Primary color (see Color enum)
color1_intensityu8Primary color intensity (see ColorIntensity enum)
color2u8Secondary color
color2_intensityu8Secondary color intensity
speedu8Animation speed (see Speed enum)
pulse_patternu8Pulse pattern (see PulsePattern enum)
scroll_bounce_styleu8Scroll/bounce style (see ScrollStyle enum)
percent_width_color1u8Color 1 width percentage (0-100)
directionu8Direction: 0=Up, 1=Down

Enums:

All enums are #[repr(u8)] and map directly to hardware PDO values.

EnumValues
AnimationOff(0), Steady(1), Flash(2), TwoColorFlash(3), TwoColorShift(4), EndsSteady(5), EndsFlash(6), Scroll(7), CenterScroll(8), Bounce(9), CenterBounce(10), IntensitySweep(11), TwoColorSweep(12), Spectrum(13), SingleEndSteady(14), SingleEndFlash(15)
ColorGreen(0), Red(1), Orange(2), Amber(3), Yellow(4), LimeGreen(5), SpringGreen(6), Cyan(7), SkyBlue(8), Blue(9), Violet(10), Magenta(11), Rose(12), DaylightWhite(13), Custom1(14), Custom2(15), IncandescentWhite(16), WarmWhite(17), FluorescentWhite(18), NeutralWhite(19), CoolWhite(20)
ColorIntensityHigh(0), Low(1), Medium(2), Off(3), Custom(4)
SpeedMedium(0), Fast(1), Slow(2), CustomFlashRate(3)
PulsePatternNormal(0), Strobe(1), ThreePulse(2), Sos(3), Random(4)
ScrollStyleSolid(0), Tail(1), Ripple(2)
DirectionUp(0), Down(1)

Complete example — machine status indicator:

use autocore_std::{ControlProgram, TickContext};
use autocore_std::banner::wls15::{Wls15RunMode, Color, ColorIntensity, Speed};
use crate::gm::GlobalMemory;

pub struct StatusLight {
    light: Wls15RunMode,
}

impl StatusLight {
    pub fn new() -> Self {
        Self { light: Wls15RunMode::new() }
    }
}

impl ControlProgram for StatusLight {
    type Memory = GlobalMemory;

    fn process_tick(&mut self, ctx: &mut TickContext<Self::Memory>) {
        if ctx.gm.fault_active {
            self.light.alert(Color::Red, ColorIntensity::High, Speed::Fast);
        } else if ctx.gm.cycle_running {
            self.light.steady(Color::Green, ColorIntensity::High);
        } else if ctx.gm.waiting_for_part {
            self.light.pulse(Color::Yellow, ColorIntensity::High, Speed::Slow);
        } else {
            self.light.off();
        }

        // Write PDO outputs to global memory
        ctx.gm.wls15_animation = self.light.animation;
        ctx.gm.wls15_color1 = self.light.color1;
        ctx.gm.wls15_color1_intensity = self.light.color1_intensity;
        ctx.gm.wls15_color2 = self.light.color2;
        ctx.gm.wls15_color2_intensity = self.light.color2_intensity;
        ctx.gm.wls15_speed = self.light.speed;
        ctx.gm.wls15_pulse_pattern = self.light.pulse_pattern;
        ctx.gm.wls15_scroll_bounce_style = self.light.scroll_bounce_style;
        ctx.gm.wls15_percent_width = self.light.percent_width_color1;
        ctx.gm.wls15_direction = self.light.direction;
    }
}

Wls15Digital — WLS15P Two-Wire Digital Control

Controls a Banner WLS15P using two digital outputs (Q1, Q2) for simple color selection with optional blinking. No IO-Link required — connect two wires directly to the light strip inputs.

use autocore_std::banner::wls15::Wls15Digital;
use std::time::Duration;

let mut light = Wls15Digital::new();

// Set colors
light.green();    // Q1=false, Q2=true
light.red();      // Q1=true,  Q2=false
light.blue();     // Q1=true,  Q2=true
light.off();      // Q1=false, Q2=false

// Enable blinking at 500ms interval
light.blink_on(Duration::from_millis(500));
light.call(); // must call every scan cycle

// Disable blinking
light.blink_off();

Color mapping:

Q1Q2Color
falsefalseOff
truefalseRed
falsetrueGreen
truetrueBlue

Methods:

MethodSignatureDescription
new() -> SelfCreate with outputs off, no blink
off(&mut self)Set color to off
red(&mut self)Set color to red
green(&mut self)Set color to green
blue(&mut self)Set color to blue
blink_on(&mut self, interval: Duration)Enable blinking at the specified interval
blink_off(&mut self)Disable blinking — outputs follow color directly
call(&mut self)Update outputs — call every scan cycle

Output fields:

FieldTypeDescription
q1boolDigital output 1 — connect to light strip channel 1
q2boolDigital output 2 — connect to light strip channel 2

Complete example — error blinker:

use autocore_std::{ControlProgram, TickContext};
use autocore_std::banner::wls15::Wls15Digital;
use std::time::Duration;
use crate::gm::GlobalMemory;

pub struct ErrorBlinker {
    light: Wls15Digital,
}

impl ErrorBlinker {
    pub fn new() -> Self {
        Self { light: Wls15Digital::new() }
    }
}

impl ControlProgram for ErrorBlinker {
    type Memory = GlobalMemory;

    fn process_tick(&mut self, ctx: &mut TickContext<Self::Memory>) {
        if ctx.gm.fault_active {
            self.light.red();
            self.light.blink_on(Duration::from_millis(250));
        } else if ctx.gm.cycle_running {
            self.light.green();
            self.light.blink_off();
        } else {
            self.light.off();
            self.light.blink_off();
        }

        self.light.call();

        // Write outputs to global memory (mapped to digital outputs)
        ctx.gm.light_q1 = self.light.q1;
        ctx.gm.light_q2 = self.light.q2;
    }
}

EtherCAT

Import EtherCAT helpers from autocore_std::ethercat.

SdoClient — Non-Blocking SDO Access

Provides an ergonomic, handle-based interface for runtime SDO (Service Data Object) operations over CoE (CANopen over EtherCAT). Create one per device, issue reads/writes from your control loop, and check results by handle on subsequent ticks.

Use SdoClient for runtime SDO access — reading diagnostic registers, changing operating parameters on the fly, or any CoE transfer that happens after the cyclic loop is running. For SDOs that must be applied before the cyclic loop starts (e.g. setting modes_of_operation), use the startup_sdo array in project.json instead.

use autocore_std::ethercat::{SdoClient, SdoResult};
use serde_json::json;
use std::time::Duration;

let mut sdo = SdoClient::new("ClearPath_0");

// Issue an SDO write (from process_tick):
let tid = sdo.write(ctx.client, 0x6060, 0, json!(1));

// Check result on subsequent ticks:
match sdo.result(ctx.client, tid, Duration::from_secs(3)) {
    SdoResult::Pending => { /* keep waiting */ }
    SdoResult::Ok(_) => { log::info!("SDO write confirmed"); }
    SdoResult::Err(e) => { log::error!("SDO error: {}", e); }
    SdoResult::Timeout => { log::error!("SDO timed out"); }
}

Methods:

MethodSignatureDescription
new(device: &str) -> SelfCreate a client scoped to a device (e.g. "ClearPath_0")
write(&mut self, client, index: u16, sub_index: u8, value: Value) -> u32Issue SDO write; returns transaction handle
read(&mut self, client, index: u16, sub_index: u8) -> u32Issue SDO read; returns transaction handle
result(&mut self, client, tid: u32, timeout: Duration) -> SdoResultCheck result of in-flight request
drain_stale(&mut self, client, timeout: Duration)Remove requests pending longer than timeout
pending_count(&self) -> usizeNumber of in-flight SDO requests

SdoResult variants:

VariantDescription
PendingNo response yet — check again next tick
Ok(Value)Success; contains the read value or null for writes
Err(String)Server/EtherCAT error with message (e.g. "SDO abort: 0x06090011")
TimeoutNo response within caller-specified deadline

IPC topics used internally:

OperationTopicPayload
Writeethercat.{device}.sdo_write{"index": "0x6060", "sub": 0, "value": 1}
Readethercat.{device}.sdo_read{"index": "0x6064", "sub": 0}

Complete example — runtime parameter change with state machine:

use autocore_std::{ControlProgram, TickContext};
use autocore_std::ethercat::{SdoClient, SdoResult};
use autocore_std::fb::StateMachine;
use serde_json::json;
use std::time::Duration;
use crate::gm::GlobalMemory;

pub struct ConfigWriter {
    sm: StateMachine,
    sdo: SdoClient,
    write_tid: Option<u32>,
}

impl ConfigWriter {
    pub fn new() -> Self {
        Self {
            sm: StateMachine::new(),
            sdo: SdoClient::new("ClearPath_0"),
            write_tid: None,
        }
    }
}

impl ControlProgram for ConfigWriter {
    type Memory = GlobalMemory;

    fn process_tick(&mut self, ctx: &mut TickContext<Self::Memory>) {
        self.sm.call();
        match self.sm.index {
            10 => {
                // Send SDO write: set modes_of_operation to Profile Position (1)
                self.write_tid = Some(
                    self.sdo.write(ctx.client, 0x6060, 0, json!(1))
                );
                self.sm.index = 20;
            }
            20 => {
                // Wait for response
                let tid = self.write_tid.unwrap();
                match self.sdo.result(ctx.client, tid, Duration::from_secs(3)) {
                    SdoResult::Pending => {}
                    SdoResult::Ok(_) => {
                        log::info!("modes_of_operation set to PP");
                        self.sm.index = 30;
                    }
                    SdoResult::Err(e) => {
                        log::error!("SDO write failed: {}", e);
                        self.sm.set_error(1, "SDO write failed");
                    }
                    SdoResult::Timeout => {
                        log::error!("SDO write timed out");
                        self.sm.set_error(2, "SDO timeout");
                    }
                }
            }
            30 => {
                // Done — continue with normal operation
            }
            _ => {}
        }
    }
}

TeknicPpView — Teknic ClearPath Servo Drive Interface

A borrowed view into GlobalMemory PDO fields for Teknic ClearPath servo drives in Profile Position (PP) or Homing mode. Provides typed access to control/status words, CiA 402 state machine commands, motion control, and homing operations.

When you configure an EtherCAT slave with "axis": "pp" in project.json, the code generator creates a <Name>PpStubs struct that holds CiA 402 fields not in the PDO mapping (like modes_of_operation). Create a view using stubs.view(&mut ctx.gm):

use crate::gm::{GlobalMemory, ClearPath0PpStubs};

// In your program struct:
stubs: ClearPath0PpStubs,

// In process_tick:
let mut view = stubs.view(&mut ctx.gm);

// CiA 402 state machine
view.cmd_shutdown();
view.cmd_switch_on();
view.cmd_enable_operation();

// Profile Position move
view.ensure_pp_mode();
view.set_target(10000, 5000, 1000, 1000); // pos, vel, accel, decel
view.trigger_move();

// Check status
if view.target_reached() {
    log::info!("Move complete, position: {}", view.position());
}

CiA 402 state machine methods:

MethodDescription
state() -> Cia402StateCurrent CiA 402 state
cmd_shutdown()Transition to Ready to Switch On
cmd_switch_on()Transition to Switched On
cmd_enable_operation()Transition to Operation Enabled
cmd_disable_operation()Back to Switched On
cmd_disable_voltage()Back to Switch On Disabled
cmd_quick_stop()Quick Stop
cmd_fault_reset()Rising edge on fault reset bit
cmd_clear_fault_reset()Clear fault reset bit

Profile Position methods:

MethodDescription
ensure_pp_mode()Set mode to Profile Position (mode 1)
set_target(position, velocity, accel, decel)Configure move parameters
trigger_move()Assert New Set-Point (begins motion)
ack_set_point()Clear New Set-Point handshake
set_halt(bool)Decelerate to stop
set_relative(bool)Relative vs. absolute move

Status queries:

MethodReturnsDescription
target_reached()boolTarget position reached
set_point_acknowledged()boolDrive acknowledged the set-point
in_range()boolPosition within in-range window
has_homed()boolHoming completed (persistent)
at_velocity()boolAt target velocity
following_error()boolPosition tracking error exceeded limit
internal_limit()boolHardware/software limit active
is_faulted()boolDrive in Fault state
position()i32Actual position in encoder counts
velocity()i32Actual velocity in counts/s
torque()i16Actual torque in per-mille of rated

Homing methods:

MethodDescription
ensure_homing_mode()Set mode to Homing (mode 6)
trigger_homing()Start homing procedure
clear_homing_start()Clear homing start bit
homing_progress() -> HomingProgressDecode homing progress from status word

HomingProgress variants:

VariantDescription
IdleHoming not started or interrupted
InProgressMotor actively searching
AttainedReference found, offset move in progress
CompleteHoming completed, position referenced
ErrorHoming error occurred

AxisView trait: TeknicPpView implements AxisView, making it compatible with all motion function blocks (Axis, SeekProbe, etc.).

Homing with Axis:

The Axis struct provides a high-level home() method that handles the full homing sequence: SDO writes for method/speed/acceleration, mode switching, triggering, and home offset capture. This is the recommended approach.

There are two categories of homing method (see HomingMethod enum):

  • Integrated methods (IntegratedLimitSwitchNeg, HardStopPos, CurrentPosition, etc.) delegate to the drive’s built-in CiA 402 homing mode. The Axis writes SDO 0x6098 (method), 0x6099 (speeds), 0x609A (acceleration), then triggers the drive’s internal homing.
  • Software methods (LimitSwitchNegRt, HomeSensorPosRt, etc.) are implemented by the Axis itself. It puts the drive in Profile Position mode and monitors AxisView sensor signals (negative_limit_active(), positive_limit_active(), home_sensor_active()) for the triggering edge. Your view must implement these trait methods — TeknicPpView does not provide them by default, so you wire them from your GlobalMemory.

Configure homing speed and acceleration on the axis config before calling home():

// Configure homing parameters (user units — mm/s, mm/s²)
axis.config_mut().homing_speed = 25.0;
axis.config_mut().homing_accel = 100.0;

// Start homing — Axis handles the full sequence internally
axis.home(&mut view, HomingMethod::LimitSwitchNegRt);

If homing_speed and homing_accel are both 0 (default), the SDO writes for speed/accel are skipped — useful when those parameters are pre-configured via startup_sdo in project.json.

HomingMethod variants:

VariantKindDescription
HardStopPosIntegratedHard stop positive direction (torque foldback)
HardStopNegIntegratedHard stop negative direction
IntegratedLimitSwitchPosIntegratedDrive’s positive limit switch (CiA 402 code 18)
IntegratedLimitSwitchNegIntegratedDrive’s negative limit switch (CiA 402 code 17)
IntegratedHomeSensorPosRtIntegratedDrive’s home sensor, positive, rising edge
IntegratedHomeSensorNegRtIntegratedDrive’s home sensor, negative, rising edge
CurrentPositionIntegratedSet current position as home (no movement)
Integrated(i8)IntegratedArbitrary CiA 402 code (vendor-specific)
LimitSwitchPosRtSoftwareMove positive, home on rising edge of positive limit
LimitSwitchNegRtSoftwareMove negative, home on rising edge of negative limit
LimitSwitchPosFtSoftwareMove positive, home on falling edge of positive limit
LimitSwitchNegFtSoftwareMove negative, home on falling edge of negative limit
HomeSensorPosRtSoftwareMove positive, home on rising edge of home sensor
HomeSensorNegRtSoftwareMove negative, home on rising edge of home sensor
HomeSensorPosFtSoftwareMove positive, home on falling edge of home sensor
HomeSensorNegFtSoftwareMove negative, home on falling edge of home sensor

For software homing methods, your AxisView implementation must override the corresponding sensor method:

impl AxisView for MyView<'_> {
    // ... standard PDO fields ...

    fn negative_limit_active(&self) -> bool {
        *self.neg_limit_switch  // wired from GlobalMemory digital input
    }
}

Complete example — home to limit switch then move to position:

use autocore_std::{ControlProgram, TickContext};
use autocore_std::motion::{Axis, AxisConfig, HomingMethod};
use crate::gm::{GlobalMemory, Servo1PpStubs};

#[derive(Debug, Clone, Copy, PartialEq)]
enum Step {
    Home,
    WaitHomed,
    Enable,
    WaitEnabled,
    MoveToWork,
    WaitMove,
    Done,
    Reset,
    WaitReset,
}

pub struct HomeThenMove {
    axis: Axis,
    step: Step,
    stubs: Servo1PpStubs,
}

impl HomeThenMove {
    pub fn new() -> Self {
        let mut config = AxisConfig::new(12_800)
            .with_user_scale(100.0);  // 100 mm per rev
        config.homing_speed = 25.0;   // mm/s
        config.homing_accel = 100.0;  // mm/s²

        Self {
            axis: Axis::new(config, "Servo1"),
            step: Step::Home,
            stubs: Servo1PpStubs::default(),
        }
    }
}

impl ControlProgram for HomeThenMove {
    type Memory = GlobalMemory;

    fn process_tick(&mut self, ctx: &mut TickContext<Self::Memory>) {
        let Self { axis, step, stubs } = self;
        let mut view = stubs.view(&mut ctx.gm);
        axis.tick(&mut view, ctx.client);

        match *step {
            Step::Home => {
                // Home to the negative limit switch (rising edge trigger)
                axis.home(&mut view, HomingMethod::LimitSwitchNegRt);
                log::info!("Homing: seeking negative limit switch");
                *step = Step::WaitHomed;
            }
            Step::WaitHomed => {
                if !axis.is_busy {
                    if !axis.is_error {
                        log::info!("Homed at {:.1} mm", axis.position);
                        *step = Step::Enable;
                    } else {
                        log::error!("Homing failed: {}", axis.error_message);
                        *step = Step::Reset;
                    }
                }
            }
            Step::Enable => {
                axis.enable(&mut view);
                *step = Step::WaitEnabled;
            }
            Step::WaitEnabled => {
                if !axis.is_busy {
                    if axis.motor_on {
                        *step = Step::MoveToWork;
                    } else {
                        log::error!("Enable failed: {}", axis.error_message);
                        *step = Step::Reset;
                    }
                }
            }
            Step::MoveToWork => {
                // Move to 50 mm at 100 mm/s, 200 mm/s² accel and decel
                axis.move_absolute(&mut view, 50.0, 100.0, 200.0, 200.0);
                log::info!("Moving to work position");
                *step = Step::WaitMove;
            }
            Step::WaitMove => {
                if !axis.is_busy {
                    if !axis.is_error {
                        log::info!("At work position: {:.1} mm", axis.position);
                        *step = Step::Done;
                    } else {
                        log::error!("Move failed: {}", axis.error_message);
                        *step = Step::Reset;
                    }
                }
            }
            Step::Done => {
                // Ready for application logic
            }
            Step::Reset => {
                axis.reset_faults(&mut view);
                *step = Step::WaitReset;
            }
            Step::WaitReset => {
                if !axis.is_busy {
                    *step = Step::Enable;
                }
            }
        }
    }
}