# FastCS EPICS IOC Implementation This document explains how CATio uses the FastCS framework to expose EtherCAT devices as EPICS Process Variables (PVs), enabling control system integration. ## What is FastCS? [FastCS](https://github.com/DiamondLightSource/FastCS) is a Python framework for building EPICS Input/Output Controllers (IOCs). It provides a declarative approach where Python class attributes automatically become EPICS PVs. CATio leverages FastCS to create a hierarchical controller structure that mirrors the physical EtherCAT topology. Key benefits of using FastCS include: - **Automatic PV generation**: Define Python attributes, get EPICS PVs - **Asynchronous I/O**: Built on `asyncio` for non-blocking operations - **Hierarchical controllers**: Natural mapping to nested hardware structures - **Built-in scanning**: Periodic polling with configurable intervals ## The Controller Hierarchy CATio organizes its FastCS controllers in a tree structure that reflects the physical EtherCAT network: ``` CATioServerController (root) └── CATioDeviceController (EtherCAT Master) ├── CATioTerminalController (EK1100 Coupler) │ ├── CATioTerminalController (EL3064 Analog Input) │ └── CATioTerminalController (EL2008 Digital Output) └── CATioTerminalController (EK1101 Coupler) └── ... ``` This hierarchy is significant because: 1. **Each level corresponds to physical hardware**: The server represents the Beckhoff PLC, devices represent EtherCAT Masters, and terminals represent individual I/O modules 2. **Attributes are scoped appropriately**: Server-level attributes (like version info) are separate from terminal-level attributes (like input values) 3. **The tree is auto-generated**: CATio introspects the hardware and builds controllers dynamically ### The Base Controller All CATio controllers inherit from `CATioController`, which extends the FastCS `Controller` class. The base class provides: - A shared TCP connection to the TwinCAT server (class-level singleton) - Unique identifiers for API dispatch - References to corresponding hardware objects (`IOServer`, `IODevice`, or `IOSlave`) - Attribute grouping for organized PV naming The `CATioController` class is defined in [catio_controller.py](../../src/fastcs_catio/catio_controller.py). It includes connection management, attribute registration, and the core interface for communicating with the ADS client. ### The Server Controller `CATioServerController` is the root of the hierarchy. It handles: - **Route establishment**: Uses UDP to register this client with the TwinCAT router - **TCP connection**: Opens the persistent ADS communication channel - **Hardware discovery**: Introspects the I/O server to find all devices and terminals - **Subcontroller creation**: Instantiates the appropriate controller classes for discovered hardware During initialization, the server controller queries the TwinCAT system and builds the complete controller tree automatically. The key method is `register_subcontrollers()` which traverses the discovered hardware tree and creates corresponding FastCS controllers. ### Device and Terminal Controllers `CATioDeviceController` represents EtherCAT Master devices and exposes attributes like: | Attribute | Description | |-----------|-------------| | `SlaveCount` | Number of terminals connected to this master | | `SlavesStates` | Array of EtherCAT state machine values for all terminals | | `SlavesCrcCounters` | CRC error counters for network diagnostics | | `FrameCounters` | Statistics on cyclic and acyclic EtherCAT frames | `CATioTerminalController` represents individual I/O modules (EK couplers, EL terminals) with attributes like: | Attribute | Description | |-----------|-------------| | `EcatState` | The terminal's EtherCAT state machine value | | `LinkStatus` | Network link health indicator | | `CrcErrorSum` | Accumulated CRC errors for this terminal | ## Dynamic Terminal Controllers Not all terminals are alike. A digital input module exposes different data than an analog output module. CATio handles this through dynamically generated controller classes based on YAML terminal definitions. ### How Dynamic Generation Works When CATio discovers a terminal, it calls `get_terminal_controller_class(terminal_id)` from `catio_dynamic_controller.py`. This factory function: 1. Looks up the terminal type (e.g., "EL3064") in `terminal_types.yaml` 2. Creates a controller class dynamically based on the YAML definition 3. Adds symbol attributes for process data (from `catio_dynamic_symbol.py`) 4. Adds CoE attributes for configuration parameters (from `catio_dynamic_coe.py`) 5. Caches the class for reuse The key modules involved: | Module | Purpose | |--------|---------| | `catio_dynamic_controller.py` | Factory function and dynamic class creation | | `catio_dynamic_symbol.py` | Adds PDO symbol attributes to controllers | | `catio_dynamic_coe.py` | Adds CoE parameter attributes to controllers | | `catio_dynamic_types.py` | Type conversion between TwinCAT, numpy, and FastCS | ### Terminal YAML Definitions Each terminal type is defined in `src/catio_terminals/terminals/terminal_types.yaml` with: - **Symbol nodes**: Process data accessible via ADS (inputs/outputs) - **CoE objects**: Configuration parameters with subindices - **Selection**: Only symbols with `selected: true` become attributes For example, a digital input terminal might expose: | Attribute Type | Source | Examples | |----------------|--------|----------| | PDO symbols | `symbol_nodes` in YAML | Input values, status bits | | Runtime symbols | `runtime_symbols.yaml` | WcState, InfoData | | CoE parameters | `coe_objects` in YAML | Filter settings, calibration | This approach allows adding new terminal types by editing YAML files without changing Python code. See [Terminal YAML Definitions](terminal-yaml-definitions.md) for details on the YAML format. ## The Attribute I/O System FastCS attributes need to know how to read (and optionally write) their values. CATio implements this through `CATioControllerAttributeIO`, which bridges FastCS attributes to the ADS client API. ### How Attribute Updates Work The update flow for a CATio attribute follows these steps: 1. FastCS calls the `update()` method on an attribute's I/O handler at the configured polling interval 2. The I/O handler constructs an API query string based on the attribute name and controller context 3. The query is sent through `CATioConnection` to the `AsyncioADSClient` 4. The client dispatches to the appropriate `get_*` method 5. The response flows back and the attribute value is updated This indirection means attributes don't need to know ADS protocol details - they just specify their name and polling period. The `CATioControllerAttributeIO` class in [catio_attribute_io.py](../../src/fastcs_catio/catio_attribute_io.py) implements this bridge between FastCS attributes and the ADS client API. ### Polling vs Notifications CATio supports two update mechanisms: **Polling** (default): The I/O handler periodically queries the ADS server. Simple and reliable, but adds latency and network traffic proportional to the number of attributes and polling rate. **Notifications**: The ADS server pushes value changes to the client. More efficient for high-frequency data, but requires subscription management and careful buffer handling. The choice depends on the attribute's requirements: | Update Mode | Use Case | Typical Period | |-------------|----------|----------------| | `ONCE` | Static configuration (device name, version) | Read at startup only | | Standard polling | Slowly-changing diagnostics (CRC counters) | 1-2 seconds | | Fast polling | Process values needing moderate rates | 100-500 ms | | Notifications | High-frequency acquisition data | Sub-millisecond | ## PV Naming Convention CATio generates EPICS PV names that reflect the hardware hierarchy: ``` ::::: ``` For example: | PV Name | Description | |---------|-------------| | `CATIO:IOServer:Name` | I/O server name | | `CATIO:IOServer:ETH1:SlaveCount` | Number of slaves on EtherCAT Master 1 | | `CATIO:IOServer:ETH1:RIO1:MOD5:Value` | Value from module 5 on remote I/O node 1 | | `CATIO:IOServer:ETH1:RIO1:MOD5:EcatState` | EtherCAT state of that module | The naming components come from: - **ecat_name**: The name configured in TwinCAT (e.g., "Device1", "Term 5 (EL3064)") - **get_type_name()**: A method that converts Beckhoff names to PV-friendly format (e.g., "ETH1", "RIO1", "MOD5") ## Lifecycle Management CATio controllers follow a specific lifecycle managed by FastCS: ### Initialization Phase 1. **Route addition**: UDP message registers this client with the TwinCAT router 2. **TCP connection**: Establishes persistent ADS communication channel 3. **Introspection**: Queries server for devices, terminals, and symbols 4. **Controller creation**: Builds the controller tree matching discovered hardware 5. **Attribute registration**: Creates FastCS attributes for each controller ### Runtime Phase - Polling handlers execute at their configured intervals - Notification streams are processed and distributed to attributes - The controller tree remains stable (hot-plugging is not supported) ### Shutdown Phase 1. **Notification cleanup**: Unsubscribes from all ADS notifications 2. **Connection closure**: Closes the TCP connection gracefully 3. **Route removal**: Optionally removes the route from TwinCAT ## Testing Considerations When writing tests for CATio controllers, you typically need to mock the ADS client layer. The `MockADSServer` class in [mock_server.py](../../tests/mock_server.py) simulates TwinCAT responses. This allows testing controller logic without real hardware by: - Simulating ADS command responses - Providing mock symbol data - Testing notification handling ## See Also - [Architecture Overview](architecture-overview.md) - High-level system architecture - [ADS Client Implementation](ads-client.md) - Details of the ADS protocol layer - [API Decoupling Analysis](decisions/0003-api-decoupling-analysis.md) - API design discussion