Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
6a1898d
add ladder diagram renderer for PLCopen XML exports
Aug 4, 2026
67d5e55
add FBD renderer and Structured Text emitter
Aug 4, 2026
ee39f4a
draw the diagrams with Unicode box characters
Aug 5, 2026
bde8cf2
render graphical POUs during Export To Files
Aug 5, 2026
23e8dd4
add a script to diagnose PLCopen parsing inside CODESYS
Aug 5, 2026
fcc94ef
strip the byte order mark before parsing PLCopen xml
Aug 5, 2026
d36f7f9
escape non-ASCII before handing PLCopen xml to the parser
Aug 5, 2026
7b0244f
stop silently dropping jumps, inline ST and negated inputs
Aug 5, 2026
4ad2b20
silence the xmllib warning and report what rendering costs
Aug 5, 2026
b5ba0f3
ignore Claude Code's local tool permissions
Aug 5, 2026
d006924
stop dropping and inverting logic in LD and FBD renderings
gsokoll Aug 5, 2026
d612fb4
hold the rendering-failure barrier around its own scaffolding
gsokoll Aug 5, 2026
c185983
honour negation bubbles on block pins and LD outVariable stores
gsokoll Aug 5, 2026
e57380d
keep the NOT on flattened block outputs and spaceless compounds
gsokoll Aug 5, 2026
67086e9
Merge branch 'main' into feat/ladder-renderer
gsokoll Aug 5, 2026
5325a94
group a fan-out into one network instead of splitting it
Aug 6, 2026
886f471
write only the diagram, not an ST rendering alongside it
Aug 6, 2026
58bce55
split the cost report into parsing and drawing
Aug 6, 2026
7562558
parse XML through .NET where it is available
Aug 6, 2026
bbe3052
keep whitespace so both XML backends agree
Aug 6, 2026
e245375
report where the two XML backends disagree
Aug 6, 2026
c664eb5
normalise line endings in the .NET backend's text
Aug 6, 2026
4f40894
stop walking the whole document to find two POUs
Aug 6, 2026
c8cebb2
keep comments, pragmas and attributes in the declaration
Aug 6, 2026
e8aef6f
try every export_xml shape, and say when the declaration is rebuilt
Aug 6, 2026
892d1cb
probe a real POU in the declaration diagnostic
Aug 6, 2026
d8a8fb7
look for the plaintext declaration outside <interface> too
Aug 6, 2026
9765856
walk the whole addData subtree for the plaintext declaration
Aug 6, 2026
0b1524a
preserve graphical POU declarations
Aug 20, 2026
c860635
harden graphical renderer review output
Aug 20, 2026
5b33acb
document graphical declaration sources
Aug 20, 2026
a7b9d61
give a box on a side pin its own call
Sep 7, 2026
98d0ddd
call a block once however many of its pins are read
Sep 7, 2026
54cc59a
number LD networks, not LD outputs
Sep 7, 2026
fd087ab
show the set and reset on a store
Sep 7, 2026
5dccd3b
show edge detection on a block pin
Sep 7, 2026
21f3cf7
number the networks the editor shows
Sep 7, 2026
2795286
centre a box title the way CODESYS does
Sep 7, 2026
47fbe67
draw a shared box once even when a reader is nested
Sep 7, 2026
cf2b3fd
stop reading a box's EN pin as one of its operands
Sep 7, 2026
266a2f9
write the equivalent Structured Text as its own file
Sep 7, 2026
62fd2ae
render an action or method from its own body, not its parent's
Sep 7, 2026
89e3b89
hang a store off its pin, and branch a shared box instead of repeatin…
Sep 7, 2026
9707d54
add the GraphicalTesting sources as a worked example
Sep 7, 2026
4d27099
head a network with its title, and put the comment under it
Sep 7, 2026
5cd3f82
note where the template will be tracked once it is cleaned
Sep 7, 2026
c3900b1
number the networks as the editor does, from the native export
Sep 7, 2026
7b10066
track the template, with the controller address and serial taken out
Sep 7, 2026
6d61217
draw a value feeding a side pin outside the box, not in it
Sep 7, 2026
80cd6e7
write a jump label the way ST writes one
Sep 7, 2026
abb9210
write a jump the way ST writes one, like the label it targets
Sep 7, 2026
9f55fca
take the ST rendering off the export path, keep it as a script
Sep 7, 2026
bb25272
re-export GraphicalTesting: network 2 switched back on, and a new ST POU
Sep 7, 2026
c0a1fa4
pin the line endings of the export, so a clone matches what CODESYS w…
Sep 7, 2026
fb5267b
correct the docs the ST removal left behind
Sep 7, 2026
ef5e007
export the library references and the visualisation manager read-only
Sep 7, 2026
d1c0f61
say so when the network list cannot be used, and correct a borrowed r…
Sep 7, 2026
752cd12
guard against an empty export, and name what was skipped
Sep 7, 2026
5fdc9c6
close the outstanding notes from the review
Sep 7, 2026
e6faf7c
document what closing the review notes changed
Sep 7, 2026
7ad1c82
keep the export's device and fieldbus xml out of the repo
Sep 7, 2026
45057d8
wire each reader of a shared box to the pin it reads
Sep 11, 2026
eb41df5
show the body of an EXECUTE box wherever the box sits in the network
Sep 11, 2026
bd00b46
wire every pin an expression reads from a shared box
Sep 11, 2026
6b7a6c1
keep a jump label on its network, never in its body
Sep 11, 2026
e3388d3
pin the label of an out-commented network to that network
Sep 11, 2026
65ab616
render a wired OR into a block pin as an OR, not two pins
Sep 11, 2026
29e490a
draw a fan-out across two pins with a column per pin
Sep 11, 2026
7fc8a06
state the stores and enables an operator box carries in the ST
Sep 11, 2026
f97b2cc
stop the export tree being dropped, and tidy two export edges
Sep 11, 2026
c3562ea
do not tell a method's rendering its declaration is the parent's
Sep 11, 2026
394c182
correct stale and overstated claims in the docs
Sep 11, 2026
04d60a0
redraw a stateless operator read by a second rung, do not name it
Sep 14, 2026
f1c79f9
draw an EXECUTE box's inline ST inside the box, not beneath it
Sep 14, 2026
c5f36b9
draw the shared head of ladder branches once, then split
Sep 14, 2026
a28307d
merge separate ladder rungs that split after a shared box
Sep 14, 2026
20eeb1f
expand tabs in an EXECUTE box body so the wall stays straight
Sep 14, 2026
01c47e2
re-export GraphicalTesting after editing the test POUs
Sep 14, 2026
95077a1
re-export LD_TEST
Sep 14, 2026
a00e0ac
tee a block output a contact reads without naming the pin
Sep 14, 2026
96a2c67
draw a parallel's block branch as the main line
Sep 14, 2026
de3e9de
Revert "draw a parallel's block branch as the main line"
Sep 14, 2026
108aca5
draw a contact feeding a block's side pin as that contact
Sep 14, 2026
c8bf69e
run each terminal branch to the right rail instead of rejoining
Sep 14, 2026
7e769cc
draw a side pin's own bubble or edge on the box wall
Sep 15, 2026
1ff84d3
bring the README's sample network up to date with the rendering
Sep 15, 2026
8a99c74
re-export LD_TEST
Sep 15, 2026
122a0b6
keep CODESYS working files out of the export folder again
gsokoll Sep 16, 2026
3e867d3
treat elements as one only when they are one node, and number ambiguo…
gsokoll Sep 16, 2026
0123ce7
draw every ladder box once, keep the box a hoist reads, and close lab…
gsokoll Sep 16, 2026
17e480b
give an FBD in-out pin its own row, and join a box read on two pins
gsokoll Sep 16, 2026
71632b2
apply black and isort to the Python files this PR touches
gsokoll Sep 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Without this file, line endings are decided by each developer's
# core.autocrlf. The same commit then checks out differently on different
# machines, and whoever has it set the other way sees every exported file
# rewritten the next time they export - a diff of the whole tree that says
# nothing about what changed.
* text=auto

