├── requirements.txt ├── dotstar.so ├── pump_config.json ├── LICENSE ├── drinks.py ├── menu.py ├── README.md └── bartender.py /requirements.txt: -------------------------------------------------------------------------------- 1 | RPi.GPIO==0.6.3 2 | -------------------------------------------------------------------------------- /dotstar.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackerShackOfficial/Smart-Bartender/HEAD/dotstar.so -------------------------------------------------------------------------------- /pump_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "pump_1": { 3 | "name": "Pump 1", 4 | "pin": 17, 5 | "value": "gin" 6 | }, 7 | "pump_2": { 8 | "name": "Pump 2", 9 | "pin": 27, 10 | "value": "tonic" 11 | }, 12 | "pump_3": { 13 | "name": "Pump 3", 14 | "pin": 22, 15 | "value": null 16 | }, 17 | "pump_4": { 18 | "name": "Pump 4", 19 | "pin": 23, 20 | "value": null 21 | }, 22 | "pump_5": { 23 | "name": "Pump 5", 24 | "pin": 24, 25 | "value": null 26 | }, 27 | "pump_6": { 28 | "name": "Pump 6", 29 | "pin": 25, 30 | "value": null 31 | } 32 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Hacker House 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /drinks.py: -------------------------------------------------------------------------------- 1 | # drinks.py 2 | drink_list = [ 3 | { 4 | "name": "Rum & Coke", 5 | "ingredients": { 6 | "rum": 50, 7 | "coke": 150 8 | } 9 | }, { 10 | "name": "Gin & Tonic", 11 | "ingredients": { 12 | "gin": 50, 13 | "tonic": 150 14 | } 15 | }, { 16 | "name": "Long Island", 17 | "ingredients": { 18 | "gin": 15, 19 | "rum": 15, 20 | "vodka": 15, 21 | "tequila": 15, 22 | "coke": 100, 23 | "oj": 30 24 | } 25 | }, { 26 | "name": "Screwdriver", 27 | "ingredients": { 28 | "vodka": 50, 29 | "oj": 150 30 | } 31 | }, { 32 | "name": "Margarita", 33 | "ingredients": { 34 | "tequila": 50, 35 | "mmix": 150 36 | } 37 | }, { 38 | "name": "Gin & Juice", 39 | "ingredients": { 40 | "gin": 50, 41 | "oj": 150 42 | } 43 | }, { 44 | "name": "Tequila Sunrise", 45 | "ingredients": { 46 | "tequila": 50, 47 | "oj": 150 48 | } 49 | } 50 | ] 51 | 52 | drink_options = [ 53 | {"name": "Gin", "value": "gin"}, 54 | {"name": "Rum", "value": "rum"}, 55 | {"name": "Vodka", "value": "vodka"}, 56 | {"name": "Tequila", "value": "tequila"}, 57 | {"name": "Tonic Water", "value": "tonic"}, 58 | {"name": "Coke", "value": "coke"}, 59 | {"name": "Orange Juice", "value": "oj"}, 60 | {"name": "Margarita Mix", "value": "mmix"} 61 | ] -------------------------------------------------------------------------------- /menu.py: -------------------------------------------------------------------------------- 1 | # menu.py 2 | class MenuItem(object): 3 | def __init__(self, type, name, attributes = None, visible = True): 4 | self.type = type 5 | self.name = name 6 | self.attributes = attributes 7 | self.visible = visible 8 | 9 | class Back(MenuItem): 10 | def __init__(self, name): 11 | MenuItem.__init__(self, "back", name) 12 | 13 | class Menu(MenuItem): 14 | def __init__(self, name, attributes = None, visible = True): 15 | MenuItem.__init__(self, "menu", name, attributes, visible) 16 | self.options = [] 17 | self.selectedOption = 0 18 | self.parent = None 19 | 20 | def addOptions(self, options): 21 | self.options = self.options + options 22 | self.selectedOption = 0 23 | 24 | def addOption(self, option): 25 | self.options.append(option) 26 | self.selectedOption = 0 27 | 28 | def setParent(self, parent): 29 | self.parent = parent 30 | 31 | def nextSelection(self): 32 | self.selectedOption = (self.selectedOption + 1) % len(self.options) 33 | 34 | def getSelection(self): 35 | return self.options[self.selectedOption] 36 | 37 | class MenuContext(object): 38 | def __init__(self, menu, delegate): 39 | self.topLevelMenu = menu 40 | self.currentMenu = menu 41 | self.delegate = delegate 42 | self.showMenu() 43 | 44 | def showMenu(self): 45 | """ 46 | Shows the first selection of the current menu 47 | """ 48 | self.display(self.currentMenu.getSelection()); 49 | 50 | def setMenu(self, menu): 51 | """ 52 | Sets a new menu to the menu context. 53 | 54 | raises ValueError if the menu has no options 55 | """ 56 | if (len(menu.options) == 0): 57 | raise ValueError("Cannot setMenu on a menu with no options") 58 | self.topLevelMenu = menu 59 | self.currentMenu = menu 60 | self.showMenu(); 61 | 62 | def display(self, menuItem): 63 | """ 64 | Tells the delegate to display the selection. Advances to the next selection if the 65 | menuItem is visible==False 66 | """ 67 | self.delegate.prepareForRender(self.topLevelMenu) 68 | if (not menuItem.visible): 69 | self.advance() 70 | else: 71 | self.delegate.displayMenuItem(menuItem) 72 | 73 | def advance(self): 74 | """ 75 | Advances the displayed menu to the next visible option 76 | 77 | raises ValueError if all options are visible==False 78 | """ 79 | for i in self.currentMenu.options: 80 | self.currentMenu.nextSelection() 81 | selection = self.currentMenu.getSelection() 82 | if (selection.visible): 83 | self.display(selection) 84 | return 85 | raise ValueError("At least one option in a menu must be visible!") 86 | 87 | def select(self): 88 | """ 89 | Selects the current menu option. Calls menuItemClicked first. If it returns false, 90 | it uses the default logic. If true, it calls display with the current selection 91 | 92 | defaults: 93 | "menu" -> sets submenu as the current menu 94 | "back" -> sets parent menu as the current menu 95 | 96 | returns True if the default logic should be overridden 97 | 98 | throws ValueError if navigating back on a top-level menu 99 | 100 | """ 101 | selection = self.currentMenu.getSelection() 102 | if (not self.delegate.menuItemClicked(selection)): 103 | if (selection.type is "menu"): 104 | self.setMenu(selection) 105 | elif (selection.type is "back"): 106 | if (not self.currentMenu.parent): 107 | raise ValueError("Cannot navigate back when parent is None") 108 | self.setMenu(self.currentMenu.parent) 109 | else: 110 | self.display(self.currentMenu.getSelection()) 111 | 112 | class MenuDelegate(object): 113 | def prepareForRender(self, menu): 114 | """ 115 | Called before the menu needs to display. Useful for changing visibility. 116 | """ 117 | raise NotImplementedError 118 | 119 | def menuItemClicked(self, menuItem): 120 | """ 121 | Called when a menu item is selected. Useful for taking action on a menu item click. 122 | """ 123 | raise NotImplementedError 124 | 125 | def displayMenuItem(self, menuItem): 126 | """ 127 | Called when the menu item should be displayed. 128 | """ 129 | raise NotImplementedError 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Smart Bartender 2 | Why spend lots of money going out for drinks when you can have your own smart personal bartender at your service right in your home?! This bartender is built from a Raspberry Pi 3 and some common DIY electronics. 3 | 4 | ## Prerequisites for the Raspberry Pi 5 | Make sure you can connect a screen and keyboard to your Raspberry Pi. I like to use VNC to connect to the Pi. I created a [tutorial](https://www.youtube.com/watch?v=2iVK8dn-6x4) about how to set that up on a Mac. 6 | 7 | Make sure the following are installed: 8 | * Python 2.7 (should already be installed on most Raspberry Pi) 9 | * [pip](https://www.raspberrypi.org/documentation/linux/software/python.md) 10 | 11 | ### Enable SPI 12 | You'll need to enable SPI for the OLED screen to work properly. Typing the following command in the terminal will bring you to a configuration menu. 13 | 14 | ``` 15 | raspi-config 16 | ``` 17 | 18 | Then navigate to `Interfacing Options` and select `SPI`. Make sure it's turned on and reboot. 19 | 20 | See this [article](https://www.raspberrypi.org/documentation/hardware/raspberrypi/spi/) for more help if you need it. 21 | 22 | ### I2C 23 | Make sure i2c is also configured properly. Type 24 | 25 | ``` 26 | sudo vim /etc/modules 27 | ``` 28 | 29 | in the terminal 30 | 31 | press `i`, then make sure to paste the following two lines in the file: 32 | 33 | ``` 34 | i2c-bcm2708 35 | i2c-dev 36 | ``` 37 | 38 | press `esc` then `ZZ` to save and exit. 39 | 40 | ## OLED Setup 41 | The Raspberry Pi Guy has a nice script to setup the OLED screen on your raspberry pi. Download the following repository on your Pi: 42 | 43 | https://github.com/the-raspberry-pi-guy/OLED 44 | 45 | then navigate to the folder with the terminal 46 | 47 | ``` 48 | cd ~/path/to/directory 49 | ``` 50 | 51 | and run the installation script 52 | 53 | ``` 54 | sh OLEDinstall.sh 55 | ``` 56 | 57 | There is also a [guide](https://learn.adafruit.com/adafruit-oled-displays-for-raspberry-pi/setting-up) on the Adafruit website if you get stuck. 58 | 59 | ## Running the Code 60 | 61 | First, make sure to download this repository on your raspberry pi. Once you do, navigate to the downloaded folder in the terminal: 62 | 63 | ``` 64 | cd ~/path/to/directory 65 | ``` 66 | 67 | and install the dependencies 68 | 69 | ``` 70 | sudo pip install -r requirements.txt 71 | ``` 72 | 73 | You can start the bartender by running 74 | 75 | ``` 76 | sudo python bartender.py 77 | ``` 78 | 79 | ### How it Works 80 | There are two files that support the bartender.py file: 81 | 82 | #### drinks.py 83 | Holds all of the possible drink options. Drinks are filtered by the values in the pump configuration. If you want to add more drinks, add more entries to `drinks_list`. If you want to add more pump beverage options, add more entries to the `drink_options`. 84 | 85 | `drinks_list` entries have the following format: 86 | 87 | ``` 88 | { 89 | "name": "Gin & Tonic", 90 | "ingredients": { 91 | "gin": 50, 92 | "tonic": 150 93 | } 94 | } 95 | ``` 96 | 97 | `name` specifies a name that will be displayed on the OLED menu. This name doesn't have to be unique, but it will help the user identify which drink has been selected. `ingredients` contains a map of beverage options that are available in `drink_options`. Each key represents a possible drink option. The value is the amount of liquid in mL. *Note: you might need a higher value for carbonated beverages since some of the CO2 might come out of solution while pumping the liquid.* 98 | 99 | `drink_options` entries have the following format: 100 | 101 | ``` 102 | {"name": "Gin", "value": "gin"} 103 | ``` 104 | 105 | The name will be displayed on the pump configuration menu and the value will be assigned to the pump. The pump values will filter out drinks that the user can't make with the current pump configuration. 106 | 107 | ### pump_config.json 108 | The pump configuration persists information about pumps and the liquids that they are assigned to. An pump entry looks like this: 109 | 110 | ``` 111 | "pump_1": { 112 | "name": "Pump 1", 113 | "pin": 17, 114 | "value": "gin" 115 | } 116 | ``` 117 | 118 | Each pump key needs to be unique. It is comprised of `name`, `pin`, and `value`. `name` is the display name shown to the user on the pump configuration menu, `pin` is the GPIO pin attached to the relay for that particular pump, and `value` is the current selected drink. `value` doesn't need to be set initially, but it will be changed once you select an option from the configuration menu. 119 | 120 | Our bartender only has 6 pumps, but you could easily use more by adding more pump config entries. 121 | 122 | ### A Note on Cleaning 123 | After you use the bartender, you'll want to flush out the pump tubes in order to avoid bacteria growth. There is an easy way to do this in the configuration menu. Hook all the tubes up to a water source, then navigate to `configure`->`clean` and press the select button. All pumps will turn on to flush the existing liquid from the tubes. I take the tubes out of the water source halfway through to remove all liquid from the pumps. *Note: make sure you have a glass under the funnel to catch the flushed out liquid.* 124 | 125 | 126 | ### Running at Startup 127 | You can configure the bartender to run at startup by starting the program from the `rc.local` file. First, make sure to get the path to the repository directory by running 128 | 129 | ``` 130 | pwd 131 | ``` 132 | 133 | from the repository folder. Copy this to your clipboard. 134 | 135 | Next, type 136 | 137 | ``` 138 | sudo vim /etc/rc.local 139 | ``` 140 | 141 | to open the rc.local file. Next, press `i` to edit. Before the last line, add the following two lines: 142 | 143 | ``` 144 | cd your/pwd/path/here 145 | sudo python bartender.py & 146 | ``` 147 | 148 | `your/pwd/path/here` should be replaced with the path you copied above. `sudo python bartender.py &` starts the bartender program in the background. Finally, press `esc` then `ZZ` to save and exit. 149 | 150 | If that doesn't work, you can consult this [guide](https://www.dexterindustries.com/howto/run-a-program-on-your-raspberry-pi-at-startup/) for more options. 151 | -------------------------------------------------------------------------------- /bartender.py: -------------------------------------------------------------------------------- 1 | import gaugette.ssd1306 2 | import gaugette.platform 3 | import gaugette.gpio 4 | import time 5 | import sys 6 | import RPi.GPIO as GPIO 7 | import json 8 | import threading 9 | import traceback 10 | 11 | from dotstar import Adafruit_DotStar 12 | from menu import MenuItem, Menu, Back, MenuContext, MenuDelegate 13 | from drinks import drink_list, drink_options 14 | 15 | GPIO.setmode(GPIO.BCM) 16 | 17 | SCREEN_WIDTH = 128 18 | SCREEN_HEIGHT = 64 19 | 20 | LEFT_BTN_PIN = 13 21 | LEFT_PIN_BOUNCE = 1000 22 | 23 | RIGHT_BTN_PIN = 5 24 | RIGHT_PIN_BOUNCE = 2000 25 | 26 | OLED_RESET_PIN = 15 27 | OLED_DC_PIN = 16 28 | 29 | NUMBER_NEOPIXELS = 45 30 | NEOPIXEL_DATA_PIN = 26 31 | NEOPIXEL_CLOCK_PIN = 6 32 | NEOPIXEL_BRIGHTNESS = 64 33 | 34 | FLOW_RATE = 60.0/100.0 35 | 36 | class Bartender(MenuDelegate): 37 | def __init__(self): 38 | self.running = False 39 | 40 | # set the oled screen height 41 | self.screen_width = SCREEN_WIDTH 42 | self.screen_height = SCREEN_HEIGHT 43 | 44 | self.btn1Pin = LEFT_BTN_PIN 45 | self.btn2Pin = RIGHT_BTN_PIN 46 | 47 | # configure interrups for buttons 48 | GPIO.setup(self.btn1Pin, GPIO.IN, pull_up_down=GPIO.PUD_UP) 49 | GPIO.setup(self.btn2Pin, GPIO.IN, pull_up_down=GPIO.PUD_UP) 50 | 51 | # configure screen 52 | spi_bus = 0 53 | spi_device = 0 54 | gpio = gaugette.gpio.GPIO() 55 | spi = gaugette.spi.SPI(spi_bus, spi_device) 56 | 57 | # Very important... This lets py-gaugette 'know' what pins to use in order to reset the display 58 | self.led = gaugette.ssd1306.SSD1306(gpio, spi, reset_pin=OLED_RESET_PIN, dc_pin=OLED_DC_PIN, rows=self.screen_height, cols=self.screen_width) # Change rows & cols values depending on your display dimensions. 59 | self.led.begin() 60 | self.led.clear_display() 61 | self.led.display() 62 | self.led.invert_display() 63 | time.sleep(0.5) 64 | self.led.normal_display() 65 | time.sleep(0.5) 66 | 67 | # load the pump configuration from file 68 | self.pump_configuration = Bartender.readPumpConfiguration() 69 | for pump in self.pump_configuration.keys(): 70 | GPIO.setup(self.pump_configuration[pump]["pin"], GPIO.OUT, initial=GPIO.HIGH) 71 | 72 | # setup pixels: 73 | self.numpixels = NUMBER_NEOPIXELS # Number of LEDs in strip 74 | 75 | # Here's how to control the strip from any two GPIO pins: 76 | datapin = NEOPIXEL_DATA_PIN 77 | clockpin = NEOPIXEL_CLOCK_PIN 78 | self.strip = Adafruit_DotStar(self.numpixels, datapin, clockpin) 79 | self.strip.begin() # Initialize pins for output 80 | self.strip.setBrightness(NEOPIXEL_BRIGHTNESS) # Limit brightness to ~1/4 duty cycle 81 | 82 | # turn everything off 83 | for i in range(0, self.numpixels): 84 | self.strip.setPixelColor(i, 0) 85 | self.strip.show() 86 | 87 | print "Done initializing" 88 | 89 | @staticmethod 90 | def readPumpConfiguration(): 91 | return json.load(open('pump_config.json')) 92 | 93 | @staticmethod 94 | def writePumpConfiguration(configuration): 95 | with open("pump_config.json", "w") as jsonFile: 96 | json.dump(configuration, jsonFile) 97 | 98 | def startInterrupts(self): 99 | GPIO.add_event_detect(self.btn1Pin, GPIO.FALLING, callback=self.left_btn, bouncetime=LEFT_PIN_BOUNCE) 100 | GPIO.add_event_detect(self.btn2Pin, GPIO.FALLING, callback=self.right_btn, bouncetime=RIGHT_PIN_BOUNCE) 101 | 102 | def stopInterrupts(self): 103 | GPIO.remove_event_detect(self.btn1Pin) 104 | GPIO.remove_event_detect(self.btn2Pin) 105 | 106 | def buildMenu(self, drink_list, drink_options): 107 | # create a new main menu 108 | m = Menu("Main Menu") 109 | 110 | # add drink options 111 | drink_opts = [] 112 | for d in drink_list: 113 | drink_opts.append(MenuItem('drink', d["name"], {"ingredients": d["ingredients"]})) 114 | 115 | configuration_menu = Menu("Configure") 116 | 117 | # add pump configuration options 118 | pump_opts = [] 119 | for p in sorted(self.pump_configuration.keys()): 120 | config = Menu(self.pump_configuration[p]["name"]) 121 | # add fluid options for each pump 122 | for opt in drink_options: 123 | # star the selected option 124 | selected = "*" if opt["value"] == self.pump_configuration[p]["value"] else "" 125 | config.addOption(MenuItem('pump_selection', opt["name"], {"key": p, "value": opt["value"], "name": opt["name"]})) 126 | # add a back button so the user can return without modifying 127 | config.addOption(Back("Back")) 128 | config.setParent(configuration_menu) 129 | pump_opts.append(config) 130 | 131 | # add pump menus to the configuration menu 132 | configuration_menu.addOptions(pump_opts) 133 | # add a back button to the configuration menu 134 | configuration_menu.addOption(Back("Back")) 135 | # adds an option that cleans all pumps to the configuration menu 136 | configuration_menu.addOption(MenuItem('clean', 'Clean')) 137 | configuration_menu.setParent(m) 138 | 139 | m.addOptions(drink_opts) 140 | m.addOption(configuration_menu) 141 | # create a menu context 142 | self.menuContext = MenuContext(m, self) 143 | 144 | def filterDrinks(self, menu): 145 | """ 146 | Removes any drinks that can't be handled by the pump configuration 147 | """ 148 | for i in menu.options: 149 | if (i.type == "drink"): 150 | i.visible = False 151 | ingredients = i.attributes["ingredients"] 152 | presentIng = 0 153 | for ing in ingredients.keys(): 154 | for p in self.pump_configuration.keys(): 155 | if (ing == self.pump_configuration[p]["value"]): 156 | presentIng += 1 157 | if (presentIng == len(ingredients.keys())): 158 | i.visible = True 159 | elif (i.type == "menu"): 160 | self.filterDrinks(i) 161 | 162 | def selectConfigurations(self, menu): 163 | """ 164 | Adds a selection star to the pump configuration option 165 | """ 166 | for i in menu.options: 167 | if (i.type == "pump_selection"): 168 | key = i.attributes["key"] 169 | if (self.pump_configuration[key]["value"] == i.attributes["value"]): 170 | i.name = "%s %s" % (i.attributes["name"], "*") 171 | else: 172 | i.name = i.attributes["name"] 173 | elif (i.type == "menu"): 174 | self.selectConfigurations(i) 175 | 176 | def prepareForRender(self, menu): 177 | self.filterDrinks(menu) 178 | self.selectConfigurations(menu) 179 | return True 180 | 181 | def menuItemClicked(self, menuItem): 182 | if (menuItem.type == "drink"): 183 | self.makeDrink(menuItem.name, menuItem.attributes["ingredients"]) 184 | return True 185 | elif(menuItem.type == "pump_selection"): 186 | self.pump_configuration[menuItem.attributes["key"]]["value"] = menuItem.attributes["value"] 187 | Bartender.writePumpConfiguration(self.pump_configuration) 188 | return True 189 | elif(menuItem.type == "clean"): 190 | self.clean() 191 | return True 192 | return False 193 | 194 | def clean(self): 195 | waitTime = 20 196 | pumpThreads = [] 197 | 198 | # cancel any button presses while the drink is being made 199 | # self.stopInterrupts() 200 | self.running = True 201 | 202 | for pump in self.pump_configuration.keys(): 203 | pump_t = threading.Thread(target=self.pour, args=(self.pump_configuration[pump]["pin"], waitTime)) 204 | pumpThreads.append(pump_t) 205 | 206 | # start the pump threads 207 | for thread in pumpThreads: 208 | thread.start() 209 | 210 | # start the progress bar 211 | self.progressBar(waitTime) 212 | 213 | # wait for threads to finish 214 | for thread in pumpThreads: 215 | thread.join() 216 | 217 | # show the main menu 218 | self.menuContext.showMenu() 219 | 220 | # sleep for a couple seconds to make sure the interrupts don't get triggered 221 | time.sleep(2); 222 | 223 | # reenable interrupts 224 | # self.startInterrupts() 225 | self.running = False 226 | 227 | def displayMenuItem(self, menuItem): 228 | print menuItem.name 229 | self.led.clear_display() 230 | self.led.draw_text2(0,20,menuItem.name,2) 231 | self.led.display() 232 | 233 | def cycleLights(self): 234 | t = threading.currentThread() 235 | head = 0 # Index of first 'on' pixel 236 | tail = -10 # Index of last 'off' pixel 237 | color = 0xFF0000 # 'On' color (starts red) 238 | 239 | while getattr(t, "do_run", True): 240 | self.strip.setPixelColor(head, color) # Turn on 'head' pixel 241 | self.strip.setPixelColor(tail, 0) # Turn off 'tail' 242 | self.strip.show() # Refresh strip 243 | time.sleep(1.0 / 50) # Pause 20 milliseconds (~50 fps) 244 | 245 | head += 1 # Advance head position 246 | if(head >= self.numpixels): # Off end of strip? 247 | head = 0 # Reset to start 248 | color >>= 8 # Red->green->blue->black 249 | if(color == 0): color = 0xFF0000 # If black, reset to red 250 | 251 | tail += 1 # Advance tail position 252 | if(tail >= self.numpixels): tail = 0 # Off end? Reset 253 | 254 | def lightsEndingSequence(self): 255 | # make lights green 256 | for i in range(0, self.numpixels): 257 | self.strip.setPixelColor(i, 0xFF0000) 258 | self.strip.show() 259 | 260 | time.sleep(5) 261 | 262 | # turn lights off 263 | for i in range(0, self.numpixels): 264 | self.strip.setPixelColor(i, 0) 265 | self.strip.show() 266 | 267 | def pour(self, pin, waitTime): 268 | GPIO.output(pin, GPIO.LOW) 269 | time.sleep(waitTime) 270 | GPIO.output(pin, GPIO.HIGH) 271 | 272 | def progressBar(self, waitTime): 273 | interval = waitTime / 100.0 274 | for x in range(1, 101): 275 | self.led.clear_display() 276 | self.updateProgressBar(x, y=35) 277 | self.led.display() 278 | time.sleep(interval) 279 | 280 | def makeDrink(self, drink, ingredients): 281 | # cancel any button presses while the drink is being made 282 | # self.stopInterrupts() 283 | self.running = True 284 | 285 | # launch a thread to control lighting 286 | lightsThread = threading.Thread(target=self.cycleLights) 287 | lightsThread.start() 288 | 289 | # Parse the drink ingredients and spawn threads for pumps 290 | maxTime = 0 291 | pumpThreads = [] 292 | for ing in ingredients.keys(): 293 | for pump in self.pump_configuration.keys(): 294 | if ing == self.pump_configuration[pump]["value"]: 295 | waitTime = ingredients[ing] * FLOW_RATE 296 | if (waitTime > maxTime): 297 | maxTime = waitTime 298 | pump_t = threading.Thread(target=self.pour, args=(self.pump_configuration[pump]["pin"], waitTime)) 299 | pumpThreads.append(pump_t) 300 | 301 | # start the pump threads 302 | for thread in pumpThreads: 303 | thread.start() 304 | 305 | # start the progress bar 306 | self.progressBar(maxTime) 307 | 308 | # wait for threads to finish 309 | for thread in pumpThreads: 310 | thread.join() 311 | 312 | # show the main menu 313 | self.menuContext.showMenu() 314 | 315 | # stop the light thread 316 | lightsThread.do_run = False 317 | lightsThread.join() 318 | 319 | # show the ending sequence lights 320 | self.lightsEndingSequence() 321 | 322 | # sleep for a couple seconds to make sure the interrupts don't get triggered 323 | time.sleep(2); 324 | 325 | # reenable interrupts 326 | # self.startInterrupts() 327 | self.running = False 328 | 329 | def left_btn(self, ctx): 330 | if not self.running: 331 | self.menuContext.advance() 332 | 333 | def right_btn(self, ctx): 334 | if not self.running: 335 | self.menuContext.select() 336 | 337 | def updateProgressBar(self, percent, x=15, y=15): 338 | height = 10 339 | width = self.screen_width-2*x 340 | for w in range(0, width): 341 | self.led.draw_pixel(w + x, y) 342 | self.led.draw_pixel(w + x, y + height) 343 | for h in range(0, height): 344 | self.led.draw_pixel(x, h + y) 345 | self.led.draw_pixel(self.screen_width-x, h + y) 346 | for p in range(0, percent): 347 | p_loc = int(p/100.0*width) 348 | self.led.draw_pixel(x + p_loc, h + y) 349 | 350 | def run(self): 351 | self.startInterrupts() 352 | # main loop 353 | try: 354 | while True: 355 | time.sleep(0.1) 356 | 357 | except KeyboardInterrupt: 358 | GPIO.cleanup() # clean up GPIO on CTRL+C exit 359 | GPIO.cleanup() # clean up GPIO on normal exit 360 | 361 | traceback.print_exc() 362 | 363 | 364 | bartender = Bartender() 365 | bartender.buildMenu(drink_list, drink_options) 366 | bartender.run() 367 | 368 | 369 | 370 | 371 | --------------------------------------------------------------------------------