├── pics ├── drinks.jpg ├── IMG_7818.JPG ├── IMG_7939.JPG ├── IMG_7941.JPG ├── IMG_7943.JPG ├── IMG_7954.JPG └── IMG_8022.JPG ├── sounds ├── camera.mp3 ├── eventually.mp3 ├── triumphant.mp3 ├── chimes-glassy.mp3 ├── goes-without-saying.mp3 └── slow-spring-board-longer-tail.mp3 ├── .gitignore ├── config.py ├── systemd ├── cocktail.service └── SERVICE.md ├── recipe.py ├── pump.py ├── cloud.py ├── README.md └── cocktailpi.py /pics/drinks.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saubury/cocktail-pi/HEAD/pics/drinks.jpg -------------------------------------------------------------------------------- /pics/IMG_7818.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saubury/cocktail-pi/HEAD/pics/IMG_7818.JPG -------------------------------------------------------------------------------- /pics/IMG_7939.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saubury/cocktail-pi/HEAD/pics/IMG_7939.JPG -------------------------------------------------------------------------------- /pics/IMG_7941.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saubury/cocktail-pi/HEAD/pics/IMG_7941.JPG -------------------------------------------------------------------------------- /pics/IMG_7943.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saubury/cocktail-pi/HEAD/pics/IMG_7943.JPG -------------------------------------------------------------------------------- /pics/IMG_7954.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saubury/cocktail-pi/HEAD/pics/IMG_7954.JPG -------------------------------------------------------------------------------- /pics/IMG_8022.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saubury/cocktail-pi/HEAD/pics/IMG_8022.JPG -------------------------------------------------------------------------------- /sounds/camera.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saubury/cocktail-pi/HEAD/sounds/camera.mp3 -------------------------------------------------------------------------------- /sounds/eventually.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saubury/cocktail-pi/HEAD/sounds/eventually.mp3 -------------------------------------------------------------------------------- /sounds/triumphant.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saubury/cocktail-pi/HEAD/sounds/triumphant.mp3 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | tmp/* 4 | tmp_mp3.mp3 5 | bday.py 6 | test.py 7 | cache/* 8 | RMME/* 9 | -------------------------------------------------------------------------------- /sounds/chimes-glassy.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saubury/cocktail-pi/HEAD/sounds/chimes-glassy.mp3 -------------------------------------------------------------------------------- /sounds/goes-without-saying.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saubury/cocktail-pi/HEAD/sounds/goes-without-saying.mp3 -------------------------------------------------------------------------------- /sounds/slow-spring-board-longer-tail.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saubury/cocktail-pi/HEAD/sounds/slow-spring-board-longer-tail.mp3 -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | gpio_button_blk=2 2 | gpio_button_red=22 3 | gpio_switch=27 4 | 5 | gpio_pump_a=12 6 | gpio_pump_b=16 7 | gpio_pump_c=20 8 | gpio_pump_d=21 9 | -------------------------------------------------------------------------------- /systemd/cocktail.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Run the Raspberry Pi Cocktail Maker 3 | After=multi-user.target 4 | 5 | [Service] 6 | User=pi 7 | Group=pi 8 | WorkingDirectory=/home/pi/Documents/git/cocktail-pi 9 | ExecStart=/usr/bin/python /home/pi/Documents/git/cocktail-pi/cocktailpi.py 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /systemd/SERVICE.md: -------------------------------------------------------------------------------- 1 | # Installing a service 2 | 3 | ``` 4 | sudo cp cocktail.service /lib/systemd/system 5 | 6 | sudo systemctl daemon-reload 7 | sudo systemctl enable cocktail.service 8 | sudo systemctl start cocktail.service 9 | ``` 10 | 11 | General checks 12 | ``` 13 | sudo systemctl status cocktail.service 14 | sudo journalctl -u cocktail.service -b 15 | ps -ef | grep cocktail | grep -v grep 16 | ``` -------------------------------------------------------------------------------- /recipe.py: -------------------------------------------------------------------------------- 1 | pump_map = {"PUMP_A": "Vodka", "PUMP_B": "Cranberry", "PUMP_C": "Tonic", "PUMP_D": "Lime"} 2 | 3 | recipe_vodkasoda = {"Name": "Vodka Soda", "Vodka": 20, "Tonic": 70} 4 | recipe_vodkasodacranburry = {"Name": "Vodka Soda Cranberry", "Vodka": 20, "Tonic": 70, "Cranberry": 40} 5 | recipe_vodkalimesoda = {"Name": "Vodka Lime Soda", "Vodka": 20, "Tonic": 70, "Lime": 30} 6 | recipe_limesoda = {"Name": "Lime Soda", "Tonic": 70, "Lime": 30} 7 | recipe_cranburrysoda = {"Name": "Cranberry Soda", "Tonic": 70, "Cranberry": 40} 8 | recipe_vodkasodalimecranburry = {"Name": "Vodka Soda Lime Cranberry", "Vodka": 20, "Tonic": 70, "Cranberry": 30, "Lime": 25} 9 | 10 | -------------------------------------------------------------------------------- /pump.py: -------------------------------------------------------------------------------- 1 | # Project Imports 2 | import config 3 | import cloud 4 | import RPi.GPIO as GPIO 5 | import datetime 6 | import time 7 | import threading 8 | from recipe import * 9 | 10 | pumpPreWaitTime = 6 11 | 12 | 13 | def pump_setup(): 14 | GPIO.setmode(GPIO.BCM) 15 | GPIO.setwarnings(False) 16 | GPIO.setup(config.gpio_pump_a, GPIO.OUT) 17 | GPIO.setup(config.gpio_pump_b, GPIO.OUT) 18 | GPIO.setup(config.gpio_pump_c, GPIO.OUT) 19 | GPIO.setup(config.gpio_pump_d, GPIO.OUT) 20 | GPIO.setwarnings(True) 21 | 22 | def pump_thread_runner(gpio_pump_name, run_seconds): 23 | time.sleep(pumpPreWaitTime) 24 | GPIO.output(gpio_pump_name, GPIO.HIGH) 25 | time.sleep(run_seconds) 26 | GPIO.output(gpio_pump_name, GPIO.LOW) 27 | 28 | def pump_thread_start(gpio_pump_name, run_seconds): 29 | thread = threading.Thread(target=pump_thread_runner, args=(gpio_pump_name, run_seconds)) 30 | thread.start() 31 | 32 | # Return the number of seconds to run 33 | def lookup_time(drink_name, pump_name): 34 | ML_per_second = 1.9 35 | 36 | try: 37 | num_ML = drink_name[pump_map[pump_name]] 38 | return num_ML / ML_per_second 39 | except KeyError: 40 | return 0 41 | 42 | def do_drink(this_drink): 43 | pump_setup() 44 | 45 | (duration_a, duration_b, duration_c, duration_d) = (lookup_time(this_drink, "PUMP_A"), lookup_time(this_drink, "PUMP_B"), lookup_time(this_drink, "PUMP_C"), lookup_time(this_drink, "PUMP_D")) 46 | wait_time = max(duration_a, duration_b, duration_c, duration_d ) 47 | 48 | # these threads start in background 49 | pump_thread_start(config.gpio_pump_a, duration_a) 50 | pump_thread_start(config.gpio_pump_b, duration_b) 51 | pump_thread_start(config.gpio_pump_c, duration_c) 52 | pump_thread_start(config.gpio_pump_d, duration_d) 53 | return int(wait_time) 54 | -------------------------------------------------------------------------------- /cloud.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import getopt 3 | import os.path 4 | import picamera 5 | import time 6 | import boto3 7 | import json 8 | import os 9 | import hashlib 10 | import re 11 | import datetime 12 | import config 13 | 14 | 15 | def quickAudioMsg(audiotext, presound='eventually.mp3', voice='Emma'): 16 | file_mp3 = './cache/' + cache_filename(audiotext) + '.mp3' 17 | 18 | if (os.path.exists(file_mp3)): 19 | playMP3('./sounds/{}'.format(presound), background=True) 20 | time.sleep(0.8) 21 | else: 22 | playMP3('./sounds/{}'.format(presound), background=True) 23 | client = boto3.client('polly') 24 | response = client.synthesize_speech(OutputFormat='mp3', Text=audiotext, VoiceId=voice) 25 | thebytes = response['AudioStream'].read() 26 | thefile = open(file_mp3, 'wb') 27 | thefile.write(thebytes) 28 | thefile.close() 29 | 30 | playMP3(file_mp3, background=False) 31 | 32 | def playMP3(file_mp3, background): 33 | if background: 34 | os.system('mpg123 -q ' + file_mp3 + ' 2>/dev/null &') 35 | else: 36 | os.system('mpg123 -q ' + file_mp3 + ' 2>/dev/null') 37 | 38 | def takePhotoAndProcess(): 39 | namebase='./tmp/snapped_{}'.format(datetime.datetime.today().strftime('%Y%m%d-%H%M%S')) 40 | file_jpg=namebase + '.jpg' 41 | file_json=namebase + '.json' 42 | emotion = 'unknown' 43 | age_range_low = 10 44 | if (not os.path.exists(file_jpg) or not os.path.exists(file_json)): 45 | with picamera.PiCamera() as camera: 46 | camera.capture(file_jpg) 47 | 48 | with open(file_jpg, 'rb') as f_file_jpg: 49 | b_a_jpg = bytearray(f_file_jpg.read()) 50 | rclient = boto3.client('rekognition') 51 | response = rclient.detect_faces(Image={ 'Bytes': b_a_jpg}, Attributes=['ALL']) 52 | with open(file_json, 'w') as outfile: 53 | json.dump(response, outfile) 54 | 55 | gender, emotion, age_range_low = processJSON(file_json) 56 | return gender, emotion, age_range_low 57 | 58 | 59 | def processJSON(file_json): 60 | with open(file_json) as data_file: 61 | data = json.load(data_file) 62 | 63 | age_range_low=data["FaceDetails"][0]["AgeRange"]["Low"] 64 | age_range_high=data["FaceDetails"][0]["AgeRange"]["High"] 65 | gender=data["FaceDetails"][0]["Gender"]["Value"] 66 | 67 | # Sort; and find the highest confidence emotion 68 | json_obj = data["FaceDetails"][0] 69 | sorted_obj = sorted(json_obj['Emotions'], key=lambda x : x['Confidence'], reverse=True) 70 | emotion = sorted_obj[0]['Type'] 71 | 72 | return gender, emotion, age_range_low 73 | 74 | def cache_filename(filename): 75 | # make a legal filename, remove spaces and punctuation - and limit to 50 characters 76 | hashstring = hashlib.md5(filename).hexdigest() 77 | fileprefix = re.sub('[^a-zA-Z0-9]', '_', filename).lower()[0:50] 78 | return fileprefix + hashstring 79 | 80 | if __name__ == '__main__': 81 | quickAudioMsg("Hello world!!!") 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🍹 Cocktail Pi 2 | Cocktails based on your mood created by a Raspberry Pi bartender 3 | 4 | Build instructions for a fully automated home build cocktail maker. How to use a Raspberry Pi, camera plus a few peristaltic pumps assembled into a home bartender. 5 | 6 | Drinks are selected based on your emotion and multilingual voice prompts let you know when your drink is available. 🍸 7 | 8 | For a complete video see [here](https://www.youtube.com/watch?v=8q_5STFzJ6c) 9 | 10 | # Hardware Components 11 | 12 | ## Pumps 13 | I used 4 [peristaltic pumps](https://en.wikipedia.org/wiki/Peristaltic_pump) to provide a "food safe" way to pump the liquids from the drink bottles 14 | 15 | ![peristaltic pumps](./pics/IMG_7818.JPG) 16 | 17 | The pumps are mounted on a basic wooden frame higher than the tallest bottle 18 | 19 | ![pumps mounted](./pics/IMG_7943.JPG) 20 | 21 | A view from the rear show the placement of pumpts and liquids 22 | 23 | ![drinks rear](./pics/IMG_7954.JPG) 24 | 25 | These are 12 volt motors. To operate them via the Raspberry Pi I used a [4 Channel 12V Relay Module](https://www.jaycar.com.au/arduino-compatible-4-channel-12v-relay-module/p/XC4440) 26 | 27 | ![relay block](./pics/IMG_7939.JPG) 28 | 29 | The Raspberry Pi is mounted with the relay board 30 | 31 | ![Raspberry pi with relay board](./pics/IMG_8022.JPG) 32 | 33 | 34 | # Software 35 | 36 | ## Storing Recipes 37 | 38 | [Recipes](./recipe.py) are stored as a map of ingredients with the number of millitres required for the perfect drink 39 | 40 | 41 | 42 | ```python 43 | pump_map = {"PUMP_A": "Vodka", "PUMP_B": "Cranberry", "PUMP_C": "Tonic", "PUMP_D": "Lime"} 44 | 45 | recipe_vodkasoda = {"Name": "Vodka Soda", "Vodka": 20, "Tonic": 70} 46 | recipe_vodkasodacranburry = {"Name": "Vodka Soda Cranberry", "Vodka": 20, "Tonic": 70, "Cranberry": 40} 47 | recipe_vodkalimesoda = {"Name": "Vodka Lime Soda", "Vodka": 20, "Tonic": 70, "Lime": 30} 48 | recipe_limesoda = {"Name": "Lime Soda", "Tonic": 70, "Lime": 30} 49 | recipe_test = {"Name": "Test", "Vodka": 10, "Cranberry": 20, "Tonic": 30, "Lime":40} 50 | ``` 51 | 52 | ## Dispensing the perfect amount 53 | 54 | The pumps I'm using dispense 1.9mL of liquid per second. I can deligate each pump to a thread timer so it knows how long to operate for once the drink is selected. Have a look at [pump.py](./pump.py) for complete code 55 | 56 | ```python 57 | # Return the number of seconds to run 58 | def lookup_time(drink_name, pump_name): 59 | ML_per_second = 1.9 60 | num_ML = drink_name[pump_map[pump_name]] 61 | return num_ML / ML_per_second 62 | 63 | # Run a pump for given number of seconds 64 | def pump_thread_runner(gpio_pump_name, run_seconds): 65 | GPIO.output(gpio_pump_name, GPIO.HIGH) 66 | time.sleep(run_seconds) 67 | GPIO.output(gpio_pump_name, GPIO.LOW) 68 | 69 | # Pick a drink 70 | this_drink = recipe_vodkasoda 71 | 72 | # How long to run each pump ... 73 | duration_a = lookup_time(this_drink, "PUMP_A") 74 | duration_b = lookup_time(this_drink, "PUMP_B") 75 | 76 | # these threads start in background 77 | pump_thread_start(config.gpio_pump_a, duration_a) 78 | pump_thread_start(config.gpio_pump_b, duration_b) 79 | ``` 80 | 81 | ## Emotion and Age 82 | I use the [AWS Rekognition](https://aws.amazon.com/rekognition/) service to determine likely emotion and approximate age. Have a look at [cloud.py](./cloud.py) for complete code 83 | ```python 84 | # Save camera image to file file_jpg 85 | with picamera.PiCamera() as camera: 86 | camera.capture(file_jpg) 87 | 88 | # Process file_jpg using AWS rekognition to extract emotion and age 89 | with open(file_jpg, 'rb') as f_file_jpg: 90 | b_a_jpg = bytearray(f_file_jpg.read()) 91 | rclient = boto3.client('rekognition') 92 | response = rclient.detect_faces(Image={'Bytes': b_a_jpg}, Attributes=['ALL']) 93 | ``` 94 | 95 | ## Spoken words 96 | For _text to speech_ I used the [AWS Polly](https://aws.amazon.com/polly/) service. Have a look at [cloud.py](./cloud.py) for complete code 97 | 98 | ```python 99 | # Using "Emma" as a voice, generate text to voice 100 | voice='Emma' 101 | client = boto3.client('polly') 102 | response = client.synthesize_speech(OutputFormat='mp3', Text=audiotext, VoiceId=voice) 103 | thebytes = response['AudioStream'].read() 104 | thefile = open(file_mp3, 'wb') 105 | thefile.write(thebytes) 106 | thefile.close() 107 | 108 | # Play mp3 file via speaker 109 | os.system('mpg123 -q {}'.format(file_mp3)) 110 | ``` 111 | # Summary 112 | A few hours and a bit of programming and you too can enjoy a 🍹 cocktail pi. Enjoy! 113 | 114 | ![drinks](./pics/drinks.jpg) 115 | 116 | -------------------------------------------------------------------------------- /cocktailpi.py: -------------------------------------------------------------------------------- 1 | # Project Imports 2 | import config 3 | import cloud 4 | import pump 5 | import RPi.GPIO as GPIO 6 | import time 7 | from recipe import * 8 | import os 9 | 10 | def button_setup(): 11 | GPIO.setmode(GPIO.BCM) 12 | GPIO.setup(config.gpio_button_red, GPIO.IN, pull_up_down=GPIO.PUD_UP) 13 | GPIO.setup(config.gpio_switch, GPIO.IN, pull_up_down=GPIO.PUD_UP) 14 | GPIO.setup(config.gpio_button_blk, GPIO.IN) 15 | 16 | def isEnglish(): 17 | return switch_is_on() 18 | 19 | def button_red(): 20 | return GPIO.input(config.gpio_button_red) == False 21 | 22 | def switch_is_on(): 23 | return GPIO.input(config.gpio_switch) == False 24 | 25 | def button_blk(): 26 | return GPIO.input(config.gpio_button_blk) == False 27 | 28 | def doBilingualMsg(msg_en, msg_fr, presound='eventually.mp3'): 29 | if isEnglish(): 30 | cloud.quickAudioMsg(msg_en, presound=presound) 31 | else: 32 | cloud.quickAudioMsg(msg_fr, presound=presound, voice='Lea') 33 | 34 | 35 | def do_bartender(): 36 | msg_en = 'Hello, my name is Bridget your lovely bar tender. Please stay still, I am going to take a quick photo.' 37 | msg_fr = 'Bonjour, Je m''appelle Brigitte. Ne bougez pas, je vais vous prendre en photo. Le petit oiseau va sortir' 38 | doBilingualMsg(msg_en, msg_fr) 39 | 40 | try: 41 | gender, emotion, age_range_low = cloud.takePhotoAndProcess() 42 | except (IndexError): 43 | doBilingualMsg('I can not see anyone at the bar', 'je ne vois personne au comptoir') 44 | return 45 | 46 | msg_en = 'It is nice to meet a human {}. I think you are at least {} years old. You appear to be {}! '.format(gender, age_range_low, emotion) 47 | msg_fr = 'Je suis ravie de rencontrer une personne {}. je crois que vous avez au moins {} ans. vous devez avoir {} ans! '.format(gender, age_range_low, emotion) 48 | doBilingualMsg(msg_en, msg_fr) 49 | 50 | # Valid Values: HAPPY | SAD | ANGRY | CONFUSED | DISGUSTED | SURPRISED | CALM | UNKNOWN | FEAR 51 | if age_range_low <25: 52 | drink_group = 'child' 53 | if (emotion in [ 'HAPPY','CONFUSED', 'SURPRISED' ]): 54 | this_drink = recipe_cranburrysoda 55 | else: 56 | this_drink = recipe_limesoda 57 | else: 58 | drink_group = 'adult' 59 | if (emotion in [ 'HAPPY' ]): 60 | this_drink = recipe_vodkasodalimecranburry 61 | elif (emotion in [ 'CONFUSED', 'SURPRISED' ]): 62 | this_drink = recipe_vodkasodacranburry 63 | elif (emotion in [ 'ANGRY', 'DISGUSTED', 'FEAR' ]): 64 | this_drink = recipe_vodkalimesoda 65 | else: 66 | this_drink = recipe_vodkasoda 67 | 68 | map_emotion = {"HAPPY":"heureux", "SAD":"triste", "ANGRY":"en colere", "CONFUSED":"confuse", "DISGUSTED":"deguse", "SURPRISED":"surpris", "CALM":"calme", "UNKNOWN":"inconnue", "FEAR":"peur"} 69 | emotion_fr = map_emotion[emotion] 70 | 71 | msg_en = "As you appear to be {}, I think an appropriate {} drink would be a {}. ".format(emotion, drink_group, this_drink['Name']) 72 | msg_fr = "Comme vous semblez etre {}, je pense qu'une boisson appropriee pour un {} serait un {}. ".format(emotion_fr, drink_group, this_drink['Name']) 73 | doBilingualMsg(msg_en, msg_fr) 74 | 75 | 76 | msg_en = "Press the black button for a {}, or the red button to cancel ".format(this_drink['Name']) 77 | msg_fr = "Appuyez sur le bouton noir pour un {}, ou appuyez sur le bouton rougue pour annuler ".format(this_drink['Name']) 78 | doBilingualMsg(msg_en, msg_fr) 79 | while True: 80 | time.sleep(0.1) 81 | if button_red(): 82 | doBilingualMsg("OK, cancelled", "OK, annule") 83 | time.sleep(1) 84 | break; 85 | 86 | elif button_blk(): 87 | wait_time = pump.do_drink(this_drink) 88 | msg_en = "Please be patient; your {} will be ready in {} seconds.".format(this_drink['Name'], wait_time) 89 | msg_fr = "Merci de patienter; votre {} sera pret dans {} secondes.".format(this_drink['Name'], wait_time) 90 | doBilingualMsg(msg_en, msg_fr) 91 | time.sleep(wait_time) 92 | msg_en = "Your drink, {}, is ready. Please enjoy.".format(this_drink['Name']) 93 | msg_fr = "votre commande, {}, est prete. Bonne degustation.".format(this_drink['Name']) 94 | doBilingualMsg(msg_en, msg_fr, 'triumphant.mp3') 95 | break; 96 | 97 | if __name__ == "__main__": 98 | abspath = os.path.abspath(__file__) 99 | dname = os.path.dirname(abspath) 100 | os.chdir(dname) 101 | 102 | button_setup() 103 | while True: 104 | if button_red(): 105 | do_bartender() 106 | 107 | if button_blk(): 108 | doBilingualMsg('Boozy boozy. Boozy boozy. Would you like a Boozy boozy?', 'Allez viens boire un petit coup!') 109 | 110 | time.sleep(0.1) 111 | --------------------------------------------------------------------------------