# CODESYS writes CRLF, and so does the export. These files are compared
# against what CODESYS writes and imported back into it, so they check out
# CRLF everywhere, whatever git is configured to do locally. Only the working
# tree is pinned; git still stores them normalised, so diffs stay line by
# line rather than whole-file.
GraphicalTesting/** text eol=crlf
tools/ladder/tests/fixtures/codesys/** text eol=crlf

# A .project is a zip. Converting one would corrupt it.
*.project binary
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ jobs:
- uses: actions/checkout@v4
- name: Fail on non-ASCII bytes in src/*.py
run: python3 tools/ci/check_ascii.py
- name: Compile src files with Python 3
run: python3 tools/ci/compile_python3.py

ironpython:
name: ironpython
Expand All @@ -30,3 +32,33 @@ jobs:
- name: Import smoke test with stubbed scriptengine
shell: pwsh
run: .\ipy\net45\ipy.exe tools\ci\import_smoke.py

# The renderers are destined for src/, so they have to pass under the
# same interpreter CODESYS embeds - not just under CI's Python 3.
- name: Renderer tests under IronPython 2.7
shell: pwsh
run: |
# A native exe's exit code does not halt a pwsh script, so a failure
# in the first suite would otherwise be masked by the second passing.
.\ipy\net45\ipy.exe tools\ladder\tests\test_ladder.py
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
.\ipy\net45\ipy.exe tools\ladder\tests\test_fbd.py
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
.\ipy\net45\ipy.exe tools\ladder\tests\test_export.py
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
# The only host with both XML backends, so the only place their
# equivalence can actually be checked.
.\ipy\net45\ipy.exe tools\ladder\tests\test_xmlbackend.py
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

ladder:
name: ladder
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Renderer tests under Python 3
run: |
python3 tools/ladder/tests/test_ladder.py
python3 tools/ladder/tests/test_fbd.py
python3 tools/ladder/tests/test_export.py
python3 tools/ladder/tests/test_xmlbackend.py
54 changes: 54 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,40 @@
# CODESYS working files. None of these belong in the repository:
# a .project is a large binary that every clone then carries for ever, and it
# and the .opt session files carry device identifiers - serial numbers and
# gateway addresses - along with the user and machine name in the .~u lock.
# The template and the exported sources together regenerate the project.
*.project
*.opt
*.~u
# The template is the exception: it and the exported sources together
# regenerate the working project, so it is tracked. Its connection label has
# had the controller address and serial taken out of it - see the commit that
# tracked it for why clearing the gateway in CODESYS is not enough.
!GraphicalTesting_template_v1.project

# Review correspondence. Kept beside the checkout, not in it.
review/

# The export's device and fieldbus xml. 449,000 lines of CR711S and CAN bus
# descriptions that say nothing about the code under review, and 36 MB that
# every clone would carry for ever. The application folders are what is
# tracked; a full export still writes these, they are just not committed.
GraphicalTesting/*/devices/
GraphicalTesting/*/communication/

# The staging and backup copies util.py writes beside an export. A locked
# target leaves one behind, and a stray "git add -A" would sweep the whole
# export into the repo under it - which is how the device xml got committed
# once already.
*.codescribe_staging/
*.codescribe_backup/

.vscode/

# Claude Code's per-developer tool permissions. Machine-specific paths, and
# permission grants that should not be inherited by whoever clones the repo.
.claude/

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down Expand Up @@ -160,3 +195,22 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

# The exported source tree is tracked in full, so none of the generic Python
# and tooling patterns above may reach into it: a POU or a folder a developer
# names "lib", "build", "var" or "target" would otherwise be dropped from the
# export with no warning. Re-include the directories first and then their
# files - git will not descend into a directory a bare pattern excluded - and
# re-exclude only the two heavy subtrees that are deliberately left out.
!GraphicalTesting/**/
!GraphicalTesting/**
GraphicalTesting/*/devices/
GraphicalTesting/*/communication/
# The re-include above is the last pattern to match inside the export, so it
# cancels the CODESYS working-file rules at the top of this file as well. Those
# carry the controller serial and gateway address; exclude them again here.
GraphicalTesting/**/*.project
GraphicalTesting/**/*.opt
GraphicalTesting/**/*.~u
GraphicalTesting/**/*.codescribe_staging/
GraphicalTesting/**/*.codescribe_backup/
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@

