TwinCAT and Unity – Part#1

Welcome to Part 1 of my TwinCAT and Unity series. In this post we will focus on preparing the basic framework for our environment. In every part of this series I will introduce new working feature of the environment, end to end. The goal for this part is to exchange boolean and integer variables between the PLC and Unity.

Project Setup

In the first step we will create a simple PLC project that will allow us to test basic functionality and exchange first data with Unity.

When creating new project select TwinCAT Projects

This standard template provides convenient structure which we can expand. In the next step we will add PLC project to our structure. When adding PLC project select “Standard PLC Project”. This will allow TwinCAT to create some basic framework, including the task assignment and associated POU (Program Organization Unit) . This also creates a folder structure that will allow us to keep the project files neat and tidy. This is my favorite aspect of working with TwinCAT – we can subdivide project into meaningful tree structure which is much more convenient than plain list of hundreds of function blocks.

After adding PLC project the structure should look like this

If we double click on the PLC project name we can check the ADS port which was assigned to this project. It will come handy when we start communicating with our PLC. Usually it is port 851.

Data Structures

The implementation of ADS (Automation Device Specification) in Beckhoff’s libraries allows us to query variables using their names. Direct addressing is possible but in this example we will omit it. To make sure that we use this capability in full we have to have robust data structures that we can refer to in the future. In “GVLs” folder we will create a new folder called “Communication” in which we’ll create Global Variable List.

For more complex projects a different approach might work better – we’ll talk about this in the future.

We’ll need two variables for now – one BOOL and one DINT.

VAR_GLOBAL
	bVar1	: BOOL;
	iVar1	: DINT;
END_VAR

Logic

At this moment our PLC is doing absolutely nothing. We need some simple code to get us going. As with all introduction projects let’s implement a PLC “Hello World” equivalent – simple square wave generator. To make things a little more interesting let’s create a separate Function Block. The FB will take as input pulse length in ms and returns boolean value as output.

FUNCTION_BLOCK FB_SquareWave
VAR_INPUT
	PulseLength	: INT;		// Pulse length in [ms]
END_VAR
VAR_OUTPUT
	Value		: BOOL;         // Generated pulse
END_VAR
VAR
	Impulse   	: TON;
	Edge		: BOOL;
END_VAR
//-------------------------------------------------------------------------
Impulse(IN := NOT Impulse.Q, PT := INT_TO_TIME(PulseLength));

IF Impulse.Q THEN
	Impulse(IN := FALSE);
	Edge := TRUE;
END_IF

IF Edge AND Value THEN
	Value := FALSE;
	Edge := FALSE;
ELSIF Edge AND NOT Value THEN
	Value := TRUE;
	Edge := FALSE;
END_IF

Again, we will take an advantage of TwinCAT project structure and create separate folder for our FB.

Now, in our MAIN program VAR section we will define an instance of newly created Function Block. In addition iPulseLength variable is created, for now we will explicitly assign a value of 500 to it.

VAR
iPulseLength : DINT;
fbWave       : FB_SquareWave;
END_VAR

In the code itself we will call our instance, populating it’s input field. Output field of the instance will be assigned to one of Communication variables we have created earlier.

iPulseLength       := 500;
fbWave(PulseLength := iPulseLength );	// instance of FB_SquareWave
DataExchange.bVar1 := fbWave.Value;

At this point we can compile the project, activate the configuration, download and run the PLC program. If everything is setup correctly the result will be as expected – pulsing variable with 1 second cycle.

Unity3D setup

The time has come, to take a look at the other side of the environment – Unity 3D. In this step we will create an empty 3D Project without any assets or packages. My Unity is configured to work with Visual Studio, but it is possible to repeat those steps using Mono or any other compatible IDE.

Empty 3D project

Unity C# scripts

The first step is to get the TwinCAT ADS assembly into our project. To achieve that, let’s create a folder in our assets directory called “Classes”. In that folder we need a new class (in Unity lingo this will be new C# Script) called “TwinCAT_Handler.cs”. If our environment is configured to use Visual Studio – double clicking on script icon will open a new instance of said IDE. The easiest solution to add ADS assembly is to use NuGet package manager and search for Beckhoff.TwinCAT.Ads. After assembly installation we can use TwinCat in our project. Our first attempts to communicate with the PLC will only involve opening connection to ADS port to see if we can talk to each other. We would like ADS to start communication before Unity renders the first frame. To indicate that we want to use TwinCAT ADS we have to add following statement at the beginning of the file.

using TwinCAT.ADS;

Our newly created class inherits from MonoBehaviour base class. This base class has a very useful Awake() method which is being called just once, when all objects are initialized – exactly what is needed. In our class we add Awake method and following code

private TcAdsClient _tcClient = null;

void Awake()
{
     _tcClient = new TcAdsClient();
     _tcClient.Connect([your AMS Net id], 851);
     if (_tcClient.IsConnected)
     {
       Debug.Log("Twin CAT ADS port connected")
     }
     else
     {
       Debug.LogError("ADS Connection failed");
     }
}

[your AMS Net id] is in fact identifier which ADS is using to connect to our PLC. This can be found using TwinCAT icon in taskbar – right click the icon and select “About TwinCAT”. AMS Net id can also be found (and changed) in TwinCAT project under System, Routes, NetId Management).

