Changelog archive¶
Older releases, newest first. The current and the two most recent releases are in CHANGELOG.md.
Version 3.2.0 (2026-09-22)¶
Headless PLC CRC probe (advanced):
cts plc-crc-headlessreadsPlcLogic/Application/Application.crcthrough a temporary, selected CODESYS or Astra IDE process. It is independent ofProject_daemon.py, creates a temporary probe project only, and uses the safeKeeplogin mode: it never downloads, updates, starts, or stops the PLC application.- One
--iptarget or a JSON--inputbatch is supported. The result includes the scanned device identity and application CRC; a failure for one PLC is retained as that target's result instead of replaying the whole batch in every discovered IDE. - Opt-in
--watchkeeps one no-UI IDE process alive and writes JSONL cycles at--interval; the session-bound control file stops only its own watch process and is cleaned up afterwards. - Password-protected PLCs can use
CDS_CRC_PLC_USERNAMEandCDS_CRC_PLC_PASSWORDfor a single target, or per-target environment-variable names in batch JSON. Secrets are never copied into request/result JSON, command-line arguments, or logs; interactive credential fallback is disabled.
cts verify — one gate, one verdict:
- New command
cts verify [--sync-folder PATH]runs every applicable check and reduces them to a single verdict with a single exit code, so an agent no longer has to know the names of four separate checks, run them in the right order and combine four outputs. Stages run cheapest-first:analyze(static analysis ofproject-view/),visu-sketch(SVG lint of every sketch in.visu/),build(the compiler, through the daemon), andtest(the.test/plans), with--only a,bto narrow the set. - The gate is strictly read-only — no
--fix, nothing written to the project — which is what makes it safe to run against a live production project. - A stage that could not run is
skipped, neverfail: an agent with no CODESYS open is not told it broke the project. Skipped and errored stages setcomplete: falseinstead, and--incomplete errorturns that into exit code 3 for CI. Exit codes:0pass,1real problems,2could not start,3incomplete. buildonly runs when a 2 spingprobe (--probe-timeout) says the daemon is answering, so a dead daemon costs ~2 s instead of stalling the run.testis opt-in via--with-testbecause it reaches past the project — it connects to a PLC and starts an application.- A daemon refusal (no project open) is reported as
error, distinct from a genuine compile failure (fail). Per-stage problem lists are capped at 20 withproblem_count/truncatedpreserved, and the report ends withnext— concrete commands for the detail behind whatever failed. --fail-on danger|suspicious|stylesets the severity that fails theanalyzestage. Static analysis is opinionated and not every project wants every opinion to block it, so a team with a looser style can dial the gate down per run (--fail-on dangergates on defects and lets style findings through as information) without editing the analyzer's own config. The effective threshold is echoed insummary.fail_on.TYPE X EXTENDS Y : STRUCTis now parsed as the struct inheritance it is. Every declaration parser in the repository shares one implementation, so the missingEXTENDSclause made each inheriting struct look unparseable — on one real project that alone accounted for 366 spurious diagnostics.manifest.jsonis read as UTF-8 instead of the platform default, so a project whose object names carry non-ASCII characters no longer fails to load on a Windows console codepage.- The daemon no longer carries a version of its own: the banner and dashboard now report the release version (
3.2.0) instead of a separate2.8.xtrack that said nothing about which release was installed. - Every view-folder text read now goes through one
utf-8-sigreader: a file re-saved with a BOM (e.g. by Notepad) no longer shows up as a phantom modification, and a file re-saved as ANSI/cp1251 is reported by name instead of crashing with a bareUnicodeDecodeError.exportalso no longer treats an unreadable file as clean — it is now reported dirty so it can't be silently overwritten.
Version 3.1.5 (2026-09-12)¶
cts visu bind splits "draw" from "wire signals":
- New offline subcommand:
cts visu bind --svg <sketch> --elem N [--var|--text-var|--tap|--toggle|--color|--action NAME] [--clear]patches only the bindingdata-*attributes on one already-drawn SVG element — geometry, fill, stroke and gradients are structurally untouched, and passing a flag the element type doesn't support (e.g.--tapon a lamp) is refused. cts visu --helpis now a two-phase workflow: PHASE 1 draws (small steps: add an element,preview,lint, repeat, thenfrom-svg --create-screen), PHASE 2 wires signals (bind, thenfrom-svg --replace --gvlto recompile in place). The top-levelcts --helpnudges the same split.cts visu lintfindings for unbound buttons/textfields/lamps and static-looking text now namects visu binddirectly instead of only describing the problem.
Gradient fills and light/dark colour schemes for from-svg:
<linearGradient>/<radialGradient>in<defs>, referenced viafill="url(#id)"onrect/circle/ellipse, compile to CODESYS's native gradient fill (two colour stops, angle from the SVG vector,stop-opacityas the alpha byte). Round-trips throughto-svgas well.--scheme light|dark(alsodata-cds-schemerecorded bycts visu new) resolves the stylesheet's CSS-variable palette against either scheme for preview/lint/from-svg.
.visu/ becomes the default sketch folder:
cts visu newwith no--outnow writes into.visu/(auto-created) instead of the current directory, so sketch.svg/.preview.svg/.preview.pngscratch files stop cluttering the project root;preview/lint/from-svgfollow the sketch's own location automatically..visu/was added to the CODESYS-host options dialog's recommended.gitignoreentries.
Clearer guidance for cts import's modal dialog and daemon timeouts:
cts import --helpand the top-levelcts --helpnow spell out that creating/replacing a native object (a screen, a POU, ...) commonly pops a CODESYS confirmation dialog, that the daemon is single-threaded so a stuck dialog stalls every other command includingcts ping, and that killing the CLI does not cancel the operation — checkcts status/cts compareinstead of blind-retrying.- The compiled-XML validator now distinguishes itself from the SVG-sketch validator by name.
cts docs produces compact documentation instead of a source dump:
- The generator no longer walks the whole CODESYS installation tree and no longer emits raw source text. It now documents the project's own POUs and only the libraries the project actually references, each as symbol + description + interface table.
- Referenced libraries are read from the exported Library Manager objects (
project-view/**/Library Manager.xml). Every manager is read and merged — a project typically has more than one (application plus visualization), and reading only one silently loses libraries. - Library descriptions and interfaces come from the installed LibDoc trees under
C:\ProgramData\CODESYS\LibDoc. TheScopecolumn there is emitted withrowspanover a group of rows, so the parser carries the last seen scope forward; without that, parameters quietly land in the wrong scope. - Unresolved references are reported, not worked around: a referenced library with no LibDoc on the machine is listed under Missing LibDoc rather than resolved to some other version. Installed-but-unreferenced libraries are listed by name only, with an explicit note that adding one is a user action in the IDE.
- Breaking:
bundle.mdis gone (a stale copy is deleted on the next run) andsymbols.jsonlchanged from one record per file to one record per symbol:{source, library, version, kind, name, path, line, description, interface}.manifest.jsonis nowformat: "cts-docs/v2"with symbol-level counts andlibraries_missing. - The daemon endpoint
generate_docswas a full IronPython 2.7 mirror of the old generator, kept format-compatible by hand. It is now a sync-folder resolver only; generation always runs locally in CPython. - Fixed two rendering nuances: triple-slash (
///) doc comments no longer leave a leading/in the rendered description, and projectFUNCTION/METHOD/PROPERTYdeclarations now get a leadingReturnrow in the interface table, matching how library functions are documented.
Sync-folder setup is now agent-controllable:
cts set-sync-folder [PATH] [--save]writes the active project'scds-sync-folderproperty through the daemon. OmittingPATHautomatically selects the saved project directory (.); relative paths (sync,./sync,../sync) use that directory, while fully qualified absolute paths are accepted directly.- The response reports both stored and resolved paths and calls out unsaved project state.
--savepersists the setting, with the same warning as import that saving also commits other pending IDE edits.
Consistent project-relative sync directories (GH #65):
../syncand./../syncnow select the same absolute directory in daemon operations, legacy menus, external UI, project discovery and snapshooter settings/logs. Previously some consumers used the process working directory, potentially missing the intended Text-first settings.- All consumers share the saved-project path lookup, including
FullName/filenamefallbacks and Unicode paths. Relative folders require an absolute saved project path; fully qualified drive and UNC paths work without a saved project. - Ambiguous Windows paths (
D:sync,D:,\sync) are rejected explicitly, independent of Python's version-dependentisabsbehavior. Menu and CLI validation happens before updating project properties.
Version 3.1.1 (2026-08-17)¶
Daemon Settings could never save (GH #64):
- Unchecking a default-denied operation in the daemon's Settings window always failed with "Failed to save settings", on every project and for every combination of checkboxes.
SettingsForm._save_config()reached for_save_daemon_configthroughide_reverse_pipe_loop, which re-exports only the_load_daemon_configtwin — the save half lives inide_daemon_stateand was never in the loop's import list. The resultingImportErrorwent into a bareexcept Exception: return False, so it surfaced only as the generic error dialog. Because the load path was re-exported, the window opened normally and showed the default deny list, which is what made the broken save easy to miss: the deny list was effectively hard-coded to the defaults. - Both helpers now come straight from
ide_daemon_state. The loop was only ever a pass-through here, and routing through it is what let the two halves drift apart. - The same shape had already broken
_cmd_help(): it importsHELP_TEXTfromcommand_registry, which defined the dict ashelp_text. That import site is the dict's only reader anywhere in the tree, and every other module-level constant in the file is upper case, so the definition is renamed rather than the import. - Regression guard:
tests/unit/test_daemon_deferred_imports.py. The existingtest_daemon_name_resolutioncannot see either defect — it walks bytecode forLOAD_GLOBALafter importing the module, but a deferredfrom X import Yinside a function compiles toIMPORT_FROMand binds a local, andide_daemon_uiis skipped outright as WinForms-only. The new guard parses instead of importing: for everyfrom <sibling> import <name>at any depth it checks the name against the target's top-level bindings, so the UI modules are covered too. - Daemon
VERSIONmoves to 2.8.4 socts statusand the startup banner distinguish a fixed daemon from a broken one. - The CPython packages are unchanged from 3.1.0 and still report
3.1.0: nothing outsideproducts/codesys-hostwas touched, and the daemon carries its own version.cts --versiontherefore reads3.1.0on a v3.1.1 install — check the daemon version instead.
Verified live on a real project: the deny list saves, cts permissions reports it, and it survives closing and reopening the project — so get_project_info() hands back a live reference and the write reaches the stored property.
Reported and diagnosed by eddiedon in #64.
Version 3.1.0 (2026-08-16)¶
cts fsm - read a CASE state machine as a GRAFCET diagram.
Understanding an unfamiliar state machine means holding a CASE block in your
head: which branch writes the state variable, what has to be true for it to
fire, and where the flow can get back to. cts fsm reads that off the exported
.st files and draws it. Like cts analyze it is offline — only
project-view/ is read, no daemon, no .dump/, no CODESYS running, and
nothing is sent anywhere.
fsm scan reports every machine in the workspace, fsm show renders one as
JSON, mermaid, PlantUML or SVG, and fsm ui opens the map window.
Project_fsm.py opens that same window from Tools > Scripting for the
project's configured sync folder.
The diagram follows IEC 60848: numbered steps, one bar per transition with its receptivity beside it, a step with several ways out fanned sideways as an OR divergence, and branches that end in the same place brought back together as a convergence rather than sent down a lane in the gutter. Transitions are listed in source order, because a later write to the state variable overrides an earlier one. Click a step or a transition row to highlight it and the step it leads into; right-click either one to read the Structured Text it was drawn from; ctrl+wheel zooms around the pointer and dragging pans.
The window is CPython and pywebview started as a separate process — the
ScriptEngine only launches it, and both desktop UIs now come up through one
shared readiness handshake with a bound on the wait. It needs the optional UI
dependency (pip install 'cds-text-sync[ui]') and python on PATH, or
CDS_PYTHON pointing at one. The IronPython FSM picker and window that
preceded it are removed: they ran inside the single-threaded IDE, where one
large function block froze the window that was analysing it.
Fixed: the Project_fmt.py review lost its place between changes.
Each Apply or Skip closed the review dialog and opened a new one, so the window went back to its default size and position and Review All picked up from somewhere other than where you were. The dialog now stays open across decisions and keeps the geometry you gave it.
Underneath, the wizard was split into a pure session controller, a line-aware diff and a two-phase apply plan, so what the preview shows and what is written back are the same thing — the formatter still refuses to overwrite a section that changed after its preview was made, and it no longer drops the trailing newline under IronPython 2.7. Scanning follows the filter, analysing only the blocks the list actually shows, and stops when the picker closes instead of running on against a window that is gone. Project discovery is explicit and cheap now, which also ends the read-error spam in the log.
Compound IF/ELSIF conditions are laid out across lines.
A condition joined by top-level AND/OR/XOR is put one operand per line
with the operator leading. Only a complete single-line header ending in THEN
is touched, so this stays a whitespace-only change with no attempt to repair ST
syntax: an operator inside parentheses, a string, or a comment is not a
boundary, and an already multi-line or unfinished expression is left alone.
Fixed: export_st exported nothing at all.
The walk started from the root project object, which has no Name, so it
returned immediately and the command exported nothing for anyone. Repairing
that surfaced the reason it had never been noticed: the fallback chain
(.save() / export_native()) is behind the same Owner-group access control
as Project > Export, and calling it on a restricted project raises a modal
login dialog — which freezes the single-threaded daemon with no way to answer
it. export_st now uses only the read-only text-document path that
Project_fmt already relies on, and skips objects that have no text document.
Removed: the GitHub repository furniture.
.github/ is gone: the CI and release workflows, the issue templates, the code
of conduct and the security policy. Releases are tagged by hand, as
docs/releases.md describes.
Fixed: one command with no PLC attached took the whole daemon down with it.
cts plc-crc against a project that has never been connected did not fail —
it hung, and took everything else with it for as long as it lasted (measured
past twenty minutes). Its preflight went looking for an online session, and
looking means create_online_application plus a walk through every login
candidate, each of which has to time out before the next is tried. CODESYS
generates code as part of that attempt, so the IDE visibly builds. The daemon
command loop is single-threaded, so for that entire window every other command
— ping included — came back "Timeout waiting for IDE to connect", which
reads as a daemon that has died rather than one that is busy.
Variable read/write were moved off that path earlier; the remaining five were
not. plc-crc, start, stop, device-status and diagnose-online now ask
for the session that already exists and return "Not connected. Run 'cts
connect' first." when there is none. Opening a session is what cts connect is
for, where the wait is the point. A test now reads the handler sources and
fails if any of them names a session-opening helper again.
Fixed: two different things called themselves compare.
cts plc-crc was wired to a daemon method named compare, so that is what
appeared in the daemon log — indistinguishable from cts compare, which is
sync_compare_text and does something else entirely. The log showed compare
followed by the IDE building, which matches neither command's description. The
method is now plc_crc; compare still resolves to it so an older CLI keeps
working, and is documented as the deprecated alias.
Fixed: a busy daemon was reported as a missing one.
The connect-phase timeout explained itself with "Make sure the reverse-pipe
daemon is running" whenever it had no cached IDE pid — which is every first
command, since each cts run is a fresh process, and the timeout that matters
is usually the first one. The pid is now looked up by image name, so the real
diagnosis (exited / blocked on a modal dialog / running and busy) is available
from the start, and the message says outright that the daemon may simply be
running an earlier command.
Fixed: cts import looked done while being silently discarded.
The daemon import handler created objects, updated POU bodies and applied the
structured view — all against the in-memory project — and then said nothing
about the fact that none of it was on disk. The work looked finished:
read-object returned the new object with its full body, right up until the
project was closed or reloaded and it was gone. The manual Project_import
action appeared to work only because a human was in the IDE to save.
Saving stays the user's call — it also commits whatever else is open in the
IDE — so import still applies in memory by default. What changed is that it
now says so: the response carries saved and, when the project was mutated
without being saved, an unsaved warning naming the consequence. cts import
--save opts into saving, reports save_error if it fails, and takes the same
binary backup into .backup/ that the manual action takes before touching
anything, refusing to apply if that backup cannot be written. Without --save
no backup is needed: the .project file is never written, so it already is the
pre-import state.
Fixed: compare never converged after an import.
manifest.json is the only record of which files on disk are managed, and only
an export writes it. Import left it untouched, so a newly created object stayed
an unmanaged .st — a pending create — and an updated object kept its stale
projection hash. compare reported the same changes after a successful import,
forever, and each following import re-applied them.
import now re-baselines project-view/ and manifest.json from the IDE. The
refresh reads the live in-memory project, so an unsaved import is still a valid
baseline; it is withheld only when an edit did not reach the IDE at all — a
failed create, or a projection that could not be applied — because in those
cases the file on disk holds the only copy. The reason is reported in
manifest_refresh_skipped. --no-refresh opts out.
Import still cannot delete: an object in the IDE with no file in project-view/
is written back to disk by the refresh, and now says so in
manifest_refresh_restored rather than reappearing silently.
Fixed: import reported objects as created that were not.
created_text_objects was built from the list of objects import intended to
create. A create that threw was logged and then reported as a success anyway.
The response now separates created_text_objects, reused_text_objects (the
object already existed and was updated in place) and failed_text_objects with
the error — matching what the native-object branch already did.
cts patch save - hand your changes to a colleague.
Packages the text you changed on disk so someone with the same project can copy it in. It runs a compare against the open IDE, then copies only the hand-authored text into a folder that mirrors the project structure:
.dump/patch/patch_20260809-143512/
├─ project-view/
│ ├─ Application/PLC_PRG.st
│ └─ HMI/Visu/Main.xml
├─ patch.json
└─ README.txt
The receiver copies project-view/ over their own sync folder root, replacing
files, then runs cts compare and cts import. No new command is needed on
that side.
What travels: changed .st and .csv projections, plus the XML of kinds listed
in xml_in_view_kinds (visualizations by default). What deliberately does not:
device descriptions, task configuration, the library manager and the .dump/xml
mirror - they encode the sending machine's state, and copying them across
machines is what makes untouched visu objects look modified.
Flags: --out DIR, --sync-folder DIR, --zip, --dry-run, --bare,
--timeout. Deleted objects cannot be shipped as files; they are listed in
patch.json and README.txt instead.
Fixed: import guidance pointed at a flag that no longer exists.
--force-online was removed when the online preflight became unconditional,
but three places still advertised it. cts --help ended the state-model
section with a dangling "use:" / "or:" and no command after either, and the
cds-text-sync skill told agents to fall back to cts import --force-online,
which argparse rejects with unrecognized arguments. All three now state the
actual rule: import is refused while the IDE is online, there is no override,
and a preflight that still reports online after cts disconnect means the
online session has to be ended in the CODESYS IDE.
Fixed: cts import "hung" on large projects — it was the default timeout.
Import exports a fresh IDE snapshot, runs the engine over it twice (once for
IMPORT.xml, once for the compare report that carries modified POU bodies),
then applies the result. On a ~70 MB snapshot that is 2-3 minutes for a
one-line edit, against a 120 s default: measured end to end at 154 s on a real
project. The CLI gave up mid-flight while the daemon kept working, which looks
exactly like a deadlock and sends you hunting for a modal dialog that isn't
there.
Defaults now match the work: export and compare 300 s, import 600 s. A
timeout is an upper bound, so raising it costs nothing when the project is
small. The timeout error now says what is actually true — the command was not
cancelled, the IDE may still change, retry with a bigger --timeout, and a
real hang also stops cts ping from answering.
Fixed: cts disconnect reported success when nothing was connected.
create_online_application builds a handle without logging in, so disconnect
called logout() on it and returned a bare {"state": "disconnected"} whether
or not a session existed. It now probes the handle first and returns
was_connected, plus a note when there was nothing to disconnect.
Version 3.0.0 (2026-08-01)¶
cts analyze - offline static analysis of the exported project-view.
cts analyze reads only the exported project-view/ tree: no daemon, no
.dump/, no IDE. It was delivered as vertical slices: the command skeleton,
the ST input model, the rule registry, and one rule per data source.
Optional desktop UI: pip install -e ".[ui]" installs pywebview, then
cts ui [--workspace <sync-folder>] opens a local WebView2 window. The first
screen selects a workspace, runs the same offline engine as the CLI, filters
findings by severity/text, shows details, and opens the corresponding source
file. It is an optional dependency: ordinary CLI and CI installations remain
dependency-free.
Project_analyze_ui.py is installed with the other CODESYS menu stubs. It
starts that CPython UI for the active project's configured cds-sync-folder;
the ScriptEngine only launches the external process and does not host the UI.
Commands:
cts analyze --workspace <sync-folder> --format json|text|sarif- run the analysis. Deterministic output sorted by path, position, rule id and fingerprint. The versioned JSON envelope (schema_version,complete,findings,diagnostics,summary) is stable from day one. A plain run writes nothing.cts analyze rules- list registered rules with severity, scope, kinds and required capabilities.cts analyze explain CTS0001 [--format md]- rule documentation with the mandatory "why it is dangerous" section.cts analyze selftest- runs every rule against the good/bad ST examples in its own documentation; a doc that rots fails the run.cts analyze baseline create|update|check- machine-written baseline of current findings (sorted, one entry per line for friendly diffs).checkreports new findings and stale entries; a new finding is never accepted silently.cts analyze triage --apply decisions.json- scriptable triage: suppress (with a mandatory reason), fix-later, or baseline. State is written atomically.
An ST file can suppress a rule for its complete contents with a standalone
line such as // cts:ignore-file CTS0001 -- legacy module (or
the equivalent (* ... *) comment). Multiple rule IDs and * are supported;
the reason is mandatory.
Exit codes: 0 policy passed, 1 unsuppressed findings at or above
--fail-on, 2 configuration error or analysis cannot start, 3 incomplete
analysis with --incomplete=error.
Rule model:
A rule declares its data dependencies (Capability) and its granularity
(Scope.UNIT / PROJECT / HISTORY) instead of a numeric tier. An
unavailable capability becomes a Diagnostic and the run is marked
incomplete - analysis never crashes on one bad object, and a missing model
never looks like "all clean".
The release now ships 95 documented human-facing rules. They cover commented
out code, declarations, data flow, control flow, type and arithmetic hazards,
pointer/reference safety, strings, function blocks, project structure, and
several conservative style checks. The complete registry is listed in
products/cds-static-analyzer/src/cds_static_analyzer/rules/implemented_rules.md
and is available through cts analyze rules.
Findings are identified by fingerprint, not by file:line: schema version + rule id + stable unit id + semantic anchor + normalised context. Reindentation and line insertion do not change a fingerprint; the baseline and suppressions survive reformats.
Configuration (cts-analyze.toml, optional): fail_on / incomplete
policy, git base, per-rule enabled and severity overrides, and
path-glob [[rule_scope]] exclusions. Suppressions live in
.cts-analyze/suppressions.toml next to project-view/, never inside it.
Packaging: rule files use the .py extension and are loaded explicitly by
SourceFileLoader. .py and .md ship in the wheel. CI runs cts analyze
selftest (no rule ships without executable docs), builds the wheel and asserts
the rule assets are in it, and enforces the asset budget: in-repo rule assets
are text/SVG only and stay well under 2 MB.
Separate machine system: the compiled-XML validator is the JSON-only
check for the SVG-to-XML/LLM pipeline. Its VISU001 rule is not
in the human analyzer registry, never appears in cts ui, and has no baseline
or triage semantics.
Version 2.9.0 (2026-07-31)¶
Read this before updating. This release changes where the tool is installed and what the package is called. The installer migrates an existing installation for you, but it moves folders on your disk, so it is worth knowing what it will do.
The CODESYS menu now lists 10 entries instead of 123:
The CODESYS Script Engine scans its ScriptDir fully recursively and filters only by file extension — proven by probe: dot and underscore folders, nesting depth and the Windows hidden attribute hide nothing from it. The installer used to unpack the whole repository into ScriptDir, so every tracked .py in the project landed in Tools > Scripting, on every user's machine and not only on a dev box. The old backup path doubled it: setup.ps1 wrote a second full tree to ScriptDir\cds-text-sync.backup, so anyone who had ever updated saw the list twice.
The install is now split in two. The tool itself — "the body" — lives outside ScriptDir, at %LOCALAPPDATA%\cds-text-sync by default or in any Git clone you point at. ScriptDir receives nothing but the generated Project_*.py stubs, each pinned to the body through a CDS_HOME literal.
- The stub folder and the file names are unchanged, so toolbar buttons you set up earlier keep working.
- Both layouts remain supported. An entry point declares its own body, so an existing in-ScriptDir install is untouched until you re-run the installer.
cts install-menuregenerates the stubs;cts wherereports which layout you are on.
What the installer does to an existing install:
- Finds your current installation in ScriptDir and moves it out to the new body location — never copy-then-delete, so a Git clone keeps its remote and its branch, and untracked files come along.
- If ScriptDir holds a developer symlink, only the link is removed; the target is left alone. (
Remove-Item -Recursefollows a junction in Windows PowerShell 5.1 and would have deleted the target's contents.) - Deletes the legacy
ScriptDir\cds-text-sync.backupleft by earlier versions, and writes future backups next to the body instead. - Fixes a long-standing data loss: every previous update silently deleted untracked files under
profiles/— a hand-writtenprofiles/astra.jsondid not survive an upgrade. Those files are now stashed and restored.
The package is renamed cli → cds_text_sync:
The distribution was called cds-text-sync but installed a top-level package named cli. Any other project with a cli package — or a user with a cli/ directory of their own — shadowed it silently, surfacing as an ImportError somewhere unrelated. For a tool people pip install -e next to their own code, that was a matter of time. external_engine becomes engine in the same pass.
Upgrading needs pip install -e . once. An editable install made before the rename still resolves the old name, and cts then dies with No module named cli — an error naming something you have never heard of. The installer detects exactly this condition and clears the stale install; if you upgrade by hand, run the reinstall yourself. The pipe name (cds-cli-*) is protocol, not package, and is deliberately unchanged.
Reading a PLC variable no longer takes the daemon down:
cts read built an online session when none was cached: create_online_application, then a walk over a list of login candidates calling login() on each. Every candidate has to fail before the call returns, and the single-threaded daemon loop serves nothing meanwhile — measured at ~145 s against a project with compile errors, after which the read failed anyway. Any command issued inside that window timed out, so the daemon looked dead rather than busy, and in practice needed a restart from the CODESYS menu.
Reads and writes now use the session they are given and refuse in about a second when there is none, which is what their docstrings already claimed. Opening a session stays with cts connect and cts download, where you asked for it and the wait is the point.
.dump/ no longer grows without bound:
Every export wrote a timestamped snapshot and nothing ever removed one. compare and import --dry-run write one too, despite neither sounding like it touches the disk, so the folder grew by well over a megabyte per invocation — an afternoon of testing left 23 MB behind. Only the newest is ever read back, so ten are kept as a short manual undo trail, matching the existing backup_retention_count default.
--pretty output is readable again:
The text renderer collapsed a list to [N items] above five entries and printed every entry in full at five or fewer, which broke in both directions at once. cts --pretty compare returned five added objects carrying a ~300 KB XML blob each and printed all five whole: 1.5 MB across 16 lines. One more object in the project and the same payload would have rendered as added: [6 items]. Meanwhile project-tree showed nothing but children: [8 items], and read-log nothing but messages: [15 items].
Output is now rendered recursively with indentation, bounded by four limits on size — per scalar, per list, by depth, and by total lines. Whenever anything is left out the output says so and points at --output json, which stays complete and exact. The same compare payload renders in 9 KB. Flat output — where, ping, permissions — is byte-identical to before.
Smaller fixes:
- A bridge module that is present but fails to import is now reported as the defect it is, instead of being conflated with an absent optional module and degrading silently.
Project_compareleaves the menu: it ran the same comparison asProject_compare_ui, which adds the object list and the import/export follow-up on top.cts comparestill covers the no-dialog case, andinstall-menuremoves the stale stub on its next run.- Regression scripts and fixtures move under
tests/.offline_regression.pywas the largest file in the engine package and pip shipped all 1550 lines of it to users. - Packaging metadata is consolidated into
pyproject.toml, field for field.
Version 2.8.3 (2026-07-27)¶
Generated HMI screens now look designed (cts visu from-svg):
A screen compiled from an SVG sketch was structurally correct and visually dated — cream field, cyan vessels, acid-green status bars, no type hierarchy, no grid. None of it was a bug: it was exactly what the defaults specified. Three causes, fixed together.
- The extracted styles were overwriting a palette they had no opinion about.
cli/visu/style_roles.pycurates a fallback per role and documents thatwater/metal/status roles have no CanonicalName anchor — they are ours, not CODESYS's. Butcli/visu/themes/*.json, snapshotted from installed styles, redefined them from arbitrary entries and won the merge, so--theme flat-style(the default) yieldedwater #00F3FFandsuccess #ADE204.load_themenow strips the roles we curate and re-fills them fromstyle_roles; everything CODESYS genuinely defines — fonts, native control colours, frame, border, accent — still resolves through the project's style. Dropping--themeused to give a better screen than keeping it; that inversion is gone. var(--water-dim)never resolved.resolve_colormaps it to the dotted keywater.dim, the JSON themes storewater-dim, so a.pipe-watershape took its fill from the theme and its stroke from the built-in fallback — a mismatched pair by construction. Hyphenated roles are now aliased to dotted at load time.--text-brightwas missing from all twelve presets..inverseworked anyway becausesvg_import.parse_svglayers the curated palette underneath the theme — butbuilderresolves straight out of a theme dict forcts visu add, with nothing underneath, where the same hole is aThemeError._finalizenow makes every theme self-sufficient for every rolestyle_rolesknows, and a new test walks everyvar(--role)instylesheet.cssagainst every theme so the next hole fails in CI rather than at a user's prompt.
cts visu preview — see the screen before it reaches the IDE (new):
- A sketch is deliberately colourless (
class="pipe-water", never a hex), so opening it in a viewer showed black shapes on white. Nobody — human reviewer or authoring model — could see the result before importing it.cts visu preview --svg FILErenders<file>.preview.svg+.preview.pngwith the colours the compiler will actually emit. - The preview renders from the parsed element list and asks
builder._resolve_golden_colorsfor the colours, so preview and compile cannot disagree about geometry, colour, or the baseline→top-left conversion: one resolution path, used twice. Native controls are drawn the way the style paints them rather than left invisible. - Rasterisation uses a headless Chrome/Edge when one is installed (
$CHROME_PATHoverrides the search); without a browser the SVG is still written.from-svgwrites a preview on the way through unless--no-preview.
cts visu lint — a design contract a weak model can follow (new):
cts visu checkvalidates a compiled screen — bounds, member consistency, Text-ID invariants, the things CODESYS would reject. That is correctness, and it runs too late to help someone still drawing.cts visu lint --svg FILEchecks the sketch for what makes a valid screen look unfinished: off-grid coordinates, text wider than its box, a font size outside the scale, a button too small to press, a field with nothing bound to it, a captionless button, overlap, near-miss gaps.--fixsnaps the mechanical findings by splicing the affected start-tags back at their own character offsets, so comments and formatting survive byte-for-byte.from-svgruns lint and prints findings; only--strictmakes them fatal.- Two rules had to know what they were grading. A
<text>yis a baseline — including a textfield's — so the rule grades the compiled box top and shifts the baseline by the same delta; snapping the baseline itself would push the box off-grid, the opposite of the intent. Andrxon a<rect>is a corner radius, not a position, so it is exempt:--fixwould otherwise have rounded a deliberate 2px radius down to a square corner. On an<ellipse>the same attribute is real geometry and stays graded.
What ten authoring runs found (cts visu lint, --fix):
Ten weak-model agents were given only skills/cds-visu-svg/SKILL.md and asked to draw a screen each. Most of what came back was not a modelling failure — it was the tooling teaching the wrong lesson.
--fixcould rewrite the wrong element.index_source_tagsmasked<defs>but not comments, so a comment that merely named a tag added a phantom entry to the source index — and from there every finding was attributed to its neighbour. The shippedpid-schematic.svgsays "a pipe is a thin RECT, not a<line>" and silently mis-indexed the twelve elements after it. Depending on where the drift landed, a fix was either applied to an innocent element or dropped on the floor. Comments are now masked the same way<defs>is, and two tests pin it.- The text-width estimate was one constant doing four jobs. 0.65 em/char is about right for uppercase and overstates mixed-case prose by a quarter, and that estimate is load-bearing in the default box width of an unsized
<text>plus the overflow, overlap and crowding rules. Inflated boxes reported collisions that were not there, and three of the ten authors dismantled a working layout to silence them — one deleted every annotation on a P&ID. Widths now sum per-character advances. - The linter's gradient pointed at an empty screen. Every rule rewarded removing content and none noticed a blank one, so an untouched starter screen scored a clean
[OK]— and one author shipped exactly that. Two rules push back:empty-panel(a panel with nothing in it) andscaffold(the starter placeholders are still here).scaffoldis info, not a warning: the first thing an author sees should not be a complaint about code they did not write. - Crowding fired on P&ID drawings. A 4px gap is a slip between two cards and correct between a pipe and the vessel it enters, so pairs of
.pipe-water/.metalshapes are exempt. - Findings could not be located. "label #16 overlaps rectangle #27" is only resolvable by counting tags by hand; findings now quote the element's text and the coordinates as written, so a
<text>reports its baseline rather than a derived box top. They also name the shape that gets drawn rather than the parser's category for it —parse_svgfiles every plain shape underrectangleand keeps the real one inparams["shape"], so a circle used to be reported as a rectangle the author never wrote. --fixneeded more than one pass. The font-scale rule rewritesfont-size, and a baseline is graded through the box top — which is the baseline minus that font size. It now iterates to a fixed point (bounded at five) and says so if two rules ever disagree.- New rules for the traps that were silent:
control-tag(a<circle data-cds-type="lamp">is the obvious shape for an indicator light and is just an ellipse —data-varis dropped and it sits at one colour forever),inert-class(classon a button, textfield or lamp changes nothing at all — not size, not colour),text-anchor(an anchor with nodata-widthaligns against a guessed box). --fixno longer resizes a<circle>:ris graded on its diameter, sor="6"— a 12px dot already on the grid — is left alone instead of being inflated to 16px.- Malformed XML now surfaces as advice rather than an
ElementTreetraceback, quoting the offending line and, for the commonest cause, explaining that--cannot appear inside an XML comment. - A text box hangs below its baseline, and nothing said so. SKILL.md documented the box top (
y − font-size) and never the height, which ismax(16, font-size × 1.4)— so the bottom edge sits0.4 × font-sizeunder the baseline, 11px for a.value. An author sizing a KPI card to the baseline of the number inside it overflows it by three pixels and cannot see why; one screen did this three times over. The five per-class numbers are now in SKILL.md and pinned by a test, so the doc cannot drift from_estimate_text_height. cts visu newwarns when the canvas is below 480×320 (the size the layout blocks are dimensioned against — under it they collide) or has a side that is not a multiple of 4 (the halves it computes then land off the grid the linter enforces).- One limitation the runs made obvious enough to write down: a screen authored here is read-and-press. Only buttons and plain shapes carry input actions —
element_textfield.xml.tmplemits an emptyVisualElementInputActions— so a textfield displaysdata-text-varand cannot be typed into. An operator can start, stop, toggle and navigate, but cannot key in a setpoint except through a button that writes one. SKILL.md now says so under "Unsupported", including that a writable field added in the CODESYS editor does not survive a round trip:to-svgreads a textfield's geometry, variable and font, not its input action.
Type scale, layout skeleton, and the authoring skill:
stylesheet.cssgains a five-step type scale as classes —.h122,.h216,.value28,.label12,.caption11 (.titlestays as an alias of.h1) — plus.card,.mutedand.inverse. An author picks a class; a barefont-sizeis now a lint finding.cts visu newno longer copies a fixed seed file with four floating examples whose coordinates ignored--w/--h. The skeleton is composed from layout tokens, so the canvas size drives the layout, and blocks that will not fit are dropped rather than squeezed — narrow canvases fall back to one KPI card, short ones drop the lamp rows. Verified lint-clean at 800×480, 1024×600, 1280×800, 640×400 and 480×320. That is the tested envelope, not a guarantee for every number: 480×320 is the floor the blocks are sized against and below it they collide, and a canvas whose sides are not multiples of 4 puts the halves it computes off the grid the linter enforces.visu newnow warns in both cases rather than handing back a skeleton that fails its own lint.skills/cds-visu-svg/SKILL.mdgains a "Layout rules" section (4px grid, 24px page margin, bands, type scale, touch targets, the baseline rule) and a "look at what you made" step — render the preview and read the PNG before asking for approval. Both shipped examples were rewritten and now lint clean; a test asserts they stay that way, because SKILL.md tells an authoring model to copy from them. The stale claim that inline<style>/:rootis unsupported is gone —_parse_inline_themehas supported it all along.- A pipe is now drawn as a thin
<rect>, not a<line>: a CODESYS line has no width member, so<line>always draws one pixel wide. Fine for a.divider, invisible for a process line.
-- is illegal inside an XML comment, which made naming a flag in the skeleton's own guidance block (cts visu lint --fix) render the whole file unparseable — as a ParseError on line 5, saying nothing about comments. Guarded by a test.
What five end-to-end runs against a live project found (--gvl binding):
The ten earlier runs stopped at lint; these five compiled into a real FACTORY I/O project and bound to its actual FIO_SIGNALS / GVL_Sensors / GVL_Routing globals. Every one of them produced a broken GVL, in two different ways that turned out to share a cause.
- A variable path names its owner in the first segment, and the lookup read the last.
ensure_gvlmatchedrpartition("."), soGVL_Sensors.Scale.Qwas searched for under a GVL calledGVL_Sensors.Scale, found nothing, and fell through to declaring the leaf. Nine sensor lamps therefore emitted nineQ : BOOL;lines into oneVAR_GLOBAL— ST that CODESYS refuses to compile. The owner is the first segment; whatGVL_Sensorsdeclares is the instanceScale. - Every hardware-mapped global was invisible to the dedup. Fixing the lookup exposed the larger defect underneath: the declaration regex
^\s+(\w+)\s*:requires the colon to follow the name, but real PLC globals are writtenEmitter AT %QX0.3 : BOOL;. Nothing located at an%I/%Qaddress had ever been seen byscan_project_gvls, so a screen bound toFIO_SIGNALS.Emitterre-declaredEmitterin its own GVL — a silent shadow of an I/O point, which is worse than the crash because it imports cleanly. Both scan sites now share one_DECL_REthat accepts the location. - A declaration only counts where the reference looks for it. The screen binds the full path it was written with, so
Q : BOOL;inVisuVarsproducesVisuVars.QandGVL_Sensors.Scale.Qstill resolves nowhere.--gvlnow declines instead of inventing: unresolvable references are reported as[WARN] Not declared: <path> — <why>, and two paths can no longer collapse onto one identifier. Against the live project all five screens now compile toAll runtime variables already declared; GVL not modified, which is the correct answer when an author binds to globals the PLC program already owns.
What those runs found in the skill document:
--sync-folderwas undocumented and all five authors needed it. Each guessed it from context; a reader following the document literally would have pointed every command at the wrong directory. It is now stated once, up front, as applying tolint,previewandfrom-svgalike.- The baseline rule was being applied to anchored text, where it does not hold. Two authors independently computed the scaffold's own caption as off-grid (
40 − 11 = 29) and concluded the shipped starter was broken. It is not: anchored text centres onyusing the box height, so the top is40 − 16/2 = 32. The document had said only "centred ony" and never gave the formula.text-anchor="end"gained a worked example too, after an author wrote the box's right edge asxand pushed it off-canvas. - "You never write a colour" contradicted the lamp table three sections later.
data-colornames a role, not a hex, but the absolute phrasing made it read as a rule being broken. Softened to what was always meant. - The preview draws a lamp as a circle after telling you to draw a square. That is the preview being honest about the native round bitmap, but two authors read it as their sketch having been rewritten. Stated, along with why textfields show
%3.1frather than a value. - A normally-closed E-stop has no representation. TRUE means healthy, and a green lamp bound straight to it is correct while reading backwards to an operator. There is no sketch-side inversion; the guidance is now to carry the polarity in the label or bind an inverted flag, and never to let colour alone imply it.
What those runs found in the linter and the preview:
The same five screens were the first drawn end-to-end by someone other than their author. Most of what the linter said about them was wrong — not wrong about geometry, wrong about whose geometry it was reading.
- Crowding was measuring its own estimate. A
<rect>carries its width in the file, so the gap beside it is a number somebody chose; a<text>carries a baseline and a font, and its box comes from_estimate_text_heightplus a table of glyph advances. SKILL.md sets a label's baseline 24px above its field's, which for a 12px label over a 16px value leaves exactly 4px between the two derived boxes — so following the documented rhythm produced the finding, and the only way to clear it was to stop following it. Text-against-text is now exempt the way P&ID pairs are. Type that really collides is still anoverlap, type off the rhythm is still agridfinding, and page furniture — two cards, a card and its caption — keeps the rule, because there the gap really is a slip. - An unsized textfield defaulted to 100×100. A box nobody asked for: it overflowed whatever card held the field, which the linter then reported as an overlap the author had not drawn — and the compiled control really was 100px tall in the IDE. A field with no
data-width/data-heightnow falls back to the same estimate a plain<text>gets, which is the one the document promises. Falling back at all is still worth a warning, andunsized-fieldis that warning: a label's width can be estimated from its own text because that text is what appears, but a field's content is%din the file and1247on the screen, so the estimate sizes the box to the format string and the value runs out of it. It is the one geometry a field cannot infer. - "The card overlaps the label" sent authors looking for the wrong mistake. When a child's box starts inside a container and ends past its edge, the author sized the parent and let the child grow — a different defect from two labels that drifted into each other, and it is the child that is wrong. Split into
overflow(filed against the child) andoverlap. The distinction is drawn on type rather than on which box is bigger, because a 100px field inside a 72px card has the larger area and is still the thing that overflowed. - A captioned button that does nothing is worse than a blank one, because the operator has no way to tell —
inert-buttonis a warning now, not info. Both button findings also quote the caption and the coordinates: five buttons on one screen madebutton #32a tag-counting exercise. The index is no longer duplicated into the message, which the reporter already prints. - The preview drew every label 2px right of where it was written. The 2px inset is real — it is the gap a native control keeps between its frame and its value — but it was charged to plain labels too, which CODESYS draws flush. So a label written
x="48"came back at 50, and lining it up against a card edge always looked 2px wrong in the one view an author reads to check alignment. The inset now applies to textfields only. - A
cardon the screen background is invisible, and the report for it was "panel and card are the same colour". They are not —#FFFFFFagainst#EDEFF2, verified through the theme palette, the compiledparams["fill"], and the rendered preview — but a card is one step in from the panel that holds it, which in the light palette lands it within 3% of the screen background. Drawn straight onto the background it renders as nothing, and two of the five screens did that eight times over. Newlonely-cardrule, gated on the resolved palette rather than on an assumption, so the dark scheme — where the two are visibly apart — never sees it. SKILL.md now says whatcardis for. - The
visuflag help described a scope the handlers do not have.--outwas documented "(for from-svg, to-svg)" and is read bynewandpreviewas well;--width/--heightsaid "(for create-screen)" and drivevisu new;--svgsaid "(for from-svg)" and is required bypreviewandlint;--themenamed onlyfrom-svg;--w/--hhad no scope at all while serving two subcommands with different meanings. Corrected against what each handler actually reads. - Two screens compiling at once could share one Text ID. Every caption in a project draws from one
GlobalTextList.xml, and an id was handed out by reading the current maximum and appending — with nothing between the read and the write. Twoctsruns against one project (two authors, or one author with two screens in flight) both read the same maximum and both append it, and the result is two screens whose labels point at one entry: in the IDE, a caption from the wrong screen, and no check we have can see it, because the XML is well formed and every reference resolves. Allocation now takes a lock file beside the list. A lock older than 30s is treated as a process that died mid-write and broken rather than waited out — one crashed run must not wedge every later compile behind a file nobody has heard of. - Every generated screen claimed the same object identity, and importing the second one froze the daemon.
builder.build_screenfell back to a shared constant —11111111-1111-1111-1111-111111111111— for the visualization's own guid whenever the caller did not supply one, and neither call site ever did. That guid is not a local placeholder: it is written into the project verbatim, and a snapshot exported by CODESYS itself after the first import shows the screen sitting in the project under it. So the first screen imports fine and every screen after it arrives claiming to be that screen. CODESYS raises an overwrite prompt, and because the script engine is single-threaded a modal dialog stops the whole daemon rather than the one call that opened it —importhung past its 180s timeout, and thepingandread-logsent to diagnose it hung too, which reads as a dead process rather than a question waiting for an answer. Screens now get a fresh guid each; an explicitvisu_guidis still honoured. --gvlsaid "Updated GVL" when it had updated nothing.ensure_gvlreturned the same path whether it appended declarations or found every reference already declared, so the caller could not tell the two apart and always reported the write. On a project with real signals that is the normal case — a screen bound entirely to existing GVLs — and the author is told their GVL just changed, goes to look, and finds either nothing or leftovers from an earlier run they now have to explain.ensure_gvl_resultreports(path, written);ensure_gvlkeeps its old contract for existing callers.from-svgwith no--screenreported a missing file instead of a missing argument. An empty name resolved to<project-view>/.xmland came back asScreen file not found: ...\.xml— which reads as damage to the project, and names a file the author never asked for. It now says which flag to pass.- Nothing measured the padding the skill grades screens on (
padding, new rule). "24px page margin, 16px panel padding" is written into the layout rules and followed by the claim that lint grades against them — and only the page margin was ever checked. Four of five screens drawn against the skill came back "Sketch OK: no design problems found" with content sitting exactly on the edge of the box holding it: a row of lamps on the panel border, a value field ending on its card's bottom edge. It is not an overlap — the child is properly inside its parent, which is why every other rule waves it through — and it renders as content clipped by its own container. The rule measures content only: a rectangle flush inside a card is the accent barcts visu newdraws itself, and a line across a panel is a divider. The bar is 8px, half the documented padding, so a deliberately tight layout is left alone and only flush-or-nearly is reported, and it names the innermost container — a card sharing its panel's bottom edge is one problem, not two. - Recompiling a screen added a second copy of it instead of replacing it.
--screen <Name>is the documented way to compile a sketch into a screen that already exists, andfrom-svgappended to whatever was there: the second run turned 31 elements into 62, each new one landing exactly on top of its twin. Nothing in the tool showed it — the sketch was still 31 elements, the preview rendered from the sketch, and[OK] Compiled 31 element(s)is what a correct run says too — so it surfaced only ascheckreporting overlaps for elements the author had never drawn twice, and in CODESYS as a screen with every label stacked. It also meant an element deleted from the sketch could not be deleted from the screen, since only additions ever reached it.from-svgnow clears the element list first and says how many it replaced; the identifier counters keep running forward so no fresh element reuses a name CODESYS has already seen. Creating a screen (--create-screen) is unaffected. visu check --name Xanswered "--screen is required".--namebelongs to screen creation (new,create-screen); the subcommands that work on an existing screen —add,list,check,to-svg— take--screenand ignore--nameentirely. Three of five authors reached for--namefirst, and the reply left them looking at a screen name they had just typed beside a message saying they had given none. When--nameis present the error now names the flag that carries it:pass --screen T_Sensors (--name is for creating one).checkwas also missing from the skill's workflow, which is why it was being guessed at from--help.- Only five named entities are legal in a sketch, and the skill never said so.
→and≤are the obvious way to write an arrow or a threshold in a caption, they are valid HTML, and they fail the parse with undefined entity — taking the whole file with them, so the error arrives as "your sketch is malformed" rather than "this one character is not XML". Documented, with the three ways to write it that do work. - A caption can state a live value it is not bound to, and nothing said so (
static-state, new rule). Text withoutdata-text-varcompiles to a Text ID: fixed at import, identical for the life of the screen. Three screens in a row shipped one anyway — a redE-Stop = Stoppedbutton, a header reading9 sensors — diagnostics active— because the phrasing is natural and every geometric rule passes it. The result is the most confident-looking thing on the screen reporting a state it has no connection to, which on an HMI is worse than an ugly layout: it says Stopped while the machine runs. The rule reads what is to the right of=,:or a spaced dash and reports it when it names a state. It deliberately does not match a state word on its own —Runningbeside a lamp is the legend that screen needs, andMotor Runningis that legend with the machine named; firing on those would penalise every screen labelled correctly. Bound fields are exempt, since there the text is a format string. Documented in the skill with the fix.
Dark screens (--scheme dark, new):
A night shift reads the same HMI in a dark control room; for an industrial screen that is not cosmetic. The sketch format already made most of it free — an author writes class="panel", never a hex — so no example sketch, no stylesheet.css rule and no themes/*.json needed to change. What had to move is the ownership boundary.
- In dark the curated palette is authoritative, not a fallback. Every CODESYS visual style that ships is a light style, so sampling one for
textwhile painting the panel behind it dark is precisely how white-on-white happens.style_roles.curated_roles(scheme)widens from 15 roles in light to all 41 the dark palette names — surfaces, the text on them, frames, native control colours — andthemes.load_theme(name, scheme)strips and re-fills those, so two different--themevalues compile the same dark screen. Light is untouched: it still defers to the project style for everything CODESYS genuinely owns. - Buttons and textfields were unreachable by any palette.
builder._resolve_golden_colorsforces their fill and frame toNoneso the style owns them at runtime — correct for light, and a light island on a dark screen. The gate now applies only where the role is not curated in the active scheme.textfield.jsonhad nothemeable_colorsblock at all and gained one for the two members its template already carried;button.json's fill role moved fromprimaryto a newbutton.fill, so a dark button is a dark surface instead of a shouting accent blue. - A style-linked colour cannot be overridden in place. A colour member written as a struct with a
CanonicalNameresolves from the project style and the literal beside it is ignored — and blanking the name crashes the CODESYS build. The short-form<Single Name="Value" Type="uint">is the only encoding that wins, and it replaces the struct rather than sitting beside it, so a member id still appears exactly once. Light continues to emit the struct, byte-for-byte what it emitted before. - Font colour is written in three places and drawn from one. A label carries it as a plain member, as a style-linked struct beside it, and inside the font descriptor — which holds it twice more, as an
ExplicitColorliteral and aNamedColorlink to the style'sFont-Default-Color. All three already carried the right value, and the first dark screen still came back with every label in the style's black, unreadable on its own panel: while that link exists the literal is ignored. Which of the three mattered was settled by importing a probe screen carrying four encodings of the same label into a live IDE — only nulling the link changed what was drawn, and switching the colour member to the short form changed nothing at all. Dark now emits<Null Name="NamedColor" />, the encoding CODESYS itself uses for a screen background and for the stock combobox font, and rewritesExplicitColoralongside it because the button template hard-codes a black one. Light keeps the link and keeps following the project's style. The comment inbuilder._render_font_color_structasserted the opposite and had been wrong from the start — light could not have exposed it, since a curated lighttextand the style's black look alike. - Two new roles, so a pairing is named rather than implied.
field.text(a textfield's value colour) andbutton.fillhave to be chosen together with the box behind them. In light both derive from the role they refine (text,primary) — a style snapshot predates them and never names them, so left to a literal fallback they would have silently ignored the project style. - The sketch carries its own scheme.
cts visu new --scheme darkrecordsdata-cds-scheme="dark"on the root<svg>; lint, preview andfrom-svgread it, so the rest of the workflow needs no flag and preview cannot drift from compile.--scheme light|darkoverrides it for a single run. The scheme is resolved by a pre-parse (svg_import.read_scheme) before the theme is loaded — loading a light theme first would layer it over the dark base palette and repaint the screen light again, but only when--themewas supplied. - The scheme survives a round trip. A compiled screen does not record which scheme produced it — the two differ only in the colours they resolved to — so
cts visu to-svghanded back a sketch with no attribute, and recompiling it repainted a dark screen light. Decompiling now infers the scheme from the screen background, whose two curated values (#F4F5F7and#10141A) are separated by most of the luminance range, and stampsdata-cds-scheme="dark"when it is dark. Light stays unstamped, matching whatcts visu newwrites. The inline:rootblock follows the same answer, so a dark sketch no longer carries light variables under a dark stamp.--schemenow also reachesto-svgfor the case the guess is wrong — a hand-set dark background on an otherwise light screen. - Preview follows.
_resolved_colorshands the scheme to the compiler instead of arriving at the same colour by a different route, its light-biased fallbacks (#DDDDDD,#808080,#000000) now come from the palette, and the--gridhairline flips to white on dark — a black one at 6% opacity reads as "the flag is broken". - Lamps are deliberately excluded. Their colour comes from
Element-Lamp-*and stays identical in both schemes: an indicator that changed meaning with the colour scheme would be a safety problem.
Light output was verified byte-identical, not merely tested: the same fixture screen compiled against flat-style, basic-style and white-style produces the same 508630 characters before and after. New tests pin the contrast of every text role against the surface it actually sits on in both schemes — the rule that would have caught white-on-white before it reached the IDE — plus scheme precedence, the struct/short-form switch, and that no preset can drag a light text into a dark screen.
The sketch commands no longer pretend to need an IDE:
cts visu lint and cts visu preview grade a file. They consult the daemon only to pick up an optional project-level visu.css — and _optional_project_view was written to shrug that off — but the failure still cost the full 10s project-command timeout and printed [ERROR] Could not get sync folder from daemon … Make sure the reverse-pipe daemon is running inside CODESYS before succeeding. These are the first two commands a new user runs, usually before CODESYS is open, so the one flow that is meant to work offline was the one that looked broken. _resolve_project_view gained timeout and quiet; the optional path waits 2s — ten attempts at the default 200ms daemon poll — and says nothing. The project commands are unchanged and still explain themselves.
cts visu to-svg gave back a sketch that was not the screen:
Decompiling is how an existing screen enters the SVG workflow, and how a compiled one is reviewed. Three things it dropped only became visible by linting the round trip — the original sketch lints clean, so nothing upstream could have caught them, and each of the three does damage twice: the sketch is wrong, and recompiling that sketch writes the loss back into the screen.
- Every caption came back moved. A CODESYS text element is a box plus an alignment; an SVG
<text>is a baseline plus an anchor, and_parse_textconverts the one into the other on import. The export emitted the raw box top asyand never read the alignment members at all, so a centred caption came out flush left and every line of text rose by one font size. It now inverts the import:HCENTERrestorestext-anchor="middle"andy + height/2,RIGHTrestorestext-anchor="end", and left-aligned text getsy + font-size— the same fallback size the builder uses when a label carries none. - Buttons came back inert. A button's behaviour is split across two places in the compiled XML — tap and toggle in
ConfiguredComplexInputs, per-event actions inVisualElementInputActions— and the export read neither, so a decompiled screen had six buttons that looked right and did nothing._read_input_actionsrecovers all of it asdata-cds-actionclauses (TAP,TOGGLE,OnMouseClick: ST|toggle|screen), suppressing the event half when the dialog reader has already claimed it so a dialog button does not emit its snippet twice. A fixed-point test now compiles, decompiles and recompiles, and asserts the wiring is identical. - Estimated text boxes failed the project's own grid rule.
_estimate_text_width/heightare what a<text>gets when the author wrote nodata-width/data-height, and they landed on arbitrary pixels — solintreported sixteen grid findings against numbers no one had written. Both now round up to the 4px grid, which keeps the box at least as wide as the glyphs need. The alternative — omitting the estimates on export — was rejected: it would silently move genuinely off-grid boxes in hand-authored CODESYS screens. The documented below-baseline overhang shifts with them (4/5/8/10/12 px for.label/.caption/.h2/.h1/.value); SKILL.md and its test move together.
A compile → decompile → lint cycle on the reference project's 95-element screen is now clean in both schemes.
from-svg --create-screen --replace (new):
Recompiling a generated screen meant deleting its .xml first, and a fresh compile mints a fresh object Guid. That Guid is the screen's identity on cts import, so the delete-and-rebuild loop adds a second screen in CODESYS beside the first rather than updating it. --replace rebuilds in place and keeps the Guid. Without it an existing screen is still never overwritten, and the refusal now names the flag instead of leaving rm as the only way forward.
Unit suite: 769 passed, 3 skipped (was 521/3).
ACTION round-trip (cts export → edit → cts import):
- ACTIONs (and other declaration-less POU children) expose an
Implementationsection only, sost_projection_contentnever matched its two-section branch and exported them as a bare body — noACTION <name>header, no// --- implementation ---marker. The file was not self-describing:_detect_st_kindcould not classify it, and the import path could not tell the body apart from a GVL/DUT declaration, so it routed the text intotextual_declaration(absent on an ACTION) and the edit never reached the IDE. The object landed inskipped_projection_objectswhilects comparekept showing the diff. - The header is now synthesised on export from the entry's own
MetaObject/Name, and stripped again bysplit_st_projection_valueson the way back, so the text-first path (folder_reader) and the patch builder store the bare body in theImplementationblob exactly as before. Both directions are covered bytests/unit/test_action_round_trip.py— a forward-only fix would write the header and the marker straight into the stored ST code. - Splitting of projected
.sttext now lives in one place,src/ide_bridge/ide_st_text.py, shared by the update path, the creation path andupdate-pou; the three used to disagree about what a marker-less file means. A marker-less file is still a declaration — that is what GVLs, DUTs and empty-bodied POUs project to, and treating it as an implementation would silently write their declaration nowhere. .stfiles exported before this change (bare body, no header) keep working without a re-export: when the target object exposes no declaration but does expose an implementation, the body is routed to the implementation. Objects that do have a declaration are never re-routed.- An empty or whitespace-only
.stis no longer reported inupdated_text_objects: nothing is written, so claiming an IDE update was misleading. - The skip reason for non-ST projections (
.csvtextlists, alarm tables) no longer advisesupdate-pou, which only understands.st.
Problem surface identified from eddiedon's report in #63; implementation and reverse-direction fix are independent. Verified live against a 894-object project in both text_first and xml_first sync modes: declaration-only objects (GVL/DUT/interfaces — 303 of them) still route to Interface, POUs and methods to both sections, and cts compare stays clean across an edit → import → revert cycle.
Version 2.8.1 (2026-07-25)¶
Non-English UI locale fix (import resolution + on-disk tree):
- On a non-English CODESYS UI locale (e.g. Chinese zh-CN) the native XML snapshot encodes the
Patharray of standard objects using localized labels (PLC逻辑,任务配置) while each object's ownNamefield stays English (Plc Logic,Task Configuration). The tool trusted thePathverbatim, soproject-view/folders became localized (Device/PLC逻辑/Application/...) and, on import,_find_child_transparent— which matches against the English IDE object name — never resolved the localized segment, creating the object under a fallback root-level folder instead of its intended parent. - Standard-container labels are now folded back to their canonical English form via a bounded locale-alias table (
cli/external_engine/_locale_aliases.py). The snapshot reader normalizesPathsegments so the on-disk tree is locale-independent, and container resolution compares names through a locale-independent key, so a localized path segment matches an English (or localized) live IDE object. The transparent "Plc Logic" hop is likewise no longer hardcoded to English. - English projects are unaffected: English names are never in the alias table, so canonicalization is a strict no-op (same folders, same matches) — verified by the offline regression harness and the full unit suite.
- Known localized labels are currently seeded for zh-CN from the reported issue; additional locales and segments extend the table one entry at a time.
Relative cds-sync-folder fix (GH #61):
- A relative sync folder (
.\CDS\,./CDS,.) failed with[Errno 13] Access deniedon CODESYS SP18. The path is anchored against the project file's directory, but every call site looked the project path up as["filename", "FileName", "FullName", "Path"]— and IronPython attribute access is case-sensitive, so the canonical lowercasepathattribute exposed by ScriptEngine was never tried. The relative folder stayed unanchored and was resolved against the CODESYS working directory, which is not writable. - Project-path lookup is now centralized in
_project_file_path()(ide_daemon_state.py) with lowercasepathtried first, and all four duplicated call sites (_get_sync_folder,project_info,_project_path, CRC file discovery) delegate to it. - When a relative folder genuinely cannot be anchored (project never saved / no path exposed), the daemon now fails loudly with an actionable message instead of falling through to a misleading "Access denied".
- Regression guard:
tests/unit/test_project_file_path.pypins the lookup order, including the exact SP18 shape (only lowercasepathpresent).
Reported and diagnosed by reibax-marcus in #61 / #62.
Version 2.8.0 (2026-07-20)¶
Text-first sync mode (opt-in, chosen at initialization):
- New
sync_modeproject setting (Project_options.pycheckbox "Text-first mode"). Selected on an empty sync folder and locked once.dump/manifest.jsonexists — recorded in the manifest, enforced by the options dialog, the options params API, and the engine itself (export/compare/import refuse a mismatched settings file). To switch modes, initialize a new empty sync folder. - In text-first mode every ST projection is force-enabled, and the structural XML sidecars move out of the view into the tool-owned, git-ignored
.dump/xml/mirror (manifest entries recordxml_root: "dump"). A per-kind "Keep XML in view for:" list (xml_in_view_kinds, default["visu"]) keeps selected kinds' XML in the view and in Git; it can be changed at any time. .stfiles are first-class import input in text-first mode: edits are applied even when the manifest never registered the projection; a hand-made.st— with or without a sidecar.xml— becomes one text-driven create; on a fresh clone (no.dump/) the.sttext is overlaid on a fresh IDE baseline, and objects missing from the IDE are recreated from text. Conflicts keep the existing ".st wins" policy. Unmanaged.stfiles are never deleted by export; entries with no editable artifact (mirror XML only) are never pushed back into the IDE.- The Project Options dialog shows a single derived-files list that follows the selected paradigm instead of two lists at once: the "Text-first mode" checkbox is the selector, and the list beneath it swaps in place — XML-first shows the
.st/.csvprojection toggles ("Derived views"), text-first shows the per-kind "Keep XML in view" list. The two are mutually exclusive, so only the relevant one is displayed.
Export overwrite protection (both modes):
- Export no longer silently overwrites locally-modified view files. Interactive export shows a review dialog with two independent, opt-in checkboxes — "Overwrite my local changes" and "Remove the unmanaged derived files" — both off by default, plus Continue / Cancel. Headless, daemon, and direct engine invocations skip dirty files by default: the files keep their content, their previous manifest hashes are carried forward so the next import still sees the edits, and they are reported as "pending import" (
pending_importin the daemonsync_export_textresponse, and in the export completion popup). - Unmanaged derived (
.st/.csv) files with no manifest entry are kept by default and reported (softly proposed for removal), not deleted. Removal is a separate opt-in:--remove-orphans(engine CLI) orremove_orphans=true(operation/daemon params); the daemon lists them asremovable_orphans. Orphan removal is independent of dirty overwrite —--overwrite-dirtyno longer deletes orphans. - Behavior change: engine
exportwithout flags now runs in skip-dirty mode and no longer deletes orphan projection files. Pass--overwrite-dirtyto regenerate locally-modified files and--remove-orphansto delete unmanaged derived files. - New engine subcommand
check-dirtywrites.dump/dirty_report.json(dirty managed files + would-be-removed orphans) without touching the view.
Engine robustness (both modes):
- The external engine no longer crashes when a diagnostic line contains non-ASCII characters — e.g. embedded-resource objects (
Resources/Embedded/*) whose name is the original Cyrillic source path. On Windows the engine's stdout defaults to the legacy ANSI codepage (cp1252), socompare— the verb that logs per-object diff details — raisedUnicodeEncodeErrorand exited non-zero, which the daemon surfaced as a bareexternal engine compare failed. Engine stdout/stderr are now forced to UTF-8 at startup (errors="replace"), matching how the daemon already reads them.
Deprecated¶
The following CLI aliases are retained for backwards compatibility but are scheduled for removal in 3.0.0:
cts rp— deprecated alias forcts raw.cts validate— deprecated alias forcts engine validate.cts resources— deprecated alias forcts engine resources.
Version 2.7.0 (2026-07-10)¶
New / fixed CLI commands (previously non-functional):
cts project open,close,list,list-devices,simulate,set-credentials,diagnose-online, and top-levelcts discovernow work against the reverse-pipe daemon. These eight methods were sent by the CLI but had no daemon handler and returnedUnknown method: Xat runtime; they are now implemented and covered by a CLI↔daemon parity contract test.cts project openguards against reopening the already-open project and uses a longer timeout for large projects;cts project closeuses a 60s timeout (a CODESYS modal save/disconnect dialog can still require manual dismissal).cts project listreports the primary project by path with a display-name fallback when the wrapper exposes no usable name.
Sync import reliability (POUs/objects inside folders):
- Native object import now succeeds for objects nested in folders. The embedded
ParentGuidin an exported native payload is rewritten to the resolved container's live GUID beforeimport_native, fixing the case where CODESYS refused the payload because the exported parent GUID no longer matched the live container. - Native import is now resilient and diagnostic: a single object that CODESYS rejects no longer aborts the whole import. Failures are collected, reported per object under
failed_native_objects, and the remaining objects still import. - A name clash with an object in a different or nested folder (or a global-scope object) is now reported with a clear, located message instead of a generic rejection.
- The device object cache is invalidated after import so newly created objects are visible immediately.
Daemon & online diagnostics:
diagnose_onlinenow primes the online-app cache before reading the PLC snapshot, so the PLC section is populated without a prior explicitapp-state/connectcall.SnapshotReader.read()returnsNoneinstead of raising aNameErroron a missing snapshot.
CLI correctness:
- Unknown CLI options now error with exit code 2 instead of being silently ignored (a typo such as
cts import --dry-runnno longer runs a real import).raw/enginepassthrough is unaffected. - Legacy
cts project ...commands now exit non-zero on failure, matching the daemon-command contract so scripting and CI see a truthful exit code. - The hidden
project/pousubcommand actions that duplicate a top-level command are now deprecated with a stderr warning pointing at the replacement.
Visualization — experimental, not yet fully tested:
cts visu from-svggained fill/stroke opacity support in SVG import. The SVG-to-visualization pipeline is still under active development and has not been fully validated end-to-end against the CODESYS visualization editor; treat it as experimental.
Maintenance and refactors:
- Extracted a shared discovery-report builder so forward-mode discover and the daemon
discoverhandler share one implementation. - Split the visualization builder into focused modules (
_builder_base,builder_frame,builder_inputs) and separatedfrom_svginto screen-create/append/GVL helpers. - Split the offline regression runner into per-scenario functions and replaced repeated
sys.pathjuggling with a single_engine_on_path()helper. - Extracted CLI dispatch into per-area handler modules (
_cli_handlers_daemon,_cli_handlers_project,_cli_handlers_visu). - Fixed a latent path bug where build/discover ops computed the engine directory one level short after the
.runtime -> src/ide_bridgemove; addedensure_engine_path()as the shared resolver. - Removed dead code and unused imports flagged during the cleanup pass.
Packaging:
setup.pynow ships every data file the code reads (themes/*.json,styles_snapshot.json,stylesheet.css,DESIGN.md,external_engine/sys_funcs.json) so a non-editablepip install .is complete (verified via wheel build).- Added
cli.__version__as the single source of truth and acts --versioncommand.
Tests:
- Added a CLI↔daemon method-parity contract test and a static daemon name-resolution guard that catches missing imports / unresolved globals in the IronPython daemon modules.
- Added end-to-end coverage for
commands.from_svgand rewrote the integration smoke test as proper pytest functions.
Version 2.6.1 (2026-06-15)¶
CLI fixes from user feedback:
cts importnow accepts--force-onlineto bypass the offline preflight check whendisconnectdoes not fully clear CODESYS' online state, and--dry-runto preview changes with the same report ascompare.cts plc-crcnow accepts--buildto compile the project before comparing CRCs.cts writenow reads the value back and returns it underread_back, so callers can verify the write actually took effect.cts engineand the deprecatedvalidate/resourcesaliases now accept--timeoutfor consistency with other commands; the value is ignored by the offline engine.cts read-objecthelp now explains path format and recommends--nameas the most reliable selector.cts rawhelp now mentionsforce_online=trueand points tocts raw helpfor the method list.cts testhelp now referencescli/TEST_FORMAT.md, and the "No .test/" error message includes format documentation pointers.
Daemon fixes:
_active_app_online_statenow prefers the cachedonline_appand only reports online whenis_connectedoris_onlineis true. It no longer falls back to a staleapplication_statestring (e.g.run) after disconnect, which caused the documenteddisconnect -> importworkflow to fail.sync_import_textnow returnsskipped_projection_objectsin its output when modified objects have projection-only changes that cannot be applied automatically, making silent no-op imports visible.variable-snapshotJSON summary now includes afailureslist with each failing path and error message.
Documentation:
cli/CLI.mdupdated with--force-online,--dry-run,--build,read-objectselector guidance,rawoverride examples, and improved test onboarding.
Version 2.6.0 (2026-06-09)¶
CLI Contract:
- Added the short
ctsconsole alias alongsidecds-text-sync. - Simplified the primary CLI surface around the main user workflows:
ping,status,export,compare,import,build, PLC lifecycle commands, variable commands, project/object tools,raw, andengine. - Removed the separate
--manualmode in favor of one short but explicitcts --helpoutput. - Renamed CLI documentation from
cli/MANUAL.mdtocli/CLI.mdand the CI/CD test-format document tocli/TEST_FORMAT.md. - Updated
cts --helpto document the operational model: folder, CODESYS IDE, and PLC are independent states; deployment moves datafolder -> IDE -> PLC; folder-to-IDE import must be done while disconnected from the PLC.
Daemon & PLC State:
project-infonow returns CODESYS Project Informationsummaryfields (Company,Title,Version,Author,Description,DefaultNamespace,URL) and all customproperties, includingcds-sync-folder,cds-daemon-config, andcds-sync-pc.pingandstatusnow include cached PLC state:connected,online,running,application_state, active application name, and application path. These commands do not auto-connect to the PLC.- Fixed stale
online_appcache handling soconnect -> plc-crc,device_status,start, andstopwork without first callingapp-state. - Closing the daemon window with the
Xbutton now requests daemon shutdown instead of leaving the script operation running and blocking the CODESYS UI.
Import / Compare / Object Tools:
sync_import_textnow updates existing.sttext objects through the CODESYS text API when native XML import does not apply the changed POU body.comparenow ignores XML serialization noise for externalized.st/.csvprojections when the effective projection content matches the IDE content, including theTextBlobForSerialisationempty-container case.- Added daemon support for
read_object;cts read-object --name MAINreturns declaration, implementation, and object path. update-pouanddelete-pounow default to the active CODESYS application instead of the previous hardcodedCI_CD_Application.
Cleanup:
- Removed legacy daemon/dead-code paths replaced by the reverse-pipe daemon flow.
- Removed unused daemon imports left after the reverse-pipe simplification.
Release Verification:
- Verified on a live CODESYS daemon test bench with a clean project state (
36/36 unchanged) and daemon permissions open (deny: []). - The daemon-driven
ctsworkflow was exercised end to end across the main user-facing functions:ping,status,permissions,raw,project-info,project-tree,export,compare,import,build,connect,disconnect,start,stop,app-state,plc-crc, variableread/write,variable-map,variable-snapshot,variable-restore,read-object,update-pou,delete-pou,read-log,sync,app_history,app_crc,app_info,create_boot_app,plc_upload, JSON output, text output, and--pretty.
Fixes:
- Fixed creating
FUNCTIONPOUs viasync_import_text: the return type is now parsed from theFUNCTION name : <TYPE>header and passed to the CODESYScreate_pouAPI (handlesSTRING(80),ARRAY[..] OF X, qualified user types, case-insensitive). Previously this crashed withSpecified argument was out of the range of valid values. Parameter name: return_type. A clear error is raised if a FUNCTION has no return type. sync_import_textno longer aborts the whole import on a projection conflict. Policy is now disk wins,.stis canonical: when an object's raw XML projection and its.sttext were both edited on disk, the.sttext wins (overlaid on the IDE baseline) and the import continues with a warning. Export-only CSV/XML projection edits with no importer are skipped with a warning instead of failing.sync_import_textnow fails early with a clear "disconnect first" error when the application has a live online session, instead of silently creating no objects. Override withforce_online.update_pounow reportsimpl_ok: truewith animpl_skippednote for objects that have no implementation section (GVL/DUT/interface), instead of a misleadingimpl_ok: false.
CLI:
- Added the top-level
read-vars EXPR ... [--file F]command for batch-reading multiple variables/expressions. It sends a proper JSON list to the daemon, avoiding therp read_variables --namespitfall where every value is passed as a raw string ('names' must be a list).
Documentation:
-
cli/CLI.mddocuments sync direction (IDE/disk), the difference between raw XML snapshot import and text edits, the disk-wins conflict policy, and thatupdate_pouis for single-object edge cases. -
Fixed UTF-8 handling in the IronPython reverse-pipe daemon for
.stand JSON text reads, includingsync_compare_textfailing on IronPython 2.7 because builtinopen()does not acceptencoding=. Thanks tokevin00156for highlighting the bug.
Version 2.5.1 (2026-05-28)¶
CLI & Daemon:
- Added the
cds-text-syncCLI and reverse-pipe daemon workflow throughProject_daemon.py. - Added concise dashboard output for
rp cicd: file-level PASS/FAIL plus suite summary. - Changed the default CI/CD test folder from
test/to.test/, with legacytest/fallback for existing projects. - CI/CD plans now require an explicit
applicationfield so tests cannot silently run against the wrong application. - Fixed
Project_options.pyruntime imports after moving the Python 3 engine tocli/external_engine/. - Updated the recommended
.gitignoreentries for.dump/, reports, logs, backups, and temporary diff files.
Installation & Documentation:
- IRM installer now validates that
python --versionworks and reports Python 3 before installing the CLI. - IRM installer now offers to install the system CLI with
python -m pip install -e <install-path>. - Documentation now states that copying files into CODESYS
ScriptDirdoes not install thecds-text-syncshell command. - README and manuals refreshed for the CLI workflow, daemon demo, and test runner behavior.
Infrastructure & Quality:
- GitHub Actions CI: Added continuous integration workflow running tests on pushes and pull requests.
- Node 24 Update: CI actions updated to target Node 24 runtime.
- Unit Test Tier: Introduced structured unit test suite for external engine components.
- Test Fixtures: Unignored fixtures directory to include test data in version control.
Security & Settings Window:
- WinForms Settings window (poll frequency slider + permissions checkboxes)
rp permissions— read-only config via CLI- Storage in
cds-daemon-configproject property (JSON) - Default deny list: reset_plc, create_boot_app, plc_upload, source_download
- Only the Settings window (not CLI) can change permissions
- Startup messages in dashboard (version + sync folder status)
Fixes:
- Stop Daemon no longer freezes CODESYS (Application.Exit, early loop break)
- Settings/Stop buttons swapped for ergonomics
- Sync folder warning on daemon start
run_external_engine()path fixed tocli/external_engine/
User Experience:
- Reference Compare Preview: Validation now shows a reference comparison before applying changes.
Documentation:
- Zed Extension: Mentioned the Zed Structured Text extension for users who prefer the Zed editor.
Version 2.0.1 (2026-05-11)¶
Ambiguous Textual Object Projections:
- TypeGuid ST Pragmas: Added
(* cds-text-sync: TypeGuid="{...}" *)metadata pragmas for textual projections whose CODESYS object type cannot be reconstructed from ST syntax alone. - Persistent Variables Projection: Persistent variable lists can now be exported and edited as
.stprojections while the sync pragma is stripped before XML rehydration and IDE text updates. - Profile-Driven GUID Policy: Added
create_type_guidsandambiguous_text_type_guidsprofile sections so special textual object handling is configured outside hardcoded syntax detection. - Textual Create TypeGuid:
CreateTextObjectpatch entries can now carry an explicitTypeGuid, preferred by the IDE bridge before built-in fallback GUID candidates. - Persistent Variables Safety Guard: Creating a second Persistent Variables object in the same application is rejected before IDE apply because CODESYS supports only one such object per application.
- IDE Bridge Cleanup: Removed noisy create fallback diagnostics and the native XML template create fallback; existing textual objects are updated through available text documents even when CODESYS does not expose reliable
has_textual_*flags. - Completion Summary Option: Export and import now show a final success popup by default, with a project option to disable these completion summaries.
Version 2.0.0 (2026-04-29)¶
XML-First Synchronization Core:
- Native XML Snapshot Contract: Reworked the sync flow around a fresh CODESYS Native XML snapshot for every export, compare, and import operation.
- External Python 3 Engine: Moved comparison, folder modeling, patch building, profile handling, and diagnostics out of the IDE bridge and into
src/external_engine/. - Thin CODESYS Bridge: Reduced IDE-side scripts to snapshot export, external engine dispatch, targeted text API updates, and native XML patch application.
- Semantic XML Compare: Added normalization for CODESYS serialization noise such as volatile timestamps, generated IDs, dictionary ordering, and whitespace.
- Mixed Patch Application: Textual POUs are now applied through CODESYS text APIs before native XML patch import handles remaining non-textual objects, preserving child method/action/property bindings.
Public Script Set:
- User-Facing Commands: Stabilized the public root entrypoints as
Project_directory.py,Project_options.py,Project_export.py,Project_import.py,Project_compare.py, andProject_compare_ui.py. - Diagnostics Commands: Added
Project_build.py,Project_discover.py, andProject_resources.pyfor build checks, environment/type discovery, and snapshot resource analysis. - Hidden Engine Helpers: Kept patch builders, project models, and runtime internals behind the
Project_*.pyscripts and external engine CLI. - Legacy Archive: Preserved older scripts under
old_scripts/for reference while making the new XML-first workflow the active path.
Project Layout & Settings:
- Project Settings File: Added tracked
cds-text-sync.jsonsupport for layout, profile, and projection choices. - View Root Modes: Added support for legacy
.dump/views, defaultproject-view/, explicit--view-root, and experimental root-view mode. - Generated State Separation: Standardized generated folders around
.dump/,.backup/, and.diff/, with stale managed files cleaned by manifest ownership. - Options UI: Reworked
Project_options.pyso users can choose layout, active CODESYS profile, and optional derived text projections from a dialog. - Pre-Import Safety Backups: Added optional timestamped
.projectbackups before IDE-changing imports, stored only in.backup/with retention cleanup.
Optional Text Projections:
- Readable POU
.stViews: Added opt-in.stprojections for POU text with declaration first,// --- implementation ---, and implementation second. - Flat Child POU Files: Added
.stprojections for methods, actions, properties, and accessors as sibling files such asST_FB.ST_METHOD.st. - DUT
.stViews: Added declaration-only.stprojections for DUT objects such as structures, enums, unions, and aliases. - Standalone
.stCreates: Added controlled creation of new text objects from standalone.stfiles when the semantic kind can be detected. - Text List CSV: Added import-safe CSV projections for TextList objects, limited to editing existing rows and translations.
- Alarm Item CSV: Added import-safe CSV projections for alarm items, limited to existing alarm row updates.
- No Duplicate PR Diffs: When projections are enabled, export externalizes owned text into
.stor.csvand redacts the duplicate text from the XML sidecar. - Projection Conflict Detection: Compare/import now fail explicitly when both canonical XML and its derived projection changed since the last export.
Compare & Review Workflow:
- Interactive Compare UI: Added checkbox review, object metadata, and disk-vs-IDE diff viewing through
Project_compare_ui.py. - Projection-First Diffs: Compare UI prefers
.stor.csvdiffs when a projection owns the edited text, while keeping XML available for fallback cases. - Selected Actions: Added filtered import/export support by GUID so Compare UI can apply only checked objects.
- Large Project Stability: Reduced compare report memory pressure and avoided flooding IDE output with repeated missing-resource messages.
Diagnostics & Large Project Support:
- Build Diagnostics:
Project_build.pybuilds the active or selected application and writes.dump/build_<Application>.logplus.dump/build_report.json. - Discover Diagnostics:
Project_discover.pyrecords live IDE tree/type information into.dump/discover_tree.logand.dump/discover_report.json. - Resource Diagnostics:
Project_resources.pyanalyzes snapshot object sizes and categories, writing.dump/resources_report.jsonand.dump/resources_top.log. - Missing External Resource Skip: Snapshot export skips missing image/file-like resources that can block CODESYS native export on large projects.
Known Limitations:
- Visualization objects can report native import success while specific visual property edits are not applied by CODESYS; this remains a targeted investigation area.
- CSV projections are update-only in this release: inserted, removed, renamed, or duplicate rows fail explicitly.
- Graphical CFC/FBD/LD implementations are intentionally excluded from
.stprojections unless a profile explicitly marks a safe textual representation.
Version 1.7.5 (2026-04-17)¶
Profiles, Semantic Kinds & Sync Policy:
- JSON Type Profiles: Added profile files in
profiles/with inheritance,guid_aliases,context_rules,sync_profile_overrides, andsync_direction_overridesso projects can remap, merge, force XML handling, or skip types without code changes. - Semantic-First Type Resolution: Completed the migration from scattered GUID checks to centralized semantic kind resolution, making object classification more consistent across export, compare, import, and discovery.
- Per-Type Direction Control: Added
bidirectional,export_only,import_only, anddisabledsync direction policies per semantic kind. - Library Manager as Export-Only:
library_managernow exports for Git visibility and diffing, but is skipped on import to avoid unreliable placeholder restoration. - Hardware Policy in Profiles:
deviceanddevice_moduleare now controlled by profile settings instead of hardcoded skips, usingnative_xml+export_onlyby default. - Profile Documentation: Reworked profile docs into
profiles/profiles.mdand added a reusableprofiles/template.json.
Runtime Architecture & Entry Points:
- Internal Runtime Extraction: Moved internal engine modules into
.runtime/and reduced the top-levelProject_*scripts to thin entrypoints. - Shared Bootstrap Layer: Centralized runtime loading into a shared bootstrap module and renamed the public bootstrap entrypoint to
cds_bootstrap.py. - Automation-Friendly Script Calls: Updated the main entry scripts so they can be invoked more cleanly by external tooling and scripted workflows.
Compare/Import Robustness:
- Nested ST Import Order Fix: Import now sorts textual files so parent POUs are created before nested children like
TaskMain.Method.st, fixing first-pass import into empty projects. - Shared Native XML Snapshot Path: Export and compare now use the same native XML snapshot builder and the same recursion policy for XML-based objects.
- Reduced False Hardware Diffs: Folder hash invalidation is now limited to the direct parent folder, preventing one
.stedit from forcing untoucheddeviceandtask_configobjects into noisy XML re-compare. - Cache Recovery for Exported Objects: Added export-side cache backfilling and better cache warnings so successfully exported objects are less likely to disappear from
sync_cache.jsonbookkeeping. - More Explainable Compare Logging: Compare now logs the exact reason an object dropped into slow-path XML comparison.
Logging, UI & Runtime Noise Reduction:
- Toggleable File Logging: Added a project setting to enable or disable file logging and updated ignore patterns accordingly.
- Quieter Compare/Import/Export Output: Reduced console/log spam in normal workflows and removed compare log teeing to
compare.log. - Settings Dialog Cleanup: Refined the Settings UI layout and grouping for a cleaner configuration flow.
- Unsupported Build Property Silence: Missing
build_propertiesmembers such asexternal_implementationare now skipped quietly instead of spammingINFO/WARNINGfor every object. - Python 3 Compatibility Cleanup: Replaced deprecated
callable()usage in runtime diagnostics with a Python-3-compatible check.
Version 1.7.4 (2026-04-11)¶
Attribute Synchronization (DRY Sync):
- Pragma-Based Metadata: Implemented a new synchronization system for IDE-specific attributes (e.g., "Exclude from build", "Link always") using
//% cds-text-sync.key=valuepragmas directly in.stfiles. - CODESYS API Fixes: Resolved issues with attribute access by correctly utilizing the
obj.build_properties(ScriptBuildProperties) API for reading and writing IDE flags. - Bi-directional Sync: Ensured that removing a pragma from the source file correctly clears the corresponding attribute in the IDE during import.
- Cache Integrity: Updated the quick hashing logic to include object attributes, ensuring that toggling IDE flags correctly invalidates the cache and triggers a re-export.
- Comparison UI Enhancement: The built-in diff viewer now renders IDE attributes as pragmas, allowing users to see and review metadata changes alongside code changes.
- Cache Migration: Bumped
CACHE_VERSIONto3.1to force a clean state rebuild and ensure all objects are tracked with attribute-aware hashes.
Version 1.7.3 (2026-04-02)¶
Move/Rename Detection & Stale File Cleanup:
- Moved File Detection: Implemented smart detection of renamed/moved project files by cross-referencing IDE orphan objects with disk orphan files using base filename matching.
- Automatic Path Invalidation: Enhanced cache invalidation logic to detect when objects are moved/renamed in the IDE, ensuring stale cached paths are refreshed during comparison.
- Stale File Cleanup: Added automatic removal of old files from disk during export when objects have been moved/renamed in the IDE, preventing orphaned files from cluttering the sync directory.
- UI Enhancements: Updated comparison dialog to display moved files with their old (IDE) and new (Disk) paths, using
~movedvisual indicator. - Import/Export Move Handling: Added logic to physically move objects within the IDE during import when path mismatches are detected, ensuring project structure stays synchronized.
- Statistics Update: Moved object count now reported in comparison summary (
~:prefix) and import/export completion messages.
Version 1.7.2 (2026-03-28)¶
Critical Fixes & UX Optimization:
- Module Import Fix: Resolved a critical
ImportErrorwherecodesys_uiwas not being loaded inProject_directory.py, causing a crash on startup for new projects. - Reference Bug Fixes:
- Fixed an undefined variable crash (
choice[0]) inProject_directory.py. - Fixed an undefined variable crash (
result[0]) inProject_export.pyduring orphaned file cleanup.
Version 1.7.1 (2026-03-27)¶
UI Robustness & Post-Sync Enhancements:
- Standard Windows Prompts: Replaced the unreliable native CODESYS
system.ui.chooseradio-button dialogs with standard Windows MessageBox dialogs (ask_yes_no,ask_yes_no_cancel) across all scripts. - Cancel Button Fix: Completely resolved an issue where clicking "Cancel" or closing dialogue windows would fail to halt script execution due to inconsistent CODESYS API return types.
- Import Final Confirmation: Added an explicit final summary dialog (
Ready to import X changes into the IDE... Proceed?) right before applying structural changes or deletions inProject_import.py. - Auto-Save & Workflow:
- Introduced optional automatic project saving and binary backup after an export is completed.
- Added a new 'Save Project after Export' toggle in the Configuration UI (
Project_parameters.py). - Centralized version compatibility checks, safety backups, and post-sync operations into
codesys_utils.pywfor cleaner architecture and standardized execution.
Version 1.7.0 (2026-03-27)¶
Merkle Tree & High-Performance Sync Overhaul:
- Lightning-Fast Comparison: Total sync/compare time reduced by ~90% (sub-10s for large projects) using a new Merkle Tree-based hierarchical hashing strategy.
- Intelligent Path/Type Caching:
- Implemented GUID-based caching for object classification and filesystem paths in
sync_cache.json. - Eliminates thousands of slow CODESYS COM API calls (
classify_object,get_children,build_expected_path) on repeat runs. - Hierarchical Merkle Skips: The comparison engine now uses folder hashes to skip entire unchanged branches of the project tree instantly.
- Import Optimization:
- Eliminated redundant double-save operations during import/backup, reducing the post-import pause by 50%.
- Optimized POU child restoration and metadata handling.
- Hybrid XML Hashing: Integrated last-known XML hashes into Pass 1 so folders containing mixed ST and XML objects can still benefit from Merkle Tree skips.
- Integrated Accessor Collection: Merged property accessor scanning into the main object pass to avoid redundant project-wide traversals.
- Profiling Tool Upgrade: Updated
Project_perf_test.pywith the new architecture to provide accurate real-world metrics, including cache hit ratios and Merkle skip statistics.
Version 1.6.7 (2026-03-25)¶
Silent Mode Removal & Backup Enhancement:
- Removed Silent Mode: All
silentparameters have been removed fromProject_import.py,Project_export.py,Project_compare.py, andProject_Build.py. Scripts now consistently use modal dialogs for all user feedback. - Unified UI Behavior: All operations now use modal dialogs (
system.ui.info/system.ui.error) in interactive mode, eliminating the previous inconsistent behavior. - Version Compatibility Checks: All version compatibility checks now always prompt the user when version mismatches occur, rather than silently logging warnings or ignoring the issue.
- Timestamped Backup with Retention: Enhanced import backup functionality with automatic retention policy:
- codesys_utils.pyw: Added
cleanup_old_backups()function to automatically delete old timestamped backups while preserving non-timestamped Git LFS backups - Enhanced Backup Function:
backup_project_binary()now acceptsretention_countparameter and returns the backup filename on success - UI Enhancement: Added "Max Backups to Keep (Optional)" field in settings dialog (default: 10, minimum: 1)
- Persistent Settings: Added
cds-sync-backup-retention-countproperty to Project_parameters.py for cross-run persistence - Import Scripts: Both
Project_import.pyandProject_compare.pynow create timestamped backups before import operations when changes exist - Backup Reports: Import completion reports now show backup confirmation message when safety backups are created
- Cleanup Pattern: Only timestamped
.bakfiles matching pattern^\d{8}_\d{6}_.*\.bak$are subject to cleanup; non-timestamped backup files are preserved
Version 1.6.6 (2026-03-18)¶
Resource Analysis UI Enhancement:
- Interactive Results Dialog:
Project_resources.pynow displays results in a modern Windows Forms dialog instead of console output. - Sortable Data Grid: Click column headers to sort by Object Name, Type, Size, or Category.
- Full Object List: Shows all analyzed objects with scrolling support (previously limited to top 30).
- Summary Panel: Displays Total Code, Total XML, and Object count at the bottom.
- Fallback Support: Console output still works if UI components are unavailable.
Version 1.6.5 (2026-03-17)¶
Interface Export Support:
- Interface Objects: Added full support for exporting and importing
INTERFACEobjects with theirEXTENDSclauses preserved. - Interface Methods: Interface methods/properties now export as flat files (
InterfaceName.Method.st) matching the existing FB pattern. - Native XML Fallback: Added
export_interface_declaration()function that extracts interface declarations via native XML export whentextual_declarationis unavailable. - Updated Type GUIDs: Corrected interface type GUID to
6654496c-404d-479a-aad2-8551054e5f1eand addeditf_methodGUID for interface members.
Version 1.6.4 (2026-03-12)¶
UI Cleanup & Module Security:
- Hidden Internal Modules: Renamed all
codesys_*.pyfiles to.pywextension. This hides them from the CODESYS Script Engine menu, providing a cleaner user interface that only shows primaryProject_*.pycommands. - Custom Module Loader: Implemented a robust
_load_hidden_modulemechanism in all entry scripts to handle.pywimports with proper dependency ordering. - Deprecated Scripts Cleanup: Removed several unused and debug scripts (
debug_metadata.py,Project_Daemon.py) to streamline the repository.
Version 1.6.3 (2026-03-07)¶
Version Tracking & Compatibility Detection:
- Single Source of Truth: Added
SCRIPT_VERSION = "1.6.3"incodesys_constants.pyas the central version reference for all scripts. - Dual Storage Strategy:
- sync_metadata.json: Metadata file stored in export directory containing script version, last action (export/import), timestamp, duration, and statistics.
- Project Property: Version also saved to CODESYS project property (
cds-sync-version) for runtime compatibility checks. - Import/Compare Warnings: Both
Project_import.pyandProject_compare.pynow detect version mismatches and display warnings without blocking operations (User can continue at their own risk). - Improved Audit Trail: Each export and import operation updates
sync_metadata.jsonwith current script version, making it easy to identify which scripts were used for operations. - Git Integration: The
sync_metadata.jsonfile is now tracked in version control, enabling teams to see export/import history.
Version 1.6.2 (2026-03-04)¶
XML Import & Object Structure Enhancements:
- POU Child Management: Implemented saving and restoring of POU children during the XML import process to maintain project hierarchy.
- Parent Lookup: Enhanced parent POU lookup logic during object creation for improved structural accuracy.
- Empty Implementation Handling: Ensured that implementation markers are always present for specific object types, even if their implementation is empty (addressing issues where empty methods or properties might be skipped).
Version 1.6.1 (2026-02-26)¶
Orphan Deletion & Stability Enhancements:
- Bi-directional Orphan Management:
- IDE-to-Disk (Sync/Export): Existing logic in
Project_export.pycontinues to clean up files on disk that are missing in the IDE. - Disk-to-IDE (Import):
Project_import.pynow supports deleting objects from the IDE if they were removed on disk (e.g., from a Git pull). The "Disk wins" principle is now fully enforced. - Improved Comparison UI:
- The Interactive Results dialog now clearly identifies objects missing on disk as "Missing on Disk (DELETE from IDE?)".
- Importing these items will now safely remove them from the CODESYS project tree.
- Hardware Stability (Device Exclusion):
- Hard-excluded
deviceanddevice_moduleobjects from the synchronization engine. - Syncing these components via XML was found to be unstable (can lead to tree reconstruction and project "emptying").
- Users should configure hardware manually and sync the application logic.
- Bug Fixes:
- Fixed an issue where the import process could fail to report the correct number of updated/created items when deletions were involved.
- Updated default
.gitignoretemplate to include*.deviceand*.device_xmlpatterns as a safety measure.
Version 1.6 (2026-02-24)¶
Core Engine Refactoring & Interactive Sync:
- Multi-PLC & Multi-Application Support: The engine now automatically handles complex project hierarchies, organizing exports into a clear
Device/Application/Folderstructure (essential for modern CODESYS projects). - Metadata-Free Sync Engine: Significant refactoring to transition from metadata files (
_metadata.csv,_config.json) to a direct, hash-based two-way comparison between the CODESYS IDE and disk. This improves reliability when moving projects between machines or using Git. - Interactive Comparison Dialog:
Project_compare.pynow includes an interactive results window where you can selectively apply changes (Import or Export) directly from the diff list. - Project Discovery Tool: New
Project_discover.pyscript for mapping the project tree structure and diagnosing supported block types (logs findings tosync_debug.log). - Maintenance:
Project_daemon.pyhas been temporarily disabled. - Improved Comparison Logic: Better handling of graphical POUs and XML-based objects (Visualizations, Task Configurations) in the comparison engine.
Version 1.5.6.1 (2026-02-21)¶
Version 1.5.6 (2026-02-18)¶
Safety Net: Timestamped Import Backups:
- Automatic Rollback Point:
Project_import.pynow creates a timestamped backup (e.g.,20260218_220000_MyProject.project.bak) at the very beginning of the import process. - Configurable Safety: Added "Timestamped Backup before Import" toggle in
Project_parameters.py(enabled by default). - Non-destructive: These backups are placed in the
/projectfolder and use a.bakextension to avoid conflict with your primary Git LFS tracking.
Version 1.5.5 (2026-02-18)¶
Relative Path Support for Team Collaboration:
- Portable Project Configuration:
Project_directory.pynow supports relative paths (e.g.,./,./folderName/) in addition to absolute paths. - Manual Path Input: Added a new "Manual Input" option in the directory setup dialog, allowing users to type paths directly.
- Automatic Directory Creation: If a specified directory doesn't exist, it will be created automatically.
- Team-Friendly: Relative paths are resolved relative to the project file location, making projects portable across different machines and users without reconfiguration.
- Examples:
./- Sync to project directory./sync/- Sync to a subfolderC:\MySync\- Traditional absolute path still supported
Version 1.5.4 (2026-02-16)¶
Comparison Logging & Rerouting:
- Dedicated Comparison Log:
Project_compare.pynow reroutes its output tocompare.login the sync directory. - Recreative Logging: The log file is truncated and recreated on every run, providing a fresh report for each comparison.
- Tee Output: Comparison results are still mirrored to the CODESYS Script Output window for immediate feedback.
Version 1.5.3 (2026-02-16)¶
Line Ending & Git Consistency Fix:
- Cross-Platform Consistency: Fixed an issue where different line endings (CRLF vs LF) on different machines caused Git to show identical files as modified.
- Deterministic Export: The export script now explicitly uses LF (
\n) for all.stfiles regardless of the host OS by usingnewline=''in file operations. - Automated Git Configuration: Updated the
.gitattributestemplate to automatically disable text conversion for.stfiles (*.st -text), ensuring they remain as LF in the repository and are treated consistently by Git on all platforms.
Version 1.5.2 (2026-02-15)¶
Improved Property Sync & Bug Fixes:
- Enhanced Property Support: Properties with combined GET/SET accessors are now correctly handled. The export script now accurately combines both the
VARdeclaration and implementation code for each accessor into a single.stfile. - Bi-directional Accessor Sync: The import script now correctly parses combined accessor content and updates both the declaration and implementation in CODESYS.
- Object Restoration: Fixed an issue where objects deleted from CODESYS but remaining on disk would not be recreated. They are now automatically detected and restored during import.
- Bug Fix (#4): Resolved an issue where properties created manually in external editors were incorrectly identified or failed to import.
Version 1.5.1 (2026-02-15)¶
Performance & Optimization Update:
- CRC32 Hashing: Switched from SHA256 to CRC32 for file tracking, achieving 10-20x faster hashing performance and significantly reducing metadata size.
Version 1.5.0 (2026-02-13)¶
The "Power User" Update:
- Project_Daemon.py: New background service with Global Hotkey (
Alt + Q). - Quick Action Dashboard: Instant access to Export, Import, Build, and Backup commands.
- Enhanced Build Log:
Project_Build.pynow generates a clean, readable table format inbuild.logwith accurate line numbers for external editors. - Focus Management: Daemon correctly handles focus switching between Virtual Desktops and restores context after execution.
Version 1.4.0 (2026-02-12)¶
UI & Experience Overhaul:
- Configuration Dialog: Replaced the text-based menu with a modern Windows Forms dialog for easier configuration.
- Silent Mode: Added a "Silent Mode" option that uses non-blocking system tray notifications (toasts) instead of blocking popups.
- Safety: Added checks to prevent sync on wrong machine (PC Name check).
Version 1.3.0 (2026-02-09)¶
Binary Backup & Configuration Overhaul:
- Project_parameters.py: New interactive menu to toggle features.
- Binary Backup: Added optional
.projectfile backup loop. The binary is now updated on both Export and Import events. - Logging: Moved
sync_debug.logto the project sync folder (or Temp) to keepScriptDirclean. - Import Logic: Removed interactive menu from Import script; now uses project settings.
Version 1.2.0 (2026-02-09)¶
Safety & Validation:
- PC Check: Validates
cds-sync-pcto prevent syncing on the wrong machine. - Properties: All settings are now stored in Project Properties (
cds-sync-*).
Version 1.0.0 - 1.1.0¶
- Full support for nested folders.
- Detection of deletions (Orphan cleanup).
- Library version tracking (
_libraries.csv).