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 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
asynciofor non-blocking operationsHierarchical 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)
└── EtherCATMasterController (EtherCAT Master device)
├── Dynamic EK1100 controller (Coupler, hoisted to server if single-segment path)
│ ├── Dynamic EL3064 controller (Analog Input)
│ └── Dynamic EL2008 controller (Digital Output)
└── Dynamic EK1101 controller (Coupler)
└── ...
This hierarchy is significant because:
Each level corresponds to physical hardware: The server represents the Beckhoff PLC, devices represent EtherCAT Masters, and terminals represent individual I/O modules
Attributes are scoped appropriately: Server-level attributes (like version info) are separate from terminal-level attributes (like input values)
The tree is auto-generated: CATio introspects the hardware and builds controllers dynamically
Couplers with top-level paths are hoisted: When a coupler or box resolves to a single-segment PV path (e.g.,
BL04I-EA-E1RIO-01), it is registered directly under the server rather than nested under the device, enabling PVI to render it as a top-level screen
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
_tcp_connection)Unique integer identifiers for API dispatch (class-level counter
_identifier)References to corresponding hardware objects (
IOServer,IODevice, orIOSlave)Attribute grouping for organized PV naming
Each controller instance registers three IO handler objects that route attribute reads and writes to the appropriate ADS mechanism:
IO class |
Purpose |
|---|---|
|
Standard polled controller attributes (device/terminal metadata) |
|
PDO symbol attributes from ADS symbol read/write |
|
CoE configuration parameter attributes |
The CATioController class is defined in 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
All server settings are packaged in a CATioServerControllerOptions dataclass, which groups:
Sub-dataclass |
Purpose |
|---|---|
|
Target IP and port for the TwinCAT server |
|
UDP route credentials (user name, password) |
|
Polling and notification update periods |
|
PV name templates for device, node, and module controllers |
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 calls get_subcontrollers_from_node() recursively to create corresponding FastCS controllers. Couplers and boxes whose resolved path consists of a single segment are hoisted from the device level up to the server level, so PVI can render them inline on the top-level screen.
Device and Terminal Controllers#
The concrete device controller for an EtherCAT master is EtherCATMasterController (a CATioDeviceController subclass defined in catio_hardware.py). The set of supported device types is stored in SUPPORTED_DEVICE_CONTROLLERS. CATioDeviceController exposes attributes including:
Attribute |
Description |
|---|---|
|
Number of terminals connected to this master |
|
Array of EtherCAT state machine values for all terminals |
|
CRC error counters for network diagnostics |
|
Number of EtherCAT nodes registered on the device |
|
EtherCAT frame timestamp |
|
Count of sent cyclic EtherCAT frames |
|
Count of lost cyclic EtherCAT frames |
|
Count of sent acyclic EtherCAT frames |
|
Count of lost acyclic EtherCAT frames |
EtherCATMasterController adds further notification-stream attributes (InFrm0State, InFrm0WcState, InFrm0InpToggle, OutFrm0Ctrl, OutFrm0WcCtrl, InputsDevState, OutputsDevCtrl, InputsSlaveCount) that are updated via ADS notifications rather than polling.
CATioTerminalController represents individual I/O modules (EK couplers, EL terminals) with attributes including:
Attribute |
Description |
|---|---|
|
The terminal’s EtherCAT state machine value |
|
Network link health indicator |
|
Accumulated CRC errors (sum across all ports) |
|
Per-port CRC error counters |
|
Chain node index for this terminal |
|
Chain position index for this terminal |
|
EtherCAT address |
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:
Looks up the terminal type (e.g., “EL3064”) across all YAML files in
src/catio_terminals/terminals/Creates a controller class dynamically based on the YAML definition (
DynamicEL3064Controller, etc.)Adds runtime symbol attributes (e.g.,
WcState,InfoData) applicable to this terminal typeAdds PDO symbol attributes for process data (from
catio_dynamic_symbol.py)Adds CoE attributes for configuration parameters (from
catio_dynamic_coe.py)Caches the class for reuse across multiple instances of the same terminal type
The key modules involved:
Module |
Purpose |
|---|---|
|
Factory function and dynamic class creation |
|
Adds PDO symbol attributes to controllers |
|
Adds CoE parameter attributes to controllers |
|
Type conversion between TwinCAT, numpy, and FastCS |
Terminal YAML Definitions#
Each terminal type is defined in src/catio_terminals/terminals/terminal_types.yaml (the terminal config loader supports multiple YAML files via glob patterns) with:
Symbol nodes: Process data accessible via ADS (inputs/outputs)
CoE objects: Configuration parameters with subindices
Selection: Only symbols with
selected: truebecome attributes
For example, a digital input terminal might expose:
Attribute Type |
Source |
Examples |
|---|---|---|
Runtime symbols |
|
WcState, InfoData |
PDO symbols |
|
Input values, status bits |
CoE parameters |
|
Filter settings, calibration |
This approach allows adding new terminal types by editing YAML files without changing Python code. See Terminal YAML Definitions 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 three IO classes in catio_attribute_io.py:
Class |
Ref class |
Used for |
|---|---|---|
|
|
Standard polled controller attributes (metadata, counters) |
|
|
PDO symbol read/write via ADS symbol names |
|
|
CoE configuration parameters read/write |
All three are registered with every controller instance in CATioController.__init__, and FastCS dispatches each attribute’s update or send call to the appropriate IO object based on the io_ref type.
How Attribute Updates Work#
The update flow for a standard polled attribute follows these steps:
FastCS calls the
update()method on an attribute’s IO handler at the configured polling intervalThe IO handler constructs an API query string based on the attribute name and controller context
The query is sent through
CATioConnectionto theAsyncioADSClientThe client dispatches to the appropriate
get_*methodThe response flows back and the attribute value is updated
For symbol attributes, the CATioControllerSymbolAttributeIO handler looks up the ADS symbol name via ads_name_map on the controller, then issues a SYMBOL_PARAM request to read or write the value directly by symbol name.
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 |
|---|---|---|
|
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. The naming is configured via CATioNameMappings (part of CATioServerControllerOptions) using three Python str.format-style templates:
Template field |
Default |
Controls |
|---|---|---|
|
|
EtherCAT master devices |
|
|
EtherCAT couplers/boxes |
|
|
Individual I/O terminals |
Templates may use {} or {n} for the numeric index, {id} for the IOC root prefix, {device_prefix} (inside node_prefix and module_prefix), and {node_prefix} (inside module_prefix). Rendered results are split on : to produce multi-segment controller paths.
In the shipped fastcs.yaml the site configuration is:
name_mappings:
device_prefix: "{id}:ETH{:02d}"
node_prefix: "BL04I-EA-E1RIO-{:02d}"
module_prefix: "{node_prefix}:MOD{:02d}"
With id: BL04I-EA-CATIO-01, this produces PV names like:
PV Name |
Description |
|---|---|
|
I/O server name |
|
Number of slaves on EtherCAT Master 1 |
|
Value from module 5 on remote I/O node 1 |
|
EtherCAT state of that module |
Since node_prefix here does not include {device_prefix}, node controllers resolve to a single path segment (e.g., BL04I-EA-E1RIO-01) and are hoisted directly under the server controller.
Lifecycle Management#
CATio controllers follow a specific lifecycle managed by FastCS:
Initialization Phase#
Route addition: UDP message registers this client with the TwinCAT router
TCP connection: Establishes persistent ADS communication channel via
create_tcp_connection()Introspection: Queries server for devices, terminals, and symbols via
CATioConnection.initialise()Controller creation:
register_subcontrollers()traverses the hardware tree and callsget_subcontrollers_from_node()recursivelyAttribute registration: Each subcontroller’s
initialise()creates FastCS attributesAttribute map:
get_complete_attribute_map()builds a flat map of all PV keys, used to gate notification subscriptions
Runtime Phase#
Polling handlers execute at their configured intervals (default
poll_period: 1.0seconds)Notification streams are processed via the
@scan(NOTIFICATION_UPDATE_PERIOD)method on the server controller (default every 0.2 seconds)Subscriptions are gated to only the attributes present in the attribute map — symbols unused by any PV are not subscribed to
The controller tree remains stable (hot-plugging is not supported)
Shutdown Phase#
Notification cleanup: Disables notification monitoring and clears the cached stream
Connection closure: Closes the TCP connection gracefully via
CATioConnection.close()Route removal: Currently disabled in code (route deletion is commented out)
Testing Considerations#
When writing tests for CATio controllers, you typically need to mock the ADS client layer. The MockADSServer class in 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 - High-level system architecture
ADS Client Implementation - Details of the ADS protocol layer
API Decoupling Analysis - API design discussion