pp - khalilbendhief/KHALIL GitHub Wiki
A complete guide to building a machine with separate digital input (750β430) and digital output (750β530) WAGO EtherCAT terminals using the QiTech Control framework.
This guide demonstrates how to set up a WAGO EtherCAT I/O system using separate digital input and output terminals the 750β430 (8βchannel DI) and 750β530 (8βchannel DO) controlled through the QiTech machine framework.
| Topic | Description |
|---|---|
| Hardware wiring | How to wire the coupler, power terminal, DI, and DO modules |
| Backend (Rust) | Machine struct, EtherCAT device initialization, and state management |
| Frontend (React/TS) | Control page UI, namespace store, and optimistic state updates |
| Dashboard | Assigning devices and testing I/O through the QiTech Control UI |
| Component | Model | Purpose |
|---|---|---|
| EtherCAT Coupler | WAGO 750β354 | Fieldbus coupler β bridges Ethernet to the EtherCAT bus |
| Power Terminal | WAGO 750β602 | Supplies fieldβside power to the I/O modules |
| Digital Input | WAGO 750β430 | 8βchannel digital input, 24 V DC, 3.0 ms filter |
| Digital Output | WAGO 750β530 | 8βchannel digital output, 24 V DC, 0.5 A per channel |
| End Module | WAGO 750β600 | Bus terminator β required at the end of every WAGO I/O node |
| Power Supply | 24 V DC / 6 A ( AC/DC adapter ) | System power |
| Cabling | Standard Ethernet (Cat 5e+), assorted 24 V wiring | Network + power |
Figure β 24 V AC/DC Adapter

Figure β 750β430 Digital Input Module (rear label)

Figure β 750β530 Digital Output Module (rear label)

Figure β 750β600 End Module (rear label)

See Device Example Basics for software prerequisites (Rust toolchain, Node.js, EtherCAT master setup).
ββββββββββββββββ Ethernet ββββββββββββββββ EtherCAT bus
β Linux PC ββββββββββββββββββΊβ 750-354 βββββββββββββββββββββ
β β β Coupler β β
β Backend β ββββββββ¬ββββββββ β
β (Rust) β β β
β β ββββββββ΄ββββββββ βββββββββ΄ββββββββ
β Frontend β β 750-602 β β 750-600 β
β (React) β β Power β β End Module β
ββββββββββββββββ ββββββββ¬ββββββββ βββββββββββββββββ
β
βββββββββ΄βββββββββ
β β
ββββββββ΄βββββββ ββββββββ΄βββββββ
β 750-430 β β 750-530 β
β 8Γ DI β β 8Γ DO β
βββββββββββββββ βββββββββββββββ
Data flow:
- The backend communicates with the 750β354 coupler over EtherCAT.
- The coupler discovers its attached modules (750β430 + 750β530) during initialization.
- Input states are read from the 750β430 at ~30 Hz and emitted to the frontend via Socket.IO.
- Output commands from the frontend are sent as mutations to the backend, which writes to the 750β530.
β οΈ Safety Warning β Always disconnect power before wiring. Doubleβcheck polarity before energizing. See Device Example Basics β Safety for the full safeβwiring procedure.
Connect the 24 V power supply to the coupler's power input terminals:
| Wire | Terminal | Description |
|---|---|---|
| Red (+24 V) | +24 V | Syste |
| m power positive | ||
| Black (0 V) | 0 V | System power ground |
Figure β 750β354 Coupler with power wiring

Slide the 750β602 onto the right side of the 750β354 coupler until it clicks and locks.
Wire the power terminal to distribute fieldβside power:
| Wire | Terminal | Description |
|---|---|---|
| Green (Ground) | Terminal 4 | Protective earth |
| Red (+24 V) from PSU | Terminal 2 | Fieldβside power positive |
| Black (0 V) from PSU | Terminal 3 | Fieldβside power ground |
| Red (+24 V) from coupler | Terminal 6 | System power bridge positive |
| Yellow (0 V) from coupler | Terminal 7 | System power bridge ground |
Figure β 750β354 Coupler + 750β602 Power Terminal wiring

