Introduction: EAL-Industri4.0-RFID Dataopsamling Til Database
Dette projekt omhandler opsamling af vægtdata, registrering af identiteter vha. RFID, lagring af data i en MySQL database vha. node-RED, samt fremvisning og behandling af de opsamlede data i et C# program i form af en Windows Form Application. Vi forestiller os følgende:
Vi har en produktionslinje som producerer leverpostej i 200g foliebakker. Alle færdigbagte leverpostejer udstyres efter afkøling med et RFID tag i plasticlåget/labelen, som indeholder et unikt ID (UID = Unique Identifier, er en 32 bits kode, 8 hexadecimale karakterer) for entydig identifikation af hver enkelt bakke leverpostej. Da færdigvægten af hver enkelt bakke leverpostej kan svinge (afhængig af råvarer, fordampning i ovn mm), og da kunderne hver har et specifikt krav færdigvægten, bruges UID tagget til at knytte hver enkelt leverpostej til en specifik lagerlokation på lageret, hvor der kun lagres leverpostejer til én specifik kunde. Kunderne er supermarkedskæder:
1. Irma. Vægten på Irmas luksus leverpostej skal holde sig inden for +/- 5%, altså minimum 190g og maksimum 210g.
2. Brugsen. Vægten på Brugsens leverpostej skal holde sig inden for +/- 10%, altså minimum 180g og maksimum 220g.
3. Aldi. Vægten på Aldis discount leverpostej skal holde sig inden for +/- 15%, altså minimum 170g og maksimum 230g.
Der er således følgende sorteringer:
Range0: out of range
Range1: minimum 190g/maximum210g
Range2: minimum 180g/maximum220g
Range3: minimum 170g/maximum230g
Step 1: Opsamling Af Data for Vægt Samt Registrering Af UID
Til opsamling af data for vægt, samt registrering af RFID tags er anvendt en Arduino MEGA2560 med en RFID-RC522 reader/writer. Da vi ikke har nogen vægt, simulerer vi data for vægten med et potmeter tilsluttet en analog indgang på Arduinoen.
Følgende opstilling er anvendt:
1 stk potmeter 25k lineært. Yder-benene er tilsluttet hhv. GND og +5V, midterbenet er tilsluttet AN0
RFID-RC522 er tilsluttet Arduino boardets SPI port på følgende måde:
SDA -> pin 53
SCK -> pin52
MOSI ->pin51
MISO->pin50
IRQ ->NC
GND ->GND
RST -> pin5
3.3V -> 3.3V
De opsamlede data, for hhv. UID og vægten, sendes på den serielle port som en komma-separeret tekststreng videre til node-Red som står for den efterfølgende præsentation på et dashboard og lagring i en database.
Step 2: Arduino-program
I Arduino programmet inkluderes de to biblioteker SPI.h og MFRC522.h for at kunne bruge RFID læseren. I starten af programmet initialiseres de anvendte variable. Der laves en instans af MFRC522. I Setup blokken initialiseres den serielle forbindelse, SPI porten og MFRC522. Derefter scannes efter RFID tags. For ikke at sende det samme UID afsted flere gange efter hinanden, er der lavet en stump kode som tjekker for dette. Når der er scannet et UID tag, loades arary nyUID med det netop læste UID. Hvis array nyUID er forskellig fra oldUID er der tale om et nyt UID som kan sendes på den serielle port. Hvis nyUID og oldUID er ens, er der tale om samme UID tag og UID'et skal ignoreres. Hvis der er tale om et nyt UID, sendes UID'et på den serielle port sammen med en læst værdi fra den serielle port. Den analoge værdi skaleres til området 150-250. Data sendes som en komma-separeret tekststreng. Som det sidste sættes oldUID = nyUID, således at koden klart til at læse et nyt RFID tag.. Den sidste funktion i programmet er den funktion som sammenligner 2 arrays. Funktionen returnerer true hvis array'ne er ens, og false hvis array'ne er forskellige.
#include <SPI.h> #include <MFRC522.h> // This program scans RFID cards using RDIF-RC522 reader/writer board. // UID is read, an analog pin is read. Analog value 0-1023 is scaled to 150-250. // UID and analog value is send as comma-separeted text on serial port using 9600,N,8,1. // Care has been taken to only send each UID one time in an row, // a new UID has to be present before the same UID can be sent again. // This function is implementet in the code by comparing arrays : oldUID[]<>nyUID in function array_cmp(oldUID[], nyUID[])
constexpr uint8_t RST_PIN = 5; constexpr uint8_t SS_PIN = 53; int sensorPin = A0; int Value = 0; String StringValue = "0000"; byte oldUID[4] = {}; byte nyUID[4] = {};
MFRC522 mfrc522(SS_PIN, RST_PIN); // Create MFRC522 instance.
void setup() { Serial.begin(9600); // Initiate a serial communication SPI.begin(); // Initiate SPI bus mfrc522.PCD_Init(); // Initiate MFRC522 }
void loop() { // Look for new cards if (! mfrc522.PICC_IsNewCardPresent()) { return; } // Select one of the cards if (! mfrc522.PICC_ReadCardSerial()) { return; } //load nyUID with UID tag for (byte i = 0; i < mfrc522.uid.size; i++) { nyUID[i] = mfrc522.uid.uidByte[i]; } // if oldUID[]<>nyUID[] if (!array_cmp(oldUID, nyUID)) { // send UID tag on serial port for (byte i = 0; i < mfrc522.uid.size; i++) { Serial.print(mfrc522.uid.uidByte[i], HEX); } // send comma on serial port Serial.print(","); // read analog value Value = analogRead(sensorPin); // scale analog value if (Value > 1000) { Value = 1000; } Value = (Value / 10) + 150; // send scaled analog value Serial.print(Value); // send newline Serial.println(); //set oldUID[] = nyUID[] for (byte z = 0; z < 4; z++) oldUID[z] = nyUID[z]; } // wait 1 secs delay(1000); }
// compare 2 arrays... boolean array_cmp(byte a[], byte b[]) { bool test = true; //test each element to be the same. if just one is not, return false for (byte n = 0; n < 4; n++) { if (a[n] != b[n]) test = false; // if on byte not equal, test = false } if (test == true) return true; else return false; }
Step 3: Node-RED, Lagring Af Data I Database
Følgende flow er lavet i node-RED:
COM4 er den serielle forbindelse hvor data modtages fra Arduino boardet. Funktionerne "Split and Get value" og "Split and Get UID" splitter teksstrengen ved kommaet og returnere hhv vægten og UID. Vægten bruges til fremvisning på dashboardet i et linechart og en scale. UID fremvises i et tekstfelt. Funktionen test_sound advarer verbalt med sætningen "Out of range", hvis vægten er under 170g eller over 230g, dvs i range 0.
Split and Get value:
var output = msg.payload.split(','); temp = {payload:(output[1])}; return temp;
Split and Get UID:
var output = msg.payload.split(","); temp = {payload:output[0]}; return temp;
test_sound:
var number = parseInt(msg.payload); if (number >230 || number<170) { newMsg = {payload:"Out of range"}; return newMsg; } else { newMsg = {payload:""}; return newMsg; }
Funktionen Split string "," indsætter et timestamp, UID og vægten i en database patedb.patelog.
var output = msg.payload.split(","); //split msg.payload by comma into array UIDTag = output[0]; //first part into first position [0] ValueTag = output[1]; //second part into second position [1]
var m = { topic : "INSERT INTO patedb.patelog (timestamp,UID,weight) VALUES('"+new Date().toISOString()+"','"+ UIDTag +"','"+ValueTag+"');" }; return m;
patelog er en MySQL database forbindelse som er sat op med følgende parametre:
Host: localhost
Port: 3306
User :root
Database: patedb
Step 4: Database-design
Databasen patedb indeholder 4 tabeller
patelog er dataopsamlingstabellen, tilskrives data af node-RED og C# programmet
ordertable er en tabel som indeholder data om de genemførte ordrer, tilskrives data af C# programmet
customertable er et kunderegister
rangetable er en tabel som indeholder grænseværdierne for de i C# programmet benyttede ranges.
Step 5: Patelog
Tabellen patelog indeholder folgende 6 kolonner:
pateID (int) er primary key og inkrementeres automatisk.
Timestamp, UID & vægt er af typen varchar (med forskellig max længde)
rangeNr er af typen tinyint (beregnes og tilføjes af C# programmet)
orderID er af typen int (orderID tilføjes af C# programmet)
Node-RED tilføjer ikke værdier til kolonnerne rangeNr og orderID. rangeNr og orderID tillader NULL værdier, det bruges i C# programmet til at detektere de rækker som skal tilskrives værdier for rangeNr og orderID
Step 6: Ordertable
ordertable indeholder 5 kolonner:
orderID (int) er det aktuelle ordrenummer
orderQuant (mediumint) er ordens pålydende antal
quantProduced (mediumint) er antal der rent faktisk er produceret på ordren. (Tælles af C# programmet)
comment (tinytext) er en eventuel kommentar til ordren.
customerID (int) er det aktuelle kundenummer på ordren.
Step 7: Customertable
customertable indeholder 6 kolonner:
customerID (int) er primary key og auto inc.
name,address,phone,email (varchar) med forskellig max længde
rangeNr (int)
Step 8: Rangetable
rangetable indeholder 3 kolonner:
rangeNr (int) er primary key og auto inc.
rangeMin (int)
rangeMax (int)
Step 9: C# Program
Når der produceres en ordre leverpostej, er proceduren følgende:
Kundenummer, ordrenummer, ordreantal og en eventuel kommentar indtastes i C# programmet (i praksis overføres det digitalt fra virksomhedens ordresystem. Produktionen startes nu ved tryk på ’start’- knappen. Når en leverpostej er færdigproduceret og låget er monteret, vejes den af Arduinoen (på et transportbånd) Samhørende værdier af UID og den aktuelle vægt sendes serielt til node-RED, som viser de opsamlede data på dashboard ’et. Samtidig skrives timestamp, UID og vægt i en ny række i patedb.patelog tabellen. Da der på nuværende tidspunkt ikke tilskrives værdier til rangeNr og orderID vil de have værdien NULL.
Med et timerinterval undersøger C# programmet patedb.patelog
tabellen for nye tilkomne rækker med NULL værdier i rangeNr kolonnen. Når der er detekteret en række med NULL værdi, beregnes rangeNr og det tilføjes sammen med det aktuelle orderID. Når en ordre er produceret, afsluttes ordren ved tryk på ”stop”- knappen. Når ordren afsluttes, tilføjes en række til patedb.ordertable med de aktuelle ordredata. Når en ordre er afsluttet, kan kan de opsamlede data i patelog tabellen fremvises ved at trykke på de forskellige knapper i gruppen Update DataGridview. ordertable kan også vises, og der kan søges ordredata på individueller UID'er eller kundedata på individuelle ordrer.
using System;<br>using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using MySql.Data.MySqlClient;
namespace show_data_from_database { public partial class Form1 : Form { MySqlConnection connection = new MySqlConnection("datasource=localhost; username=root; password=''"); int RowNumber = 0; // Variable for storing pateID value int RangeNumber = 0; //Variable for storing rangenumber int weight =0; // Variable for storing the weight int OrderNr = 0; // Variable for storing OrderNR int QuantProduced = 0; //Variable for storing quantity produced int NumberOfRows = 0; //number of rows with nulls.. bool ProdRunning = false; //Variable indicating if start & stop buttons has been activated int[] limits = new int[6];// initialize array int CustomerID; // Variable for storing customerID public Form1() { InitializeComponent(); load_table(); // call load_table }
void load_table() { MySqlCommand command = new MySqlCommand("SELECT * FROM patedb.patelog ORDER BY timestamp DESC;", connection); try { MySqlDataAdapter adapter = new MySqlDataAdapter(); adapter.SelectCommand = command; DataTable dbdataset = new DataTable(); adapter.Fill(dbdataset); BindingSource bsource = new BindingSource(); bsource.DataSource = dbdataset; dataGridView1.DataSource = bsource; SetRowOrder(); adapter.Update(dbdataset); } catch (Exception ex) { MessageBox.Show(ex.Message); } }
private void SetRowOrder() { dataGridView1.Columns["pateID"].DisplayIndex = 0; // Her kan rækkefølgen af kolonner ændres dataGridView1.Columns["timestamp"].DisplayIndex = 1; // Her kan rækkefølgen af kolonner ændres dataGridView1.Columns["UID"].DisplayIndex = 2; // Her kan rækkefølgen af kolonner ændres dataGridView1.Columns["weight"].DisplayIndex = 3; // Her kan rækkefølgen af kolonner ændres dataGridView1.Columns["rangeNr"].DisplayIndex = 4; // Her kan rækkefølgen af kolonner ændres dataGridView1.Columns["orderID"].DisplayIndex = 5; // Her kan rækkefølgen af kolonner ændres }
private void GetData_Click(object sender, EventArgs e) // Reads database table and orders by Timestamp { load_table(); }
private void btnRefreshUID_Click(object sender, EventArgs e) // { string timeStr = "SELECT * FROM patedb.patelog ORDER BY UID;"; MySqlCommand command = new MySqlCommand(timeStr, connection); try { MySqlDataAdapter adapter = new MySqlDataAdapter(); adapter.SelectCommand = command; DataTable dbdataset = new DataTable(); adapter.Fill(dbdataset); BindingSource bsource = new BindingSource(); bsource.DataSource = dbdataset; dataGridView1.DataSource = bsource; SetRowOrder(); adapter.Update(dbdataset); } catch (Exception ex) { MessageBox.Show(ex.Message); } }
private void btnRefreshValue_Click(object sender, EventArgs e) { string weightSort = "SELECT * FROM patedb.patelog ORDER BY CAST(weight AS SIGNED INTEGER);"; MySqlCommand command = new MySqlCommand(weightSort, connection); try { MySqlDataAdapter adapter = new MySqlDataAdapter(); adapter.SelectCommand = command; DataTable dbdataset = new DataTable(); adapter.Fill(dbdataset); BindingSource bsource = new BindingSource(); bsource.DataSource = dbdataset; dataGridView1.DataSource = bsource; SetRowOrder(); adapter.Update(dbdataset); } catch (Exception ex) { MessageBox.Show(ex.Message); } }
private void ChkNullBtn_Click(object sender, EventArgs e) { if (ProdRunning) { CheckTableForNull(); load_table(); } }
private void CheckTableForNull() { //Check/set timerinterval minimum 100 ms int i; int.TryParse(textTimer1.Text, out i); if (i < 100) { timer1.Stop(); i = 100; timer1.Interval = i; MessageBox.Show("Minimum value i 100mS"); timer1.Start(); } else { timer1.Interval = i; } textTimer1.Text = timer1.Interval.ToString(); //Check if any rows with null available in table, returns number of rows in variable :NumberOfRows string weightStr = ""; string chkNull = "SELECT COUNT(*) FROM patedb.patelog WHERE rangeNR IS NULL ORDER BY pateID LIMIT 1;"; MySqlCommand command = new MySqlCommand(chkNull, connection); try { connection.Open(); NumberOfRows = Convert.ToInt32(command.ExecuteScalar()); connection.Close(); } catch (Exception ex) { MessageBox.Show(ex.Message); } finally { if (NumberOfRows != 0) { try { //Selects lowest pateID number where rangeNr is NULL string readID = "SELECT pateID FROM patedb.patelog WHERE rangeNR IS NULL ORDER BY pateID ASC LIMIT 1;"; MySqlCommand cmdID = new MySqlCommand(readID, connection); { connection.Open(); RowNumber = (int)cmdID.ExecuteScalar(); //integer!! connection.Close(); } listPateID.Text = RowNumber.ToString(); // read out selected PateID number // Selects weight from selected rownumber string row = RowNumber.ToString(); string readweight = "SELECT weight FROM patedb.patelog WHERE pateID=" + row; MySqlCommand cmdweight = new MySqlCommand(readweight, connection); { connection.Open(); weightStr = (string)cmdweight.ExecuteScalar(); //String !! connection.Close(); } weight = int.Parse(weightStr); // convert to int txtWeight.Text = weight.ToString(); // print int RangeNumber = 0; if (weight >= limits[0] && weight <=limits[1]) { RangeNumber = 1; } if (RangeNumber == 0) { if (weight >= limits[2] && weight <= limits[3]) { RangeNumber = 2; } } if (RangeNumber == 0) { if (weight >= limits[4] && weight <= limits[5]) { RangeNumber = 3; } } txtRange.Text = RangeNumber.ToString(); UpdateLog(); } catch (Exception ex) { MessageBox.Show(ex.Message); } QuantProduced = QuantProduced + 1; } } } private void btnStart_Click(object sender, EventArgs e) { if (ProdRunning == false) { int valtest; try { CustomerID = int.Parse(txtCustomerNr.Text); //read customerID } catch { MessageBox.Show("Enter production data and press 'start' button."); }
string test = "SELECT COUNT(*) FROM patedb.customertable WHERE customerID ="+CustomerID; MySqlCommand cmdtestcustomer = new MySqlCommand(test, connection); { connection.Open(); valtest = Convert.ToInt32(cmdtestcustomer.ExecuteScalar()); // returns 0 if customer does not exist connection.Close(); } if (valtest==1) // if customer exist in database - start production { try { OrderNr = int.Parse(txtOrderNumber.Text); ProdRunning = true; timer1.Start(); textTimer1.Text = timer1.Interval.ToString(); ReadLimits(); } catch (Exception ex) { MessageBox.Show("Enter production data and press 'start' button."); } } else MessageBox.Show("Customer not in database, try again"); } //ReadLimits(); }
private void ReadLimits() { // Reads limits from rangetable, range 1 to 3 int counter = 0; for (int rangeNr = 1; rangeNr < 4; rangeNr++) { string readmin = "SELECT rangeMin FROM patedb.rangetable WHERE rangeNr=" + rangeNr; MySqlCommand cmdmin = new MySqlCommand(readmin, connection); { connection.Open(); limits[counter] = (int)cmdmin.ExecuteScalar(); counter = counter + 1; connection.Close(); } // MessageBox.Show(counter.ToString()); string readmax = "SELECT rangeMax FROM patedb.rangetable WHERE rangeNr=" + rangeNr; MySqlCommand cmdmax = new MySqlCommand(readmax, connection); { connection.Open(); limits[counter] = (int)cmdmax.ExecuteScalar(); counter = counter + 1; connection.Close(); } } // end for loop }
private void UpdateLog() { // UPDATE rangeNR & orderID string Range = RangeNumber.ToString(); string Order = OrderNr.ToString(); string update = "UPDATE patedb.patelog SET rangeNr= "+Range+',' + "orderID= "+OrderNr+" WHERE pateID="+RowNumber; MySqlCommand updatecmd = new MySqlCommand(update, connection); try { connection.Open(); updatecmd.ExecuteNonQuery(); connection.Close(); } catch (Exception ex) { MessageBox.Show(ex.Message); } }
private void btnStop_Click(object sender, EventArgs e) { if (ProdRunning == true) { timer1.Stop(); ProdRunning = false; UpdateOrderTable(); } else { MessageBox.Show("No production started yet. Enter data and press 'start' button"); } }
private void UpdateOrderTable() { string insert = "INSERT INTO patedb.ordertable (orderID, orderQuant, quantProduced, comment ,customerID) VALUES ('" + this.txtOrderNumber.Text + "','" + this.txtOrderQuant.Text + "','"+QuantProduced.ToString()+"','"+this.txtComment.Text+"','"+this.txtCustomerNr.Text+"');"; MySqlCommand insertcmd = new MySqlCommand(insert, connection); try { connection.Open(); insertcmd.ExecuteNonQuery(); connection.Close(); QuantProduced = 0; } catch (Exception ex) { MessageBox.Show(ex.Message); } }
private void timer1_Tick(object sender, EventArgs e) { CheckTableForNull(); load_table(); }
private void btnShowOrderTable_Click(object sender, EventArgs e) { if (ProdRunning == false) { MySqlCommand command = new MySqlCommand("SELECT * FROM patedb.ordertable ORDER BY orderID DESC;", connection); try { MySqlDataAdapter adapter = new MySqlDataAdapter(); adapter.SelectCommand = command; DataTable dbdataset = new DataTable(); adapter.Fill(dbdataset); BindingSource bsource = new BindingSource(); bsource.DataSource = dbdataset; dataGridView1.DataSource = bsource; adapter.Update(dbdataset); } catch (Exception ex) { MessageBox.Show(ex.Message); } } else { MessageBox.Show("Press stop to wiev orderTable"); } }
private void btnShowOrderDetails_Click(object sender, EventArgs e) { if (ProdRunning == false) { string test = ("SELECT patedb.ordertable.orderID, orderQuant, quantProduced, comment, customerID FROM patedb.ordertable INNER JOIN patedb.patelog ON patedb.patelog.orderID= patedb.ordertable.orderID WHERE patedb.patelog.UID = '" + txtShowOrderDetails.Text + "'"); MySqlCommand command = new MySqlCommand(test, connection); try { connection.Open(); MySqlDataAdapter adapter = new MySqlDataAdapter(); adapter.SelectCommand = command; DataTable dbdataset = new DataTable(); adapter.Fill(dbdataset); BindingSource bsource = new BindingSource(); bsource.DataSource = dbdataset; dataGridView1.DataSource = bsource; adapter.Update(dbdataset); } catch (Exception ex) { MessageBox.Show(ex.Message); } connection.Close(); } else { MessageBox.Show("Press stop to view order details"); } }
private void btnShowCustomerDetails_Click(object sender, EventArgs e) { if (ProdRunning == false) { string test = ("SELECT patedb.customertable.customerID, name, address, phone, email, rangeNr FROM patedb.customertable INNER JOIN patedb.ordertable ON patedb.ordertable.customerID= patedb.customertable.customerID WHERE patedb.ordertable.orderID = '" + txtShowCustomerDetails.Text + "'"); MySqlCommand command = new MySqlCommand(test, connection); try { MySqlDataAdapter adapter = new MySqlDataAdapter(); adapter.SelectCommand = command; DataTable dbdataset = new DataTable(); adapter.Fill(dbdataset); BindingSource bsource = new BindingSource(); bsource.DataSource = dbdataset; dataGridView1.DataSource = bsource; adapter.Update(dbdataset); } catch (Exception ex) { MessageBox.Show(ex.Message); } } else { MessageBox.Show("Press stop to wiev customer details"); } } }
}