@aryacode/dsh-engineering 0.4.7 → 0.4.21
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.
- package/CHANGELOG.md +202 -0
- package/README.md +40 -26
- package/cordis.patch.yml +7 -6
- package/entries/config.js +62 -0
- package/entries/ftview.js +5 -4
- package/entries/l5x.js +47 -15
- package/entries/mwi.js +26 -6
- package/entries/optix.js +105 -0
- package/lib/client.js +335 -6
- package/package.json +11 -10
- package/vendor/ftview/SKILL.md +17 -0
- package/vendor/l5x/SKILL.md +17 -0
- package/vendor/l5x/studio-bridge/NuGet.Config +1 -1
- package/vendor/mwi/README.md +41 -18
- package/vendor/mwi/SKILL.md +39 -20
- package/vendor/mwi/bridge/mw_bridge.ps1 +2768 -2763
- package/vendor/mwi/bridge/sdk-interop-importer.cs +30 -30
- package/vendor/mwi/bridge/sdk-variable-reader.cs +22 -22
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/__init__.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/errors.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/staging.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/build.py +339 -339
- package/vendor/mwi/code/engine/motionworks_iec_mcp/cfb.py +765 -765
- package/vendor/mwi/code/engine/motionworks_iec_mcp/declarations.py +399 -399
- package/vendor/mwi/code/engine/motionworks_iec_mcp/ide.py +313 -313
- package/vendor/mwi/code/engine/motionworks_iec_mcp/manuals.py +520 -520
- package/vendor/mwi/code/engine/motionworks_iec_mcp/mwt_bind.py +133 -133
- package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/catalog.json +672 -672
- package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/patterns.json +86 -86
- package/vendor/mwi/code/engine/motionworks_iec_mcp/restore.py +208 -208
- package/vendor/mwi/code/engine/motionworks_iec_mcp/staging.py +134 -130
- package/vendor/mwi/code/engine/motionworks_iec_mcp/stlint.py +673 -673
- package/vendor/mwi/code/engine/motionworks_iec_mcp/tree_writer.py +790 -790
- package/vendor/mwi/code/engine/motionworks_iec_mcp/writer.py +642 -642
- package/vendor/mwi/lib/core/code-engine.d.ts +13 -0
- package/vendor/mwi/lib/core/code-engine.js +18 -1
- package/vendor/mwi/lib/edit.d.ts +1 -1
- package/vendor/mwi/lib/edit.js +1 -1
- package/vendor/mwi/lib/read/service.js +8 -1
- package/vendor/mwi/lib/session/backup.d.ts +72 -0
- package/vendor/mwi/lib/session/backup.js +257 -0
- package/vendor/mwi/lib/session/project.d.ts +4 -1
- package/vendor/mwi/lib/session/project.js +13 -2
- package/vendor/mwi/lib/session/service.d.ts +13 -6
- package/vendor/mwi/lib/session/service.js +127 -36
- package/vendor/mwi/lib/session/workspace.d.ts +10 -0
- package/vendor/mwi/lib/session/workspace.js +53 -3
- package/vendor/mwi/lib/skill.d.ts +2 -2
- package/vendor/mwi/lib/skill.js +2 -2
- package/vendor/mwi/lib/tools/edit.d.ts +3 -3
- package/vendor/mwi/lib/tools/edit.js +14 -15
- package/vendor/mwi/lib/tools/read.js +1 -1
- package/vendor/mwi/lib/tools/session.js +13 -9
- package/vendor/optix/templates/arya-optix-ipc/project/AryaNewHMIProject_IPC.optix +81 -81
- package/vendor/optix/templates/arya-optix-ipc/project/AryaNewHMIProject_IPC.optix.design +38 -38
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Alarms/Alarms.yaml +3 -3
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/AryaNewHMIProject_IPC.yaml +67 -67
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/CommDrivers/CommDrivers.yaml +2 -2
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Converters/Converters.yaml +2 -2
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/DataStores/DataStores.yaml +2 -2
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Loggers/Loggers.yaml +2 -2
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/MQTT/MQTT.yaml +2 -2
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Model/Model.yaml +2 -2
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/NetLogic/NetLogic.yaml +26 -26
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/OPC-UA/OPC-UA.yaml +2 -2
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Recipes/Recipes.yaml +2 -2
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Reports/Reports.yaml +2 -2
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Retentivity/Retentivity.yaml +57 -57
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Security/Groups/Groups.yaml +3 -3
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Security/Roles/Roles.yaml +3 -3
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Security/Security.yaml +7 -7
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Security/Users/Users.yaml +3 -3
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/System/System.yaml +2 -2
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/Translations/Translations.yaml +16 -16
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/UI/Keyboards/Keyboards.yaml +2 -2
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/UI/Screens/Screens.yaml +2 -2
- package/vendor/optix/templates/arya-optix-ipc/project/Nodes/UI/UI.yaml +250 -250
- package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/.vscode/launch.json +18 -18
- package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/AryaNewHMIProject_IPC.code-workspace +9 -9
- package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/AryaNewHMIProject_IPC.csproj +19 -19
- package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/AryaNewHMIProject_IPC.references +172 -172
- package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/AryaNewHMIProject_IPC.sln +21 -21
- package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/Private/TypeConstants.cs +20 -20
- package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/Private/UITypeDefinitions.cs +11 -11
- package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/NetSolution/StudioMCPBridge.cs +4340 -4340
- package/vendor/optix/templates/arya-optix-ipc/project/ProjectFiles/UserDefinedModule.xml +26 -26
- package/vendor/optix/templates/arya-optix-panel/project/AryaNewHMIProject_OptixPanel.optix +97 -97
- package/vendor/optix/templates/arya-optix-panel/project/AryaNewHMIProject_OptixPanel.optix.design +38 -38
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/Alarms/Alarms.yaml +3 -3
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/AryaNewHMIProject_OptixPanel.yaml +67 -67
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/CommDrivers/CommDrivers.yaml +2 -2
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/Converters/Converters.yaml +2 -2
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/DataStores/DataStores.yaml +2 -2
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/Loggers/Loggers.yaml +2 -2
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/MQTT/MQTT.yaml +2 -2
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/Model/Model.yaml +2 -2
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/NetLogic/NetLogic.yaml +26 -26
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/OPC-UA/OPC-UA.yaml +2 -2
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/Recipes/Recipes.yaml +2 -2
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/Reports/Reports.yaml +2 -2
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/Retentivity/Retentivity.yaml +57 -57
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/Security/Groups/Groups.yaml +3 -3
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/Security/Roles/Roles.yaml +3 -3
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/Security/Security.yaml +7 -7
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/Security/Users/Users.yaml +3 -3
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/System/System.yaml +444 -444
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/Translations/Translations.yaml +81 -81
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/Keyboards/Keyboards.yaml +2 -2
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/Screens/Screens.yaml +2 -2
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/DeviceSettingsWidget/DeviceSettingsWidget.yaml +163 -163
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/DeviceSettingsWidget/DeviceSettingsWidgetComponents/DeviceSettingsWidgetComponents.yaml +1663 -1663
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/FactoryTalkRemoteAccessWidget/FTRemoteAccessWidgetComponents/FTRemoteAccessWidgetComponents.yaml +2471 -2471
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/FactoryTalkRemoteAccessWidget/FactoryTalkRemoteAccessWidget.yaml +218 -218
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/NetworkSettingsWidget/NetworkSettingsWidget.yaml +524 -524
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/NetworkSettingsWidget/NetworkSettingsWidgetComponents/NetworkSettingsWidgetComponents.yaml +690 -690
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/SettingsWidgets/SettingsWidgets.yaml +1265 -1265
- package/vendor/optix/templates/arya-optix-panel/project/Nodes/UI/UI.yaml +252 -252
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/.vscode/launch.json +18 -18
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/ApplicationNameLogic.cs +19 -19
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/AryaNewHMIProject_OptixPanel.code-workspace +9 -9
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/AryaNewHMIProject_OptixPanel.csproj +19 -19
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/AryaNewHMIProject_OptixPanel.references +172 -172
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/AryaNewHMIProject_OptixPanel.sln +21 -21
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/ConnectionSwitchLogic.cs +74 -74
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/DateAndTimeWidgetLogic.cs +420 -420
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/DeviceSettingsWidgetLogic.cs +34 -34
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/FTOptixStudioVersionLogic.cs +47 -47
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/FTRemoteAccessWidgetSetupLogic.cs +153 -153
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/IncomingConnectionApprovalLogic.cs +118 -118
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/IncomingConnectionReceiverLogic.cs +166 -166
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkConfiguratorEditModelsLogic.cs +56 -56
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkInterfaceLogic.cs +138 -138
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkInterfacesEditModelReaderLogic.cs +87 -87
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkInterfacesEditModelToSystemWriterLogic.cs +139 -139
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/NetworkSettingsLogic.cs +168 -168
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/Private/FTRemoteAccessWidgetDataObject.cs +106 -106
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/Private/TypeConstants.cs +37 -37
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/Private/UITypeDefinitions.cs +91 -91
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/RequestAssistanceSwitchLogic.cs +93 -93
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/StudioMCPBridge.cs +4340 -4340
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/NetSolution/SystemToNetworkInterfacesEditModelWriterLogic.cs +77 -77
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/UserDefinedModule.xml +111 -111
- package/vendor/optix/templates/arya-optix-panel/project/ProjectFiles/WarningIcon.svg +4 -4
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/block_interfaces.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/cfb.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/datatypes.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/declarations.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/graphical_listing.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/grid.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/knowledge.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/mwt_bind.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/navigation.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/program_checks.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/project.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/structure.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/task_bindings.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tasks.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tree.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/validation.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variable_edit.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variables.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/workflow.cpython-312.pyc +0 -0
|
@@ -1,399 +1,399 @@
|
|
|
1
|
-
"""Pure text transforms for MotionWorks IEC variable declarations.
|
|
2
|
-
|
|
3
|
-
These functions operate on the ``.VB`` declaration text only. That is
|
|
4
|
-
sufficient to add, edit and delete variables: measured empirically, the compiler
|
|
5
|
-
reads declarations from this stream and does **not** require the binary ``.VGR``
|
|
6
|
-
grid to be updated (see docs/tier2-format-notes.md). A `.VB`-only edit compiled
|
|
7
|
-
clean with the grid left byte-identical.
|
|
8
|
-
|
|
9
|
-
Keeping the transforms pure -- text in, text out -- means they can be unit
|
|
10
|
-
tested without touching a project, and the write layer only has to handle
|
|
11
|
-
backup, atomic replacement and read-back verification.
|
|
12
|
-
|
|
13
|
-
Observed format (CRLF, latin-1, tab-indented)::
|
|
14
|
-
|
|
15
|
-
<blank>
|
|
16
|
-
(*Group:Default*)
|
|
17
|
-
<blank>
|
|
18
|
-
<blank>
|
|
19
|
-
VAR_EXTERNAL
|
|
20
|
-
\tTopCutter :\tAXIS_REF;(*SGD7S - 1 (Do Not Modify!!) *)
|
|
21
|
-
\tEIP_FromCLX_Axis_SVON_Cmd :\tBOOL;
|
|
22
|
-
END_VAR
|
|
23
|
-
|
|
24
|
-
Blocks with the same keyword may repeat, each optionally preceded by its own
|
|
25
|
-
``(*Group:...*)`` marker. MotionWorks writes a blank line between blocks.
|
|
26
|
-
"""
|
|
27
|
-
|
|
28
|
-
from __future__ import annotations
|
|
29
|
-
|
|
30
|
-
import re
|
|
31
|
-
from dataclasses import dataclass
|
|
32
|
-
|
|
33
|
-
#: Declaration blocks this module can address. ``VAR`` is the default for new
|
|
34
|
-
#: local variables; ``VAR_EXTERNAL`` is for symbols owned by another POU.
|
|
35
|
-
BLOCK_KEYWORDS = (
|
|
36
|
-
"VAR_GLOBAL",
|
|
37
|
-
"VAR_EXTERNAL",
|
|
38
|
-
"VAR_INPUT",
|
|
39
|
-
"VAR_OUTPUT",
|
|
40
|
-
"VAR_IN_OUT",
|
|
41
|
-
"VAR_TEMP",
|
|
42
|
-
"VAR_RETAIN",
|
|
43
|
-
"VAR_CONFIG",
|
|
44
|
-
"VAR_ACCESS",
|
|
45
|
-
"VAR",
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
_INDENT = "\t"
|
|
49
|
-
_SEPARATOR = " :\t"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
class DeclarationError(ValueError):
|
|
53
|
-
"""Raised when a declaration cannot be parsed or edited safely."""
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
@dataclass
|
|
57
|
-
class Block:
|
|
58
|
-
"""A ``VAR``-family block located in the declaration text."""
|
|
59
|
-
|
|
60
|
-
keyword: str
|
|
61
|
-
start: int # index of the keyword line
|
|
62
|
-
end: int # index of the matching END_VAR line
|
|
63
|
-
lines: list[str] # the block's lines, inclusive
|
|
64
|
-
|
|
65
|
-
@property
|
|
66
|
-
def is_empty(self) -> bool:
|
|
67
|
-
return self.end == self.start + 1
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def _line_ending(text: str) -> str:
|
|
71
|
-
return "\r\n" if "\r\n" in text else "\n"
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def split_lines(text: str) -> tuple[list[str], str]:
|
|
75
|
-
"""Split into lines without terminators, plus the dominant line ending."""
|
|
76
|
-
ending = _line_ending(text)
|
|
77
|
-
normalised = text.replace("\r\n", "\n")
|
|
78
|
-
return normalised.split("\n"), ending
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def join_lines(lines: list[str], ending: str) -> str:
|
|
82
|
-
return ending.join(lines)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def find_blocks(lines: list[str]) -> list[Block]:
|
|
86
|
-
"""Locate every declaration block, in order."""
|
|
87
|
-
blocks: list[Block] = []
|
|
88
|
-
index = 0
|
|
89
|
-
while index < len(lines):
|
|
90
|
-
stripped = lines[index].strip()
|
|
91
|
-
upper = stripped.upper()
|
|
92
|
-
keyword = None
|
|
93
|
-
for candidate in BLOCK_KEYWORDS:
|
|
94
|
-
if upper == candidate or upper.startswith(candidate + " "):
|
|
95
|
-
keyword = candidate
|
|
96
|
-
break
|
|
97
|
-
if keyword is None:
|
|
98
|
-
index += 1
|
|
99
|
-
continue
|
|
100
|
-
# Find the matching END_VAR.
|
|
101
|
-
end = index + 1
|
|
102
|
-
while end < len(lines) and lines[end].strip().upper() != "END_VAR":
|
|
103
|
-
end += 1
|
|
104
|
-
if end >= len(lines):
|
|
105
|
-
raise DeclarationError(
|
|
106
|
-
f"unterminated {keyword} block starting at line {index + 1}"
|
|
107
|
-
)
|
|
108
|
-
blocks.append(
|
|
109
|
-
Block(keyword=keyword, start=index, end=end, lines=lines[index : end + 1])
|
|
110
|
-
)
|
|
111
|
-
index = end + 1
|
|
112
|
-
return blocks
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def format_declaration(
|
|
116
|
-
name: str,
|
|
117
|
-
type_name: str,
|
|
118
|
-
address: str | None = None,
|
|
119
|
-
initial_value: str | None = None,
|
|
120
|
-
description: str | None = None,
|
|
121
|
-
) -> str:
|
|
122
|
-
"""Render one declaration line the way MotionWorks writes it."""
|
|
123
|
-
left = name
|
|
124
|
-
if address:
|
|
125
|
-
left = f"{name} AT {address}"
|
|
126
|
-
text = f"{_INDENT}{left}{_SEPARATOR}{type_name}"
|
|
127
|
-
if initial_value:
|
|
128
|
-
text += f" := {initial_value}"
|
|
129
|
-
text += ";"
|
|
130
|
-
if description:
|
|
131
|
-
text += f"(*{description}*)"
|
|
132
|
-
return text
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def _find_declaration_line(lines: list[str], name: str) -> int:
|
|
136
|
-
"""Return the line index declaring ``name``, or raise."""
|
|
137
|
-
pattern = re.compile(rf"^[ \t]*{re.escape(name)}[ \t]*(?:AT[ \t]+\S+[ \t]*)?:",
|
|
138
|
-
re.IGNORECASE)
|
|
139
|
-
matches = [i for i, line in enumerate(lines) if pattern.match(line)]
|
|
140
|
-
if not matches:
|
|
141
|
-
raise DeclarationError(f"variable {name!r} is not declared")
|
|
142
|
-
if len(matches) > 1:
|
|
143
|
-
raise DeclarationError(
|
|
144
|
-
f"variable {name!r} is declared on {len(matches)} lines "
|
|
145
|
-
f"({[m + 1 for m in matches]}); refusing to guess"
|
|
146
|
-
)
|
|
147
|
-
return matches[0]
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
#: Address prefixes and the IEC elementary types each can legitimately hold.
|
|
151
|
-
#: ``%IX``/``%QX`` are bit addresses, so they must be BOOL. A word address can
|
|
152
|
-
#: hold any numeric type of the matching width or narrower.
|
|
153
|
-
_ADDRESS_CLASSES: tuple[tuple[str, set[str] | None], ...] = (
|
|
154
|
-
("%IX", {"BOOL"}),
|
|
155
|
-
("%QX", {"BOOL"}),
|
|
156
|
-
("%MX", {"BOOL"}),
|
|
157
|
-
("%IW", None),
|
|
158
|
-
("%QW", None),
|
|
159
|
-
("%MW", None),
|
|
160
|
-
("%ID", None),
|
|
161
|
-
("%QD", None),
|
|
162
|
-
("%MD", None),
|
|
163
|
-
("%IL", None),
|
|
164
|
-
("%QL", None),
|
|
165
|
-
("%ML", None),
|
|
166
|
-
("%IB", None),
|
|
167
|
-
("%QB", None),
|
|
168
|
-
("%MB", None),
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
def address_class_types(address: str) -> set[str] | None:
|
|
173
|
-
"""Return the types an address may hold, or None when unconstrained.
|
|
174
|
-
|
|
175
|
-
None means "not a bit address", i.e. a numeric width that we do not attempt
|
|
176
|
-
to police; the callers only use this to catch the clear contradiction of
|
|
177
|
-
putting a non-BOOL type on a ``%IX``/``%QX``/``%MX`` bit address.
|
|
178
|
-
"""
|
|
179
|
-
upper = address.strip().upper()
|
|
180
|
-
for prefix, allowed in _ADDRESS_CLASSES:
|
|
181
|
-
if upper.startswith(prefix):
|
|
182
|
-
return allowed
|
|
183
|
-
return None
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
def check_address_type(address: str | None, type_name: str) -> str | None:
|
|
187
|
-
"""Return a warning when ``type_name`` is impossible at ``address``.
|
|
188
|
-
|
|
189
|
-
MotionWorks binds a ``%IX``/``%QX``/``%MX`` address to a single bit, so
|
|
190
|
-
declaring it as anything but BOOL cannot compile. This was observed
|
|
191
|
-
directly: changing an I/O-mapped ``BOOL AT %IX21488.5`` to ``DINT`` made the
|
|
192
|
-
build fail (``IsCompiled=False``, no outputs), while the same type change on
|
|
193
|
-
an unaddressed variable compiled cleanly.
|
|
194
|
-
"""
|
|
195
|
-
if not address:
|
|
196
|
-
return None
|
|
197
|
-
allowed = address_class_types(address)
|
|
198
|
-
if allowed is None:
|
|
199
|
-
return None
|
|
200
|
-
if type_name.strip().upper() not in allowed:
|
|
201
|
-
return (
|
|
202
|
-
f"type {type_name!r} cannot be bound to the bit address {address!r}; "
|
|
203
|
-
f"only {', '.join(sorted(allowed))} is valid there"
|
|
204
|
-
)
|
|
205
|
-
return None
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
def find_referencing_pous(
|
|
209
|
-
variable: str, bodies: dict[str, str]
|
|
210
|
-
) -> list[str]:
|
|
211
|
-
"""Return the names of POUs whose body references ``variable``.
|
|
212
|
-
|
|
213
|
-
Deleting a variable that code still uses produces a dangling reference and
|
|
214
|
-
the build fails; this was observed directly (deleting a POU-local variable
|
|
215
|
-
made the build fail while the same delete of an unreferenced variable
|
|
216
|
-
compiled). Checking first turns a confusing build error into a clear
|
|
217
|
-
refusal.
|
|
218
|
-
"""
|
|
219
|
-
pattern = re.compile(rf"(?<![A-Za-z0-9_.]){re.escape(variable)}(?![A-Za-z0-9_])")
|
|
220
|
-
return sorted(name for name, body in bodies.items() if pattern.search(body))
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
def add_variable(
|
|
224
|
-
text: str,
|
|
225
|
-
name: str,
|
|
226
|
-
type_name: str,
|
|
227
|
-
section: str = "VAR",
|
|
228
|
-
address: str | None = None,
|
|
229
|
-
initial_value: str | None = None,
|
|
230
|
-
description: str | None = None,
|
|
231
|
-
group: str | None = None,
|
|
232
|
-
) -> tuple[str, list[str]]:
|
|
233
|
-
"""Add a declaration, returning ``(new_text, notes)``.
|
|
234
|
-
|
|
235
|
-
Appends to the last block of the requested ``section``; if there is none,
|
|
236
|
-
a new block is appended at the end of the text. Refuses if the name is
|
|
237
|
-
already declared, because a duplicate would be a confusing build error.
|
|
238
|
-
"""
|
|
239
|
-
section = section.upper()
|
|
240
|
-
if section not in BLOCK_KEYWORDS:
|
|
241
|
-
raise DeclarationError(
|
|
242
|
-
f"unknown section {section!r}; expected one of {BLOCK_KEYWORDS}"
|
|
243
|
-
)
|
|
244
|
-
if not re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", name):
|
|
245
|
-
raise DeclarationError(f"invalid variable name {name!r}")
|
|
246
|
-
if not type_name.strip():
|
|
247
|
-
raise DeclarationError("type name is required")
|
|
248
|
-
|
|
249
|
-
lines, ending = split_lines(text)
|
|
250
|
-
notes: list[str] = []
|
|
251
|
-
|
|
252
|
-
try:
|
|
253
|
-
_find_declaration_line(lines, name)
|
|
254
|
-
except DeclarationError as exc:
|
|
255
|
-
if "not declared" not in str(exc):
|
|
256
|
-
raise
|
|
257
|
-
else:
|
|
258
|
-
raise DeclarationError(f"variable {name!r} is already declared")
|
|
259
|
-
|
|
260
|
-
declaration = format_declaration(
|
|
261
|
-
name, type_name, address, initial_value, description
|
|
262
|
-
)
|
|
263
|
-
blocks = find_blocks(lines)
|
|
264
|
-
def block_group(block):
|
|
265
|
-
current = None
|
|
266
|
-
for line in lines[:block.start]:
|
|
267
|
-
match = re.match(r"^\s*\(\*Group:\s*(.*?)\*\)", line)
|
|
268
|
-
if match: current = match.group(1)
|
|
269
|
-
return current
|
|
270
|
-
target = next((b for b in reversed(blocks) if b.keyword == section
|
|
271
|
-
and (group is None or block_group(b) == group)), None)
|
|
272
|
-
if group is not None and target is None:
|
|
273
|
-
raise DeclarationError(f"No native {section} block for group {group!r}")
|
|
274
|
-
|
|
275
|
-
if target is not None:
|
|
276
|
-
lines.insert(target.end, declaration)
|
|
277
|
-
else:
|
|
278
|
-
notes.append(f"no {section} block existed; appended a new one")
|
|
279
|
-
# Separate with exactly one blank line, reusing a trailing blank if the
|
|
280
|
-
# file already ends with blanks rather than adding another.
|
|
281
|
-
#
|
|
282
|
-
# This is what makes add and delete symmetric. Appending unconditionally
|
|
283
|
-
# puts the new block below a growing tail of blanks, and delete cannot tell
|
|
284
|
-
# that tail apart from blanks the file legitimately ended with -- a stress
|
|
285
|
-
# matrix caught exactly that as stray lines left behind on every cycle.
|
|
286
|
-
if lines and lines[-1].strip():
|
|
287
|
-
lines.append("")
|
|
288
|
-
lines.append(section)
|
|
289
|
-
lines.append(declaration)
|
|
290
|
-
lines.append("END_VAR")
|
|
291
|
-
return join_lines(lines, ending), notes
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
def edit_variable(
|
|
295
|
-
text: str,
|
|
296
|
-
name: str,
|
|
297
|
-
new_name: str | None = None,
|
|
298
|
-
type_name: str | None = None,
|
|
299
|
-
address: str | None = None,
|
|
300
|
-
initial_value: str | None = None,
|
|
301
|
-
description: str | None = None,
|
|
302
|
-
clear_address: bool = False,
|
|
303
|
-
) -> tuple[str, list[str]]:
|
|
304
|
-
"""Rewrite a variable's declaration line, returning ``(new_text, notes)``.
|
|
305
|
-
|
|
306
|
-
Only the fields supplied are changed. ``clear_address`` exists because
|
|
307
|
-
``address=None`` means "leave the address alone", so removing an ``AT``
|
|
308
|
-
clause needs an explicit flag rather than an ambiguous default.
|
|
309
|
-
"""
|
|
310
|
-
from .variables import parse_declarations
|
|
311
|
-
|
|
312
|
-
lines, ending = split_lines(text)
|
|
313
|
-
index = _find_declaration_line(lines, name)
|
|
314
|
-
# The declaration parser expects a complete block, so wrap the single line
|
|
315
|
-
# in a temporary one rather than parsing a bare fragment.
|
|
316
|
-
probe = "\r\nVAR\r\n" + lines[index] + "\r\nEND_VAR\r\n"
|
|
317
|
-
existing = parse_declarations(probe, source_stream=None)
|
|
318
|
-
candidate = next(
|
|
319
|
-
(v for v in existing.variables if v.name.casefold() == name.casefold()), None
|
|
320
|
-
)
|
|
321
|
-
if candidate is None:
|
|
322
|
-
raise DeclarationError(f"could not parse the declaration of {name!r}")
|
|
323
|
-
current = candidate
|
|
324
|
-
|
|
325
|
-
if new_name and new_name != name:
|
|
326
|
-
try:
|
|
327
|
-
_find_declaration_line(lines, new_name)
|
|
328
|
-
except DeclarationError as exc:
|
|
329
|
-
if "not declared" not in str(exc):
|
|
330
|
-
raise
|
|
331
|
-
else:
|
|
332
|
-
raise DeclarationError(f"variable {new_name!r} already exists")
|
|
333
|
-
|
|
334
|
-
final_name = new_name or current.name
|
|
335
|
-
final_type = type_name if type_name is not None else current.type_name
|
|
336
|
-
if clear_address:
|
|
337
|
-
final_address = None
|
|
338
|
-
elif address is not None:
|
|
339
|
-
final_address = address
|
|
340
|
-
else:
|
|
341
|
-
final_address = current.address
|
|
342
|
-
final_initial = (
|
|
343
|
-
initial_value if initial_value is not None else current.initial_value
|
|
344
|
-
)
|
|
345
|
-
|
|
346
|
-
# Description handling: an empty string clears it, None preserves it.
|
|
347
|
-
if description is None:
|
|
348
|
-
final_description = current.description
|
|
349
|
-
else:
|
|
350
|
-
final_description = description or None
|
|
351
|
-
|
|
352
|
-
lines[index] = format_declaration(
|
|
353
|
-
final_name, final_type, final_address, final_initial, final_description
|
|
354
|
-
)
|
|
355
|
-
notes = [f"rewrote line {index + 1}"]
|
|
356
|
-
return join_lines(lines, ending), notes
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
def delete_variable(
|
|
360
|
-
text: str,
|
|
361
|
-
name: str,
|
|
362
|
-
remove_empty_blocks: bool = True,
|
|
363
|
-
) -> tuple[str, list[str]]:
|
|
364
|
-
"""Remove a declaration, returning ``(new_text, notes)``.
|
|
365
|
-
|
|
366
|
-
By default an emptied block is removed too, so the text does not accumulate
|
|
367
|
-
stray ``VAR``/``END_VAR`` pairs. MotionWorks tolerates empty blocks, but
|
|
368
|
-
leaving them behind makes later diffs noisy.
|
|
369
|
-
"""
|
|
370
|
-
lines, ending = split_lines(text)
|
|
371
|
-
index = _find_declaration_line(lines, name)
|
|
372
|
-
del lines[index]
|
|
373
|
-
notes = [f"removed the declaration of {name!r}"]
|
|
374
|
-
|
|
375
|
-
if remove_empty_blocks:
|
|
376
|
-
blocks = find_blocks(lines)
|
|
377
|
-
removed: list[str] = []
|
|
378
|
-
for block in reversed(blocks):
|
|
379
|
-
if block.end == block.start + 1:
|
|
380
|
-
# Take the blank padding immediately in front of the block, but
|
|
381
|
-
# stop at any real content -- and never walk past the start of the
|
|
382
|
-
# text.
|
|
383
|
-
#
|
|
384
|
-
# An earlier version also consumed a preceding ``(*Group:...*)``
|
|
385
|
-
# marker and every blank before it. On a POU whose declaration
|
|
386
|
-
# file contains only a group marker (a real case: a POU with no
|
|
387
|
-
# variables), removing a just-added block then emptied the whole
|
|
388
|
-
# file, destroying the marker. A group marker is content that was
|
|
389
|
-
# never ours to delete.
|
|
390
|
-
start = block.start
|
|
391
|
-
while start > 1 and not lines[start - 1].strip():
|
|
392
|
-
start -= 1
|
|
393
|
-
del lines[start : block.end + 1]
|
|
394
|
-
removed.append(block.keyword)
|
|
395
|
-
if removed:
|
|
396
|
-
notes.append(
|
|
397
|
-
"removed now-empty block(s): " + ", ".join(reversed(removed))
|
|
398
|
-
)
|
|
399
|
-
return join_lines(lines, ending), notes
|
|
1
|
+
"""Pure text transforms for MotionWorks IEC variable declarations.
|
|
2
|
+
|
|
3
|
+
These functions operate on the ``.VB`` declaration text only. That is
|
|
4
|
+
sufficient to add, edit and delete variables: measured empirically, the compiler
|
|
5
|
+
reads declarations from this stream and does **not** require the binary ``.VGR``
|
|
6
|
+
grid to be updated (see docs/tier2-format-notes.md). A `.VB`-only edit compiled
|
|
7
|
+
clean with the grid left byte-identical.
|
|
8
|
+
|
|
9
|
+
Keeping the transforms pure -- text in, text out -- means they can be unit
|
|
10
|
+
tested without touching a project, and the write layer only has to handle
|
|
11
|
+
backup, atomic replacement and read-back verification.
|
|
12
|
+
|
|
13
|
+
Observed format (CRLF, latin-1, tab-indented)::
|
|
14
|
+
|
|
15
|
+
<blank>
|
|
16
|
+
(*Group:Default*)
|
|
17
|
+
<blank>
|
|
18
|
+
<blank>
|
|
19
|
+
VAR_EXTERNAL
|
|
20
|
+
\tTopCutter :\tAXIS_REF;(*SGD7S - 1 (Do Not Modify!!) *)
|
|
21
|
+
\tEIP_FromCLX_Axis_SVON_Cmd :\tBOOL;
|
|
22
|
+
END_VAR
|
|
23
|
+
|
|
24
|
+
Blocks with the same keyword may repeat, each optionally preceded by its own
|
|
25
|
+
``(*Group:...*)`` marker. MotionWorks writes a blank line between blocks.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import re
|
|
31
|
+
from dataclasses import dataclass
|
|
32
|
+
|
|
33
|
+
#: Declaration blocks this module can address. ``VAR`` is the default for new
|
|
34
|
+
#: local variables; ``VAR_EXTERNAL`` is for symbols owned by another POU.
|
|
35
|
+
BLOCK_KEYWORDS = (
|
|
36
|
+
"VAR_GLOBAL",
|
|
37
|
+
"VAR_EXTERNAL",
|
|
38
|
+
"VAR_INPUT",
|
|
39
|
+
"VAR_OUTPUT",
|
|
40
|
+
"VAR_IN_OUT",
|
|
41
|
+
"VAR_TEMP",
|
|
42
|
+
"VAR_RETAIN",
|
|
43
|
+
"VAR_CONFIG",
|
|
44
|
+
"VAR_ACCESS",
|
|
45
|
+
"VAR",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
_INDENT = "\t"
|
|
49
|
+
_SEPARATOR = " :\t"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class DeclarationError(ValueError):
|
|
53
|
+
"""Raised when a declaration cannot be parsed or edited safely."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class Block:
|
|
58
|
+
"""A ``VAR``-family block located in the declaration text."""
|
|
59
|
+
|
|
60
|
+
keyword: str
|
|
61
|
+
start: int # index of the keyword line
|
|
62
|
+
end: int # index of the matching END_VAR line
|
|
63
|
+
lines: list[str] # the block's lines, inclusive
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def is_empty(self) -> bool:
|
|
67
|
+
return self.end == self.start + 1
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _line_ending(text: str) -> str:
|
|
71
|
+
return "\r\n" if "\r\n" in text else "\n"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def split_lines(text: str) -> tuple[list[str], str]:
|
|
75
|
+
"""Split into lines without terminators, plus the dominant line ending."""
|
|
76
|
+
ending = _line_ending(text)
|
|
77
|
+
normalised = text.replace("\r\n", "\n")
|
|
78
|
+
return normalised.split("\n"), ending
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def join_lines(lines: list[str], ending: str) -> str:
|
|
82
|
+
return ending.join(lines)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def find_blocks(lines: list[str]) -> list[Block]:
|
|
86
|
+
"""Locate every declaration block, in order."""
|
|
87
|
+
blocks: list[Block] = []
|
|
88
|
+
index = 0
|
|
89
|
+
while index < len(lines):
|
|
90
|
+
stripped = lines[index].strip()
|
|
91
|
+
upper = stripped.upper()
|
|
92
|
+
keyword = None
|
|
93
|
+
for candidate in BLOCK_KEYWORDS:
|
|
94
|
+
if upper == candidate or upper.startswith(candidate + " "):
|
|
95
|
+
keyword = candidate
|
|
96
|
+
break
|
|
97
|
+
if keyword is None:
|
|
98
|
+
index += 1
|
|
99
|
+
continue
|
|
100
|
+
# Find the matching END_VAR.
|
|
101
|
+
end = index + 1
|
|
102
|
+
while end < len(lines) and lines[end].strip().upper() != "END_VAR":
|
|
103
|
+
end += 1
|
|
104
|
+
if end >= len(lines):
|
|
105
|
+
raise DeclarationError(
|
|
106
|
+
f"unterminated {keyword} block starting at line {index + 1}"
|
|
107
|
+
)
|
|
108
|
+
blocks.append(
|
|
109
|
+
Block(keyword=keyword, start=index, end=end, lines=lines[index : end + 1])
|
|
110
|
+
)
|
|
111
|
+
index = end + 1
|
|
112
|
+
return blocks
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def format_declaration(
|
|
116
|
+
name: str,
|
|
117
|
+
type_name: str,
|
|
118
|
+
address: str | None = None,
|
|
119
|
+
initial_value: str | None = None,
|
|
120
|
+
description: str | None = None,
|
|
121
|
+
) -> str:
|
|
122
|
+
"""Render one declaration line the way MotionWorks writes it."""
|
|
123
|
+
left = name
|
|
124
|
+
if address:
|
|
125
|
+
left = f"{name} AT {address}"
|
|
126
|
+
text = f"{_INDENT}{left}{_SEPARATOR}{type_name}"
|
|
127
|
+
if initial_value:
|
|
128
|
+
text += f" := {initial_value}"
|
|
129
|
+
text += ";"
|
|
130
|
+
if description:
|
|
131
|
+
text += f"(*{description}*)"
|
|
132
|
+
return text
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _find_declaration_line(lines: list[str], name: str) -> int:
|
|
136
|
+
"""Return the line index declaring ``name``, or raise."""
|
|
137
|
+
pattern = re.compile(rf"^[ \t]*{re.escape(name)}[ \t]*(?:AT[ \t]+\S+[ \t]*)?:",
|
|
138
|
+
re.IGNORECASE)
|
|
139
|
+
matches = [i for i, line in enumerate(lines) if pattern.match(line)]
|
|
140
|
+
if not matches:
|
|
141
|
+
raise DeclarationError(f"variable {name!r} is not declared")
|
|
142
|
+
if len(matches) > 1:
|
|
143
|
+
raise DeclarationError(
|
|
144
|
+
f"variable {name!r} is declared on {len(matches)} lines "
|
|
145
|
+
f"({[m + 1 for m in matches]}); refusing to guess"
|
|
146
|
+
)
|
|
147
|
+
return matches[0]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
#: Address prefixes and the IEC elementary types each can legitimately hold.
|
|
151
|
+
#: ``%IX``/``%QX`` are bit addresses, so they must be BOOL. A word address can
|
|
152
|
+
#: hold any numeric type of the matching width or narrower.
|
|
153
|
+
_ADDRESS_CLASSES: tuple[tuple[str, set[str] | None], ...] = (
|
|
154
|
+
("%IX", {"BOOL"}),
|
|
155
|
+
("%QX", {"BOOL"}),
|
|
156
|
+
("%MX", {"BOOL"}),
|
|
157
|
+
("%IW", None),
|
|
158
|
+
("%QW", None),
|
|
159
|
+
("%MW", None),
|
|
160
|
+
("%ID", None),
|
|
161
|
+
("%QD", None),
|
|
162
|
+
("%MD", None),
|
|
163
|
+
("%IL", None),
|
|
164
|
+
("%QL", None),
|
|
165
|
+
("%ML", None),
|
|
166
|
+
("%IB", None),
|
|
167
|
+
("%QB", None),
|
|
168
|
+
("%MB", None),
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def address_class_types(address: str) -> set[str] | None:
|
|
173
|
+
"""Return the types an address may hold, or None when unconstrained.
|
|
174
|
+
|
|
175
|
+
None means "not a bit address", i.e. a numeric width that we do not attempt
|
|
176
|
+
to police; the callers only use this to catch the clear contradiction of
|
|
177
|
+
putting a non-BOOL type on a ``%IX``/``%QX``/``%MX`` bit address.
|
|
178
|
+
"""
|
|
179
|
+
upper = address.strip().upper()
|
|
180
|
+
for prefix, allowed in _ADDRESS_CLASSES:
|
|
181
|
+
if upper.startswith(prefix):
|
|
182
|
+
return allowed
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def check_address_type(address: str | None, type_name: str) -> str | None:
|
|
187
|
+
"""Return a warning when ``type_name`` is impossible at ``address``.
|
|
188
|
+
|
|
189
|
+
MotionWorks binds a ``%IX``/``%QX``/``%MX`` address to a single bit, so
|
|
190
|
+
declaring it as anything but BOOL cannot compile. This was observed
|
|
191
|
+
directly: changing an I/O-mapped ``BOOL AT %IX21488.5`` to ``DINT`` made the
|
|
192
|
+
build fail (``IsCompiled=False``, no outputs), while the same type change on
|
|
193
|
+
an unaddressed variable compiled cleanly.
|
|
194
|
+
"""
|
|
195
|
+
if not address:
|
|
196
|
+
return None
|
|
197
|
+
allowed = address_class_types(address)
|
|
198
|
+
if allowed is None:
|
|
199
|
+
return None
|
|
200
|
+
if type_name.strip().upper() not in allowed:
|
|
201
|
+
return (
|
|
202
|
+
f"type {type_name!r} cannot be bound to the bit address {address!r}; "
|
|
203
|
+
f"only {', '.join(sorted(allowed))} is valid there"
|
|
204
|
+
)
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def find_referencing_pous(
|
|
209
|
+
variable: str, bodies: dict[str, str]
|
|
210
|
+
) -> list[str]:
|
|
211
|
+
"""Return the names of POUs whose body references ``variable``.
|
|
212
|
+
|
|
213
|
+
Deleting a variable that code still uses produces a dangling reference and
|
|
214
|
+
the build fails; this was observed directly (deleting a POU-local variable
|
|
215
|
+
made the build fail while the same delete of an unreferenced variable
|
|
216
|
+
compiled). Checking first turns a confusing build error into a clear
|
|
217
|
+
refusal.
|
|
218
|
+
"""
|
|
219
|
+
pattern = re.compile(rf"(?<![A-Za-z0-9_.]){re.escape(variable)}(?![A-Za-z0-9_])")
|
|
220
|
+
return sorted(name for name, body in bodies.items() if pattern.search(body))
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def add_variable(
|
|
224
|
+
text: str,
|
|
225
|
+
name: str,
|
|
226
|
+
type_name: str,
|
|
227
|
+
section: str = "VAR",
|
|
228
|
+
address: str | None = None,
|
|
229
|
+
initial_value: str | None = None,
|
|
230
|
+
description: str | None = None,
|
|
231
|
+
group: str | None = None,
|
|
232
|
+
) -> tuple[str, list[str]]:
|
|
233
|
+
"""Add a declaration, returning ``(new_text, notes)``.
|
|
234
|
+
|
|
235
|
+
Appends to the last block of the requested ``section``; if there is none,
|
|
236
|
+
a new block is appended at the end of the text. Refuses if the name is
|
|
237
|
+
already declared, because a duplicate would be a confusing build error.
|
|
238
|
+
"""
|
|
239
|
+
section = section.upper()
|
|
240
|
+
if section not in BLOCK_KEYWORDS:
|
|
241
|
+
raise DeclarationError(
|
|
242
|
+
f"unknown section {section!r}; expected one of {BLOCK_KEYWORDS}"
|
|
243
|
+
)
|
|
244
|
+
if not re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", name):
|
|
245
|
+
raise DeclarationError(f"invalid variable name {name!r}")
|
|
246
|
+
if not type_name.strip():
|
|
247
|
+
raise DeclarationError("type name is required")
|
|
248
|
+
|
|
249
|
+
lines, ending = split_lines(text)
|
|
250
|
+
notes: list[str] = []
|
|
251
|
+
|
|
252
|
+
try:
|
|
253
|
+
_find_declaration_line(lines, name)
|
|
254
|
+
except DeclarationError as exc:
|
|
255
|
+
if "not declared" not in str(exc):
|
|
256
|
+
raise
|
|
257
|
+
else:
|
|
258
|
+
raise DeclarationError(f"variable {name!r} is already declared")
|
|
259
|
+
|
|
260
|
+
declaration = format_declaration(
|
|
261
|
+
name, type_name, address, initial_value, description
|
|
262
|
+
)
|
|
263
|
+
blocks = find_blocks(lines)
|
|
264
|
+
def block_group(block):
|
|
265
|
+
current = None
|
|
266
|
+
for line in lines[:block.start]:
|
|
267
|
+
match = re.match(r"^\s*\(\*Group:\s*(.*?)\*\)", line)
|
|
268
|
+
if match: current = match.group(1)
|
|
269
|
+
return current
|
|
270
|
+
target = next((b for b in reversed(blocks) if b.keyword == section
|
|
271
|
+
and (group is None or block_group(b) == group)), None)
|
|
272
|
+
if group is not None and target is None:
|
|
273
|
+
raise DeclarationError(f"No native {section} block for group {group!r}")
|
|
274
|
+
|
|
275
|
+
if target is not None:
|
|
276
|
+
lines.insert(target.end, declaration)
|
|
277
|
+
else:
|
|
278
|
+
notes.append(f"no {section} block existed; appended a new one")
|
|
279
|
+
# Separate with exactly one blank line, reusing a trailing blank if the
|
|
280
|
+
# file already ends with blanks rather than adding another.
|
|
281
|
+
#
|
|
282
|
+
# This is what makes add and delete symmetric. Appending unconditionally
|
|
283
|
+
# puts the new block below a growing tail of blanks, and delete cannot tell
|
|
284
|
+
# that tail apart from blanks the file legitimately ended with -- a stress
|
|
285
|
+
# matrix caught exactly that as stray lines left behind on every cycle.
|
|
286
|
+
if lines and lines[-1].strip():
|
|
287
|
+
lines.append("")
|
|
288
|
+
lines.append(section)
|
|
289
|
+
lines.append(declaration)
|
|
290
|
+
lines.append("END_VAR")
|
|
291
|
+
return join_lines(lines, ending), notes
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def edit_variable(
|
|
295
|
+
text: str,
|
|
296
|
+
name: str,
|
|
297
|
+
new_name: str | None = None,
|
|
298
|
+
type_name: str | None = None,
|
|
299
|
+
address: str | None = None,
|
|
300
|
+
initial_value: str | None = None,
|
|
301
|
+
description: str | None = None,
|
|
302
|
+
clear_address: bool = False,
|
|
303
|
+
) -> tuple[str, list[str]]:
|
|
304
|
+
"""Rewrite a variable's declaration line, returning ``(new_text, notes)``.
|
|
305
|
+
|
|
306
|
+
Only the fields supplied are changed. ``clear_address`` exists because
|
|
307
|
+
``address=None`` means "leave the address alone", so removing an ``AT``
|
|
308
|
+
clause needs an explicit flag rather than an ambiguous default.
|
|
309
|
+
"""
|
|
310
|
+
from .variables import parse_declarations
|
|
311
|
+
|
|
312
|
+
lines, ending = split_lines(text)
|
|
313
|
+
index = _find_declaration_line(lines, name)
|
|
314
|
+
# The declaration parser expects a complete block, so wrap the single line
|
|
315
|
+
# in a temporary one rather than parsing a bare fragment.
|
|
316
|
+
probe = "\r\nVAR\r\n" + lines[index] + "\r\nEND_VAR\r\n"
|
|
317
|
+
existing = parse_declarations(probe, source_stream=None)
|
|
318
|
+
candidate = next(
|
|
319
|
+
(v for v in existing.variables if v.name.casefold() == name.casefold()), None
|
|
320
|
+
)
|
|
321
|
+
if candidate is None:
|
|
322
|
+
raise DeclarationError(f"could not parse the declaration of {name!r}")
|
|
323
|
+
current = candidate
|
|
324
|
+
|
|
325
|
+
if new_name and new_name != name:
|
|
326
|
+
try:
|
|
327
|
+
_find_declaration_line(lines, new_name)
|
|
328
|
+
except DeclarationError as exc:
|
|
329
|
+
if "not declared" not in str(exc):
|
|
330
|
+
raise
|
|
331
|
+
else:
|
|
332
|
+
raise DeclarationError(f"variable {new_name!r} already exists")
|
|
333
|
+
|
|
334
|
+
final_name = new_name or current.name
|
|
335
|
+
final_type = type_name if type_name is not None else current.type_name
|
|
336
|
+
if clear_address:
|
|
337
|
+
final_address = None
|
|
338
|
+
elif address is not None:
|
|
339
|
+
final_address = address
|
|
340
|
+
else:
|
|
341
|
+
final_address = current.address
|
|
342
|
+
final_initial = (
|
|
343
|
+
initial_value if initial_value is not None else current.initial_value
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
# Description handling: an empty string clears it, None preserves it.
|
|
347
|
+
if description is None:
|
|
348
|
+
final_description = current.description
|
|
349
|
+
else:
|
|
350
|
+
final_description = description or None
|
|
351
|
+
|
|
352
|
+
lines[index] = format_declaration(
|
|
353
|
+
final_name, final_type, final_address, final_initial, final_description
|
|
354
|
+
)
|
|
355
|
+
notes = [f"rewrote line {index + 1}"]
|
|
356
|
+
return join_lines(lines, ending), notes
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def delete_variable(
|
|
360
|
+
text: str,
|
|
361
|
+
name: str,
|
|
362
|
+
remove_empty_blocks: bool = True,
|
|
363
|
+
) -> tuple[str, list[str]]:
|
|
364
|
+
"""Remove a declaration, returning ``(new_text, notes)``.
|
|
365
|
+
|
|
366
|
+
By default an emptied block is removed too, so the text does not accumulate
|
|
367
|
+
stray ``VAR``/``END_VAR`` pairs. MotionWorks tolerates empty blocks, but
|
|
368
|
+
leaving them behind makes later diffs noisy.
|
|
369
|
+
"""
|
|
370
|
+
lines, ending = split_lines(text)
|
|
371
|
+
index = _find_declaration_line(lines, name)
|
|
372
|
+
del lines[index]
|
|
373
|
+
notes = [f"removed the declaration of {name!r}"]
|
|
374
|
+
|
|
375
|
+
if remove_empty_blocks:
|
|
376
|
+
blocks = find_blocks(lines)
|
|
377
|
+
removed: list[str] = []
|
|
378
|
+
for block in reversed(blocks):
|
|
379
|
+
if block.end == block.start + 1:
|
|
380
|
+
# Take the blank padding immediately in front of the block, but
|
|
381
|
+
# stop at any real content -- and never walk past the start of the
|
|
382
|
+
# text.
|
|
383
|
+
#
|
|
384
|
+
# An earlier version also consumed a preceding ``(*Group:...*)``
|
|
385
|
+
# marker and every blank before it. On a POU whose declaration
|
|
386
|
+
# file contains only a group marker (a real case: a POU with no
|
|
387
|
+
# variables), removing a just-added block then emptied the whole
|
|
388
|
+
# file, destroying the marker. A group marker is content that was
|
|
389
|
+
# never ours to delete.
|
|
390
|
+
start = block.start
|
|
391
|
+
while start > 1 and not lines[start - 1].strip():
|
|
392
|
+
start -= 1
|
|
393
|
+
del lines[start : block.end + 1]
|
|
394
|
+
removed.append(block.keyword)
|
|
395
|
+
if removed:
|
|
396
|
+
notes.append(
|
|
397
|
+
"removed now-empty block(s): " + ", ".join(reversed(removed))
|
|
398
|
+
)
|
|
399
|
+
return join_lines(lines, ending), notes
|