Most Powerful Open Source ERP

Using Godot game engine to simulate shop floor machinery controlled by ProviewR

  • Last Update:2020-11-16
  • Version:001
  • Language:en


The whole idea is to make Godot simulate industrial devices and expose them with the modbus protocol so you can use any modbus-capable industrial control suite out there, in our case it's ProviewR. This way we can build test and our Plc in a virtual shop floor.

Our concrete example is to reproduce a setup in a real fruit selection machine using ProviewR, modbus and different sensors



Godot is a 2D and 3D, cross-platform, free and open-source game engine released under the MIT license. It was initially developed by argentinian Juan Linietsky and Ariel Manzurfor for several companies in Latin America prior to its public release. The development environment runs on multiple operating systems including Linux, macOS, and Windows. Godot can create games targeting PC, mobile, and web platforms.


ProviewR is probably the first Open Source system for process control and automation in the world. Originally developed in Sweden by Mandator and SSAB Oxelösund as a process control system based on standard computers, the system has become a fully-fledged, integrated and low-cost solution that is running on standard PC's with Linux as operating system.
ProviewR is Open Source and the license is GNU/GPL.


Modbus is a data communications protocol originally published by Modicon (now Schneider Electric) in 1979 for use with its programmable logic controllers (PLCs). Modbus has become a de facto standard communication protocol and is now a commonly available means of connecting industrial electronic devices. Modbus is popular in industrial environments because it is openly published and royalty-free. It was developed for industdeploy and maintain compared to other standards, and rial applications, is relatively easy to places few restrictions - other than the datagram (packet) size - on the format of the data to be transmitted. Modbus uses the RS485 or Ethernet as its wiring type. Modbus supports communication to and from multiple devices connected to the same cable or Ethernet network.

Godot shop floor

Before we begin, be aware that we will not cover in details how to setup and use Godot, there is a lot of good tutorials out there for you to reproduce or modify our simulation. If you want to test our simulation, just follow the steps in the README of our repo.

Here is our scene:

There are five objects in the scene:

  • The hatched cube is a cube spawner. It produces a new cube every three seconds. This cube will fall and land on...
  • ... a rectangle with yellow arrow on it which represent a convoyer (which on the real setup has its speed controlled by a VFD using by modbus) belt. The cube is then displaced from left to right on it.
  • A cube. obviously. which is in the middle of the conveyor belt on this image. This cube represents the coffee cup of the real world shop floor.
  • The blue rectangle with a black sphere in it in the sky is an optical sensor. It can detect a cube on the conveyor belt.
  • And finally there is the pusher (which on the real world setup is an air valve with a compressor) on the left side of the conveyor belt. It can be activated to push a cube outside the conveyor belt, making it fall into the underworld.

Cube spawner

Here is the code for spawning a new cube every 3 seconds:

extends Spatial # We need this to be able to call add_child
export var frequency = 0.3
onready var timer =
var boxscene = preload("res://TestBox.tscn") # Preload cube object

func _ready():
	timer.wait_time = 1.0/float(frequency)
	timer.connect("timeout",self,"spawn") # On each timer trigger, call spawn function
	spawn() # Spawn the first cube to avoid waiting for the first timer trigger

func spawn():
	var box = boxscene.instance()

The ``_ready`` is a function called when a scene (in our case the cube spawner) is loaded and "ready" to be used in the 3D world (see Godot documentation) )

Conveyor belt

Nothing fancy here, a force is applied to the cube touching it. This can be done directly in the editor, but as we want the yellow arrow to go at the same speed than the cube, we used a script in order to change the speed in only one place.

Modbus in Godot

This was the simple part. Now let's focus on our modbus devices. Before going into the scripts we are using, we need to understand how modbus work.
Modbus devices are communicating using a client/server relationship (new wording of the Modbus Organization), where clients make requests and servers respond to it (except for Modbus TCP where every devices can be a client). Here are the different data types that a ModBus server can provide:

Object type Access Size Address Space
Coil Read-write 1 bit 00001 - 09999
Discrete input Read-only 1 bit 10001 - 19999
Input register Read-only 16 bits 30001 - 39999
Holding register Read-write 16 bits 40001 - 49999

