Repository navigation
feat: read CODESYS / Machine Expert .project files offline - #6
Merged
Merged
Conversation
Add scripts/codesys_project.py (list, st, dump-st, graph) and the scripts/codesysproj/ modules behind it: the zip container, the shared string table, .meta and .object records, ST declaration and implementation text, and the element records and network comments of LD/FBD POUs. The .project format is undocumented. The reader is reverse-engineered and checked on Machine Expert V2.x files only; decoded facts and heuristics are labelled in the output, graphical wiring is not decoded, and damaged objects are reported instead of stopping the run. Adds skills/other-platforms/references/codesys-project-files.md, links from the other-platforms and schneider skills and from codesys.md, README and CONTRIBUTING entries, and tests on synthetic projects (86 tests). Bumps the plugin version to 0.2.0 in both plugin.json files.
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait...
|
info prints each device application's CodeId / DataId (the build identity the IDE compares with a controller) and the project's last save time, both byte orders of the id, from the two plain-XML entries of the project. There is no compile time in the project; the page says so and says how to match a saved project to a running PLC. diff compares one POU between two project files (ST text, declarations, and the decoded LD/FBD graph with element ids, header ids, byte offsets, empty slot ids and unreadable-record bytes left out, so constants, modifiers, comments and boxes still show), or lists objects added, removed and changed by guid. Wiring is not decoded and so not compared; the output says so. xref lists every ST and LD/FBD use of a name with a read / write role where one can be told, including a function's or type's own header. Name-based, case-insensitive, no scope resolution; all of it is marked heuristic. graph.py's renderers move to graphview.py and xref's text scanner to stscan.py to keep files under 300 lines; no behaviour change there. Tests are synthetic (two versions of one project, a build-identity entry, an xref project, header declarations); 96 mutants of the new guards and branches were run, none survived.
Input the reader is handed but did not write now ends in a message and an exit code: - one reader for zip entries (zipread.py): a CRC error, a corrupt deflate stream or an encrypted entry is that object's problem, the string table's is fatal (exit 2), and no entry is read past 256 MiB (a zip bomb); a bad UTF-8 entry name is exit 2, not a traceback - the declaration-keyword regex no longer backtracks on a commented-out declaration - info refuses a DOCTYPE or ENTITY declaration in any encoding (expat, not a byte regex), notes an entry with no Device, and ignores a non-ASCII digit save counter - output on a Windows console code page escapes characters it cannot print xref: identifiers in any script, every name of a multi-line list at its own line, EXTENDS and IMPLEMENTS as types, and a quoted constant in a graph element is not a name. diff: a renamed POU is found by its old or new name, each header carries its own version's name, PROBLEM lines say OLD or NEW, and notes are printed when an object also has a problem. st and dump-st print GVL and DUT declarations; names and guids match in any case.
…ked; new behaviour info points to which saved project a controller runs; the IDE's comparison decides. The .compileinfo claims are marked unverified, the empty-string index is documented as observed, and the reference page covers damaged entries, the entry cap, rename lookup in diff, and GVL / DUT declarations in st and dump-st. The skill description names info, diff and xref.
…fore a header From the verification pass: many entries just under the per-entry cap could still fill memory, so one load reads at most 1 GiB in total (exit 2 past it); an EXTENDS or IMPLEMENTS clause on a line of its own was read as a declared name and hid the first variable; a pragma line before `FUNCTION F : T` cut the header's type at the pragma's line end.
SmailG
marked this pull request as ready for review
November 9, 2026 16:28
5 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A reader for CODESYS 3.5 / EcoStruxure Machine Expert
.projectfiles, so a repository that commits only the binary.projectis no longer opaque to the plugin. Today the plugin reads PLCopen XML exports only.scripts/codesys_project.py(standard library, Python 3.9+):list,st <name|guid>,dump-st <outdir>,graph <name|guid> [--json], and three commands that compare and identify builds:info <project>: each device application's CodeId / DataId (the build identity the IDE compares with a controller) and the project's last save time. Both byte orders of the id are printed, because which one the IDE shows was not checked. There is no compile time in the project; the reference page says where it lives (the.compileinfobeside the project file, not in the zip) and how to match a saved project to a running PLC, or to a behaviour change in logged data.diff <old> <new> [<name|guid>]: with a POU, the unified diff of its declaration and implementation (ST) or of its decoded LD/FBD graph; without one, the objects added, removed and changed (matched by guid). For graphs, element ids, box header ids, byte offsets, empty-slot ids and unreadable-record bytes are left out, so only real changes show: elements, operands, constants (500 to 600 shows), modifiers, box types and instances, network comments.xref <project> <name>: every ST and LD/FBD use of a variable, instance or type name, with a read / write role where one can be told. All heuristic: by name only, case-insensitive, no scope resolution.Modules in
scripts/codesysproj/(buildinfo.py,diff.py,xref.pyare new).skills/other-platforms/references/codesys-project-files.md: what is decoded, what is a heuristic, what is not decoded, the safe workflow; sections on comparing versions, finding where a name is used, and which saved project matches a running PLC.tests/test_codesys_project.py+tests/test_codesys_tools.py+tests/codesys_synth.py: tests on synthetic projects built in the test. No real project, name, text or guid is committed.Trust level
The
.projectformat is undocumented. The reader is reverse-engineered and was checked on three Machine Expert V2.x files (CODESYS 3.5.21.50) only. Nothing else is verified: other vendors, service packs, CFC/SFC/IL, methods, actions, properties, visualisations.info(devicesmetainformation.auxiliary,engineeringtoolsversions.auxiliary).~in the text output and underheuristicin the JSON): object kind,IN/OUT,coil, modifier meaning (negated/set/reset), box nesting, everyidslist, and everyxrefrole.diffcannot see a re-wire, and bytes that match no recognised record are skipped: "no differences" means no difference in the compared fields, and the output says so.xrefcannot see objects of kindother(the device tree, I/O mappings) and says how many it did not search.infois a lead, not a verdict. A CodeId match against the controller is decided in the IDE. Of 8 ids checked against the.bootinfo_guidsbeside each project, 7 were there in byte-reversed order and none in plain order; one CodeId was in neither, cause unknown (documented on the page).Safe workflow, also in the reference page: prefer a PLCopen XML export from the IDE; use the reader for ST text, inventories and as a lead; verify graphical wiring in the IDE before acting on it.
Checks
python3 -m unittest discover -s tests: 276 tests pass on Python 3.14 and on 3.9.2.claude plugin validate .passes.git diff --checkclean.info,diff,xrefand their CLI branches: 96 mutants, 96 killed. Two earlier runs left survivors (equivalent mutants removed, the rest missing tests, added); the whole run was repeated after each. A review ofxrefalso found that aFUNCTION F : TorTYPE T : STRUCTheader made the first variable after it read as a type and the keyword as a name; fixed with tests first (red, then green).diffon the same real files: the graph diff of one POU between two saved versions found the same element, comment and box changes as the earlier text-dump diff, plus two four-digit constant changes that diff had normalised away; a POU with identical bytes in two versions gave no difference; the six ST POUsdiffreported as changed between two versions were exactly the six whosedump-stfiles differ byte for byte.infoprinted the build identities and last save times the three files carry. For the one name compared,xreffound the same graphical elements as an earlier regex tool, and the ST uses besides. A real function header and a real struct type readdeclaredfor their first variable. No real graphical object had different bytes and an equal normalised graph, so the id and offset normalisation is shown by the synthetic tests only.Notes for review
dump-stfile names are<Device>_<Name>, where Device is the root-most ancestor of the object (a heuristic); colliding names all get_<first 8 of the guid>.st,graph,diff <pou>refuse an ambiguous name (exit 2) and list the candidates; a guid or a guid prefix of 8+ characters always works.diffexits 0 when the two projects differ: a difference is its result. Exit 1 stays "result printed, but some object had a problem or a decoder note was raised".graph.py's renderers moved tographview.pyandxref's text scanner tostscan.py, so no file this PR adds underscripts/is over 300 lines (a pure move; the first commit'sgraph.pywas 303).Review fixes (a65471b, 6b5e4b8, 1d42e7d)
An independent review of the whole PR found no leak of real project data. Fixed:
inforefuses DOCTYPE/ENTITY in any encoding, through expat. A Windows console code page no longer crashes the output.EXTENDSandIMPLEMENTSread as types; a quoted constant in a graph element is not a name.infois a lead, and the IDE decides. The.compileinfoclaims are marked unverified. The skill description namesinfo,diffandxref.Checks.
.pychad faked one result.list,info,diffandxref(40 names) give byte-identical output before and after.Left as is, with the reason:
infosilent on wrong nesting: covered by the new no-Device note.short_idnot checking the zero tail, anddump-stwriting into an existing directory: behaviour to decide, not a bug.newlineon Windows: not testable on POSIX CI.diffmarking every descendant of a renamed ancestor as moved: true, and noisy only when the Application itself is renamed.Verification pass: findings 1–13 are confirmed fixed. It found two new Mediums in the fix code, fixed in 1d42e7d, each with mutants that make a test fail:
EXTENDS/IMPLEMENTSclause on its own line no longer hides the first variable.Also fixed in 1d42e7d: a pragma line before
FUNCTION F : Tused to break the header (an existing bug).Left as is: the first enum member of a
TYPEis no longer reported as declared. Enums were never parsed, and that one hit was an accident.Summary by CodeRabbit