Slide the 750β430 onto the right side of the 750β602 until it locks.
Important: The 750β430 must be in slot 0 (first module after the power terminal). The backend initialization code expects this order.
Slide the 750β530 onto the right side of the 750β430 until it locks.
Important: The 750β530 must be in slot 1 (second module). The backend validates this during startup.
Slide the 750β600 onto the right side of the 750β530 until it locks. No wiring is needed for the end module.
To verify the setup, wire any digital output channel on the 750β530 to any digital input channel on the 750β430. For example:
750-530 DO1 βββββββΊ 750-430 DI1
This lets you toggle an output in the dashboard and immediately see the corresponding input change state.
Figure β Fully assembled I/O node (top view)

- Connect the 24 V power supply to mains.
- Run an Ethernet cable from your Linux PC to the 750β354 coupler (X1 IN port).
- Verify the coupler LEDs:
- RUN β solid green = running
- I/O β solid green = I/O data exchanging
- ERR β off = no errors
Figure β Complete setup with Ethernet connected

See Device Example Basics to install and run the QiTech Control software, then return here for the deviceβspecific code and demo.
# Clone the repo (if you haven't already)
git clone https://github.com/qitechgmbh/control.git
cd control
# Build and run the backend
cargo build --release
sudo ./target/release/control
# In a separate terminal, start the frontend
cd frontend
npm install
npm run devThis section explains every file in the wago_dio_separate machine module. The code is organized into four Rust files (backend) and four TypeScript/React files (frontend).
This is the main module file. It defines the WagoDioSeparate struct and its helper methods.
use std::time::Instant;
use control_core::socketio::namespace::NamespaceCacheingLogic;
use ethercat_hal::io::{
digital_input::DigitalInput,
digital_output::DigitalOutput,
};
use smol::channel::{Receiver, Sender};
use self::api::{StateEvent, WagoDioSeparateEvents, WagoDioSeparateNamespace};
use crate::{
AsyncThreadMessage, Machine, MachineMessage,
VENDOR_QITECH, MACHINE_WAGO_DIO_SEPARATE_V1,
machine_identification::{MachineIdentification, MachineIdentificationUnique},
};
pub mod act;
pub mod api;
pub mod new;
#[derive(Debug)]
pub struct WagoDioSeparate {
pub api_receiver: Receiver<MachineMessage>,
pub api_sender: Sender<MachineMessage>,
pub machine_identification_unique: MachineIdentificationUnique,
pub main_sender: Option<Sender<AsyncThreadMessage>>,
pub namespace: WagoDioSeparateNamespace,
pub last_state_emit: Instant,
// I/O state
pub inputs: [bool; 8], // current DI readings (750-430)
pub led_on: [bool; 8], // current DO states (750-530)
// Hardware handles
pub digital_input: [DigitalInput; 8], // bound to 750-430 ports
pub digital_output: [DigitalOutput; 8], // bound to 750-530 ports
}Key design decisions:
-
inputsandled_onare fixedβsize[bool; 8]arrays β matching the 8 channels on each module. -
digital_input/digital_outputare hardware handles fromethercat_halthat abstract the raw EtherCAT PDO mapping. - State is emitted at 30 Hz (see
act.rs) to provide smooth UI updates without overloading the Socket.IO connection.
Helper methods:
impl WagoDioSeparate {
/// Read all 8 digital inputs and emit the full state to the frontend.
pub fn emit_state(&mut self) {
for (i, di) in self.digital_input.iter().enumerate() {
self.inputs[i] = match di.get_value() {
Ok(v) => v,
Err(_) => false,
};
}
let event = self.get_state().build();
self.namespace.emit(WagoDioSeparateEvents::State(event));
}
/// Set a single digital output by index.
pub fn set_led(&mut self, index: usize, on: bool) {
if index < self.led_on.len() {
self.led_on[index] = on;
self.digital_output[index].set(on);
self.emit_state();
}
}
/// Set all 8 digital outputs at once.
pub fn set_all_leds(&mut self, on: bool) {
self.led_on = [on; 8];
for dout in self.digital_output.iter() {
dout.set(on);
}
self.emit_state();
}
}This file handles EtherCAT device discovery and module initialization. It is the most critical file β if the hardware order is wrong, initialization will fail with a clear error message.
impl MachineNewTrait for WagoDioSeparate {
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
// Validate device group
let device_identification = params.device_group.iter()
.map(|d| d.clone()).collect::<Vec<_>>();
validate_same_machine_identification_unique(&device_identification)?;
validate_no_role_duplicates(&device_identification)?;
let hardware = match ¶ms.hardware {
MachineNewHardware::Ethercat(x) => x,
_ => return Err(anyhow::anyhow!(
"MachineNewHardware is not Ethercat"
)),
};
block_on(async {
// 1. Get the 750-354 coupler
let wago_750_354 = get_ethercat_device::<Wago750_354>(
hardware, params, 0,
[WAGO_750_354_IDENTITY_A].to_vec(),
).await?;
// 2. Initialize all attached modules
let modules = Wago750_354::initialize_modules(
wago_750_354.1
).await?;
let mut coupler = wago_750_354.0.write().await;
for module in modules {
coupler.set_module(module);
}
coupler.init_slot_modules(wago_750_354.1);
// 3. Get the 750-430 (slot 0 = first module)
let dev_input = coupler.slot_devices.get(0)
.ok_or_else(|| anyhow::anyhow!(
"Expected 750-430 in slot 0"
))?.clone()
.ok_or_else(|| anyhow::anyhow!(
"Slot 0 is empty"
))?;
let wago750_430: Arc<RwLock<Wago750_430>> =
downcast_device::<Wago750_430>(dev_input).await?;
// 4. Get the 750-530 (slot 1 = second module)
let dev_output = coupler.slot_devices.get(1)
.ok_or_else(|| anyhow::anyhow!(
"Expected 750-530 in slot 1"
))?.clone()
.ok_or_else(|| anyhow::anyhow!(
"Slot 1 is empty"
))?;
let wago750_530: Arc<RwLock<Wago750_530>> =
downcast_device::<Wago750_530>(dev_output).await?;
// 5. Create digital I/O handles for all 8 ports
let di1 = DigitalInput::new(wago750_430.clone(), Wago750_430Port::Port1);
let di2 = DigitalInput::new(wago750_430.clone(), Wago750_430Port::Port2);
// ... (di3 through di8)
let di8 = DigitalInput::new(wago750_430.clone(), Wago750_430Port::Port8);
let do1 = DigitalOutput::new(wago750_530.clone(), Wago750_530Port::Port1);
let do2 = DigitalOutput::new(wago750_530.clone(), Wago750_530Port::Port2);
// ... (do3 through do8)
let do8 = DigitalOutput::new(wago750_530.clone(), Wago750_530Port::Port8);
drop(coupler);
// 6. Construct the machine
let (sender, receiver) = smol::channel::unbounded();
let mut machine = Self {
api_receiver: receiver,
api_sender: sender,
machine_identification_unique: params
.get_machine_identification_unique(),
namespace: WagoDioSeparateNamespace {
namespace: params.namespace.clone(),
},
last_state_emit: Instant::now(),
inputs: [false; 8],
led_on: [false; 8],
main_sender: params.main_thread_channel.clone(),
digital_input: [di1, di2, di3, di4, di5, di6, di7, di8],
digital_output: [do1, do2, do3, do4, do5, do6, do7, do8],
};
machine.emit_state();
Ok(machine)
})
}
}Slot order matters! The 750β430 (DI) must be physically mounted in slot 0 and the 750β530 (DO) in slot 1. The coupler discovers modules leftβtoβright.
The act method is called on every tick of the machine runtime loop. It processes incoming API messages and emits state at a throttled rate.
impl MachineAct for WagoDioSeparate {
fn act(&mut self, now: Instant) {
// Process any queued API messages (mutations from the frontend)
if let Ok(msg) = self.api_receiver.try_recv() {
self.act_machine_message(msg);
}
// Emit state at ~30 Hz
if now.duration_since(self.last_state_emit)
> Duration::from_secs_f64(1.0 / 30.0)
{
self.emit_state();
self.last_state_emit = now;
}
}
fn act_machine_message(&mut self, msg: MachineMessage) {
match msg {
MachineMessage::SubscribeNamespace(namespace) => {
self.namespace.namespace = Some(namespace);
self.emit_state();
}
MachineMessage::UnsubscribeNamespace => {
self.namespace.namespace = None;
}
MachineMessage::HttpApiJsonRequest(value) => {
use crate::MachineApi;
let _res = self.api_mutate(value);
}
MachineMessage::RequestValues(sender) => {
sender.send_blocking(MachineValues {
state: serde_json::to_value(self.get_state())
.expect("Failed to serialize state"),
live_values: serde_json::Value::Null,
}).expect("Failed to send values");
sender.close();
}
}
}
}Defines the state event schema, mutation types, and Socket.IO namespace logic.
// === State Event (sent to frontend at ~30 Hz) ===
#[derive(Serialize, Debug, Clone)]
pub struct StateEvent {
pub inputs: [bool; 8], // 750-430 readings
pub led_on: [bool; 8], // 750-530 states
}
// === Mutations (received from frontend) ===
#[derive(Deserialize)]
#[serde(tag = "action", content = "value")]
pub enum Mutation {
SetLed { index: usize, on: bool },
SetAllLeds { on: bool },
}
// === API implementation ===
impl MachineApi for WagoDioSeparate {
fn api_mutate(
&mut self,
request_body: Value,
) -> Result<(), anyhow::Error> {
let mutation: Mutation = serde_json::from_value(request_body)?;
match mutation {
Mutation::SetLed { index, on } => self.set_led(index, on),
Mutation::SetAllLeds { on } => self.set_all_leds(on),
}
Ok(())
}
}Once the backend and frontend are running, open the QiTech Control dashboard. You should see the discovered EtherCAT devices:
- WAGO 750β354 (Coupler)
- WAGO 750β430 (8βCH Digital Input)
- WAGO 750β530 (8βCH Digital Output)
Figure β Discovery