## Unreleased

- Ladder and Function Block Diagram POUs export a derived `<name>.txt` beside their native xml, holding the declaration and a diagram of each network. Graphical POUs have no textual implementation, so the native xml was the only thing tracked and no change to one could be reviewed in a diff. The declaration is copied from the CODESYS declaration source, keeping comments, pragmas, safety-qualified types and literal spelling; the diagram is derived from a PLCopen export, and its layout comes from how the elements are wired rather than from their coordinates, so moving a block in the editor produces no diff. Networks are numbered as the editor numbers them, read from the native xml beside the rendering: an out-commented or empty network keeps its number and says why it has no diagram, instead of being dropped and renumbering every network after it. A graphical action, transition or method is rendered from its own body, not from the parent POU that its PLCopen export wraps it in. The file is derived and read-only - `Import From Files` dispatches on `.xml` and `.st`, so a `.txt` is ignored by construction and the round trip is unaffected. SFC and CFC POUs are not yet rendered and export as native xml alone. An EXECUTE box shows the inline ST that is its whole body inside the box, below its pins, rather than as an empty rectangle. Where the export carries no plaintext declaration the rendering is rebuilt from the structured interface and says so on its first line, and a variable whose type the export omits reads `UNKNOWN` rather than being assumed to be a `BOOL`. The export summary reports how many POUs were rendered and which were skipped for having no renderable body; a POU whose rendering raises is reported on its own warning line as the export carries on. An equivalent-Structured-Text rendering of the same networks is available from `tools/ladder/write_st.py`, but the export does not write one.
- An export that produces no files no longer replaces the previous export folder with an empty one; it fails with an explanation and leaves the folder untouched. `Export Lib To Files` on a device project was the trigger: its walker only exports objects directly under the project root, so on a device project it exported nothing and wiped the existing export. That script now also refuses a project with devices up front and points to `Export To Files`.
- The Library Manager exports its reference list as a read-only `<name>.libraries.txt` (name, version, vendor per line). Library behaviour itself is not exportable, but any bench check of a library needs the version it resolves to - shown where CODESYS reports it, and as the requested constraint (such as `*` for newest) where it does not.
- The Visualization Manager exports natively as a read-only `<name>.service.txt`. It is exported recursively to take in the target and web visualization settings under it; the global hotkey (key configuration) mapping is in the manager entry itself. Both become reviewable. It is still never imported - importing it raises interactive overwrite dialogs, which is why earlier versions dropped it from the export entirely - and the project template continues to carry the real object.