A modbus client can ask a modbus server to:

  • change the value in one of its registers, that is written to Coil and Holding registers.
  • read an I/O port: Read data from a Discrete and Coil ports,
  • command the device to send back one or more values contained in its Coil and Holding registers.

This is enough technical stuff to understand what we will do in order to communicate data between ProviewR and Godot.

As one can except, Godot does not provide ModBus communication protocol directly. We will need to use libmodbus for this.

To use it, we have created dynamic library which will be loaded and used by some GDScript later on. We will skip the setup part as this not really interesting and very well documented in the Godot documentation. Our dynamic library provides four functions used in Godot:

  • start_server(address, port): This function call modbus_mapping_new in order to setup the registers of the modbus servers. Then we spin a new thread using pthread_create which call the server_routine. This routine will wait for a connection from a client and the accept it, then it's waiting for a request sent by the client using modbus_receive. We directly respond to it using modbus_reply which also read or write different register/coils if this what was asked in the request. And that it, we are ready to read and write server register/coils directly in Godot using:
  • set_holding_register(value) which set a register with a given value,
  • get_holding_register() which returns the value of a register,
  • get_coil() which returns the value of a coil.

Here is how we load and wrap our dynamic library symbol in Godot:

class_name ModbusServer
var c_backend = load('res://simple.gdns').new()
var hold_register = 17 setget set_holding_register, get_holding_register # getter and setter for smother GDScript integration

func _init(address, port):
    c_backend.start_server(address, port)

func set_holding_register(value):

func get_holding_register():
    return c_backend.get_holding_register()

func get_coil():
    return c_backend.get_coil()

Now that we can communicate, let's add some logic to our sensor and pusher


extends ModbusSpatial # By extanding ModbusSpatial which call, we create a new ModBus server

func _physics_process(delta):
    ms.hold_register = floor($RayCast.distance*100.0) # This call ms.set_holding_register as we have set a setter and a getter

Nothing fancy here, we get the distance to the cube using a raycast and store the result in a register of the modbus server corresponding to the sensor.


extends ModbusSpatial # By extanding ModbusSpatial which call, we create a new ModBus server device

... # Varibles initialization

func _physics_process(delta):
    var needs_to_extend = ms.get_coil()

    if needs_to_extend and extension < max_extension:
        ... # Extend
    if !needs_to_extend and extension > 0:
        ... # Retract

We read the coil of the modbus server corresponding to the pusher and extend the pusher if the coil is set and retract it otherwise.

We used the _physics_process callback because it provides consistent updates over time, regardless of how fast or slow time advances, see Godot documentation.

That the end of the Godot part. We have two modbus server, but nobody is talking to them, what a shame. Let's fix that by using ProviewR.

ProviewR automaton:

ProviewR is a complex system to setup and since we already covered Modbus setup in ProviewR in another article.

Our setup is one Modbusmaster (I can't use client/server wording as ProviewR hasn't made the switch...) with two Modbusslaves, one for the sensor and one for the pusher, with one Modbusmodule containing one signal each. For the Plant we have one Digital Input and one Digital Output connect to their respective node counterpart.

Let's talk about our automaton. We are using grafcet to represent our automaton.

From top to bottom we find:

  • ISO: Start step
  • T0: Transition triggered by a value of 1 while reading the sensor register
  • S0 and S1: Two step running in parallel. S0 set a timer of 1.25 second and S1 set the coil of the pusher to 1.
  • T1: Transition triggered by the end of the timer. A coil is unset by default while your not actively pushing a 1 one it, so when the transition happens, the coil of the pusher is set to 0 and the pusher retract itself.
  • Loop to ISO

This plc is then compiled (stay tuned for an article on this) to a native executable that ProviewR will run.

If we start both Godot and our ProviewR runtime....

Et voilà!

Thanks for reading this long article.



08-10 2019

Adding 10000 rows in a table by Unknown User

