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.
Files changed (93) hide show
  1. pytmc-2.19.0/.github/CODEOWNERS +27 -0
  2. {pytmc-2.18.2 → pytmc-2.19.0}/.gitmodules +3 -0
  3. {pytmc-2.18.2 → pytmc-2.19.0}/.pre-commit-config.yaml +3 -3
  4. {pytmc-2.18.2/pytmc.egg-info → pytmc-2.19.0}/PKG-INFO +3 -2
  5. {pytmc-2.18.2 → pytmc-2.19.0}/docs/source/release_notes.rst +10 -0
  6. pytmc-2.19.0/pytmc/_version.py +34 -0
  7. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/bin/template.py +150 -32
  8. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/parser.py +145 -0
  9. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/pragmas.py +7 -39
  10. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/test_motion.py +2 -3
  11. pytmc-2.19.0/pytmc/tests/tmc_files/motion_abstraction_sample.tmc +718 -0
  12. {pytmc-2.18.2 → pytmc-2.19.0/pytmc.egg-info}/PKG-INFO +3 -2
  13. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc.egg-info/SOURCES.txt +2 -0
  14. pytmc-2.18.2/pytmc/_version.py +0 -16
  15. {pytmc-2.18.2 → pytmc-2.19.0}/.coveragerc +0 -0
  16. {pytmc-2.18.2 → pytmc-2.19.0}/.flake8 +0 -0
  17. {pytmc-2.18.2 → pytmc-2.19.0}/.git_archival.txt +0 -0
  18. {pytmc-2.18.2 → pytmc-2.19.0}/.gitattributes +0 -0
  19. {pytmc-2.18.2 → pytmc-2.19.0}/.github/workflows/standard.yml +0 -0
  20. {pytmc-2.18.2 → pytmc-2.19.0}/.gitignore +0 -0
  21. {pytmc-2.18.2 → pytmc-2.19.0}/.landscape.yml +0 -0
  22. {pytmc-2.18.2 → pytmc-2.19.0}/AUTHORS.rst +0 -0
  23. {pytmc-2.18.2 → pytmc-2.19.0}/CONTRIBUTING.rst +0 -0
  24. {pytmc-2.18.2 → pytmc-2.19.0}/LICENSE +0 -0
  25. {pytmc-2.18.2 → pytmc-2.19.0}/MANIFEST.in +0 -0
  26. {pytmc-2.18.2 → pytmc-2.19.0}/README.rst +0 -0
  27. {pytmc-2.18.2 → pytmc-2.19.0}/conda-recipe/meta.yaml +0 -0
  28. {pytmc-2.18.2 → pytmc-2.19.0}/dev-requirements.txt +0 -0
  29. {pytmc-2.18.2 → pytmc-2.19.0}/docs/Makefile +0 -0
  30. {pytmc-2.18.2 → pytmc-2.19.0}/docs/source/_static/.gitkeep +0 -0
  31. {pytmc-2.18.2 → pytmc-2.19.0}/docs/source/conf.py +0 -0
  32. {pytmc-2.18.2 → pytmc-2.19.0}/docs/source/getting_started.rst +0 -0
  33. {pytmc-2.18.2 → pytmc-2.19.0}/docs/source/index.rst +0 -0
  34. {pytmc-2.18.2 → pytmc-2.19.0}/docs/source/installation.rst +0 -0
  35. {pytmc-2.18.2 → pytmc-2.19.0}/docs/source/pragma_usage.rst +0 -0
  36. {pytmc-2.18.2 → pytmc-2.19.0}/docs/source/source_code.rst +0 -0
  37. {pytmc-2.18.2 → pytmc-2.19.0}/docs/source/terminal_usage.rst +0 -0
  38. {pytmc-2.18.2 → pytmc-2.19.0}/docs/source/user_guide.rst +0 -0
  39. {pytmc-2.18.2 → pytmc-2.19.0}/docs-requirements.txt +0 -0
  40. {pytmc-2.18.2 → pytmc-2.19.0}/pyproject.toml +0 -0
  41. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/__init__.py +0 -0
  42. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/__main__.py +0 -0
  43. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/beckhoff.py +0 -0
  44. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/bin/__init__.py +0 -0
  45. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/bin/code.py +0 -0
  46. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/bin/db.py +0 -0
  47. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/bin/debug.py +0 -0
  48. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/bin/iocboot.py +0 -0
  49. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/bin/pragmalint.py +0 -0
  50. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/bin/pytmc.py +0 -0
  51. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/bin/stcmd.py +0 -0
  52. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/bin/summary.py +0 -0
  53. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/bin/types.py +0 -0
  54. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/bin/util.py +0 -0
  55. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/bin/xmltranslate.py +0 -0
  56. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/code.py +0 -0
  57. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/default_settings/__init__.py +0 -0
  58. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/default_settings/conf.ini +0 -0
  59. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/default_settings/unified_ordered_field_list.py +0 -0
  60. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/defaults.py +0 -0
  61. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/linter.py +0 -0
  62. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/record.py +0 -0
  63. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/templates/EPICS_proto_template.proto +0 -0
  64. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/templates/asyn_standard_file.jinja2 +0 -0
  65. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/templates/asyn_standard_record.jinja2 +0 -0
  66. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/templates/stcmd_default.cmd +0 -0
  67. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/__init__.py +0 -0
  68. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/ads.dbd +0 -0
  69. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/conftest.py +0 -0
  70. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/static_routes.xml +0 -0
  71. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/templates/basic_test.txt +0 -0
  72. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/templates/smoke_test.txt +0 -0
  73. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/test_archive.py +0 -0
  74. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/test_commandline.py +0 -0
  75. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/test_integrations.py +0 -0
  76. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/test_lint.py +0 -0
  77. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/test_parsing.py +0 -0
  78. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/test_project.py +0 -0
  79. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/test_record.py +0 -0
  80. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/test_xml_collector.py +0 -0
  81. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/test_xml_obj.py +0 -0
  82. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/tmc_files/ArbiterPLC.tmc +0 -0
  83. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/tmc_files/tc_mot_example.tmc +0 -0
  84. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/tests/tmc_files/xtes_sxr_plc.tmc +0 -0
  85. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/validation/__init__.py +0 -0
  86. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/validation/v0.1.py +0 -0
  87. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc/version.py +0 -0
  88. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc.egg-info/dependency_links.txt +0 -0
  89. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc.egg-info/entry_points.txt +0 -0
  90. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc.egg-info/requires.txt +0 -0
  91. {pytmc-2.18.2 → pytmc-2.19.0}/pytmc.egg-info/top_level.txt +0 -0
  92. {pytmc-2.18.2 → pytmc-2.19.0}/requirements.txt +0 -0
  93. {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: v4.4.0
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: 6.0.0
29
+ rev: 7.3.0
30
30
  hooks:
31
31
  - id: flake8
32
32
 
33
33
  - repo: https://github.com/timothycrosley/isort
34
- rev: 5.12.0
34
+ rev: 7.0.0
35
35
  hooks:
36
36
  - id: isort
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: pytmc
3
- Version: 2.18.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 get_motors(plc: parser.Plc) -> list[parser.Symbol_ST_MotionStage]:
317
- """Get pragma'd motor symbols for the PLC (non-pointer ST_MotionStage)."""
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
- return [
320
- stage
321
- for stage in symbols.get("Symbol_ST_MotionStage", [])
322
- if not stage.is_pointer and pragmas.has_pragma(stage)
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
- Parameters
776
- ----------
777
- projects : list
778
- List of projects or TwinCAT project files (.sln, .tsproj, .tmc, etc.)
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
- debug : bool, optional
786
- Open a debug session after rendering with IPython.
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