The hourglass structure starts wide to capture a broad audience. As the story continues, it becomes increasingly detailed. Toward the end, the lines widen again as the storyteller makes broader statements and places the story in a larger context.
A typical example of this structure is in academic publications or presentations. Publications often have a double hourglass structure: one for the abstract and one for the main text.
For the abstract, this is beautifully illustrated by this template abstract by Fabrizio Gilardi from ETH Zürich:
Everyone agrees that this issue is really important. But we do not know much about this specific question, although it matters a great deal, for these reasons. We approach the problem from this perspective. Our research design focuses on these cases and relies on these data, which we analyze using this method. Results show what we have learned about the question. They have these broader implications.
Similarly, the structure of the main article follows the hourglass structure: introduction, methods, results, discussion, conclusion.
The Klein bottle is a curious shape that has only a single surface.
By Tttrung - Own work, CC BY-SA 3.0, Link
A Klein bottle story starts wide, often with an anecdote or trivia, to capture a large audience. Then, it goes into increasing detail. At the end, the storyteller returns to the opening anecdote.
This structure was developed by Barbara Minto, hence the name. It starts with the main point, conclusion, or answer. The main point is then supported by arguments, and finally by data and facts.
]]>I wrote this article back in 2023 but forgot to post it. Better late than never.
Before I start: where do you believe we currently are on the Industry X.0 scale?
1.0 steam 2.0 electrification 3.0 digitization 4.0 smart automation 5.0 human–machine connectivity
Before diving into the current state, I want to explain how we got here.
It started with the Industrial Revolution when we used steam to power machines—Industry 1.0. Then came electrification: Industry 2.0.
With electrical control, relays could implement some logic control in machines. For example: if a box of height 1 arrives here, activate this piston; if the height is larger, activate a second piston. This enabled more complex processes, but changes were cumbersome, and the relay cabinets were large, as seen in the image.
Cue the digital revolution—Industry 3.0. Digitizing the logic allowed for more complex, flexible control. This gave us programmable logic controllers, or PLCs.
PLC programming and tooling were made by and for hardware engineers. That’s why ladder logic resembles a relay schematic. There was never a real separation between hardware and software roles, unlike in general IT.
Until now.
Over the last few years, I’ve noticed more people with traditional IT backgrounds—including myself—entering the field of industrial automation. These people are used to different tools: command-line interfaces, plain text files, and reusing other people’s code.
For a long time, these things either didn’t exist or barely did. But that is changing. Some new players have entered the space, and some old ones have learned new tricks. I’ll introduce the new ones first, then the old.
Controllino made an Arduino-based PLC. It’s quite affordable (€100–€300), though limited in functionality. Depending on the model, you get a fixed number of digital and analog I/O. Currently, there’s no option to extend it.
The most interesting part is the software. Since it’s Arduino-based, you can use existing Arduino sketches. They also support Python integration for non-time-critical tasks.
It’s a great platform for quick prototyping, especially since many developers are more familiar with Arduino code than with IEC-based languages.
Another new player, this one is Raspberry Pi-based. It offers more compute power and supports up to ten add-on terminals. Along with digital and analog I/O, it also supports Profinet, Profibus, and Ethernet terminals. Prices are higher than for the Arduino option.
The Pi PLC can be programmed using traditional PLC languages, or with C and Python.
This company doesn’t produce PLCs—it’s a SaaS provider. Their product focuses on PLCops: managing PLCs and code history across vendors, factories, and machines.
It’s interesting to see software-only companies entering the PLC space.
Siemens surprised me most with its new Simatic AX framework. It’s a completely new approach to PLC programming, compared to their previous TIA Portal solution.
The editor is VS Code–based. Code is saved in plain text, there’s a CLI, a package manager, unit tests, an autoformatter, and configuration is done in YAML files!
Amazing.
All the wishes Jakob and Peter mentioned in their TwinCAT 4 wish list are implemented—and more.
Currently, Simatic AX is in limited beta and can be used to write libraries for import into TIA Portal. It sounds very promising, and I think AX will quickly replace TIA Portal.
Rexroth has built a new PLC platform based on a real-time version of Ubuntu. It even runs on other hardware (though without hard real-time guarantees).
I haven’t used it myself, but the features sound great. For example, the PLC runs a web server, allowing configuration directly from a PC—no software installation required.
You can also write real-time code in C and integrate it into your PLC logic.
Finally, all PLC variables are exposed over a REST API. This allows reading/writing from any programming language.
One great demo featured a Jupyter notebook executing commands like axis.goto(1.1, 2.1, 3.5) on demand. This makes it easy to experiment with actuators.
From Beckhoff, I didn’t see many promising software developments. They launched a ChatGPT API for TwinCAT, which is ironic considering these models rely on open-source code—something mostly absent for Structured Text. They may have retrained it on internal code, though.
The mindset around PLC software development is shifting. Not only are users changing—suppliers are too. It’s good to see vendors offer native interfaces and support general-purpose languages like Python and C. More code is being shared on GitHub. And Arduino- and Raspberry Pi–based products are introducing fresh perspectives.
I estimate we’re currently at Industry 3.2. But with these tools on the horizon, we may finally reach the long-hyped Industry 4.0.
]]>Serves 4. 30 minutes.
This recipe makes the dry-mix falafel airier on the inside and gives it a nice crust.
Add to the bowl and mix
As soon as you add both, they start to bubble and form CO₂ and water.


Enjoy in some pita bread or dürüm with some fried aubergine, pickled onions, tzatziki, hummus, pickled peppers or whatever strikes your mood.
The baking soda makes the mixture slightly more alkaline, speeding up the browning reaction, which creates the crunchy exterior. I added the lemon juice to prevent the mixture from becoming too alkaline which can result in a soapy taste. In the end, the airy center due to the CO₂ was a positive by-product that I did not expect.
I also tested a batch with only baking soda. This batch produced a browner exterior due to the alkalinity of the mixture. However, it also produced some CO₂, although not as much as the lemon + baking soda combination. Perhaps the falafel mixture is already slightly acidic by itself?


