Rhum Service

Connected cocktail machine using RFID badges linked to an online account to process customer orders. Data processing via LoRaWAN and Python.

IntermediateShowcase (no instructions)Over 2 days72
Rhum Service

Things used in this project

Hardware components

SODAQ ExpLoRer
This board is an Arduino Zero with an integrated RN2483 LoRa module and more!
×1
Breadboard (generic)
Breadboard (generic)
Useful for wiring all components, given the SODAQ's limited number of pins.
×1
RFID reader (generic)
To scan the badge UID.
×1
Standard LCD - 16x2 White on Blue
Adafruit Standard LCD - 16x2 White on Blue
To give customers a visual feedback.
×1
FS90R Micro-servo motor
To distribute the peanuts.
×1
HX711 load cell amplifier module
To measure tank weights to estimate remaining quantity.
×3
Air pump motor DC 6V (Generic)
To dispense the beverage by "pushing" the liquid with air blown from the motors.
×3
Relay (generic)
To control air pump motors with the SODAQ board.
×3
Big Red Dome Button
SparkFun Big Red Dome Button
To activate the peanut dispenser, and read the UID of a badge.
×2

Software apps and online services

VS Code
Microsoft VS Code
The only code editor you really need.
PlatformIO Core
PlatformIO Core
Extension for VSCode - adds an IDE for embedded programming.
The Things Stack
The Things Industries The Things Stack
To receive and transmit data using our LoRa module.

Hand tools and fabrication machines

Laser cutter (generic)
Laser cutter (generic)
3D Printer (generic)
3D Printer (generic)
Tape, Scotch
Tape, Scotch

Story

Read more

Custom parts and enclosures

Pump holder

Sketchfab still processing.

RFID base

Sketchfab still processing.

Pipe holder

Sketchfab still processing.

Angle

Sketchfab still processing.

Container - spiral home

Sketchfab still processing.

Container - funnel

Sketchfab still processing.

Cup box

Sketchfab still processing.

Propeller - spiral

Sketchfab still processing.

Schematics

Pin definitions

Code

main.cpp

C/C++
main code file for the SODAQ ExpLoRer board
#include <Arduino.h>
#include <Sodaq_RN2483.h>
#include <Utils.h>
#include <SPI.h>
#include <MFRC522.h>
#include <HX711.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <Servo.h>

const uint8_t devEUI[8] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
const uint8_t appEUI[8] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
const uint8_t appKey[16] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};

MFRC522 mfrc522(RFID_SDA_PIN, RFID_RST_PIN);

HX711 loadcell1, loadcell2, loadcell3;

LiquidCrystal_I2C lcd(LCD_ADDRESS, 16, 2);

Servo servodist;

int poidsCapteurs[3] = {0, 0, 0};

void distribPeanuts()
{
	lcd.clear();
	lcd.setCursor(0, 0);
	lcd.print("Distribution de");
	lcd.setCursor(0, 1);
	lcd.print("cacahuetes...");
	DEBUGLN("Peanut distribution is happening...");
	servodist.attach(SERVO_PEANUT_PIN);
	delay(2000);
	servodist.detach();
	DEBUGLN("Peanut distribution complete.");
}

void checkUID()
{
	lcd.clear();
	lcd.setCursor(0, 0);
	lcd.print("Presentez votre");
	lcd.setCursor(0, 1);
	lcd.print("badge");

	while (!mfrc522.PICC_IsNewCardPresent() || !mfrc522.PICC_ReadCardSerial())
		;

	lcd.clear();
	lcd.setCursor(0, 0);
	lcd.print("UID Verre");
	lcd.setCursor(0, 1);

	for (byte i = 0; i < 4; i++)
		lcd.print(mfrc522.uid.uidByte[i], HEX);

	delay(10000);
}

void scanI2C()
{
	Wire.begin();
	DEBUGLN("-- I2C Scanner --");
	for (byte address = 1; address < 127; address++)
	{
		Wire.beginTransmission(address);
		if (Wire.endTransmission() == 0)
		{
			DEBUG("I2C device found at address 0x");
			debugSerial.println(address, HEX);
			delay(1000);
		}
	}
}

