-
Notifications
You must be signed in to change notification settings - Fork 35
Wago DIO Separate
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.
QiTech Control | GitHub | Video Demo | Open Source Framework for Industrial Control