Hello, I am trying to create a table containing 82 columns and 10000 rows (11028 to be exact) in a sheet. I tried to add the lines one by one and then to do a Sync but the table is not created. I thought it was a lot of data at one time so I tried adding lines in packets of 1000. This time, I get the table but only the first 1000 rows are filled. In the log, I see all the packets go by.   In the code below : - "values" is the json formatted data i try to add  - "tableName" speaks for itself. The steps before 5 are inscriptions of some data in other sheets but these works.   function CreateODataQuerySheetStep5_List(values, tableName) {     let n = values.length;     if (n > 0) {         CreateODataQuerySheetStep5_ListTableHeader(values, tableName);           } else { (context) {             let headers = [["No data"]];             let sheet = context.workbook.worksheets.getItem(newQueryName);             let table = sheet.tables.add("A1", true);    = tableName;             table.getHeaderRowRange().values = headers;             sheet.activate();             window.location.href = "Home.html";             return context.sync();         }).catch(function (error) {             logging(sessionStorage.getItem("Token"), error);         });             } } function CreateODataQuerySheetStep5_ListTableHeader(values, tableName) {     console.log("Creating header"); (context) {         console.log("Get Sheet");         let sheet = context.workbook.worksheets.getItem(newQueryName);         console.log("Define table horizontal length");         let line = values[0];         let keys = Object.keys(line);         let address = "A1:" + ColumnToLetter(keys.length) + "1";         console.log("Computing header columns");         let headers = [];         for (let j = 0; j < keys.length; j++) {             let key = keys[j];             headers.push(key);         }         console.log("Adding table");         let table = sheet.tables.add(address, true); = tableName;         let temp = [];         temp.push(headers);         console.log("Putting header");         table.getHeaderRowRange().values = temp;         return context.sync().then(function () {             console.log("Header created")             CreateODataQuerySheetStep5_List1000(values, 0, tableName, headers);         }).catch(function (error) {             logging(sessionStorage.getItem("Token"), error);         });     }).catch(function (error) {         logging(sessionStorage.getItem("Token"), error);     }); } function CreateODataQuerySheetStep5_List1000(values, start, tableName, headers) {     console.log("Adding 1000 rows"); (context) {         console.log("Get Sheet");         let sheet = context.workbook.worksheets.getItem(newQueryName);         console.log("Get Table");         let table = sheet.tables.getItem(tableName);         console.log("Computing max between 1000 and remaining lines");         let x = values.length - start;         let n = Math.min(1000, x);         console.log("Adding rows from " + start.toString() + " to " + (start + n).toString());         for (let i = start; i < n; i++) {             let line = values[i];             let keys = Object.keys(line);             let row = [];             for (let jj = 0; jj < headers.length; jj++) {                 let prop = headers[jj];                 let value = line[prop];                 if ($.type(value).toLowerCase() === 'string') {                     value = "'" + value;                 }                 row.push(value);             }             table.rows.add(null, [row]);         }         console.log("Rows added");              let y = start + 1000;         if (y < values.length)         {             console.log("Rows are remaining");             return context.sync().then(function () {                 console.log("Recursive Call");                 CreateODataQuerySheetStep5_List1000(values, y, tableName, headers);             }).catch(function (error) {                 logging(sessionStorage.getItem("Token"), error);             });         }         else         {             //if (Office.context.requirements.isSetSupported("ExcelApi", 1.2)) {             //    sheet.getUsedRange().format.autofitColumns();             //    sheet.getUsedRange().format.autofitRows();             //}             console.log("Sheet activation");             sheet.activate();             return context.sync().then(function () {                 console.log("return to home");                 window.location.href = "Home.html";             });         }              }).catch(function (error) {         logging(sessionStorage.getItem("Token"), error);     });      }
31-01 2019

Unprotect the excel sheet with password pro-grammatically using officejs. by Unknown User

await function(context) {   const sheet = context.workbook.worksheets.getActiveWorksheet(); // const workBook = context.workbook. console.log(;"abc") const range = sheet.getUsedRange(); range.load("values"); range.load("address"); return context.sync().then(function () { console.log(range.address); }) }).catch(function(error) { console.log("Error: " + error);   });   Giving Error: Uncaught (in promise): InvalidArgument: The argument is invalid or missing or has an incorrect format.