void distribBoisson(int pourcentage1, int pourcentage2, int pourcentage3)
{
	int duree1 = map(pourcentage1, 0, 100, 0, 16000);
	int duree2 = map(pourcentage2, 0, 100, 0, 16000);
	int duree3 = map(pourcentage3, 0, 100, 0, 16000);

	unsigned long startTime = millis();

	while (millis() - startTime < max(duree1, max(duree2, duree3)))
	{
		if (millis() - startTime < duree1)
			digitalWrite(6, HIGH);
		else
			digitalWrite(6, LOW);

		if (millis() - startTime < duree2)
			digitalWrite(7, HIGH);
		else
			digitalWrite(7, LOW);

		if (millis() - startTime < duree3)
			digitalWrite(8, HIGH);
		else
			digitalWrite(8, LOW);
	}

	// Scurit
	for (int i = 6; i < 9; i++)
		digitalWrite(i, LOW);
}

void handleCocktailDownlink(byte downlinkData[], byte downlinkSize)
{
	// Vrifier que la taille du payload est bien de 4 octets
	if (downlinkSize == 4)
	{
		uint8_t percentA = downlinkData[0];
		uint8_t percentB = downlinkData[1];
		uint8_t percentC = downlinkData[2];
		uint8_t peanut = downlinkData[3];

		// Afficher la composition du cocktail sur l'cran
		lcd.clear();
		lcd.setCursor(0, 0);
		lcd.print("Cocktail mix");
		lcd.setCursor(0, 1);
		lcd.print("A:");
		lcd.print(percentA);
		lcd.print("%B:");
		lcd.print(percentB);
		lcd.print("%C:");
		lcd.print(percentC);
		lcd.print("%");
		distribBoisson(percentA, percentB, percentC);

		if (peanut)
		{
			delay(7000);
			distribPeanuts();
		}
	}
	else
	{
		// Si la taille du payload n'est pas celle attendue, afficher un message d'erreur
		lcd.clear();
		lcd.setCursor(0, 0);
		lcd.print("Cocktail mix");
		lcd.setCursor(0, 1);
		lcd.print("invalide (");
		lcd.print(downlinkSize);
		lcd.print(")");
		delay(5000);
	}
}

void sendData(byte payload[], byte payloadSize)
{
	LoRaBee.wakeUp();
	delay(500);

	int returnedMacCode = LoRaBee.send(1, payload, payloadSize);
	if (returnedMacCode == NoError)
	{
		DEBUGLN("UID and weight were sent successfully");

		delay(1000); // RX1
		byte downlinkData[64];
		byte downlinkSize = LoRaBee.receive(downlinkData, sizeof(downlinkData));
		if (downlinkSize > 0)
		{
			DEBUG("Downlink data received on RX1: ");
			for (byte i = 0; i < downlinkSize; i++)
			{
				debugSerial.print(downlinkData[i], HEX);
				debugSerial.print(" ");
			}
			DEBUGLN();

			// Traiter le payload cocktail
			handleCocktailDownlink(downlinkData, downlinkSize);
		}
		else
		{
			delay(1000); // RX2
			downlinkSize = LoRaBee.receive(downlinkData, sizeof(downlinkData));
			if (downlinkSize > 0)
			{
				DEBUG("Downlink data received on RX2: ");
				for (byte i = 0; i < downlinkSize; i++)
				{
					debugSerial.print(downlinkData[i], HEX);
					debugSerial.print(" ");
				}
				DEBUGLN();

				// Traiter le payload cocktail
				handleCocktailDownlink(downlinkData, downlinkSize);
			}
			else
			{
				lcd.clear();
				lcd.setCursor(0, 0);
				lcd.print("Aucun cocktail");
				lcd.setCursor(0, 1);
				lcd.print("en attente !");
				DEBUGLN("No downlink data received");
				delay(4000);
			}
		}
	}
	else
	{
		DEBUG("Failed to sent LoRa payload - Error code: ");
		DEBUGLN(returnedMacCode);
	}

	LoRaBee.sleep();
}

void getWeights(HX711 loadcell1, HX711 loadcell2, HX711 loadcell3, int poids[])
{
	// Capteur poids n1
	if (loadcell1.is_ready())
	{
		poids[0] = abs((int)loadcell1.get_units());
		if (poids[0] > 2500)
			poids[0] = 2500;
		DEBUG("Weight 1 : ");
		DEBUG(poids[0]);
		DEBUGLN(" g");
	}
	else
	{
		DEBUGLN("HX711 (1) read error");
	}

	delay(200);

	// Capteur poids n2
	if (loadcell2.is_ready())
	{
		poids[1] = abs((int)loadcell2.get_units());
		if (poids[1] > 2500)
			poids[1] = 2500;
		DEBUG("Weight 2 : ");
		DEBUG(poids[1]);
		DEBUGLN(" g");
	}
	else
	{
		DEBUGLN("HX711 (2) read error");
	}

	delay(200);

	// Capteur poids n3
	if (loadcell3.is_ready())
	{
		poids[2] = abs((int)loadcell3.get_units());
		if (poids[2] > 2500)
			poids[2] = 2500;
		DEBUG("Weight 3 : ");
		DEBUG(poids[2]);
		DEBUGLN(" g");
	}
	else
	{
		DEBUGLN("HX711 (3) read error");
	}
}

