From 52ca402683ad97e4b80db48c69ad53cd7eb022af Mon Sep 17 00:00:00 2001 From: Giles Knap Date: Thu, 5 Feb 2026 12:06:56 +0000 Subject: [PATCH 1/2] Add additional sever configs to tests --- AGENTS.md | 4 +- tests/ads_sim/README.md | 6 +- tests/ads_sim/__init__.py | 3 +- tests/ads_sim/__main__.py | 26 +- tests/ads_sim/ethercat_chain.py | 74 ++- tests/ads_sim/server.py | 4 +- tests/ads_sim/server_config.yaml | 484 -------------------- tests/ads_sim/server_config_CX7000_cs1.yaml | 109 +++++ tests/ads_sim/server_config_CX7000_cs2.yaml | 437 ++++++++++++++++++ tests/ads_sim/server_config_CX8290_cs1.yaml | 77 ++++ tests/diagnose_hardware.py | 68 ++- tests/test_system.py | 35 +- 12 files changed, 793 insertions(+), 534 deletions(-) delete mode 100644 tests/ads_sim/server_config.yaml create mode 100644 tests/ads_sim/server_config_CX7000_cs1.yaml create mode 100644 tests/ads_sim/server_config_CX7000_cs2.yaml create mode 100644 tests/ads_sim/server_config_CX8290_cs1.yaml diff --git a/AGENTS.md b/AGENTS.md index 8add21a3..8645f6d5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -279,7 +279,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:** @@ -328,7 +328,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 --- diff --git a/tests/ads_sim/README.md b/tests/ads_sim/README.md index c024ae63..ed0a3cbe 100644 --- a/tests/ads_sim/README.md +++ b/tests/ads_sim/README.md @@ -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 @@ -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 diff --git a/tests/ads_sim/__init__.py b/tests/ads_sim/__init__.py index 9c079878..be7ec9d9 100644 --- a/tests/ads_sim/__init__.py +++ b/tests/ads_sim/__init__.py @@ -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 diff --git a/tests/ads_sim/__main__.py b/tests/ads_sim/__main__.py index 87c7101b..b0dfaa9b 100644 --- a/tests/ads_sim/__main__.py +++ b/tests/ads_sim/__main__.py @@ -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 @@ -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 @@ -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, @@ -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, ) diff --git a/tests/ads_sim/ethercat_chain.py b/tests/ads_sim/ethercat_chain.py index 5b856e8f..ec590b09 100644 --- a/tests/ads_sim/ethercat_chain.py +++ b/tests/ads_sim/ethercat_chain.py @@ -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] = {} @@ -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() @@ -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: @@ -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: """ diff --git a/tests/ads_sim/server.py b/tests/ads_sim/server.py index 5c653fa0..08ec533e 100644 --- a/tests/ads_sim/server.py +++ b/tests/ads_sim/server.py @@ -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, ): """ @@ -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 @@ -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 diff --git a/tests/ads_sim/server_config.yaml b/tests/ads_sim/server_config.yaml deleted file mode 100644 index 86ace940..00000000 --- a/tests/ads_sim/server_config.yaml +++ /dev/null @@ -1,484 +0,0 @@ -# EtherCAT Chain Configuration for ADS Simulation Server -# ======================================================== -# -# This file defines the server configuration and device instances. -# Terminal type definitions are loaded from separate YAML files in -# src/catio_terminals/terminals/ -# -# Device configuration references terminal types by name (e.g., "EL2024"). -# The EtherCATChain class will load terminal type definitions from -# the separate YAML files. - -# ============================================================ -# Server Configuration -# ============================================================ -server: - name: "I/O Server" - version: "3.1" - build: 4024 - -# ============================================================ -# Device Configuration -# ============================================================ -devices: - - id: 1 - name: "Device 1 (EtherCAT)" - type: 94 # IODEVICETYPE_ETHERCAT - netid: "10.0.0.1.3.1" - identity: - vendor_id: 2 - product_code: 0x044C2C52 - revision_number: 1 - serial_number: 1001 - slaves: - # Node 0 - EK1110 EtherCAT extension terminal - - type: "EK1110" - name: "Term 2 (EK1110)" - node: 0 - position: 0 - - # Node 1 - First EK1100 coupler with EL2024 digital output slices - - type: "EK1100" - name: "Term 3 (EK1100)" - node: 1 - position: 0 - - # EL2024 digital output terminals on node 1 (positions 1-16) - - type: "EL2024" - name: "Term 4 (EL2024)" - node: 1 - position: 1 - - type: "EL2024" - name: "Term 5 (EL2024)" - node: 1 - position: 2 - - type: "EL2024" - name: "Term 6 (EL2024)" - node: 1 - position: 3 - - type: "EL2024" - name: "Term 7 (EL2024)" - node: 1 - position: 4 - - type: "EL2024" - name: "Term 8 (EL2024)" - node: 1 - position: 5 - - type: "EL2024" - name: "Term 9 (EL2024)" - node: 1 - position: 6 - - type: "EL2024" - name: "Term 10 (EL2024)" - node: 1 - position: 7 - - type: "EL2024" - name: "Term 11 (EL2024)" - node: 1 - position: 8 - - type: "EL2024" - name: "Term 12 (EL2024)" - node: 1 - position: 9 - - type: "EL2024" - name: "Term 13 (EL2024)" - node: 1 - position: 10 - - type: "EL2024" - name: "Term 14 (EL2024)" - node: 1 - position: 11 - - type: "EL2024" - name: "Term 15 (EL2024)" - node: 1 - position: 12 - - type: "EL2024" - name: "Term 16 (EL2024)" - node: 1 - position: 13 - - type: "EL2024" - name: "Term 17 (EL2024)" - node: 1 - position: 14 - - type: "EL2024" - name: "Term 18 (EL2024)" - node: 1 - position: 15 - - type: "EL2024" - name: "Term 19 (EL2024)" - node: 1 - position: 16 - - # EL9410 power supply - - type: "EL9410" - name: "Term 20 (EL9410)" - node: 1 - position: 17 - - # More EL2024 terminals on node 1 (positions 18-49) - - type: "EL2024" - name: "Term 21 (EL2024)" - node: 1 - position: 18 - - type: "EL2024" - name: "Term 22 (EL2024)" - node: 1 - position: 19 - - type: "EL2024" - name: "Term 23 (EL2024)" - node: 1 - position: 20 - - type: "EL2024" - name: "Term 24 (EL2024)" - node: 1 - position: 21 - - type: "EL2024" - name: "Term 25 (EL2024)" - node: 1 - position: 22 - - type: "EL2024" - name: "Term 26 (EL2024)" - node: 1 - position: 23 - - type: "EL2024" - name: "Term 27 (EL2024)" - node: 1 - position: 24 - - type: "EL2024" - name: "Term 28 (EL2024)" - node: 1 - position: 25 - - type: "EL2024" - name: "Term 29 (EL2024)" - node: 1 - position: 26 - - type: "EL2024" - name: "Term 30 (EL2024)" - node: 1 - position: 27 - - type: "EL2024" - name: "Term 31 (EL2024)" - node: 1 - position: 28 - - type: "EL2024" - name: "Term 32 (EL2024)" - node: 1 - position: 29 - - type: "EL2024" - name: "Term 33 (EL2024)" - node: 1 - position: 30 - - type: "EL2024" - name: "Term 34 (EL2024)" - node: 1 - position: 31 - - type: "EL2024" - name: "Term 35 (EL2024)" - node: 1 - position: 32 - - type: "EL2024" - name: "Term 36 (EL2024)" - node: 1 - position: 33 - - # EL9410 power supply - - type: "EL9410" - name: "Term 37 (EL9410)" - node: 1 - position: 34 - - # More EL2024 terminals - - type: "EL2024" - name: "Term 38 (EL2024)" - node: 1 - position: 35 - - type: "EL2024" - name: "Term 39 (EL2024)" - node: 1 - position: 36 - - type: "EL2024" - name: "Term 40 (EL2024)" - node: 1 - position: 37 - - type: "EL2024" - name: "Term 41 (EL2024)" - node: 1 - position: 38 - - type: "EL2024" - name: "Term 42 (EL2024)" - node: 1 - position: 39 - - type: "EL2024" - name: "Term 43 (EL2024)" - node: 1 - position: 40 - - type: "EL2024" - name: "Term 44 (EL2024)" - node: 1 - position: 41 - - type: "EL2024" - name: "Term 45 (EL2024)" - node: 1 - position: 42 - - type: "EL2024" - name: "Term 46 (EL2024)" - node: 1 - position: 43 - - type: "EL2024" - name: "Term 47 (EL2024)" - node: 1 - position: 44 - - type: "EL2024" - name: "Term 48 (EL2024)" - node: 1 - position: 45 - - type: "EL2024" - name: "Term 49 (EL2024)" - node: 1 - position: 46 - - type: "EL2024" - name: "Term 50 (EL2024)" - node: 1 - position: 47 - - type: "EL2024" - name: "Term 51 (EL2024)" - node: 1 - position: 48 - - type: "EL2024" - name: "Term 52 (EL2024)" - node: 1 - position: 49 - - # Node 2 - Second EK1100 coupler with EL1014 digital inputs and EL1502 counters - - type: "EK1100" - name: "Term 53 (EK1100)" - node: 2 - position: 0 - - # EL1014 digital input terminals - - type: "EL1014" - name: "Term 54 (EL1014)" - node: 2 - position: 1 - - type: "EL1014" - name: "Term 55 (EL1014)" - node: 2 - position: 2 - - type: "EL1014" - name: "Term 56 (EL1014)" - node: 2 - position: 3 - - type: "EL1014" - name: "Term 57 (EL1014)" - node: 2 - position: 4 - - type: "EL1014" - name: "Term 58 (EL1014)" - node: 2 - position: 5 - - type: "EL1014" - name: "Term 59 (EL1014)" - node: 2 - position: 6 - - type: "EL1014" - name: "Term 60 (EL1014)" - node: 2 - position: 7 - - type: "EL1014" - name: "Term 61 (EL1014)" - node: 2 - position: 8 - - type: "EL1014" - name: "Term 62 (EL1014)" - node: 2 - position: 9 - - type: "EL1014" - name: "Term 63 (EL1014)" - node: 2 - position: 10 - - type: "EL1014" - name: "Term 64 (EL1014)" - node: 2 - position: 11 - - # EL1502 counter terminals - - type: "EL1502" - name: "Term 65 (EL1502)" - node: 2 - position: 12 - - type: "EL1502" - name: "Term 66 (EL1502)" - node: 2 - position: 13 - - type: "EL1502" - name: "Term 67 (EL1502)" - node: 2 - position: 14 - - type: "EL1502" - name: "Term 68 (EL1502)" - node: 2 - position: 15 - - type: "EL1502" - name: "Term 69 (EL1502)" - node: 2 - position: 16 - - type: "EL1502" - name: "Term 70 (EL1502)" - node: 2 - position: 17 - - # EL1004 digital input terminal - - type: "EL1004" - name: "Term 71 (EL1004)" - node: 2 - position: 18 - - # EL9410 power supply - - type: "EL9410" - name: "Term 72 (EL9410)" - node: 2 - position: 19 - - # EL2024 digital output terminals on node 2 - - type: "EL2024" - name: "Term 73 (EL2024)" - node: 2 - position: 20 - - type: "EL2024" - name: "Term 74 (EL2024)" - node: 2 - position: 21 - - type: "EL2024" - name: "Term 75 (EL2024)" - node: 2 - position: 22 - - type: "EL2024" - name: "Term 76 (EL2024)" - node: 2 - position: 23 - - type: "EL2024" - name: "Term 77 (EL2024)" - node: 2 - position: 24 - - type: "EL2024" - name: "Term 78 (EL2024)" - node: 2 - position: 25 - - type: "EL2024" - name: "Term 79 (EL2024)" - node: 2 - position: 26 - - type: "EL2024" - name: "Term 80 (EL2024)" - node: 2 - position: 27 - - type: "EL2024" - name: "Term 81 (EL2024)" - node: 2 - position: 28 - - type: "EL2024" - name: "Term 82 (EL2024)" - node: 2 - position: 29 - - type: "EL2024" - name: "Term 83 (EL2024)" - node: 2 - position: 30 - - type: "EL2024" - name: "Term 84 (EL2024)" - node: 2 - position: 31 - - # EL1084 8-channel digital input terminals - - type: "EL1084" - name: "Term 85 (EL1084)" - node: 2 - position: 32 - - type: "EL1084" - name: "Term 86 (EL1084)" - node: 2 - position: 33 - - type: "EL1084" - name: "Term 87 (EL1084)" - node: 2 - position: 34 - - type: "EL1084" - name: "Term 88 (EL1084)" - node: 2 - position: 35 - - type: "EL1084" - name: "Term 89 (EL1084)" - node: 2 - position: 36 - - type: "EL1084" - name: "Term 90 (EL1084)" - node: 2 - position: 37 - - # Node 3 - Third EK1100 coupler with EL1502 counters - - type: "EK1100" - name: "Term 91 (EK1100)" - node: 3 - position: 0 - - # EL1502 counter terminals on node 3 - - type: "EL1502" - name: "Term 92 (EL1502)" - node: 3 - position: 1 - - type: "EL1502" - name: "Term 93 (EL1502)" - node: 3 - position: 2 - - type: "EL1502" - name: "Term 94 (EL1502)" - node: 3 - position: 3 - - type: "EL1502" - name: "Term 95 (EL1502)" - node: 3 - position: 4 - - type: "EL1502" - name: "Term 96 (EL1502)" - node: 3 - position: 5 - - type: "EL1502" - name: "Term 97 (EL1502)" - node: 3 - position: 6 - - type: "EL1502" - name: "Term 98 (EL1502)" - node: 3 - position: 7 - - type: "EL1502" - name: "Term 99 (EL1502)" - node: 3 - position: 8 - - type: "EL1502" - name: "Term 100 (EL1502)" - node: 3 - position: 9 - - type: "EL1502" - name: "Term 101 (EL1502)" - node: 3 - position: 10 - - type: "EL1502" - name: "Term 102 (EL1502)" - node: 3 - position: 11 - - type: "EL1502" - name: "Term 103 (EL1502)" - node: 3 - position: 12 - - type: "EL1502" - name: "Term 104 (EL1502)" - node: 3 - position: 13 - - type: "EL1502" - name: "Term 105 (EL1502)" - node: 3 - position: 14 - - type: "EL1502" - name: "Term 106 (EL1502)" - node: 3 - position: 15 diff --git a/tests/ads_sim/server_config_CX7000_cs1.yaml b/tests/ads_sim/server_config_CX7000_cs1.yaml new file mode 100644 index 00000000..4ad77a39 --- /dev/null +++ b/tests/ads_sim/server_config_CX7000_cs1.yaml @@ -0,0 +1,109 @@ +# EtherCAT Chain Configuration for ADS Simulation Server +# ======================================================== +# +# Auto-generated from hardware at 172.23.242.39 +# + +server: + name: I/O Server + version: '3.1' + build: 2103 +devices: +- id: 1 + name: Device 1 (EtherCAT) + type: 94 + netid: 5.108.227.22.2.1 + identity: + vendor_id: 2 + product_code: 65539 + revision_number: 1844 + serial_number: 0 + slaves: + - type: EK1110 + name: Term 2 (EK1110) + node: 0 + position: 0 + - type: EK1100 + name: Term 3 (EK1100) + node: 1 + position: 0 + - type: EL3702 + name: Term 4 (EL3702) + node: 1 + position: 1 + - type: EL3104 + name: Term 5 (EL3104) + node: 1 + position: 2 + - type: EL3602 + name: Term 6 (EL3602) + node: 1 + position: 3 + - type: EL9410 + name: Term 7 (EL9410) + node: 1 + position: 4 + - type: ELM3704-0000 + name: Term 8 (ELM3704-0000) + node: 1 + position: 5 + - type: EK1100 + name: Term 9 (EK1100) + node: 2 + position: 0 + - type: EL3104 + name: Term 10 (EL3104) + node: 2 + position: 1 + - type: EL4134 + name: Term 11 (EL4134) + node: 2 + position: 2 + - type: EL2024-0010 + name: Term 12 (EL2024-0010) + node: 2 + position: 3 + - type: EL1014 + name: Term 13 (EL1014) + node: 2 + position: 4 + - type: EL9410 + name: Term 14 (EL9410) + node: 2 + position: 5 + - type: EL3602 + name: Term 15 (EL3602) + node: 2 + position: 6 + - type: EL3602 + name: Term 16 (EL3602) + node: 2 + position: 7 + - type: EL3602 + name: Term 17 (EL3602) + node: 2 + position: 8 + - type: EL3602 + name: Term 18 (EL3602) + node: 2 + position: 9 + - type: EL3602 + name: Term 19 (EL3602) + node: 2 + position: 10 + - type: EL3602 + name: Term 20 (EL3602) + node: 2 + position: 11 + - type: EL3602 + name: Term 21 (EL3602) + node: 2 + position: 12 + - type: EL3602 + name: Term 22 (EL3602) + node: 2 + position: 13 + - type: EL3602 + name: Term 23 (EL3602) + node: 2 + position: 14 diff --git a/tests/ads_sim/server_config_CX7000_cs2.yaml b/tests/ads_sim/server_config_CX7000_cs2.yaml new file mode 100644 index 00000000..290fb727 --- /dev/null +++ b/tests/ads_sim/server_config_CX7000_cs2.yaml @@ -0,0 +1,437 @@ +# EtherCAT Chain Configuration for ADS Simulation Server +# ======================================================== +# +# Auto-generated from hardware at 172.23.242.42 +# + +server: + name: I/O Server + version: '3.1' + build: 2103 +devices: +- id: 1 + name: Device 1 (EtherCAT) + type: 94 + netid: 5.166.203.208.2.1 + identity: + vendor_id: 2 + product_code: 65539 + revision_number: 1832 + serial_number: 0 + slaves: + - type: EK1110 + name: Term 2 (EK1110) + node: 0 + position: 0 + - type: EK1100 + name: Term 3 (EK1100) + node: 1 + position: 0 + - type: EL2024 + name: Term 4 (EL2024) + node: 1 + position: 1 + - type: EL2024 + name: Term 5 (EL2024) + node: 1 + position: 2 + - type: EL2024 + name: Term 6 (EL2024) + node: 1 + position: 3 + - type: EL2024 + name: Term 7 (EL2024) + node: 1 + position: 4 + - type: EL2024 + name: Term 8 (EL2024) + node: 1 + position: 5 + - type: EL2024 + name: Term 9 (EL2024) + node: 1 + position: 6 + - type: EL2024 + name: Term 10 (EL2024) + node: 1 + position: 7 + - type: EL2024 + name: Term 11 (EL2024) + node: 1 + position: 8 + - type: EL2024 + name: Term 12 (EL2024) + node: 1 + position: 9 + - type: EL2024 + name: Term 13 (EL2024) + node: 1 + position: 10 + - type: EL2024 + name: Term 14 (EL2024) + node: 1 + position: 11 + - type: EL2024 + name: Term 15 (EL2024) + node: 1 + position: 12 + - type: EL2024 + name: Term 16 (EL2024) + node: 1 + position: 13 + - type: EL2024 + name: Term 17 (EL2024) + node: 1 + position: 14 + - type: EL2024 + name: Term 18 (EL2024) + node: 1 + position: 15 + - type: EL2024 + name: Term 19 (EL2024) + node: 1 + position: 16 + - type: EL9410 + name: Term 20 (EL9410) + node: 1 + position: 17 + - type: EL2024 + name: Term 21 (EL2024) + node: 1 + position: 18 + - type: EL2024 + name: Term 22 (EL2024) + node: 1 + position: 19 + - type: EL2024 + name: Term 23 (EL2024) + node: 1 + position: 20 + - type: EL2024 + name: Term 24 (EL2024) + node: 1 + position: 21 + - type: EL2024 + name: Term 25 (EL2024) + node: 1 + position: 22 + - type: EL2024 + name: Term 26 (EL2024) + node: 1 + position: 23 + - type: EL2024 + name: Term 27 (EL2024) + node: 1 + position: 24 + - type: EL2024 + name: Term 28 (EL2024) + node: 1 + position: 25 + - type: EL2024 + name: Term 29 (EL2024) + node: 1 + position: 26 + - type: EL2024 + name: Term 30 (EL2024) + node: 1 + position: 27 + - type: EL2024 + name: Term 31 (EL2024) + node: 1 + position: 28 + - type: EL2024 + name: Term 32 (EL2024) + node: 1 + position: 29 + - type: EL2024 + name: Term 33 (EL2024) + node: 1 + position: 30 + - type: EL2024 + name: Term 34 (EL2024) + node: 1 + position: 31 + - type: EL2024 + name: Term 35 (EL2024) + node: 1 + position: 32 + - type: EL2024 + name: Term 36 (EL2024) + node: 1 + position: 33 + - type: EL9410 + name: Term 37 (EL9410) + node: 1 + position: 34 + - type: EL2024 + name: Term 38 (EL2024) + node: 1 + position: 35 + - type: EL2024 + name: Term 39 (EL2024) + node: 1 + position: 36 + - type: EL2024 + name: Term 40 (EL2024) + node: 1 + position: 37 + - type: EL2024 + name: Term 41 (EL2024) + node: 1 + position: 38 + - type: EL2024 + name: Term 42 (EL2024) + node: 1 + position: 39 + - type: EL2024 + name: Term 43 (EL2024) + node: 1 + position: 40 + - type: EL2024 + name: Term 44 (EL2024) + node: 1 + position: 41 + - type: EL2024 + name: Term 45 (EL2024) + node: 1 + position: 42 + - type: EL2024 + name: Term 46 (EL2024) + node: 1 + position: 43 + - type: EL2024 + name: Term 47 (EL2024) + node: 1 + position: 44 + - type: EL2024 + name: Term 48 (EL2024) + node: 1 + position: 45 + - type: EL2024 + name: Term 49 (EL2024) + node: 1 + position: 46 + - type: EL2024 + name: Term 50 (EL2024) + node: 1 + position: 47 + - type: EL2024 + name: Term 51 (EL2024) + node: 1 + position: 48 + - type: EL2024 + name: Term 52 (EL2024) + node: 1 + position: 49 + - type: EK1100 + name: Term 53 (EK1100) + node: 2 + position: 0 + - type: EL1014 + name: Term 54 (EL1014) + node: 2 + position: 1 + - type: EL1014 + name: Term 55 (EL1014) + node: 2 + position: 2 + - type: EL1014 + name: Term 56 (EL1014) + node: 2 + position: 3 + - type: EL1014 + name: Term 57 (EL1014) + node: 2 + position: 4 + - type: EL1014 + name: Term 58 (EL1014) + node: 2 + position: 5 + - type: EL1014 + name: Term 59 (EL1014) + node: 2 + position: 6 + - type: EL1014 + name: Term 60 (EL1014) + node: 2 + position: 7 + - type: EL1014 + name: Term 61 (EL1014) + node: 2 + position: 8 + - type: EL1014 + name: Term 62 (EL1014) + node: 2 + position: 9 + - type: EL1014 + name: Term 63 (EL1014) + node: 2 + position: 10 + - type: EL1014 + name: Term 64 (EL1014) + node: 2 + position: 11 + - type: EL1502 + name: Term 65 (EL1502) + node: 2 + position: 12 + - type: EL1502 + name: Term 66 (EL1502) + node: 2 + position: 13 + - type: EL1502 + name: Term 67 (EL1502) + node: 2 + position: 14 + - type: EL1502 + name: Term 68 (EL1502) + node: 2 + position: 15 + - type: EL1502 + name: Term 69 (EL1502) + node: 2 + position: 16 + - type: EL1502 + name: Term 70 (EL1502) + node: 2 + position: 17 + - type: EL1004 + name: Term 71 (EL1004) + node: 2 + position: 18 + - type: EL9410 + name: Term 72 (EL9410) + node: 2 + position: 19 + - type: EL2024 + name: Term 73 (EL2024) + node: 2 + position: 20 + - type: EL2024 + name: Term 74 (EL2024) + node: 2 + position: 21 + - type: EL2024 + name: Term 75 (EL2024) + node: 2 + position: 22 + - type: EL2024 + name: Term 76 (EL2024) + node: 2 + position: 23 + - type: EL2024 + name: Term 77 (EL2024) + node: 2 + position: 24 + - type: EL2024 + name: Term 78 (EL2024) + node: 2 + position: 25 + - type: EL2024 + name: Term 79 (EL2024) + node: 2 + position: 26 + - type: EL2024 + name: Term 80 (EL2024) + node: 2 + position: 27 + - type: EL2024 + name: Term 81 (EL2024) + node: 2 + position: 28 + - type: EL2024 + name: Term 82 (EL2024) + node: 2 + position: 29 + - type: EL2024 + name: Term 83 (EL2024) + node: 2 + position: 30 + - type: EL2024 + name: Term 84 (EL2024) + node: 2 + position: 31 + - type: EL1084 + name: Term 85 (EL1084) + node: 2 + position: 32 + - type: EL1084 + name: Term 86 (EL1084) + node: 2 + position: 33 + - type: EL1084 + name: Term 87 (EL1084) + node: 2 + position: 34 + - type: EL1084 + name: Term 88 (EL1084) + node: 2 + position: 35 + - type: EL1084 + name: Term 89 (EL1084) + node: 2 + position: 36 + - type: EL1084 + name: Term 90 (EL1084) + node: 2 + position: 37 + - type: EK1100 + name: Term 91 (EK1100) + node: 3 + position: 0 + - type: EL1502 + name: Term 93 (EL1502) + node: 3 + position: 1 + - type: EL1502 + name: Term 94 (EL1502) + node: 3 + position: 2 + - type: EL1502 + name: Term 95 (EL1502) + node: 3 + position: 3 + - type: EL1502 + name: Term 96 (EL1502) + node: 3 + position: 4 + - type: EL1502 + name: Term 97 (EL1502) + node: 3 + position: 5 + - type: EL1502 + name: Term 98 (EL1502) + node: 3 + position: 6 + - type: EL1502 + name: Term 99 (EL1502) + node: 3 + position: 7 + - type: EL1502 + name: Term 100 (EL1502) + node: 3 + position: 8 + - type: EL1502 + name: Term 101 (EL1502) + node: 3 + position: 9 + - type: EL1502 + name: Term 102 (EL1502) + node: 3 + position: 10 + - type: EL1502 + name: Term 103 (EL1502) + node: 3 + position: 11 + - type: EL1502 + name: Term 104 (EL1502) + node: 3 + position: 12 + - type: EL1502 + name: Term 105 (EL1502) + node: 3 + position: 13 + - type: EL1502 + name: Term 106 (EL1502) + node: 3 + position: 14 diff --git a/tests/ads_sim/server_config_CX8290_cs1.yaml b/tests/ads_sim/server_config_CX8290_cs1.yaml new file mode 100644 index 00000000..c29ea383 --- /dev/null +++ b/tests/ads_sim/server_config_CX8290_cs1.yaml @@ -0,0 +1,77 @@ +# EtherCAT Chain Configuration for ADS Simulation Server +# ======================================================== +# +# Auto-generated from hardware at 172.23.242.40 +# + +server: + name: I/O Server + version: '3.1' + build: 2103 +devices: +- id: 2 + name: Device 2 (EtherCAT) + type: 94 + netid: 5.163.234.5.3.1 + identity: + vendor_id: 2 + product_code: 65539 + revision_number: 1845 + serial_number: 0 + slaves: + - type: EK1110 + name: Term 2 (EK1110) + node: 0 + position: 0 + - type: EK1100 + name: Term 3 (EK1100) + node: 1 + position: 0 + - type: EL3602 + name: Term 4 (EL3602) + node: 1 + position: 1 + - type: EL3602 + name: Term 5 (EL3602) + node: 1 + position: 2 + - type: EL3602 + name: Term 6 (EL3602) + node: 1 + position: 3 + - type: EL3602 + name: Term 7 (EL3602) + node: 1 + position: 4 + - type: EL3602 + name: Term 8 (EL3602) + node: 1 + position: 5 + - type: EL9410 + name: Term 9 (EL9410) + node: 1 + position: 6 + - type: EL3702 + name: Term 10 (EL3702) + node: 1 + position: 7 + - type: EL3702 + name: Term 11 (EL3702) + node: 1 + position: 8 + - type: EK1100 + name: Term 13 (EK1100) + node: 2 + position: 0 + - type: EL3104 + name: Term 14 (EL3104) + node: 2 + position: 1 + - type: EL3104 + name: Term 15 (EL3104) + node: 2 + position: 2 + - type: EL3104 + name: Term 16 (EL3104) + node: 2 + position: 3 diff --git a/tests/diagnose_hardware.py b/tests/diagnose_hardware.py index 7d0d97bb..775c959b 100755 --- a/tests/diagnose_hardware.py +++ b/tests/diagnose_hardware.py @@ -20,6 +20,7 @@ from pathlib import Path from typing import Any +import numpy as np import yaml # Add src to path for imports @@ -39,6 +40,34 @@ SIMULATOR_AVAILABLE = False +# Add numpy type converters for YAML serialization +def numpy_int_representer(dumper, data): + """Convert numpy integers to Python int.""" + return dumper.represent_int(int(data)) + + +def numpy_float_representer(dumper, data): + """Convert numpy floats to Python float.""" + return dumper.represent_float(float(data)) + + +# Register numpy type handlers +for np_type in [ + np.int8, + np.int16, + np.int32, + np.int64, + np.uint8, + np.uint16, + np.uint32, + np.uint64, +]: + yaml.add_representer(np_type, numpy_int_representer) + +for np_type in [np.float16, np.float32, np.float64]: + yaml.add_representer(np_type, numpy_float_representer) + + def generate_yaml_config( ioserver: IOServer, devices: dict[Any, IODevice], @@ -50,12 +79,16 @@ def generate_yaml_config( devices: Dictionary of discovered EtherCAT devices. Returns: - Dictionary suitable for YAML serialization matching server_config.yaml format. + Dictionary suitable for YAML serialization matching + server_config_CX7000_cs2.yaml format. """ + # Convert version string: replace hyphens with dots for proper version format + version_str = str(ioserver.version).replace("-", ".") + config: dict[str, Any] = { "server": { - "name": ioserver.name, - "version": ioserver.version, + "name": str(ioserver.name), + "version": version_str, "build": int(ioserver.build), }, "devices": [], @@ -67,12 +100,12 @@ def generate_yaml_config( current_node = -1 for slave in device.slaves: - node = slave.loc_in_chain.node - position = slave.loc_in_chain.position + node = int(slave.loc_in_chain.node) + position = int(slave.loc_in_chain.position) slave_entry: dict[str, Any] = { - "type": slave.type, - "name": slave.name, + "type": str(slave.type), + "name": str(slave.name), "node": node, "position": position, } @@ -82,17 +115,19 @@ def generate_yaml_config( if node != current_node: current_node = node - device_type = ( - device.type.value if hasattr(device.type, "value") else int(device.type) + # Ensure device.type is a native Python int + device_type = int( + device.type.value if hasattr(device.type, "value") else device.type ) + device_config: dict[str, Any] = { "id": int(device_id), - "name": device.name, + "name": str(device.name), "type": device_type, "netid": str(device.netid), "identity": { "vendor_id": int(device.identity.vendor_id), - "product_code": hex(int(device.identity.product_code)), + "product_code": int(device.identity.product_code), "revision_number": int(device.identity.revision_number), "serial_number": int(device.identity.serial_number), }, @@ -331,7 +366,14 @@ async def diagnose_hardware( f.write("#\n") f.write(f"# Auto-generated from hardware at {ip}\n") f.write("#\n\n") - yaml.dump(config, f, default_flow_style=False, sort_keys=False) + yaml.dump( + config, + f, + default_flow_style=False, + sort_keys=False, + allow_unicode=True, + width=float("inf"), + ) print(f"\n YAML configuration written to: {output_path}") @@ -379,7 +421,7 @@ def main() -> None: default=None, help=( "Simulator config YAML to compare against hardware " - "(e.g., tests/ads_sim/server_config.yaml)" + "(e.g., tests/ads_sim/erver_config_CX7000_cs2.yaml)" ), ) parser.add_argument( diff --git a/tests/test_system.py b/tests/test_system.py index f0c1865e..dbd1c199 100644 --- a/tests/test_system.py +++ b/tests/test_system.py @@ -30,6 +30,14 @@ from ads_sim.ethercat_chain import EtherCATChain +# List of ADS simulator YAML config files to test against. +# Add or remove files here to control which configurations are tested. +SIMULATOR_CONFIG_FILES: list[str] = [ + "server_config_CX7000_cs1.yaml", + "server_config_CX7000_cs2.yaml", + "server_config_CX8290_cs1.yaml", +] + # To enable debug logging # instead of doing this use `pytest --log-cli-level=DEBUG` @@ -47,19 +55,18 @@ def _is_port_in_use(port: int, host: str = "127.0.0.1") -> bool: return False -@pytest.fixture(scope="session") -def expected_chain() -> EtherCATChain: +@pytest.fixture(scope="function") +def expected_chain(config_file: str) -> EtherCATChain: """Load and return the expected EtherCAT chain configuration.""" - config_path = Path(__file__).parent / "ads_sim" / "server_config.yaml" + config_path = Path(__file__).parent / "ads_sim" / config_file return EtherCATChain(config_path) -@pytest.fixture(scope="session") -def simulator_process(request): +@pytest.fixture(scope="function") +def simulator_process(request, config_file: str): """Launch the ADS simulator and return pexpect child process. - This is a session-scoped fixture so the simulator is started once - and shared across all tests in the session. + This is a function-scoped fixture so each test config gets its own simulator. If --external-simulator flag is passed, this fixture will not launch a simulator but instead assume one is already running externally. @@ -84,11 +91,16 @@ def simulator_process(request): "Stop it before running tests or use --external-simulator flag." ) + # Get the config file path + config_path = Path(__file__).parent / "ads_sim" / config_file + # Launch the simulator subprocess with verbose logging cmd = [ sys.executable, "-m", "tests.ads_sim", + "--config", + str(config_path), "--log-level", "INFO", "--disable-notifications", @@ -151,12 +163,13 @@ def simulator_process(request): @pytest_asyncio.fixture(scope="function") -async def fastcs_catio_controller(simulator_process): +async def fastcs_catio_controller(simulator_process, config_file: str): """Create fastcs-catio controller and test basic connection. This fixture depends on simulator_process to ensure the simulator is running first. Note: We only test connection, not full initialization which hangs. """ + _ = config_file # Used by dependent fixtures from fastcs_catio.catio_controller import CATioServerController from fastcs_catio.client import RemoteRoute @@ -212,8 +225,9 @@ class TestFastcsCatioConnection: """Test fastcs-catio IOC connection to simulator.""" @pytest.mark.asyncio + @pytest.mark.parametrize("config_file", SIMULATOR_CONFIG_FILES) async def test_ioc_connects_and_discovers_symbols( - self, fastcs_catio_controller, expected_chain: EtherCATChain + self, fastcs_catio_controller, expected_chain: EtherCATChain, config_file: str ): """Test that fastcs-catio IOC connects to the simulator. @@ -255,8 +269,9 @@ async def test_ioc_connects_and_discovers_symbols( ) @pytest.mark.asyncio + @pytest.mark.parametrize("config_file", SIMULATOR_CONFIG_FILES) async def test_discovered_terminals_match_yaml_config( - self, fastcs_catio_controller, expected_chain: EtherCATChain + self, fastcs_catio_controller, expected_chain: EtherCATChain, config_file: str ): """Test that discovered EtherCAT terminals match the YAML configuration. From 0201368d207f2dab123fb7b1a29f3ab58a3cd6c8 Mon Sep 17 00:00:00 2001 From: giles knap Date: Thu, 5 Feb 2026 14:00:54 +0000 Subject: [PATCH 2/2] add troubleshooting re simulator port in use --- AGENTS.md | 2 ++ tests/test_system.py | 10 +--------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8645f6d5..4b3b1dfe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/tests/test_system.py b/tests/test_system.py index dbd1c199..97cfc41d 100644 --- a/tests/test_system.py +++ b/tests/test_system.py @@ -82,16 +82,8 @@ def simulator_process(request, config_file: str): # No cleanup needed return - # Check if simulator port is already in use - simulator_port = 48898 # ADS_TCP_PORT - if _is_port_in_use(simulator_port): - pytest.fail( - f"Port {simulator_port} is already in use. " - "A simulator may already be running. " - "Stop it before running tests or use --external-simulator flag." - ) - # Get the config file path + simulator_port = 48898 # ADS_TCP_PORT config_path = Path(__file__).parent / "ads_sim" / config_file # Launch the simulator subprocess with verbose logging