From d3fe34b6ec849697e4b03a76ce08fc9499e5692c Mon Sep 17 00:00:00 2001 From: Evangelos Oulis Date: Wed, 15 Jan 2020 09:49:05 +0200 Subject: [PATCH] Update v1.3 --- project.adoc | 89 +++++++++++++++++++++++++------------ project.html | 44 ++++++++++-------- serverNode/DB.sql | 7 +++ serverNode/Procfile | 1 + serverNode/requirements.txt | 3 +- serverNode/serv.py | 64 +++++++++++++++++++++++--- webInterface/parking.html | 73 +++++++++++++++++++++++++----- 7 files changed, 218 insertions(+), 63 deletions(-) create mode 100644 serverNode/DB.sql create mode 100644 serverNode/Procfile diff --git a/project.adoc b/project.adoc index 5ff1af9..8ca0426 100644 --- a/project.adoc +++ b/project.adoc @@ -25,30 +25,29 @@ == Smart Parking Το "Smart Parking" Έξυπνο πάρκινγκ βασίζεται στη διαδικασία όπου η κατάσταση του πάρκινγκ κοινοποιείται -μέσω ενός συνόλου hardware και software στο διαδίκτυο και έτσι πετυχαίνουμε η κατάσταση του να είναι διαθέσιμη -"accesable" από το διαδίκτυο. Αυτό το χαρακτηριστικό κάνει αυτό το αντικειμένο μέρος του διαδικτύου και του κόσμου IoT. +μέσω ενός συνόλου hardware και software στο διαδίκτυο έτσι ώστε να πετυχουμε η κατάσταση του να είναι διαθέσιμη +"accesable" από το διαδίκτυο. Αυτό το χαρακτηριστικό κάνει αυτό το αντικειμένο μέρος του διαδικτύου και του κόσμου του IoT. -*Η υλοποίηση του Smart Parking χωρίζεται σε 3 βασικά μέρη:* +*Η υλοποίηση του Smart Parking χωρίζεται σε 4 βασικά μέρη:* -* Το 1~ο~ μέρος αποτελείται από ένα σύνολο αισθητήρων που εγκαθιστούντε σε κάθε θέση parking (sensor), τα οποία αποτελούνται -από έναν αισθητήρα μέτρησης απόστασης (ultrasonic) και έναν μικροελεγκτή (Arduino Uno), έτσι ώστε να ανιχνεύει και να κωδικοποιεί -για μία συγκεκριμένη θέση έαν υπάρχει κάποιο όχημα ή όχι. +* Το 1~ο~ μέρος αποτελείται από ένα σύνολο αισθητήρων (ultrasonic) που εγκαθιστούντε σε κάθε θέση parking (sensor) και +έναν μικροελεγκτή (Arduino Uno), έτσι ώστε να ανιχνεύει και να κωδικοποιεί για μία συγκεκριμένη θέση έαν υπάρχει +κάποιο όχημα ή όχι. -* Το 2~ο~ μέρος αποτελείται από την συσκευή gateway σε Raspberry Pi1, η οποία διαβάζει στη σειριακή του -τη πληροφορία από το Arduino Uno, που κάνει sense μία θέση parking, και στέλνει αυτή την πληροφορία +* Το 2~ο~ μέρος αποτελείται από τον συσκευή gateway σε Raspberry Pi1, η οποία διαβάζει στη σειριακή του +τη πληροφορία από το Arduino Uno, που κάνει "sense" μία θέση parking, και στέλνει αυτή την πληροφορία σε έναν web server με χρήση REST API. -* To 3~o~ μέρος αποτελείται από τον WEB Server ο οποίος αποτελείται από ένα process γραμμένο σε python που υλοποιεί -ένα REST API έτσι ώστε να μπορεί να αποθηκεύει την κατάσταση κάθε θέσης parking σε μία δομή λίστας με χαρακτηριστικό +* To 3~o~ μέρος αποτελείται από τον WEB Server ο οποίος αποτελείται από ένα process γραμμένο σε python. Το proccess αυτό υλοποιεί +έναν REST API WEB Server έτσι ώστε να μπορεί να αποθηκεύει την κατάσταση κάθε θέσης parking σε μία δομή λίστας με χαρακτηριστικό κλειδί τον κωδικό κάθε θέσης parking. -* Το 4~o~ μέρος αποτελείται από την διεπαφή (Interface), με την οποία μέσω web σελίδας βλέπει κανείς την κατάσταση +* Το 4~o~ μέρος αποτελείται από την διεπαφή χρήστη (Interface), η οποία μέσω WEB σελίδας βλέπει κανείς την κατάσταση του parking, δηλαδή πόσες και ποιές θέσεις μέσα στον χώρο είναι ελεύθερες. -=== Parking Sensor Node +=== Parking Sensor Node (1~ο~ μέρος) ==== Υλικά Κόμβου - * 1 x Bread-Board * 1 x Arduino Uno * 1 x Red led * 1 x Blue led @@ -56,10 +55,10 @@ * 1 x Ultrasonic Sensor ==== Υλοποίηση του Parking Sensor -Σε κάθε θέση parking υπάρχει ένας κόμβος απότελούμενος από ένα Arduino και έναν αισθητήρα -απόστασης (ultrasonic) εγκατεστημένος πάνω στο Arduino. Η λειτουργία αυτού βασίζεται στη -μέτρηση της απόστασης από τον κόμβο μέχρι κάποιο αντικείμενο (αυτοκίνητο) που εμποδίζει τη θέση του parking -και την εξαγωγή της κατάσταασης της αυτής της θέσης στη σειριακή θύρα του Arduino. +Σε κάθε θέση parking υπάρχει ένας κόμβος που αποτελείται από ένα Arduino Uno και έναν αισθητήρα +απόστασης (ultrasonic) εγκατεστημένος πάνω σε μικροελεγκτή Arduino Uno. Η λειτουργία αυτού βασίζεται στη +μέτρηση της απόστασης από τον κόμβο μέχρι κάποιο αντικείμενο (πιθανό αυτοκίνητο) που εμποδίζει τη θέση του parking, +καθώς και την εξαγωγή της κατάσταασης αντίστοιχης θέσης στη σειριακή θύρα του Arduino. [.float-group] -- @@ -87,9 +86,10 @@ image::Photos/arduino2.jpg[300,200] ==== Διασύνδεση Κόμβου Ο κόμβος αυτός συνδέεται με ένας "Gateway" κόμβο (βασισμένος σε Raspberry Pi) ο οποίος είναι υπεύθυνος για την μετάδοση της πληροφορίας που αφορά την διαθεσιμότητα της θέσης του parking στο διαδίκτυο. Η πληροφορία αυτή -λαμβάνεται στον "Gateway" κόμβο οποίος στη συνέχεια την αποκωδικοποιεί και την αποστέλει στον WEB server μέσω του διαδικτύου. +λαμβάνεται στον "Gateway" κόμβο ο οποίος στη συνέχεια την αποκωδικοποιεί και την αποστέλει στον WEB server μέσω του διαδικτύου. -=== Gateway Node + +=== Gateway Node (2~ο~ μέρος) ==== Υλικά Κόμβου * 1 x Raspberry Pi 1 @@ -100,19 +100,29 @@ image::Photos/arduino2.jpg[300,200] ==== Υλοποίηση και Προγραμματισμός Η υλοποίηση αποτελείται από την εγκατάσταση του Raspbian OS στο Raspberry και τη δημιουργία ενός proccess -σε γλώσσα python το οποίο διαβάζει από την σειριακή θύρα του την πληροφορία που λαμβάνει από το Arduino με +σε γλώσσα Python. Το process αυτό διαβάζει από την σειριακή θύρα του την πληροφορία που λαμβάνει από το αντίστοιχο Arduino Uno με την μορφή <κωδικός θέσης>#<διαθεσιμότητα 0 ή 1>. Ύστερα αποκωδικοποιεί αυτή την πληροφορία η οποία περιγράφει τον κωδικό της θέσης -και την διαθεσιμότητα της και την αποστέλει μέσω ενός REST API με την μέθοδο POST σε έναν WEB Server. +και την διαθεσιμότητα της και την αποστέλει μέσω ενός REST API με την μέθοδο POST σε έναν WEB Server. Τα δεδομέμα μας σε αυτήν +την επικοινωνία παίρνουν μία μορφή JSON (JavaScript Object Notation). ==== Διασύνδεηση στο Διαδίκτυο Ο κόμβος Gateway έχει διασύνδεση με το διαδίκτυο μέσω ενός καλωδίου Ethernet (UTP) έτσι ώστε να μπορέσει -να στείλει την πληροφορία +να στείλει την πληροφορία στο διαδίκτυο. + + +== Server Node (3~ο~ μέρος - Κεντρικός Server όπου κρατά την κατάσταση της κάθε θέσης του Parking) +Ο κόμβος αυτός υλοποιεί ένα process γραμμένο σε γλώσσα προγραμματισμού Python 3. Αυτό το process εκτελεί ένα +REST API έτσι ώστε να μπορούν να επικοινωνούν εύκολα πολλοί Gateway κόμβοι. Στην είσοδό του και στην έξοδό του +τα δεδομένα μας έχουν την μορφή JSON. -== Server Node (Κεντρικός Server όπου κρατά την κατάσταση της κάθε θέσης του Parking) -Ο κόμβος αυτός υλοποιεί ένα process γραμμένο σε γλώσσα προγραμματισμού Python 3. +Ο server αποθηκεύει όλα τα απαραίτητα δεδομένα σε μία Βάση δεδομένων MySQL, η οποία διαθέτει ένα πίνακα. +Ο πίνακας κρατά όλα τα απαραίτητα πεδία που είναι: + + * Τον κωδικό της θέσης parking + * Την διαθεσιμότητά της αντίστοιχης θέση (0 ή 1) === Εκτέλεση του Process στο Cloud -Για την εκτέλεση του process χρησιμοποιούμε την πλατφόρμα IAAS (Infrastructure as a Service) +Για την εκτέλεση του process χρησιμοποιούμε μία πλατφόρμα IAAS (Infrastructure as a Service) ονόματι link:++https://www.heroku.com/platform++[Heroku], για την οποιά μπορούμε να βρούμε περεταίρω πληροφορίες στον σύνδεσμο παραπάνω. @@ -124,12 +134,35 @@ image::Photos/itops-pizza_as_a_service.png[1000,800] -- Για την διαδικασία του deployment εκτελούμε ένα σύνολο βημάτων τα οποία αποτελούνται από την αντιγραφή του κώδικα -σε ένα reposetory του link:++https://github.com/++[GitHub] και την δημιουργία ενός project στην πλατφόρμα για το -τρέξιμο του process. link:++https://stackabuse.com/deploying-a-flask-application-to-heroku/++[περισσότερα] +σε ένα reposetory του link:++https://github.com/oulievancs/serverNode++[GitHub] και την δημιουργία ενός project στην πλατφόρμα για το +τρέξιμο του process. link:++https://stackabuse.com/deploying-a-flask-application-to-heroku/++[περισσότερα]. Ακόμα +εγκαθισούμε στο project που μόλις φτιάξαμε μία MySQL βάση δεδομένων για να μπορούμε να αποθηκεύσουμε τα δεδομένα μας. +==== Deployment * Το πρώτο πράγμα που χρειαζόμαστε είναι όλες οι απαραίτητες βιβλιοθήκες που χρησιμοποιεί ο κώδικας, έτσι ώστε να γνωρίζει το Heroku τι να μας προσφέρει. Αυτό επιτυγχάνεται με την αρχειοθέτηση αυτών σε ένα αρχείο -ονόματι re +ονόματι requirements.txt . + +* Έπειτα την δημιουργία ενός αρχείου που περιγράφει το που βρίσκεται η κύρια συνέρτησή μας (main) για την +έναρξη του process. Αυτό το αρχείο ονομάζεται Procfile . Στο αρχείο αυτό αναφέτεται ένα gunicorn module. +Ο gunicorn είναι ένας Python HTTP WEB server. Αυτό ουσιαστικά είναι ο ο πυρήνας για την εκτέλεση του API μας. + +* Έπειτα με μια απομακρυσμένη σύνδεση στη βάση μας της οποίας τα στοιχεία πρόσβασης γαίνονται στο Heroku, +πραγματοποιούμε μία σύνδεση και δημιουργούμε τον πίνακά μας για την αποθήκευση. + +==== REST API +Το REST API ουσιαστικά σηκώνει δύο υπηρεσίες. Αυτές είναι: + +* / [GET]: που πας επιστρέφει για κάθε θέση του parking αν είναι διαθέσιμη ή όχι κωδικοποιημένα με 0 ή 1. +Στο response τα δεδομένα μας παίρνουν μορφή JSON. Τα δεδομένα που επιστρέφει γίνονται fetch από τη βάση δεδομένων. + +* /parkingStatus [POST]: που μας επιτρέπει να αλλάξουμε την κατάσταση μίας θέσης parking. Το POST των δεδομένων +στο body γίνεται με την JSON αναπαράστασή τους έτσι ώστε να μπορέσει ο Server να τα επεξεργαστεί, ο οποίος στη +συνέχεια αποθηκεύει την νέα θέση στη Βάση δεδομένων. + + +== Διεπαφή Χρήστη (4~ο~ μέρος) +Η διεπαφή του χρήσρη π == Autonomous Parking diff --git a/project.html b/project.html index 5911191..84f263d 100644 --- a/project.html +++ b/project.html @@ -473,45 +473,42 @@ body.book #toc,body.book #preamble,body.book h1.sect0,body.book .sect1>h2{page-b