void setup()
{
	debugSerial.begin(57600);
	loraSerial.begin(LoRaBee.getDefaultBaudRate());
	SPI.begin();
	delay(3000);

	pinMode(BOUTON_UID, INPUT_PULLUP);
	pinMode(BOUTON_MORE_PEANUT, INPUT_PULLUP);

	DEBUGLN("Initializing LCD I2C");
	lcd.begin();
	delay(500);
	lcd.backlight();
	lcd.print("LCD OK");
	DEBUGLN("LCD I2C initialized");

	DEBUGLN("Initializing MFRC522");
	mfrc522.PCD_Init();
	delay(500);
	lcd.clear();
	lcd.print("MFRC522 OK");
	DEBUGLN("MFRC522 initialized");
	delay(500);

	lcd.clear();
	lcd.setCursor(0, 0);
	lcd.print("Liberez les");
	lcd.setCursor(0, 1);
	lcd.print("balances !");
	delay(5000);

	// HX711 (1)
	DEBUGLN("Initializing HX711 (1)");
	loadcell1.begin(LOADCELL1_DT_PIN, LOADCELL_SCK_PIN);
	delay(500);
	if (loadcell1.is_ready())
	{
		loadcell1.set_scale(398.5F);
		loadcell1.tare();
		lcd.clear();
		lcd.print("HX711 (1) OK");
		DEBUGLN("HX711 (1) initialized");
	}
	else
	{
		lcd.clear();
		lcd.print("HX711 (1) failed");
		DEBUGLN("Failed HX711 (1) initialization");
		while (true)
			;
	}

	// HX711 (2)
	DEBUGLN("Initializing HX711 (2)");
	loadcell2.begin(LOADCELL2_DT_PIN, LOADCELL_SCK_PIN);
	delay(500);
	if (loadcell2.is_ready())
	{
		loadcell2.set_scale(444.5F);
		loadcell2.tare();
		lcd.clear();
		lcd.print("HX711 (2) OK");
		DEBUGLN("HX711 (2) initialized");
	}
	else
	{
		lcd.clear();
		lcd.print("HX711 (2) failed");
		DEBUGLN("Failed HX711 (2) initialization");
		while (true)
			;
	}

	// HX711 (3)
	DEBUGLN("Initializing HX711 (3)");
	loadcell3.begin(LOADCELL3_DT_PIN, LOADCELL_SCK_PIN);
	delay(500);
	if (loadcell3.is_ready())
	{
		loadcell3.set_scale(444.5F);
		loadcell3.tare();
		lcd.clear();
		lcd.print("HX711 (3) OK");
		DEBUGLN("HX711 (3) initialized");
	}
	else
	{
		lcd.clear();
		lcd.print("HX711 (3) failed");
		DEBUGLN("Failed HX711 (3) initialization");
		while (true)
			;
	}

	// Servo peanuts
	DEBUGLN("Initializing Servo");
	delay(500);
	servodist.write(80);
	DEBUGLN("Servo initialized");
	lcd.clear();
	lcd.print("Servo OK");
	delay(500);

	// LoRa RN2483
	DEBUGLN("Initializing LoRa");
	lcd.clear();
	lcd.setCursor(0, 0);
	lcd.print("LoRa join...");
	unsigned int otaaCooldown = 10;
	while (!LoRaBee.initOTA(loraSerial, devEUI, appEUI, appKey, true))
	{
		DEBUG("OTAA Keys setup failed -- retrying in ");
		lcd.clear();
		lcd.setCursor(0, 0);
		lcd.print("OTAA failed");
		lcd.setCursor(0, 1);
		lcd.print("Retrying in ");
		lcd.print(otaaCooldown);
		lcd.print("s");
		DEBUG(otaaCooldown);
		DEBUGLN(" seconds");
		delay(otaaCooldown * 1000);
		otaaCooldown += 10;
	}

	DEBUGLN("OTAA Keys accepted, device joined TTN");
	LoRaBee.setSpreadingFactor(7);
	LoRaBee.setFsbChannels(0);
	LoRaBee.setPowerIndex(5);
	lcd.clear();
	lcd.setCursor(0, 0);
	lcd.print("LoRa OK");
	DEBUGLN("LoRa initialized");

	delay(500);
	DEBUGLN("Initializing motors");

	for (int i = 6; i < 9; i++)
		pinMode(i, OUTPUT);

	lcd.clear();
	lcd.setCursor(0, 0);
	lcd.print("Motors OK");
	DEBUGLN("Motors initialized");

	// scanI2C();

	delay(500);
}

