DD - khalilbendhief/KHALIL GitHub Wiki
- Introduction
- Requirements
- Hardware Setup
- Software Setup
- Architecture & Design Choices
- Backend Code Walkthrough
- Frontend Code Walkthrough
- Demo
- References
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
| 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 |
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 } }
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>;
const setLed = (index: number, on: boolean) => {
updateStateOptimistically(
(current) => { current.led_on[index] = on; },
() => sendMutation({
machine_identification_unique: machineIdentification,
data: { action: "SetLed", value: { index, on } },
}),
);
};
<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>
Once the backend and frontend are running, you should see the discovered EtherCAT devices. Assign them as follows:
- Click Assign on the
750-354coupler module. - Select "Wago DIO Separate V1" as the machine type.
- Enter a non-zero serial number.
- Keep the device role as "Wago 750-354 Bus Coupler".
- Repeat for the
750-430and750-530modules using the same serial number. - Restart the backend process.
Figure — Device assignment screen

⚠️ All three devices must share the same serial number so the framework groups them into one machine instance.
In the left sidebar, WAGO DIO Separate should appear. Click it to open the control page.
Figure — Machine in sidebar

Figure — Control page

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.
- WAGO 750-430 Documentation
- WAGO 750-530 Documentation
- WAGO 750-354 EtherCAT Coupler Documentation
- WAGO 750-600 End Module Documentation
- Device Example Basics
- [Introduction](#1-introduction)
- [Requirements](#2-requirements)
- [Hardware Setup](#3-hardware-setup)
- [Software Setup](#4-software-setup)
- [Architecture & Design Choices](#5-architecture--design-choices)
- [Backend Code Walkthrough](#6-backend-code-walkthrough)
- [Frontend Code Walkthrough](#7-frontend-code-walkthrough)
- [Demo](#8-demo)
- [References](#9-references)
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
| 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

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

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

⚠️ 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 innew.rs.
See Device Example Basics for software prerequisites.
⚠️ Always disconnect power before wiring. See Device Example Basics for the safe wiring procedure.
Connect the power supply to the 852-1322 Ethernet Switch:
- Red wire (+24V) → PWR+
- Black wire (0V) → RPS-
Wire the 750-354 EtherCAT Coupler to the power supply:
- Red wire (+24V) → Coupler +V terminal
- Yellow wire (0V) → Coupler 0V terminal
Figure — 750-354 Coupler with power wiring

Slide the 750-602 EtherCAT Power Terminal onto the right side of the 750-354 Coupler until it locks.
Wiring:
- Green wire (Ground) → Terminal 4
- Red wire (+24V) from 750-602 → Terminal 2
- Black wire (0V) from 750-602 → Terminal 3
- Red wire (+24V) from 750-354 → Terminal 6
- Yellow wire (0V) from 750-354 → Terminal 7
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.
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.
Slide the 750-600 Endmodule onto the right side of the 750-530 until it locks. No wiring required.
Figure — 750-600 End Module

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

Figure — Full assembly connected to PC

Final assembly order (left to right):
[750-354 Coupler] → [750-602 Power Terminal] → [750-430 DI] → [750-530 DO] → [750-600 End]
See Device Example Basics to install and run the software, then return here for the device-specific demo steps.
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.
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.
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.
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.
The backend follows the standard 4-file machine structure used across the QiTech codebase: mod.rs, new.rs, act.rs, and api.rs.
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 eachemit_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__();
}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 do8impl 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 |
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 } }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>;const setLed = (index: number, on: boolean) => {
updateStateOptimistically(
(current) => { current.led_on[index] = on; },
() => sendMutation({
machine_identification_unique: machineIdentification,
data: { action: "SetLed", value: { index, on } },
}),
);
};<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>Once the backend and frontend are running, you should see the discovered EtherCAT devices. Assign them as follows:
- Click Assign on the
750-354coupler module. - Select "Wago DIO Separate V1" as the machine type.
- Enter a non-zero serial number.
- Keep the device role as "Wago 750-354 Bus Coupler".
- Repeat for the
750-430and750-530modules using the same serial number. - Restart the backend process.
Figure — Device assignment screen

⚠️ All three devices must share the same serial number so the framework groups them into one machine instance.
In the left sidebar, WAGO DIO Separate should appear. Click it to open the control page.
Figure — Machine in sidebar

Figure — Control page

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.
- [WAGO 750-430 Documentation](https://www.wago.com/global/i-o-systems/8-channel-digital-input/p/750-430)
- [WAGO 750-530 Documentation](https://www.wago.com/global/i-o-systems/8-channel-digital-output/p/750-530)
- [WAGO 750-354 EtherCAT Coupler Documentation](https://www.wago.com/de/io-systeme/feldbuskoppler-ethercat/p/750-354)
- [WAGO 750-600 End Module Documentation](https://www.wago.com/de/io-systeme/endmodul/p/750-600)
- Device Example Basics