Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ This project interfaces with Beckhoff EtherCAT I/O terminals via the ADS protoco

- **Testing with Hardware**: **NEVER** run `fastcs-catio ioc` commands yourself. Let the user run the IOC and report any errors back to you. The IOC requires network access to real hardware that may not be available or may have specific configuration requirements.

- **Terminal Definitions**: YAML files describing Beckhoff terminal types, their symbols, and CoE objects. See [docs/explanations/terminal-definitions.md](docs/explanations/terminal-definitions.md) for:
- **Terminal Definitions**: YAML files describing Beckhoff terminal types, their symbols, and CoE objects. See [docs/explanations/terminal-yaml-definitions.md](docs/explanations/terminal-yaml-definitions.md) for:
- How to generate terminal YAML files using `catio-terminals`
- Understanding ADS symbol nodes and index groups
- The difference between XML-defined symbols and ADS runtime symbols (e.g., `WcState`)
Expand All @@ -213,15 +213,15 @@ This project interfaces with Beckhoff EtherCAT I/O terminals via the ADS protoco

- **catio-terminals**: GUI editor for terminal YAML files. Use `catio-terminals update-cache` to fetch Beckhoff XML definitions, then use `catio-terminals edit [filename]` to edit files with the GUI.

- **Terminal YAML Files Are Generated**: **NEVER manually edit** terminal YAML files in `src/catio_terminals/terminals/`. These files are generated from Beckhoff XML by the code in `src/catio_terminals/xml_parser.py` and `src/catio_terminals/xml_pdo.py`. If the YAML has incorrect values:
- **Terminal YAML Files Are Generated**: **NEVER manually edit** terminal YAML files in `src/catio_terminals/terminals/`. These files are generated from Beckhoff XML by the code in `src/catio_terminals/xml/`. If the YAML has incorrect values:
1. Fix the XML parsing code that generates the YAML
2. Regenerate the YAML using `uv run catio-terminals clean-yaml <file>` (default is src/catio_terminals/terminals/terminal_types.yaml)
3. Manual edits will be lost on next regeneration

**Special cases:**
- Index groups default to 0xF031/0xF021 for standard I/O
- Counter terminals (group_type="Measuring") use 0xF030/0xF020 instead
- Group-specific logic is in `process_pdo_entries()` in `xml_pdo.py`
- Group-specific logic is in `process_pdo_entries()` in `xml/pdo.py`

## Agent Skills