void loop()
{
	lcd.clear();
	lcd.setCursor(0, 0);
	lcd.print("Rhum Service");
	lcd.setCursor(0, 1);
	lcd.print("Bienvenue !");

	while (!mfrc522.PICC_IsNewCardPresent() || !mfrc522.PICC_ReadCardSerial())
	{
		if (digitalRead(BOUTON_UID) == LOW)
		{
			checkUID();
			return;
		}
		if (digitalRead(BOUTON_MORE_PEANUT) == LOW)
		{
			distribPeanuts();
			return;
		}
	}

	getWeights(loadcell1, loadcell2, loadcell3, poidsCapteurs);

	byte payload[10];
	DEBUG("RFID UID : ");
	for (byte i = 0; i < 4; i++)
	{
		payload[i] = mfrc522.uid.uidByte[i];
		debugSerial.print(payload[i] < 0x10 ? " 0" : " ");
		debugSerial.print(payload[i], HEX);
	}
	DEBUGLN();

	// Encodage des valeurs des 3 capteurs de poids (2 octets par valeur)
	payload[4] = (poidsCapteurs[0] >> 8) & 0xFF;
	payload[5] = poidsCapteurs[0] & 0xFF;
	payload[6] = (poidsCapteurs[1] >> 8) & 0xFF;
	payload[7] = poidsCapteurs[1] & 0xFF;
	payload[8] = (poidsCapteurs[2] >> 8) & 0xFF;
	payload[9] = poidsCapteurs[2] & 0xFF;

	for (byte i = 0; i < 10; i++)
	{
		debugSerial.print(payload[i] < 0x10 ? " 0" : " ");
		debugSerial.print(payload[i], HEX);
	}
	DEBUGLN();

	lcd.clear();
	lcd.setCursor(0, 0);
	lcd.print("Interrogation");
	lcd.setCursor(0, 1);
	lcd.print("serveur...");

	sendData(payload, 10);

	lcd.clear();
	lcd.setCursor(0, 0);
	lcd.print("Recuperez");
	lcd.setCursor(0, 1);
	lcd.print("votre verre");

	delay(16000);

	lcd.clear();
	lcd.setCursor(0, 0);
	lcd.print("Pret pour un");
	lcd.setCursor(0, 1);
	lcd.print("autre verre !");
	delay(4000);
}

Utils.h

C/C++
Pin definitions
// #define SERIAL_DEBUG
#ifdef SERIAL_DEBUG
#define DEBUG(x) debugSerial.print(x)
#define DEBUGLN(x) debugSerial.println(x)
#else
#define DEBUG(x)
#define DEBUGLN(x)
#endif

#define debugSerial SerialUSB
#define loraSerial Serial2

#define RFID_SDA_PIN 10
#define RFID_SCK_PIN 13
#define RFID_MOSI_PIN 11
#define RFID_MISO_PIN 12
#define RFID_RST_PIN 9
#define LOADCELL1_DT_PIN 2
#define LOADCELL2_DT_PIN 3
#define LOADCELL3_DT_PIN 4
#define LOADCELL_SCK_PIN 5
#define SERVO_PEANUT_PIN A0
#define BOUTON_UID A1
#define BOUTON_MORE_PEANUT A2
#define LCD_ADDRESS 0x20

// Pour l'cran LCD, bien brancher sur les pins "SDA" et "SCL"
// et pas les A4, A5 !!!

lora_api.py

Python
Backend API Python for the website (Flask) and managing the database (SQLite)
from flask import Flask, request, abort, render_template, redirect, url_for, session, flash
import logging
import sqlite3
from argon2 import PasswordHasher
import requests
import json
import base64
import struct

app = Flask(__name__)
app.secret_key = 'clesecrete'

# Supprimer les logs de Flask
log = logging.getLogger('werkzeug')
log.setLevel(logging.ERROR)

DATABASE = 'rhumservice.db'

ph = PasswordHasher()

TTN_URL = "xxxxxx"
TTN_API_KEY = "xxxxxx"

admin_list = ('admin', 'toor')