- Visualisations export as `<name>.vis.xml`. Earlier versions wrote `<name>.xml`, which silently collided with any POU of the same name (a `Main` program plus a `Main` visualisation is common). Old plain `.xml` exports still import correctly; re-exporting once migrates the tracked files.
- Visualisation service objects (visualisation manager and related service GUIDs) are recognised and no longer exported.
- `Export To Files` writes the export into a sibling folder named `<project_name>.codescribe_staging` and only swaps it into place once the export completes. Earlier versions deleted the target folder up front, so a locked folder (an open Explorer window, IDE, git client or antivirus) aborted the export immediately and a mid-export crash left the on-disk copy destroyed. If the target folder is locked and cannot be swapped, the staged files are synced into it instead and the export still succeeds; if that also fails, the error dialog reports the staging folder path, so the completed export is preserved.
- `Export To Files` writes the export into a sibling folder named `<project_name>.codescribe_staging` and only swaps it into place once the export completes. Earlier versions deleted the target folder up front, so a locked folder (an open Explorer window, IDE, git client or antivirus) aborted the export immediately and a mid-export crash left the on-disk copy destroyed. If the target folder is locked and cannot be swapped, the staged files are synced into it instead and the export still succeeds; if that also fails, the error dialog reports the staging folder path, so the completed export is preserved. Syncing also removes files the export did not write, as the swap would have, so a rendering from a previous export is never left beside a newer native xml; any that cannot be removed are named.
- Device-tree sibling devices (Ethernet, Modbus and fieldbus devices that sit next to `Plc Logic` as direct children of the PLC device) are exported to `<device>/devices/<name>.xml`, one file per top-level device. On import, each tracked device is removed and recreated from its exported xml; devices fixed by the device package (for example `Local_IO` and `HMI` on IFM hardware) cannot be removed and are skipped with a message, with their configuration carried by the project template. A `_NO_EXPORT` folder added as a direct child of the PLC device disables the device-tree export.
- New `Export Lib To Files` script exports library projects, which keep their objects directly under the project root rather than under a Device. It writes the same on-disk format as `Export To Files`, without the device and application folder levels. Importing a library export back into a project is not yet supported.

