pp - khalilbendhief/KHALIL GitHub Wiki

WAGO EtherCAT Digital I/O Separate Terminals Setup

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.


Table of Contents

  1. Introduction

  2. Requirements

  3. Architecture Overview

  4. Hardware Setup

  5. Software Setup

  6. Code Walkthrough

  7. Dashboard & Demo

  8. Troubleshooting

  9. References


1. Introduction

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.

What You Will Learn

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

2. Requirements

Hardware

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

Adapter

Figure β€” 750‑430 Digital Input Module (rear label)

DigitalIn

Figure β€” 750‑530 Digital Output Module (rear label)

DigitalOut

Figure β€” 750‑600 End Module (rear label)

Endmodule

Software

See Device Example Basics for software prerequisites (Rust toolchain, Node.js, EtherCAT master setup).


3. Architecture Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    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:

  1. The backend communicates with the 750‑354 coupler over EtherCAT.
  2. The coupler discovers its attached modules (750‑430 + 750‑530) during initialization.
  3. Input states are read from the 750‑430 at ~30 Hz and emitted to the frontend via Socket.IO.
  4. Output commands from the frontend are sent as mutations to the backend, which writes to the 750‑530.

4. Hardware Setup

⚠️ Safety Warning β€” Always disconnect power before wiring. Double‑check polarity before energizing. See Device Example Basics β€” Safety for the full safe‑wiring procedure.

4.1 Wiring the Coupler (750‑354)

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 coupler

4.2 Integrating the Power Terminal (750‑602)

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 2

4.3 Mounting the I/O Modules

Step 1 β€” Attach the 750‑430 (Digital Input)

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.

Step 2 β€” Attach the 750‑530 (Digital Output)

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.

Step 3 β€” Attach the 750‑600 (End Module)

Slide the 750‑600 onto the right side of the 750‑530 until it locks. No wiring is needed for the end module.

4.4 Test Wiring (Output β†’ Input Loopback)

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) Machine

4.5 Ethernet & Power‑On

  1. Connect the 24 V power supply to mains.
  2. Run an Ethernet cable from your Linux PC to the 750‑354 coupler (X1 IN port).
  3. 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

wholething


5. Software Setup

See Device Example Basics to install and run the QiTech Control software, then return here for the device‑specific code and demo.

Quick Start

# 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 dev

6. Code Walkthrough

This 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).

6.1 Backend (Rust)

mod.rs β€” Machine Struct & Core Logic

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:

  • inputs and led_on are fixed‑size [bool; 8] arrays β€” matching the 8 channels on each module.
  • digital_input / digital_output are hardware handles from ethercat_hal that 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();
    }
}

new.rs β€” Hardware Initialization

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 &params.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.


act.rs β€” Real‑Time Loop

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();
            }
        }
    }
}

api.rs β€” API & Events

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(())
    }
}

7. Dashboard & Demo

7.1 Assigning Devices

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 Subdevices

Steps to assign:

  1. Click Assign on the 750‑354 coupler.
  2. Select "WAGO DIO Separate V1" as the machine type.
  3. Enter a serial number other than 0.
  4. Keep the device role as "WAGO 750‑354 Bus Coupler".
  5. Repeat for the 750‑430 and 750‑530 modules using the same serial number.
  6. Restart the backend process to apply the assignment.

Figure β€” Assign Assign

7.2 Testing I/O

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. WhatsApp Image 2026-03-26 at 15 52 56

8. Troubleshooting

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

9. References

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.

⚠️ **GitHub.com Fallback** ⚠️