Skip to content
Merged
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: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,8 @@ 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.

- **Testing Troubleshooting**: If `test_system.py` reports that simulator port 48898 is already in use, check if VS Code has auto-forwarded the port. In VS Code's "Ports" panel (View → Ports), delete any forwarding for port 48898. VS Code's auto port-forwarding can prevent the test simulator from binding to the port.

- **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
Expand Down Expand Up @@ -279,7 +281,7 @@ Skills are specialized knowledge that can be loaded on demand. Use these prompts
2. **Instantiate and load configuration:**
```python
chain = EtherCATChain() # Create instance first
chain.load_config(Path('tests/ads_sim/server_config.yaml')) # Instance method, not class method
chain.load_config(Path('tests/ads_sim/erver_config_CX7000_cs2.yaml')) # Instance method, not class method
```

3. **Check symbol counts:**
Expand Down Expand Up @@ -328,7 +330,7 @@ Skills are specialized knowledge that can be loaded on demand. Use these prompts
**Related files:**
- `tests/ads_sim/ethercat_chain.py` - Chain and device/slave models
- `tests/ads_sim/server.py` - ADS protocol server
- `tests/ads_sim/server_config.yaml` - default YAML representation of the Simulator
- `tests/ads_sim/erver_config_CX7000_cs2.yaml` - default YAML representation of the Simulator
- `tests/test_system.py` - Integration tests against simulator

---
Expand Down
6 changes: 3 additions & 3 deletions tests/ads_sim/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,13 @@ asyncio.run(main())
## Configuration

The EtherCAT chain is configured via YAML. The default configuration is in
`server_config.yaml`, which defines the server settings and device instances.
`erver_config_CX7000_cs2.yaml`, which defines the server settings and device instances.
Terminal type definitions are stored in separate YAML files in
`src/catio_terminals/terminals/`, organized by terminal class.

### Structure

Server configuration file (`server_config.yaml`):
Server configuration file (`erver_config_CX7000_cs2.yaml`):