Expand Down
7 changes: 7 additions & 0 deletions GraphicalTesting/SafetyPLC/application/EVL.gvl.st
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
(* THIS EVL IS AUTOGENERATED. DO NOT MODIFY THESE ATTRIBUTES. *)
{attribute 'location' := '16#800' }
{attribute 'linkalways'}
{attribute 'qualified_only'}
VAR_GLOBAL

END_VAR
90 changes: 90 additions & 0 deletions GraphicalTesting/SafetyPLC/application/EVL.gvl.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<ExportFile>
<StructuredView Guid="{d9b2b2cc-ea99-4c3b-aa42-1e5c49e65b84}">
<Single xml:space="preserve" Type="{3daac5e4-660e-42e4-9cea-3711b98bfb63}" Method="IArchivable">
<Null Name="Profile" />
<List2 Name="EntryList">
<Single Type="{6198ad31-4b98-445c-927f-3258a0e82fe3}" Method="IArchivable">
<Single Name="IsRoot" Type="bool">True</Single>
<Single Name="MetaObject" Type="{81297157-7ec9-45ce-845e-84cab2b88ade}" Method="IArchivable">
<Single Name="Guid" Type="System.Guid">c92bf834-0a70-4e8b-9367-2d81823c2f23</Single>
<Single Name="ParentGuid" Type="System.Guid">4403a37e-f528-4863-8b5f-3c6def9b45a5</Single>
<Single Name="Name" Type="string">EVL</Single>
<Dictionary Type="{2c41fa04-1834-41c1-816e-303c7aa2c05b}" Name="Properties">
<Entry>
<Key>
<Single Type="System.Guid">0fc3aa33-c9bf-436a-b5e4-fcd0ccb10d58</Single>
</Key>
<Value>
<Single Type="{0fc3aa33-c9bf-436a-b5e4-fcd0ccb10d58}" Method="IArchivable" />
</Value>
</Entry>
</Dictionary>
<Single Name="TypeGuid" Type="System.Guid">ffbfa93a-b94d-45fc-a329-229860183b1d</Single>
<Array Name="EmbeddedTypeGuids" Type="System.Guid">
<Single Type="System.Guid">a9ed5b7e-75c5-4651-af16-d2c27e98cb94</Single>
</Array>
<Single Name="Timestamp" Type="long">0</Single>
</Single>
<Single Name="Object" Type="{ffbfa93a-b94d-45fc-a329-229860183b1d}" Method="IArchivable">
<Single Name="Interface" Type="{a9ed5b7e-75c5-4651-af16-d2c27e98cb94}" Method="IArchivable">
<Single Name="TextDocument" Type="{f3878285-8e4f-490b-bb1b-9acbb7eb04db}" Method="IArchivable">
<Array Name="TextLines" Type="{a5de0b0b-1cb5-4913-ac21-9d70293ec00d}">
<Single Type="{a5de0b0b-1cb5-4913-ac21-9d70293ec00d}" Method="IArchivable">
<Single Name="Id" Type="long">59</Single>
<Null Name="Tag" />
<Single Name="Text" Type="string">(* THIS EVL IS AUTOGENERATED. DO NOT MODIFY THESE ATTRIBUTES. *)</Single>
</Single>
<Single Type="{a5de0b0b-1cb5-4913-ac21-9d70293ec00d}" Method="IArchivable">
<Single Name="Id" Type="long">60</Single>
<Null Name="Tag" />
<Single Name="Text" Type="string">{attribute 'location' := '16#800' }</Single>
</Single>
<Single Type="{a5de0b0b-1cb5-4913-ac21-9d70293ec00d}" Method="IArchivable">
<Single Name="Id" Type="long">61</Single>
<Null Name="Tag" />
<Single Name="Text" Type="string">{attribute 'linkalways'}</Single>
</Single>
<Single Type="{a5de0b0b-1cb5-4913-ac21-9d70293ec00d}" Method="IArchivable">
<Single Name="Id" Type="long">62</Single>
<Null Name="Tag" />
<Single Name="Text" Type="string">{attribute 'qualified_only'}</Single>
</Single>
<Single Type="{a5de0b0b-1cb5-4913-ac21-9d70293ec00d}" Method="IArchivable">
<Single Name="Id" Type="long">63</Single>
<Null Name="Tag" />
<Single Name="Text" Type="string">VAR_GLOBAL</Single>
</Single>
<Single Type="{a5de0b0b-1cb5-4913-ac21-9d70293ec00d}" Method="IArchivable">
<Single Name="Id" Type="long">64</Single>
<Null Name="Tag" />
<Single Name="Text" Type="string"></Single>
</Single>
<Single Type="{a5de0b0b-1cb5-4913-ac21-9d70293ec00d}" Method="IArchivable">
<Single Name="Id" Type="long">65</Single>
<Null Name="Tag" />
<Single Name="Text" Type="string">END_VAR</Single>
</Single>
<Single Type="{a5de0b0b-1cb5-4913-ac21-9d70293ec00d}" Method="IArchivable">
<Single Name="Id" Type="long">1</Single>
<Null Name="Tag" />
<Single Name="Text" Type="string"></Single>
</Single>
</Array>
</Single>
</Single>
<Null Name="NetVarProperties" />
<Single Name="ParameterList" Type="bool">False</Single>
<Single Name="AddAttributeSubsequent" Type="bool">False</Single>
</Single>
<Single Name="ParentSVNodeGuid" Type="System.Guid">4403a37e-f528-4863-8b5f-3c6def9b45a5</Single>
<Array Name="Path" Type="string">
<Single Type="string">SafetyPLC</Single>
<Single Type="string">PLC Logic</Single>
<Single Type="string">Application</Single>
</Array>
<Single Name="Index" Type="int">-1</Single>
</Single>
</List2>
<Null Name="ProfileName" />
</Single> </StructuredView>
</ExportFile>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
(* Library references - regenerated by Export To Files; read-only, never imported *)
ifmR360-3, * (ifm electronic)
#IoStandard -> IoStandard, 3.5.10.0 (System)
#CmpSIL2 -> CmpSIL2, 3.5.7.0 (System)
#3SLicense -> 3SLicense, 3.5.10.0 (3S - Smart Software Solutions GmbH)
8 changes: 8 additions & 0 deletions GraphicalTesting/SafetyPLC/application/PLC_PRG.st
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
PROGRAM PLC_PRG
VAR

END_VAR

// --- BEGIN IMPLEMENTATION ---


Loading
Loading