def init_db():
	"""Cre les tables si elles n'existent pas."""
	with sqlite3.connect(DATABASE) as conn:
		cursor = conn.cursor()
		cursor.execute('''
			CREATE TABLE IF NOT EXISTS user (
				uid TEXT PRIMARY KEY,
				username TEXT NOT NULL,
				password TEXT NOT NULL,
				first_name TEXT NOT NULL,
				last_name TEXT NOT NULL,
				is_driver BOOLEAN DEFAULT 0,
				balance INT DEFAULT 10
			)
		''')
		cursor.execute('''
			CREATE TABLE IF NOT EXISTS lora_data (
				id INTEGER PRIMARY KEY AUTOINCREMENT,
				poids_capteur1 INT,
				poids_capteur2 INT,
				poids_capteur3 INT,
				timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
			)
		''')
		cursor.execute('''
			CREATE TABLE IF NOT EXISTS cocktail_order (
				order_id INTEGER PRIMARY KEY AUTOINCREMENT,
				uid TEXT NOT NULL,
				boissonA INT,
				boissonB INT,
				boissonC INT,
				peanut BOOLEAN,
				timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
			)
		''')
		conn.commit()

def hash_password(password):
	"""Hache un mot de passe avec Argon2id."""
	return ph.hash(password)

def verify_password(input_password, stored_hash):
	"""Vrifie si le mot de passe saisi correspond au hachage stock."""
	try:
		return ph.verify(stored_hash, input_password)
	except Exception as e:
		print(f"Erreur lors de la vrification du mot de passe : {e}")
		return False

def user_exists(username):
	"""Vrifie si un nom d'utilisateur existe dj dans la BDD."""
	with sqlite3.connect(DATABASE) as conn:
		cursor = conn.cursor()
		cursor.execute('''
			SELECT 1 FROM user WHERE username = ?
		''', (username,))
		return cursor.fetchone() is not None

def get_user_hashed_pass(username):
	"""Rcupre le hash du mot de passe d'un utilisateur."""
	with sqlite3.connect(DATABASE) as conn:
		cursor = conn.cursor()
		cursor.execute('''
			SELECT password FROM user WHERE username = ?
		''', (username,))
		row = cursor.fetchone()
		return row[0]

def insert_user(uid, username, password, first_name, last_name):
	"""Insre un nouvel utilisateur dans la table user."""
	password = hash_password(password)
	with sqlite3.connect(DATABASE) as conn:
		cursor = conn.cursor()
		cursor.execute('''
			INSERT INTO user (uid, username, password, first_name, last_name) VALUES (?, ?, ?, ?, ?)
		''', (uid, username, password, first_name, last_name))
		conn.commit()

def insert_lora_data(poids_capteur1, poids_capteur2, poids_capteur3):
	"""Insre les donnes LoRa dans la table lora_data."""
	with sqlite3.connect(DATABASE) as conn:
		cursor = conn.cursor()
		cursor.execute('''
			INSERT INTO lora_data (poids_capteur1, poids_capteur2, poids_capteur3) VALUES (?, ?, ?)
		''', (poids_capteur1, poids_capteur2, poids_capteur3))
		conn.commit()

@app.route('/post-lora', methods=['POST'])
def receive_lora_data():
	"""Rception des donnes LoRa et insertion en BDD."""
	auth_header = request.headers.get('Authorization')
	if auth_header != f'Bearer {TTN_API_KEY}':
		abort(403, description='Forbidden: Invalid API Key')

	data = request.json

	# Vrification des donnes reues
	if not data or 'uplink_message' not in data:
		return 'Donnes invalides', 400

	decoded_payload = data['uplink_message'].get('decoded_payload', {})
	rfid_uid = decoded_payload.get('uid')
	poids_capteur1 = decoded_payload.get('weight1')
	poids_capteur2 = decoded_payload.get('weight2')
	poids_capteur3 = decoded_payload.get('weight3')

	# Log format des donnes reues
	print(f"RFID UID: {rfid_uid}\nPoids capteur 1: {poids_capteur1}\nPoids capteur 2: {poids_capteur2}\nPoids capteur 3: {poids_capteur3}\n")

	# Consultation de la table cocktail_order pour vrifier les commandes en attente
	with sqlite3.connect(DATABASE) as conn:
		cursor = conn.cursor()
		cursor.execute('''
			SELECT order_id, boissonA, boissonB, boissonC, peanut
			FROM cocktail_order
			WHERE uid = ?
			ORDER BY timestamp ASC
			LIMIT 1
		''', (rfid_uid,))
		pending_order = cursor.fetchone()

	if pending_order is not None:
		order_id, boissonA, boissonB, boissonC, want_peanut = pending_order
		send_cocktail_payload(boissonA, boissonB, boissonC, want_peanut)
		with sqlite3.connect(DATABASE) as conn:
			cursor = conn.cursor()
			cursor.execute('''
				DELETE FROM cocktail_order WHERE order_id = ?
			''', (order_id,))
			conn.commit()
	else:
		print(f"Aucune commande en attente pour l'UID '{rfid_uid}'.")

	# Insertion des donnes en BDD
	try:
		insert_lora_data(poids_capteur1, poids_capteur2, poids_capteur3)
		return 'Donnes enregistres', 204
	except Exception as e:
		print(f"Erreur lors de l'insertion en BDD : {e}")
		return 'Erreur serveur', 500