Το "Smart Parking" Έξυπνο πάρκινγκ βασίζεται στη διαδικασία όπου η κατάσταση του πάρκινγκ κοινοποιείται -μέσω ενός συνόλου hardware και software στο διαδίκτυο και έτσι πετυχαίνουμε η κατάσταση του να είναι διαθέσιμη -"accesable" από το διαδίκτυο. Αυτό το χαρακτηριστικό κάνει αυτό το αντικειμένο μέρος του διαδικτύου και του κόσμου IoT.

+μέσω ενός συνόλου hardware και software στο διαδίκτυο έτσι ώστε να πετυχουμε η κατάσταση του να είναι διαθέσιμη +"accesable" από το διαδίκτυο. Αυτό το χαρακτηριστικό κάνει αυτό το αντικειμένο μέρος του διαδικτύου και του κόσμου του IoT.

-

Η υλοποίηση του Smart Parking χωρίζεται σε 3 βασικά μέρη:

+

Η υλοποίηση του Smart Parking χωρίζεται σε 4 βασικά μέρη:

  • -

    Το 1ο μέρος αποτελείται από ένα σύνολο αισθητήρων που εγκαθιστούντε σε κάθε θέση parking (sensor), τα οποία αποτελούνται -από έναν αισθητήρα μέτρησης απόστασης (ultrasonic) και έναν μικροελεγκτή (Arduino Uno), έτσι ώστε να ανιχνεύει και να κωδικοποιεί -για μία συγκεκριμένη θέση έαν υπάρχει κάποιο όχημα ή όχι.

    +

    Το 1ο μέρος αποτελείται από ένα σύνολο αισθητήρων (ultrasonic) που εγκαθιστούντε σε κάθε θέση parking (sensor) και +έναν μικροελεγκτή (Arduino Uno), έτσι ώστε να ανιχνεύει και να κωδικοποιεί για μία συγκεκριμένη θέση έαν υπάρχει +κάποιο όχημα ή όχι.

  • -

    Το 2ο μέρος αποτελείται από την συσκευή gateway σε Raspberry Pi1, η οποία διαβάζει στη σειριακή του -τη πληροφορία από το Arduino Uno, που κάνει sense μία θέση parking, και στέλνει αυτή την πληροφορία +

    Το 2ο μέρος αποτελείται από τον συσκευή gateway σε Raspberry Pi1, η οποία διαβάζει στη σειριακή του +τη πληροφορία από το Arduino Uno, που κάνει "sense" μία θέση parking, και στέλνει αυτή την πληροφορία σε έναν web server με χρήση REST API.

  • -

    To 3o μέρος αποτελείται από τον WEB Server ο οποίος αποτελείται από ένα process γραμμένο σε python που υλοποιεί -ένα REST API έτσι ώστε να μπορεί να αποθηκεύει την κατάσταση κάθε θέσης parking σε μία δομή λίστας με χαρακτηριστικό +

    To 3o μέρος αποτελείται από τον WEB Server ο οποίος αποτελείται από ένα process γραμμένο σε python. Το proccess αυτό υλοποιεί +έναν REST API WEB Server έτσι ώστε να μπορεί να αποθηκεύει την κατάσταση κάθε θέσης parking σε μία δομή λίστας με χαρακτηριστικό κλειδί τον κωδικό κάθε θέσης parking.

  • -

    Το 4o μέρος αποτελείται από την διεπαφή (Interface), με την οποία μέσω web σελίδας βλέπει κανείς την κατάσταση +

    Το 4o μέρος αποτελείται από την διεπαφή χρήστη (Interface), η οποία μέσω WEB σελίδας βλέπει κανείς την κατάσταση του parking, δηλαδή πόσες και ποιές θέσεις μέσα στον χώρο είναι ελεύθερες.