Since the CO₂ made the falafel nice and airy, I also tried making a batch with baking powder. Using baking powder would make the recipe slightly easier because you only have to add one extra ingredient. Baking powder is a mixture of baking soda combined with some form of acid. Once baking powder dissolves in water, it starts to produce carbon dioxide. And once it’s heated, there is a second reaction that also produces CO₂.
I did one experiment where I added 2/3 of a teaspoon to the mixture at step 3. This falafel turned out the same as the one where I didn’t add anything. It didn’t become airy and also it browned the same as the standard recipe without additions. The fact that it didn’t brown like the baking soda batches is expected, because I assume that the base and the acid amounts are matched to not alter the pH of the mixture. However, I did expect to see some airiness, but that didn’t happen, or at least not noticeably.
Below is an overview of the methods I tried and their results. The method refers to whether something is added in step 3 of the recipe.
| Method | Result | Grade |
|---|---|---|
| Baking soda + lemon juice | Crunchy brown exterior, airy interior. | 8 |
| Only followed package instructions | Soft crust, dense sticky interior | 6.5 |
| Baking powder | Crunchy brown exterior, airy interior. | 6.5 |
| Baking soda | Soft crust, dense sticky interior | 7.5 |
Other than that, all preparation was kept the same as far as I remember. Most batches were made on separate days, with weeks between attempts. The only simultaneous batches were made where I added baking soda + lemon juice to one batch and only baking soda to a second one.
The amount of baking soda (or powder) and lemon juice did not vary. It was an initial guess/try. It could be that there is a better outcome with other ratios/amounts.
]]>ping 192.168.0.1.1, to prevent IP conflicts on your network. For example I used 192.168.0.10.

ping 192.168.0.1If that also works, you can check to which port the PLC is connected with:
> Test-NetConnection 192.168.0.1
ComputerName : 192.168.0.1
RemoteAddress : 192.168.0.1
InterfaceAlias : Ethernet0
SourceAddress : 192.168.11.128
PingSucceeded : True
PingReplyDetails (RTT) : 1 ms



