EtherCAT write task
Learn how to send commands to EtherCAT devices with Synnax.
This task follows the standard task lifecycle.
Before creating a write task, configure your EtherCAT devices and identify the RxPDOs (output PDOs) to write to each one. State channels carry feedback read back from the device.
All channels in a task must use devices connected to the same network interface. Create separate tasks for devices on different EtherCAT networks.
Task configuration reference
Channel configuration
Each channel writes one RxPDO entry at its CoE index and sub-index. The Driver gets the size of the entry from the device, so the data type of the channel must be the same size.
Entry output channel
Writes one RxPDO entry.
The Console lists each entry the device reports, with its address. In code, find the
address of an entry by name with ethercat.find_entry(dev, name, direction="output") in
Python or ethercat.findEntry in TypeScript.
A task from an earlier release can hold an "unresolved" channel: one that named an
entry the device does not have, or has more than once. Pick an entry for it in the
Console before the task runs.
Common servo drive RxPDO objects include control word (0x6040), target position
(0x607A), and target velocity (0x60FF).
Important rules
- Network constraint -> All channels must use devices from the same network interface.
- State rate -> Higher state rates provide faster feedback but increase network load. Typically 10-50 Hz is sufficient.
- Execution rate -> The bus cycles at least this fast. A higher rate sends new commands to the device sooner.
- Stopping -> When the task stops, its outputs go to zero, unless another running write task commanded them. See how the Driver runs a network.
How-to
When you create channels with a client library, give each command channel its own index channel. A writer that holds several commands on one index must write all of them in every frame.
Console
Python
TypeScript
Configure and run task
import synnax as sy
from synnax import ethercat
client = sy.Synnax()
# Retrieve EtherCAT device
dev = client.devices.retrieve(name="Servo Drive")
# Create command channels, each with its own index
control_cmd_time = client.channels.create(
name="control_word_cmd_time",
is_index=True,
data_type=sy.DataType.TIMESTAMP,
retrieve_if_name_exists=True,
)
control_cmd = client.channels.create(
name="control_word_cmd",
index=control_cmd_time.key,
data_type=sy.DataType.UINT16,
retrieve_if_name_exists=True,
)
target_pos_cmd_time = client.channels.create(
name="target_position_cmd_time",
is_index=True,
data_type=sy.DataType.TIMESTAMP,
retrieve_if_name_exists=True,
)
target_pos_cmd = client.channels.create(
name="target_position_cmd",
index=target_pos_cmd_time.key,
data_type=sy.DataType.INT32,
retrieve_if_name_exists=True,
)
# Create state time index
state_time = client.channels.create(
name="ec_state_time",
is_index=True,
data_type=sy.DataType.TIMESTAMP,
retrieve_if_name_exists=True,
)
# Create state channels
control_state = client.channels.create(
name="control_word_state",
index=state_time.key,
data_type=sy.DataType.UINT16,
retrieve_if_name_exists=True,
)
target_pos_state = client.channels.create(
name="target_position_state",
index=state_time.key,
data_type=sy.DataType.INT32,
retrieve_if_name_exists=True,
)
# Find the address of each entry by name
control_entry = ethercat.find_entry(dev, "Controlword", direction="output")
target_pos_entry = ethercat.find_entry(dev, "Target position", direction="output")
task = ethercat.WriteTask(
name="EtherCAT Write Task",
execution_rate=100, # Send commands at 100 Hz
state_rate=20, # Update state at 20 Hz
channels=[
ethercat.EntryWriteChannel(
device=dev.key,
index=control_entry.index,
sub_index=control_entry.sub_index,
data_type=sy.DataType(control_entry.data_type),
cmd_channel=control_cmd.key,
state_channel=control_state.key,
),
ethercat.EntryWriteChannel(
device=dev.key,
index=target_pos_entry.index,
sub_index=target_pos_entry.sub_index,
data_type=sy.DataType(target_pos_entry.data_type),
cmd_channel=target_pos_cmd.key,
state_channel=target_pos_state.key,
),
],
)
client.tasks.configure(task)
# Start task and write commands
with task.run():
with client.open_writer(
start=sy.TimeStamp.now(),
channels=["control_word_cmd", "target_position_cmd"],
) as writer:
# Enable drive (control word 0x000F is common for CiA 402)
writer.write({
"control_word_cmd": 0x000F,
"target_position_cmd": 10000,
})
# Read back states
with client.open_streamer(["control_word_state", "target_position_state"]) as streamer:
frame = streamer.read()
print(f"Control State: {frame['control_word_state'][-1]}")
print(f"Target Position State: {frame['target_position_state'][-1]}") Edit task configuration
import synnax as sy
from synnax import ethercat
client = sy.Synnax()
# Retrieve existing task and wrap with typed class
task = client.tasks.retrieve(name="EtherCAT Write Task")
task = ethercat.WriteTask(internal=task)
# Update task-level settings
task.config.execution_rate = 200 # Increase to 200 Hz
task.config.state_rate = 50 # Increase state rate
# Apply changes
client.tasks.configure(task) Configure and run task
import { Synnax, TimeStamp } from "@synnaxlabs/client";
const client = new Synnax();
// Retrieve EtherCAT device
const [dev] = await client.devices.retrieve({ names: ["Servo Drive"] });
// Create command channels, each with its own index
const controlCmdTime = await client.channels.create(
{
name: "control_word_cmd_time",
isIndex: true,
dataType: "timestamp",
},
{ retrieveIfNameExists: true },
);
const controlCmd = await client.channels.create(
{
name: "control_word_cmd",
index: controlCmdTime.key,
dataType: "uint16",
},
{ retrieveIfNameExists: true },
);
const targetPosCmdTime = await client.channels.create(
{
name: "target_position_cmd_time",
isIndex: true,
dataType: "timestamp",
},
{ retrieveIfNameExists: true },
);
const targetPosCmd = await client.channels.create(
{
name: "target_position_cmd",
index: targetPosCmdTime.key,
dataType: "int32",
},
{ retrieveIfNameExists: true },
);
// Create state time index
const stateTime = await client.channels.create(
{
name: "ec_state_time",
isIndex: true,
dataType: "timestamp",
},
{ retrieveIfNameExists: true },
);
// Create state channels
const controlState = await client.channels.create(
{
name: "control_word_state",
index: stateTime.key,
dataType: "uint16",
},
{ retrieveIfNameExists: true },
);
const targetPosState = await client.channels.create(
{
name: "target_position_state",
index: stateTime.key,
dataType: "int32",
},
{ retrieveIfNameExists: true },
);
// Create and configure the task with the CoE address of each entry
const task = await client.tasks.create({
name: "EtherCAT Write Task",
rack: dev.rack,
type: "ethercat_write",
config: {
executionRate: 100, // Send commands at 100 Hz
stateRate: 20, // Update state at 20 Hz
channels: [
{
type: "entry",
device: dev.key,
index: 0x6040, // Controlword
subIndex: 0,
dataType: "uint16",
cmdChannel: controlCmd.key,
stateChannel: controlState.key,
},
{
type: "entry",
device: dev.key,
index: 0x607a, // Target position
subIndex: 0,
dataType: "int32",
cmdChannel: targetPosCmd.key,
stateChannel: targetPosState.key,
},
],
},
});
// Start task
await task.start();
// Write commands
const writer = await client.openWriter({
start: TimeStamp.now(),
channels: ["control_word_cmd", "target_position_cmd"],
});
// Enable drive (control word 0x000F is common for CiA 402)
await writer.write({
control_word_cmd: 0x000f,
target_position_cmd: 10000,
});
// Read back states
const streamer = await client.openStreamer([
"control_word_state",
"target_position_state",
]);
const frame = await streamer.read();
console.log(`Control State: ${frame.get("control_word_state").at(-1)}`);
console.log(`Target Position State: ${frame.get("target_position_state").at(-1)}`);
// Stop task
await task.stop();
await writer.close();
await streamer.close(); Edit task configuration
import { ethercat, Synnax } from "@synnaxlabs/client";
const client = new Synnax();
// Retrieve existing task
const task = await client.tasks.retrieve({ name: "EtherCAT Write Task" });
// Parse current configuration
const config = ethercat.writeConfigZ.parse(task.config);
// Update task-level settings
config.executionRate = 200; // Increase to 200 Hz
config.stateRate = 50; // Increase state rate
// Apply changes
await client.tasks.create({
key: task.key,
rack: task.rack,
name: task.name,
type: task.type,
config,
});