def send_cocktail_payload(boissonA, boissonB, boissonC, want_peanut):
	"""
	Compose et envoie un payload vers TTN contenant la composition du cocktail.
	Le payload est constitu de 3 octets correspondant aux pourcentages des boissons.
	"""
	# Pack des 3 valeurs + cacahutes dans 4 octets
	payload_bytes = struct.pack("BBBB", boissonA, boissonB, boissonC, want_peanut)

	# Encodage en Base64 pour TTN
	payload_base64 = base64.b64encode(payload_bytes).decode('utf-8')

	headers = {
		'Content-Type': 'application/json',
		'Authorization': f'Bearer {TTN_API_KEY}'
	}

	data = {
		"downlinks": [
			{
				"frm_payload": payload_base64,
				"f_port": 1,
				"priority": "NORMAL"
			}
		]
	}

	response = requests.post(TTN_URL, headers=headers, data=json.dumps(data))

	if response.status_code == 200:
		print("Downlink envoy avec succs")
	else:
		print(f"Erreur sur l'envoie du downlink : {response.text}")

# --- Endpoints Web ---

@app.route('/')
def home():
	"""Page d'accueil avec liens vers l'inscription et la connexion."""
	return render_template('home.html')

@app.route('/register', methods=['GET', 'POST'])
def register():
	"""Page d'inscription utilisateur."""
	if request.method == 'POST':
		uid = request.form['uid']
		username = request.form['username']
		password = request.form['password']
		first_name = request.form['first_name']
		last_name = request.form['last_name']

		if user_exists(username):
			flash("Ce nom d'utilisateur existe dj.")
			return redirect(url_for('register'))

		try:
			insert_user(uid, username, password, first_name, last_name)
			flash("Inscription russie. Veuillez vous connecter.")
			return redirect(url_for('login'))
		except Exception as e:
			print(f"Erreur lors de l'inscription : {e}")
			flash("Erreur lors de l'inscription.")
			return redirect(url_for('register'))
	return render_template('register.html')

@app.route('/login', methods=['GET', 'POST'])
def login():
	"""Page de connexion utilisateur."""
	if request.method == 'POST':
		username = request.form['username']
		password = request.form['password']

		if verify_password(password, get_user_hashed_pass(username)):
			session['username'] = username
			return redirect(url_for('dashboard'))
		else:
			flash("Identifiants invalides.")
			return redirect(url_for('login'))
	return render_template('login.html')

@app.route('/dashboard')
def dashboard():
	"""Page dashboard affichant les informations de l'utilisateur."""
	if 'username' not in session:
		return redirect(url_for('login'))
	username = session['username']
	with sqlite3.connect(DATABASE) as conn:
		cursor = conn.cursor()
		cursor.execute('''
			SELECT uid, username, first_name, last_name, is_driver, balance FROM user WHERE username = ?
		''', (username,))
		user = cursor.fetchone()
		cursor.execute('''
			SELECT boissonA, boissonB, boissonC, peanut, timestamp
			FROM cocktail_order
			WHERE uid = ?
			ORDER BY timestamp ASC
		''', (user[0],))
		cocktail_orders = cursor.fetchall()

	return render_template('dashboard.html', user=user, cocktail_orders=cocktail_orders)

@app.route('/toggle_driver', methods=['POST'])
def toggle_driver():
	"""Permet  l'utilisateur de changer son statut 'Conducteur'."""
	if 'username' not in session:
		return redirect(url_for('login'))
	username = session['username']
	with sqlite3.connect(DATABASE) as conn:
		cursor = conn.cursor()
		cursor.execute('''
			UPDATE user SET is_driver = CASE WHEN is_driver = 1 THEN 0 ELSE 1 END WHERE username = ?
		''', (username,))
		conn.commit()
	flash("Statut conducteur modifi.")
	return redirect(url_for('dashboard'))