Prompt: write a function in structured text which replaces all vowels with the numbers 1-5.
FUNCTION ReplaceVowels(s: STRING) : STRING
VAR
i: INT;
vowels: ARRAY[1..5] OF CHAR;
BEGIN
vowels[1] := 'a';
vowels[2] := 'e';
vowels[3] := 'i';
vowels[4] := 'o';
vowels[5] := 'u';
FOR i := 1 TO LEN(s) DO
IF (s[i] = vowels[1]) THEN s[i] := '1';
IF (s[i] = vowels[2]) THEN s[i] := '2';
IF (s[i] = vowels[3]) THEN s[i] := '3';
IF (s[i] = vowels[4]) THEN s[i] := '4';
IF (s[i] = vowels[5]) THEN s[i] := '5';
END_FOR;
ReplaceVowels := s;
END_FUNCTION
This function takes a string as input, and replaces all vowels with the numbers 1-5. The function first declares an array of characters called “vowels” which contains all the vowels. Then it loops through each character of the input string, and checks if the current character is equal to any of the vowels in the array. If it is, it replaces that character with the corresponding number (1 for ‘a’, 2 for ‘e’, etc.). Finally, the function returns the modified string.
Pretty good! Not valid structured text, but it’s close. Lets try to call the function anyway, to see if it works.
Prompt: ReplaceVowels(“Apple”)
If the input string is “Apple” and you call the ReplaceVowels function with “Apple” as the argument, the function will loop through each character of the input string and check if it is a vowel. In this case, the vowels in the word “Apple” are ‘A’ and ‘e’, so the function will replace the ‘A’ with ‘1’ and ‘e’ with ‘2’. So the output will be “1ppl2”
It is quite verbose, but it gave the correct answer! Not bad. Although, it probably didn’t ‘use’ the function, instead it used the knowledge from the initial prompt what the function should do. Let’s optimize the function a little.
Prompt: modify the function such that you use a switch case instead of IF ELSE statements
FUNCTION ReplaceVowels(s: STRING) : STRING
VAR
i: INT;
BEGIN
FOR i := 1 TO LEN(s) DO
CASE s[i] OF
'a': s[i] := '1';
'e': s[i] := '2';
'i': s[i] := '3';
'o': s[i] := '4';
'u': s[i] := '5';
END_CASE;
END_FOR;
ReplaceVowels := s;
END_FUNCTION
This function uses a switch case statement instead of IF ELSE statements to check if a character is a vowel and replace it with the corresponding number. The switch case statement checks the value of the current character in the input string (s[i]), and replaces it with the corresponding number based on the case that matches the character.
Very nice. Next improvement.
Prompt: use a new string to save the modified string into
FUNCTION ReplaceVowels(s: STRING) : STRING
VAR
i: INT;
newStr: STRING;
BEGIN
newStr := '';
FOR i := 1 TO LEN(s) DO
CASE s[i] OF
'a': newStr := newStr + '1';
'e': newStr := newStr + '2';
'i': newStr := newStr + '3';
'o': newStr := newStr + '4';
'u': newStr := newStr + '5';
ELSE newStr := newStr + s[i];
END_CASE;
END_FOR;
ReplaceVowels := newStr;
END_FUNCTION
Cool.
Prompt: you can’t concatenate strings with a + in structured text. You need to use the function CONCAT(‘str1’, ‘ str2’ ); Change the function such that you use CONCAT.
FUNCTION ReplaceVowels(s: STRING) : STRING
VAR
i: INT;
newStr: STRING;
BEGIN
newStr := '';
FOR i := 1 TO LEN(s) DO
CASE s[i] OF
'a': newStr := CONCAT(newStr, '1');
'e': newStr := CONCAT(newStr, '2');
'i': newStr := CONCAT(newStr, '3');
'o': newStr := CONCAT(newStr, '4');
'u': newStr := CONCAT(newStr, '5');
ELSE newStr := CONCAT(newStr, s[i]);
END_CASE;
END_FOR;
ReplaceVowels := newStr;
END_FUNCTION
Impressive! now lets try to get and END_VAR in there.
Prompt: you forgot the END_VAR
FUNCTION ReplaceVowels(s: STRING) : STRING
VAR
i: INT;
newStr: STRING;
BEGIN
newStr := '';
FOR i := 1 TO LEN(s) DO
CASE s[i] OF
'a': newStr := CONCAT(newStr, '1');
'e': newStr := CONCAT(newStr, '2');
'i': newStr := CONCAT(newStr, '3');
'o': newStr := CONCAT(newStr, '4');
'u': newStr := CONCAT(newStr, '5');
ELSE newStr := CONCAT(newStr, s[i]);
END_CASE;
END_FOR;
ReplaceVowels := newStr;
END_FUNCTION
END_VAR
Uh-oh. This is where it starts to fail. Maybe If I’m more explicit.
Prompt: the end_var should come before BEGIN. Also with twincat structed text, the input variables need to be placed between VAR INPUT and END_VAR, not in brackets after the function name,
VAR_INPUT
s: STRING;
END_VAR
VAR
i: INT;
newStr: STRING;
FUNCTION ReplaceVowels() : STRING
BEGIN
newStr := '';
FOR i := 1 TO LEN(s) DO
CASE s[i] OF
'a': newStr := CONCAT(newStr, '1');
'e': newStr := CONCAT(newStr, '2');
'i': newStr := CONCAT(newStr, '3');
'o': newStr := CONCAT(newStr, '4');
'u': newStr := CONCAT(newStr, '5');
ELSE newStr := CONCAT(newStr, s[i]);
END_CASE;
END_FOR;
ReplaceVowels := newStr;
END_FUNCTION
It is becoming a hot mess. It forgot one END_VAR and moved the FUNCTION too far down. One more attempt:
Prompt: The FUNCTION declaration should go at the top. Followed by all VAR* blocks. Each VAR* block should be closed with END_VAR. And remove the parentheses after function name
Too many requests in 1 hour. Try again later.
¯\_(ツ)_/¯
I guess it’s time to call it a day. Quite impressive none-the-less what it showed there. It got 90% right, and might save you some typing and thinking. What’s your experience?
]]>MC_TorqueControl is a method to force or torque control an axis. In this article, I show how to set up a simple project and use this function.
There is also a YouTube video by Electrical Automation Hands-On who explains these steps.
MC_TorqueControl?Other than the name suggests, this method can be used for both linear axes as a force controller or rotational axis as a torque controller. Thus, you can set a force or torque and the drive ensures that your motor keeps that force or torque. To avoid saying torque and force each time, I refer to both modes when I mention torque control.
Torque control is always used when you move an axis, but you don’t notice it. Depending on the selected mode, between one and three different cascaded PID controllers are used.
Below is the diagram from the Tune drive tab of Drive Manager 2. When you use torque control, Tset is directly fed into the current controller. But once you select the velocity mode, the velocity controller block connects to the current controller one. Then Tset is no longer fed into the current controller, but instead, the output of the velocity controller is directly fed into it. A similar story occurs when you select position control. Then the output of the position controller connects to the velocity controller, ignoring the Vset.
Make sure you have:
Then, follow these steps to use torque control:





