EAL-Industri4.0-RFID Dataopsamling Til Database

417

7

1

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");
            }
        }
    }
 }

Step 10:

Share

    Recommendations

    • Metalworking Contest

      Metalworking Contest
    • Tiny Home Contest

      Tiny Home Contest
    • Fix It! Contest

      Fix It! Contest

    Discussions

    Godt stykke arbejde :)