-
Notifications
You must be signed in to change notification settings - Fork 1
Minimal Example: Wago Digital I O
This guide walks you through reading digital inputs and driving digital outputs on Wago 750-series I/O modules using the QiTech framework over EtherCAT. By the end you will be able to monitor input channels and toggle output channels in real time from the built-in TUI.
The two modules covered are:
| Module | Type | Channels | Description |
|---|---|---|---|
| Wago 750-402 | Digital Input | 4 × 24 V DC (3 ms filter) | Reads external signals |
| Wago 750-531 | Digital Output | 4 × 24 V DC (0.5 A/ch) | Drives external loads |
The architecture is the same in both cases - only the data direction, the property type, and a handful of API calls differ. This guide covers the shared structure first and calls out the differences inline.
Neither I/O module can operate on its own. Each must be plugged into a Wago 750-354 EtherCAT fieldbus coupler that bridges the EtherCAT network to the internal Wago module bus.
| Component | Role |
|---|---|
| Wago 750-354 | EtherCAT fieldbus coupler - bridges the EtherCAT network to the internal Wago module bus |
| Wago 750-402 and/or 750-531 | Digital input / output module(s) |
| Wago 750-600 | End module - terminates the module bus (required) |
| 24 V DC power supply | Powers the coupler, the module bus, and the field-side I/O |
| Ethernet cable | Connects the coupler to your PC's EtherCAT-capable network interface |
┌──────────────────────────────────────────────────────────────────┐
│ 24 V DC PSU │
│ ┌──────┐ │
│ │ +24V ├──────┬─── Coupler power input (24 V) │
│ │ │ └─── I/O module field-side power input (24 V) │
│ │ 0V ├──────┬─── Coupler power input (0 V) │
│ │ │ └─── I/O module field-side power input (0 V) │
│ └──────┘ │
└──────────────────────────────────────────────────────────────────┘
Your PC Wago DIN-rail assembly
┌──────┐ Ethernet ┌──────────┐ ┌──────────┐ ┌─────────┐
│ NIC ├──────────────►│ 750-354 │──│ I/O mod │──│ 750-600 │
│(ETH) │ │ Coupler │ │ │ │ End Mod │
└──────┘ └──────────┘ └──────────┘ └─────────┘
You can mount multiple I/O modules between the coupler and the end module. Slot numbering starts at 0 and follows the physical order after the coupler.
Key points:
- The modules snap together on a standard DIN rail. Order matters: coupler first, then I/O modules, then the end module.
- The coupler has two power sections: system power (for the coupler's logic) and field power (for the I/O). Consult the coupler datasheet for exact terminal assignments.
- Both modules have integrated LEDs on each channel. An active input or output lights the corresponding LED automatically.
- Make sure the Ethernet cable connects to the EtherCAT port on the coupler, not the configuration/web interface port (if present).
Digital input (750-402):
24 V ──── Switch / Sensor ──── Input terminal (e.g. I1)
│
Reads HIGH when 24 V is present
Each input channel reads true when 24 V is present at the terminal and false when it is not. You can test with a simple wire jumper from the 24 V rail to an input terminal.
Digital output (750-531):
Output terminal (e.g. O1) ──── Load ──── 0 V
│
Conducts when the output is active
Each output channel sources up to 0.5 A at 24 V when active. The integrated LED lights up with no external wiring needed to verify operation.
[package]
name = "wago_digital_io"
version = "0.1.0"
edition = "2024"
[dependencies]
qitech_framework = { git = "https://github.com/qitechgmbh/qitech_framework" }
qitech_framework_tui = { git = "https://github.com/qitechgmbh/qitech_framework_tui" }
qitech_lib = { git = "https://github.com/qitechgmbh/qitech_lib" }
tokio = { version = "1", features = ["full"] }What each dependency does:
-
qitech_framework- the core framework: provides theMachinetrait and derive macro, the runtime, EtherCAT configuration, and the property mechanisms (StatePropertyfor inputs,ConfigPropertyfor outputs) for exposing values to the UI. -
qitech_framework_tui- a terminal-based user interface that lets you inspectStatePropertyvalues and changeConfigPropertyvalues at runtime. -
qitech_lib- the hardware abstraction layer. Contains typed drivers for EtherCAT devices like the Wago 750-354 coupler and the various I/O modules. -
tokio- async runtime, required because the framework's entry point is asynchronous.
Every machine needs a YAML configuration file that declares its identity and the properties the TUI can display or modify.
Digital input example:
qms_version: 1.0
revision: 1
identification:
name: wago_digital_input
vendor_id: 0
machine_id: 0
state:
in1: !boolean
in2: !boolean
in3: !boolean
in4: !booleanEntries under state map to StateProperty<bool> values - the TUI renders them as read-only indicators that update in real time.
Digital output example:
qms_version: 1.0
revision: 1
identification:
name: wago_digital_output
vendor_id: 0
machine_id: 0
config:
led1_on: !boolean
led2_on: !boolean
led3_on: !boolean
led4_on: !booleanEntries under config map to ConfigProperty<bool> values - the TUI renders them as toggles the user can change at runtime.
Both examples live in a single main.rs. The sections below present the digital input and digital output variants side by side, explaining the shared structure and noting where they diverge.
// ── Framework types ──────────────────────────────────────────────
use qitech_framework::Machine;
use qitech_framework::TuiConfiguration;
use qitech_framework::machine::ActResult;
use qitech_framework::machine::BuildContext;
use qitech_framework::machine::BuildResult;
use qitech_framework::machine::Machine;
use qitech_framework::machine::MachineBuild;
use qitech_framework::machine_build;
use qitech_framework::run_with_tui;
use qitech_framework::runtime::EtherCATConfig;
use qitech_framework::runtime::RuntimeConfiguration;
// ── Property types (pick the one you need) ───────────────────────
use qitech_framework::machine::StateProperty; // for digital inputs
use qitech_framework::machine::ConfigProperty; // for digital outputs
// ── Hardware drivers ─────────────────────────────────────────────
use qitech_lib::ethercat_hal::devices::EthercatDevice;
use qitech_lib::ethercat_hal::devices::wago_modules::wago_750_354::Wago750_354;
// Import the module driver for the hardware you are using:
use qitech_lib::ethercat_hal::devices::wago_modules::wago_750_402::Wago750_402;
use qitech_lib::ethercat_hal::devices::wago_modules::wago_750_531::Wago750_531;
// Import the I/O trait that matches your module:
use qitech_lib::ethercat_hal::io::digital_input::DigitalInputDevice;
use qitech_lib::ethercat_hal::io::digital_output::DigitalOutputDevice;
// ── Standard library ─────────────────────────────────────────────
use std::cell::RefCell;
use std::rc::Rc;
use std::time::Duration; // needed for the act() signature (inputs)The imports follow a consistent pattern regardless of the I/O direction:
-
Framework types -
Machine,MachineBuild,BuildContext, etc. - define and run the machine. -
Property type -
StateProperty(read-only, machine-driven) for inputs;ConfigProperty(read-write, user-driven) for outputs. -
Hardware drivers - the coupler (
Wago750_354) is always needed. Then import the specific module driver and its matching I/O trait. -
Standard library -
Rc<RefCell<…>>for shared ownership of the coupler;Durationif you overrideact().
#[tokio::main]
pub async fn main() {
let config_rt = RuntimeConfiguration::new()
.ethercat(EtherCATConfig::default())
.machine::<MyMachine>(); // your machine type
run_with_tui(config_rt, TuiConfiguration::default())
.await
.unwrap();
}This is identical for inputs and outputs. Three things happen:
-
RuntimeConfiguration::new()- creates a blank runtime configuration. -
.ethercat(EtherCATConfig::default())- enables the EtherCAT subsystem with default settings (it will scan the bus and discover all connected devices). -
.machine::<MyMachine>()- registers your machine type. The framework will call itsbuild()method during startup.
run_with_tui starts the framework with the terminal UI so you can monitor state properties or interact with config properties at runtime.
The struct always holds a shared reference to the coupler and an array of properties - one per I/O channel. The I/O module itself is not stored as a separate field; it stays inside the coupler's slot_devices array and is accessed on demand via a helper method.
Digital input:
#[derive(Machine)]
pub struct Wago750_402Machine {
coupler: Rc<RefCell<Wago750_354>>,
inputs: [StateProperty<bool>; 4],
}Digital output:
#[derive(Machine)]
pub struct Wago750_531Machine {
coupler: Rc<RefCell<Wago750_354>>,
leds: [ConfigProperty<bool>; 4],
}
impl Machine for Wago750_531Machine {}The key difference is the property type:
| Direction | Property type | Behaviour in the TUI |
|---|---|---|
| Input | StateProperty<bool> |
Read-only indicator, updated by the machine |
| Output | ConfigProperty<bool> |
Toggle, changed by the user |
For outputs, the empty impl Machine for Wago750_531Machine {} is required by the trait system. The default lifecycle hooks are sufficient because output changes are driven by user callbacks, not the cyclic loop.
Digital inputs override the act() lifecycle hook to poll the hardware on every cycle:
impl Machine for Wago750_402Machine {
fn act(&mut self, _dt: Duration) -> ActResult {
let coupler = self.coupler.borrow();
let wago402 = Self::wago402(&coupler);
for (port, state) in self.inputs.iter_mut().enumerate() {
state.set(wago402.get_input(port).unwrap_or(false));
}
Ok(())
}
}On each cycle:
-
self.coupler.borrow()- immutably borrows the coupler (reading inputs does not require&mut). -
Self::wago402(&coupler)- obtains an immutable reference to the 750-402 from the coupler's slot devices. -
wago402.get_input(port)- reads the current state of each channel. ReturnsSome(true)if 24 V is present,Some(false)if not, orNoneif the port is invalid. -
state.set(…)- pushes the value into theStateProperty, which updates the TUI display in real time.
The _dt: Duration parameter is the time elapsed since the last cycle. It is unused here but is available for time-dependent logic (debouncing, timing measurements, etc.).
Digital outputs do not need to override act(). Updates are driven by user interaction through the on_external_changed() callback (see section 6c below).
Both examples need a helper to reach the I/O module inside the coupler's slot device array. The only difference is mutability.
Digital input (immutable access):
impl Wago750_402Machine {
fn wago402(coupler: &Wago750_354) -> &Wago750_402 {
coupler.slot_devices[0]
.as_ref()
.unwrap()
.as_any()
.downcast_ref::<Wago750_402>()
.unwrap()
}
}Digital output (mutable access):
impl Wago750_531Machine {
fn wago531(coupler: &mut Wago750_354) -> &mut Wago750_531 {
coupler.slot_devices[0]
.as_mut()
.unwrap()
.as_any_mut()
.downcast_mut::<Wago750_531>()
.unwrap()
}
fn update_led(&mut self, port: usize) -> ActResult {
let value = self.leds[port].get();
let mut coupler = self.coupler.borrow_mut();
Self::wago531(&mut coupler).set_output(port, value);
Ok(())
}
}| Aspect | Input helper | Output helper |
|---|---|---|
| Borrow |
&Wago750_354 (immutable) |
&mut Wago750_354 (mutable) |
| Downcast | as_any().downcast_ref() |
as_any_mut().downcast_mut() |
| Returns | &Wago750_402 |
&mut Wago750_531 |
Both helpers' unwrap() calls are safe because the build() function validates at startup that the expected module type is present in the slot.
The output example adds an update_led() bridge method that reads the current config property value and writes it to the physical output. This is the method called by the on_external_changed() callback.
The build() function runs once at startup. It discovers hardware, initializes it, wires up properties, and returns the constructed machine. The structure is the same for inputs and outputs.
let (coupler, coupler_addr) = ctx.find_ethercat_device_and_addr::<Wago750_354>(0)?;
let channel = ctx.get_ethercat_interface()?;-
find_ethercat_device_and_addr::<Wago750_354>(0)- finds the first (0) Wago 750-354 coupler on the EtherCAT bus. Returns a shared reference and the device's EtherCAT address. -
get_ethercat_interface()- obtains the low-level EtherCAT communication channel.
This code is identical for both directions.
{
let mut c = coupler.borrow_mut();
let modules = Wago750_354::initialize_modules(channel.clone(), coupler_addr)
.expect("Failed to initialize coupler modules");
for module in modules {
c.set_module(module);
}
c.init_slot_modules(channel, coupler_addr);
let slot_dev = c.slot_devices[0]
.as_ref()
.expect("No device in slot 0");
assert!(
slot_dev.as_any().is::<ExpectedModuleType>(), // Wago750_402 or Wago750_531
"Slot 0 is not the expected module type"
);
}Step by step:
- Borrow the coupler mutably inside a scoped block - the borrow must be released before proceeding.
-
initialize_modules(…)- scans the coupler's internal module bus and returns detected modules (the I/O module, the 750-600 end module, etc.). -
set_module(module)- registers each module with the coupler driver. -
init_slot_modules(…)- final initialization handshake for every slot module. -
Validate the slot - confirms the expected module type is present. This makes later
unwrap()calls in the helper methods safe for the program's lifetime.
Troubleshooting: If
.as_ref()returnsNone(theexpectfires), the module was not detected. Check physical wiring and make sure the end module is in place. If the assertion fails, the module in that slot is a different type - verify the slot order on your DIN rail.
This is where inputs and outputs diverge most.
Digital input - StateProperty:
let in1 = ctx.state::<bool>("in1").build()?;
let in2 = ctx.state::<bool>("in2").build()?;
let in3 = ctx.state::<bool>("in3").build()?;
let in4 = ctx.state::<bool>("in4").build()?;State properties have no callback. Data flows from hardware → machine → TUI. The act() loop writes values into the properties; the TUI displays them as read-only indicators.
Digital output - ConfigProperty:
let led1 = ctx
.config::<bool>("led1_on")
.on_external_changed(|m: &mut Wago750_531Machine| m.update_led(0))
.build()?;
let led2 = ctx
.config::<bool>("led2_on")
.on_external_changed(|m: &mut Wago750_531Machine| m.update_led(1))
.build()?;
let led3 = ctx
.config::<bool>("led3_on")
.on_external_changed(|m: &mut Wago750_531Machine| m.update_led(2))
.build()?;
let led4 = ctx
.config::<bool>("led4_on")
.on_external_changed(|m: &mut Wago750_531Machine| m.update_led(3))
.build()?;Config properties register an on_external_changed() callback that fires when the user toggles the value in the TUI. The callback calls update_led() to push the new value to the physical hardware. Data flows from TUI → machine → hardware.
StateProperty (input) |
ConfigProperty (output) |
|
|---|---|---|
| Declared with | ctx.state::<T>(name) |
ctx.config::<T>(name) |
| YAML section | state: |
config: |
| Data flow | Hardware → machine → TUI | TUI → machine → hardware |
| Callback | None | on_external_changed() |
| TUI appearance | Read-only indicator | Interactive toggle |
// Digital input
Ok(Self {
coupler,
inputs: [in1, in2, in3, in4],
})
// Digital output
Ok(Self {
coupler,
leds: [led1, led2, led3, led4],
})The coupler reference and the property array are assembled into the struct and returned. The framework takes ownership and begins the main loop - the TUI is now live.
-
Wire everything up as shown in the wiring diagrams above.
-
Power on the 24 V supply. The coupler's status LEDs should indicate it is on the bus.
-
Identify your network interface. EtherCAT uses raw Ethernet frames, so you need the OS-level interface name (e.g.
eth0,enp3s0). The framework's EtherCAT config may auto-detect it, or you may need to set it explicitly - consult theEtherCATConfigdocs. -
Run the example (typically requires root or
CAP_NET_RAWfor raw socket access):sudo cargo run --release -p wago_digital_io
-
Use the TUI:
-
Inputs: You will see read-only indicators (
in1throughin4). Apply 24 V to an input terminal - the indicator updates in real time and the module's LED lights up. -
Outputs: You will see toggles (
led1_onthroughled4_on). Toggle any of them - the physical output switches and the module's LED lights up or turns off immediately.
-
Inputs: You will see read-only indicators (
| Aspect | Digital Input (e.g. 750-402) | Digital Output (e.g. 750-531) |
|---|---|---|
| Module driver | Wago750_402 |
Wago750_531 |
| I/O trait |
DigitalInputDevice → get_input()
|
DigitalOutputDevice → set_output()
|
| Data direction | Read values from the module | Write values to the module |
| Property type |
StateProperty<bool> (machine-driven, read-only in TUI) |
ConfigProperty<bool> (user-driven, writable from TUI) |
| YAML section | state: |
config: |
| Update mechanism |
act() lifecycle hook polls on every cycle |
on_external_changed() callback fires on user toggle |
| Borrow type | Immutable (borrow(), downcast_ref()) |
Mutable (borrow_mut(), downcast_mut()) |
| Wiring | 24 V → switch/sensor → input terminal | Output terminal → load → 0 V |