-

1.1. Parking Sensor Node

+

1.1. Parking Sensor Node (1ο μέρος)

1.1.1. Υλικά Κόμβου

  • -

    1 x Bread-Board

    -
  • -
  • 1 x Arduino Uno

  • @@ -642,12 +639,23 @@ body.book #toc,body.book #preamble,body.book h1.sect0,body.book .sect1>h2{page-b
    1000
    -
    Figure 3. Συνδεσμολογία moter, motor driver, arduino, ultrasonic sensor και servo motor.
    +
    Figure 3. Describe Infrastructure as a Service as an example.
-

Για την διαδικασία του deployment εκτελούμε ένα σύνολο βημάτων τα οποία αποτελούνται από

+

Για την διαδικασία του deployment εκτελούμε ένα σύνολο βημάτων τα οποία αποτελούνται από την αντιγραφή του κώδικα +σε ένα reposetory του GitHub και την δημιουργία ενός project στην πλατφόρμα για το +τρέξιμο του process. περισσότερα

+
+
+
    +
  • +

    Το πρώτο πράγμα που χρειαζόμαστε είναι όλες οι απαραίτητες βιβλιοθήκες που χρησιμοποιεί ο κώδικας, έτσι +ώστε να γνωρίζει το Heroku τι να μας προσφέρει. Αυτό επιτυγχάνεται με την αρχειοθέτηση αυτών σε ένα αρχείο +ονόματι re

    +
  • +
