Most Powerful Open Source ERP

Building a fruit selection machine from scratch

Within the first phase of the project, we built a small production line entirely from open source and open hardware materials in order to have a demonstrator to work on and develop our OSIE components. This blog post explains some of the details.
  • Last Update:2020-11-03
  • Version:005
  • Language:en


As part of OSIE project (Open Source Industrial Edge) we required to have a demonstrator and development system. We decided to implement a small industrial production line using only open source technology. Due to the COVID pandemic and travel and working restrictions, we had no way to visit any of the industrial project partners and thus had to come up with an alternative way so we could implement controls of a production line which we had access to. As one of the project partners and one the Nexedi team leader were located in a region known for cherry production in Bulgaria, we decided to build a cherry picking machine that could sort good from bad cherries and later also fruits in general.

Our long term objectives for this machine are:

State of the Art

Nexedi is a software company first and foremost, so building a mechanical conveyor with all needed electronic components for this was not something we were very familiar with at first. Our initial designs were very naive, our understanding of the problems we faced very limited.


Initial simulation prototype in a virtual environment (Godot)

We decided early on to work with ProviewR - an open source process control software used in large scale industrials. During the first months of the project we had a chance to test how ProviewR would connect to a "soft" modbus interface which our machine would provide but without having the physical machine itself. More details can be found in this blog post on using ProviewR with the Godot 3D game engine.

Below you can see a simulated video of the machine we wanted to build and control with ProviewR.

Architecture of the machine

Our critiera to meet from the get go are listed below:

  • Use ONLY open-source components - we could NOT use commercial PLCs and / or software in building this machine
  • The machine should be built using standard industrial components (belts, motors, VFDs, gears, etc) so that anyone can rebuild it easily
  • The machine should be able to "speak" Modbus (initially) and OPC UA (later)
  • The machine should be controllable using ProviewR
  • Despite initially targeted at cherry fruit selection, we quickly saw that with proper measurements, gears and motors (and of course software) the machine could be used for other fruit types (and not only fruits)

Based on these criteria we knew that we would have to implement our own versions of already existing industrial components like PLC with modbus protocol support. For this we decided to use Olimex's Lime2 open hardware board

First Goal: Building a PLC

We had to be able to control electrical devices (motors, valves, etc), which is usually done by a PLC. We had to build our own but before starting we had to handle a number of prerequisities:

  1. Build an enclosure for Lime2 which can be mounted on a DIN rail
  2. Using this enclosure to connect over UEXT to Olimex's MOD-IO board and control electrical relays using I2C
  3. Be able to programmatically control a relay (switch it On / Off)

It took some efforts to build a case for Lime2. We did this by starting from an existing FreeCad model which we extended so it could fit an additional component from Olimex called Lime2-shield. The process of printing a box was done using an entry level 3D printer. Luckily for us, Olimex had already developed a library that handles communication over I2c called pyA20Lime2.

As result we had our first first PLC with its own DIN rail mountable box that could be controlled by a Python script. Not fancy but working.

Second goal: Build a conveyor

Nexedi hired an external engineer to build a conveyor belt around which we started to construct the fruit selection machine. Our goal was to have the most simple conveyor with loading and offloading units that:

  1. Uses VFD (which "speaks" modbus) so we can control over modbus the linear speed of it
  2. Is build from standard industrial components without any welding needed (just using bolts). This way any amateur can rebuild the machine once we open source it

Thanks to industrial standards and existing components this was the easiest part of all. With some good mechanical knowledge we had our first conveyor belt running with START / STOP button within a few weeks.

Third goal: Use the PLC to control the conveyor

At this point in project we could use simple Python script to turn conveyor On / Off. To begin, we did not want to handle VFD's modbus communication and thus we had to use a hard-wired configuration and hard-coded linear speed settings which we then developed to controling the electrical relay using remote code. As part of this phase, we added an air valve controlled via a 12V DC circuit which we would use later in project to blow off bad fruits from the line. We added an air compressor which would supply the compressed air for the valve - of course using existing industrial components like relays, air valves, compressors, etc.

And final wiring:

Fourth goal: Modbus integration. Camera and OpenCV to do an optical inspection

The last part of project was adding Modbus support for our software PLC layer. This would allow a remote application and / or ProviewR setup to control the conveyor. In addition to this we started to experiment with real time camera which uses OpenCV to do shape detection. Since it was a first attempt, the optical inspection we implemented was quite modest and simple: it uses simple masks and countour detection so our conveyor is "trained" to blow away any red items which have more than three countours. This algorithm will of course be improved as the project progresses so it can be used on for real fruits with more complex and realistic models. However for the initial goals we proved that it's possible to have a open source machine within 5 months.

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.