DD - khalilbendhief/KHALIL GitHub Wiki

WAGO EtherCAT Digital I/O Separate (750-430 + 750-530)

Table of Contents

  1. Introduction
  2. Requirements
  3. Hardware Setup
  4. Software Setup
  5. Architecture & Design Choices
  6. Backend Code Walkthrough
  7. Frontend Code Walkthrough
  8. Demo
  9. References

1. Introduction

The WAGO DIO Separate machine demonstrates how to combine a WAGO 750-430 (8-channel Digital Input) and a WAGO 750-530 (8-channel Digital Output) terminal on the same EtherCAT coupler, while treating inputs and outputs as fully independent channels.

Unlike a combined DIO terminal, this setup uses two physically separate modules — one dedicated to reading 8 digital inputs, and one dedicated to controlling 8 digital outputs. This mirrors real industrial scenarios where input sensors and output actuators are wired to distinct terminals.

What this example demonstrates:

  • Reading 8 digital input channels from a 750-430 module
  • Controlling 8 digital output channels on a 750-530 module
  • Using the QiTech machine framework to handle both modules under a single machine abstraction
  • Optimistic UI updates for responsive output toggling

2. Requirements

Hardware

Component Model Purpose
EtherCAT Coupler WAGO 750-354 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
Digital Output WAGO 750-530 8-channel digital output, 24 V DC
End Module WAGO 750-600 Bus terminator
Power Supply 24 V DC (AC/DC adapter) System power
Cabling Standard Ethernet, assorted 24 V wiring Network + power
Linux PC Ubuntu/Debian/NixOS Runs backend + frontend

6.4 api.rs — Events, Mutations & Namespace

State event (emitted to frontend):

#[derive(Serialize, Debug, Clone)]
pub struct StateEvent {
    pub inputs: [bool; 8],  // from 750-430
    pub led_on: [bool; 8],  // from 750-530
}

Mutations (received from frontend):

#[derive(Deserialize)]
#[serde(tag = "action", content = "value")]
pub enum Mutation {
    SetLed { index: usize, on: bool },
    SetAllLeds { on: bool },
}

The JSON payload must look like:

{ "action": "SetLed", "value": { "index": 3, "on": true } }

or:

{ "action": "SetAllLeds", "value": { "on": false } }

7. Frontend Code Walkthrough

7.1 wagoDioSeparateNamespace.ts — Socket.IO Schema & Store

export const stateEventDataSchema = z.object({
    inputs: z.array(z.boolean()).length(8),  // from 750-430
    led_on: z.array(z.boolean()).length(8),  // from 750-530
});
export type StateEvent = z.infer<typeof stateEventDataSchema>;

7.2 useWagoDioSeparate.ts — Hook with Optimistic Updates

const setLed = (index: number, on: boolean) => {
    updateStateOptimistically(
        (current) => { current.led_on[index] = on; },
        () => sendMutation({
            machine_identification_unique: machineIdentification,
            data: { action: "SetLed", value: { index, on } },
        }),
    );
};

7.3 WagoDioSeparateControlPage.tsx — UI

<ControlCard title="Digital Inputs (750-430)">
    {safeState.inputs.map((input, index) => (
        <Label key={index} label={`Input ${index + 1}`}>
            <Badge variant={input ? "outline" : "destructive"}>
                {input ? "ON" : "OFF"}
            </Badge>
        </Label>
    ))}
</ControlCard>

<ControlCard title="Digital Outputs (750-530)"> {safeState.led_on.map((on, index) => ( <Label key={index} label={Output ${index + 1}}> <Button variant={on ? "default" : "outline"} onClick={() => setLed(index, !on)}> {on ? "ON" : "OFF"} </Button> </Label> ))} <Button onClick={() => setAllLeds(true)}>All ON</Button> <Button variant="outline" onClick={() => setAllLeds(false)}>All OFF</Button> </ControlCard>


8. Demo

8.1 Assigning Devices in the Dashboard

Once the backend and frontend are running, you should see the discovered EtherCAT devices. Assign them as follows:

  1. Click Assign on the 750-354 coupler module.
  2. Select "Wago DIO Separate V1" as the machine type.
  3. Enter a non-zero serial number.
  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.

Figure — Device assignment screen

Assign