@app.route('/add_balance', methods=['POST'])
def add_balance():
	"""Ajoute 10 au solde de l'utilisateur."""
	if 'username' not in session:
		return redirect(url_for('login'))
	username = session['username']
	with sqlite3.connect(DATABASE) as conn:
		cursor = conn.cursor()
		cursor.execute('''
			UPDATE user SET balance = balance + 10 WHERE username = ?
		''', (username,))
		conn.commit()
	flash("Solde augment de 10.")
	return redirect(url_for('dashboard'))

# Fonction de mapping : 0% -> 0 et 100% -> 400 (40cl)
def estimated_weight(percentage, margin=1.05):
	return (400 * percentage / 100) * margin

@app.route('/cocktail')
def cocktail_page():
	"""Page de commande de cocktails."""
	if 'username' not in session:
		return redirect(url_for('login'))
	return render_template('cocktail.html')

@app.route('/order_cocktail', methods=['POST'])
def order_cocktail():
	"""Ajoute une commande dans la file d'attente pour l'utilisateur."""
	if 'username' not in session:
		return redirect(url_for('login'))

	# Rcupration et conversion des pourcentages envoys par le formulaire
	try:
		boissonA = int(request.form.get('boissonA', 0))
		boissonB = int(request.form.get('boissonB', 0))
		boissonC = int(request.form.get('boissonC', 0))
	except ValueError:
		flash("Valeurs invalides pour les pourcentages.")
		return redirect(url_for('cocktail_page'))

	# Rcupration et conversion du statut "Cacahutes ?"
	try:
		want_peanut = 1 if request.form.get('Cacahuetes') else 0
	except ValueError:
		flash("Valeur pour 'Cacahutes' invalide.")
		return redirect(url_for('cocktail_page'))

	# Vrification que les valeurs sont comprises entre 0 et 100
	if not (0 <= boissonA <= 100 and 0 <= boissonB <= 100 and 0 <= boissonC <= 100):
		flash("Les pourcentages doivent tre entre 0 et 100.")
		return redirect(url_for('cocktail_page'))

	# Vrification de la somme des pourcentages
	total = boissonA + boissonB + boissonC
	if total > 100:
		flash("La somme des pourcentages ne peut dpasser 100%.")
		return redirect(url_for('cocktail_page'))

	# Rcuprer la dernire donne de lora_data pour vrifier la disponibilit
	with sqlite3.connect(DATABASE) as conn:
		cursor = conn.cursor()
		cursor.execute('''
			SELECT poids_capteur1, poids_capteur2, poids_capteur3 FROM lora_data ORDER BY timestamp DESC LIMIT 1
		''')
		lora_row = cursor.fetchone()
	if not lora_row:
		flash("Donnes des rservoirs indisponibles.")
		return redirect(url_for('cocktail_page'))

	# Rcuprer les commandes en attente pour calculer le poids estim  dduire
	with sqlite3.connect(DATABASE) as conn:
		cursor = conn.cursor()
		cursor.execute('''
			SELECT boissonA, boissonB, boissonC FROM cocktail_order
		''')
		pending_orders = cursor.fetchall()

	pending_weight_A = sum(estimated_weight(int(order[0])) for order in pending_orders)
	pending_weight_B = sum(estimated_weight(int(order[1])) for order in pending_orders)
	pending_weight_C = sum(estimated_weight(int(order[2])) for order in pending_orders)

	available_A = lora_row[0] - pending_weight_A
	available_B = lora_row[1] - pending_weight_B
	available_C = lora_row[2] - pending_weight_C

	# Debug
	# print(f"Quantit virtuelle rservoir A : {available_A}\n"
	# 	  f"Quantit virtuelle rservoir B : {available_B}\n"
	# 	  f"Quantit virtuelle rservoir C : {available_C}\n")

	# Vrifier la disponibilit pour chaque boisson
	if boissonA > 0 and available_A < estimated_weight(boissonA):
		flash("Quantit insuffisante pour la Boisson A.")
		return redirect(url_for('cocktail_page'))
	if boissonB > 0 and available_B < estimated_weight(boissonB):
		flash("Quantit insuffisante pour la Boisson B.")
		return redirect(url_for('cocktail_page'))
	if boissonC > 0 and available_C < estimated_weight(boissonC):
		flash("Quantit insuffisante pour la Boisson C.")
		return redirect(url_for('cocktail_page'))

	# Rcuprer l'UID et le solde de l'utilisateur
	with sqlite3.connect(DATABASE) as conn:
		cursor = conn.cursor()
		cursor.execute('''
			SELECT uid, balance, is_driver FROM user WHERE username = ?
		''', (session['username'],))
		result = cursor.fetchone()
		if result:
			uid, balance, is_driver = result
		else:
			flash("Erreur utilisateur.")
			return redirect(url_for('cocktail_page'))

		# Test : L'utilisateur doit avoir au moins 10 de solde
		if balance < 10:
			flash("Solde insuffisant pour commander un cocktail.")
			return redirect(url_for('cocktail_page'))

		# Test : Limitation des boissons si l'utilisateur est conducteur
		if is_driver and boissonA > 0:
			flash("En tant que conducteur, vous ne pouvez commander que la boisson B et C (sans alcool).")
			return redirect(url_for('cocktail_page'))

		# Insrer la commande dans la table cocktail_order
		cursor.execute('''
			INSERT INTO cocktail_order (uid, boissonA, boissonB, boissonC, peanut)
			VALUES (?, ?, ?, ?, ?)
		''', (uid, boissonA, boissonB, boissonC, want_peanut))
		conn.commit()

		# Dbiter de 10 le solde de l'utilisateur
		cursor.execute('''
			UPDATE user SET balance = balance - 10 WHERE username = ?
		''', (session['username'],))
		conn.commit()

	flash("Cocktail command ! 10 ont t dbits de votre solde.")
	return redirect(url_for('cocktail_page'))