```yaml
# Server information
Expand Down Expand Up @@ -181,7 +181,7 @@ tests/ads_sim/
├── __init__.py # Package exports
├── __main__.py # CLI entry point
├── ethercat_chain.py # Chain configuration parser
├── server_config.yaml # Default server/device configuration
├── erver_config_CX7000_cs2.yaml # Default server/device configuration
├── server.py # ADS protocol server
└── README.md # This file

Expand Down
3 changes: 2 additions & 1 deletion tests/ads_sim/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
python -m tests.ads_sim [--host HOST] [--port PORT] [--config CONFIG]

Example:
python -m tests.ads_sim --host 127.0.0.1 --port 48898 --config server_config.yaml
python -m tests.ads_sim --host 127.0.0.1 --port 48898 --config
server_config_CX7000_cs2.yaml
"""

from .ethercat_chain import EtherCATChain, EtherCATSlave
Expand Down
26 changes: 24 additions & 2 deletions tests/ads_sim/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
--host HOST Host address to bind to (default: 127.0.0.1)
--port PORT Port to listen on (default: 48898)
--config PATH Path to YAML config file
(default: server_config.yaml)
(default: erver_config_CX7000_cs2.yaml)
--terminal-defs PATTERN Glob pattern(s) for terminal definition YAML files
(default: DLS embedded definitions)
--log-level LEVEL Set logging level: DEBUG, INFO, WARNING, ERROR
(default: INFO)
--disable-notifications Disable the notification system to reduce
Expand All @@ -18,6 +20,8 @@
Example:
python -m tests.ads_sim --host 0.0.0.0 --port 48898 --log-level DEBUG
python -m tests.ads_sim --disable-notifications --log-level INFO
python -m tests.ads_sim --terminal-defs "custom_terminals/*.yaml"
python -m tests.ads_sim --terminal-defs "term1.yaml,term2.yaml"
"""

from __future__ import annotations
Expand Down Expand Up @@ -66,6 +70,17 @@ def parse_args() -> argparse.Namespace:
default=None,
help="Path to YAML configuration file for EtherCAT chain",
)
parser.add_argument(
"--terminal-defs",
type=str,
default=None,
help=(
"Glob pattern for terminal definition YAML files. "
"Can use wildcards like '*.yaml' or '**/*.yaml' for recursive search. "
"Defaults to DLS yaml descriptions embedded in the python package. "
"May also be a comma separated list of glob patterns or filenames."
),
)
parser.add_argument(
"--log-level",
type=str,
Expand Down Expand Up @@ -98,16 +113,23 @@ async def main() -> int:
return 1
else:
# Use default config from package
default_config = Path(__file__).parent / "server_config.yaml"
default_config = Path(__file__).parent / "erver_config_CX7000_cs2.yaml"
if default_config.exists():
config_path = default_config
logger.info(f"Using default config: {config_path}")

# Parse terminal definitions patterns
terminal_patterns: list[str] | None = None
if args.terminal_defs:
terminal_patterns = [p.strip() for p in args.terminal_defs.split(",")]
logger.info(f"Using terminal definition patterns: {terminal_patterns}")

# Create and start server
server = ADSSimServer(
host=args.host,
port=args.port,
config_path=config_path,
terminal_patterns=terminal_patterns,
enable_notifications=not args.disable_notifications,
)

Expand Down
74 changes: 56 additions & 18 deletions tests/ads_sim/ethercat_chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -464,12 +464,18 @@ class EtherCATChain:
device and slave information.
"""

def __init__(self, config_path: str | Path | None = None):
def __init__(
self,
config_path: str | Path | None = None,
terminal_patterns: list[str] | None = None,
):
"""
Initialize the EtherCAT chain.

Args:
config_path: Path to YAML configuration file. If None, uses default config.
terminal_patterns: Glob patterns for terminal definition YAML files.
If None, uses default DLS terminal definitions.
"""
self.server_info = ServerInfo()
self.devices: dict[int, EtherCATDevice] = {}
Expand All @@ -478,6 +484,7 @@ def __init__(self, config_path: str | Path | None = None):
str, ModelTerminalType
] = {} # Full terminal models for PDO filtering
self.runtime_symbols: RuntimeSymbolsConfig | None = None
self.terminal_patterns = terminal_patterns

# Load runtime symbols configuration
self._load_runtime_symbols()
Expand All @@ -489,7 +496,7 @@ def __init__(self, config_path: str | Path | None = None):
self.load_config(config_path)
else:
# Load default config from package
default_config = Path(__file__).parent / "server_config.yaml"
default_config = Path(__file__).parent / "server_config_CX7000_cs2.yaml"
if default_config.exists():
self.load_config(default_config)
else:
Expand Down Expand Up @@ -527,27 +534,58 @@ def _load_terminal_types(self) -> None:
Load terminal type definitions from YAML files.

Loads terminal types from:
1. Built-in terminal types in src/catio_terminals/terminals/
2. Legacy terminal_types in the main config (for backwards compatibility)
1. Custom patterns if provided via terminal_patterns
2. Built-in terminal types in src/catio_terminals/terminals/ (default)
"""
# load full model TerminalType definitions for PDO group filtering
import glob

from catio_terminals.models import TerminalConfig

# use the default terminal types used by the ioc to ensure we are testing
# things that will really happen!
terminals_path = (
Path(__file__).parents[2]
/ "src"
/ "catio_terminals"
/ "terminals"
/ "terminal_types.yaml"
)
if terminals_path.exists():
config = TerminalConfig.from_yaml(terminals_path)
self.model_terminals = config.terminal_types
logger.debug(f"Loaded {len(self.model_terminals)} model terminal types")
# Determine which patterns to use
if self.terminal_patterns:
patterns = self.terminal_patterns
logger.info(f"Using custom terminal definition patterns: {patterns}")
else:
raise RuntimeError(f"Terminal types file not found: {terminals_path}")
# Use the default terminal types used by the IOC to ensure we are testing
# things that will really happen!
terminals_path = (
Path(__file__).parents[2]
/ "src"
/ "catio_terminals"
/ "terminals"
/ "*.yaml"
)
patterns = [str(terminals_path)]
logger.debug(f"Using default terminal definition pattern: {patterns[0]}")

# Expand all glob patterns and collect YAML files
yaml_files = []
for pattern in patterns:
matched = glob.glob(pattern, recursive=True)
yaml_files.extend(matched)

if not yaml_files:
raise RuntimeError(
f"No terminal definition YAML files found matching patterns: {patterns}"
)

logger.debug(f"Loading terminal types from {len(yaml_files)} YAML files")

# Load all terminal types from the collected files
all_terminals: dict[str, ModelTerminalType] = {}
for yaml_file in yaml_files:
try:
config = TerminalConfig.from_yaml(Path(yaml_file))
all_terminals.update(config.terminal_types)
logger.debug(
f"Loaded {len(config.terminal_types)} terminals from {yaml_file}"
)
except Exception as e:
logger.warning(f"Failed to load terminal types from {yaml_file}: {e}")

self.model_terminals = all_terminals
logger.info(f"Loaded {len(self.model_terminals)} model terminal types total")

def load_config(self, config_path: str | Path) -> None:
"""
Expand Down
4 changes: 3 additions & 1 deletion tests/ads_sim/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ def __init__(
host: str = "127.0.0.1",
port: int = ADS_TCP_PORT,
config_path: str | Path | None = None,
terminal_patterns: list[str] | None = None,
enable_notifications: bool = True,
):
"""
Expand All @@ -297,6 +298,7 @@ def __init__(
host: Host address to bind to.
port: Port to listen on.
config_path: Path to YAML config file for EtherCAT chain.
terminal_patterns: Glob patterns for terminal definition YAML files.
enable_notifications: Whether to enable the notification system.
"""
self.host = host
Expand All @@ -307,7 +309,7 @@ def __init__(
self.enable_notifications = enable_notifications

# Load EtherCAT chain configuration
self.chain = EtherCATChain(config_path)
self.chain = EtherCATChain(config_path, terminal_patterns=terminal_patterns)

# ADS state
self.ads_state = AdsState.ADSSTATE_RUN
Expand Down
Loading