When we save the class, we can return to Unity. There is a little caveat here. The added assembly will work fine in Visual Studio, but when we switch to Unity to utilize our newly written script – we will see following error message:

Assets\Classes\TwinCAT_Handler.cs(4,7): error CS0246: The type or namespace name ‘TwinCAT’ could not be found (are you missing a using directive or an assembly reference?)

To solve this annoying problem we have to copy assembly DLL to the Assets folder. NuGet copies the assembly into the “Packages” folder in our project directory. This folder is not usually visible to Unity, hence the error. Copying the assembly resolves the issue. At this stage our Assets folder should look like this:

First Game Objects

Having the class itself is not enough. As in normal C# program we need to create an object of our class. In Unity world this will be a GameObject. Anything that can take C# script. We can create an Empty GameObject and add our class to it. This will instantiate our class and call its methods, including – Awake. Lets call this object “TC_Handle”, we are going to utilize it later.

If everything goes well, at this step we should be able to press “Play” in Unity and in the console we will see “TwinCAT ADS Connected”. Make sure that the PLC configuration has been activated and the PLC program is running.

Reading values

TwinCAT handler class has now the ability to connect to the PLC. We now should extend its functionality to allow it to read values. The Beckhoff documentation provides some hints how to approach this task. Some of the reading methods provided by the assembly allow us to use listeners and events to control how the variables are being read. This time however, we’re not going to use them. We have to take into consideration how Unity is cycling through the objects and their methods. The most appropriate solution in this environment is to poll variables every frame.

It is worth saying that this approach is not the most accurate (or efficient) – frame rendering process and its cycle time will most likely change depending on the complexity of the scene and the hardware it is being run on. The topic of real-time and accuracy of the simulation goes beyond the scope of this series.

The method we are going to use in our environment is “ReadAny”. This method can be configured to read most of the basic data types. We are going to create two Read methods – one to read BOOL and one to read DINT.

public bool ReadBool(string pou, string variableName)
{
  try
  {
     var hVar = _tcClient.CreateVariableHandle(pou + "." + variableName);
     var readVariable = _tcClient.ReadAny(hVar, typeof(bool));
     _tcClient.DeleteVariableHandle(hVar);
     return bool.Parse(readVariable.ToString());
  }
  catch (AdsErrorException)
  {
     Debug.LogError("TC Error - reading BOOL failed");
     return false;
  }        
}

public int ReadDInt(string pou, string variableName)
{
    var value = 0;
    try
    {
      var hVar = _tcClient.CreateVariableHandle(pou + "." + variableName);
      value = (int)_tcClient.ReadAny(hVar, typeof(int));
      _tcClient.DeleteVariableHandle(hVar);
    }
    catch
    {
      Debug.LogError("TC Error - reading DINT failed");
    }
    return value;     
}

To test our new functionality we can call both methods in “Update” method of our GameObject. We just have to remember to delete that later on. This simple code snippet will read our DataExchange variables and output them to the debug console. Make sure that the PLC program is running, as port 851 can be available even if the program is stopped.

void Update()
{
   if (_tcClient.IsConnected)
   {
     bool bVar1 = ReadBool("DataExchange", "bVar1");
     int iVar1 = ReadInt("DataExchange", "iVar1");
     Debug.Log(string.Format("Bool: {0}, Int: {1}", bVar1.ToString(), 
     iVar1.ToString()));
   }
}

If everything goes well, we should be able to see that the bool variable is switching between true and false. To see if an integer is read correctly we have to switch to TwinCAT Visual Studio and force online values.

Online view of DataExchange
Associated response in Unity

Writing values

The next feature we need before we can start visualizing things in Unity is writing data. Our new method will return TRUE if writing is successful or FALSE if we encounter any problems. We are using “WriteAny” method from the TwinCAR.ADS assembly. This method allow for any System.Object to be sent. It is our responsibility to make sure that the value we trying to write is of correct type.

public bool WriteValue(string pou, string variableName, int value)
{
  try
  {
     var hVar = _tcClient.CreateVariableHandle(pou + "." + variableName);
     _tcClient.WriteAny(hVar, value);
     _tcClient.DeleteVariableHandle(hVar);
     return true;
   }
   catch (AdsErrorException exc)
   {
     Debug.LogError("TC Write Error " + exc.Message);
   }
   return false;
}

