A fork of fbarresi/TwinCatAdsTool — the TwinCAT tool you ever wanted and your lite alternative to Visual Studio.
This fork fixes several defects in the persistent variable backup and restore engine, two of which could lose data without saying so, adds a command line so a backup can be automated, turns the live value graph into a scope that can be stopped and scrolled through, and modernises the application: .NET 8, ADS 7 (one binary for TwinCAT 4024 and 4026) and a Fluent interface.
Everything below is either measured on a real plant or stated as unverified. Where a claim rests on a measurement, the numbers are given.
Dark by default, with a light theme a click away — the Theme button at the foot of the navigation panel switches between the two without a restart, and the choice is remembered. The shot is of the backup tab; the explore tab is further down.
| Using it | The manual: connecting, backing up, restoring, comparing, the scope, the command line, and what to do when something goes wrong. Also as a PDF. |
| Why this fork exists | Six defects in the original backup and restore engine, with the evidence for each |
| What got faster | A backup from ten minutes to 0.1 s, a restore from 11.4 s to 1.9 s, and where the time actually went |
| Evidence | 10,978 leaves written and read back, one by one, on a real installation |
| The explore tab is now a scope | Stop, scroll back, zoom, trigger, export, watch sets |
| The compare tab can repair | Pick differences one by one and write just those onto the PLC |
| Command line | Backup, restore and compare without the window, for scheduled tasks and scripts |
| Modernisation | .NET 8, ADS 7 for TwinCAT 4024 and 4026, the Fluent interface |
| What is verified, and what is not | Measured, tested, and honestly untested |
| Download | Two builds, and which one you want |
| Reporting a problem | Where to send it and what to include |
The first restore attempted against a real PLC reported success and had written only part of the data. Chasing that down turned up a chain of defects, each hidden behind the previous one, all of them present in the original code.
The list matters more than the fork: anyone who has used this tool to restore structured persistent variables may have a plant that is only partly configured, with no error to show for it.
IValueSymbol.ReadValueAsync returns a result object, not the value:
Task<ResultReadValueAccess> ReadValueAsync(CancellationToken cancel);The writer passed that wrapper straight into the conversion layer, which accepts object, so the
compiler had nothing to say. Inside, the cast to DynamicValue produced null: no members, no
elements, every structure and array treated as a scalar. The restore then reported
GVL.PersVarGlobalUser1_1: failed - backup value '' does not fit the plc type
for every structured variable. The signature is identical in the 5.x line of the ADS library, so this defect predates any change made here.
WriteValueAsync returns a result too, and its Succeeded flag was ignored outright: a write the
PLC refused was counted as a write that worked.
This is the serious one, because it reported success.
When the ADS library enters a struct member or an array element, it hands out a copy of the
buffer, not a view (DynamicValueFactory.CreateValue calls sourceData.ToArray()). The old engine
read a whole variable, changed the value tree in memory and wrote the variable back. Anything
written into a nested branch went into a copy nobody ever sent to the PLC — and
TrySetMemberValue returns true without doing anything for a member that is not primitive, so
nothing failed.
Measured on a live PLC. A backup taken before the restore, and one taken after, with the restore asking for seven values:
| Variable | Before | Asked for | After | |
|---|---|---|---|---|
PersVarGlobalArray |
[6,7,8,0] |
[11,22,33,44] |
[11,22,33,44] |
written |
PersVarGlobalUser1_1.Int1 |
157 | 1001 | 1001 | written |
.Int2 |
168 | 1002 | 1002 | written |
.Bool1 .Bool2 .Bool3 |
true, true, false | false, false, true | false, false, true | written |
.InInVar.IntInIn1 |
12 | 9999 | 12 | lost |
.InInVar.RealInIn1 |
123.2345 | 999.5 | 123.2345 | lost |
The tool reported the whole variable as written.
The boundary is not "arrays work, structures do not": the array of INT at the root was written.
What is lost is everything reached by passing through a non primitive child.
The fix. The engine no longer modifies the value it read in order to write it back.
PlcLeafPlanner works out which individual leaves the backup wants written, and each one is
written to the symbol that owns it, grouped into sum commands. The value that was read is only
consulted, to learn the declared type of each leaf. The copy is gone from the path rather than
worked around.
Two things fall out of this. The report now names the leaf — GVL.Var.InInVar.IntInIn1: ads error ... instead of just the variable — and array elements are addressed by position among the
child symbols rather than by a computed index, which removes the Dimensions mismatch! failure on
multidimensional arrays in the write path, and turned out to agree with the order the backup
flattens them into.
The planner is a pure function, so the whole thing is testable without a PLC:
Tests/TwinCatAdsTool.Logic.Tests/PlcLeafPlannerTests.cs.
The first backup taken against a real PLC serialised a User1DUT as twenty numbers:
"PersVarGlobalUser1_1": [157, 0, 168, 0, 1, 0, 0, 0, 12, 0, 0, 0, 16, 120, 246, 66, 1, 0, 0, 0]The data was all there — 16,120,246,66 is 123.2345 as a little endian REAL — but the shape was
gone, and the report still said 20 ok, 0 failed.
The symbols were being loaded with SymbolsLoadMode.VirtualTree, which hands a STRUCT back as
byte[]. Only SymbolsLoadMode.DynamicTree produces a DynamicValue whose members can be walked.
Verified by reading the same symbol both ways:
| Load mode | Type of the value |
|---|---|
VirtualTree |
System.Byte[] |
DynamicTree |
DynamicValue with Int1, Int2, Bool1, InInVar, Bool2, Bool3 |
What made this hard to see: other structures in the backup looked correct, but they had not been
read as structures. Inside a STRUCT PERSISTENT every member is persistent in its own right, so
the scanner picked them up as separate scalars and the JSON object was rebuilt from the paths. A
structure is a single root only when it is the outermost persistent symbol, and that is the only
case where the defect showed.
Restoring a backup onto the very PLC it came from failed on four variables out of twenty:
GVL.ActualLTime: backup value '12.01:26:40.8640000' does not fit the plc type
GVL.PersVarGlobalTime1: backup value '00:00:00.3000000' does not fit the plc type
All three types normalise to a TimeSpan. JSON has no notion of one, so a backup read back from
disk hands the value over as a string — and TimeSpan does not implement IConvertible, so the
general Convert.ChangeType threw. Timestamps were spared only because Newtonsoft re-parses them
as a date token, already typed.
Also present in the original code, and invisible until a restore finally reached those variables.
In the original PersistentVariableService, the per-variable catch wrote a log line and the
variable did not go into the JSON. The file was saved as though it were complete. At restore
time the variable was not there, so nothing was written and there was nothing to report.
Every persistent variable now produces an outcome — written, failed with its ADS error, or skipped with the reason — and both backup and restore show the report in the interface.
LoggerFactory lives in TwinCatAdsTool.Interfaces and calls LogManager.GetLogger(name), an
overload that resolves the repository from the calling assembly. Startup configured the
executable's repository instead, so every logger the application created had no appender at all.
Two more things were in the way. log.config was not copied to the publish output, and once it was,
a single file build unpacks bundled content into a temporary folder rather than next to the
executable — so CopyToPublishDirectory and ExcludeFromSingleFile are both needed. And an
appender that cannot open its file keeps the reason to itself, in its own error handler, which
nothing read.
The application now checks that a log file actually appeared and, when it did not, writes
logging-error.txt saying why, including the appender's own message. If the folder next to the
executable cannot be written to, everything falls back to %LOCALAPPDATA%\TwinCatAdsTool — a
blocked folder used to swallow the log and the note explaining its absence.
A missing log.config no longer means no log: the executable configures the same rolling file
appender in code. log.config stays as an optional override.
The original engine delegated to TwinCAT.JsonExtension, whose ReadRecursive walks down to every
leaf and issues ReadSymbol + ReadValue for each: two ADS round trips per leaf. An
ARRAY[1..500] OF ST_Dati with 20 members is 10,000 leaves, so about 20,000 sequential telegrams.
On the client side iterator.Contains(s.Parent) inside a Where made the scan O(n²).
Each persistent variable is now transferred whole, and variables are grouped into sum commands, which move a batch in one telegram. Sum commands also return a per-symbol error code, which is what makes the reporting possible at all.
Measured on a real plant:
| Operation | Before | After |
|---|---|---|
| Backup, 71 variables | ~10 minutes | 0.1 s |
| Restore, 48 variables / 10,978 leaves | — | 2.5 s |
Writing leaf by leaf means more ADS writes than writing whole variables, which was the obvious objection to the fix. The measurement settles it: 2.5 seconds for a plant with 465 arrays and 833 structures.
A restore on a larger plant — 49 variables, 103,234 individual values — took 11.4 s, which was worth understanding rather than accepting. The log now carries the breakdown, so the same question can be answered on any plant:
Restore timing: scan 0.04 s, read 0.13 s, plan 1.32 s, prepare 0.00 s, transfer 0.39 s
for 103234 values in 12 commands
Three things came out of measuring it, and two of them contradicted what looked obvious:
The cost of writing is set by the number of commands, not by how many values they carry. At 500 values per command the transfer took 6.43 s across about 207 commands; at 10,000 it took 0.39 s across 12. That is 31 ms per command in both readings — the payload barely matters. The ceiling is now high and the byte limit is what decides, with a refused command halved and retried rather than abandoned, so a controller with tighter limits than this one is met by asking for less at a time instead of collapsing into a write per value.
Reading the current values was still one round trip per variable. The restore reads each persistent variable before writing it — not to change it, but because it says which type every leaf holds. Those reads now go out as one sum command, the way the backup has always read. It was the original defect of this project, two round trips per leaf, surviving in a corner of the write path.
Walking down to a leaf rebuilt the parent's children every time. Asking a symbol for its sub-symbols builds that collection afresh, so a structure holding a hundred-element array had those hundred rebuilt once for every leaf underneath it. The nodes already reached are now remembered for the length of one variable, which turns a descent per leaf into one per node.
| before | after | |
|---|---|---|
| read | 1.0 s | 0.13 s |
| plan | 3.5 s | 1.32 s |
| transfer | 6.4 s | 0.39 s |
| total | 11.4 s | 1.9 s |
Two things that looked like the answer were not: the ADS handles the write commands need cost 0.00 s, and stopping the PLC changed nothing at all. Both were measured before being acted on, which is the only reason no time was spent fixing them.
The strongest check available. A backup of a real installation — 48 persistent variables, 10,978 leaves, nesting up to eight levels deep, 465 arrays, 833 structures, of which 10,960 leaves lie below the second level — was filled with a distinct value per leaf, restored, and backed up again for a leaf by leaf comparison.
| Run | PLC | Leaves correct | Divergent |
|---|---|---|---|
| 1 | in Run | 10,874 / 10,978 | 104 |
| 2 | in Stop | 10,978 / 10,978 | 0 |
The 104 in the first run were all of the leaves of Pers_CC[0] and Pers_CC_Axia[0], while every
other index of those same arrays was correct, as were index 0 of five other arrays of structures.
No structural rule separates them. What pointed at the answer:
Pers_CC[0]._Setting._ConteggioCicli held 105, a value no leaf in the file asked for — so it
had not been misplaced, it had appeared on its own. Slot 0 of those two arrays is the working slot
the PLC program keeps cleared.
With the program stopped the divergence disappears entirely.
A note on method, which cost a wrong conclusion before it was understood: a backup restored onto a running PLC cannot be compared leaf by leaf, because what the program writes by itself is indistinguishable from what the restore would have lost. Verifying the tool needs the PLC stopped.
The generator is in Tests/ManualVerification/fill_backup.py. It guarantees two things without
which the comparison proves nothing: every leaf changes (a leaf left equal is blind — if the
restore dropped it, the returned backup would match anyway), and no two sibling leaves get the
same value (otherwise a swap between neighbours cancels out). Values stay in ranges that fit any
declared PLC type, because the width is not visible from the JSON: integers in 1..127, floats as
whole numbers plus 0.5 (exact even in a single precision REAL, so a difference is an error and not
a rounding), strings of the same length as the original.
Full procedure and results: docs/RESTORE-VERIFICATION.md.
The explore tab watches live values and plots them. It had one weakness that made it hard to use for the thing it exists for, and it was structural rather than cosmetic: one duration decided both what was kept and what was shown. Samples were discarded at the moment they left the screen, so going back to look at what had just happened was impossible by construction — there was no missing button, there were no data.
The scope in a window of its own, stopped and being looked at — Frozen, with Live offered to
return to the present. Two BOOL signals hold a lane each along the bottom, drawn as steps: the
narrow green pulse is a single scan, and on a shared linear axis it would have been a spike on a flat
line at the foot of the plot. wSecond runs above them on a scale of its own. One minute is on
screen out of ten kept, sampled every 10 ms rather than at the ADS default of 200.
Recording and viewing are now two separate spans:
| Memory | how much of the past is kept |
| Window | how much of it is on screen |
Scrolling and zooming move the window inside the recording. Everything on the plot is derived from the recording on every redraw — the series hold no state of their own — so freezing, scrolling and zooming are the same operation on two numbers, and the cost of a redraw is set by what is visible rather than by how long the tool has been running.
Stop freezes the view as well as the recording. A scope that carries on scrolling into empty time after stop has thrown away the very thing that was being looked at. Start returns to the live edge.
Arrows move by a quarter of a window at a time — a fraction of the width rather than a fixed time, so the gesture means the same thing at every zoom level. The wheel zooms; shift and the wheel scrolls. When the view has left the live edge it says Frozen and offers Live to return.
OxyPlot's own zoom is deliberately off: the visible slice is state the view model owns, and two things moving the same axis would fight over it on every redraw.
A BOOL used to share a linear axis with the analogue signals — a flat line at the bottom of the
plot — and its transitions were drawn by interpolating between two readings, so a change appeared as
a diagonal ramp as long as the sampling interval. A bit does not travel through the values in
between.
Bits now get a stair-step trace in a lane of their own, stacked along the bottom, with the analogue signals sharing the band above. On a machine the interlocks and the step bits are most of what is worth watching, and they were the least readable thing on the plot.
The question a scope is really asked on a machine is not what the last ten minutes looked like but what happened around the moment something changed.
Pick a plotted signal and a condition — goes TRUE, goes FALSE, rises above, falls below — and arm it. When it fires the recording carries on for half a window, so what followed the event is captured too, and only then does everything hold still with the event in the middle of the plot, marked by a line.
Every condition is a crossing, never a state. A signal that is already TRUE when the trigger is armed has not just gone TRUE; firing on it would make arming useless for the one case it exists for. For the same reason the first reading of a signal never fires: with nothing before it there is no crossing, only a value.
Symbols were watched with the ADS default notification settings — OnChange with a cycle time of
200 ms — which was never stated anywhere. The graph could not show anything shorter than that,
and there was no way to find out, let alone change it.
The cycle is now a field in the scope toolbar. Two things are worth knowing, and they are why the number is shown rather than hidden:
- it is how often the server looks, not how often it transmits; the mode is still
OnChange. It is the shortest event that can be seen at all; - timestamps are taken when the notification arrives at the pc. Going down to 10 ms makes the order of edges visible, not their duration to the millisecond. Faithful timing needs code inside the PLC, which is the territory of TwinCAT Scope.
Changing it registers the notification again: ADS reads those settings when a notification is created and does not revisit them.
CSV exports the window being shown, not the whole recording: the slice on screen was framed deliberately, and exporting more would silently throw that away. The layout is one column per signal and one row per instant at which any of them was read. Signals do not change together, so a naive table would be mostly blank; every cell instead carries the value its signal was holding at that instant, which is what the signal actually was, and is what makes the file plottable in a spreadsheet without further work. The field separator follows the culture — where the decimal mark is a comma, a comma cannot also separate the fields.
PNG saves the plot on white with dark text: the plot on screen is transparent and coloured for the current theme, and neither survives being put in a file.
The scope shares the page with the symbol tree and the watch list, so it is never more than a band across the bottom. One button moves it into a window that can be made as large as the screen, or put on a second one beside the machine; closing that window brings it back. The control itself is moved rather than a second one built beside it — a plot model belongs to one plot view at a time — so the recording, the window being looked at and the trigger carry on undisturbed.
The symbols being watched can be written to a file and read back, so a set survives the session and can be handed to whoever is at the machine next. The format is deliberately flat and meant to be edited by hand:
{
"variables": [
{ "path": "MAIN.bEnable", "graph": true },
{ "path": "GVL.rSpeed", "graph": true },
{ "path": "MAIN.sState", "graph": false }
]
}Values are not saved. A watch set says what to look at; carrying stale readings in the same file would invite reading them as measurements. Paths the PLC does not have are collected and reported together with what ADS answered — a set written against another version of the program is exactly when knowing which symbols have gone matters.
Type and the persistence marker P are read straight off the symbol and had never been shown, and
the tooltip on the name now carries the path together with the comment from the declaration. Knowing
while watching a value that it is one of the variables the backup and restore tabs act on saves a
trip back to the project.
Comparing used to end where it got interesting. Two backups were put side by side as text, and
that is a different question from the one being asked: a textual diff of two json files reports how
the files were written — key order, spacing, whether a number was printed as 1 or 1.0 — as
though the plant had changed. Worse, a line of a diff names nothing. You could see that something was
wrong and then had to go and fix it by hand, in another tab, one value at a time.
It now compares leaf by leaf, the same way the command line's compare always did. Every row is
one symbol on the PLC, named by its path — and because a row names a symbol, a row can be acted on.
| Pick a difference | The two arrows on a row send that value one way or the other |
| Pick all of them | All to left / All to right, for a wholesale correction |
| Move between them | First, previous, next, last — instead of hunting through thousands of rows |
| Only differences | On by default. Off, every value of the backup is listed, which is how you confirm that a value has not moved |
| Undo all | Takes back every pick. Nothing has been written yet |
| Write to plc | One deliberate action, after a confirmation that says how many values it will write |
Picking is not writing. Marks are collected, counted at the top of the tab and can be dropped wholesale, and only the last button touches the machine — which is the only safe order when the other end of the cable is running.
Differences are written to the PLC only. A backup file is left exactly as it was found, so the direction that is open depends on which side you filled from the PLC: if the plant is on the left, values travel left. A value that exists on only one side is shown, and greyed out rather than offered: bringing it across would mean declaring a symbol on the PLC, and ADS writes values, it does not create variables.
The write goes through the same path as a restore — the same leaf addressing, the same sum commands, the same report — with one difference: it is told that the values it was given are the whole of what was asked for, so the thousands of variables it did not touch are not reported as missing. What it writes is exactly what was picked, and nothing else on the PLC is read into the bargain.
Everything the backup and restore tabs do can be asked for without opening the window, so a backup can be a scheduled task, a step in a deployment, or something the PLC itself starts before an update.
TwinCatAdsTool backup <netid> <port> <file>
TwinCatAdsTool restore <netid> <port> <file>
TwinCatAdsTool compare <netid> <port> <file>
TwinCatAdsTool --help
It is the same executable and the same engine — there is no second implementation to drift out of step with the one the window uses.
TwinCatAdsTool.exe backup 5.24.108.31.1.1 851 D:\backups\plant.json
The exit code carries the outcome, because a script has no other way of finding out how the run went. The distinctions are the ones a script would treat differently:
0 |
done, and every variable was processed |
1 |
the command line could not be understood |
2 |
the PLC could not be reached — worth retrying |
3 |
the run finished, but variables failed or were skipped — not worth treating as a backup |
4 |
something unexpected went wrong |
5 |
compare only: the PLC and the file differ |
3 is the one that matters. A backup that could not read everything still writes what it got, and a
script that only checks whether the file exists would keep it as though it were complete. That is the
same class of mistake as a restore reporting success without having written anything, which is what
this fork exists to fix.
compare reads the PLC and compares it against a file leaf by leaf, not as text: key order,
formatting and whitespace are properties of the file rather than of the plant, and a textual diff
reports them as changes. It names each difference by its path — MAIN.fbAxis.Position, GVL.arr[3]
— and says which side each value is on. This is what makes a restore verifiable from a script:
restore, then compare, and let the exit code decide.
An AMS net id is checked before anything connects. A mistyped one would otherwise come back much later as a timeout, which reads like a network problem rather than a typing one.
start /wait /b is what makes errorlevel meaningful: the application is a Windows program rather
than a console one, so cmd would otherwise carry on to the next line while it is still running.
A nightly backup, keeping the date in the name, as a scheduled task:
for /f "tokens=2 delims==" %%d in ('wmic os get localdatetime /value') do set t=%%d
start /wait /b TwinCatAdsTool.exe backup 5.24.108.31.1.1 851 D:\backups\plant_%t:~0,8%.json
if errorlevel 3 echo INCOMPLETE BACKUP >> D:\backups\alarm.txtif errorlevel 3 is true for 3 and anything above it, which is what you want: the backup file was
written but it does not hold everything.
Verifying a restore, which until now had to be done by hand:
start /wait /b TwinCatAdsTool.exe restore 5.24.108.31.1.1 851 D:\backups\plant.json
start /wait /b TwinCatAdsTool.exe compare 5.24.108.31.1.1 851 D:\backups\plant.json
if errorlevel 1 echo THE PLC DOES NOT MATCH THE FILEThe second line is the one worth having. A restore that reports success is not proof that the plant holds what the file says, which is the defect this fork was written to fix; reading the plant back and comparing it is.
In PowerShell the equivalent of start /wait /b is Start-Process -Wait. Neither is needed for a
scheduled task or for NT_StartProcess called from the PLC, which do not read an exit code from a
shell.
A restore writes to the PLC without asking. That is what a command line is for, and it is worth saying out loud.
Beckhoff's Symbol Explorer, part of TF8040 TwinCAT 3 Building Automation, does much of this and more — comparing two live controllers against each other, regular expressions over the symbol list, search and replace, integration with the XAE Shell. If you have TF8040, you already have it. Picking differences out of a comparison and writing them back, which it has had for years, is the feature this tool copied most directly.
--SnapShotFromPlc, --SyncPlcToSnapShot and --SyncSnapShotToPlc are accepted here as spellings of
backup, backup and restore, so a script written against it can be pointed at this without being
rewritten.
.NET 5 went out of support in May 2022. All four projects now target .NET 8, and
RuntimeIdentifier moves from the deprecated win10-x64 to win-x64.
The GUI keeps the 10.0.19041 suffix on purpose: System.Reactive only exposes
ObserveOnDispatcher for WPF under that target.
Directory.Build.props turns on EnableWindowsTargeting for non-Windows hosts, so the WPF
projects build and publish from macOS and Linux — running them, of course, still needs Windows.
dotnet test in CI now runs something. There was no .NET test project in the solution before, so
that step tested nothing; there are now 78 tests covering the backup and restore engine.
On a PC with TwinCAT 4026 the tool could not connect at all, failing with
AdsException: Cannot register Port '0' against any target, including the PC itself. The 5.x
library reaches the local router only over TCP loopback on 127.0.0.1:48898, and 4026 changed
that layer. The same executable worked on a 4024 machine, which is what showed the code was not at
fault.
| ADS library | TwinCAT supported |
|---|---|
| 5.x | up to 4024, no support for the 4026 router |
| 6.x | requires >= 4024.10 |
| 7.x | 2.11 and later — covers 4024 and 4026 with one binary |
7.x is therefore the only version that satisfies the requirement, and is what this fork uses.
It took three compilation changes (DT.Date → DT.Value, the same for DATE, a short cast in
DeviceFinder). The change the compiler does not report is that the PlcOpen types now expose
DateTime instead of DateTimeOffset. The offset was an artefact of the wrapper — a PLC DT
carries no time zone — so a backup now records the timestamp as the PLC holds it. The round trip
was verified by running the library: same ticks in and out, and backups produced with 5.x are still
restorable.
The views were still the 2019 ones: flat tabs, Margin="16" written by hand on every element,
black text boxes with green text, no dark theme. MahApps and Material Design were doing two
overlapping jobs and neither looked like the operating system the application runs on.
The UI is now built on WPF-UI 4.3.0 (Fluent / WinUI 3): Mica backdrop, NavigationView instead
of the tab control, Windows 11 control templates.
Both a dark and a light theme, dark being the default. The Theme button at the foot of the
navigation panel switches between them with the window open — no restart — and the choice is kept in
%LOCALAPPDATA%\TwinCatAdsTool, not next to the executable, which on a cabinet PC is usually not
writable. No colour is a literal in the views: they are all theme lookups, which is what lets the
switch propagate. The plot is the one thing that does not go through the WPF resource system, so it
repaints itself when the theme changes.
Details, and the parts that are deliberately unchanged, in docs/UI-FLUENT.md.
Verified on a real PLC: connection on TwinCAT 4026 with ADS 7; backup of 71 variables in 0.1 s; restore of 10,978 leaves in 2.5 s with every leaf correct; nested branches, arrays of structures, arrays inside structures inside arrays, strings, REALs, BOOLs, TIME, LTIME and DT.
Verified by test: 187 unit tests, run on every build. Thirty-five cover the scope — the recording buffer and the sample that has to be carried in from before the window, the table a capture is exported as, the trigger conditions — forty-one cover the command line, and thirty-three cover comparing and merging: the leaf by leaf comparison, cutting a backup down to chosen values, and the whole way a picked difference travels from the row it is shown on to the symbol it is written to.
Verified on a real PLC, by hand: backup and restore from the command line against a running
controller. In the compare tab, against a live PLC: picking single differences and writing them,
picking every difference at once, a value inside an array, and moving between differences with the
filter both on and off. Nobody has yet run the command line from a scheduled task, and its compare
verb has not been exercised on a plant.
On a second plant, an ARRAY[0..30, 0..30] OF DUT_Cass — 92,256 leaves in a single variable —
restored with 92,256 correct and none out of place, which settles the multidimensional case: the
backup flattens such an array into a single list, and the flattening and the write path agree on the
order. Test 2 in docs/RESTORE-VERIFICATION.md.
Not verified: the accuracy of scope timings below roughly ten milliseconds — the samples are stamped when they reach the pc, not when the PLC changed them, so short events are ordered correctly but not measured. Of the multidimensional case, one shape only: square, rank 2.
Latest release — two builds of the same application:
| Asset | When to use it |
|---|---|
TwinCatAdsTool-<version>-win-x64-selfcontained.zip |
Nothing to install on the target machine. The obvious choice for a cabinet PC. |
TwinCatAdsTool-<version>-win-x64.zip |
Much smaller, but needs the .NET 8 Desktop Runtime already present. |
Unzip and run the executable; no installation. The manual travels with it as a PDF, and the Help
item at the foot of the navigation panel opens it. log.config beside it is optional — without it the
application logs to logs\ on its own, and the file is only there for anyone who wants to change
the logging.
Versions follow the tag the release was built from, and the tag sets the version of every assembly, so what the release is called, what the binary reports and what the window shows are the same number.
Open an issue. Bugs and requests for something new both belong there; the forms ask for the few things a report cannot be acted on without - TwinCAT version, controller, whether the program was running, and for a restore problem the shape of the variable involved.
Issues about the original tool rather than about the changes in this fork belong upstream.
The most useful report anyone could send is a restore onto a plant whose persistent variables have
a shape none of the verifications covers — a rank 3 array, or a non square ARRAY[0..n,0..m].
TwinCAT ADS must be installed, and a route to the PLC configured. See Beckhoff Information System.
Both TwinCAT 4024 and 4026 are supported by the same build.
| Document | Contents |
|---|---|
| docs/MANUAL.md | The manual. How to use the tool, tab by tab, with the traps named. Also as a PDF |
| docs/PERSISTENT-BACKUP-RESTORE.md | The backup and restore engine: defects, fixes, design |
| docs/RESTORE-VERIFICATION.md | Field verification procedure and results |
| docs/UPGRADE-NET8.md | .NET 8 and package upgrade, ADS 7 migration |
| docs/UI-FLUENT.md | The Fluent interface |
Original work by Federico Barresi, MIT licensed, sponsored by evopro AG. This fork keeps the same licence.