Expand Down
21 changes: 8 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,9 @@
[![PyPI](https://img.shields.io/pypi/v/fastcs-catio.svg)](https://pypi.org/project/fastcs-catio)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0)

# fastcs_catio
# fastcs-catio

Control system integration of EtherCAT I/O devices running under TwinCAT using pyads and FastCS

This is where you should write a short paragraph that describes what your module does,
how it does it, and why people should use it.
CATio provides control system integration for Beckhoff EtherCAT I/O devices running under TwinCAT. It uses the ADS protocol to communicate with TwinCAT PLCs and exposes device data as EPICS Process Variables through the FastCS framework.

Source | <https://github.com/DiamondLightSource/fastcs-catio>
:---: | :---:
Expand All @@ -17,16 +14,14 @@ Docker | `docker run ghcr.io/diamondlightsource/fastcs-catio:latest`
Documentation | <https://diamondlightsource.github.io/fastcs-catio>
Releases | <https://github.com/DiamondLightSource/fastcs-catio/releases>

This is where you should put some images or code snippets that illustrate
some relevant examples. If it is a library then you might put some
introductory code here:

Some tips about using Claude Code[CLAUDE_NOTES.md].
## Quick Start

```python
from fastcs_catio import __version__
```bash
# Install
pip install fastcs-catio

print(f"Hello fastcs_catio {__version__}")
# Run the IOC
fastcs-catio ioc --target-ip 192.168.1.100
```

<!-- README only content. Anything below this line won't be included in index.md -->
Expand Down
7 changes: 7 additions & 0 deletions docs/_static/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@
height: auto !important;
}

/* Simple diagrams - don't force full width, use natural size */
.mermaid-simple .mermaid,
.mermaid-simple .mermaid svg {
width: auto !important;
max-width: 100% !important;
}

/* Make expanded/fullscreen mermaid diagrams prioritize width with vertical scroll */
.mermaid-modal,
.mermaid-fullscreen,
Expand Down
2 changes: 0 additions & 2 deletions docs/explanations.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@ Explanations of how it works and why it works that way.
explanations/architecture-overview.md
explanations/fastcs-epics-ioc.md
explanations/ads-client.md
explanations/api-decoupling.md
explanations/nomenclature.md
explanations/terminal-yaml-definitions.md
explanations/terminal-definitions.md
explanations/composite-types.md
explanations/coe-parameters.md
explanations/dynamic-pdos.md
Expand Down
38 changes: 6 additions & 32 deletions docs/explanations/ads-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,7 @@ TwinCAT maintains a routing table of authorized clients. CATio uses UDP messages
1. **Discover the target's AMS NetId**: Query the TwinCAT system for its network identity
2. **Register this client**: Add an entry to the routing table with authentication credentials

```{literalinclude} ../../src/fastcs_catio/client.py
:language: python
:start-at: class RemoteRoute
:end-before: def _get_route_info_as_bytes
```

The route registration includes:
The `RemoteRoute` class in [client.py](../../src/fastcs_catio/client.py) handles route management with these parameters:

| Parameter | Purpose |
|-----------|---------|
Expand All @@ -72,14 +66,7 @@ Default TwinCAT credentials are typically `Administrator` / `1`. Production syst

### TCP Connection

Once routed, CATio opens a persistent TCP connection for ADS communication:

```{literalinclude} ../../src/fastcs_catio/client.py
:language: python
:pyobject: AsyncioADSClient.connected_to
```

The connection uses Python's `asyncio.open_connection()` for non-blocking I/O, returning stream reader/writer pairs for bidirectional communication.
Once routed, CATio opens a persistent TCP connection for ADS communication. The `AsyncioADSClient.connected_to()` method in [client.py](../../src/fastcs_catio/client.py) uses Python's `asyncio.open_connection()` for non-blocking I/O, returning stream reader/writer pairs for bidirectional communication.

## ADS Commands

Expand Down Expand Up @@ -142,13 +129,7 @@ ADS symbols provide named access to PLC variables, avoiding hard-coded index gro

### The AdsSymbol Structure

```{literalinclude} ../../src/fastcs_catio/devices.py
:language: python
:pyobject: AdsSymbol
:end-before: @property
```

Symbols carry type information (`dtype`) allowing CATio to correctly interpret binary data. The `group` and `offset` fields are used in ADS read/write commands.
The `AdsSymbol` dataclass in [devices.py](../../src/fastcs_catio/devices.py) represents ADS symbols. Symbols carry type information (`dtype`) allowing CATio to correctly interpret binary data. The `group` and `offset` fields are used in ADS read/write commands.

### Symbol-Based Access vs Direct Access

Expand Down Expand Up @@ -201,7 +182,7 @@ The client uses string-based dispatch to route API calls:
- `query("SYSTEM_TREE")` → calls `get_system_tree()`
- `command("DEVICE_STATE", ...)` → calls `set_device_state(...)`

This pattern, while flexible, has tradeoffs discussed in [API Decoupling Analysis](api-decoupling.md).
This pattern, while flexible, has tradeoffs discussed in [API Decoupling Analysis](decisions/0003-api-decoupling-analysis.md).

### Key API Methods

Expand Down Expand Up @@ -230,15 +211,8 @@ The client raises exceptions with meaningful messages when operations fail, allo

## Testing with MockADSServer

CATio includes a mock ADS server for testing without hardware:

```{literalinclude} ../../tests/mock_server.py
:language: python
:pyobject: MockADSServer
:end-before: async def start
```
CATio includes a mock ADS server for testing without hardware. The `MockADSServer` class in [mock_server.py](../../tests/mock_server.py) provides:

The mock server:
- Accepts TCP connections on the standard ADS port
- Parses AMS headers and dispatches to command handlers
- Returns configurable mock responses
Expand All @@ -250,4 +224,4 @@ This enables comprehensive testing of the client logic independent of real TwinC

- [Architecture Overview](architecture-overview.md) - High-level system architecture
- [FastCS EPICS IOC Implementation](fastcs-epics-ioc.md) - Details of the EPICS layer
- [API Decoupling Analysis](api-decoupling.md) - API design discussion
- [API Decoupling Analysis](decisions/0003-api-decoupling-analysis.md) - API design discussion
88 changes: 68 additions & 20 deletions docs/explanations/architecture-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,19 @@ CATio is a Python-based control system integration for EtherCAT I/O devices runn
## High-Level Architecture Diagram

```mermaid
%%{init: {'theme': 'base', 'themeVariables': { 'fontSize': '14px'}}}%%
flowchart TB
clients["EPICS Clients / Control Systems"]

subgraph fastcs["FastCS EPICS IOC Layer"]
direction LR
server["CATioServerController"] --> device["CATioDeviceController<br/>(EtherCAT Master)"] --> terminal["CATioTerminalController<br/>(EK1100, EL3xxx, etc.)"]
fastcs_files["catio_controller.py<br/>catio_hardware.py<br/>catio_attribute_io.py"]
server["CATioServerController"] --> device["CATioDeviceController<br/>(EtherCAT Master)"] --> terminal["Dynamic Terminal Controllers<br/>(generated from YAML)"]
fastcs_files["catio_controller.py<br/>catio_dynamic_controller.py<br/>catio_dynamic_symbol.py<br/>catio_dynamic_coe.py"]
end

subgraph yamldef["Terminal Definitions"]
direction LR
yaml["terminal_types.yaml"] --> dynamic["get_terminal_controller_class()"]
runtime["runtime_symbols.yaml"] --> dynamic
end

subgraph bridge["CATio API Bridge"]
Expand All @@ -36,6 +41,7 @@ flowchart TB
end

clients -->|"Channel Access / PVAccess"| fastcs
yamldef -->|"Controller classes"| fastcs
fastcs -->|"CATioConnection API<br/>(CATioFastCSRequest/Response)"| bridge
bridge -->|"ADS Protocol (TCP/UDP)"| adslayer
adslayer -->|"ADS/AMS Protocol<br/>(TCP 48898, UDP 48899)"| twincat
Expand All @@ -49,9 +55,31 @@ The top layer provides EPICS integration through the FastCS framework:

- **CATioServerController**: Root controller representing the I/O server; manages TCP connections and device discovery
- **CATioDeviceController**: Represents EtherCAT Master devices with their associated attributes
- **CATioTerminalController**: Represents individual EtherCAT slave terminals (couplers, I/O modules)
- **Dynamic Terminal Controllers**: Generated at runtime from YAML terminal definitions (see below)
- **CATioControllerAttributeIO**: Handles attribute read/write operations through the API

### Dynamic Controller Generation

Terminal controllers are generated dynamically from YAML definitions in `src/catio_terminals/terminals/`:

- **`get_terminal_controller_class()`**: Factory function that creates controller classes on demand
- **`catio_dynamic_controller.py`**: Creates FastCS controller classes from YAML terminal type definitions
- **`catio_dynamic_symbol.py`**: Adds PDO symbol attributes (process data) to controllers
- **`catio_dynamic_coe.py`**: Adds CoE parameter attributes (configuration) to controllers
- **`catio_dynamic_types.py`**: Type conversion between TwinCAT, numpy, and FastCS types

Controller classes are cached so only one class is created per terminal type:

```python
from fastcs_catio.catio_dynamic_controller import get_terminal_controller_class

# Get or create a controller class for a terminal type
controller_class = get_terminal_controller_class("EL1004")

# Use it like any other controller class
controller = controller_class(name="MOD1", node=node)
```

### API Bridge Layer

The middle layer provides a clean interface between FastCS and the ADS client:
Expand All @@ -77,25 +105,44 @@ The bottom layer implements the TwinCAT ADS protocol:
2. **Route Addition**: Client machine is added to the TwinCAT server's routing table
3. **TCP Connection**: Establish persistent TCP connection for ADS communication
4. **I/O Introspection**: Query server for devices, slaves, and symbol information
5. **Controller Creation**: Build FastCS controller hierarchy matching hardware topology
6. **Attribute Registration**: Create EPICS PVs for each accessible parameter
5. **Dynamic Controller Creation**: Generate controller classes from YAML definitions based on discovered terminal types
6. **Attribute Registration**: Create EPICS PVs for each accessible parameter (symbols and CoE objects)

### Runtime Data Flow

```mermaid
flowchart TB
A["EPICS Client Request"] --> B["FastCS Attribute Access"]
B --> C["CATioControllerAttributeIO.update()"]
C --> D["CATioConnection.send_query()"]
D --> E["AsyncioADSClient.query() / command()"]
E --> F["API method dispatch (get_* / set_*)"]
F --> G["ADS Read/Write/ReadWrite commands"]
G --> H["TwinCAT Server Response"]
H --> I["Response propagation back to EPICS"]
sequenceDiagram
participant Client as EPICS Client
participant FastCS as FastCS Attribute
participant IO as CATioControllerAttributeIO
participant Conn as CATioConnection
participant ADS as AsyncioADSClient
participant TC as TwinCAT Server

Client->>FastCS: Read/Write PV
FastCS->>IO: update()
IO->>Conn: send_query()
Conn->>ADS: query() / command()
ADS->>ADS: API method dispatch (get_* / set_*)
ADS->>TC: ADS Read/Write/ReadWrite
TC-->>ADS: Response
ADS-->>Conn: Response
Conn-->>IO: Response
IO-->>FastCS: Update attribute
FastCS-->>Client: PV updated
```

## Key Design Decisions

### Dynamic Controller Generation

Terminal controllers are generated from YAML definitions rather than explicit Python classes:

- **Flexibility**: New terminal types can be added by editing YAML without code changes
- **Maintainability**: Single source of truth for terminal definitions in `terminal_types.yaml`
- **Runtime symbols**: Diagnostic symbols from the EtherCAT master defined separately in `runtime_symbols.yaml`
- **Selection**: Only symbols marked `selected: true` in YAML become FastCS attributes

### Asynchronous Architecture

The entire stack uses Python's `asyncio` for non-blocking I/O operations:
Expand All @@ -112,8 +159,8 @@ Controllers form a tree structure mirroring the physical EtherCAT topology:
IOServer
└── IODevice (EtherCAT Master)
├── IOSlave (EK1100 Coupler)
│ ├── IOSlave (EL3xxx Input)
│ └── IOSlave (EL4xxx Output)
│ ├── IOSlave (EL3xxx Input) - DynamicEL3xxxController
│ └── IOSlave (EL4xxx Output) - DynamicEL4xxxController
└── IOSlave (EK1101 Coupler)
└── ...
```
Expand All @@ -137,6 +184,7 @@ CATio is configured through command-line parameters:

## See Also

- [FastCS EPICS IOC Implementation](fastcs-epics-ioc.md) - Detailed explanation of the EPICS layer
- [ADS Client Implementation](ads-client.md) - Detailed explanation of the ADS protocol layer
- [API Decoupling Analysis](api-decoupling.md) - Discussion of the API design and potential improvements
- [FastCS EPICS IOC Implementation](fastcs-epics-ioc.md) - Details of the EPICS layer
- [ADS Client Implementation](ads-client.md) - Details of the ADS protocol layer
- [Terminal YAML Definitions](terminal-yaml-definitions.md) - How to define terminal types in YAML
- [API Decoupling Analysis](decisions/0003-api-decoupling-analysis.md) - Discussion of the API design and potential improvements
4 changes: 2 additions & 2 deletions docs/explanations/composite-types.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Composite Types in TwinCAT

This document explains how TwinCAT generates composite type names and how they are implemented in the catio-terminals codebase. It complements the [Terminal Definitions](terminal-definitions.md) document.
This document explains how TwinCAT generates composite type names and how they are implemented in the catio-terminals codebase. It complements the [Terminal YAML Definitions](terminal-yaml-definitions.md) document.

## What Are Composite Types?

Expand Down Expand Up @@ -212,6 +212,6 @@ When adding support for a new terminal type:

## Related Documentation

- [Terminal Definitions](terminal-definitions.md) - YAML file structure and symbol nodes
- [Terminal YAML Definitions](terminal-yaml-definitions.md) - YAML file structure and symbol nodes
- [Beckhoff XML Format](../reference/beckhoff-xml-format.md) - ESI XML schema reference
- [Architecture Overview](architecture-overview.md) - Overall system design
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,6 @@ These improvements would make the codebase more maintainable, testable, and easi

## See Also

- [Architecture Overview](architecture-overview.md) - High-level system architecture
- [FastCS EPICS IOC Implementation](fastcs-epics-ioc.md) - Details of the EPICS layer
- [ADS Client Implementation](ads-client.md) - Details of the ADS protocol layer
- [Architecture Overview](../architecture-overview.md) - High-level system architecture
- [FastCS EPICS IOC Implementation](../fastcs-epics-ioc.md) - Details of the EPICS layer
- [ADS Client Implementation](../ads-client.md) - Details of the ADS protocol layer
2 changes: 1 addition & 1 deletion docs/explanations/dynamic-pdos.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ The data models for PDO groups are defined in [models.py](../../src/catio_termin

### XML Parsing

The `xml_pdo_groups.py` module handles parsing of PDO group definitions using two methods:
The `xml/pdo_groups.py` module handles parsing of PDO group definitions using two methods:

**Method 1: AlternativeSmMapping (preferred)**

Expand Down
Loading