If the size of the variable is incorrect we will get ADS Error 0x705. Important thing here – the size of the variables is different between IEC61131-3 and .Net. For example:

SizeIEC61131-3.Net
BitBOOLbool
1 ByteBYTEbyte
2 BytesINTshort
4 BytesDINTint
2 BytesUINTuint
4 BytesREALfloat

Once again we can test the method immediately inside Update method. This time we will use reading and writing in the same method.

void Update()
{
  if (_tcClient.IsConnected)
  {          
     bool bVar1 = ReadBool("DataExchange", "bVar1");
     int iVar1 = ReadDInt("DataExchange", "iVar1");
     bool writeTest = WriteValue("DataExchange", "iVar1", iVar1 + 1);            
     Debug.Log(string.Format("Bool: {0}, Int: {1}", bVar1.ToString(), 
                iVar1.ToString()));
  }
}

The result should be iVar1 being incremented every frame.

Visualizing data

Last step of this part will allow us to visualize the data we receive from the PLC. We are going to create a new class that will utilize TwinCAT Handler object and interact with variables that are being used in our square wave generator. The goal is to change the pulse length and observe its behavior in Unity. The state of the square wave will be reflected by color changes of the object that will utilize this class.

At the beginning we are creating variables that will be available from Unity level. Using those variables will allow us to reuse the code and inject existing handler object. Methods “readState” and “writePulseLength” are utilizing handler methods we have implemented earlier. The only difference in this method, is that we are only writing the variable to the PLC if the value is different from the one previously written. This has been done to reduce the amount of write operations.

public class FB_SquareWave : MonoBehaviour
{

    [SerializeField]
    public TwinCAT_Handler _tcHandler;
    public string sPouName;             // Beckhoff POU name
    public string sStateName;           // Output variable name
    public string sPulseLengthName;     // Pulse length variable name
    public int iPulseLength;

    private int iLastWrittenPulseLength;
    private bool bState;

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        readState();
        writePulseLenght();
    }

    private void readState()
    {
        bState = _tcHandler.ReadBool(sPouName, sStateName);
        toggleColor(bState);
    }

    private void writePulseLenght()
    {
        if(iLastWrittenPulseLength != iPulseLength)
        {
            if(_tcHandler.WriteValue(sPouName, sPulseLengthName, 
                iPulseLength))
            {
                iLastWrittenPulseLength = iPulseLength;
            }
        }
    }

    private void toggleColor(bool state)
    {
        var objectRendered = gameObject.GetComponent<Renderer>();
        if (state)
        {
            if (objectRendered != null)
            {
                // red for TRUE
                objectRendered.material.color = new Color(255, 0, 0);
            }
        }
        else
        {
            if (objectRendered != null)
            {
                // green for FALSE
                objectRendered.material.color = new Color(0, 255, 0);
            }
        }
    }
}

Finally we are ready to connect our class to Unity object. I have created a sphere gameObject and assigned newly created class to it. The _tcHandler property has to have our initial empty gameOjbect assigned which implements the TwinCatHandler class.

Small code change is required in the PLC program. Modify the MAIN program to the following. Download and start the program.

iPulseLength := DataExchange.iVar1;	// pulse length in ms
fbWave(PulseLength := iPulseLength);	// instance of FB_SquareWave
DataExchange.bVar1 := fbWave.Value;

Going back to Unity – when we press “Play” we will see the sphere game object change color according to the output of square wave generator. It is possible to change the iPulseLength during runtime – effects should be visible instantly.

That’s it! The most basic functionality has been implemented – we can read and write BOOL and DINT variable types. In the next part we will start using those methods to simulate turning stuff on and off, implement simple sensors equivalents, and utilize Unity physics engine – stay tuned!

4 thoughts to “TwinCAT and Unity – Part#1”

  1. Hello´╝îI have a question about establish the FB_SquareWave in TwinCAT, I’m confused about these process. I don’t know how to do it. ( I start learning TwinCAT working with Unity recently) Could you please tell me how to establish this function block or could you send these project files to my email box so I can study it carefully? That will be a great help to me. Really appreciated it. My email address is [edited]

    1. Hi Mars, to create a function block in TwinCAT right click the folder you want to create it in and then click Add -> POU. The code is on my GitHub page – link in the second part of this tutorial.

  2. Thanks for this tutorial, it’s been very helpful and exactly what I needed. I have a licensing question about using the TwinCAT 3.1 -eXtended Automation Engineering (XAE) software. Right now I’m testing some communication to get familiar with TwinCAT, but I currently only have a 7 day license. How did you manage to get free access to their software?

Leave a Reply

Your email address will not be published. Required fields are marked *