@app.route('/admin')
def admin_dashboard():
	# Vrification de l'accs admin (ici, on suppose que admin_list a le droit)
	if 'username' not in session or session['username'] not in admin_list:
		return redirect(url_for('home'))

	with sqlite3.connect(DATABASE) as conn:
		cursor = conn.cursor()

		# Partie Physique : rcuprer la dernire donne LoRa
		cursor.execute('''
			SELECT poids_capteur1, poids_capteur2, poids_capteur3, timestamp FROM lora_data ORDER BY timestamp DESC LIMIT 1
		''')
		lora_data = cursor.fetchone()

		# Rcuprer toutes les commandes en attente
		cursor.execute('''
			SELECT uid, boissonA, boissonB, boissonC, timestamp FROM cocktail_order
		''')
		cocktail_orders = cursor.fetchall()

		# Rcuprer la liste des utilisateurs
		cursor.execute('''
			SELECT uid, username, first_name, last_name, balance, is_driver FROM user
		''')
		users = cursor.fetchall()

	# Si des donnes LoRa existent, rcuprer les valeurs des capteurs
	if lora_data:
		sensor_A = lora_data[0]
		sensor_B = lora_data[1]
		sensor_C = lora_data[2]
	else:
		sensor_A = sensor_B = sensor_C = 0

	# Calculer le poids total des commandes en attente pour chaque boisson
	pending_weight_A = sum(estimated_weight(int(order[1])) for order in cocktail_orders)
	pending_weight_B = sum(estimated_weight(int(order[2])) for order in cocktail_orders)
	pending_weight_C = sum(estimated_weight(int(order[3])) for order in cocktail_orders)

	# Calculer l'tat virtuel (disponibilit) des stocks
	available_A = sensor_A - pending_weight_A
	available_B = sensor_B - pending_weight_B
	available_C = sensor_C - pending_weight_C

	return render_template('admin_dashboard.html',
						   lora_data=lora_data,
						   available_A=available_A,
						   available_B=available_B,
						   available_C=available_C,
						   cocktail_orders=cocktail_orders,
						   users=users)

@app.route('/logout')
def logout():
	"""Dconnecte l'utilisateur."""
	session.pop('username', None)
	return redirect(url_for('home'))

if __name__ == '__main__':
	init_db()
	app.run(host='0.0.0.0', port=60100)

Credits

Arthur Blanchot
1 project • 2 followers
Contact
Yohann Vergniole
1 project • 2 followers
Contact
Nicolas DAILLY
33 projects • 21 followers
Associated Professor at UniLaSalle - Amiens / Head of the Computer Network Department / Teach Computer and Telecommunication Networks
Contact
fcaron
15 projects • 3 followers
Contact
Alexandre Létocart
3 projects • 3 followers
Contact
Julien Delplanque
1 project • 3 followers
Contact

Comments

Please log in or sign up to comment.