pytmc 2.18.2__tar.gz → 2.19.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pytmc-2.19.0/.github/CODEOWNERS +27 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/.gitmodules +3 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/.pre-commit-config.yaml +3 -3
- {pytmc-2.18.2/pytmc.egg-info → pytmc-2.19.0}/PKG-INFO +3 -2
- {pytmc-2.18.2 → pytmc-2.19.0}/docs/source/release_notes.rst +10 -0
- pytmc-2.19.0/pytmc/_version.py +34 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/bin/template.py +150 -32
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/parser.py +145 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/pragmas.py +7 -39
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/test_motion.py +2 -3
- pytmc-2.19.0/pytmc/tests/tmc_files/motion_abstraction_sample.tmc +718 -0
- {pytmc-2.18.2 → pytmc-2.19.0/pytmc.egg-info}/PKG-INFO +3 -2
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc.egg-info/SOURCES.txt +2 -0
- pytmc-2.18.2/pytmc/_version.py +0 -16
- {pytmc-2.18.2 → pytmc-2.19.0}/.coveragerc +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/.flake8 +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/.git_archival.txt +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/.gitattributes +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/.github/workflows/standard.yml +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/.gitignore +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/.landscape.yml +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/AUTHORS.rst +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/CONTRIBUTING.rst +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/LICENSE +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/MANIFEST.in +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/README.rst +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/conda-recipe/meta.yaml +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/dev-requirements.txt +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/docs/Makefile +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/docs/source/_static/.gitkeep +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/docs/source/conf.py +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/docs/source/getting_started.rst +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/docs/source/index.rst +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/docs/source/installation.rst +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/docs/source/pragma_usage.rst +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/docs/source/source_code.rst +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/docs/source/terminal_usage.rst +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/docs/source/user_guide.rst +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/docs-requirements.txt +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pyproject.toml +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/__init__.py +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/__main__.py +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/beckhoff.py +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/bin/__init__.py +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/bin/code.py +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/bin/db.py +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/bin/debug.py +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/bin/iocboot.py +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/bin/pragmalint.py +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/bin/pytmc.py +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/bin/stcmd.py +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/bin/summary.py +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/bin/types.py +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/bin/util.py +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/bin/xmltranslate.py +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/code.py +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/default_settings/__init__.py +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/default_settings/conf.ini +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/default_settings/unified_ordered_field_list.py +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/defaults.py +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/linter.py +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/record.py +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/templates/EPICS_proto_template.proto +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/templates/asyn_standard_file.jinja2 +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/templates/asyn_standard_record.jinja2 +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/templates/stcmd_default.cmd +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/__init__.py +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/ads.dbd +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/conftest.py +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/static_routes.xml +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/templates/basic_test.txt +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/templates/smoke_test.txt +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/test_archive.py +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/test_commandline.py +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/test_integrations.py +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/test_lint.py +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/test_parsing.py +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/test_project.py +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/test_record.py +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/test_xml_collector.py +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/test_xml_obj.py +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/tmc_files/ArbiterPLC.tmc +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/tmc_files/tc_mot_example.tmc +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/tmc_files/xtes_sxr_plc.tmc +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/validation/__init__.py +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/validation/v0.1.py +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/version.py +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc.egg-info/dependency_links.txt +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc.egg-info/entry_points.txt +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc.egg-info/requires.txt +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/pytmc.egg-info/top_level.txt +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/requirements.txt +0 -0
- {pytmc-2.18.2 → pytmc-2.19.0}/setup.cfg +0 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# default group
|
|
2
|
+
* @pcdshub/python-reviewers @pcdshub/twincat-reviewers
|
|
3
|
+
|
|
4
|
+
# language-specific group(s)
|
|
5
|
+
## PYTHON files
|
|
6
|
+
*.py @pcdshub/python-reviewers
|
|
7
|
+
*.pyi @pcdshub/python-reviewers
|
|
8
|
+
*.pyc @pcdshub/python-reviewers
|
|
9
|
+
*.pyw @pcdshub/python-reviewers
|
|
10
|
+
*.pyx @pcdshub/python-reviewers
|
|
11
|
+
*.pyd @pcdshub/python-reviewers
|
|
12
|
+
## TWINCAT files
|
|
13
|
+
*.tsproj @pcdshub/twincat-reviewers
|
|
14
|
+
*.plcproj @pcdshub/twincat-reviewers
|
|
15
|
+
*.tmc @pcdshub/twincat-reviewers
|
|
16
|
+
*.tpr @pcdshub/twincat-reviewers
|
|
17
|
+
*.xti @pcdshub/twincat-reviewers
|
|
18
|
+
*.TcTTO @pcdshub/twincat-reviewers
|
|
19
|
+
*.TcPOU @pcdshub/twincat-reviewers
|
|
20
|
+
*.TcDUT @pcdshub/twincat-reviewers
|
|
21
|
+
*.TcGVL @pcdshub/twincat-reviewers
|
|
22
|
+
*.TcVis @pcdshub/twincat-reviewers
|
|
23
|
+
*.TcVMO @pcdshub/twincat-reviewers
|
|
24
|
+
*.TcGTLO @pcdshub/twincat-reviewers
|
|
25
|
+
|
|
26
|
+
# github folder holds administrative files
|
|
27
|
+
.github/** @pcdshub/software-admin
|
|
@@ -16,3 +16,6 @@
|
|
|
16
16
|
[submodule "pytmc/tests/projects/lcls-plc-lfe-motion"]
|
|
17
17
|
path = pytmc/tests/projects/lcls-plc-lfe-motion
|
|
18
18
|
url = https://github.com/pcdshub/lcls-plc-lfe-motion
|
|
19
|
+
[submodule "pytmc/tests/projects/lcls-plc-motion-abstraction-sample"]
|
|
20
|
+
path = pytmc/tests/projects/lcls-plc-motion-abstraction-sample
|
|
21
|
+
url = https://github.com/pcdshub/lcls-plc-motion-abstraction-sample.git
|
|
@@ -9,7 +9,7 @@ exclude: |
|
|
|
9
9
|
|
|
10
10
|
repos:
|
|
11
11
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
12
|
-
rev:
|
|
12
|
+
rev: v6.0.0
|
|
13
13
|
hooks:
|
|
14
14
|
- id: no-commit-to-branch
|
|
15
15
|
- id: trailing-whitespace
|
|
@@ -26,11 +26,11 @@ repos:
|
|
|
26
26
|
exclude: '^(pytmc/bin/util.py)$'
|
|
27
27
|
|
|
28
28
|
- repo: https://github.com/pycqa/flake8.git
|
|
29
|
-
rev:
|
|
29
|
+
rev: 7.3.0
|
|
30
30
|
hooks:
|
|
31
31
|
- id: flake8
|
|
32
32
|
|
|
33
33
|
- repo: https://github.com/timothycrosley/isort
|
|
34
|
-
rev:
|
|
34
|
+
rev: 7.0.0
|
|
35
35
|
hooks:
|
|
36
36
|
- id: isort
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: pytmc
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.19.0
|
|
4
4
|
Summary: Generate Epics DB records from TwinCAT .tmc files
|
|
5
5
|
Author: SLAC National Accelerator Laboratory
|
|
6
6
|
License: Copyright (c) 2023, The Board of Trustees of the Leland Stanford Junior
|
|
@@ -66,6 +66,7 @@ Requires-Dist: sphinx-argparse; extra == "doc"
|
|
|
66
66
|
Requires-Dist: sphinx-autodoc-typehints; extra == "doc"
|
|
67
67
|
Requires-Dist: sphinx_rtd_theme>=1.2.0; extra == "doc"
|
|
68
68
|
Requires-Dist: sphinxcontrib-websupport; extra == "doc"
|
|
69
|
+
Dynamic: license-file
|
|
69
70
|
|
|
70
71
|
PYTMC
|
|
71
72
|
=====
|
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
Release History
|
|
3
3
|
=================
|
|
4
4
|
|
|
5
|
+
v2.19.0 (2025-10-28)
|
|
6
|
+
====================
|
|
7
|
+
|
|
8
|
+
Changes
|
|
9
|
+
-------
|
|
10
|
+
- Add support for non-EtherCATMC type motors.
|
|
11
|
+
This is part of an PLC/IOC motion improvement project to improve the reliability
|
|
12
|
+
of our motion devices by avoiding custom motor record builds and related issues.
|
|
13
|
+
|
|
14
|
+
|
|
5
15
|
v2.18.2 (2025-02-12)
|
|
6
16
|
====================
|
|
7
17
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '2.19.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (2, 19, 0)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = 'g234728430'
|
|
@@ -27,6 +27,8 @@ The following helpers are available in the environment::
|
|
|
27
27
|
get_links
|
|
28
28
|
get_linter_results
|
|
29
29
|
get_motors
|
|
30
|
+
get_legacy_motors
|
|
31
|
+
get_stream_motors
|
|
30
32
|
get_nc
|
|
31
33
|
get_plc_by_name
|
|
32
34
|
get_pou_call_blocks
|
|
@@ -57,7 +59,7 @@ import os
|
|
|
57
59
|
import pathlib
|
|
58
60
|
import sys
|
|
59
61
|
from collections.abc import Generator
|
|
60
|
-
from typing import Any, Optional, Union
|
|
62
|
+
from typing import Any, List, Optional, Union
|
|
61
63
|
|
|
62
64
|
import jinja2
|
|
63
65
|
|
|
@@ -312,15 +314,101 @@ def get_symbols_by_type(plc: parser.Plc) -> dict[str, list[parser.Symbol]]:
|
|
|
312
314
|
return parser.separate_by_classname(symbols)
|
|
313
315
|
|
|
314
316
|
|
|
317
|
+
def has_axis_link(stage: parser.Symbol_FB_MotionStage) -> bool:
|
|
318
|
+
"""
|
|
319
|
+
Only valid for Symbol_FB_MotionStage (or alias); checks for axis-link pragma.
|
|
320
|
+
Returns True if an 'axis-link:' pragma is found, else False.
|
|
321
|
+
"""
|
|
322
|
+
# Accept both direct class and known aliases for compatibility:
|
|
323
|
+
if not (isinstance(stage, parser.Symbol_FB_MotionStage) or
|
|
324
|
+
set(getattr(stage, "symbol_aliases", [])) & {'Symbol_FB_MotionStageNC', 'Symbol_FB_MotionStageNCDS402', 'Symbol_FB_MotionStageMCS2'}):
|
|
325
|
+
return False # Not a supported FB_MotionStage type; skip
|
|
326
|
+
|
|
327
|
+
for pragma in parser.get_pragma(stage, name="pytmc"):
|
|
328
|
+
for line in pragma.splitlines():
|
|
329
|
+
if line.strip().startswith("axis-link:"):
|
|
330
|
+
return True
|
|
331
|
+
return False
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@functools.lru_cache
|
|
335
|
+
def get_legacy_motors(plc: parser.Plc) -> list[parser.Symbol_ST_MotionStage]:
|
|
336
|
+
"""
|
|
337
|
+
Find all valid legacy motors (ST_MotionStage) in the provided PLC.
|
|
338
|
+
|
|
339
|
+
Criteria:
|
|
340
|
+
- The symbol is of type Symbol_ST_MotionStage.
|
|
341
|
+
- The symbol is not a pointer (i.e., is_pointer is False).
|
|
342
|
+
- The symbol has a pytmc pragma (parser.has_pragma is True).
|
|
343
|
+
|
|
344
|
+
For each returned motor, a dynamic attribute 'is_legacy_motor' is set to True,
|
|
345
|
+
enabling template logic or further processing to easily distinguish legacy motors.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
plc (parser.Plc): The parsed PLC object to examine.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
list[parser.Symbol_ST_MotionStage]:
|
|
352
|
+
List of all valid legacy motor symbols from this PLC.
|
|
353
|
+
"""
|
|
354
|
+
symbols = get_symbols_by_type(plc)
|
|
355
|
+
st_motors = symbols.get("Symbol_ST_MotionStage", [])
|
|
356
|
+
legacy = []
|
|
357
|
+
for stage in st_motors:
|
|
358
|
+
if (
|
|
359
|
+
not stage.is_pointer
|
|
360
|
+
and parser.has_pragma(stage)
|
|
361
|
+
):
|
|
362
|
+
stage.is_legacy_motor = True
|
|
363
|
+
legacy.append(stage)
|
|
364
|
+
return legacy
|
|
365
|
+
|
|
366
|
+
|
|
315
367
|
@functools.lru_cache
|
|
316
|
-
def
|
|
317
|
-
"""
|
|
368
|
+
def get_stream_motors(plc: parser.Plc) -> list[parser.Symbol_FB_MotionStage]:
|
|
369
|
+
"""
|
|
370
|
+
Find all valid stream (FB_MotionStage) motors in the given PLC.
|
|
371
|
+
|
|
372
|
+
Criteria for inclusion:
|
|
373
|
+
- The symbol is of type Symbol_FB_MotionStage.
|
|
374
|
+
- The symbol is not a pointer (i.e., 'is_pointer' is False).
|
|
375
|
+
- The symbol has a pytmc pragma (parser.has_pragma is True).
|
|
376
|
+
- The symbol has a valid axis-link pragma (checked with has_axis_link(stage)),
|
|
377
|
+
ensuring it maps to a real NC axis and is suitable for EPICS motor creation.
|
|
378
|
+
|
|
379
|
+
For each returned motor symbol, a dynamic attribute 'is_legacy_motor' is set to False,
|
|
380
|
+
distinguishing them from legacy motors during template generation or further logic.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
plc (parser.Plc):
|
|
384
|
+
The parsed TwinCAT PLC object to search for motors.
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
list[parser.Symbol_FB_MotionStage]:
|
|
388
|
+
A list of all valid stream (modern/FB_MotionStage) motor symbols
|
|
389
|
+
in this PLC, ready for StreamDevice-style and per-axis record creation.
|
|
390
|
+
"""
|
|
318
391
|
symbols = get_symbols_by_type(plc)
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if
|
|
323
|
-
|
|
392
|
+
fb_motors = symbols.get("Symbol_FB_MotionStage", [])
|
|
393
|
+
stream = []
|
|
394
|
+
for stage in fb_motors:
|
|
395
|
+
if (
|
|
396
|
+
not stage.is_pointer
|
|
397
|
+
and parser.has_pragma(stage)
|
|
398
|
+
and has_axis_link(stage)
|
|
399
|
+
):
|
|
400
|
+
stage.is_legacy_motor = False
|
|
401
|
+
stream.append(stage)
|
|
402
|
+
return stream
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
@functools.lru_cache
|
|
406
|
+
def get_motors(plc: parser.Plc) -> List[Union[parser.Symbol_ST_MotionStage, parser.Symbol_FB_MotionStage]]:
|
|
407
|
+
"""
|
|
408
|
+
Return all valid motors for the PLC by concatenating legacy and stream (FB) motors.
|
|
409
|
+
Each entry is tagged via is_legacy_motor.
|
|
410
|
+
"""
|
|
411
|
+
return get_legacy_motors(plc) + get_stream_motors(plc)
|
|
324
412
|
|
|
325
413
|
|
|
326
414
|
def get_plc_by_name(
|
|
@@ -670,6 +758,8 @@ helpers = [
|
|
|
670
758
|
get_links,
|
|
671
759
|
get_linter_results,
|
|
672
760
|
get_motors,
|
|
761
|
+
get_legacy_motors,
|
|
762
|
+
get_stream_motors,
|
|
673
763
|
get_nc,
|
|
674
764
|
get_plc_by_name,
|
|
675
765
|
get_symbols,
|
|
@@ -762,6 +852,43 @@ def split_input_output(arg: str) -> tuple[str, str]:
|
|
|
762
852
|
)
|
|
763
853
|
|
|
764
854
|
|
|
855
|
+
def resolve_motors(template_args: dict) -> dict[str, Any]:
|
|
856
|
+
"""
|
|
857
|
+
Given template_args (with macros and 'projects'), resolve the correct motors lists
|
|
858
|
+
for legacy and FB_MotionStage, enforcing that only one family is present.
|
|
859
|
+
Returns a context dict for updating template_args.
|
|
860
|
+
"""
|
|
861
|
+
plc_name = template_args.get("plc", None)
|
|
862
|
+
projects_dict = template_args["projects"]
|
|
863
|
+
|
|
864
|
+
if plc_name is not None:
|
|
865
|
+
project, plc = get_plc_by_name(projects_dict, plc_name)
|
|
866
|
+
if plc is None:
|
|
867
|
+
raise RuntimeError(f"PLC '{plc_name}' not found in loaded projects!")
|
|
868
|
+
else:
|
|
869
|
+
# Default: take first PLC from the first project found (safe for single-PLC projects)
|
|
870
|
+
project = next(iter(projects_dict.values()))
|
|
871
|
+
plcs = list(project.plcs)
|
|
872
|
+
plc = plcs[0]
|
|
873
|
+
|
|
874
|
+
legacy_motors = get_legacy_motors(plc)
|
|
875
|
+
stream_motors = get_stream_motors(plc)
|
|
876
|
+
|
|
877
|
+
if legacy_motors and stream_motors:
|
|
878
|
+
raise RuntimeError(
|
|
879
|
+
f"Both legacy (ST_MotionStage) motors and stream (FB_MotionStage) motors are present. "
|
|
880
|
+
f"Not allowed! Legacy: {[m.name for m in legacy_motors]}, Stream: {[m.name for m in stream_motors]}"
|
|
881
|
+
)
|
|
882
|
+
|
|
883
|
+
motors = legacy_motors if legacy_motors else stream_motors
|
|
884
|
+
|
|
885
|
+
return {
|
|
886
|
+
"legacy_motors": legacy_motors,
|
|
887
|
+
"stream_motors": stream_motors,
|
|
888
|
+
"motors": motors,
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
|
|
765
892
|
def main(
|
|
766
893
|
projects: list[parser.AnyPath],
|
|
767
894
|
templates: Optional[Union[list[str], str]] = None,
|
|
@@ -769,23 +896,17 @@ def main(
|
|
|
769
896
|
debug: bool = False,
|
|
770
897
|
) -> dict[str, str]:
|
|
771
898
|
"""
|
|
772
|
-
Render template(s) based on a TwinCAT3 project, or XML-format file such as
|
|
773
|
-
TMC.
|
|
899
|
+
Render template(s) based on a TwinCAT3 project, or XML-format file such as TMC.
|
|
774
900
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
templates : str or list of str, optional
|
|
781
|
-
Template filename (default: standard input) to output filename
|
|
782
|
-
in the form ``input_filename[{os.pathsep}output_filename]``
|
|
783
|
-
Defaults to '-' (standard input -> standard output)
|
|
901
|
+
Args:
|
|
902
|
+
projects: List of project paths (.sln, .tsproj, .tmc, ...)
|
|
903
|
+
templates: Input template(s) (default: stdin; can be list or single string with [:] for output)
|
|
904
|
+
macros: Dict or list of "MACRO=VALUE" items, e.g., "plc=MyPLC"
|
|
905
|
+
debug: Interactive Python debug after render
|
|
784
906
|
|
|
785
|
-
|
|
786
|
-
|
|
907
|
+
Returns:
|
|
908
|
+
all_rendered: Dict mapping input template to rendered output (string)
|
|
787
909
|
"""
|
|
788
|
-
|
|
789
910
|
macros = macros or {}
|
|
790
911
|
if not isinstance(macros, dict):
|
|
791
912
|
macros = dict(_split_macro(macro) for macro in macros)
|
|
@@ -797,20 +918,17 @@ def main(
|
|
|
797
918
|
templates = [templates]
|
|
798
919
|
|
|
799
920
|
all_rendered = {}
|
|
921
|
+
|
|
800
922
|
for template in templates:
|
|
801
923
|
input_filename, output_filename = split_input_output(template)
|
|
802
924
|
if input_filename == "-":
|
|
803
|
-
# Check if it's an interactive user to warn them what we're doing:
|
|
804
925
|
is_tty = os.isatty(sys.stdin.fileno())
|
|
805
926
|
if is_tty:
|
|
806
927
|
logger.warning("Reading template from standard input...")
|
|
807
928
|
logger.warning("Press ^D on a blank line when done.")
|
|
808
|
-
|
|
809
929
|
template_text = sys.stdin.read()
|
|
810
930
|
if is_tty:
|
|
811
|
-
logger.warning(
|
|
812
|
-
"Read template from standard input (len=%d)", len(template_text)
|
|
813
|
-
)
|
|
931
|
+
logger.warning("Read template from standard input (len=%d)", len(template_text))
|
|
814
932
|
else:
|
|
815
933
|
with open(input_filename) as fp:
|
|
816
934
|
template_text = fp.read()
|
|
@@ -820,6 +938,10 @@ def main(
|
|
|
820
938
|
template_args = get_render_context()
|
|
821
939
|
template_args.update(projects_to_dict(*projects))
|
|
822
940
|
template_args.update(**macros)
|
|
941
|
+
|
|
942
|
+
# updating template_args with motors
|
|
943
|
+
template_args.update(resolve_motors(template_args))
|
|
944
|
+
|
|
823
945
|
rendered = render_template(template_text, template_args)
|
|
824
946
|
except Exception as ex:
|
|
825
947
|
stashed_exception = ex
|
|
@@ -828,7 +950,6 @@ def main(
|
|
|
828
950
|
if not debug:
|
|
829
951
|
if stashed_exception is not None:
|
|
830
952
|
raise stashed_exception
|
|
831
|
-
|
|
832
953
|
if output_filename:
|
|
833
954
|
with open(output_filename, "w") as fp:
|
|
834
955
|
print(rendered, file=fp)
|
|
@@ -837,10 +958,7 @@ def main(
|
|
|
837
958
|
else:
|
|
838
959
|
message = ["Variables: projects, template_text, rendered, template_args. "]
|
|
839
960
|
if stashed_exception is not None:
|
|
840
|
-
message.append(
|
|
841
|
-
f"Exception: {type(stashed_exception)} " f"{stashed_exception}"
|
|
842
|
-
)
|
|
843
|
-
|
|
961
|
+
message.append(f"Exception: {type(stashed_exception)} {stashed_exception}")
|
|
844
962
|
util.python_debug_session(namespace=locals(), message="\n".join(message))
|
|
845
963
|
all_rendered[input_filename] = rendered
|
|
846
964
|
|
|
@@ -1661,6 +1661,151 @@ class Symbol_ST_MotionStage(Symbol):
|
|
|
1661
1661
|
Symbol_DUT_MotionStage = Symbol_ST_MotionStage
|
|
1662
1662
|
|
|
1663
1663
|
|
|
1664
|
+
def has_pragma(item, *, name: str = "pytmc"):
|
|
1665
|
+
"Does `item` have a pragma titled `name`?"
|
|
1666
|
+
|
|
1667
|
+
return any(True for pragma in get_pragma(item, name=name) if pragma is not None)
|
|
1668
|
+
|
|
1669
|
+
|
|
1670
|
+
def get_pragma(
|
|
1671
|
+
item: Union[SubItem, type[Symbol]], *, name: str = "pytmc"
|
|
1672
|
+
) -> Generator[str, None, None]:
|
|
1673
|
+
"""
|
|
1674
|
+
Get all pragmas with a certain tag.
|
|
1675
|
+
|
|
1676
|
+
Parameters
|
|
1677
|
+
----------
|
|
1678
|
+
item : parser.SubItem, parser.Symbol, parser.Symbol subclass
|
|
1679
|
+
Representation of beckhoff variable or data structure
|
|
1680
|
+
|
|
1681
|
+
name : str, optional
|
|
1682
|
+
Accept tmc entries where the <Name> field equals the passed string
|
|
1683
|
+
|
|
1684
|
+
Yields
|
|
1685
|
+
------
|
|
1686
|
+
str
|
|
1687
|
+
|
|
1688
|
+
"""
|
|
1689
|
+
name_list = [name, f"plcAttribute_{name}"]
|
|
1690
|
+
if hasattr(item, "Properties"):
|
|
1691
|
+
properties = item.Properties[0]
|
|
1692
|
+
for prop in getattr(properties, "Property", []):
|
|
1693
|
+
# Return true if any of the names searched for are found
|
|
1694
|
+
if any(indiv_name == prop.name for indiv_name in name_list):
|
|
1695
|
+
yield prop.value
|
|
1696
|
+
|
|
1697
|
+
|
|
1698
|
+
class Symbol_FB_MotionStage(Symbol):
|
|
1699
|
+
"""[TMC] Customized Symbol representing only FB_MotionStage.
|
|
1700
|
+
|
|
1701
|
+
Variants:
|
|
1702
|
+
- Symbol_FB_MotionStageNC
|
|
1703
|
+
- Symbol_FB_MotionStageNCDS402
|
|
1704
|
+
- Symbol_FB_MotionStageMCS2
|
|
1705
|
+
"""
|
|
1706
|
+
symbol_aliases = ['Symbol_FB_MotionStageNC', 'Symbol_FB_MotionStageMCS2', 'Symbol_FB_MotionStageNCDS402']
|
|
1707
|
+
|
|
1708
|
+
def _repr_info(self):
|
|
1709
|
+
repr_info = super()._repr_info()
|
|
1710
|
+
try:
|
|
1711
|
+
repr_info["nc_axis"] = self.nc_axis.name
|
|
1712
|
+
except Exception as ex:
|
|
1713
|
+
repr_info["nc_axis"] = repr(ex)
|
|
1714
|
+
return repr_info
|
|
1715
|
+
|
|
1716
|
+
def get_axis_link(self):
|
|
1717
|
+
"""
|
|
1718
|
+
Returns the first axis-link value from a pytmc pragma (e.g. 'GVL_Axes.Axes[2]').
|
|
1719
|
+
If no pytmc pragma is found, returns None (treat symbol as ignored/simulation-only).
|
|
1720
|
+
Raises RuntimeError if pragma is present but axis-link is missing or blank.
|
|
1721
|
+
"""
|
|
1722
|
+
has_any_pytmc = False
|
|
1723
|
+
for pragma_entry in get_pragma(self, name='pytmc'):
|
|
1724
|
+
has_any_pytmc = True
|
|
1725
|
+
for line in pragma_entry.splitlines():
|
|
1726
|
+
line = line.strip()
|
|
1727
|
+
if line.lower().startswith("axis-link:"):
|
|
1728
|
+
value = line.partition(":")[2].strip()
|
|
1729
|
+
if value:
|
|
1730
|
+
return value
|
|
1731
|
+
if not has_any_pytmc:
|
|
1732
|
+
# No pytmc pragma at all: not an EPICS-exported axis, just skip/return None
|
|
1733
|
+
return None
|
|
1734
|
+
# Had a pytmc pragma but no axis-link: this is a config error
|
|
1735
|
+
raise RuntimeError(f"No valid axis-link pragma found for symbol '{self.name}'")
|
|
1736
|
+
|
|
1737
|
+
@property
|
|
1738
|
+
def ncAxis(self):
|
|
1739
|
+
"""Returns the axis link value, e.g. 'gvl_axes.axes[2]'."""
|
|
1740
|
+
# Note: GVL_Axes.Axes[n] in PLC is usually gvl_axes.axes[n] in NC
|
|
1741
|
+
return self.get_axis_link().lower()
|
|
1742
|
+
|
|
1743
|
+
@property
|
|
1744
|
+
def program_name(self):
|
|
1745
|
+
"""'Main' from 'Main.M1'"""
|
|
1746
|
+
return self.name.split(".")[0]
|
|
1747
|
+
|
|
1748
|
+
@property
|
|
1749
|
+
def motor_name(self):
|
|
1750
|
+
"""'M1' from 'Main.M1'"""
|
|
1751
|
+
return self.name.split(".")[1]
|
|
1752
|
+
|
|
1753
|
+
@property
|
|
1754
|
+
def nc_to_plc_link(self):
|
|
1755
|
+
"""
|
|
1756
|
+
Returns the Link whose 'a[1]' contains the axis-link (and '.NcToPlc').
|
|
1757
|
+
Raises RuntimeError for mismatches or missing self.plc.
|
|
1758
|
+
"""
|
|
1759
|
+
plc = getattr(self, "plc", None)
|
|
1760
|
+
if plc is None:
|
|
1761
|
+
raise RuntimeError(f"Symbol {self.name!r} has no associated PLC (plc attribute is None)")
|
|
1762
|
+
axis_link = self.get_axis_link() # e.g. 'GVL_Axes.Axes[2]'
|
|
1763
|
+
axis_link_lc = axis_link.lower()
|
|
1764
|
+
for link in getattr(plc, "links", []):
|
|
1765
|
+
a1_lc = link.a[1].lower()
|
|
1766
|
+
# Look for axis-link pattern and NcToPlc ending
|
|
1767
|
+
if axis_link_lc in a1_lc and a1_lc.endswith(".nctoplc"):
|
|
1768
|
+
return link
|
|
1769
|
+
# If not found, raise error with details
|
|
1770
|
+
raise RuntimeError(
|
|
1771
|
+
f"No NC-to-PLC link found for axis-link '{axis_link}' ({self.name!r})"
|
|
1772
|
+
)
|
|
1773
|
+
|
|
1774
|
+
@property
|
|
1775
|
+
def nc_axis(self):
|
|
1776
|
+
"""
|
|
1777
|
+
The NC Axis object associated with this FB_MotionStage.
|
|
1778
|
+
"""
|
|
1779
|
+
link = self.nc_to_plc_link
|
|
1780
|
+
if link is None:
|
|
1781
|
+
raise RuntimeError(f"No NC-to-PLC link found for {self.name}")
|
|
1782
|
+
|
|
1783
|
+
parent_name = link.parent.name.split("^")
|
|
1784
|
+
if parent_name[0] == "TINC":
|
|
1785
|
+
parent_name = parent_name[1:]
|
|
1786
|
+
|
|
1787
|
+
if len(parent_name) != 3:
|
|
1788
|
+
raise RuntimeError(f"Unexpected link parent name format: {link.parent.name}")
|
|
1789
|
+
|
|
1790
|
+
task_name, axis_section, axis_name = parent_name
|
|
1791
|
+
|
|
1792
|
+
# Find correct NC from root
|
|
1793
|
+
ncs = [
|
|
1794
|
+
nc for nc in self.root.find(NC, recurse=False)
|
|
1795
|
+
if hasattr(nc, 'SafTask') and nc.SafTask[0].name == task_name
|
|
1796
|
+
]
|
|
1797
|
+
if not ncs:
|
|
1798
|
+
raise RuntimeError(f"No NC with SafTask name {task_name!r} found.")
|
|
1799
|
+
|
|
1800
|
+
nc = ncs[0]
|
|
1801
|
+
try:
|
|
1802
|
+
nc_axis = nc.axis_by_name[axis_name]
|
|
1803
|
+
except KeyError:
|
|
1804
|
+
raise RuntimeError(f"No NC axis named {axis_name!r} found in NC {task_name!r}.")
|
|
1805
|
+
|
|
1806
|
+
return nc_axis
|
|
1807
|
+
|
|
1808
|
+
|
|
1664
1809
|
class GVL(_TwincatProjectSubItem):
|
|
1665
1810
|
"[TcGVL] A Global Variable List"
|
|
1666
1811
|
Declaration: list
|
|
@@ -7,12 +7,14 @@ import itertools
|
|
|
7
7
|
import logging
|
|
8
8
|
import math
|
|
9
9
|
import re
|
|
10
|
-
from collections.abc import Generator
|
|
11
|
-
from typing import Union
|
|
12
10
|
|
|
13
11
|
from . import parser
|
|
14
12
|
from .record import RecordPackage
|
|
15
13
|
|
|
14
|
+
# from collections.abc import Generator
|
|
15
|
+
# from typing import Union
|
|
16
|
+
|
|
17
|
+
|
|
16
18
|
logger = logging.getLogger(__name__)
|
|
17
19
|
|
|
18
20
|
|
|
@@ -308,7 +310,7 @@ def _expand_configurations_from_chain(
|
|
|
308
310
|
for item in chain:
|
|
309
311
|
subitems = subitems.get(item.name, {})
|
|
310
312
|
pragmas = list(
|
|
311
|
-
pragma for pragma in get_pragma(item, name=pragma) if pragma is not None
|
|
313
|
+
pragma for pragma in parser.get_pragma(item, name=pragma) if pragma is not None
|
|
312
314
|
)
|
|
313
315
|
if not pragmas:
|
|
314
316
|
if allow_no_pragma:
|
|
@@ -712,45 +714,11 @@ class SingularChain:
|
|
|
712
714
|
def find_pytmc_symbols(tmc, allow_no_pragma=False):
|
|
713
715
|
"Find all symbols in a tmc file that contain pragmas"
|
|
714
716
|
for symbol in tmc.find(parser.Symbol):
|
|
715
|
-
if has_pragma(symbol) or allow_no_pragma:
|
|
717
|
+
if parser.has_pragma(symbol) or allow_no_pragma:
|
|
716
718
|
if symbol.name.count(".") == 1:
|
|
717
719
|
yield symbol
|
|
718
720
|
|
|
719
721
|
|
|
720
|
-
def get_pragma(
|
|
721
|
-
item: Union[parser.SubItem, type[parser.Symbol]], *, name: str = "pytmc"
|
|
722
|
-
) -> Generator[str, None, None]:
|
|
723
|
-
"""
|
|
724
|
-
Get all pragmas with a certain tag.
|
|
725
|
-
|
|
726
|
-
Parameters
|
|
727
|
-
----------
|
|
728
|
-
item : parser.SubItem, parser.Symbol, parser.Symbol subclass
|
|
729
|
-
Representation of beckhoff variable or data structure
|
|
730
|
-
|
|
731
|
-
name : str, optional
|
|
732
|
-
Accept tmc entries where the <Name> field equals the passed string
|
|
733
|
-
|
|
734
|
-
Yields
|
|
735
|
-
------
|
|
736
|
-
str
|
|
737
|
-
|
|
738
|
-
"""
|
|
739
|
-
name_list = [name, f"plcAttribute_{name}"]
|
|
740
|
-
if hasattr(item, "Properties"):
|
|
741
|
-
properties = item.Properties[0]
|
|
742
|
-
for prop in getattr(properties, "Property", []):
|
|
743
|
-
# Return true if any of the names searched for are found
|
|
744
|
-
if any(indiv_name == prop.name for indiv_name in name_list):
|
|
745
|
-
yield prop.value
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
def has_pragma(item, *, name: str = "pytmc"):
|
|
749
|
-
"Does `item` have a pragma titled `name`?"
|
|
750
|
-
|
|
751
|
-
return any(True for pragma in get_pragma(item, name=name) if pragma is not None)
|
|
752
|
-
|
|
753
|
-
|
|
754
722
|
def always_true(*a, **kwargs):
|
|
755
723
|
return True
|
|
756
724
|
|
|
@@ -760,7 +728,7 @@ def chains_from_symbol(symbol, *, pragma: str = "pytmc", allow_no_pragma=False):
|
|
|
760
728
|
if allow_no_pragma:
|
|
761
729
|
condition = always_true
|
|
762
730
|
else:
|
|
763
|
-
condition = has_pragma
|
|
731
|
+
condition = parser.has_pragma
|
|
764
732
|
for full_chain in symbol.walk(condition=condition):
|
|
765
733
|
configs = itertools.product(
|
|
766
734
|
*_expand_configurations_from_chain(
|
|
@@ -2,7 +2,6 @@ from pathlib import Path
|
|
|
2
2
|
|
|
3
3
|
from pytmc import parser
|
|
4
4
|
from pytmc.bin import stcmd
|
|
5
|
-
from pytmc.pragmas import get_pragma
|
|
6
5
|
|
|
7
6
|
from .conftest import get_real_motor_symbols
|
|
8
7
|
|
|
@@ -98,7 +97,7 @@ def test_macro_in_motor_stcmd():
|
|
|
98
97
|
tmc_item = parser.parse(file)
|
|
99
98
|
all_motors = list(tmc_item.find(parser.Symbol_ST_MotionStage))
|
|
100
99
|
yes_sub = [
|
|
101
|
-
motor for motor in all_motors if "@" in next(get_pragma(motor))
|
|
100
|
+
motor for motor in all_motors if "@" in next(parser.get_pragma(motor))
|
|
102
101
|
]
|
|
103
102
|
assert len(yes_sub) > 0, "No motors found that need subs"
|
|
104
103
|
assert len(all_motors) > len(yes_sub), "No motors found that don't need subs"
|
|
@@ -110,7 +109,7 @@ def test_macro_in_motor_stcmd():
|
|
|
110
109
|
assert "$(PREFIX)" in prefix
|
|
111
110
|
else:
|
|
112
111
|
# Make sure the non-subs don't get distorted
|
|
113
|
-
assert prefix + name in next(get_pragma(motor))
|
|
112
|
+
assert prefix + name in next(parser.get_pragma(motor))
|
|
114
113
|
# Make sure we didn't miscategorize a yes_sub
|
|
115
114
|
assert "$(PREFIX)" not in prefix + name
|
|
116
115
|
assert "@" not in prefix
|