@@ -752,7 +760,7 @@ SLOW SUCCESS BUILDS CHARACTER, FAST SUCCESS BUILDS EGO. diff --git a/serverNode/DB.sql b/serverNode/DB.sql new file mode 100644 index 0000000..61a2b60 --- /dev/null +++ b/serverNode/DB.sql @@ -0,0 +1,7 @@ +DROP TABLE IF EXISTS PARKING; + +CREATE TABLE IF NOT EXISTS PARKING ( + PARKING_CODE INT(4) NOT NULL UNIQUE, + PARKING_STATUS BOOLEAN NOT NULL +); + diff --git a/serverNode/Procfile b/serverNode/Procfile new file mode 100644 index 0000000..60ee360 --- /dev/null +++ b/serverNode/Procfile @@ -0,0 +1 @@ +web: gunicorn serv:app --preload --timeout 150000 diff --git a/serverNode/requirements.txt b/serverNode/requirements.txt index 17e33d4..8ade642 100644 --- a/serverNode/requirements.txt +++ b/serverNode/requirements.txt @@ -1,4 +1,5 @@ flask flask_restful flask_cors - +gunicorn==19.9.0 +mysql-connector diff --git a/serverNode/serv.py b/serverNode/serv.py index 3f4be91..778f532 100644 --- a/serverNode/serv.py +++ b/serverNode/serv.py @@ -6,6 +6,7 @@ from flask_restful import Resource, Api from json import dumps import json from flask_cors import CORS +import mysql.connector # ================================================================== # ================================================================== @@ -17,7 +18,29 @@ CORS(app) # creating an API object api = Api(app) -parks = dict() +# Initialize the database Connection +mydb = mysql.connector.connect( + host = "q2gen47hi68k1yrb.chr7pe7iynqr.eu-west-1.rds.amazonaws.com", + user = "zsgmj50h7zgz9ioq", + password = "omk5l1hrwsgvlcez", + database = "g0s9cnmdkziq6fsp" +) + +myCursor = mydb.cursor() + +# ================================== +# Define our functions. +# Define a function that gets the parking status +# for all parking codes. +def getParkings(): + parks = dict() + + myCursor.execute("SELECT * FROM PARKING") + myRes = myCursor.fetchall() + + for res in myRes: + parks[res[0]] = res[1] + return parks # ================================================================== # making a class for a particular resource @@ -26,6 +49,7 @@ parks = dict() # other methods include put, delete, etc. class Parking(Resource): def get(self): + parks = getParkings() return parks, 200 class ParkingStatus(Resource): @@ -35,15 +59,43 @@ class ParkingStatus(Resource):

