From c8e822a650c07b1d6ce6a34d031472696f1ce47b Mon Sep 17 00:00:00 2001 From: Adrian Brandemuehl Date: Mon, 3 Apr 2017 10:23:24 -0500 Subject: [PATCH 1/9] Take args from JSON post data Makes a change in the API to allow for multiple locations to be passed Resolves #7 --- machine_controller/app.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/machine_controller/app.py b/machine_controller/app.py index 1460f5e..df2434d 100644 --- a/machine_controller/app.py +++ b/machine_controller/app.py @@ -45,11 +45,11 @@ def hello_world(): if request.headers.get('TOKEN', '') != token_value: abort(401) - if 'item' not in request.args: - abort(400) - item = request.args['item'] - merch.vend(item[0], int(item[1])) - return json.dumps({'success': True}), 200, {'ContentType': 'application/json'} + data = request.json() + items = data['items'] + for item in items: + merch.vend(item[0], int(item[1])) + return jsonify(success=True) if __name__ == '__main__': app.run(debug=True, host='0.0.0.0') From e47adf4a8bcde58f535958eac2afb5401f47ee35 Mon Sep 17 00:00:00 2001 From: Adrian Brandemuehl Date: Mon, 3 Apr 2017 10:56:33 -0500 Subject: [PATCH 2/9] Add vending error checking The machine will return the index of the vend that failed --- machine_controller/app.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/machine_controller/app.py b/machine_controller/app.py index df2434d..21e0865 100644 --- a/machine_controller/app.py +++ b/machine_controller/app.py @@ -47,10 +47,18 @@ def hello_world(): abort(401) data = request.json() items = data['items'] - for item in items: - merch.vend(item[0], int(item[1])) + + for i, item in enumerate(items): + try: + merch.vend(item[0], int(item[1])) + except: + # Some error occurred, return the first index that failed + return jsonify(success=False, failed=i) + return jsonify(success=True) if __name__ == '__main__': - app.run(debug=True, host='0.0.0.0') + # Make sure flask runs in a single thread. Otherwise concurrent requests + # may cause problems with vending + app.run(debug=True, host='0.0.0.0', threaded=False) From dac83a83f8e79bc09723d5748da3e81b9996271c Mon Sep 17 00:00:00 2001 From: Adrian Brandemuehl Date: Mon, 3 Apr 2017 11:34:23 -0500 Subject: [PATCH 3/9] Add success or failure to vend endpoint --- machine_controller/app.py | 16 ++++++++++++---- machine_controller/vend.py | 19 +++++++++++++------ 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/machine_controller/app.py b/machine_controller/app.py index 21e0865..4171ab1 100644 --- a/machine_controller/app.py +++ b/machine_controller/app.py @@ -47,15 +47,23 @@ def hello_world(): abort(401) data = request.json() items = data['items'] + transaction_id = data['transaction_id'] + + statuses = [] for i, item in enumerate(items): try: merch.vend(item[0], int(item[1])) - except: - # Some error occurred, return the first index that failed - return jsonify(success=False, failed=i) + statuses.append({'error': None, 'location': item}) + + except Exception as e: + # Some error occurred while vending + # I'd prefer to catch Merch.VendError's only, but if something else + # goes wrong, we still need to let the client know instead of + # throwing a 500 + statuses.append({'error': str(e), 'location': item}) - return jsonify(success=True) + return jsonify(transaction_id=transaction_id, items=statuses) if __name__ == '__main__': # Make sure flask runs in a single thread. Otherwise concurrent requests diff --git a/machine_controller/vend.py b/machine_controller/vend.py index bf2fb18..61ce3df 100644 --- a/machine_controller/vend.py +++ b/machine_controller/vend.py @@ -44,6 +44,11 @@ class Merch: MAX_LETTER = 'F' MAX_NUMBER = '0' + class VendError(Exception): + pass + + InvalidLocationError = VendError("Invalid location") + def __init__(self, debug=False): self.debug = debug @@ -93,22 +98,24 @@ def vend(self, letter, number): try: char = ord(letter) except TypeError: - raise TypeError('Letter %s does not represent a character' % - str(letter)) + raise Merch.VendError("Invalid location: %s" % + (str(letter) + str(number))) # Maybe we should use the actual keypad value? if char < ord('A') or char > ord('Z'): - raise ValueError('Invalid Letter: %s' % str(letter)) + raise Merch.VendError("Invalid location: %s" % + (str(letter) + str(number))) num = 0 try: num = int(number) except TypeError: - raise TypeError('Number %s is not convertible to an integer' % - str(num)) + raise Merch.VendError("Invalid location: %s" % + (str(letter) + str(number))) if num < 0 or num > 10: - raise ValueError('Number %d is not in the range 1-10' % num) + raise Merch.VendError("Invalid location: %s" % + (str(letter) + str(number))) self.__vend(letter, str(number)) From 13dea4d3abe862f8d0efa48ca17c868d1a3c1dd7 Mon Sep 17 00:00:00 2001 From: Adrian Brandemuehl Date: Tue, 4 Apr 2017 12:41:07 -0500 Subject: [PATCH 4/9] Update README with API documentation --- README.md | 53 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index ad237bc..d44e8b6 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,46 @@ -# Merch Embedded - +# Merch Embedded [![Join the chat at https://gitter.im/acm-uiuc/merch-development](https://badges.gitter.im/acm-uiuc/merch-development.svg)](https://gitter.im/acm-uiuc/merch-development?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -Possible information sources: + +This repository contains all of the code related to controlling the underlying merch hardware (the vending machine). +Merch runs an embedded webserver that can be accessed at merch.acm.illinois.edu. + +## API + +Every request through the API has a token in the header that is unique to groot. +This way, only groot can make requests to vend things. +The token is stored in the request header. + +### Vend a location +To vend a list of items, POST a request to `/vend`. +The request is of the form +```json +{ + "transaction_id": 1, + "items": ["A1", "B2", "C3"] +} +``` + +The machine will respond with +```json +{ + "transaction_id": 1, + "items": [ + {"location": "A1", "error": null}, + {"location": "B2", "error": "some sort of error"}, + {"location": "C3", "error": null}, + + ] +} +``` + +The errors that can take place while vending are currently: +* `"Invalid location"` + + +## Some related datasheets + [http://bajavending.com/Manual-de-Operacion-BevMax.pdf](http://bajavending.com/Manual-de-Operacion-BevMax.pdf) * Has the right picture of the main controller board, no programming information though @@ -18,15 +55,15 @@ has some useful info about what commands are sent ## License -This project is licensed under the University of Illinois/NCSA Open Source License. For a full copy of this license take a look at the LICENSE file. +This project is licensed under the University of Illinois/NCSA Open Source License. For a full copy of this license take a look at the LICENSE file. -When contributing new files to this project, preappend the following header to the file as a comment: +When contributing new files to this project, preappend the following header to the file as a comment: ``` Copyright © 2017, ACM@UIUC -This file is part of the Merch Project. - -The Merch Project is open source software, released under the University of Illinois/NCSA Open Source License. +This file is part of the Merch Project. + +The Merch Project is open source software, released under the University of Illinois/NCSA Open Source License. You should have received a copy of this license in a file with the distribution. ``` From b75a99e8e9576f14d4226c77a1113e864add176d Mon Sep 17 00:00:00 2001 From: Adrian Brandemuehl Date: Tue, 4 Apr 2017 20:07:51 -0500 Subject: [PATCH 5/9] Raise InvalidLocationError on Invalid Locations --- README.md | 7 ++++--- machine_controller/vend.py | 12 ++++-------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index d44e8b6..0e6a753 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,12 @@ Merch runs an embedded webserver that can be accessed at merch.acm.illinois.edu. ## API -Every request through the API has a token in the header that is unique to groot. -This way, only groot can make requests to vend things. -The token is stored in the request header. +Requests to the device must contain a valid token in the Authorization Header for a request to be processed. +As of now the only token will be given solely to the groot merch service, so if you wish to make merch requests go through groot. + ### Vend a location + To vend a list of items, POST a request to `/vend`. The request is of the form ```json diff --git a/machine_controller/vend.py b/machine_controller/vend.py index 61ce3df..7203c66 100644 --- a/machine_controller/vend.py +++ b/machine_controller/vend.py @@ -98,24 +98,20 @@ def vend(self, letter, number): try: char = ord(letter) except TypeError: - raise Merch.VendError("Invalid location: %s" % - (str(letter) + str(number))) + raise self.InvalidLocationError # Maybe we should use the actual keypad value? if char < ord('A') or char > ord('Z'): - raise Merch.VendError("Invalid location: %s" % - (str(letter) + str(number))) + raise self.InvalidLocationError num = 0 try: num = int(number) except TypeError: - raise Merch.VendError("Invalid location: %s" % - (str(letter) + str(number))) + raise self.InvalidLocationError if num < 0 or num > 10: - raise Merch.VendError("Invalid location: %s" % - (str(letter) + str(number))) + raise self.InvalidLocationError self.__vend(letter, str(number)) From 7ee4f659abf332c14ad94426775465c6ca893dc7 Mon Sep 17 00:00:00 2001 From: Adrian Brandemuehl Date: Tue, 4 Apr 2017 21:30:47 -0500 Subject: [PATCH 6/9] Fix bounds checking --- machine_controller/vend.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/machine_controller/vend.py b/machine_controller/vend.py index 7203c66..004d901 100644 --- a/machine_controller/vend.py +++ b/machine_controller/vend.py @@ -42,7 +42,7 @@ class Merch: ROW = [21, 20, 16] COL = [19, 13] MAX_LETTER = 'F' - MAX_NUMBER = '0' + MAX_NUMBER = 9 class VendError(Exception): pass @@ -101,7 +101,7 @@ def vend(self, letter, number): raise self.InvalidLocationError # Maybe we should use the actual keypad value? - if char < ord('A') or char > ord('Z'): + if char < ord('A') or char > ord(self.MAX_LETTER): raise self.InvalidLocationError num = 0 @@ -110,7 +110,7 @@ def vend(self, letter, number): except TypeError: raise self.InvalidLocationError - if num < 0 or num > 10: + if num < 1 or num > MAX_NUMBER: raise self.InvalidLocationError self.__vend(letter, str(number)) From 6942484b27f5326abcc5efd27ae3f43640cb2fbc Mon Sep 17 00:00:00 2001 From: Adrian Brandemuehl Date: Wed, 5 Apr 2017 00:03:12 -0500 Subject: [PATCH 7/9] Switch to Authorization Header for Authentication --- machine_controller/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/machine_controller/app.py b/machine_controller/app.py index 4171ab1..828d51e 100644 --- a/machine_controller/app.py +++ b/machine_controller/app.py @@ -43,7 +43,7 @@ @app.route('/vend', methods=['POST']) def hello_world(): - if request.headers.get('TOKEN', '') != token_value: + if request.headers.get('Authorization', '') != token_value: abort(401) data = request.json() items = data['items'] From c624e4d5a0873894e3ea72869f026cfc419dc942 Mon Sep 17 00:00:00 2001 From: Adrian Brandemuehl Date: Sat, 8 Apr 2017 14:12:58 -0500 Subject: [PATCH 8/9] Fix up errors --- machine_controller/app.py | 4 ++-- machine_controller/vend.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/machine_controller/app.py b/machine_controller/app.py index 828d51e..76aa110 100644 --- a/machine_controller/app.py +++ b/machine_controller/app.py @@ -42,10 +42,10 @@ merch = Merch() @app.route('/vend', methods=['POST']) -def hello_world(): +def vend(): if request.headers.get('Authorization', '') != token_value: abort(401) - data = request.json() + data = request.json items = data['items'] transaction_id = data['transaction_id'] diff --git a/machine_controller/vend.py b/machine_controller/vend.py index 004d901..0289cfa 100644 --- a/machine_controller/vend.py +++ b/machine_controller/vend.py @@ -110,7 +110,7 @@ def vend(self, letter, number): except TypeError: raise self.InvalidLocationError - if num < 1 or num > MAX_NUMBER: + if num < 1 or num > self.MAX_NUMBER: raise self.InvalidLocationError self.__vend(letter, str(number)) @@ -126,6 +126,9 @@ def __vend(self, letter, number): self.__sendKey(number) self.__commit() + # Wait for vend to complete + time.sleep(10) + def __sendKey(self, key): # TABLE OF OUTPUTS # ROW = {ROW[0],ROW[1],ROW[2]} From 595d0eed1865f7db716e29d43f4b1ad092d11adb Mon Sep 17 00:00:00 2001 From: Adrian Brandemuehl Date: Sat, 8 Apr 2017 14:13:34 -0500 Subject: [PATCH 9/9] Make status page return useful information The flask app is now multithreaded. Only one request will handle merch at a time through /vend, and the /status endpoint will return 200 if the machine is available to vend, and 503 if it is currently vending --- machine_controller/app.py | 22 ++++++++++++++++++---- machine_controller/vend.py | 14 ++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/machine_controller/app.py b/machine_controller/app.py index 76aa110..7e8ae89 100644 --- a/machine_controller/app.py +++ b/machine_controller/app.py @@ -51,6 +51,7 @@ def vend(): statuses = [] + merch.acquire() for i, item in enumerate(items): try: merch.vend(item[0], int(item[1])) @@ -62,11 +63,24 @@ def vend(): # goes wrong, we still need to let the client know instead of # throwing a 500 statuses.append({'error': str(e), 'location': item}) + merch.release() return jsonify(transaction_id=transaction_id, items=statuses) -if __name__ == '__main__': - # Make sure flask runs in a single thread. Otherwise concurrent requests - # may cause problems with vending - app.run(debug=True, host='0.0.0.0', threaded=False) +@app.route('/status', methods=['GET']) +def status(): + if request.headers.get('Authorization', '') != token_value: + abort(401) + + notready = merch.inUse() + if(notready): + return ('', 503) + else: + # 200 to indicate success + return ('', 200) + + + +if __name__ == '__main__': + app.run(debug=True, host='0.0.0.0', threaded=True) diff --git a/machine_controller/vend.py b/machine_controller/vend.py index 0289cfa..5b1f5ce 100644 --- a/machine_controller/vend.py +++ b/machine_controller/vend.py @@ -34,6 +34,7 @@ # THE SOFTWARE. import RPi.GPIO as GPIO import time +from threading import Condition, Lock class Merch: @@ -56,9 +57,22 @@ def __init__(self, debug=False): self.__low() self.__commit() + self.lock = Lock() + def __del__(self): self.__cleanup() + def acquire(self): + self.lock.acquire() + + def release(self): + self.lock.release() + + def inUse(self): + # Trylock + return self.lock.locked() + + def __cleanup(self): ''' Clean up all of the GPIO pins ''' GPIO.cleanup()