diff --git a/README b/README index 2f2ff040..08dc2d6d 100644 --- a/README +++ b/README @@ -1,3 +1,26 @@ +This is a forked version of the original Bartendro software which +supports the [Hello Drinkbot](https://www.facebook.com/groups/602734573508700) project + +(the original hellodrinkbot.com domain was lost, and now appears to be a malware +distribution vector) + +# Quickstart for Hello Drinkbot fork + + + +- git clone https://github.com/RichGibson/bartendro.git +- cd bartendro/ui +- #this requires python 2.7, for now. If you use Conda this works: +- conda create --name py27 python=2.7 +- conda activate py27 +- pip install -r requirements.txt +- cp bartendro.db.default bartendro.db +- sh ./start_bartendro.sh + +Got to http://http://127.0.0.1:8080/ + +---- + Programs for the various systems of the Bartendro drink dispensing robot. Created by Pierre Michael and Robert Kaye diff --git a/scripts/restartd.conf b/scripts/restartd.conf index 7e9c6a71..5e16ad63 100644 --- a/scripts/restartd.conf +++ b/scripts/restartd.conf @@ -19,5 +19,4 @@ # Example: # # restartd ".*restartd" "/bin/echo 'It is not running!' >/tmp/restartd.out" "/bin/echo 'It is running!' >/tmp/restartd.out" -bartendro "bartendro_server\.py" "/home/robert/bartendro/scripts/start_bartendro.sh" "/bin/true" -user_button "user_button\.py" "/home/robert/bartendro/scripts/user_button.py &" "/bin/true" +bartendro "bartendro_server\.py" "/home/pi/start_bartendro.sh" "/bin/true" diff --git a/ui/README.md b/ui/README.md index 5d4f5472..708d25a0 100644 --- a/ui/README.md +++ b/ui/README.md @@ -33,9 +33,7 @@ the required tables for you to start playing with. Configuration ------------- -You'll need to copy the config.py.default file to config.py . This will assume -the basic sane setting for your Bartendro configuration. These settings will be migrated -to the DB soon, so please take a look at the file to see what can be changed. +Configuration has been moved from a config.py file to the option table. Starting -------- diff --git a/ui/README_DEV.md b/ui/README_DEV.md new file mode 100644 index 00000000..6231d504 --- /dev/null +++ b/ui/README_DEV.md @@ -0,0 +1,211 @@ +# Developer notes on hello drinkbot fork of bartendro + +# July 2022 + +## A list of all of the routes + +In bartender/ + +grep -r 'app.route' ui/* + + +ui/README_DEV.md:grep -r 'app.route' ui/* +ui/bartendro/view/booze.py:@app.route('/booze') +ui/bartendro/view/booze.py:@app.route('/booze/') +ui/bartendro/view/booze.py:@app.route('/booze/all') +ui/bartendro/view/booze.py:@app.route('/booze/loaded') +ui/bartendro/view/trending.py:@app.route('/trending') +ui/bartendro/view/trending.py:@app.route('/trending/date/') +ui/bartendro/view/trending.py:@app.route('/trending/') +ui/bartendro/view/snooze.py:@app.route('/snooze') +ui/bartendro/view/admin/options.py:@app.route('/admin/options') +ui/bartendro/view/admin/options.py:@app.route('/admin/lost-passwd') +ui/bartendro/view/admin/options.py:@app.route('/admin/upload') +ui/bartendro/view/admin/dispenser.py:@app.route('/admin') +ui/bartendro/view/admin/dispenser.py:@app.route('/admin/save', methods=['POST']) +ui/bartendro/view/admin/user.py:@app.route("/admin/login", methods=["GET", "POST"]) +ui/bartendro/view/admin/user.py:@app.route("/admin/logout") +ui/bartendro/view/admin/booze.py:@app.route('/admin/booze') +ui/bartendro/view/admin/booze.py:@app.route('/admin/booze/edit/') +ui/bartendro/view/admin/booze.py:@app.route('/admin/booze/save', methods=['POST']) +ui/bartendro/view/admin/drink.py:@app.route('/admin/drink') +ui/bartendro/view/admin/debug.py:@app.route('/admin/debug') +ui/bartendro/view/admin/liquidlevel.py:@app.route('/admin/liquidlevel') +ui/bartendro/view/admin/report.py:@app.route('/admin/report') +ui/bartendro/view/admin/report.py:@app.route('/admin/report//') +ui/bartendro/view/root.py:@app.route('/') +ui/bartendro/view/root.py:@app.route('/shots') +ui/bartendro/view/root.py:@app.route('/graphical_shots') +ui/bartendro/view/drink/drink.py:@app.route('/drink/') +ui/bartendro/view/drink/drink.py:@app.route('/drink//go') +ui/bartendro/view/drink/drink.py:@app.route('/drink/sobriety') +ui/bartendro/view/drink/drink.py:@app.route('/drink/all') +ui/bartendro/view/drink/drink.py:@app.route('/drink/available') +ui/bartendro/view/ws/dispenser.py:@app.route('/ws/dispenser//on') +ui/bartendro/view/ws/dispenser.py:@app.route('/ws/dispenser//on/reverse') +ui/bartendro/view/ws/dispenser.py:@app.route('/ws/dispenser//off') +ui/bartendro/view/ws/dispenser.py:@app.route('/ws/dispenser//test') +ui/bartendro/view/ws/dispenser.py:@app.route('/ws/clean') +ui/bartendro/view/ws/dispenser.py:@app.route('/ws/clean/right') +ui/bartendro/view/ws/dispenser.py:@app.route('/ws/clean/left') +ui/bartendro/view/ws/misc.py:@app.route('/ws/reset') +ui/bartendro/view/ws/misc.py:@app.route('/ws/test') +ui/bartendro/view/ws/misc.py:@app.route('/ws/checklevels') +ui/bartendro/view/ws/misc.py:@app.route('/ws/download/bartendro.db') +ui/bartendro/view/ws/option.py:@app.route('/ws/options', methods=["POST", "GET"]) +ui/bartendro/view/ws/option.py:@app.route('/ws/upload', methods=["POST"]) +ui/bartendro/view/ws/option.py:@app.route('/ws/upload/confirm', methods=["POST"]) +ui/bartendro/view/ws/booze.py:@app.route('/ws/booze/match/') +ui/bartendro/view/ws/drink.py:@app.route('/ws/drink/') +ui/bartendro/view/ws/drink.py:@app.route('/ws/drink/custom') +ui/bartendro/view/ws/drink.py:@app.route('/ws/drink//available/') +ui/bartendro/view/ws/drink.py:@app.route('/ws/shots/') +ui/bartendro/view/ws/drink.py:@app.route('/ws/drink//load') +ui/bartendro/view/ws/drink.py:@app.route('/ws/drink//save', methods=["POST"]) +ui/bartendro/view/ws/liquidlevel.py:@app.route('/ws/liquidlevel/test/') +ui/bartendro/view/ws/liquidlevel.py:@app.route('/ws/liquidlevel/out//set') +ui/bartendro/view/ws/liquidlevel.py:@app.route('/ws/liquidlevel/low//set') +ui/bartendro/view/ws/liquidlevel.py:@app.route('/ws/liquidlevel/out/all/set') +ui/bartendro/view/ws/liquidlevel.py:@app.route('/ws/liquidlevel/low/all/set') + + + +# Bartendro software notes Mon Feb 11 15:49:36 PST 2019 + + +Goal? It would be nice to be able to have features from the bartendro software. + +Notes: +sqlite3 bartendro.db + +see options, including password +select * from option; +username: bartendro +password (default): boozemeup + +class Mixer in mixer.py is the point of it all.. +'''The mixer object is the heart of Bartendro. This is where the state of the bot +is managed, checked if drinks can be made, and actually make drinks. Everything +else in Bartendro lives for *this* *code*. :) ''' + +bartendro +/Users/richgibson/wa/pistonbot/bartendro/ui + + ./bartendro_server.py --debug + +To Add a view + +in bartendro/ui/bartendro/view +cp booze.py snooze.py + +in bartendro/ui/bartendro +edit __init__.py +from bartendro.view import snooze + +edit + @app.route('/booze') + @login_required + def booze(): + +templates in +bartendro/ui/content/templates + +cp booze snooze +edit snooze + +http://127.0.0.1:8080/snooze + +Yay! That worked. +Next: add an endpoint which talks to my pumps. + +the bartendro has a restfulish api. yay. + +/ws/drink/34?booze1=60&booze24=10&booze28=20&booze8=50 +/ws/drink/[drink id]?booze[booze id]=[qty ml]&booze24=10&... + +booze[booze.id]=[qty in ml] +booze1 = booze id=1, vodka +booze24 = booze id=24, triple sec + +The quantity is in ml + +I think I can hack my pumps in in ws/drink.py ws_make_drink() +rather than app.mixer.make_drink(drink, recipe) + +from bartendro import app +from bartendro import mixer +from bartendro import db +from bartendro.model.drink import Drink +from bartendro.model.drink import DrinkName +drink = Drink.query.filter_by(id=1)[0] +drink +(1,Sour Apple Martini,A fruity martini made with tart apple pucker and vodka. Stir or shake after it's dispensed.,(1) (2))> +# mixer object normally requires driver and mc - but in my hacked world... +# this almost works, for large values of 'almost' +mix=mixer.Mixer(None,None) + + +The question is: how much do I want to hack it? +- a custom app.mixer.py +- hack /ui/bartendro/view/ws/drink.py + + +app.mixer.dispense_shot +app.mixer.make_drink + + +-- +- made hello_drinkbot branch on my git hub +- checked it out on pi +- to install dependencies +```pip install -r requirements.txt``` +- to start bartendro +```./bartendro_server.py --debug``` + +(need to export : +export BARTENDRO_SOFTWARE_ONLY=1) + +specify address +./bartendro_server.py --debug -t 10.1.10.214 + +now sqlalchemy.orm.exc.FlushError +but that is cool. + +```./bartendro_server.py --debug -t 10.1.10.214``` +... +I have the code runnnig on the pi except for that sql error. + + +Methods from bartendro/ui/bartendro/mixer.py + def _can_make_drink(self, boozes, booze_dict): + def _check_liquid_levels(self): + def _dispense_recipe(self, recipe, always_fast = False): + def _state_check(self): + def _state_current_sense(self): + def _state_error(self): + def _state_hard_out(self): + def _state_low(self): + def _state_out(self): + def _state_pour_done(self): + def _state_pouring(self): + def _state_pre_pour(self): + def _state_pre_shot(self): + def _state_ready(self): + def _state_test_dispense(self): + def check_levels(self): + def clean(self): + def clean_left(self): + def clean_right(self): + def dispense_ml(self, dispenser, ml): + def dispense_shot(self, dispenser, ml): + def do_event(self, event): + def get_available_drink_list(self): + def liquid_level_test(self, dispenser, threshold): + def make_drink(self, drink, recipe): + def reset(self): + +It may be driver.py /Users/richgibson/wa/pistonbot/bartendro/ui/bartendro/router/driver.py +which we need to mess with. + +select count(booze_id), name from drink_booze db, booze bz where booze_id=bz.id group by name order by name; + diff --git a/ui/bartendro.db-2019-05-20 b/ui/bartendro.db-2019-05-20 new file mode 100644 index 00000000..5cb90286 Binary files /dev/null and b/ui/bartendro.db-2019-05-20 differ diff --git a/ui/bartendro.db.default b/ui/bartendro.db.default index 1eea75ab..22de9ea6 100644 Binary files a/ui/bartendro.db.default and b/ui/bartendro.db.default differ diff --git a/ui/bartendro/__init__.py b/ui/bartendro/__init__.py index bf4131d3..4d4cf64a 100644 --- a/ui/bartendro/__init__.py +++ b/ui/bartendro/__init__.py @@ -2,8 +2,12 @@ import os from flask import Flask, request, session, g, redirect, url_for, abort, render_template, flash -from flask.ext.sqlalchemy import SQLAlchemy -from flask.ext.login import LoginManager + +from flask_sqlalchemy import SQLAlchemy +from flask_login import LoginManager + +#from flask.ext.sqlalchemy import SQLAlchemy +#from flask.ext.login import LoginManager from sqlalchemy.orm import mapper, relationship, backref SQLALCHEMY_DATABASE_FILE = 'bartendro.db' @@ -31,6 +35,7 @@ from bartendro.model.drink_name import DrinkName from bartendro.model.drink_booze import DrinkBooze + from bartendro.model.booze import Booze from bartendro.model.booze_group import BoozeGroup from bartendro.model.booze_group_booze import BoozeGroupBooze @@ -61,6 +66,8 @@ # Import views from bartendro.view import root, trending +from bartendro.view import booze +from bartendro.view import snooze from bartendro.view.admin import booze as booze_admin, drink as drink_admin, \ dispenser as admin_dispenser, report, liquidlevel, user, options, debug from bartendro.view.drink import drink diff --git a/ui/bartendro/clean.py b/ui/bartendro/clean.py index a159b1bf..dd1ff7b0 100644 --- a/ui/bartendro/clean.py +++ b/ui/bartendro/clean.py @@ -4,7 +4,8 @@ from threading import Thread from bartendro import db, app from bartendro.error import BartendroBrokenError -from bartendro.router.driver import MOTOR_DIRECTION_FORWARD +#from bartendro.router.driver import MOTOR_DIRECTION_FORWARD +from bartendro.router.hello_drinkbot_driver import MOTOR_DIRECTION_FORWARD CLEAN_DURATION = 10 # seconds @@ -31,7 +32,7 @@ def clean(self): else: disp_list.extend(self.left_set) else: - for d in xrange(self.mixer.disp_count): + for d in range(self.mixer.disp_count): disp_list.append(d) self.mixer.driver.led_clean() @@ -52,5 +53,5 @@ def clean(self): try: self.mixer.check_levels() - except BartendroBrokenError, msg: + except BartendroBrokenError as msg: log.error("Post clean: %s" % msg) diff --git a/ui/bartendro/form/booze.py b/ui/bartendro/form/booze.py index 08462fdc..b8842c27 100644 --- a/ui/bartendro/form/booze.py +++ b/ui/bartendro/form/booze.py @@ -1,17 +1,20 @@ #!/usr/bin/env python -from wtforms import Form, TextField, DecimalField, HiddenField, validators, \ +from wtforms import Form, StringField, DecimalField, HiddenField, validators, \ TextAreaField, SubmitField, SelectField +#from wtforms import Form, TextField, DecimalField, HiddenField, validators, \ +# TextAreaField, SubmitField, SelectField from bartendro.model import booze class BoozeForm(Form): id = HiddenField(u"id", default=0) - name = TextField(u"Name", [validators.Length(min=3, max=255)]) - brand = TextField(u"Brand") # Currently unused + name = StringField(u"Name", [validators.Length(min=3, max=255)]) + brand = StringField(u"Brand") # Currently unused desc = TextAreaField(u"Description", [validators.Length(min=3, max=1024)]) abv = DecimalField(u"ABV", [validators.NumberRange(0, 97)], default=0, places=0) type = SelectField(u"Type", [validators.NumberRange(0, len(booze.booze_types))], choices=booze.booze_types, coerce=int) + image = StringField(u"image") # save = SubmitField(u"save") cancel = SubmitField(u"cancel") diff --git a/ui/bartendro/form/login.py b/ui/bartendro/form/login.py index d434075a..a96d1d85 100644 --- a/ui/bartendro/form/login.py +++ b/ui/bartendro/form/login.py @@ -1,8 +1,8 @@ #!/usr/bin/env python -from wtforms import Form, TextField, PasswordField, SubmitField, SelectField, validators +from wtforms import Form, StringField, PasswordField, SubmitField, SelectField, validators class LoginForm(Form): - user = TextField(u"Name", [validators.Length(min=3, max=255)]) + user = StringField(u"Name", [validators.Length(min=3, max=255)]) password = PasswordField(u"Password", [validators.Length(min=3, max=255)]) login = SubmitField(u"login") diff --git a/ui/bartendro/global_lock.py b/ui/bartendro/global_lock.py index 5052b69b..fa301a17 100755 --- a/ui/bartendro/global_lock.py +++ b/ui/bartendro/global_lock.py @@ -68,9 +68,10 @@ def get_state(self): # If we're not running inside uwsgi, then we can't keep global state if not have_uwsgi: return self.state - uwsgi.lock() - state = uwsgi.sharedarea_readbyte(1) - uwsgi.unlock() + # this gaves me an error under uwsgi, so comment it out. + #uwsgi.lock() + #state = uwsgi.sharedarea_readbyte(1) + #uwsgi.unlock() return state diff --git a/ui/bartendro/mixer.py b/ui/bartendro/mixer.py index 4470b0bd..14a8a19d 100644 --- a/ui/bartendro/mixer.py +++ b/ui/bartendro/mixer.py @@ -1,13 +1,15 @@ # -*- coding: utf-8 -*- +import pdb import logging import sys import traceback from time import sleep, time from threading import Thread from flask import Flask, current_app -from flask.ext.sqlalchemy import SQLAlchemy +from flask_sqlalchemy import SQLAlchemy import memcache from sqlalchemy.orm import mapper, relationship, backref +from sqlalchemy.sql import text from bartendro import db, app from bartendro import fsm from bartendro.clean import CleanCycle @@ -22,32 +24,36 @@ from bartendro.error import BartendroBusyError, BartendroBrokenError, BartendroCantPourError, BartendroCurrentSenseError TICKS_PER_ML = 2.78 -CALIBRATE_ML = 60 +CALIBRATE_ML = 60 CALIBRATION_TICKS = TICKS_PER_ML * CALIBRATE_ML FULL_SPEED = 255 HALF_SPEED = 166 -SLOW_DISPENSE_THRESHOLD = 20 # ml -MAX_DISPENSE = 1000 # ml max dispense per call. Just for sanity. :) +SLOW_DISPENSE_THRESHOLD = 20 # ml +MAX_DISPENSE = 1000 # ml max dispense per call. Just for sanity. :) -LIQUID_OUT_THRESHOLD = 75 -LIQUID_LOW_THRESHOLD = 120 +LIQUID_OUT_THRESHOLD = 75 +LIQUID_LOW_THRESHOLD = 120 -LL_OUT = 0 -LL_OK = 1 -LL_LOW = 2 +LL_OUT = 0 +LL_OK = 1 +LL_LOW = 2 log = logging.getLogger('bartendro') + class BartendroLiquidLevelReadError(Exception): pass + class Recipe(object): ''' Define everything related to dispensing one or more liquids at the same time ''' + def __init__(self): self.data = {} self.drink = None # Use for dispensing drinks - self.booze = None # Use for dispensing single shots of one booze + self.booze = None # Use for dispensing single shots of one booze + class Mixer(object): '''The mixer object is the heart of Bartendro. This is where the state of the bot @@ -55,11 +61,15 @@ class Mixer(object): else in Bartendro lives for *this* *code*. :) ''' def __init__(self, driver, mc): - self.driver = driver - self.mc = mc - self.disp_count = self.driver.count() - self.do_event(fsm.EVENT_START) - self.err = "" + try: + if driver: + self.driver = driver + self.mc = mc + self.disp_count = self.driver.count() + self.do_event(fsm.EVENT_START) + self.err = "" + except BaseException as err: + print('error in Mixer.__init__()',err) def check_levels(self): with BartendroLock(app.globals): @@ -67,12 +77,16 @@ def check_levels(self): def dispense_shot(self, dispenser, ml): r = Recipe() - r.data = { dispenser.booze.id : ml } + r.data = {dispenser.booze.id: ml} r.booze = dispenser.booze self.recipe = r + print('in dispense shot') with BartendroLock(app.globals): + + print('in dispense shot before') self.do_event(fsm.EVENT_MAKE_SHOT) + print('in dispense shot after') t = int(time()) slog = ShotLog(dispenser.booze.id, t, ml) db.session.add(slog) @@ -80,7 +94,7 @@ def dispense_shot(self, dispenser, ml): def dispense_ml(self, dispenser, ml): r = Recipe() - r.data = { dispenser.booze.id : ml } + r.data = {dispenser.booze.id: ml} r.booze = dispenser.booze self.recipe = r @@ -92,13 +106,13 @@ def make_drink(self, drink, recipe): r.data = recipe r.drink = drink self.recipe = r - + log.info("make_drink drink: %r reciple: %r " % (drink, recipe)) with BartendroLock(app.globals): self.do_event(fsm.EVENT_MAKE_DRINK) if drink and drink.id: size = 0 for k in recipe.keys(): - size += recipe[k] + size += recipe[k] t = int(time()) dlog = DrinkLog(drink.id, t, size) db.session.add(dlog) @@ -106,18 +120,21 @@ def make_drink(self, drink, recipe): def do_event(self, event): cur_state = app.globals.get_state() - + log.info("do_event event: %r " % (event)) while True: next_state = None for t_state, t_event, t_next_state in fsm.transition_table: if t_state == cur_state and event == t_event: next_state = t_next_state + log.info("do_event next_state: %r " % (next_state)) break - + if not next_state: - log.error("Current state %d, event %d. No next state." % (cur_state, event)) - raise BartendroBrokenError("Bartendro is unable to pour drinks right now. Sorry.") - #print "cur state: %d event: %d next state: %d" % (cur_state, event, next_state) + log.error("Current state %d, event %d. No next state." % + (cur_state, event)) + #print( "cur state: %d event: %d next state: %d" % (cur_state, event, next_state)) + raise BartendroBrokenError( + "Bartendro is unable to pour drinks right now. Sorry.") try: if next_state == fsm.STATE_PRE_POUR: @@ -147,24 +164,27 @@ def do_event(self, event): else: self._state_error() app.globals.set_state(fsm.STATE_ERROR) - log.error("Current state: %d, event %d. Can't find next state." % (cur_state, event)) - raise BartendroBrokenError("Internal error. Bartendro has had one too many.") + log.error("Current state: %d, event %d. Can't find next state." % ( + cur_state, event)) + raise BartendroBrokenError( + "Internal error. Bartendro has had one too many.") - except BartendroBrokenError, err: + except BartendroBrokenError as err: exc_type, exc_value, exc_traceback = sys.exc_info() - #traceback.print_tb(exc_traceback) + # traceback.print_tb(exc_traceback) self._state_error() app.globals.set_state(fsm.STATE_ERROR) raise - except BartendroCantPourError, err: + except BartendroCantPourError as err: exc_type, exc_value, exc_traceback = sys.exc_info() - #traceback.print_tb(exc_traceback) + # traceback.print_tb(exc_traceback) raise - - except BartendroCurrentSenseError, err: + + except BartendroCurrentSenseError as err: + exc_type, exc_value, exc_traceback = sys.exc_info() - #traceback.print_tb(exc_traceback) + # traceback.print_tb(exc_traceback) raise BartendroBrokenError(err) cur_state = next_state @@ -221,7 +241,8 @@ def _state_pre_shot(self): except BartendroLiquidLevelReadError: raise BartendroBrokenError("Failed to read liquid levels") - booze_id = self.recipe.data.keys()[0] + lst=[k for k in self.recipe.data.keys()] + booze_id=lst[0] dispensers = db.session.query(Dispenser).order_by(Dispenser.id).all() for i, disp in enumerate(dispensers): if disp.booze_id == booze_id: @@ -235,7 +256,8 @@ def _state_pre_shot(self): else: app.globals.set_state(fsm.STATE_HARD_OUT) - raise BartendroCantPourError("Cannot make drink: Dispenser %d is out of booze." % (i+1)) + raise BartendroCantPourError( + "Cannot make drink: Dispenser %d is out of booze." % (i+1)) break return fsm.EVENT_LL_OK @@ -270,14 +292,15 @@ def _state_error(self): def _state_pouring(self): self.driver.led_dispense() + log.info('Start state pouring') recipe = {} size = 0 log_lines = {} sql = "SELECT id FROM booze WHERE type = :d" - ext_booze_list = db.session.query("id") \ - .from_statement(sql) \ - .params(d=BOOZE_TYPE_EXTERNAL).all() + ext_booze_list = db.session.query(text("id")) \ + .from_statement(text(sql)) \ + .params(d=BOOZE_TYPE_EXTERNAL).all() ext_boozes = {} for booze in ext_booze_list: ext_boozes[booze[0]] = 1 @@ -289,32 +312,37 @@ def _state_pouring(self): continue found = False - for i in xrange(self.disp_count): + for i in range(self.disp_count): disp = dispensers[i] - if booze_id == disp.booze_id: # if we're out of booze, don't consider this drink if app.options.use_liquid_level_sensors and disp.out == LL_OUT: - raise BartendroCantPourError("Cannot make drink: Dispenser %d is out of booze." % (i+1)) + raise BartendroCantPourError( + "Cannot make drink: Dispenser %d is out of booze." % (i+1)) found = True ml = self.recipe.data[booze_id] + ml = int(ml) if ml <= 0: - log_lines[i] = " %-2d %-32s %d ml (not dispensed)" % (i, "%s (%d)" % (disp.booze.name, disp.booze.id), ml) + log_lines[i] = " %-2d %-32s %d ml (not dispensed)" % ( + i, "%s (%d)" % (disp.booze.name, disp.booze.id), ml) continue if ml > MAX_DISPENSE: - raise BartendroCantPourError("Cannot make drink. Invalid dispense quantity: %d ml. (Max %d ml)" % (ml, MAX_DISPENSE)) + raise BartendroCantPourError( + "Cannot make drink. Invalid dispense quantity: %d ml. (Max %d ml)" % (ml, MAX_DISPENSE)) - recipe[i] = ml + recipe[i] = ml size += ml - log_lines[i] = " %-2d %-32s %d ml" % (i, "%s (%d)" % (disp.booze.name, disp.booze.id), ml) - self.driver.set_motor_direction(i, MOTOR_DIRECTION_FORWARD); + log_lines[i] = " %-2d %-32s %d ml" % ( + i, "%s (%d)" % (disp.booze.name, disp.booze.id), ml) + self.driver.set_motor_direction(i, MOTOR_DIRECTION_FORWARD) continue if not found: - raise BartendroCantPourError("Cannot make drink. I don't have the required booze: %d" % booze_id) + raise BartendroCantPourError( + "Cannot make drink. I don't have the required booze: %d" % booze_id) self._dispense_recipe(recipe) @@ -329,15 +357,15 @@ def _state_pouring(self): return fsm.EVENT_POUR_DONE def _state_test_dispense(self): - - booze_id = self.recipe.data.keys()[0] + lst=[k for k in self.recipe.data.keys()] + booze_id=lst[0] ml = self.recipe.data[booze_id] recipe = {} dispensers = db.session.query(Dispenser).order_by(Dispenser.id).all() - for i in xrange(self.disp_count): + for i in range(self.disp_count): if booze_id == dispensers[i].booze_id: - recipe[i] = ml + recipe[i] = ml self._dispense_recipe(recipe, True) break @@ -365,20 +393,22 @@ def clean_left(self): def liquid_level_test(self, dispenser, threshold): if app.globals.get_state() == fsm.STATE_ERROR: - return - if not app.options.use_liquid_level_sensors: return + return + if not app.options.use_liquid_level_sensors: + return - log.info("Start liquid level test: (disp %s thres: %d)" % (dispenser, threshold)) + log.info("Start liquid level test: (disp %s thres: %d)" % + (dispenser, threshold)) if not self.driver.update_liquid_levels(): raise BartendroBrokenError("Failed to update liquid levels") sleep(.01) level = self.driver.get_liquid_level(dispenser) - log.info("initial reading: %d" % level) + log.info("initial reading: %d" % level) if level <= threshold: - log.info("liquid is out before starting: %d" % level) - return + log.info("liquid is out before starting: %d" % level) + return last = -1 self.driver.start(dispenser) @@ -389,12 +419,12 @@ def liquid_level_test(self, dispenser, threshold): sleep(.01) level = self.driver.get_liquid_level(dispenser) if level != last: - log.info(" %d" % level) + log.info(" %d" % level) last = level self.driver.stop(dispenser) log.info("Stopped at level: %d" % level) - sleep(.1); + sleep(.1) level = self.driver.get_liquid_level(dispenser) log.info("motor stopped at level: %d" % level) @@ -403,54 +433,102 @@ def get_available_drink_list(self): return [] can_make = self.mc.get("available_drink_list") - if can_make: + if can_make: return can_make - add_boozes = db.session.query("abstract_booze_id") \ - .from_statement("""SELECT bg.abstract_booze_id - FROM booze_group bg - WHERE id - IN (SELECT distinct(bgb.booze_group_id) - FROM booze_group_booze bgb, dispenser - WHERE bgb.booze_id = dispenser.booze_id)""") - - if app.options.use_liquid_level_sensors: + #sql = """SELECT bg.abstract_booze_id FROM booze_group bg + # WHERE id IN + # (SELECT distinct(bgb.booze_group_id) FROM booze_group_booze bgb, dispenser + # WHERE bgb.booze_id = dispenser.booze_id)""" + #add_boozes=db.session.query(sql) + + sql = """SELECT bg.abstract_booze_id FROM booze_group bg + WHERE id IN + (SELECT distinct(bgb.booze_group_id) FROM booze_group_booze bgb, dispenser + WHERE bgb.booze_id = dispenser.booze_id)""" + + add_boozes = db.session.query(text("abstract_booze_id")) \ + .from_statement(text(sql)) + + #add_boozes = db.session.query(text("abstract_booze_id")) \ + # .from_statement(text("""SELECT bg.abstract_booze_id + # FROM booze_group bg + # WHERE id + # IN (SELECT distinct(bgb.booze_group_id) + # FROM booze_group_booze bgb, dispenser + # WHERE bgb.booze_id = dispenser.booze_id)""")) + + if app.options.use_liquid_level_sensors: sql = "SELECT booze_id FROM dispenser WHERE out == 1 or out == 2 ORDER BY id LIMIT :d" else: + sql = text("SELECT booze_id FROM dispenser ORDER BY id LIMIT :d") sql = "SELECT booze_id FROM dispenser ORDER BY id LIMIT :d" - boozes = db.session.query("booze_id") \ - .from_statement(sql) \ - .params(d=self.disp_count).all() + import pdb + #pdb.set_trace() + boozes = db.session.query(text("booze_id")).from_statement(text(sql)).params(d=self.disp_count).all() boozes.extend(add_boozes) # Load whatever external boozes we have and add them to this list sql = "SELECT id FROM booze WHERE type = :d" - ext_boozes = db.session.query("id") \ - .from_statement(sql) \ - .params(d=BOOZE_TYPE_EXTERNAL).all() + ext_boozes = db.session.query(text("id")).from_statement(text(sql)).params(d=BOOZE_TYPE_EXTERNAL).all() + booze_dict = {} + for booze_id in boozes: + booze_dict[booze_id[0]] = 1 + + drinks = db.session.query(text("drink_id"), text("booze_id")) \ + .from_statement(text("SELECT d.id AS drink_id, db.booze_id AS booze_id FROM drink d, drink_booze db WHERE db.drink_id = d.id ORDER BY d.id, db.booze_id")) \ + .all() + last_drink = -1 + boozes = [] + can_make = [] + for drink_id, booze_id in drinks: + if last_drink < 0: + last_drink = drink_id + if drink_id != last_drink: + if self._can_make_drink(boozes, booze_dict): + can_make.append(last_drink) + boozes = [] + boozes.append(booze_id) + last_drink = drink_id + + if self._can_make_drink(boozes, booze_dict): + can_make.append(last_drink) + + self.mc.set("available_drink_list", can_make) + return can_make + + # ---------------------------------------- + # Private methods + # ---------------------------------------- + ext_boozes = db.session.query(text("id")) \ + .from_statement(text(sql)).all() boozes.extend(ext_boozes) + #ext_boozes = db.session.query("id") \ + # .from_statement(text(sql)) \ + # .params(d=BOOZE_TYPE_EXTERNAL).all() booze_dict = {} for booze_id in boozes: booze_dict[booze_id[0]] = 1 - drinks = db.session.query("drink_id", "booze_id") \ - .from_statement("SELECT d.id AS drink_id, db.booze_id AS booze_id FROM drink d, drink_booze db WHERE db.drink_id = d.id ORDER BY d.id, db.booze_id") \ + drinks = db.session.query(text("drink_id"), text("booze_id")) \ + .from_statement(text("SELECT d.id AS drink_id, db.booze_id AS booze_id FROM drink d, drink_booze db WHERE db.drink_id = d.id ORDER BY d.id, db.booze_id")) \ .all() last_drink = -1 boozes = [] can_make = [] for drink_id, booze_id in drinks: - if last_drink < 0: last_drink = drink_id + if last_drink < 0: + last_drink = drink_id if drink_id != last_drink: - if self._can_make_drink(boozes, booze_dict): + if self._can_make_drink(boozes, booze_dict): can_make.append(last_drink) boozes = [] boozes.append(booze_id) last_drink = drink_id - if self._can_make_drink(boozes, booze_dict): + if self._can_make_drink(boozes, booze_dict): can_make.append(last_drink) self.mc.set("available_drink_list", can_make) @@ -464,15 +542,16 @@ def _check_liquid_levels(self): """ Ask the dispense to update their own liquid levels and then fetch the levels and set the machine state accordingly. """ - if not app.options.use_liquid_level_sensors: + if not app.options.use_liquid_level_sensors: return LL_OK ll_state = LL_OK - log.info("mixer.check_liquid_levels: check levels"); + log.info("mixer.check_liquid_levels: check levels") # step 1: ask the dispensers to update their liquid levels if not self.driver.update_liquid_levels(): - raise BartendroLiquidLevelReadError("Failed to update liquid levels") + raise BartendroLiquidLevelReadError( + "Failed to update liquid levels") # wait for the dispensers to determine the levels sleep(.01) @@ -487,9 +566,11 @@ def _check_liquid_levels(self): level = self.driver.get_liquid_level(i) if level < 0: - raise BartendroLiquidLevelReadError("Failed to read liquid levels from dispenser %d" % (i+1)) + raise BartendroLiquidLevelReadError( + "Failed to read liquid levels from dispenser %d" % (i+1)) - log.info("dispenser %d level: %d (stored: %d)" % (i, level, dispenser.out)) + log.info("dispenser %d level: %d (stored: %d)" % + (i, level, dispenser.out)) if level <= LIQUID_OUT_THRESHOLD: ll_state = LL_OUT @@ -522,21 +603,26 @@ def _check_liquid_levels(self): return ll_state - def _dispense_recipe(self, recipe, always_fast = False): + def _dispense_recipe(self, recipe, always_fast=False): active_disp = [] for disp in recipe: if not recipe[disp]: continue + print(type(recipe[disp])) + print(type(TICKS_PER_ML)) + recipe[disp]=int(recipe[disp]) ticks = int(recipe[disp] * TICKS_PER_ML) if recipe[disp] < SLOW_DISPENSE_THRESHOLD and not always_fast: - speed = HALF_SPEED + speed = HALF_SPEED else: - speed = FULL_SPEED - - self.driver.set_motor_direction(disp, MOTOR_DIRECTION_FORWARD); - if not self.driver.dispense_ticks(disp, ticks, speed): - raise BartendroBrokenError("Dispense error. Dispense %d ticks, speed %d on dispenser %d failed." % (ticks, speed, disp + 1)) + speed = FULL_SPEED + self.driver.set_motor_direction(disp, MOTOR_DIRECTION_FORWARD) + # todo: create dispense_ml, why do we care about 'ticks'? + #if not self.driver.dispense_ticks(disp, ticks, speed): + if not self.driver.dispense_ml(disp, ticks, speed): + raise BartendroBrokenError( + "Dispense error. Dispense %d ml, speed %d on dispenser %d failed." % (recipe[disp], speed, disp + 1)) active_disp.append(disp) sleep(.01) @@ -544,23 +630,27 @@ def _dispense_recipe(self, recipe, always_fast = False): for disp in active_disp: while True: (is_dispensing, over_current) = app.driver.is_dispensing(disp) - log.debug("is_disp %d, over_cur %d" % (is_dispensing, over_current)) + log.debug("is_disp %d, over_cur %d" % + (is_dispensing, over_current)) # If we get errors here, try again. Running motors can cause noisy comm lines if is_dispensing < 0 or over_current < 0: - log.error("Is dispensing test on dispenser %d failed. Ignoring." % (disp + 1)) + log.error( + "Is dispensing test on dispenser %d failed. Ignoring." % (disp + 1)) sleep(.2) continue if over_current: - raise BartendroCurrentSenseError("One of the pumps did not operate properly. Your drink is broken. Sorry. :(") + raise BartendroCurrentSenseError( + "One of the pumps did not operate properly. Your drink is broken. Sorry. :(") - if is_dispensing == 0: - break + if is_dispensing == 0: + break sleep(.1) def _can_make_drink(self, boozes, booze_dict): + #log.info("in _can_make_drink") ok = True for booze in boozes: try: @@ -568,4 +658,3 @@ def _can_make_drink(self, boozes, booze_dict): except KeyError: ok = False return ok - diff --git a/ui/bartendro/model/booze.py b/ui/bartendro/model/booze.py index e100b47d..26cdd0e3 100644 --- a/ui/bartendro/model/booze.py +++ b/ui/bartendro/model/booze.py @@ -27,6 +27,7 @@ class Booze(db.Model): name = Column(UnicodeText, nullable=False) brand = Column(UnicodeText, nullable=True) desc = Column(UnicodeText, nullable=False) + image = Column(UnicodeText, nullable=True) abv = Column(Integer, default=0) type = Column(Integer, default=0) @@ -34,7 +35,7 @@ class Booze(db.Model): UniqueConstraint('name', name='booze_name_undx') query = db.session.query_property() - def __init__(self, name = u'', brand = u'', desc = u'', abv = 0, type = 0, out = 0, data = None): + def __init__(self, name = u'', brand = u'', desc = u'', abv = 0, type = 0, out = 0, image=None, data = None): if data: self.update(data) return @@ -44,6 +45,7 @@ def __init__(self, name = u'', brand = u'', desc = u'', abv = 0, type = 0, out = self.abv = abv self.type = type self.out = out + self.image = image def update(self, data): self.name = data['name'] @@ -51,6 +53,9 @@ def update(self, data): self.brand = data['brand'] self.abv = int(data['abv']) self.type = int(data['type']) + self.image = data['image'] + # what was I thinking with an int? rlg + #self.image = int(data['image']) def is_abstract(self): return len(self.booze_group) diff --git a/ui/bartendro/options.py b/ui/bartendro/options.py index 0df95816..b9e9a795 100644 --- a/ui/bartendro/options.py +++ b/ui/bartendro/options.py @@ -17,6 +17,7 @@ u'taster_size' : 30, u'shot_size' : 30, u'test_dispense_ml' : 10, + u'dispenser_count' : 4, u'show_strength' : True, u'show_size' : True, u'show_taster' : False, @@ -37,7 +38,8 @@ def add(self, key, value): def setup_options_table(): '''Check to make sure the options table is present''' - + # this fails with the 'wrong' version of Flask-SQLAlchemy is loaded + # if it fails for you, look there. if not db.engine.dialect.has_table(db.engine.connect(), "option"): log.info("Creating options table") option = Option() @@ -87,15 +89,24 @@ def load_options(): options = Options() for o in db.session.query(Option).all(): + try: + value = o.value + # TODO: if we care to keep python 2.7, then revisit this code. + if isinstance(bartendro_options[o.key], int): value = int(o.value) - elif isinstance(bartendro_options[o.key], unicode): - value = unicode(o.value) - elif isinstance(bartendro_options[o.key], boolean): - value = boolean(o.value) - else: - raise BadConfigOptionsError + + # we don't have any booleans in the option table. I think. + #elif isinstance(bartendro_options[o.key], boolean): + # value = boolean(o.value) + + #elif isinstance(bartendro_options[o.key], unicode): + # value = unicode(o.value) + #else: + # #raise BadConfigOptionsError + # pass + except KeyError: # Ignore options we don't understand pass diff --git a/ui/bartendro/router/__init__.py b/ui/bartendro/router/__init__.py index 139597f9..e69de29b 100644 --- a/ui/bartendro/router/__init__.py +++ b/ui/bartendro/router/__init__.py @@ -1,2 +0,0 @@ - - diff --git a/ui/bartendro/router/dispenser_select.py b/ui/bartendro/router/dispenser_select.py index 8154bc0e..6ea934af 100755 --- a/ui/bartendro/router/dispenser_select.py +++ b/ui/bartendro/router/dispenser_select.py @@ -4,7 +4,7 @@ import os import logging from time import sleep -from bartendro.error import BartendroBrokenError +#from bartendro.error import BartendroBrokenError from bartendro import app ROUTER_BUS = 1 @@ -17,13 +17,15 @@ ROUTER_CMD_RESET = 255 log = logging.getLogger('bartendro') - try: import smbus smbus_missing = 0 -except ImportError, e: - if e.message != 'No module named smbus': - raise +except ImportError as e: + if e.msg != 'No module named smbus': + #raise + # TODO: smbus appears to work under python 2.7, but not 3.* + # for hello drinkbot purposes this code is not used, so just pass + pass smbus_missing = 1 class DispenserSelect(object): @@ -39,14 +41,14 @@ def __init__(self, max_dispensers, software_only): def _write_byte_with_retry(self, address, byte): try: self.router.write_byte(address, byte) - except IOError, e: + except IOError as e: # if we get an error, try again, just once try: log.error("*** router send: error while sending. Retrying. " + repr(e)) self.router.write_byte(address, byte) except IOError: app.globals.set_state(fsm.STATE_ERROR) - raise BartendroBrokenError + #raise BartendroBrokenError def reset(self): if self.software_only: return @@ -74,7 +76,7 @@ def sync(self, state): self._write_byte_with_retry(ROUTER_ADDRESS, ROUTER_CMD_SYNC_OFF) except IOError: app.globals.set_state(fsm.STATE_ERROR) - raise BartendroBrokenError + #raise BartendroBrokenError def count(self): return self.num_dispensers @@ -93,7 +95,7 @@ def open(self): self.router = smbus.SMBus(ROUTER_BUS) except IOError: app.globals.set_state(fsm.STATE_ERROR) - raise BartendroBrokenError + #raise BartendroBrokenError log.info("Done.") if __name__ == "__main__": diff --git a/ui/bartendro/router/driver.py b/ui/bartendro/router/driver.py index d552bbad..841ce5e8 100755 --- a/ui/bartendro/router/driver.py +++ b/ui/bartendro/router/driver.py @@ -6,83 +6,96 @@ from time import sleep, localtime, time import serial from struct import pack, unpack -import pack7 -import dispenser_select +import bartendro.router.pack7 +import bartendro.router.dispenser_select from bartendro.error import SerialIOError import random +try: + from Adafruit_MotorHAT import Adafruit_MotorHAT, Adafruit_DCMotor + mh = Adafruit_MotorHAT(addr=0x60) + myMotor = mh.getMotor(1) + myMotor.setSpeed(255) +except: + pass + +#import atext + DISPENSER_DEFAULT_VERSION = 2 DISPENSER_DEFAULT_VERSION_SOFTWARE_ONLY = 3 -BAUD_RATE = 9600 -DEFAULT_TIMEOUT = 2 # in seconds +BAUD_RATE = 9600 +DEFAULT_TIMEOUT = 2 # in seconds -MAX_DISPENSERS = 15 -SHOT_TICKS = 20 +MAX_DISPENSERS = 4 +#MAX_DISPENSERS = 15 +SHOT_TICKS = 20 -RAW_PACKET_SIZE = 10 -PACKET_SIZE = 8 +RAW_PACKET_SIZE = 10 +PACKET_SIZE = 8 -PACKET_ACK_OK = 0 -PACKET_CRC_FAIL = 1 -PACKET_ACK_TIMEOUT = 2 -PACKET_ACK_INVALID = 3 -PACKET_ACK_INVALID_HEADER = 4 +PACKET_ACK_OK = 0 +PACKET_CRC_FAIL = 1 +PACKET_ACK_TIMEOUT = 2 +PACKET_ACK_INVALID = 3 +PACKET_ACK_INVALID_HEADER = 4 PACKET_ACK_HEADER_IN_PACKET = 5 -PACKET_ACK_CRC_FAIL = 6 - -PACKET_PING = 3 -PACKET_SET_MOTOR_SPEED = 4 -PACKET_TICK_DISPENSE = 5 -PACKET_TIME_DISPENSE = 6 -PACKET_LED_OFF = 7 -PACKET_LED_IDLE = 8 -PACKET_LED_DISPENSE = 9 -PACKET_LED_DRINK_DONE = 10 -PACKET_IS_DISPENSING = 11 -PACKET_LIQUID_LEVEL = 12 -PACKET_UPDATE_LIQUID_LEVEL = 13 -PACKET_ID_CONFLICT = 14 -PACKET_LED_CLEAN = 15 -PACKET_SET_CS_THRESHOLD = 16 -PACKET_SAVED_TICK_COUNT = 17 +PACKET_ACK_CRC_FAIL = 6 + +PACKET_PING = 3 +PACKET_SET_MOTOR_SPEED = 4 +PACKET_TICK_DISPENSE = 5 +PACKET_TIME_DISPENSE = 6 +PACKET_LED_OFF = 7 +PACKET_LED_IDLE = 8 +PACKET_LED_DISPENSE = 9 +PACKET_LED_DRINK_DONE = 10 +PACKET_IS_DISPENSING = 11 +PACKET_LIQUID_LEVEL = 12 +PACKET_UPDATE_LIQUID_LEVEL = 13 +PACKET_ID_CONFLICT = 14 +PACKET_LED_CLEAN = 15 +PACKET_SET_CS_THRESHOLD = 16 +PACKET_SAVED_TICK_COUNT = 17 PACKET_RESET_SAVED_TICK_COUNT = 18 -PACKET_GET_LIQUID_THRESHOLDS = 19 -PACKET_SET_LIQUID_THRESHOLDS = 20 +PACKET_GET_LIQUID_THRESHOLDS = 19 +PACKET_SET_LIQUID_THRESHOLDS = 20 PACKET_FLUSH_SAVED_TICK_COUNT = 21 -PACKET_TICK_SPEED_DISPENSE = 22 -PACKET_PATTERN_DEFINE = 23 -PACKET_PATTERN_ADD_SEGMENT = 24 -PACKET_PATTERN_FINISH = 25 -PACKET_SET_MOTOR_DIRECTION = 26 -PACKET_GET_VERSION = 27 -PACKET_COMM_TEST = 0xFE - -DEST_BROADCAST = 0xFF - -MOTOR_DIRECTION_FORWARD = 1 -MOTOR_DIRECTION_BACKWARD = 0 - -LED_PATTERN_IDLE = 0 -LED_PATTERN_DISPENSE = 1 -LED_PATTERN_DRINK_DONE = 2 -LED_PATTERN_CLEAN = 3 +PACKET_TICK_SPEED_DISPENSE = 22 +PACKET_PATTERN_DEFINE = 23 +PACKET_PATTERN_ADD_SEGMENT = 24 +PACKET_PATTERN_FINISH = 25 +PACKET_SET_MOTOR_DIRECTION = 26 +PACKET_GET_VERSION = 27 +PACKET_COMM_TEST = 0xFE + +DEST_BROADCAST = 0xFF + +MOTOR_DIRECTION_FORWARD = 1 +MOTOR_DIRECTION_BACKWARD = 0 + +LED_PATTERN_IDLE = 0 +LED_PATTERN_DISPENSE = 1 +LED_PATTERN_DRINK_DONE = 2 +LED_PATTERN_CLEAN = 3 LED_PATTERN_CURRENT_SENSE = 4 -MOTOR_DIRECTION_FORWARD = 1 -MOTOR_DIRECTION_BACKWARD = 0 +MOTOR_DIRECTION_FORWARD = 1 +MOTOR_DIRECTION_BACKWARD = 0 log = logging.getLogger('bartendro') + def crc16_update(crc, a): crc ^= a - for i in xrange(0, 8): + for i in range(0, 8): if crc & 1: crc = (crc >> 1) ^ 0xA001 else: crc = (crc >> 1) return crc + class RouterDriver(object): '''This object interacts with the bartendro router controller.''' @@ -95,30 +108,31 @@ def __init__(self, device, software_only): self.dispenser_select = None self.dispenser_version = DISPENSER_DEFAULT_VERSION self.startup_log = "" - self.debug_levels = [ 200, 180, 120 ] + self.debug_levels = [200, 180, 120] - # dispenser_ids are the ids the dispensers have been assigned. These are logical ids + # dispenser_ids are the ids the dispensers have been assigned. These are logical ids # used for dispenser communication. - self.dispenser_ids = [255 for i in xrange(MAX_DISPENSERS)] + self.dispenser_ids = [255 for i in range(MAX_DISPENSERS)] # dispenser_ports are the ports the dispensers have been plugged into. - self.dispenser_ports = [255 for i in xrange(MAX_DISPENSERS)] + self.dispenser_ports = [255 for i in range(MAX_DISPENSERS)] if software_only: self.num_dispensers = MAX_DISPENSERS else: - self.num_dispensers = 0 + self.num_dispensers = 0 def get_startup_log(self): return self.startup_log - + def get_dispenser_version(self): return self.dispenser_version def reset(self): """Reset the hardware. Do this if there is shit going wrong. All motors will be stopped and reset.""" - if self.software_only: return + if self.software_only: + return self.close() self.open() @@ -132,19 +146,20 @@ def set_timeout(self, timeout): def open(self): '''Open the serial connection to the router''' - if self.software_only: return + if self.software_only: + return self._clear_startup_log() try: log.info("Opening %s" % self.device) - self.ser = serial.Serial(self.device, - BAUD_RATE, - bytesize=serial.EIGHTBITS, - parity=serial.PARITY_NONE, + self.ser = serial.Serial(self.device, + BAUD_RATE, + bytesize=serial.EIGHTBITS, + parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, timeout=.01) - except serial.serialutil.SerialException, e: + except (serial.serialutil.SerialException, e): raise SerialIOError(e) log.info("Done.\n") @@ -153,28 +168,29 @@ def open(self): self.status = status_led.StatusLED(self.software_only) self.status.set_color(0, 0, 1) - self.dispenser_select = dispenser_select.DispenserSelect(MAX_DISPENSERS, self.software_only) + self.dispenser_select = dispenser_select.DispenserSelect( + MAX_DISPENSERS, self.software_only) self.dispenser_select.open() self.dispenser_select.reset() - # This primes the communication line. + # This primes the communication line. self.ser.write(chr(170) + chr(170) + chr(170)) sleep(.001) log.info("Discovering dispensers") self.num_dispensers = 0 - for port in xrange(MAX_DISPENSERS): + for port in range(MAX_DISPENSERS): self._log_startup("port %d:" % port) self.dispenser_select.select(port) sleep(.01) while True: self.ser.flushInput() - self.ser.write("???") + self.ser.write("???") data = self.ser.read(3) ll = "" for ch in data: ll += "%02X " % ord(ch) - if len(data) == 3: + if len(data) == 3: if data[0] != data[1] or data[0] != data[2]: self._log_startup(" %s -- inconsistent" % ll) continue @@ -182,44 +198,51 @@ def open(self): self.dispenser_ids[self.num_dispensers] = id self.dispenser_ports[self.num_dispensers] = port self.num_dispensers += 1 - self._log_startup(" %s -- Found dispenser with pump id %02X, index %d" % (ll, id, self.num_dispensers)) + self._log_startup( + " %s -- Found dispenser with pump id %02X, index %d" % (ll, id, self.num_dispensers)) break elif len(data) > 1: - self._log_startup(" %s -- Did not receive 3 characters back. Trying again." % ll) + self._log_startup( + " %s -- Did not receive 3 characters back. Trying again." % ll) sleep(.5) else: break self._select(0) self.set_timeout(DEFAULT_TIMEOUT) - self.ser.write(chr(255)); + self.ser.write(chr(255)) - duplicate_ids = [x for x, y in collections.Counter(self.dispenser_ids).items() if y > 1] + duplicate_ids = [x for x, y in collections.Counter( + self.dispenser_ids).items() if y > 1] if len(duplicate_ids): for dup in duplicate_ids: - if dup == 255: continue + if dup == 255: + continue self._log_startup("ERROR: Dispenser id conflict!\n") sent = False for i, d in enumerate(self.dispenser_ids): - if d == dup: - if not sent: + if d == dup: + if not sent: self._send_packet8(i, PACKET_ID_CONFLICT, 0) sent = True - self._log_startup(" dispenser %d has id %d\n" % (i, d)) + self._log_startup( + " dispenser %d has id %d\n" % (i, d)) self.dispenser_ids[i] = 255 self.num_dispensers -= 1 self.dispenser_version = self.get_dispenser_version(0) if self.dispenser_version < 0: - self.dispenser_version = DISPENSER_DEFAULT_VERSION + self.dispenser_version = DISPENSER_DEFAULT_VERSION else: self.status.swap_blue_green() - log.info("Detected dispensers version %d. (Only checked first dispenser)" % self.dispenser_version) + log.info("Detected dispensers version %d. (Only checked first dispenser)" % + self.dispenser_version) self.led_idle() def close(self): - if self.software_only: return + if self.software_only: + return self.ser.close() self.ser = None self.status = None @@ -227,58 +250,75 @@ def close(self): def log(self, msg): return - if self.software_only: return + if self.software_only: + return try: t = localtime() - self.cl.write("%d-%d-%d %d:%02d %s" % (t.tm_year, t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, msg)) + self.cl.write("%d-%d-%d %d:%02d %s" % (t.tm_year, + t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, msg)) self.cl.flush() except IOError: pass def make_shot(self): - if self.software_only: return True + if self.software_only: + return True self._send_packet32(0, PACKET_TICK_DISPENSE, 90) return True def ping(self, dispenser): - if self.software_only: return True + if self.software_only: + return True return self._send_packet32(dispenser, PACKET_PING, 0) def start(self, dispenser): - if self.software_only: return True + if self.software_only: + return True return self._send_packet8(dispenser, PACKET_SET_MOTOR_SPEED, 255, True) def set_motor_direction(self, dispenser, direction): - if self.software_only: return True + if self.software_only: + return True return self._send_packet8(dispenser, PACKET_SET_MOTOR_DIRECTION, direction) def stop(self, dispenser): - if self.software_only: return True + if self.software_only: + return True return self._send_packet8(dispenser, PACKET_SET_MOTOR_SPEED, 0) def dispense_time(self, dispenser, duration): - if self.software_only: return True + if self.software_only: + return True return self._send_packet32(dispenser, PACKET_TIME_DISPENSE, duration) def dispense_ticks(self, dispenser, ticks, speed=255): - if self.software_only: return True - ret = self._send_packet16(dispenser, PACKET_TICK_SPEED_DISPENSE, ticks, speed) + if self.software_only: + myMotor.run(Adafruit_MotorHAT.FORWARD) + time.sleep(5) + myMotor.run(Adafruit_MotorHAT.RELEASE) + return True + + ret = self._send_packet16( + dispenser, PACKET_TICK_SPEED_DISPENSE, ticks, speed) # if it fails, re-try once. if not ret: log.error("*** dispense command failed. re-trying once.") - ret = self._send_packet16(dispenser, PACKET_TICK_SPEED_DISPENSE, ticks, speed) + ret = self._send_packet16( + dispenser, PACKET_TICK_SPEED_DISPENSE, ticks, speed) return ret def led_off(self): - if self.software_only: return True + if self.software_only: + return True self._sync(0) self._send_packet8(DEST_BROADCAST, PACKET_LED_OFF, 0) return True def led_idle(self): - if self.software_only: return True + if self.software_only: + return True self._sync(0) self._send_packet8(DEST_BROADCAST, PACKET_LED_IDLE, 0) sleep(.01) @@ -286,7 +326,8 @@ def led_idle(self): return True def led_dispense(self): - if self.software_only: return True + if self.software_only: + return True self._sync(0) self._send_packet8(DEST_BROADCAST, PACKET_LED_DISPENSE, 0) sleep(.01) @@ -294,7 +335,8 @@ def led_dispense(self): return True def led_complete(self): - if self.software_only: return True + if self.software_only: + return True self._sync(0) self._send_packet8(DEST_BROADCAST, PACKET_LED_DRINK_DONE, 0) sleep(.01) @@ -302,7 +344,8 @@ def led_complete(self): return True def led_clean(self): - if self.software_only: return True + if self.software_only: + return True self._sync(0) self._send_packet8(DEST_BROADCAST, PACKET_LED_CLEAN, 0) sleep(.01) @@ -310,7 +353,8 @@ def led_clean(self): return True def led_error(self): - if self.software_only: return True + if self.software_only: + return True self._sync(0) self._send_packet8(DEST_BROADCAST, PACKET_LED_CLEAN, 0) sleep(.01) @@ -326,15 +370,16 @@ def is_dispensing(self, dispenser): Returns a tuple of (dispensing, is_over_current) """ - if self.software_only: return (False, False) + if self.software_only: + return (False, False) # Sometimes the motors can interfere with communications. - # In such cases, assume the motor is still running and + # In such cases, assume the motor is still running and # then assume the caller will again to see if it is still running self.set_timeout(.1) ret = self._send_packet8(dispenser, PACKET_IS_DISPENSING, 0) self.set_timeout(DEFAULT_TIMEOUT) - if ret: + if ret: ack, value0, value1 = self._receive_packet8_2() if ack == PACKET_ACK_OK: return (value0, value1) @@ -343,39 +388,45 @@ def is_dispensing(self, dispenser): return (True, False) def update_liquid_levels(self): - if self.software_only: return True + if self.software_only: + return True return self._send_packet8(DEST_BROADCAST, PACKET_UPDATE_LIQUID_LEVEL, 0) def get_liquid_level(self, dispenser): - if self.software_only: return 100 + if self.software_only: + return 100 if self._send_packet8(dispenser, PACKET_LIQUID_LEVEL, 0): ack, value, dummy = self._receive_packet16() if ack == PACKET_ACK_OK: # Returning a random value as below is really useful for testing. :) #self.debug_levels[dispenser] = max(self.debug_levels[dispenser] - 20, 50) - #return self.debug_levels[dispenser] - #return random.randint(50, 200) + # return self.debug_levels[dispenser] + # return random.randint(50, 200) return value return -1 def get_liquid_level_thresholds(self, dispenser): - if self.software_only: return True + if self.software_only: + return True if self._send_packet8(dispenser, PACKET_GET_LIQUID_THRESHOLDS, 0): ack, low, out = self._receive_packet16() if ack == PACKET_ACK_OK: return (low, out) return (-1, -1) - + def set_liquid_level_thresholds(self, dispenser, low, out): - if self.software_only: return True + if self.software_only: + return True return self._send_packet16(dispenser, PACKET_SET_LIQUID_THRESHOLDS, low, out) def set_motor_direction(self, dispenser, dir): - if self.software_only: return True + if self.software_only: + return True return self._send_packet8(dispenser, PACKET_SET_MOTOR_DIRECTION, dir) def get_dispenser_version(self, dispenser): - if self.software_only: return DISPENSER_DEFAULT_VERSION_SOFTWARE_ONLY + if self.software_only: + return DISPENSER_DEFAULT_VERSION_SOFTWARE_ONLY if self._send_packet8(dispenser, PACKET_GET_VERSION, 0): # set a short timeout, in case its a v2 dispenser self.set_timeout(.1) @@ -386,12 +437,15 @@ def get_dispenser_version(self, dispenser): return -1 def set_status_color(self, red, green, blue): - if self.software_only: return - if not self.status: return + if self.software_only: + return + if not self.status: + return self.status.set_color(red, green, blue) def get_saved_tick_count(self, dispenser): - if self.software_only: return True + if self.software_only: + return True if self._send_packet8(dispenser, PACKET_SAVED_TICK_COUNT, 0): ack, ticks, dummy = self._receive_packet16() if ack == PACKET_ACK_OK: @@ -399,47 +453,54 @@ def get_saved_tick_count(self, dispenser): return -1 def flush_saved_tick_count(self): - if self.software_only: return True + if self.software_only: + return True return self._send_packet8(DEST_BROADCAST, PACKET_FLUSH_SAVED_TICK_COUNT, 0) def pattern_define(self, dispenser, pattern): - if self.software_only: return True + if self.software_only: + return True return self._send_packet8(dispenser, PACKET_PATTERN_DEFINE, pattern) def pattern_add_segment(self, dispenser, red, green, blue, steps): - if self.software_only: return True + if self.software_only: + return True return self._send_packet8(dispenser, PACKET_PATTERN_ADD_SEGMENT, red, green, blue, steps) def pattern_finish(self, dispenser): - if self.software_only: return True + if self.software_only: + return True return self._send_packet8(dispenser, PACKET_PATTERN_FINISH, 0) # ----------------------------------------------- - # Past this point we only have private functions. + # Past this point we only have private functions. # ----------------------------------------------- def _sync(self, state): """Turn on/off the sync signal from the router. This signal is used to syncronize the LEDs""" - if self.software_only: return + if self.software_only: + return self.dispenser_select.sync(state) def _select(self, dispenser): """Private function to select a dispenser.""" - if self.software_only: return True + if self.software_only: + return True # If for broadcast, then ignore this select - if dispenser == 255: return + if dispenser == 255: + return port = self.dispenser_ports[dispenser] self.dispenser_select.select(port) - def _send_packet(self, dest, packet): - if self.software_only: return True + if self.software_only: + return True - self._select(dest); + self._select(dest) self.ser.flushInput() self.ser.flushOutput() @@ -449,7 +510,8 @@ def _send_packet(self, dest, packet): encoded = pack7.pack_7bit(packet + pack("> 1) ^ 0xA001 + else: + crc = (crc >> 1) + return crc + +class Fake_Motor(): + def __init__(self, num): + self.num = num + + def setSpeed(self, speed): + self.speed = speed + + def run(self, command=None): + log.info('run fake motor %s' % self.num) + +class Fake_MotorHAT(): + + def __init__(self): + self.port = 0x60 + self.motors = [Fake_Motor(i) for i in range(9)] + + def getMotor(self, num): + + return self.motors[num] + + + +class RouterDriver(object): + ''' plug in replacement for Bartendro RouterDriver to control the naive + peristlatic pumps from Hello Drinkbot project. Provides a layer above + the AdaFruit motor library which understands pumps. There is a lot of + code here which only applies to the real Bartendro pumps.''' + + def __init__(self, device, software_only=False): + ''' device is ignored, it is required for bartendro hardware''' + self.device = device + self.__doc__ = 'foo' + self.dispenser_cnt = 8 + self.software_only = software_only + + self.dispenser_version = DISPENSER_DEFAULT_VERSION + self.startup_log = "" + + # I need a hellodrinkbot switch + #if not software_only: + + if 1: + try: + self.mh1 = Adafruit_MotorHAT(addr=0x60) + #self.ports = [self.mh1.getMotor(foo+1) for foo in range(4)] + #for motor in range(4): + #self.ports[motor].setSpeed(255) + except: + # no motor hat, but that might be fine if you are developing on another machine + self.mh1 = Fake_MotorHAT() + log.info("No Motor Hat?") + + self.ports = [self.mh1.getMotor(foo+1) for foo in range(4)] + for motor in range(4): + self.ports[motor].setSpeed(255) + pass + # Add a second motor hat, with a second address. Comment the + # above lines, replace with something like this: + # self.mh1 = Adafruit_MotorHAT(addr=0x60) + # self.mh2 = Adafruit_MotorHAT(addr=0xXX) + # self.ports = [self.mh1.getMotor(range(1,9))] + # for motor in range(8): + # self.ports[motor].setSpeed(255) + + else: + #self.ports = [i for i in range(1, 5)] + pass + + self.num_dispensers = MAX_DISPENSERS + # The pumptest16.py does the right thing, but here dispensers + # 3 and 4, and 7 and 8 are reversed. But reversing them in this list + # of dispensers doesn't do what I need. + self.dispensers = [ + #{'port': None, 'direction': MOTOR_DIRECTION_FORWARD}, + {'port': 0, 'direction': MOTOR_DIRECTION_FORWARD}, + {'port': 0, 'direction': MOTOR_DIRECTION_BACKWARD}, + {'port': 1, 'direction': MOTOR_DIRECTION_FORWARD}, + {'port': 1, 'direction': MOTOR_DIRECTION_BACKWARD}, + {'port': 2, 'direction': MOTOR_DIRECTION_FORWARD}, + {'port': 2, 'direction': MOTOR_DIRECTION_BACKWARD}, + {'port': 3, 'direction': MOTOR_DIRECTION_FORWARD}, + {'port': 3, 'direction': MOTOR_DIRECTION_BACKWARD}, + ] + + + def get_startup_log(self): + return self.startup_log + + def get_dispenser_version(self): + return self.dispenser_version + + def reset(self): + """Reset the hardware. Do this if there is shit going wrong. All motors will be stopped + and reset.""" + if self.software_only: + return + + self.close() + self.open() + + def count(self): + return self.num_dispensers + + def set_timeout(self, timeout): + self.ser.timeout = timeout + + def open(self): + '''Open the serial connection to the router''' + if self.software_only: + return + + self._clear_startup_log() + + try: + log.info("Opening %s" % self.device) + self.ser = serial.Serial(self.device, + BAUD_RATE, + bytesize=serial.EIGHTBITS, + parity=serial.PARITY_NONE, + stopbits=serial.STOPBITS_ONE, + timeout=.01) + except (serial.serialutil.SerialException, e): + #raise SerialIOError(e) + pass + + log.info("Done.\n") + + import status_led + self.status = status_led.StatusLED(self.software_only) + self.status.set_color(0, 0, 1) + + #self.dispenser_select = dispenser_select.DispenserSelect( + # MAX_DISPENSERS, self.software_only) + #self.dispenser_select.open() + #self.dispenser_select.reset() + + # This primes the communication line. + self.ser.write(chr(170) + chr(170) + chr(170)) + sleep(.001) + + log.info("Discovering dispensers") + self.num_dispensers = 0 + for port in range(MAX_DISPENSERS): + self._log_startup("port %d:" % port) + #self.dispenser_select.select(port) + sleep(.01) + while True: + self.ser.flushInput() + self.ser.write("???") + data = self.ser.read(3) + ll = "" + for ch in data: + ll += "%02X " % ord(ch) + if len(data) == 3: + if data[0] != data[1] or data[0] != data[2]: + self._log_startup(" %s -- inconsistent" % ll) + continue + id = ord(data[0]) + self.dispenser_ids[self.num_dispensers] = id + self.dispenser_ports[self.num_dispensers] = port + self.num_dispensers += 1 + self._log_startup( + " %s -- Found dispenser with pump id %02X, index %d" % (ll, id, self.num_dispensers)) + break + elif len(data) > 1: + self._log_startup( + " %s -- Did not receive 3 characters back. Trying again." % ll) + sleep(.5) + else: + break + + self._select(0) + self.set_timeout(DEFAULT_TIMEOUT) + self.ser.write(chr(255)) + + duplicate_ids = [x for x, y in collections.Counter( + self.dispenser_ids).items() if y > 1] + if len(duplicate_ids): + for dup in duplicate_ids: + if dup == 255: + continue + self._log_startup("ERROR: Dispenser id conflict!\n") + sent = False + for i, d in enumerate(self.dispenser_ids): + if d == dup: + if not sent: + self._send_packet8(i, PACKET_ID_CONFLICT, 0) + sent = True + self._log_startup( + " dispenser %d has id %d\n" % (i, d)) + self.dispenser_ids[i] = 255 + self.num_dispensers -= 1 + + self.dispenser_version = self.get_dispenser_version(0) + if self.dispenser_version < 0: + self.dispenser_version = DISPENSER_DEFAULT_VERSION + else: + self.status.swap_blue_green() + log.info("Detected dispensers version %d. (Only checked first dispenser)" % + self.dispenser_version) + + self.led_idle() + + def close(self): + if self.software_only: + return + # change to adafruit all motors off off + for port in self.ports: + port.run(Adafruit_MotorHAT.RELEASE) + self.status = None + self.dispenser_select = None + + def log(self, msg): + return + if self.software_only: + return + try: + t = localtime() + self.cl.write("%d-%d-%d %d:%02d %s" % (t.tm_year, + t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, msg)) + self.cl.flush() + except IOError: + pass + + def make_shot(self): + if self.software_only: + return True + # TODO: change to use dispense_ticks() + self._send_packet32(0, PACKET_TICK_DISPENSE, 90) + return True + + def ping(self, dispenser): + if self.software_only: + return True + return self._send_packet32(dispenser, PACKET_PING, 0) + + def start(self, dispenser): + if self.software_only: + return True + return self._send_packet8(dispenser, PACKET_SET_MOTOR_SPEED, 255, True) + + def set_motor_direction(self, dispenser, direction): + if self.software_only: + return True + return self._send_packet8(dispenser, PACKET_SET_MOTOR_DIRECTION, direction) + + def dispenser_port(self, disp): + """ Take a dispenser, return the port + """ + + port = (disp)//2+1 + port = (disp)//2 + print('disp: %i port: %i' % (disp, port)) + #pdb.set_trace() + return port + + def dispenser_sibling(self, disp): + if (disp % 2): + sibling = disp - 1 + else: + sibling = disp + 1 + print('disp %i sibling %i ' % (disp, sibling)) + + return sibling + + def stop(self, dispenser=None): + """ turn one or all dispensers off """ + log.info('\tdispenser_off %r ' % + ( dispenser)) + #if self.software_only: + #return True + # if dispenser==None turn them all off + if not dispenser: + log.info('\tstop no dispenser passed, turn off all') + + #for disp in (range(1, 4)): + for disp in (range(9)): + #self.ports[disp]['timer'] = None + try: + self.ports[disp].run(Adafruit_MotorHAT.RELEASE) + except: + pass + else: + log.info('\tstop dispenser %r' % dispenser) + #self.ports[dispenser]['timer'] = None + + port = self.dispenser_port(dispenser) + self.ports[port].run(Adafruit_MotorHAT.RELEASE) + + def dispense_time(self, dispenser, duration): + log.info('dispense_time dispenser: %r port: %r ' % ( + dispenser, self.dispensers[dispenser]['port'])) + + # can we dispense? Are we dispensing, or is our port-sibling dispensing? + try: + if self.dispensers[dispenser]['timer'].isAlive(): + log.info('Error: %r:%r dispenser in use. Not starting duplicate.' % ( + self.dispensers[dispenser]['port'], dispenser)) + return False + except: + pass + + print('dispense_time') + print('\tdispenser: %i ' % dispenser) + port = self.dispenser_port(dispenser) + print('\tport', port) + print('\t',type(port)) + sibling = self.dispenser_sibling(dispenser) + + try: + if self.dispensers[sibling]['timer'].isAlive(): + log.info('\tWarning: %r:%r port in use. Waiting to start.' % ( + self.dispensers[dispenser]['port'], dispenser)) + self.dispensers[dispenser]['timerwait'] = threading.Timer( + 1, self.dispense_time, [dispenser, duration]) + self.dispensers[dispenser]['timerwait'].start() + return True + except: + pass + + # I feel too stupid to properly do software. + if dispenser == 0: + self.ports[port].run(Adafruit_MotorHAT.FORWARD) + if dispenser == 1: + self.ports[port].run(Adafruit_MotorHAT.BACKWARD) + if dispenser == 2: + self.ports[port].run(Adafruit_MotorHAT.BACKWARD) + if dispenser == 3: + self.ports[port].run(Adafruit_MotorHAT.FORWARD) + if dispenser == 4: + self.ports[port].run(Adafruit_MotorHAT.FORWARD) + if dispenser == 5: + self.ports[port].run(Adafruit_MotorHAT.BACKWARD) + if dispenser == 6: + self.ports[port].run(Adafruit_MotorHAT.BACKWARD) + if dispenser == 7: + self.ports[port].run(Adafruit_MotorHAT.FORWARD) + + #if (dispenser % 2): + # self.ports[port].run(Adafruit_MotorHAT.BACKWARD) + # print('\t dispenser %i backward' % dispenser) + #else: + # self.ports[port].run(Adafruit_MotorHAT.FORWARD) + # print('\t dispenser %i forward' % dispenser) + + #if not self.software_only: + # if (dispenser % 2): + # self.ports[dispenser].run(Adafruit_MotorHAT.FORWARD) + # else: + # self.ports[dispenser].run(Adafruit_MotorHAT.BACKWARD) + + log.info('setting stop callback to dispenser: %r ' % dispenser) + self.dispensers[dispenser]['timer'] = threading.Timer( + duration, self.stop, [dispenser]) + self.dispensers[dispenser]['timer'].start() + return True + + # todo: Add dispense_ml, which calls dispense_time with a conversion factor + def dispense_ml(self, dispenser, ml, speed=255): + if self.software_only: + pass + SECONDS_PER_ML = 18/100. # huh? + time = ml*SECONDS_PER_ML + ret = self.dispense_time(dispenser,time) + return ret + + def dispense_ticks(self, dispenser, ticks, speed=255): + if self.software_only: + pass + + log.info('need to convert ticks to time') + ret = self.dispense_time(dispenser, 5) + + # if it fails, do something? + if not ret: + log.error("*** dispense command failed. re-trying once.") + + return ret + + def led_off(self): + if self.software_only: + return True + self._sync(0) + self._send_packet8(DEST_BROADCAST, PACKET_LED_OFF, 0) + return True + + def led_idle(self): + if self.software_only: + return True + self._sync(0) + self._send_packet8(DEST_BROADCAST, PACKET_LED_IDLE, 0) + sleep(.01) + self._sync(1) + return True + + def led_dispense(self): + if self.software_only: + return True + self._sync(0) + self._send_packet8(DEST_BROADCAST, PACKET_LED_DISPENSE, 0) + sleep(.01) + self._sync(1) + return True + + def led_complete(self): + if self.software_only: + return True + self._sync(0) + self._send_packet8(DEST_BROADCAST, PACKET_LED_DRINK_DONE, 0) + sleep(.01) + self._sync(1) + return True + + def led_clean(self): + if self.software_only: + return True + self._sync(0) + self._send_packet8(DEST_BROADCAST, PACKET_LED_CLEAN, 0) + sleep(.01) + self._sync(1) + return True + + def led_error(self): + if self.software_only: + return True + self._sync(0) + self._send_packet8(DEST_BROADCAST, PACKET_LED_CLEAN, 0) + sleep(.01) + self._sync(1) + return True + + def comm_test(self): + self._sync(0) + return self._send_packet8(0, PACKET_COMM_TEST, 0) + + def is_dispensing(self, dispenser): + """ + Returns a tuple of (dispensing, is_over_current) + """ + + if self.software_only: + return (False, False) + + # Sometimes the motors can interfere with communications. + # In such cases, assume the motor is still running and + # then assume the caller will again to see if it is still running + self.set_timeout(.1) + ret = self._send_packet8(dispenser, PACKET_IS_DISPENSING, 0) + self.set_timeout(DEFAULT_TIMEOUT) + if ret: + ack, value0, value1 = self._receive_packet8_2() + if ack == PACKET_ACK_OK: + return (value0, value1) + if ack == PACKET_ACK_TIMEOUT: + return (-1, -1) + return (True, False) + + def update_liquid_levels(self): + if self.software_only: + return True + return self._send_packet8(DEST_BROADCAST, PACKET_UPDATE_LIQUID_LEVEL, 0) + + def get_liquid_level(self, dispenser): + if self.software_only: + return 100 + if self._send_packet8(dispenser, PACKET_LIQUID_LEVEL, 0): + ack, value, dummy = self._receive_packet16() + if ack == PACKET_ACK_OK: + # Returning a random value as below is really useful for testing. :) + # self.debug_levels[dispenser] = max(self.debug_levels[dispenser] - 20, 50) + # return self.debug_levels[dispenser] + # return random.randint(50, 200) + return value + return -1 + + def get_liquid_level_thresholds(self, dispenser): + if self.software_only: + return True + if self._send_packet8(dispenser, PACKET_GET_LIQUID_THRESHOLDS, 0): + ack, low, out = self._receive_packet16() + if ack == PACKET_ACK_OK: + return (low, out) + return (-1, -1) + + def set_liquid_level_thresholds(self, dispenser, low, out): + if self.software_only: + return True + return self._send_packet16(dispenser, PACKET_SET_LIQUID_THRESHOLDS, low, out) + + def set_motor_direction(self, dispenser, dir): + if self.software_only: + return True + return self._send_packet8(dispenser, PACKET_SET_MOTOR_DIRECTION, dir) + + def get_dispenser_version(self, dispenser): + if self.software_only: + return DISPENSER_DEFAULT_VERSION_SOFTWARE_ONLY + if self._send_packet8(dispenser, PACKET_GET_VERSION, 0): + # set a short timeout, in case its a v2 dispenser + self.set_timeout(.1) + ack, ver, dummy = self._receive_packet16(True) + self.set_timeout(DEFAULT_TIMEOUT) + if ack == PACKET_ACK_OK: + return ver + return -1 + + def set_status_color(self, red, green, blue): + if self.software_only: + return + if not self.status: + return + self.status.set_color(red, green, blue) + + def get_saved_tick_count(self, dispenser): + if self.software_only: + return True + if self._send_packet8(dispenser, PACKET_SAVED_TICK_COUNT, 0): + ack, ticks, dummy = self._receive_packet16() + if ack == PACKET_ACK_OK: + return ticks + return -1 + + def flush_saved_tick_count(self): + if self.software_only: + return True + return self._send_packet8(DEST_BROADCAST, PACKET_FLUSH_SAVED_TICK_COUNT, 0) + + def pattern_define(self, dispenser, pattern): + if self.software_only: + return True + return self._send_packet8(dispenser, PACKET_PATTERN_DEFINE, pattern) + + def pattern_add_segment(self, dispenser, red, green, blue, steps): + if self.software_only: + return True + return self._send_packet8(dispenser, PACKET_PATTERN_ADD_SEGMENT, red, green, blue, steps) + + def pattern_finish(self, dispenser): + if self.software_only: + return True + return self._send_packet8(dispenser, PACKET_PATTERN_FINISH, 0) + + # ----------------------------------------------- + # Past this point we only have private functions. + # ----------------------------------------------- + + def _sync(self, state): + """Turn on/off the sync signal from the router. This signal is used to syncronize the LEDs""" + + if self.software_only: + return + self.dispenser_select.sync(state) + + def _select(self, dispenser): + """Private function to select a dispenser.""" + + if self.software_only: + return True + + # If for broadcast, then ignore this select + if dispenser == 255: + return + + port = self.dispenser_ports[dispenser] + self.dispenser_select.select(port) + + def _send_packet(self, dest, packet): + if self.software_only: + return True + + self._select(dest) + self.ser.flushInput() + self.ser.flushOutput() + + crc = 0 + for ch in packet: + crc = crc16_update(crc, ord(ch)) + + encoded = pack7.pack_7bit(packet + pack("0] for b in xrange(7,-1,-1)]) for i in xrange(256)] +bits = [''.join(['01'[i&(1<0] for b in range(7,-1,-1)]) for i in range(256)] def pack_7bit(data): buffer = 0 diff --git a/ui/bartendro/view/admin/booze.py b/ui/bartendro/view/admin/booze.py index 61cb0ca3..3c2e9924 100644 --- a/ui/bartendro/view/admin/booze.py +++ b/ui/bartendro/view/admin/booze.py @@ -2,7 +2,7 @@ from bartendro import app, db from sqlalchemy import func, asc from flask import Flask, request, redirect, render_template -from flask.ext.login import login_required +from flask_login import login_required from bartendro.model.drink import Drink from bartendro.model.booze import Booze from bartendro.model.booze_group import BoozeGroup @@ -11,6 +11,7 @@ @app.route('/admin/booze') @login_required def admin_booze(): + ''' admin to add or edit booze ''' form = BoozeForm(request.form) boozes = Booze.query.order_by(asc(func.lower(Booze.name))) return render_template("admin/booze", options=app.options, boozes=boozes, form=form, title="Booze") @@ -34,6 +35,7 @@ def admin_booze_save(): form = BoozeForm(request.form) if request.method == 'POST' and form.validate(): id = int(request.form.get("id") or '0') + print(bool(id)) if id: booze = Booze.query.filter_by(id=int(id)).first() booze.update(form.data) diff --git a/ui/bartendro/view/admin/debug.py b/ui/bartendro/view/admin/debug.py index d5bb9ec2..3cc8fad4 100644 --- a/ui/bartendro/view/admin/debug.py +++ b/ui/bartendro/view/admin/debug.py @@ -2,14 +2,14 @@ import time from bartendro import app, db from flask import Flask, request, render_template -from flask.ext.login import login_required +from flask_login import login_required LOG_LINES_TO_SHOW = 1000 @app.route('/admin/debug') @login_required def debug_index(): - + ''' debugging page with additional information and ability to test dispensers ''' startup_log = app.driver.get_startup_log() try: b_log = open("logs/bartendro.log", "r") @@ -17,9 +17,9 @@ def debug_index(): b_log.close() lines = lines[-LOG_LINES_TO_SHOW:] bartendro_log = "".join(lines) - print bartendro_log - except IOError, e: - print "file open fail" + print( bartendro_log) + except IOError as e: + print("file open fail") bartendro_log = "%s" % e return render_template("admin/debug", options=app.options, diff --git a/ui/bartendro/view/admin/dispenser.py b/ui/bartendro/view/admin/dispenser.py index a1a9f58a..a22580e9 100644 --- a/ui/bartendro/view/admin/dispenser.py +++ b/ui/bartendro/view/admin/dispenser.py @@ -3,7 +3,7 @@ import memcache from bartendro import app, db from flask import Flask, request, redirect, render_template -from flask.ext.login import login_required +from flask_login import login_required from wtforms import Form, SelectField, IntegerField, validators from bartendro.model.drink import Drink from bartendro.model.booze import Booze @@ -17,6 +17,7 @@ @app.route('/admin') @login_required def dispenser(): + ''' shows list of dispensers and allows you to dispense a test pour. ''' driver = app.driver count = driver.count() @@ -38,7 +39,7 @@ class F(DispenserForm): kwargs = {} fields = [] - for i in xrange(1, 17): + for i in range(1, 17): dis = "dispenser%d" % i actual = "actual%d" % i setattr(F, dis, SelectField("%d" % i, choices=sorted_booze_list)) diff --git a/ui/bartendro/view/admin/drink.py b/ui/bartendro/view/admin/drink.py index a9026efe..c6c9df4b 100644 --- a/ui/bartendro/view/admin/drink.py +++ b/ui/bartendro/view/admin/drink.py @@ -3,7 +3,7 @@ from operator import itemgetter from bartendro import app, db from flask import Flask, request, redirect, render_template -from flask.ext.login import login_required +from flask_login import login_required from bartendro.model.drink import Drink from bartendro.model.booze import Booze from bartendro.model.dispenser import Dispenser @@ -13,6 +13,8 @@ @app.route('/admin/drink') @login_required def admin_drink_new(): + ''' shows the admin drink page ''' + drinks = db.session.query(Drink).join(DrinkName).filter(Drink.name_id == DrinkName.id) \ .order_by(asc(func.lower(DrinkName.name))).all() diff --git a/ui/bartendro/view/admin/liquidlevel.py b/ui/bartendro/view/admin/liquidlevel.py index 6df87bab..29c54bbd 100644 --- a/ui/bartendro/view/admin/liquidlevel.py +++ b/ui/bartendro/view/admin/liquidlevel.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from bartendro import app, db from flask import Flask, request, render_template -from flask.ext.login import login_required +from flask_login import login_required @app.route('/admin/liquidlevel') @login_required @@ -9,7 +9,7 @@ def admin_liquidlevel(): driver = app.driver count = driver.count() thresholds = [] - for disp in xrange(count): + for disp in range(count): low, out = driver.get_liquid_level_thresholds(disp) thresholds.append((low, out)) diff --git a/ui/bartendro/view/admin/options.py b/ui/bartendro/view/admin/options.py index 6cb2771f..672c2fe0 100644 --- a/ui/bartendro/view/admin/options.py +++ b/ui/bartendro/view/admin/options.py @@ -7,20 +7,40 @@ from bartendro import app from flask import Flask, request, render_template, Response from werkzeug.exceptions import Unauthorized -from flask.ext.login import login_required +from flask_login import login_required from bartendro.model.version import DatabaseVersion -def get_ip_address_from_interface(ifname): +def get_ip_address(): + ''' Gets the current connected address. + + This appears to work on the Mac when get_ip_address_from_interface fails ''' + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("8.8.8.8", 80)) + return s.getsockname()[0] + +def get_ip_address_from_interface(ifname): + # This is erroring out when run on a mac under emulation try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) return socket.inet_ntoa(fcntl.ioctl(s.fileno(), 0x8915, struct.pack('256s', ifname[:15]))[20:24]) except IOError: + pass + except struct.error: + pass + + try: + # fail over for Mac + add = get_ip_address() + return add + except: return "[none]" @app.route('/admin/options') @login_required def admin_options(): + ''' admin options page ''' ver = DatabaseVersion.query.one() recover = not request.remote_addr.startswith("10.0.0") diff --git a/ui/bartendro/view/admin/report.py b/ui/bartendro/view/admin/report.py index e0417f77..de83a26f 100644 --- a/ui/bartendro/view/admin/report.py +++ b/ui/bartendro/view/admin/report.py @@ -2,7 +2,7 @@ import time from bartendro import app, db from flask import Flask, request, render_template -from flask.ext.login import login_required +from flask_login import login_required from bartendro.model.drink import Drink from bartendro.model.booze import Booze from bartendro.model.booze_group import BoozeGroup diff --git a/ui/bartendro/view/admin/user.py b/ui/bartendro/view/admin/user.py index cfb96f6c..df7e5bec 100644 --- a/ui/bartendro/view/admin/user.py +++ b/ui/bartendro/view/admin/user.py @@ -2,7 +2,7 @@ from bartendro import app, db, login_manager from bartendro.form.login import LoginForm from flask import Flask, request, render_template, flash, redirect, url_for -from flask.ext.login import login_required, login_user, logout_user +from flask_login import login_required, login_user, logout_user class User(object): id = 0 diff --git a/ui/bartendro/view/booze.py b/ui/bartendro/view/booze.py new file mode 100644 index 00000000..fd6a3827 --- /dev/null +++ b/ui/bartendro/view/booze.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +from bartendro import app, db +from sqlalchemy import func, asc, text +from flask import Flask, request, redirect, render_template +from flask import Response +import json +from flask_login import login_required +from bartendro.model.drink import Drink, DrinkName +from bartendro.model.drink_booze import DrinkBooze +from bartendro.model.booze import Booze, booze_types +from bartendro.model.booze_group import BoozeGroup +from bartendro.form.booze import BoozeForm +from bartendro.model.dispenser import Dispenser +from bartendro.view.root import filter_drink_list + +def load_loaded_boozes(): + # this assumes we have 16 dispensers, or, this doesn't care what max dispensers is + loaded = db.session.query("id", "name", "abv", "type","dispenser")\ + .from_statement(text("""SELECT booze.id, + booze.name, + booze.abv, + booze.type, + booze.image, + dispenser.id as dispenser + FROM booze, dispenser + WHERE booze.id = dispenser.booze_id + AND dispenser.id < 9 + ORDER BY booze.name ;"""))\ + .params(foo='', bar='').all() + return loaded + +def load_drink_list(booze_id): + """ load drinks that can be made with booze_id. We have both + all the possible drinks and the drinks we can make with the + booze we have """ + drink_list = [] + all_drink_list = db.session.query(Drink) \ + .join(DrinkName) \ + .join(DrinkBooze) \ + .filter(Drink.name_id == DrinkName.id) \ + .filter(Drink.popular == 0) \ + .filter(Drink.available == 1) \ + .filter(DrinkBooze.booze_id == booze_id) \ + .order_by(asc(func.lower(DrinkName.name))).all() + + can_make = app.mixer.get_available_drink_list() + can_make_dict = {} + for drink in can_make: + can_make_dict[drink] = 1 + + can_make_drink_list = filter_drink_list(can_make_dict, all_drink_list) + return (all_drink_list, can_make_drink_list) + +@app.route('/booze') +@login_required +def booze(): + ''' Page showing all booze, and all loaded booze ''' + + all_boozes = Booze.query.order_by(asc(func.lower(Booze.name))) + loaded_boozes = load_loaded_boozes() + (all_drink_list, can_make_drink_list) = load_drink_list(0) + + return render_template("booze", options=app.options, all_drink_list=all_drink_list, all_boozes=all_boozes, loaded_boozes=loaded_boozes, can_make_drink_list=can_make_drink_list, title="Explore Booze") + +@app.route('/booze/') +@login_required +def booze_detail(id): + ''' Page showing the selected booze, and drinks that can be made with that booze.''' + + booze = Booze.query.filter_by(id=int(id)).first() + # what is your booze_types + #import pdb + #pdb.set_trace() + booze.type = booze_types[booze.type][1] + all_boozes = Booze.query.order_by(asc(func.lower(Booze.name))) + loaded_boozes = load_loaded_boozes() + drink_list = load_drink_list(booze.id) + (all_drink_list, can_make_drink_list) = load_drink_list(booze.id) + return render_template("booze", options=app.options, all_drink_list=all_drink_list, all_boozes=all_boozes, loaded_boozes=loaded_boozes, can_make_drink_list=can_make_drink_list, title="Explore Booze", booze=booze ) + +@app.route('/booze/all') +def booze_all(): + ''' Returns json of all booze in our database. ''' + + data = Booze.query.order_by(asc(func.lower(Booze.name))) + lst = [{'id':b.id, 'name':b.name} for b in data] + js = json.dumps(lst) + resp = Response(js, status=200, mimetype="application/json") + return resp + +@app.route('/booze/loaded') +def booze_loaded(): + ''' Returns json of all loaded booze. But does not show the dispenser where it is loaded.''' + data = Booze.query.order_by(asc(func.lower(Booze.name))) + data = load_loaded_boozes() + lst = [{'id':b.id, 'name':b.name} for b in data] + js = json.dumps(lst) + resp = Response(js, status=200, mimetype="application/json") + return resp diff --git a/ui/bartendro/view/drink/drink.py b/ui/bartendro/view/drink/drink.py index 197c6869..1cfb96ae 100644 --- a/ui/bartendro/view/drink/drink.py +++ b/ui/bartendro/view/drink/drink.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- from bartendro import app, db from flask import Flask, request, render_template +from flask import Response +import json from bartendro.model.drink import Drink from bartendro.model.drink_booze import DrinkBooze from bartendro.model.custom_drink import CustomDrink @@ -14,10 +16,14 @@ @app.route('/drink/') def normal_drink(id): + ''' displays the drink page for the numbered drink ''' + return drink(id, 0) @app.route('/drink//go') def lucky_drink(id): + ''' immediately makes this drink ''' + return drink(id, 1) def drink(id, go): @@ -55,6 +61,8 @@ def drink(id, go): show_sweet_tart = has_sweet and has_tart show_strength = has_alcohol and has_non_alcohol + import pdb + #pdb.set_trace() if not custom_drink: return render_template("drink/index", drink=drink, @@ -93,6 +101,7 @@ def drink(id, go): title=drink.name.name, is_custom=1, custom_drink=drink.custom_drink[0], + booze_group=booze_group, show_sweet_tart=show_sweet_tart, show_sobriety=show_sobriety, @@ -102,4 +111,27 @@ def drink(id, go): @app.route('/drink/sobriety') def drink_sobriety(): + ''' Provides 'test' of your sobriety. If you can't click on a random spot + on the screen fast enough No Good Booze for you!''' + # Todo: Currently broken return render_template("drink/sobriety") + +@app.route('/drink/all') +def drink_all(): + ''' Returns json list of all drinks in our database ''' + + data = [{'id':d.id, 'name':d.name.name, 'description':d.desc} for d in Drink.query.all()] + js = json.dumps(data) + resp = Response(js, status=200, mimetype='application/json') + return resp + +@app.route('/drink/available') +def drink_available(): + ''' Returns json list of all drinks available with our current booze load. ''' + + available = app.mixer.get_available_drink_list() + data = [{'id':d.id, 'name':d.name.name, 'description':d.desc} for d in Drink.query.all() if d.id in available] + js = json.dumps(data) + resp = Response(js, status=200, mimetype='application/json') + return resp + diff --git a/ui/bartendro/view/root.py b/ui/bartendro/view/root.py index 38555e5f..e159a090 100644 --- a/ui/bartendro/view/root.py +++ b/ui/bartendro/view/root.py @@ -68,7 +68,7 @@ def index(): other_drinks = filter_drink_list(can_make_dict, other_drinks) process_ingredients(other_drinks) - print "%d, %d" % (len(top_drinks), len(other_drinks)) + print( "%d, %d" % (len(top_drinks), len(other_drinks))) if (not len(top_drinks) and not len(other_drinks)) or app.globals.get_state() == fsm.STATE_HARD_OUT: return render_template("index", @@ -81,11 +81,22 @@ def index(): if app.options.show_feeling_lucky: lucky = Drink("Make sure there is a cup under the spout, the drink will pour immediately!") lucky.name = DrinkName("I'm feeling lucky!") - lucky.id = can_make[int(random.randint(0, len(can_make) - 1))] + # Todo: This generates an error with 'Drink' conflicting with a persistent drink? + # FlushError: New instance with identity key (, (54,)) conflicts with persistent instance + #lucky.id = can_make[int(random.randint(0, len(can_make) - 1))] lucky.set_lucky(True) - lucky.set_ingredients_text("Pour a random drink now") + lucky.set_ingredients_text("Pour a random drink now (possibly broken see root.py line 88") top_drinks.insert(0, lucky) + #lucky = Drink("Make sure there is a cup under the spout, the drink will pour immediately!") + #lucky.name = DrinkName("I'm feeling lucky!") + #lucky.id = can_make[int(random.randint(0, len(can_make) - 1))] + #lucky.id = 1 + #lucky.set_lucky(True) + #lucky.set_ingredients_text("Pour a random drink now") + #top_drinks.insert(0, lucky) + # Todo: I'm feeling lucky is broken for me. + return render_template("index", options=app.options, top_drinks=top_drinks, @@ -93,13 +104,14 @@ def index(): title="Bartendro") @app.route('/shots') -def shots(): +def shots(template='shots'): + ''' Shows the shots page. ''' if not app.options.use_shotbot_ui: return redirect("/") if app.globals.get_state() == fsm.STATE_ERROR: - return render_template("shots", + return render_template(template, num_shots_ready=0, options=app.options, error_message="Bartendro is in trouble!

I need some attention! Please find my master, so they can make me feel better.", @@ -114,14 +126,53 @@ def shots(): shots.append(disp.booze) if len(shots) == 0: - return render_template("shots", + return render_template(template, num_shots_ready=0, options=app.options, error_message="Bartendro is out of all boozes. Oh no!

I need some attention! Please find my master, so they can make me feel better.", title="Bartendro error") - return render_template("shots", + return render_template(template, num_shots_ready= len(shots), options=app.options, shots=shots, title="Shots") + +@app.route('/graphical_shots') +def graphical_shots(): + ''' Shows the shots menu with pictures of the different booze.''' + template='graphical_shots' + # not sure why I can't do this. + #return shots('graphical_shots') + + if not app.options.use_shotbot_ui: + return redirect("/") + + if app.globals.get_state() == fsm.STATE_ERROR: + return render_template(template, + num_shots_ready=0, + options=app.options, + error_message="Bartendro is in trouble!

I need some attention! Please find my master, so they can make me feel better.", + title="Bartendro error") + + dispensers = db.session.query(Dispenser).all() + dispensers = dispensers[:app.driver.count()] + + shots = [] + for disp in dispensers: + if disp.out == LL_OK or disp.out == LL_LOW or not app.options.use_liquid_level_sensors: + shots.append(disp.booze) + + if len(shots) == 0: + return render_template(template, + num_shots_ready=0, + options=app.options, + error_message="Bartendro is out of all boozes. Oh no!

I need some attention! Please find my master, so they can make me feel better.", + title="Bartendro error") + + return render_template(template, + num_shots_ready= len(shots), + options=app.options, + shots=shots, + title="Shots") + diff --git a/ui/bartendro/view/snooze.py b/ui/bartendro/view/snooze.py new file mode 100644 index 00000000..c8e26106 --- /dev/null +++ b/ui/bartendro/view/snooze.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +from bartendro import app, db +from sqlalchemy import func, asc +from sqlalchemy.sql import text +from flask import Flask, request, redirect, render_template +from flask_login import login_required +from bartendro.model.drink import Drink, DrinkName +from bartendro.model.drink_booze import DrinkBooze +from bartendro.model.booze import Booze, booze_types +from bartendro.model.booze_group import BoozeGroup +from bartendro.form.booze import BoozeForm +from bartendro.model.dispenser import Dispenser +from bartendro.view.root import filter_drink_list + +def load_loaded_boozes(): + loaded = db.session.query("id", "name", "abv", "type","dispenser")\ + .from_statement(text("""SELECT booze.id, + booze.name, + booze.abv, + booze.type, + dispenser.id as dispenser + FROM booze, dispenser + WHERE booze.id = dispenser.booze_id + ORDER BY booze.name ;"""))\ + .params(foo='', bar='').all() + return loaded + +def load_drink_list(booze_id): + """ load drinks that can be made with booze_id. We have both + all the possible drinks and the drinks we can make with the + booze we have """ + drink_list = [] + all_drink_list = db.session.query(Drink) \ + .join(DrinkName) \ + .join(DrinkBooze) \ + .filter(Drink.name_id == DrinkName.id) \ + .filter(Drink.popular == 0) \ + .filter(Drink.available == 1) \ + .filter(DrinkBooze.booze_id == booze_id) \ + .order_by(asc(func.lower(DrinkName.name))).all() + + can_make = app.mixer.get_available_drink_list() + can_make_dict = {} + for drink in can_make: + can_make_dict[drink] = 1 + + can_make_drink_list = filter_drink_list(can_make_dict, all_drink_list) + return (all_drink_list, can_make_drink_list) + +@app.route('/snooze') +@login_required +def snooze(): + ''' sample of adding an endpoint. This is pretty much not needed, but I am a coward and don't delete code. ''' + all_boozes = Booze.query.order_by(asc(func.lower(Booze.name))) + loaded_boozes = load_loaded_boozes() + (all_drink_list, can_make_drink_list) = load_drink_list(0) + + return render_template("snooze", options=app.options, all_drink_list=all_drink_list, all_boozes=all_boozes, loaded_boozes=loaded_boozes, can_make_drink_list=can_make_drink_list, title="Explore Booze") + diff --git a/ui/bartendro/view/trending.py b/ui/bartendro/view/trending.py index 807f007d..79254f2e 100644 --- a/ui/bartendro/view/trending.py +++ b/ui/bartendro/view/trending.py @@ -1,15 +1,16 @@ # -*- coding: utf-8 -*- -import time +import time, datetime from bartendro import app, db -from sqlalchemy import desc +from sqlalchemy import desc, text from flask import Flask, request, render_template -from flask.ext.login import login_required +from flask_login import login_required from bartendro.model.drink import Drink from bartendro.model.drink_log import DrinkLog from bartendro.model.booze import Booze from bartendro.model.booze_group import BoozeGroup from bartendro.form.booze import BoozeForm +BARTENDRO_DAY_START_TIME = 10 * 60 * 60 DEFAULT_TIME = 12 display_info = { 12 : 'Drinks poured in the last 12 hours.', @@ -20,7 +21,49 @@ @app.route('/trending') def trending_drinks(): - return trending_drinks_detail(DEFAULT_TIME) + ''' The trending drinks page ''' + return trending_drinks_detail(DEFAULT_TIME,'') + +# figure out begindate and enddate +# begindate exists, but no enddate = assume it is one day +# enddate exists but no beginndate = assume begindate=first day +# need some text for this. +# begin and enddate need to be in timestamp format. + +@app.route('/trending/date/') +def trending_drinks_date(): + """ this assumes ?begindate=yyyy-mm-dd&enddate=yyyy-mm-dd + or ?begindate=yyyy-mm-dd (with no enddate) + """ + + title = "Drinks by date" + + begindate = request.args.get("begindate", "") + if (len(begindate) > 0) : + begin_ts = time.mktime(datetime.datetime.strptime(begindate, "%Y-%m-%d").timetuple()) + begin_ts = begin_ts + BARTENDRO_DAY_START_TIME + begindate = datetime.datetime.fromtimestamp(begin_ts).strftime('%c') + else: + begin_ts = 0 + begindate = 'The beginning of time' + + enddate = request.args.get("enddate", "") + if (len(enddate) == 0): + end_ts = begin_ts + (24 * 60 * 60) - 1 + #end_ts = end_ts + BARTENDRO_DAY_START_TIME - 1 + enddate = datetime.datetime.fromtimestamp(end_ts).strftime('%c') + else: + end_ts = time.mktime(datetime.datetime.strptime(enddate, "%Y-%m-%d").timetuple()) + end_ts = end_ts + 24*60*60+BARTENDRO_DAY_START_TIME - 1 + enddate = datetime.datetime.fromtimestamp(end_ts).strftime('%c') + + try: + txt = "Drinks poured from %s to %s " % (begindate, enddate) + except IndexError: + txt = "Drinks poured by date" + + hours = 0 + return trending_drinks_detail(begin_ts, end_ts, txt, hours) @app.route('/trending/') def trending_drinks_detail(hours): @@ -44,26 +87,35 @@ def trending_drinks_detail(hours): else: begindate = 0 else: - begindate = 0 + begindate = 0 enddate = 0 txt = "" + return trending_drinks_detail(begindate, enddate, txt, hours) + + +def trending_drinks_detail(begindate, enddate, txt='', hours=''): + + title = "Trending drinks" + + #import pdb + #pdb.set_trace() total_number = db.session.query("number")\ - .from_statement("""SELECT count(*) as number + .from_statement(text("""SELECT count(*) as number FROM drink_log WHERE drink_log.time >= :begin - AND drink_log.time <= :end""")\ + AND drink_log.time <= :end"""))\ .params(begin=begindate, end=enddate).first() total_volume = db.session.query("volume")\ - .from_statement("""SELECT sum(drink_log.size) as volume + .from_statement(text("""SELECT sum(drink_log.size) as volume FROM drink_log WHERE drink_log.time >= :begin - AND drink_log.time <= :end""")\ + AND drink_log.time <= :end"""))\ .params(begin=begindate, end=enddate).first() top_drinks = db.session.query("id", "name", "number", "volume")\ - .from_statement("""SELECT drink.id, + .from_statement(text("""SELECT drink.id, drink_name.name, count(drink_log.drink_id) AS number, sum(drink_log.size) AS volume @@ -72,12 +124,27 @@ def trending_drinks_detail(hours): AND drink_name.id = drink.id AND drink_log.time >= :begin AND drink_log.time <= :end GROUP BY drink_name.name - ORDER BY count(drink_log.drink_id) desc;""")\ + ORDER BY count(drink_log.drink_id) desc;"""))\ .params(begin=begindate, end=enddate).all() - return render_template("trending", top_drinks = top_drinks, options=app.options, + drinks_by_date = db.session.query("date", "number", "volume")\ + .from_statement(text("""SELECT date(time- :BARTENDRO_DAY_START_TIME,'unixepoch') as date, + count(drink_log.drink_id) AS number, + sum(drink_log.size) AS volume + FROM drink_log, drink_name, drink + WHERE drink_log.drink_id = drink_name.id + AND drink_name.id = drink.id + GROUP BY date + ORDER BY date desc;"""))\ + .params(BARTENDRO_DAY_START_TIME=BARTENDRO_DAY_START_TIME).all() + + return render_template("trending", top_drinks = top_drinks, + drinks_by_date = drinks_by_date, + options=app.options, title="Trending drinks", txt=txt, total_number=total_number[0], total_volume=total_volume[0], hours=hours) + + diff --git a/ui/bartendro/view/ws/booze.py b/ui/bartendro/view/ws/booze.py index 8db122c7..aca91b28 100644 --- a/ui/bartendro/view/ws/booze.py +++ b/ui/bartendro/view/ws/booze.py @@ -1,12 +1,19 @@ # -*- coding: utf-8 -*- from bartendro import app, db from flask import Flask, request, jsonify +from flask import Response +from sqlalchemy.sql import text from bartendro.model.drink import Drink from bartendro.model.booze import Booze from bartendro.form.booze import BoozeForm +import json @app.route('/ws/booze/match/') -def ws_booze(request, str): - str = str + "%%" - boozes = db.session.query("id", "name").from_statement("SELECT id, name FROM booze WHERE name LIKE :s").params(s=str).all() - return jsonify(boozes) +def ws_booze(str): + ''' Does a case insensitive search on booze for the partial string. + entering 'equ' will find all of the tequillas. ''' + str = "%%%s%%" % str + boozes = db.session.query("id", "name").from_statement(text("SELECT id, name FROM booze WHERE name LIKE :s")).params(s=str).all() + js = json.dumps(boozes) + resp=Response(js, status=200, mimetype="application/json") + return resp diff --git a/ui/bartendro/view/ws/dispenser.py b/ui/bartendro/view/ws/dispenser.py index 92266b59..7e1febc2 100644 --- a/ui/bartendro/view/ws/dispenser.py +++ b/ui/bartendro/view/ws/dispenser.py @@ -4,7 +4,7 @@ from werkzeug.exceptions import ServiceUnavailable from bartendro import app, db, mixer from flask import Flask, request -from flask.ext.login import current_user +from flask_login import current_user from bartendro.model.drink import Drink from bartendro.model.booze import Booze from bartendro.model.dispenser import Dispenser @@ -72,7 +72,9 @@ def ws_dispenser_test(disp): try: app.mixer.dispense_ml(dispenser, app.options.test_dispense_ml) except BartendroBrokenError: - raise InternalServerError + pass + # todo: Handle errors correctly + #raise InternalServerError return "" @@ -86,11 +88,11 @@ def ws_dispenser_clean(): try: app.mixer.clean() - except BartendroCantPourError, err: + except BartendroCantPourError as err: raise BadRequest(err) - except BartendroBrokenError, err: + except BartendroBrokenError as err: raise InternalServerError(err) - except BartendroBusyError, err: + except BartendroBusyError as err: raise ServiceUnavailable(err) return "" @@ -105,11 +107,11 @@ def ws_dispenser_clean_right(): try: app.mixer.clean_right() - except BartendroCantPourError, err: + except BartendroCantPourError as err: raise BadRequest(err) - except BartendroBrokenError, err: + except BartendroBrokenError as err: raise InternalServerError(err) - except BartendroBusyError, err: + except BartendroBusyError as err: raise ServiceUnavailable(err) return "" @@ -123,11 +125,11 @@ def ws_dispenser_clean_left(): try: app.mixer.clean_left() - except BartendroCantPourError, err: + except BartendroCantPourError as err: raise BadRequest(err) - except BartendroBrokenError, err: + except BartendroBrokenError as err: raise InternalServerError(err) - except BartendroBusyError, err: + except BartendroBusyError as err: raise ServiceUnavailable(err) return "" diff --git a/ui/bartendro/view/ws/drink.py b/ui/bartendro/view/ws/drink.py index e26b8e42..6bacfa80 100644 --- a/ui/bartendro/view/ws/drink.py +++ b/ui/bartendro/view/ws/drink.py @@ -1,10 +1,13 @@ # -*- coding: utf-8 -*- + +# --- import json from time import sleep from operator import itemgetter from bartendro import app, db, mixer -from flask import Flask, request -from flask.ext.login import login_required, current_user +from flask import Flask, request, Response +from flask_login import login_required, current_user +from sqlalchemy.sql import text from werkzeug.exceptions import ServiceUnavailable, BadRequest, InternalServerError from bartendro.model.drink import Drink from bartendro.model.drink_name import DrinkName @@ -12,36 +15,71 @@ from bartendro.model.drink_booze import DrinkBooze from bartendro.model.dispenser import Dispenser from bartendro.error import BartendroBusyError, BartendroBrokenError, BartendroCantPourError, BartendroCurrentSenseError +import json + + +@app.route('/ws/drink/match/') +def ws_drink_match(str): + ''' Does a case insensitive search on drinks for the partial string. + entering 'equ' will find all of the tequilla sunrises. ''' + str = "%%%s%%" % str + drinks = db.session.query("id", "name").from_statement(text("SELECT id, name FROM drink_name WHERE name LIKE :s")).params(s=str).all() + js = json.dumps(drinks) + resp=Response(js, status=200, mimetype="application/json") + return resp + + def ws_make_drink(drink_id): recipe = {} + size=0 + drink = Drink.query.filter_by(id=int(drink_id)).first() for arg in request.args: - disp = int(arg[5:]) - recipe[disp] = int(request.args.get(arg)) + if arg[0:4] == 'size': + size = int(request.args.get(arg)) + else: + booze = int(arg[5:]) + recipe[booze] = int(request.args.get(arg)) + + # todo: add values for recipe_return array for drinks which use the + # normal menu. OTOH, you won't see the response unless you are calling + # ws_make_drink from the API + recipe_return = [] + if size: + # figure out the recipe based on the drink + size_unit = 150 / sum([db.value for db in drink.drink_boozes]) + # recipe is a dict of {booze_id:quantity, } + recipe = {b.booze_id:b.value*size_unit for b in drink.drink_boozes} + recipe_return = [ {'booze_id':b.booze_id, 'booze_name':b.booze.name, 'quantity':b.value*size_unit} for b in drink.drink_boozes] + + js = json.dumps({'drink_name':drink.name.name,'drink_description':drink.desc, 'boozes':recipe_return}) - drink = Drink.query.filter_by(id=int(drink_id)).first() try: app.mixer.make_drink(drink, recipe) - except mixer.BartendroCantPourError, err: + except mixer.BartendroCantPourError as err: raise BadRequest(err) - except mixer.BartendroBrokenError, err: + except mixer.BartendroBrokenError as err: raise InternalServerError(err) - except mixer.BartendroBusyError, err: + except mixer.BartendroBusyError as err: raise ServiceUnavailable(err) - return "ok\n" + # todo: I'd like to return more than ok + + resp = Response(js, status=200, mimetype='application/json') + return resp + #return "%r\nok\n" % recipe @app.route('/ws/drink/') def ws_drink(drink): drink_mixer = app.mixer - if app.options.must_login_to_dispense and not current_user.is_authenticated(): + if app.options.must_login_to_dispense and not current_user.is_authenticated: return "login required" return ws_make_drink(drink) @app.route('/ws/drink/custom') def ws_custom_drink(): - if app.options.must_login_to_dispense and not current_user.is_authenticated(): + if app.options.must_login_to_dispense and not current_user.is_authenticated: return "login required" return ws_make_drink(0) @@ -58,29 +96,46 @@ def ws_drink_available(drink, state): @app.route('/ws/shots/') def ws_shots(booze_id): - if app.options.must_login_to_dispense and not current_user.is_authenticated(): + ''' pour a shot of booze=booze_id ''' + # version 0.3 of flask contained a breaking change which changed is_authenticated + # from a method to a property. + if app.options.must_login_to_dispense and not current_user.is_authenticated: return "login required" dispensers = db.session.query(Dispenser).all() dispenser = None for d in dispensers: - if d.booze.id == booze_id: - dispenser = d + if d.booze.id == booze_id: dispenser = d if not dispenser: return "this booze is not available" try: app.mixer.dispense_shot(dispenser, app.options.shot_size) - except mixer.BartendroCantPourError, err: + except mixer.BartendroCantPourError as err: raise BadRequest(err) - except mixer.BartendroBrokenError, err: + except mixer.BartendroBrokenError as err: raise InternalServerError(err) - except mixer.BartendroBusyError, err: + except mixer.BartendroBusyError as err: raise ServiceUnavailable(err) return "" + +# app.route('/ws/dispense///') +#def ws_dispense_ml(dispenser, ml, wait=0): +@app.route('/ws/dispense//') +def ws_dispense_ml(dispenser, ml): + '''dispense a given amount in ml from a given dispenser. if 'wait==1' then wait for it to complete + before returning''' + print('ws_dispense_ml: %i ml: %i' % (dispenser, ml)) + app.driver.dispense_ml(dispenser,ml) + print('in ws_dispense_ml look at timer') + #if wait==1: + # while app.driver.dispensers[dispenser]['timer'].is_alive(): + # sleep(0.01) + return "dispenser: %i ml: %i" % (dispenser, ml) + @app.route('/ws/drink//load') @login_required def ws_drink_load(id): @@ -109,6 +164,14 @@ def ws_drink_save(drink): id = int(data["id"] or 0) if id > 0: drink = Drink.query.filter_by(id=int(id)).first() + # If the drink name has changed copy to a new drink + if drink.name != data['name'] : + id = 0 + drink = Drink() + for booze in data['boozes']: + # clear the old_booze_id's + booze[2] = 0 + db.session.add(drink) else: id = 0 drink = Drink() diff --git a/ui/bartendro/view/ws/liquidlevel.py b/ui/bartendro/view/ws/liquidlevel.py index 98e15c1e..17fe8604 100644 --- a/ui/bartendro/view/ws/liquidlevel.py +++ b/ui/bartendro/view/ws/liquidlevel.py @@ -6,7 +6,7 @@ from werkzeug.exceptions import BadRequest, InternalServerError from bartendro import app, db from flask import Flask, request, Response -from flask.ext.login import login_required +from flask_login import login_required log = logging.getLogger('bartendro') @@ -76,7 +76,7 @@ def ws_liquidlevel_out_all_set(): driver = app.driver data = [] - for disp in xrange(driver.count()): + for disp in range(driver.count()): out = driver.get_liquid_level(disp) if out < 0: log.error("Failed to read liquid level threshold from dispenser %d" % (disp + 1)) @@ -103,7 +103,7 @@ def ws_liquidlevel_low_all_set(): driver = app.driver data = [] - for disp in xrange(driver.count()): + for disp in range(driver.count()): low = driver.get_liquid_level(disp) if low < 0: log.error("Failed to read liquid level threshold from dispenser %d" % (disp + 1)) diff --git a/ui/bartendro/view/ws/misc.py b/ui/bartendro/view/ws/misc.py index 0c7419b1..97b331fc 100644 --- a/ui/bartendro/view/ws/misc.py +++ b/ui/bartendro/view/ws/misc.py @@ -4,7 +4,7 @@ from werkzeug.exceptions import ServiceUnavailable, InternalServerError from bartendro import app, db, STATIC_FOLDER from flask import Flask, request, Response -from flask.ext.login import login_required +from flask_login import login_required from bartendro.model.drink import Drink from bartendro.model.booze import Booze from bartendro.form.booze import BoozeForm @@ -28,10 +28,10 @@ def ws_reset(): @login_required def ws_test_chain(): driver = app.driver - for disp in xrange(driver.count()): - if not driver.ping(disp): + for disp in range(driver.count()): + if not driver.ping(disp): log.error("Dispense %d failed ping" % (disp + 1)) - return "Dispenser %d failed ping." % (disp + 1) + return "Dispenser %d failed ping." % (disp + 1) return "" @@ -41,11 +41,11 @@ def ws_check_levels(): mixer = app.mixer try: mixer.check_levels() - except BartendroCantPourError, err: + except BartendroCantPourError as err: raise BadRequest(err) - except BartendroBrokenError, err: + except BartendroBrokenError as err: raise InternalServerError(err) - except BartendroBusyError, err: + except BartendroBusyError as err: raise ServiceUnavailable(err) return "" @@ -62,7 +62,7 @@ def ws_download_db(): fh = open("bartendro.db", "r") db_data = fh.read() fh.close() - except IOError, e: + except IOError as e: raise ServiceUnavailable("Error: downloading database failed: %s" % e) r = Response(db_data, mimetype='application/x-sqlite') diff --git a/ui/bartendro/view/ws/option.py b/ui/bartendro/view/ws/option.py index d0eb31ca..ee4dcab1 100644 --- a/ui/bartendro/view/ws/option.py +++ b/ui/bartendro/view/ws/option.py @@ -8,7 +8,7 @@ from sqlalchemy import asc, func from bartendro import app, db, mixer from flask import Flask, request -from flask.ext.login import login_required, logout_user +from flask_login import login_required, logout_user from werkzeug.exceptions import InternalServerError, BadRequest from bartendro.model.option import Option from bartendro.options import bartendro_options @@ -25,8 +25,10 @@ def ws_options(): try: if isinstance(bartendro_options[o.key], int): value = int(o.value) - elif isinstance(bartendro_options[o.key], unicode): - value = unicode(o.value) + # str are unicode in python 3, so this should work. + #elif isinstance(bartendro_options[o.key], unicode): + elif isinstance(bartendro_options[o.key], str): + value = str(o.value) elif isinstance(bartendro_options[o.key], boolean): value = boolean(o.value) else: @@ -88,8 +90,8 @@ def ws_upload(): @login_required def ws_upload_confirm(): file_name = request.json['file_name'] - print file_name - print "Move file '%s' into place." % file_name + print( file_name) + print( "Move file '%s' into place." % file_name) if not os.path.exists(DB_BACKUP_DIR): try: diff --git a/ui/bartendro_server.py b/ui/bartendro_server.py index f9d7729c..72ac3345 100755 --- a/ui/bartendro_server.py +++ b/ui/bartendro_server.py @@ -11,7 +11,8 @@ import traceback from bartendro.global_lock import BartendroGlobalLock -from bartendro.router import driver +#from bartendro.router import driver +from bartendro.router import hello_drinkbot_driver as driver from bartendro import mixer from bartendro.error import BartendroBrokenError, SerialIOError from bartendro.options import load_options @@ -22,7 +23,7 @@ else: version = subprocess.check_output(["git", "rev-parse", "HEAD"]) if version: - version = "git commit " + version[:10] + version = "git commit " + version[:10].decode() else: version = "[unknown]" @@ -45,7 +46,7 @@ have_uwsgi = False def print_software_only_notice(): - print """If you're trying to run this code without having Bartendro hardware, + print("""If you're trying to run this code without having Bartendro hardware, you can still run the software portion of it in a simulation mode. In this mode no communication with the Bartendro hardware will happen to allow the software to run. To enable this mode, set the BARTENDRO_SOFTWARE_ONLY environment variable to 1 and @@ -53,7 +54,7 @@ def print_software_only_notice(): > export BARTENDRO_SOFTWARE_ONLY=1 -""" +""") # Set up logging if not os.path.exists("logs"): @@ -74,8 +75,8 @@ def print_software_only_notice(): app.software_only = 0 if not os.path.exists("bartendro.db"): - print "bartendro.db file not found. Please copy bartendro.db.default to " - print "bartendro.db in order to provide Bartendro with a starting database." + print( "bartendro.db file not found. Please copy bartendro.db.default to ") + print( "bartendro.db in order to provide Bartendro with a starting database.") sys.exit(-1) # Create a memcache connection and flush everything @@ -96,9 +97,9 @@ def print_software_only_notice(): if have_uwsgi: startup_err = err else: - print - print err - print + print() + print(err) + print() print_software_only_notice() sys.exit(-1) except SerialIOError: @@ -106,9 +107,9 @@ def print_software_only_notice(): if have_uwsgi: startup_err = err else: - print - print err - print + print() + print(err) + print() print_software_only_notice() sys.exit(-1) except: @@ -116,9 +117,9 @@ def print_software_only_notice(): if have_uwsgi: startup_err = err else: - print - print err - print + print() + print(err) + print() print_software_only_notice() sys.exit(-1) diff --git a/ui/content/static/css/bartendro-stock.css b/ui/content/static/css/bartendro-stock.css new file mode 100644 index 00000000..8f0bd3f5 --- /dev/null +++ b/ui/content/static/css/bartendro-stock.css @@ -0,0 +1,480 @@ +/* For mobile phone screens */ +@media (max-width: 767px) { + html, body { + height: 100%; + margin: 0; + font-family: Verdana, Geneva, sans-serif; + background-color: #DADADA; + padding: 0px; + } + #content { + width: 100%; + height: 100%; + min-height: 100%; + display: block; + } + .fill { + min-height: 100%; + height: 100%; + background-color: #DADADA; + } + #moar { + padding: 5px; + } + + .orange-border { + border-radius: 15px; + border-style: solid; + border-width: 6px; + border-color: #D9A180; + background-color: #D9A180; + } + + .dark-border { + border-radius: 7px; + border-style: solid; + border-width: 1px; + border-color: #B98160; + padding: 2px; + background-color: white; + } + + #scroll-pane { + padding: 5px; + } + .dispense-stack { + padding: 0 5px 0 5px; + } + + .lb-box { + background-color: white; + float: center; + height:250px; + width: 250; + border-radius: 15px; + border-style: solid; + border-width: 12px; + border-color: #D9A180; + text-align: center; + overflow: hidden; + padding: 10px; + background-color: white; + } + + .lb-drink { + padding-top: 10px; + font-weight: bold; + font-size: 20pt; + line-height: 20pt; + text-align: center; + } + + .lb-error { + padding-top: 10px; + font-weight: bold; + font-size: 11pt; + line-height: 13pt; + text-align: center; + } + + .stack-element-title { + color: #005991; + font-weight: normal; + font-size: 17pt; + padding-left: 5px; + } + + .ui-dialog { + width: 80% !important; + } + + .smaller-font { + font-size: 16px; + } + +} + +@media (min-width: 768px) { + html, body { + height: 100%; + margin: 0; + font-family: Verdana, Geneva, sans-serif; + background-color: #DADADA; + } + #content { + width: 100%; + height: 100%; + min-height: 100%; + display: block; + } + .fill { + min-height: 100%; + height: 100%; + background-color: #DADADA; + } + #moar { + padding: 20px; + } + + .orange-border { + border-radius: 15px; + border-style: solid; + border-width: 12px; + border-color: #D9A180; + background-color: #D9A180; + } + + .dark-border { + border-radius: 7px; + border-style: solid; + border-width: 2px; + border-color: #B98160; + padding: 4px; + background-color: white; + } + + #scroll-pane { + padding: 20px; + } + + .dispense-stack { + padding: 0 20px 0 20px; + } + + .lb-box { + background-color: white; + float: center; + height:250px; + width: 500px; + border-radius: 15px; + border-style: solid; + border-width: 12px; + border-color: #D9A180; + text-align: center; + overflow: hidden; + padding: 20px; + background-color: white; + } + + .lb-drink { + padding-top: 50px; + font-weight: bold; + font-size: 35pt; + line-height: 35pt; + text-align: center; + } + + .lb-error { + padding-top: 10px; + font-weight: bold; + font-size: 16pt; + line-height: 18pt; + text-align: center; + } + + .stack-element-title { + color: #005991; + font-weight: normal; + font-size: 22pt; + padding-left: 10px; + } + + .ui-dialog { + width: 40% !important; + } + + .smaller-font { + font-size: 17.5px; + } + +} + +@media (min-width: 530px) { + .bartendro-logo { + float: right; + } + .bartendro-logo-img { + width: 150px; + padding-top: 10px; + } + .party-robot { + display: none + } +} + +@media (max-width: 530px) { + .bartendro-logo { + display: none; + } + .party-robot { + width: 20px; + float: right; + } + .party-robot-img { + width: 20px; + padding-top: 8px; + } + .slim-nav-item { + padding: 10px 10px 10px !important; + } +} + +.trending-drinks { + float: right; + font-size: 11pt; + padding-top: 20px; +} + +.menuitem +{ + padding: 8px; + font-size: 15pt; +} + +.menuitem-button { + display: block; + margin-bottom: 8px; +} + +h1 { + color: #005991; + font-weight: normal; + font-size: 24pt; + padding-left: 10px; +} + +h3 { + color: #005991; + font-weight: normal; + font-size: 16pt; + margin: 0px; + padding: 20px 0 0 0; +} + +.navbar .nav > .active > a { + color: #0088cc; +} + +.ingredients { + font-weight: bold; + margin-bottom: 8px; +} + +.button-border { + border-width: 2px; + border-color: #4788BF; +} + +.drink-btn { + font-size: 15pt; + text-align: left; + padding-left: 20px; +} + +.drink-plus-minus { + font-size: 25pt; + font-weight: bold; + text-align: left; + padding-left: 20px; +} + +.drink-heading { + color: white; + background-color: #005991; + border-radius: 5px; + font-size: 30pt; + padding: 25px; + line-height: 30pt; +} + +.dispense-drink-detail { + padding: 20px 0 20px 20px; + font-size: 15pt; +} + +.dispense-stack-element { + padding-top: 20px; +} + +table { + width: 100%; +} + +.margin-td { + width: 5%; +} + +.main-td { + width: 90%; + text-align: center; +} + +.margin-td-label { + font-weight: bold; + text-align: center; +} + +.adjust-middle { + font-size: 15pt; + font-weight: bold; +} + +.dispense-buttons { + margin: 40px 0 20px 0; +} + +.dispense-buttons-left { + width: 30%; +} + +.dispense-buttons-middle { + text-align: center; + width: 30%; +} + +.dispense-buttons-right { + font-weight: bold; + text-align: right; + width: 30%; +} + +.dispenser-out { + display: inline; + color: red; +} + +.dispenser-warning { + display: inline; + color: orange; +} + +.dispenser-ok { + display: inline; +} + +.motor-button { + width: 100px; +} + +.test-dispense-button { + width: 130px; +} + +.admin-box { + border: 2px solid #D9A180; + padding: 10px; + border-radius: 5px; + background: white; + height: 800px; + overflow-y: auto; +} + +.admin-box-full { + border: 2px solid #D9A180; + padding: 10px; + border-radius: 5px; + background: white; +} + +.pre-scrollable { + height: 500px; + scroll-y: auto; + clear: both; +} + +.select-field { + height: 44px; + vertical-align: top !important; +} + +.form-div { + margin-bottom: 10px; +} + +.edit-field { + margin-left: 10px; + width: 30px; + height: 35px !important; + vertical-align: top !important; +} + +.form-element { + width: 80%; + height: 100px; +} + +.div-spacer { + margin-top: 15px; + clear: both; +} + +.submit-button { + float: right; + margin-right: 25px; +} + +.no-close .ui-dialog-titlebar-close { + display: none; +} + +.ll-calibrate-textbox { + width: 75px; + height: 35px !important; + vertical-align: top !important; +} + +.ll-dispenser-number { + font-size: 13pt; +} + +.ll-column-spacer { + margin-right: 40px; +} + +.error { + color: red; + display: block; + font-size: 9pt; +} + +.shotbot-button { + width: 100%; + font-size: 16pt; + line-height: 30pt; + border-radius: 10px; + margin-top: 5px; + margin-bottom: 10px; +} + +.shotbot-text { + font-size: 13pt; +} + +.shots-warning { + font-weight: bold; + text-align: center; + margin-top: 30px; + margin-bottom: 30px; +} + +.ui-dialog .ui-dialog-buttonpane button +{ + float: right; +} + +.ui-dialog .ui-button-text { + font-size: 20pt; +} + +.ui-dialog .ui-dialog-title { + font-size: 20pt; +} + +.ui-dialog .ui-dialog-titlebar { + line-height: 26pt; +} + +.ui-dialog p { + font-size: 18pt; + line-height: 20pt; +} diff --git a/ui/content/static/css/bartendro.css b/ui/content/static/css/bartendro.css index 66dd208b..8f0bd3f5 100644 --- a/ui/content/static/css/bartendro.css +++ b/ui/content/static/css/bartendro.css @@ -400,6 +400,7 @@ table { .form-element { width: 80%; + height: 100px; } .div-spacer { diff --git a/ui/content/static/css/style.css b/ui/content/static/css/style.css index a2f9a9fc..1fc472af 100644 --- a/ui/content/static/css/style.css +++ b/ui/content/static/css/style.css @@ -360,7 +360,7 @@ div.forminput{ margin-left:30px; } textarea.desc{ - height:50px;width:300px; + height:75px;width:300px; font-family:Arial; } input.form{ diff --git a/ui/content/static/images/baileys.jpg b/ui/content/static/images/baileys.jpg new file mode 100644 index 00000000..ba519be1 Binary files /dev/null and b/ui/content/static/images/baileys.jpg differ diff --git a/ui/content/static/images/butterscotch.jpg b/ui/content/static/images/butterscotch.jpg new file mode 100644 index 00000000..5267f53e Binary files /dev/null and b/ui/content/static/images/butterscotch.jpg differ diff --git a/ui/content/static/images/cow.jpg b/ui/content/static/images/cow.jpg new file mode 100644 index 00000000..be0a812d Binary files /dev/null and b/ui/content/static/images/cow.jpg differ diff --git a/ui/content/static/images/cranberry.png b/ui/content/static/images/cranberry.png new file mode 100644 index 00000000..a55e610e Binary files /dev/null and b/ui/content/static/images/cranberry.png differ diff --git a/ui/content/static/images/kahlua.jpg b/ui/content/static/images/kahlua.jpg new file mode 100644 index 00000000..040202de Binary files /dev/null and b/ui/content/static/images/kahlua.jpg differ diff --git a/ui/content/static/images/lemon.jpg b/ui/content/static/images/lemon.jpg new file mode 100644 index 00000000..f34bb816 Binary files /dev/null and b/ui/content/static/images/lemon.jpg differ diff --git a/ui/content/static/images/orange_syrup.png b/ui/content/static/images/orange_syrup.png new file mode 100644 index 00000000..12b055fa Binary files /dev/null and b/ui/content/static/images/orange_syrup.png differ diff --git a/ui/content/static/images/potato.jpg b/ui/content/static/images/potato.jpg new file mode 100644 index 00000000..1c5341e1 Binary files /dev/null and b/ui/content/static/images/potato.jpg differ diff --git a/ui/content/static/images/sourapple.jpg b/ui/content/static/images/sourapple.jpg new file mode 100644 index 00000000..1bc4e337 Binary files /dev/null and b/ui/content/static/images/sourapple.jpg differ diff --git a/ui/content/templates/admin/dispenser b/ui/content/templates/admin/dispenser index 840549be..a46fdb21 100644 --- a/ui/content/templates/admin/dispenser +++ b/ui/content/templates/admin/dispenser @@ -258,7 +258,7 @@ function toggle_reverse(disp) } }); } -function test_dispense(disp) +function test_dispense(disp, mult) { if (disp < 1 || disp > 15) return; diff --git a/ui/content/templates/admin/drink b/ui/content/templates/admin/drink index dff1794b..f30f8edc 100644 --- a/ui/content/templates/admin/drink +++ b/ui/content/templates/admin/drink @@ -8,7 +8,7 @@
- +
diff --git a/ui/content/templates/admin/options b/ui/content/templates/admin/options index bdfc9edf..0568827a 100644 --- a/ui/content/templates/admin/options +++ b/ui/content/templates/admin/options @@ -124,6 +124,18 @@
{% endif %} +
+ + {% if 1 %} +

Hello Drinkbot options

+
+
+ + + +
+
+ {% endif %}
@@ -159,7 +171,7 @@

Bartendro's database contains all the information needed to run your Bartendro, including booze you've added, your drink recipes and a log of your drinks poured. This information is not automatically backed up - and you should make period backups of this data base using the two buttons below. + and you should make periodic backups of this data base using the two buttons below.

Download Bartendro's database to your local machine. We recommend saving this file to a DropBox account! diff --git a/ui/content/templates/booze b/ui/content/templates/booze new file mode 100644 index 00000000..68db2648 --- /dev/null +++ b/ui/content/templates/booze @@ -0,0 +1,71 @@ +{% extends 'layout' %} +{% set active = "explore" %} +{% block body %} + + +{% if booze %} +
+
+
+ {{ title }}: {{booze.name}} +
+
+
+ {{ booze.desc }}

+

+ Brand: {{booze.brand}} ABV: {{booze.abv}} Type: {{booze.type}}
+

Drinks we can make NOW with {{booze.name}}

+ + {% if can_make_drink_list %} + + {% for drink in can_make_drink_list %} + + + + {% endfor %} +
{{ drink.name.name }}
+ {% else %} + ({{booze.name}} is not loaded, or maybe there are no drinks which use it, so we can't make any drinks with {{booze.name}}.) + {% endif %} + +

All possible drinks one could make with {{booze.name}}

+ + {% for drink in all_drink_list %} + + + + {% endfor %} +
{{ drink.name.name }}
+ +
+
+
+{% endif %} + +
+ +
+

Currently loaded

+ + + {% for booze in loaded_boozes %} + + + {% endfor %} +
Booze! (dispenser #)
{{ booze.name }} ({{booze.dispenser}})
+
+ +
+

All the Booze

+ + {% for booze in all_boozes %} +
{{ booze.name }} + {% if booze.is_abstract() %} + (abstract) + {% endif %} + + {% endfor %} +
+
+
+{% endblock %} diff --git a/ui/content/templates/drink/index b/ui/content/templates/drink/index index 11cec482..91f391e2 100644 --- a/ui/content/templates/drink/index +++ b/ui/content/templates/drink/index @@ -18,6 +18,9 @@ {% endfor %} + +
{{ options.drink_size }}
+
@@ -334,7 +337,7 @@ function show_dialog(title, text) $("#log-in-required-dialog").dialog({ buttons: [ { text: "Ok", click: function() { $( this ).dialog( "close" ); } } ] }); } -{% if options.must_login_to_dispense and not current_user.is_authenticated() %} +{% if options.must_login_to_dispense and not current_user.is_authenticated %} function make_drink(drink, is_taster) { $("#log-in-required-dialog").dialog({ buttons: [ { text: "Ok", click: function() { $( this ).dialog( "close" ); } } ] }); @@ -362,6 +365,7 @@ function make_drink(drink, is_taster) lb += '
{{ drink.name.name }}
'; lb += ""; $.modal(lb, { 'escClose' : false }); + //alert(url+args); $.ajax({ url: url + args, success: function(html) diff --git a/ui/content/templates/graphical_shots b/ui/content/templates/graphical_shots new file mode 100644 index 00000000..198efa2d --- /dev/null +++ b/ui/content/templates/graphical_shots @@ -0,0 +1,139 @@ +{% extends 'layout' %} +{% set active = "shots_with_pictures" %} +{% block body %} + +
+
+
+ {% if num_shots_ready > 0 %} +
+
+

Shots, but with pictures

+
+
+
+
+
+ + Now serving + {% if options.metric %} + {{ options.shot_size }} ml + {% else %} + {{ "%.1f" % (options.shot_size / 30) }} fl oz + {% endif %} + shots. +
Make sure there is a glass under the spout, the shot will pour immediately!
+
+
+
+
+ {% for i in range(0, num_shots_ready) %} +
+
+
+
+ +
+ + + +
+
+ {{ shots[i].name }} + {{ shots[i].desc }} +
+
+
+
+
+
+ {% endfor %} + admin +
+ {% else %} +
+
+
+

{{ error_message }}

+
+
+ +
+
+ + {% endif %} +
+ + + + + + + + + + +{% endblock %} diff --git a/ui/content/templates/index b/ui/content/templates/index index 5ef2743e..f46557ab 100644 --- a/ui/content/templates/index +++ b/ui/content/templates/index @@ -85,9 +85,11 @@ $(document).ready(function() { {% else %}
No drinks from this section can currently be made.
{% endfor %} + {% if (drinks|count % 2 == 1) %}
 
{% endif %} + {% endmacro %} diff --git a/ui/content/templates/layout b/ui/content/templates/layout index 74e6370c..1fd01a21 100644 --- a/ui/content/templates/layout +++ b/ui/content/templates/layout @@ -26,12 +26,25 @@ class="active" {% endif %} >Shots +
  • Shots with pictures +
  • {% endif %}
  • Trending
  • +
  • + Explore Booze
  • +