Steps to assign:
- Click Assign on the 750β354 coupler.
- Select "WAGO DIO Separate V1" as the machine type.
- Enter a serial number other than 0.
- Keep the device role as "WAGO 750β354 Bus Coupler".
- Repeat for the 750β430 and 750β530 modules using the same serial number.
- Restart the backend process to apply the assignment.
Figure β Assign

After restart, click the machine in the right side bar to open it:
- Digital Inputs (750β430): Displays the current state of all 8 input channels as ON/OFF badges.
-
Digital Outputs (750β530): Provides toggle buttons for each of the 8 output channels, plus bulk "All ON" / "All OFF" controls.
| Symptom | Likely Cause | Fix |
|---|---|---|
| Coupler LEDs are off | No power to 750β354 | Check 24 V wiring to coupler terminals |
| ERR LED is red | EtherCAT communication error | Check Ethernet cable, verify EtherCAT master is running |
| I/O LED is off | No process data exchange | Restart backend, check module assignment |
| "Expected 750β430 in slot 0" error | Modules in wrong physical order | Ensure DI module is mounted directly after the power terminal |
| "Expected 750β530 in slot 1" error | Modules in wrong physical order | Ensure DO module is mounted after the DI module |
| Inputs always show OFF | No signal on DI terminals | Check loopback wiring, verify DO is toggled ON |
| Outputs don't respond | Backend not running or mutation error | Check backend logs, verify serial number matches |
| Resource | Link |
|---|---|
| WAGO 750β430 Documentation | wago.com/750-430 |
| WAGO 750β530 Documentation | wago.com/750-530 |
| WAGO 750β354 Documentation | wago.com/750-354 |
| WAGO 750β602 Documentation | wago.com/750-602 |
| WAGO 750β600 Documentation | wago.com/750-600 |
| QiTech Control Repository | github.com/qitechgmbh/control |
| Device Example Basics (Wiki) | Device-Example-Basics |
Contributing: If you find issues or want to improve this guide, please open a PR or issue in the control repo.