⚠️ All three devices must share the same serial number so the framework groups them into one machine instance.

8.2 Using the Control Page

In the left sidebar, WAGO DIO Separate should appear. Click it to open the control page.

Figure — Machine in sidebar

UI

Figure — Control page

Machine

You will see two cards:

Digital Inputs (750-430): 8 channels labeled Input 1–8, each showing a live ON/OFF badge updating at 30 Hz. Apply 24VDC to any input terminal to see the channel switch to ON.

Digital Outputs (750-530): 8 channels labeled Output 1–8, each with a toggle button. Use All ON / All OFF to set all channels simultaneously.


9. References

# WAGO EtherCAT Digital I/O Separate (750-430 + 750-530)

Table of Contents

  1. [Introduction](#1-introduction)
  2. [Requirements](#2-requirements)
  3. [Hardware Setup](#3-hardware-setup)
  4. [Software Setup](#4-software-setup)
  5. [Architecture & Design Choices](#5-architecture--design-choices)
  6. [Backend Code Walkthrough](#6-backend-code-walkthrough)
  7. [Frontend Code Walkthrough](#7-frontend-code-walkthrough)
  8. [Demo](#8-demo)
  9. [References](#9-references)

1. Introduction

The WAGO DIO Separate machine demonstrates how to combine a WAGO 750-430 (8-channel Digital Input) and a WAGO 750-530 (8-channel Digital Output) terminal on the same EtherCAT coupler, while treating inputs and outputs as fully independent channels.

Unlike a combined DIO terminal, this setup uses two physically separate modules — one dedicated to reading 8 digital inputs, and one dedicated to controlling 8 digital outputs. This mirrors real industrial scenarios where input sensors and output actuators are wired to distinct terminals.

What this example demonstrates:

  • Reading 8 digital input channels from a 750-430 module
  • Controlling 8 digital output channels on a 750-530 module
  • Using the QiTech machine framework to handle both modules under a single machine abstraction
  • Optimistic UI updates for responsive output toggling

2. Requirements

Hardware

Component Model Purpose
EtherCAT Coupler WAGO 750-354 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
Digital Output WAGO 750-530 8-channel digital output, 24 V DC
End Module WAGO 750-600 Bus terminator
Power Supply 24 V DC (AC/DC adapter) System power
Cabling Standard Ethernet, assorted 24 V wiring Network + power
Linux PC Ubuntu/Debian/NixOS Runs backend + frontend

Figure — 24V AC/DC Power Supply

Adapter

Figure — 750-430 Digital Input Module (8-channel)

DigitalIn

Figure — 750-530 Digital Output Module (8-channel)

DigitalOut

⚠️ Module slot order matters. The 750-430 (DI) must be in slot 0 and the 750-530 (DO) must be in slot 1 on the coupler. This is enforced in new.rs.

Software

See Device Example Basics for software prerequisites.


3. Hardware Setup

⚠️ Always disconnect power before wiring. See Device Example Basics for the safe wiring procedure.

3.1 Wiring the Ethernet Switch

Connect the power supply to the 852-1322 Ethernet Switch:

  1. Red wire (+24V) → PWR+
  2. Black wire (0V) → RPS-

3.2 Wiring the Coupler

Wire the 750-354 EtherCAT Coupler to the power supply:

  1. Red wire (+24V) → Coupler +V terminal
  2. Yellow wire (0V) → Coupler 0V terminal

Figure — 750-354 Coupler with power wiring

Coupler with power

3.3 750-602 Power Terminal

Slide the 750-602 EtherCAT Power Terminal onto the right side of the 750-354 Coupler until it locks.

Wiring:

  1. Green wire (Ground) → Terminal 4
  2. Red wire (+24V) from 750-602 → Terminal 2
  3. Black wire (0V) from 750-602 → Terminal 3
  4. Red wire (+24V) from 750-354 → Terminal 6
  5. Yellow wire (0V) from 750-354 → Terminal 7

3.4 750-430 Digital Input Terminal (Slot 0)

Slide the 750-430 onto the right side of the 750-602 until it locks. It receives power through the pin connectors — no additional power wiring is required.

To test inputs: wire a 24VDC signal to any of the 8 input terminals. The state will be reflected live in the dashboard.

3.5 750-530 Digital Output Terminal (Slot 1)

Slide the 750-530 onto the right side of the 750-430 until it locks. It also receives bus power through the pin connectors.

To test outputs: the terminal will drive 24VDC on whichever output channels are activated via the dashboard.

3.6 750-600 End Module

Slide the 750-600 Endmodule onto the right side of the 750-530 until it locks. No wiring required.

Figure — 750-600 End Module

Endmodule

3.7 Power & Ethernet

Power: Connect the 24V power supply to the outlet.

Ethernet:

  • PC → 852-1322 Ethernet Switch (LAN cable)
  • 852-1322 Ethernet Switch → 750-354 EtherCAT Coupler (LAN cable)

Figure — Ethernet connected

Ethernet

Figure — Full assembly connected to PC

wholething

Final assembly order (left to right):

[750-354 Coupler] → [750-602 Power Terminal] → [750-430 DI] → [750-530 DO] → [750-600 End]

4. Software Setup

See Device Example Basics to install and run the software, then return here for the device-specific demo steps.


5. Architecture & Design Choices

Why "Separate"?

The name wago_dio_separate reflects that inputs and outputs are handled by two separate physical modules (750-430 and 750-530), rather than a single combined DIO terminal. Each module has its own Arc<RwLock<T>> handle, and they are accessed independently in the machine struct.

State Model

The machine state is flat and simple:

pub struct StateEvent {
    pub inputs: [bool; 8],   // live readings from 750-430
    pub led_on: [bool; 8],   // current output state on 750-530
}

inputs is always read from hardware on each emit_state call. led_on is the commanded output state, updated on every SetLed or SetAllLeds mutation.

30 Hz State Emission

The act loop runs continuously and emits state at 30 Hz (every ~33ms):

if now.duration_since(self.last_state_emit) > Duration::from_secs_f64(1.0 / 30.0) {
    self.__emit_state__();
    self.last_state_emit = now;
}

This ensures input changes are reflected in the UI promptly without spamming the bus.

Optimistic UI

On the frontend, output toggles use optimistic state updates: the UI reflects the new state immediately before the server confirms, making the dashboard feel instantaneous. If the request fails, the state is rolled back automatically.


6. Backend Code Walkthrough

The backend follows the standard 4-file machine structure used across the QiTech codebase: mod.rs, new.rs, act.rs, and api.rs.

6.1 mod.rs — Machine Struct & Core Methods

mod.rs defines the WagoDioSeparate struct and its core helper methods.

#[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,
    pub inputs: [bool; 8],
    pub led_on: [bool; 8],
    pub digital_input: [DigitalInput; 8],
    pub digital_output: [DigitalOutput; 8],
}

Key fields:

  • digital_input / digital_output — fixed-size arrays of 8 channel handles each, one per physical terminal port.
  • inputs — cached input readings, refreshed on each emit_state.
  • led_on — current commanded output state.
  • last_state_emit — timestamp used for 30 Hz throttling.

Core methods:

__emit_state__ reads all 8 input channels, updates self.inputs, then broadcasts the full state over the Socket.IO namespace:

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_led__ toggles a single output channel and immediately re-emits state:

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_leds__ sets all 8 output channels 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__();
}

6.2 new.rs — Hardware Initialization

new.rs implements MachineNewTrait and is responsible for discovering, validating, and wiring up the hardware.

Step 1: Validate device group

validate_same_machine_identification_unique(&device_identification)?;
validate_no_role_duplicates(&device_identification)?;

Step 2: Acquire the EtherCAT coupler

let wago_750_354 = get_ethercat_device::<Wago750_354>(
    hardware, params, 0, [WAGO_750_354_IDENTITY_A].to_vec(),
).await?;

Step 3: Initialize slot 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);

Step 4: Downcast slot devices

// Slot 0 → 750-430 (Digital Input)
let wago750_430: Arc<RwLock<Wago750_430>> =
    downcast_device::<Wago750_430>(dev_input).await?;

// Slot 1 → 750-530 (Digital Output)
let wago750_530: Arc<RwLock<Wago750_530>> =
    downcast_device::<Wago750_530>(dev_output).await?;

Step 5: Create channel handles

let di1 = DigitalInput::new(wago750_430.clone(), Wago750_430Port::Port1);
// ... di2 through di8

let do1 = DigitalOutput::new(wago750_530.clone(), Wago750_530Port::Port1);
// ... do2 through do8

6.3 act.rs — The Main Loop

impl MachineAct for WagoDioSeparate {
    fn __act__(&mut self, now: Instant) {
        if let Ok(msg) = self.api_receiver.try_recv() {
            self.__act_machine_message__(msg);
        }
        if now.duration_since(self.last_state_emit) > Duration::from_secs_f64(1.0 / 30.0) {
            self.__emit_state__();
            self.last_state_emit = now;
        }
    }
}

Message handling:

Message Action
SubscribeNamespace Attaches a Socket.IO namespace and immediately emits current state
UnsubscribeNamespace Detaches the namespace
HttpApiJsonRequest Deserializes and dispatches a mutation (SetLed / SetAllLeds)
RequestValues Sends a snapshot of the current state over a one-shot channel

6.4 api.rs — Events, Mutations & Namespace

State event (emitted to frontend):

#[derive(Serialize, Debug, Clone)]
pub struct StateEvent {
    pub inputs: [bool; 8],  // from 750-430
    pub led_on: [bool; 8],  // from 750-530
}

Mutations (received from frontend):

#[derive(Deserialize)]
#[serde(tag = "action", content = "value")]
pub enum Mutation {
    SetLed { index: usize, on: bool },
    SetAllLeds { on: bool },
}

The JSON payload must look like:

{ "action": "SetLed", "value": { "index": 3, "on": true } }

or:

{ "action": "SetAllLeds", "value": { "on": false } }

7. Frontend Code Walkthrough

7.1 wagoDioSeparateNamespace.ts — Socket.IO Schema & Store

export const stateEventDataSchema = z.object({
    inputs: z.array(z.boolean()).length(8),  // from 750-430
    led_on: z.array(z.boolean()).length(8),  // from 750-530
});
export type StateEvent = z.infer<typeof stateEventDataSchema>;

7.2 useWagoDioSeparate.ts — Hook with Optimistic Updates

const setLed = (index: number, on: boolean) => {
    updateStateOptimistically(
        (current) => { current.led_on[index] = on; },
        () => sendMutation({
            machine_identification_unique: machineIdentification,
            data: { action: "SetLed", value: { index, on } },
        }),
    );
};

7.3 WagoDioSeparateControlPage.tsx — UI

<ControlCard title="Digital Inputs (750-430)">
    {safeState.inputs.map((input, index) => (
        <Label key={index} label={`Input ${index + 1}`}>
            <Badge variant={input ? "outline" : "destructive"}>
                {input ? "ON" : "OFF"}
            </Badge>
        </Label>
    ))}
</ControlCard>

<ControlCard title="Digital Outputs (750-530)">
    {safeState.led_on.map((on, index) => (
        <Label key={index} label={`Output ${index + 1}`}>
            <Button variant={on ? "default" : "outline"}
                onClick={() => setLed(index, !on)}>
                {on ? "ON" : "OFF"}
            </Button>
        </Label>
    ))}
    <Button onClick={() => setAllLeds(true)}>All ON</Button>
    <Button variant="outline" onClick={() => setAllLeds(false)}>All OFF</Button>
</ControlCard>

8. Demo

8.1 Assigning Devices in the Dashboard

Once the backend and frontend are running, you should see the discovered EtherCAT devices. Assign them as follows:

  1. Click Assign on the 750-354 coupler module.
  2. Select "Wago DIO Separate V1" as the machine type.
  3. Enter a non-zero serial number.
  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.

Figure — Device assignment screen

Assign

⚠️ All three devices must share the same serial number so the framework groups them into one machine instance.

8.2 Using the Control Page

In the left sidebar, WAGO DIO Separate should appear. Click it to open the control page.

Figure — Machine in sidebar

UI

Figure — Control page

Machine

You will see two cards:

Digital Inputs (750-430): 8 channels labeled Input 1–8, each showing a live ON/OFF badge updating at 30 Hz. Apply 24VDC to any input terminal to see the channel switch to ON.

Digital Outputs (750-530): 8 channels labeled Output 1–8, each with a toggle button. Use All ON / All OFF to set all channels simultaneously.


9. References

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