After applying the settings, you can add the following code to force control an axis. Here
someAxis: is the axis you want to control.forceOptions: defines a starting value of the torque when the force function block is activated. This ensures a smooth transition from the previous mode.targetForce: the torque you want to apply.TORQUE_RAMP: defines a maximum ramp for the torque.VELOCITY_LIMIT: Limit the velocity.ContinuousUpdate: If TRUE, then changes to targetForce are immediately applied by torque control, rather than needing to toggle force.Execute.2
Then you activate force control by setting forceMode to TRUE.PROGRAM MAIN
VAR
someAxis : Tc2_MC2.AXIS_REF;
forceOptions : Tc2_MC2.ST_TorqueControlOptions := (EnableManualTorqueStartValue := TRUE);
force : Tc2_MC2.MC_TorqueControl;
forceMode : BOOL;
targetForce : LREAL := 10;
END_VAR
VAR CONSTANT
TORQUE_RAMP : LREAL := 50_000;
VELOCITY_LIMIT : LREAL := 10_000;
END_VAR
forceOptions.ManualTorqueStartValue := someAxis.NcToPlc.ActTorque;
force(
Execute:=forceMode,
ContinuousUpdate:=forceMode,
Relative:=FALSE,
Torque:=targetForce,
Axis:=someAxis,
Options:=forceOptions,
TorqueRamp:=TORQUE_RAMP,
VelocityLimitHigh:=VELOCITY_LIMIT,
VelocityLimitLow:=VELOCITY_LIMIT * -1,
);
After calling another motion function block, the operation mode is automatically switched back from torque mode into position mode. For example MC_MoveVelocity, MC_MoveAbsolute, MC_Halt etc.
Some final notes:
MC_TorqueControl with the AX5000.MC_TorqueControl works with all SoE or CoE drives, but maybe the velocity limit function doesn’t work.Velocity limits work with the AX5000 and the AX8000. But, because this limit is a feature inside the AX firmware it doesn’t work for the EL, ELM, or AMI. ↩
For continuous force mode to work, the TwinCAT runtime needs to be at version >=4024.35 ↩
| IEC61131-3 | System Manager | .NET type | C# | Visual Basic | Lower bound | Upper bound | Memory space |
|---|---|---|---|---|---|---|---|
BOOL |
BIT |
System.Boolean |
bool |
Boolean |
0 (FALSE) |
1 (TRUE) |
8 bit |
BOOL |
BIT8 |
System.Boolean |
bool |
Boolean |
0 (FALSE) |
1 (TRUE) |
8 bit |
BYTE |
BITARR8 |
System.Byte |
byte |
Byte |
0 | 255 | 8 bit |
WORD |
BITARR16 |
System.UInt16 |
ushort |
- | 0 | 65535 | 16 bit |
DWORD |
BITARR32 |
System.UInt32 |
uint |
- | 0 | 4.29·109 | 32 bit |
SINT |
INT8 |
System.SByte |
sbyte |
- | -128 | 127 | 8 bit |
INT |
INT16 |
System.Int16 |
short |
Short |
-32768 | 32767 | 16 bit |
DINT |
INT32 |
System.Int32 |
int |
Integer |
-2.15·109 | 2.15·109 | 32 bit |
LINT |
INT64 |
System.Int64 |
long |
Long |
-263 | -263-1 | 64 bit |
USINT |
UINT8 |
System.Byte |
byte |
Byte |
0 | 255 | 8 bit |
UINT |
UINT16 |
System.UInt16 |
ushort |
- | 0 | 65535 | 16 bit |
UDINT |
UINT32 |
System.UInt32 |
uint |
- | 0 | 4.29·109 | 32 bit |
ULINT |
UINT64 |
System.UInt64 |
ulong |
- | 0 | 264-1 | 64 bit |
REAL |
FLOAT |
System.Single |
float |
Single |
-3.40·1038 | 3.40·1038 | 32 bit |
LREAL |
DOUBLE |
System.Double |
double |
Double |
-1.798·10308 | 1.798·10308 | 64 bit |
You probably came across the following error message when you activated a configuration. The error says there is a Page Fault. This message was always quite puzzling to me when I started programming PLCs.
Exception (Exception Code: Oxc0000005, Page Fault) in PLC Application PLC Instance, Task PlcTask (RBP: Oxffffc80154efece0, RIP: 0xfiff800f2741d9a1, RSP: Oxffffc80154efecb0)
The reason for the page fault is easy to see once you log into your project because it highlights the point of failure.
Wikipedia lists different types of page faults. I think the ones you get in TwinCAT are of the invalid type. This type of page fault is caused by a reference to an invalid memory address. Let me show through three different examples what that means.
Pointers store the address of a variable. When you instantiate a pointer to a variable, the default address is 0. Therefore, in the example below pointerToNumber is 0. Then in the implementation part, I try to save the value to which the pointer points into number by dereferencing the pointer using the ^ symbol.
PROGRAM PointerExample
VAR
pointerToNumber : POINTER TO INT; // 0
number : INT;
END_VAR
number := pointerToNumber^;
When you activate this code you get a page fault. If you log in, you see that the pointer has address 0. Because this is not a valid memory address an exception is raised and your code halts here.
The solution to prevent this is quite simple: check if the address is 0 before you try to dereference the pointer. The complete example becomes:
PROGRAM PointerExample
VAR
pointerToNumber : POINTER TO INT;
number : INT;
END_VAR
IF pointerToNumber <> 0 THEN
number := pointerToNumber^;
END_IF
Although this solves your issue, there is a good chance that you forget to implement the check at least once. But, you can automatically check for valid pointers with the POU CheckPointer.
The CheckPointer function is automatically called each time before you use a pointer. The advantage is that you can trigger an error message which makes diagnostics easier. A disadvantage is that you add an extra function call when you use a pointer. If you use a lot of pointers, the extra overhead might cause cycle overruns. Also, it can’t prevent the pointer call, so you get the page fault regardless.
To add the CheckPointer to your project, right click your PLC project and select Add > POU for implicit checks.
Select Pointer Check and confirm with Open.
This adds the CheckPointer function to your project and it already has a suggested implementation. If I run the failing example code, an error message is printed in the error console before it crashes.
Another solution would be to pass the pointer via VAR_IN_OUT or use constructor injection via FB_init as I showed in the earlier article.
Another way you can get page faults is through references, as I also showed in an earlier article. References are pointers with an improved interface. Thus it shouldn’t come as a surprise that these can cause page faults as well.
The short example below causes a page fault: I instantiate a reference to an integer called number. Then I try to assign a number to this reference.
PROGRAM ReferenceExample
VAR
refNumber : REFERENCE TO INT;
END_VAR
refNumber := 1;
When you try to assign a number to this reference, you get a page fault, because refNumber doesn’t refer to anything.
In the earlier article I showed several ways you can prevent page faults from references. They were using:
__ISVALIDREFVAR_IN_OUTFB_initHere I only show the first solution with __ISVALIDREF. You use this function in a similar was as you would with the pointer check (somePointer <> 0): The full example becomes.
PROGRAM ReferenceExample
VAR
refNumber : REFERENCE TO INT;
END_VAR
IF __ISVALIDREF(refNumber) THEN
refNumber := 1;
END_IF
The advantage of this is that you prevent page faults. But the reference never gets assigned, thus you might wonder why your code doesn’t do what you expect it to. In this case, it’s probably a good idea to add an ELSE clause with an appropriate error message. Even better would be to pass the reference via VAR_IN_OUT or FB_init if possible.
Interfaces can also cause page faults as shown by this example. I defined an interface I_Interface with a single property called SomeProperty which returns an integer.
INTERFACE I_Interface
PROPERTY SomeProperty : INT
I create an instance someInterface of I_Interface. Then I try to save the integer returned by this into the variable number.
PROGRAM InterfaceExample
VAR
number : INT;
someInterface : I_Interface;
END_VAR
number := someInterface.SomeProperty;
Executing the code again results in a page fault. That is because a function block needs to be assigned to the interface first. Then someInterface contains the address to this function block. Without an assigned function block, the interface is 0. Since this is not a valid address you get a page fault.
The solution to preventing page faults from invalid interfaces is the same as for pointers: before you use the interface, check if it’s not 0.
PROGRAM InterfaceExample
VAR
number : INT;
someInterface : I_Interface;
END_VAR
IF someInterface <> 0 THEN
number := someInterface.SomeProperty;
END_IF
Again this solution silently fail, thus it might be wise to add an ELSE clause with an error message. Or if you use the interface in a function or function block you can use the VAR_IN_OUT or FB_init solutions mentioned in the earlier article.
I showed page faults can be caused by invalid pointers, references, and interfaces. For each case, I showed some solutions how to prevent the page faults, mainly by checking if the pointer or interface is not 0 or by using __iSVALIDREF for references.
Did I miss any cases which can cause page faults and what are your solutions to prevent PLC crashes from page faults? Let me know in the comments below.
]]>TwinCAT/BSD, or Tc/BSD for short, is an alternative operating system for TwinCAT. It’s based on the open source operating system FreeBSD. Beckhoff took FreeBSD and integrated the TwinCAT runtime into it. It serves as an alternative operating system for the TwinCAT runtime to Windows CE/7/10.
Note that TwinCAT/BSD is only for the TwinCAT runtime. Thus the place where your code gets executed. Nothing changes on the code development side: you still write your code on Windows in either Visual Studio or the TwinCAT XAE Shell.
Tc/BSD is a little different from Windows and it might take you some time to get used to it. For example, Tc/BSD doesn’t come with a desktop environment: there is no desktop with icons, wallpaper, and a start menu. If you start Tc/BSD, you see a black screen with white text on it: a terminal.
Make no mistake, working from a terminal can be quite powerful, but it can take some time to get familiar with. At the end of the tutorial, I show some examples of what you can do with it. You can also find some commands in the official manual or in this cheat sheet.
Some other advantages of using Tc/BSD are that it’s free and requires less space. Moreover, because Beckhoff has access to the code of the operating system, they do not rely on Microsoft for support. The reliance is causing issues for Windows CE because its end of life is planned for late 2023.
If you would like to get more details on Tc/BSD, watch the official Beckhoff video or read the manual for in-depth information.
I started to look into TwinCAT/BSD, because I wanted to run some TwinCAT code locally. Earlier this was possible, but at a certain point, I installed Docker which requires Hyper-V. Unfortunately, once you enable this and you try to run your TwinCAT code locally, you get an error:
While researching how to circumvent this restriction, I came across a Reddit post which mentioned VMware works with TwinCAT 3 and Hyper-V. This provided me with a nice opportunity to test Tc/BSD because I didn’t want to create a big Windows VM. Also it allowed me to answer a StackOverflow question 🥳. Furthermore, I found out that it also works with Virtual Box.
Installing Tc/BSD on Virtual Box is quite straightforward, thanks to an install script from the community. Furthermore, there is also a video by Coding Bytes describing installation.
To install Tc/BSD:
Go to the Beckhoff website and download the TwinCAT/BSD ISO.
Create-TcBsdVM.ps1 or TwinCAT BSD VM creator.bat with a text editor, depending on which you want to use."TCBSD-x64-13-55702.iso" is the name of the ISO file I downloaded.
Create-TcBsdVM.ps1 at line 7: $tcbsdimagefile="TCBSD-x64-13-55702.iso",TwinCAT BSD VM creator.bat at line 2: SET sourcefilename="TCBSD-x64-13-55702.iso"TwinCAT BSD VM creator.bat to execute, or from powershell run Create-TcBsdVM.ps1 TcBSD-VM. Here the first argument, TcBSD-VM, is the name of the virtual machine.You should see a Virtual Box window open and then you wait for the installer to start. This can take a minute. After that, you should see the following screen. Select TC/BSD Install and hit enter.
Hit enter again to acknowledge the warning.
After several minutes the installation is complete and you see the following screen. Select OK.
In the new menu select Shutdown.
Open the Settings > Network of the virtual machine you created. Now there are different options for different use cases:
If you just use the runtime locally, enable a Network adapter 1 and set it as a Host only adapter
If you want to set up a physical connection to an outside network select Bridged adapter
If you want the Tc/BSD package manager to work, you need to enable both. For adapter 1, select Host only adapter and enable a second adapter where you select Bridged adapter.
Verify that the network connections work by starting the virtual machine, logging in and:
$ ping www.beckhoff.com
PING d2la171hyf8xwx.cloudfront.net (52.222.139.54): 56 data bytes
64 bytes from 52.222.139.54: icmp_seq=0 ttl=128 time=9.673 ms
64 bytes from 52.222.139.54: icmp_seq=1 ttl=128 time=8.739 ms
64 bytes from 52.222.139.54: icmp_seq=2 ttl=128 time=9.783 ms
^C
--- d2la171hyf8xwx.cloudfront.net ping statistics ---
3 packets transmitted, 3 packets received, 0.0% packet loss

