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:
| Field | Type | Description |
|---|---|---|
q | bool | Output — true when timer has elapsed |
et | Duration | Elapsed 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:
| Method | Signature | Description |
|---|---|---|
new | () -> Self | Create 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) -> bool | A shutdown initiation is pending |
is_cancelling | (&self) -> bool | A shutdown cancellation is pending |
Output fields:
| Field | Type | Description |
|---|---|---|
busy | bool | true while waiting for the server to respond |
done | bool | true for one cycle after the server confirms the command |
error | bool | true for one cycle after the server returns an error |
error_message | String | Error 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:
| Method | Signature | Description |
|---|---|---|
new | (daq_fqdn: &str) -> Self | Create 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:
| Field | Type | Description |
|---|---|---|
busy | bool | true from the rising edge of execute until capture completes or an error/timeout occurs |
active | bool | true while the DAQ is armed and waiting for the hardware trigger event |
error | bool | true when an error or timeout occurs. Stays true until the next rising edge of execute. |
error_message | String | Error description (empty when no error) |
data | Option<CaptureData> | Some(...) after a successful capture, None otherwise |
CaptureData fields:
| Field | Type | Description |
|---|---|---|
channels | Vec<Vec<f64>> | Sample data per channel. channels[ch_idx][sample_idx]. |
channel_count | usize | Number of channels in the capture |
capture_length | usize | Configured post-trigger samples per channel |
pre_trigger_samples | usize | Configured pre-trigger samples per channel |
actual_samples | usize | Total samples per channel actually written (pre + post) |
sample_rate | f64 | Sample rate in Hz |
timestamp_ns | u64 | UNIX timestamp (nanoseconds) of the trigger event |
sequence | u64 | Monotonically 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:
| Method | Signature | Description |
|---|---|---|
new | () -> Self | Create 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) -> bool | true while seek operation is in progress |
Output fields:
| Field | Type | Description |
|---|---|---|
done | bool | true for one cycle when the probe is found and axis has stopped |
error | bool | true for one cycle when an error occurs |
state | StateMachine | State machine with index, error_code, error_message |
Error codes:
| Code | Meaning |
|---|---|
| 1 | Abort called while motion was active |
| 100 | Axis position is not > 0 at start |
| 120 | Axis error or control disabled during motion |
| 200 | Axis 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);
}
}
}
Banner
Import Banner device helpers from autocore_std::banner::wls15.
Wls15RunMode — WLS15P IO-Link Light Strip
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:
| Method | Signature | Description |
|---|---|---|
new | () -> Self | Create 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):
| Field | Type | Description |
|---|---|---|
animation | u8 | Animation mode (see Animation enum) |
color1 | u8 | Primary color (see Color enum) |
color1_intensity | u8 | Primary color intensity (see ColorIntensity enum) |
color2 | u8 | Secondary color |
color2_intensity | u8 | Secondary color intensity |
speed | u8 | Animation speed (see Speed enum) |
pulse_pattern | u8 | Pulse pattern (see PulsePattern enum) |
scroll_bounce_style | u8 | Scroll/bounce style (see ScrollStyle enum) |
percent_width_color1 | u8 | Color 1 width percentage (0-100) |
direction | u8 | Direction: 0=Up, 1=Down |
Enums:
All enums are #[repr(u8)] and map directly to hardware PDO values.
| Enum | Values |
|---|---|
Animation | Off(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) |
Color | Green(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) |
ColorIntensity | High(0), Low(1), Medium(2), Off(3), Custom(4) |
Speed | Medium(0), Fast(1), Slow(2), CustomFlashRate(3) |
PulsePattern | Normal(0), Strobe(1), ThreePulse(2), Sos(3), Random(4) |
ScrollStyle | Solid(0), Tail(1), Ripple(2) |
Direction | Up(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:
| Q1 | Q2 | Color |
|---|---|---|
false | false | Off |
true | false | Red |
false | true | Green |
true | true | Blue |
Methods:
| Method | Signature | Description |
|---|---|---|
new | () -> Self | Create 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:
| Field | Type | Description |
|---|---|---|
q1 | bool | Digital output 1 — connect to light strip channel 1 |
q2 | bool | Digital 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:
| Method | Signature | Description |
|---|---|---|
new | (device: &str) -> Self | Create a client scoped to a device (e.g. "ClearPath_0") |
write | (&mut self, client, index: u16, sub_index: u8, value: Value) -> u32 | Issue SDO write; returns transaction handle |
read | (&mut self, client, index: u16, sub_index: u8) -> u32 | Issue SDO read; returns transaction handle |
result | (&mut self, client, tid: u32, timeout: Duration) -> SdoResult | Check result of in-flight request |
drain_stale | (&mut self, client, timeout: Duration) | Remove requests pending longer than timeout |
pending_count | (&self) -> usize | Number of in-flight SDO requests |
SdoResult variants:
| Variant | Description |
|---|---|
Pending | No 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") |
Timeout | No response within caller-specified deadline |
IPC topics used internally:
| Operation | Topic | Payload |
|---|---|---|
| Write | ethercat.{device}.sdo_write | {"index": "0x6060", "sub": 0, "value": 1} |
| Read | ethercat.{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:
| Method | Description |
|---|---|
state() -> Cia402State | Current 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:
| Method | Description |
|---|---|
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:
| Method | Returns | Description |
|---|---|---|
target_reached() | bool | Target position reached |
set_point_acknowledged() | bool | Drive acknowledged the set-point |
in_range() | bool | Position within in-range window |
has_homed() | bool | Homing completed (persistent) |
at_velocity() | bool | At target velocity |
following_error() | bool | Position tracking error exceeded limit |
internal_limit() | bool | Hardware/software limit active |
is_faulted() | bool | Drive in Fault state |
position() | i32 | Actual position in encoder counts |
velocity() | i32 | Actual velocity in counts/s |
torque() | i16 | Actual torque in per-mille of rated |
Homing methods:
| Method | Description |
|---|---|
ensure_homing_mode() | Set mode to Homing (mode 6) |
trigger_homing() | Start homing procedure |
clear_homing_start() | Clear homing start bit |
homing_progress() -> HomingProgress | Decode homing progress from status word |
HomingProgress variants:
| Variant | Description |
|---|---|
Idle | Homing not started or interrupted |
InProgress | Motor actively searching |
Attained | Reference found, offset move in progress |
Complete | Homing completed, position referenced |
Error | Homing 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. TheAxiswrites SDO 0x6098 (method), 0x6099 (speeds), 0x609A (acceleration), then triggers the drive’s internal homing. - Software methods (
LimitSwitchNegRt,HomeSensorPosRt, etc.) are implemented by theAxisitself. It puts the drive in Profile Position mode and monitorsAxisViewsensor signals (negative_limit_active(),positive_limit_active(),home_sensor_active()) for the triggering edge. Your view must implement these trait methods —TeknicPpViewdoes 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:
| Variant | Kind | Description |
|---|---|---|
HardStopPos | Integrated | Hard stop positive direction (torque foldback) |
HardStopNeg | Integrated | Hard stop negative direction |
IntegratedLimitSwitchPos | Integrated | Drive’s positive limit switch (CiA 402 code 18) |
IntegratedLimitSwitchNeg | Integrated | Drive’s negative limit switch (CiA 402 code 17) |
IntegratedHomeSensorPosRt | Integrated | Drive’s home sensor, positive, rising edge |
IntegratedHomeSensorNegRt | Integrated | Drive’s home sensor, negative, rising edge |
CurrentPosition | Integrated | Set current position as home (no movement) |
Integrated(i8) | Integrated | Arbitrary CiA 402 code (vendor-specific) |
LimitSwitchPosRt | Software | Move positive, home on rising edge of positive limit |
LimitSwitchNegRt | Software | Move negative, home on rising edge of negative limit |
LimitSwitchPosFt | Software | Move positive, home on falling edge of positive limit |
LimitSwitchNegFt | Software | Move negative, home on falling edge of negative limit |
HomeSensorPosRt | Software | Move positive, home on rising edge of home sensor |
HomeSensorNegRt | Software | Move negative, home on rising edge of home sensor |
HomeSensorPosFt | Software | Move positive, home on falling edge of home sensor |
HomeSensorNegFt | Software | Move 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;
}
}
}
}
}