# EtherCAT write task

> Learn how to send commands to EtherCAT devices with Synnax.

This task follows the standard [task lifecycle](https://docs.synnaxlabs.com/reference/driver/task-basics#task-lifecycle).

Before creating a write task, [configure your EtherCAT devices](https://docs.synnaxlabs.com/reference/driver/ethercat/configure-device) 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

| Parameter        | Type   | Required | Default | Description                                      |
| ---------------- | ------ | -------- | ------- | ------------------------------------------------ |
| `execution_rate` | number | No       | `1000`  | Rate (Hz) at which commands are sent to hardware |
| `state_rate`     | number | No       | `25`    | Rate (Hz) at which state channels are updated    |
| `channels`       | array  | Yes      | -       | List of output channel configurations            |

## Channel configuration modes

### Automatic mode (recommended)

In automatic mode, you select a device and RxPDO name. The system automatically resolves the CoE index, subindex, and data type from the device’s ESI information.

Automatic output channel

Writes data to an RxPDO using the PDO name for automatic configuration.

| Parameter       | Type   | Required | Default | Description                                           |
| --------------- | ------ | -------- | ------- | ----------------------------------------------------- |
| `type`          | string | Yes      | -       | Must be `"automatic"`                                 |
| `device`        | string | Yes      | -       | Device key                                            |
| `pdo`           | string | Yes      | -       | PDO name from device discovery (e.g., `control_word`) |
| `cmd_channel`   | number | Yes      | -       | Synnax command channel key                            |
| `state_channel` | number | Yes      | -       | Synnax state channel key                              |

**When to use automatic mode:**

* Device has complete ESI information
* PDO names are visible in the Console device properties
* Standard device configurations

### Manual mode

In manual mode, you specify the CoE index, subindex, and data type directly. Use this mode when the ESI file is incomplete or when working with non-standard PDO mappings.

Manual output channel

Writes data to an RxPDO using explicit CoE addressing.

| Parameter       | Type   | Required | Default  | Description                                          |
| --------------- | ------ | -------- | -------- | ---------------------------------------------------- |
| `type`          | string | Yes      | -        | Must be `"manual"`                                   |
| `device`        | string | Yes      | -        | Device key                                           |
| `index`         | number | Yes      | -        | CoE object index (e.g., `0x6040`)                    |
| `sub_index`     | number | Yes      | -        | CoE object subindex (typically `0`)                  |
| `bit_length`    | number | No       | `16`     | Data width in bits (8, 16, 32, 64)                   |
| `data_type`     | string | No       | `uint16` | Synnax data type (`uint8`, `int16`, `float32`, etc.) |
| `cmd_channel`   | number | Yes      | -        | Synnax command channel key                           |
| `state_channel` | number | Yes      | -        | Synnax state channel key                             |

**When to use manual mode:**

* ESI file is incomplete or missing PDO definitions
* Custom PDO mappings configured on the device
* Non-standard devices
* Accessing vendor-specific objects

> 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** -> Determines how frequently commands are sent to hardware. Higher rates provide more responsive control.

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

[](https://synnax.nyc3.cdn.digitaloceanspaces.com/docs/device-drivers/ethercat/write-task/ethercat-write-dark.mp4#t=0.001)

**Python**

Configure and run task (automatic mode)

```python
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,
)

# Create and configure task using automatic mode
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.AutomaticWriteChannel(
            device=dev.key,
            pdo="control_word",
            cmd_channel=control_cmd.key,
            state_channel=control_state.key,
        ),
        ethercat.AutomaticWriteChannel(
            device=dev.key,
            pdo="target_position",
            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]}")
```

Configure and run task (manual mode)

```python
import synnax as sy
from synnax import ethercat

client = sy.Synnax()

# Retrieve EtherCAT device
dev = client.devices.retrieve(name="Servo Drive")

# Create command time index
cmd_time = client.channels.create(
    name="ec_cmd_time",
    is_index=True,
    data_type=sy.DataType.TIMESTAMP,
    retrieve_if_name_exists=True,
)

# Create command channel
control_cmd = client.channels.create(
    name="control_word_cmd",
    index=cmd_time.key,
    data_type=sy.DataType.UINT16,
    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 channel
control_state = client.channels.create(
    name="control_word_state",
    index=state_time.key,
    data_type=sy.DataType.UINT16,
    retrieve_if_name_exists=True,
)

# Create task using manual mode with CoE addressing
task = ethercat.WriteTask(
    name="EtherCAT Manual Write",
    execution_rate=100,
    state_rate=20,
    channels=[
        ethercat.ManualWriteChannel(
            device=dev.key,
            index=0x6040,      # Control word (CiA 402)
            sub_index=0,
            bit_length=16,
            data_type="uint16",
            cmd_channel=control_cmd.key,
            state_channel=control_state.key,
        ),
    ],
)

client.tasks.configure(task)

# Start and write
with task.run():
    with client.open_writer(
        start=sy.TimeStamp.now(),
        channels=["control_word_cmd"],
    ) as writer:
        writer.write({"control_word_cmd": 0x000F})
```

Edit task configuration

```python
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)
```

**TypeScript**

Configure and run task (automatic mode)

```typescript
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 task using automatic mode
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: "automatic",
        device: dev.key,
        pdo: "control_word",
        cmdChannel: controlCmd.key,
        stateChannel: controlState.key,
      },
      {
        type: "automatic",
        device: dev.key,
        pdo: "target_position",
        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();
```

Configure and run task (manual mode)

```typescript
import { Synnax, TimeStamp } from "@synnaxlabs/client";

const client = new Synnax();

// Retrieve EtherCAT device
const [dev] = await client.devices.retrieve({ names: ["Servo Drive"] });

// Create command time index
const cmdTime = await client.channels.create(
  {
    name: "ec_cmd_time",
    isIndex: true,
    dataType: "timestamp",
  },
  { retrieveIfNameExists: true },
);

// Create command channel
const controlCmd = await client.channels.create(
  {
    name: "control_word_cmd",
    index: cmdTime.key,
    dataType: "uint16",
  },
  { retrieveIfNameExists: true },
);

// Create state time index
const stateTime = await client.channels.create(
  {
    name: "ec_state_time",
    isIndex: true,
    dataType: "timestamp",
  },
  { retrieveIfNameExists: true },
);

// Create state channel
const controlState = await client.channels.create(
  {
    name: "control_word_state",
    index: stateTime.key,
    dataType: "uint16",
  },
  { retrieveIfNameExists: true },
);

// Create task using manual mode with CoE addressing
const task = await client.tasks.create({
  name: "EtherCAT Manual Write",
  rack: dev.rack,
  type: "ethercat_write",
  config: {
    executionRate: 100,
    stateRate: 20,
    channels: [
      {
        type: "manual",
        device: dev.key,
        index: 0x6040, // Control word (CiA 402)
        subIndex: 0,
        bitLength: 16,
        dataType: "uint16",
        cmdChannel: controlCmd.key,
        stateChannel: controlState.key,
      },
    ],
  },
});

// Start and write
await task.start();

const writer = await client.openWriter({
  start: TimeStamp.now(),
  channels: ["control_word_cmd"],
});
await writer.write({ control_word_cmd: 0x000f });

await task.stop();
await writer.close();
```

Edit task configuration

```typescript
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,
});
```