Not get at '/parkingStatus'.

""" def post(self): - print (request) + # Gets the data into a JSON Object. data = json.loads(request.data) - print (data) - parks[data['no']] = data['status'] + + # SQL get all Parking places status. + parks = getParkings() + + thereIs = False + toUpdate = False + try: + if parks[int(data['no'])] != int(data['status']): + toUpdate = True + thereIs = True + except IndexError: + # handle Index Error + thereIs = False + except KeyError: + # handle the KeyError + thereIs = False + + if not thereIs: + # Make a new insert entry for a new Parking Code. + values = (int(data['no']), int(data['status'])) + myCursor.execute("INSERT INTO PARKING (PARKING_CODE, PARKING_STATUS) VALUES (%s, %s)", values) + mydb.commit() + parks = getParkings() + elif toUpdate: + # Make an Update status for Parking Code that availability changed. + values = (int(data['status']), int(data['no'])) + myCursor.execute("UPDATE PARKING SET PARKING_STATUS=%s WHERE PARKING_CODE=%s", values) + mydb.commit() + parks = getParkings() + return parks[data['no']], 201 # ================================================================== -# adding the defined resources along with their corresponding urls +# adding the defined resources along with their corresponding urls to REST APIs api.add_resource(Parking, '/') api.add_resource(ParkingStatus, '/parkingStatus') @@ -53,7 +105,7 @@ if __name__ == '__main__': app.run( debug=True, host=app.config.get("HOST", "0.0.0.0"), - port=app.config.get("PORT", "8080") + port=app.config.get("PORT", "5000") ) # END diff --git a/webInterface/parking.html b/webInterface/parking.html index 9c3d809..7ffe4a6 100644 --- a/webInterface/parking.html +++ b/webInterface/parking.html @@ -21,6 +21,54 @@ $(document).ready(function(){ if (server_ip !== "" && server_ip !== null && server_port !== "" && server_port !== null && server_protocol !== "" && server_protocol != null) { $("#msg").html("ok!"); + $("#approve").prop("disabled", true); + jQuery.ajax({ + url: server_protocol + "://" + server_ip + ":" + server_port + "/", + type: "GET", + + contentType: "application/json; charset=utf-8", + success: function(resultData) { + row = 1, pos = "r"; + $("talbe#2").append(""); + $("tr#row0").append("
"); + $("tr#row0").append("
"); + $("tr#row0").append("
"); + $("tr#row0").append("
"); + $.each(resultData, function(key, val) { + console.log("Key " + key + ", val: " + val); + content = ""; + if (val == 0) { + if (pos == "l") { + content = "No" + key + "
"; + } else { + content = "No" + key + "
"; + } + } else { + if (pos == "l") { + content = "No" + key + "
"; + } + else { + content = "No" + key + "
"; + } + } + if (pos == "r") { + $("table#tab2").append(""); + $("tr#row" + row).append(content); + $("tr#row" + row).append("
"); + pos = "l"; + } else if (pos == "l") { + $("tr#row" + row).append("
"); + $("tr#row" + row).append(content); + row++; + pos = "r"; + } + }); + }, + error: function(jqXHR, testStatus, errorThrown) { + }, + + timeout: 120000, + }); setInterval(function () { jQuery.ajax({ url: server_protocol + "://" + server_ip + ":" + server_port + "/", @@ -31,15 +79,15 @@ $(document).ready(function(){ $.each(resultData, function(key, val) { console.log(key + " -> " + val); if (val == "1") - $("#" + key).removeClass("full").addClass("empty"); + $("div#" + key).removeClass("full").addClass("empty"); else if (val == "0") - $("#" + key).removeClass("empty").addClass("full"); + $("div#" + key).removeClass("empty").addClass("full"); }); }, error: function(jqXHR, testStatus, errorThrown) { }, - timeout: 12000, + timeout: 120000, }); }, 1000); } else { @@ -47,37 +95,42 @@ $(document).ready(function(){ } }); }); + +

Parking

Server Settings

- + +
- + - +
Server Protocol:
Server IP:
Server PORT:
+

Parking Diagram

- - +
+