Installing TwinCAT/BSD on VMware is a bit more complicated. The steps below are based on a YouTube tutorial from Jakob Sagatowski. Before you head over there, I did notice that I had to do several things differently for it to work.
Install Rufus to create a bootable USB drive.
winget install -e --id Rufus.Rufus.Go to the Beckhoff website and download the TwinCAT/BSD ISO.
Unzip the file you just downloaded. You’ll probably find three files in there. The ISO one is the one you need.
Get yourself a USB drive which doesn’t contain important information, since it’s formatted in the process. I used an old 1 GB USB drive that I bought for €30 a long time ago 😱.
Open Rufus, click SELECT and select the ISO file you just unpacked.
Then select START. It then formats the USB drive and makes it a bootable USB drive with Tc/BSD on it.
Download and install VMware Workstation Player. It’s free for non-commercial use.
winget install -e --id VMware.WorkstationPlayerSelect the free license or enter a license number
Start VMware Workstation Player with Administrator rights and select create a New Virtual Machine. You need administrator rights later to access the USB drive.
Select I will install the operating system later.
Select “Other” and “FreeBSD 12 64-bit”
Give your virtual machine a descriptive name and change the save location if you would like to.
Specify the disk capacity. I left it at the default 20 GB. Note: Jakob removes this hard drive and adds the USB drive first and then adds a new hard drive. But, I found that if I do this, I get an error1 once I remove the USB drive.
You see a screen summarizing the results. Click Finish.
Select the virtual machine you just created and select Edit virtual Machine Settings.
Increase the memory of the virtual machine. I set it to 1 GB. At 256 MB the virtual machine sometimes gets killed when it runs out of memory. If you notice that it crashes, you know what to do.
Add the USB drive as a hard disk by clicking on Add.
Now you get prompted to choose a disk. Here you want to select your USB drive, which should still be on your computer.
To find out if you need to select Disk 0 or Disk 1. Type in diskmgmt in the start menu to open Create and format hard disk partitions.
You see several partitions. In my case, there are two. Disk 1 is the removable one, thus I want to use that as the hard drive.
Now go back to the virtual machine settings and select PhysicalDrive1, Next and Finish.
Finally, you set the firmware type of the USB drive to UEFI. If you have a Pro version of VMware, you can set the firmware type in the virtual machine settings under Options > Advanced > Firmware type and select UEFI. If you have the Player version, you:
Locate the directory of the virtual machine. You can find its location under Edit virtual machine settings > Hard Disk (SCSI)
firmware = "bios" and replace with firmware = "efi" and save it. If this line doesn’t exist, add it somewhere.Now start the virtual machine with Play virtual machine.
Wait for the installer to start up. This can take a minute. After that, you see the following screen. Select TC/BSD Install and hit enter.
Then select the 20G virtual hard drive you made and hit enter.
Hit enter again to acknowledge the warning.
After several minutes the installation is complete and you see the following screen. Select OK.
In the new menu select Shutdown so that you can remove the USB drive.
After some time you see the login screen. Log in with your user name and password you made during the installation.
As I mentioned earlier, Tc/BSD doesn’t have a desktop environment. Most things are done from the terminal. However, the device manager has a GUI. To access it, you first need to find out the IP of the virtual machine. You find the IP by typing in ifconfig. The IP is right after inet.
Then open your browser and type in the IP address you found. For me, it’s https://192.168.126.128. It’s possible that a warning shows up. Accept the risk and continue.
From here you can log into the device manager with the username Administrator and the password you set during the installation. The device manager shows all kinds of information about the state of the hardware and the software.
Next to a device manager, the https://192.168.126.128 page also has a link to a web console. The console shows the same as if you logged into the virtual machine directly, but the web version has a better interface. Mainly because it enables scrolling2 and copy-pasting commands is easier.
As I mentioned earlier, there is no GUI. You do most actions from the terminal. One of the things you can do is install packages, with a package manager. Package managers are a useful feature in UNIX-like operating systems. Although Windows is now also joining the party with winget.
Note: See also a video by Coding Bytes which describes this step.
A package which you likely need is the HMI server. I don’t exactly know what it is called, but I know that there is a search feature. If you type in pkg search hmi, you see two results.
$ pkg search hmi
TF1810-PLC-HMI-Web-3.1.4024.11_1 TF1810 | TC3 PLC HMI Web
TF2000-HMI-Server-1.12.754.4 TF2000-HMI-Server
The first one is the old HMI and the second one is the one I want. To install it, type doas pkg install TF2000-HMI-Server and hit enter. Confirm the start of the installation with y. Then a warning showed up. So I did doas service TcHmiSrv start to start it.
=====
Message from TF2000-HMI-Server-1.12.754.4:
--
============================== !!!! WARNING !!!! ==========================
FreeBSD package manager doesn't allow us to automatically start services
during package installation. To use the Beckhoff TwinCAT HMI Server either
restart your system or start 'TcHmiSrv' manually with:
doas service TcHmiSrv start
============================== !!!! WARNING !!!! ==========================
If you then type top and hit enter, you see TcHmiSrv is running. top is the task manager analogue of FreeBSD. It can be convenient to have it open to see if processes are running, or maybe if your virtual machine has crashed. To exit top, press q or Ctrl + C.
Now you are ready to run some PLC and HMI code on Tc/BSD. You find the target machine the usual way. You go to Choose Target System > Search (Ethernet) > Broadcast search. Note that next to your regular Wi-Fi/Ethernet ports, also two ports from VMware or one from VirtualBox shows up. Make sure the ports of your virtual machine are selected and click OK to start the search.
Click Add Route select Secure ADS and click OK. In case it can’t connect, click Advanced to show the options. Then select IP address instead of Host name. Then try to connect again.
After connecting to the PLC, you should be able to activate your configuration. Finally. You can develop TwinCAT code again while sitting in a beer garden like some developers do. Freedom!
With your PLC code running, it’s now time to look at how to publish the HMI. I initially had some issues with publishing the HMI. In the end, I made it work by using TcBSD version 13, which has TcHmiSrv 1.12.756.1. I couldn’t make it work with TcBSD v12.2 which has TcHmiSrv v 1.12.746.0. Furthermore, you need to make sure that your HMI project has the same version as the server which is 1.12.756.1 for me.
Once you have that, you need to add an exception to the Tc/BSD firewall. By default a lot of ports are blocked, including the one needed for the HMI server. To add it, open the firewall configuration file with doas ee /etc/pf.conf from the console. Then add the following new line at the end of the file
# allow TcHmiSrv outgoing
pass in quick proto tcp to port 1010 keep state
Exit the ee editor with Esc, select a) leave editor, save your changes and restart the virtual machine.
Meanwhile, open the PLC project you want to publish on the virtual machine. Verify that the TF2000 HMI Server license is present in your system manager under SYSTEM > License. If it’s not present, add it manually via the Manage Licenses tab. Then activate your PLC project which serves as the back end for your HMI project. Open the HMI publishing settings, fill in the IP address of the virtual machine and press Validate connection. If the connection was successful, select Publish.
If you connect to the HMI server for the first time, it asks you to set a password for the HMI server. You need this password whenever you want to publish your HMI project or need to log into the HMI server.
Once the password is set you can publish your HMI which then should be reachable at the Browser URL as shown in the publishing settings.
The error message:
No suitable dump device was found.
Setting hostuuid: 60e34d56-aa3b-ddb2-f508-cbe7cce89d64.
Setting hostid: 0x0789008c.
swapon: /dev/ada1p2: No such file or directory Starting file system checks: Can't open `/dev/ada1p1'
/dev/ada1p1: UNEXPECTED INCONSISTENCY; RUN fsck_msdosfs MANUALLY.
THE FOLLOWING FILE SYSTEM HAD AN UNEXPECTED INCONSISTENCY:
msdosfs: /dev/ada1p1 (/boot/efi)
Automatic file system check failed; help!
ERROR: ABORTING BOOT (sending SIGTERM to parent)!
2022-06-04T08:26:05.670175+00:00 - init 1 - - /bin/sh on /etc/rc terminated abnormally, going to single user mode
Enter full pathname of shell or RETURN for /bin/sh:
root@:/
While playing with Tc/BSD in the virtual machine I ran into an annoying issue: I couldn’t scroll up. For example, when I do TcHmiSrv --help, the help text is to long for the console screen. At first, I couldn’t figure out how to scroll back up, because there is no scroll bar, ↑ shows the previous command and Page Up just shows a ~. After consulting Google, I found out that is where the Scroll Lock key is for. I always wondered what this button did. Note: If you’re on a laptop without a Scroll Lock key, you can remap one of the existing key combo’s.
↩
Use the GitHub TwinCAT template repo to set up a TwinCAT repo, including the pre-commits.
Pre-commits are part of a class of so-called git hooks. Hooks enable you to run a script during a git command. Most of these hooks are pre-commit, meaning they do something before a commit, merge, or rebase. Pre-commit hooks can be beneficial; for instance, they can guarantee a uniform code formatting style or verify a file’s JSON or XML format.
You can see some examples of git hooks if you have a project which uses git. Navigate to the .git/hooks folder and you should see a list of example hooks there. If you can’t see the .git folder, make sure you have enabled “Hidden items” under the View tab in your Windows Explorer.
If you open one of the files, you see some bash scripts. It’s not necessary to use bash. You can use any programming or scripting language which is available on your system.
You can write your own hooks from scratch, but for most files, there is already a large variety available. Let me show you how you can use these and a structured text hook for your TwinCAT projects.
A popular framework to manage pre-commits is pre-commit. It’s a Python based framework and thus you need a working Python installation on your system before you can use it.
(In case you do not have Python) Download and install conda via one of the methods below. You can either install Miniconda which is a minimal installation. It comes with the bare necessities to get started. Miniconda should be enough for this tutorial. Or, if you would like to have a bit more tools/modules installed (mainly for data analyses), choose Anaconda.
Install pre-commit with either:
pip install pre-commitconda install -c conda-forge pre-commitCheck if the installation went OK by running the command below in the terminal. If you see a version number then pre-commit works.
$ pre-commit --version
pre-commit 2.17.0
.pre-commit-config.yaml in the git project folder (same folder as where your .git folder resides) where you want to start using pre-commits. Now you’re all set up to start using pre-commits.The kind people of the Photon Controls and Data Systems at SLAC have open-sourced their TwinCAT pre-commits. To use the SLAC pre-commits, add the following lines to your .pre-commit-config.yaml file:
- repo: https://github.com/pcdshub/pre-commit-hooks
rev: v1.2.0
hooks:
# Replaces all leading tabs with spaces
- id: twincat-leading-tabs-remover
# Removes line ids. See point 4 of the link for why you don't need them
# https://cookncode.com/twincat/2021/06/07/tc-source-control-tips#2-creating-independent-files
- id: twincat-lineids-remover
# Formats .tmc and .tpy files
- id: twincat-xml-format
# Check if there are any libraries whose versions are not fixed
- id: check-fixed-library-versions
For completeness, I’ll also add the following standard pre-commits. You can first run these pre-commits. Later I go into details about what they do and why they are useful.
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
hooks:
# Removes trailing white spaces
- id: trailing-whitespace
# Checks yaml files for parseable syntax
- id: check-yaml
# Prevents git from committing large files
- id: check-added-large-files
Install the pre-commit hooks with pre-commit install. This needs to be done once per git repository. Afterwards, you can run pre-commit run --all-files to let the pre-commits do their job. Note: you only run this command if you have added new pre-commits. In other cases, all pre-commits are automatically executed when you use git commit .... In this case, the automatic execution limits to the changed files.
Depending on the project you see no, some, or a lot of changed files. For example, when I ran it on my TwinCAT Tutorial repo I saw the following:
For each git hook, you see if it had files to check. If that was the case, you see if any files were changed.
Below I show an example where two hooks were triggered. Here both the leading tabs remover failed (as shown in the screenshot), but also the trailing white space one failed (not shown). Below are the differences I saw afterward in SourceTree:
You see that it removed a trailing space after the CASE _state OF. Additionally, the file had a mix of tabs and spaces. The tabs were replaced by spaces. These changes were made by the trailing-whitespace and twincat-leading-tabs-remover respectively. For a full list of all the changes, you can see the differences of this commit.
Here I go a little deeper into what the hooks do and why you would want to use them.
twincat-leading-tabs-remover
What: replaces all leading tabs with four spaces.
Why: consistency. Also in some editors, the length of a tab can differ from the length of four spaces.
twincat-lineids-remover
What: removes LineIDs from a POU file.
Why: they are only useful locally. When uploaded to source control they only cause visual clutter. For more information see point 4
twincat-xml-format
What: formats the .tmc and .tcp files with newlines and indentation.
Why: makes these files readable for humans. Normally TwinCAT doesn’t put any newlines or indentations in these files. Useful if you would like to have these files in source control and see clear differences.
check-fixed-library-versions
What: checks if there are TwinCAT libraries whose versions are not fixed or explicitly set to the latest version.
Why: ensures that your software behaves the same, even if you install newer library version on your system.
trailing-whitespace
What: removes spaces and tabs at the end of lines.
Why: whitespace at the end of a line does not influence code execution; you can add or remove as many as you’d like. But, they show up as (useless) changes if someone adds some or removes them.
check-yaml
What: checks that programs can read your YAML file. For example, the .pre-commit-config.yaml one.
Why: ensures that your YAML files do not break.
check-added-large-files:
What: prevents git from adding large files to its history.
Why: to prevent your git tree from becoming huge. Saves time for new users when they download the repo for the first time. Large, non-text files, such as images or binary files usually contain no useful diffs. You want to commit these files using Git LFS.
If you would like to develop your own hooks there are two options: local and remote repo-based hooks. Local hooks are quite easy to set up, but they can only be used in the repo where they are saved. On the other hand, remote-based hooks can be shared across many projects. The TwinCAT hooks you saw earlier, is an example of a remote hook.
You can use many languages to develop your hooks. Most languages need a working installation of that specific language on the system where the hooks are executed. The exceptions are node, python, and ruby. For these languages, no existing installation is needed. Hooks developed in these three languages set up their own (node, python, ruby) environment when first executed. On subsequent runs, the environment is reused.
Remote hooks also need to be a valid git repo. That is because pre-commit tries to do a git clone ... of the repo URL you supplied in the .pre-commit.yaml file. To make a remote hook, see the TwinCAT hooks repo as an example.
Developing a local hook is quite straightforward. I’ll explain how to do it by making a hook that checks if all links on this blog start with https. If they don’t, it replaces http with https.
First I added a file called .pre-commit-config.yaml with the following content. See the comments for their meaning.
repos:
- repo: local
hooks:
# Name of the hook
- id: check-https
# Hook name shown during hook execution
name: check if all links are https
# Where pre-commit can find the script it should call and how it should call it
entry: python _hooks/check_https.py
# The language to use, in this case we're using a language present on the system.
# Using python as an argument would work as well
language: system
# A valid regular expression pattern to define which files should be passed to `check_https.py`
files: '.*\.(md|markdown)'
When pre-commit runs this local hook, it first searches for files that conform to the regular expression pattern mentioned in files:. Then it calls the command mentioned under entry with each filename. For example, it finds the files README.md and about.md. Then it calls check_https.py with python _hooks/check_https.py README.md about.md.
Next, I created a new file called _hooks/check_https.py with the following content. See comments for the meaning.
#!/usr/bin/env python
import argparse
def fix_file(filename):
# Open the file
with open(filename, 'r') as fd:
original_lines = fd.readlines()
new_lines = []
changed = False
# For each line in the file check if a https:// is found.
# If so, replace it with https://
for line in original_lines:
if "https://" in line:
line = line.replace("https://", "https://")
changed = True
new_lines.append(line)
# If a line was changed, print a message in the console and overwrite the
# original file with the fixed one.
if changed:
print(f'Fixing {filename}')
with open(filename, 'w') as fd:
fd.write(''.join(new_lines))
def main(args=None):
# Parse the filename arguments:
# e.g. Namespace(filenames=['README.md', 'about.md'])
if args is None:
parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*')
args = parser.parse_args()
# For each filename execute the file fixer on it
try:
for filename in args.filenames:
fix_file(filename)
# Return 0, which means the hook executed successfully and pre-commit is happy
return 0
except Exception as exc:
print(exc)
# If something went wrong return an error code other than 0 and pre-commit
# then knows something went wrong
return 1
if __name__ == "__main__":
exit(main())
If you now run pre-commit run --all-files you should see all the files it changed.
> pre-commit run --all-files
check if all links are https.................................................Failed
- hook id: check-https
- files were modified by this hook
Fixing _posts/2021-09-13-units.markdown
Fixing _posts/2021-06-07-tc-source-control-tips.markdown
Fixing _posts/2021-08-17-tc-simulation.markdown
Fixing tclinks.md
Other ideas could be to use Prettier to format JavaScript, HTML, or CSS files from HMI projects. Unfortunately, TwinCAT saves the HMI pages as .content and .view files. These files are not recognized as HTML files by prettier, but you could probably make it work by temporarily renaming these files to .html and then running prettier.
Pre-commits are automatically executed locally whenever you commit something. But you can also add pre-commit to your CI workflow. For example, use prettier.ci to automatically format markdown files of a pull request.
Have you already used pre-commits for your (TwinCAT) projects? Or do you have other ideas? Let me know in the comments below.
]]>