Skip to content

Commit badc4c2

Browse files
committed
Add tests, vend queue
Adds unittests for the vending functions, makes vends get queued into a workthread, making them nonblocking Resolves #2
1 parent d5ce3c2 commit badc4c2

File tree

6 files changed

+537
-39
lines changed

6 files changed

+537
-39
lines changed

machine_controller/app.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,14 @@
3737
from vend import Merch
3838

3939
app = Flask(__name__)
40-
merch = Merch()
40+
merch = Merch.Instance()
4141

4242

4343
@app.route('/vend', methods=['POST'])
44-
def hello_world():
44+
def vend():
4545
if 'item' not in request.args:
4646
abort(400)
47-
item = request.args['item']
47+
item = request.args.getlist('item')
4848
merch.vend(item[0], item[1])
4949
return json.dumps({'success': True}), 200, {'ContentType': 'application/json'}
5050

machine_controller/mockgpio.py

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import copy
2+
3+
4+
5+
class MockGPIO:
6+
# From https://github.com/TCAllen07/raspi-device-mocks
7+
# Map format is <BCM-#>: <BOARD-#>
8+
bcm_board_map = { 2: 3,
9+
3: 5, 4: 7, 14: 8, 15: 10, 17: 11,
10+
18: 12, 27: 13, 22: 15, 23: 16, 24: 18,
11+
10: 19, 9: 21, 25: 22, 11: 23, 8: 24,
12+
7: 26, 5: 29, 6: 31, 12: 32, 13: 33,
13+
19: 35, 16: 36, 26: 37, 20: 38, 21: 40}
14+
15+
# Map format is <BOARD-#>: <BCM-#>
16+
gpio_board_map = { 3: 2,
17+
5: 3, 7: 4, 8: 14, 10: 15, 11: 17,
18+
12: 18, 13: 27, 15: 22, 16: 23, 18: 24,
19+
19: 10, 21: 9, 22: 25, 23: 11, 24: 8,
20+
26: 7, 29: 5, 31: 6, 32: 12, 33: 13,
21+
35: 19, 36: 16, 37: 26, 38: 20, 40: 21}
22+
23+
24+
25+
LOW = 0
26+
HIGH = 1
27+
28+
BCM = 11
29+
BOARD = 10
30+
31+
OUT = 0
32+
IN = 1
33+
34+
PUD_OFF = 20
35+
PUD_DOWN = 21
36+
PUD_UP = 22
37+
38+
# Indexed by board pin number
39+
gpio_direction = {k: 1 for k in bcm_board_map.values()}
40+
gpio_values = {}
41+
42+
def __init__(self):
43+
self.mode = -1
44+
45+
self.setmode_run = False
46+
self.setup_run = False
47+
48+
self.states = []
49+
50+
51+
52+
def setmode(self, mode):
53+
if mode not in (self.BCM, self.BOARD):
54+
raise ValueError("An invalid mode was passed to setmode()")
55+
self.mode = mode
56+
self.setmode_run = True
57+
58+
def getmode(self):
59+
return self.mode
60+
61+
62+
def __pin_validate(self, pin):
63+
if self.mode == self.BCM:
64+
if pin not in self.bcm_board_map.keys():
65+
raise ValueError('Pin is invalid')
66+
elif self.mode == self.BOARD:
67+
if pin not in self.gpio_board_map.keys():
68+
raise ValueError('Pin is invalid')
69+
else:
70+
raise ValueError('Setup has not been called yet')
71+
72+
73+
74+
75+
def output(self, pins, value):
76+
if not hasattr(pins, '__iter__'):
77+
pins = [pins, ]
78+
for pin in pins:
79+
self.__pin_validate(pin)
80+
81+
if value not in (self.HIGH, self.LOW):
82+
raise ValueError('An invalid value was passed to output()')
83+
84+
if not self.setmode_run:
85+
raise RuntimeError('output() was called before setmode()')
86+
if not self.setup_run:
87+
raise RuntimeError('output() was called before setup()')
88+
89+
for pin in pins:
90+
self.gpio_values[pin] = value
91+
self.states.append(copy.deepcopy(self.gpio_values))
92+
93+
94+
def input(self, pins):
95+
if not hasattr(pins, '__iter__'):
96+
pins = [pins, ]
97+
for pin in pins:
98+
self.__pin_validate(pin)
99+
100+
if not self.setmode_run:
101+
raise RuntimeError('input() was called before setmode()')
102+
if not self.setup_run:
103+
raise RuntimeError('input() was called before setup()')
104+
105+
def gpio_function(self, pin):
106+
self.__pin_validate(pin)
107+
if not self.setmode_run:
108+
raise RuntimeError('gpio_function() was called before setmode()')
109+
if self.mode == self.BCM:
110+
return self.gpio_direction[self.bcm_board_map[pin]]
111+
else:
112+
return self.gpio_direction[pin]
113+
114+
def cleanup(self):
115+
self.setup_run = False
116+
self.setmode_run = False
117+
self.mode = -1
118+
119+
for pin in self.gpio_direction:
120+
self.gpio_direction[pin] = self.IN
121+
122+
def setup(self, pins, direction, pull_up_down=None, initial=None):
123+
if not hasattr(pins, '__iter__'):
124+
pins = [pins, ]
125+
126+
for pin in pins:
127+
self.__pin_validate(pin)
128+
129+
if direction not in (self.IN, self.OUT):
130+
raise ValueError('An invalid direction was passed to setup()')
131+
if (pull_up_down is not None and
132+
pull_up_down not in (self.PUD_OFF, self.PUD_DOWN, self.PUD_UP)):
133+
raise ValueError('Invalid Pull Up Down setting passed to setup()')
134+
self.setup_run = True
135+
if self.mode == self.BCM:
136+
self.gpio_direction[self.bcm_board_map[pin]] = direction
137+
else:
138+
self.gpio_direction[pin] = direction
139+
140+
141+
# Placeholders
142+
def add_event_callback(self, *args):
143+
pass
144+
145+
def add_event_detect(self, *args):
146+
pass
147+
148+
def setwarnings(self, *args):
149+
pass
150+
151+
def wait_for_edge(self, *args):
152+
pass
153+
154+
def event_detected(self, *args):
155+
pass
156+
157+
def remove_event_detect(self, *args):
158+
pass
159+
160+
161+
GPIO = MockGPIO()

machine_controller/singleton.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# From http://stackoverflow.com/questions/31875/is-there-a-simple-elegant-way-to-define-singletons
2+
class Singleton:
3+
"""
4+
A non-thread-safe helper class to ease implementing singletons.
5+
This should be used as a decorator -- not a metaclass -- to the
6+
class that should be a singleton.
7+
8+
The decorated class can define one `__init__` function that
9+
takes only the `self` argument. Also, the decorated class cannot be
10+
inherited from. Other than that, there are no restrictions that apply
11+
to the decorated class.
12+
13+
To get the singleton instance, use the `Instance` method. Trying
14+
to use `__call__` will result in a `TypeError` being raised.
15+
16+
"""
17+
18+
def __init__(self, decorated):
19+
self._decorated = decorated
20+
21+
def Instance(self):
22+
"""
23+
Returns the singleton instance. Upon its first call, it creates a
24+
new instance of the decorated class and calls its `__init__` method.
25+
On all subsequent calls, the already created instance is returned.
26+
27+
"""
28+
try:
29+
return self._instance
30+
except AttributeError:
31+
self._instance = self._decorated()
32+
return self._instance
33+
34+
def __call__(self):
35+
raise TypeError('Singletons must be accessed through `Instance()`.')
36+
37+
def __instancecheck__(self, inst):
38+
return isinstance(inst, self._decorated)

machine_controller/task_queue.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# University of Illinois/NCSA Open Source License
2+
#
3+
# Copyright (c) 2017 ACM@UIUC
4+
# All rights reserved.
5+
#
6+
# Developed by: SIGBot
7+
# ACM@UIUC
8+
# https://acm.illinois.edu
9+
#
10+
# Permission is hereby granted, free of charge, to any person obtaining a copy
11+
# of this software and associated documentation files (the 'Software'), to deal
12+
# with the Software without restriction, including without limitation the rights
13+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14+
# copies of the Software, and to permit persons to whom the Software is
15+
# furnished to do so, subject to the following conditions:
16+
#
17+
# * Redistributions of source code must retain the above copyright notice,
18+
# this list of conditions and the following disclaimers.
19+
#
20+
# * Redistributions in binary form must reproduce the above copyright
21+
# notice, this list of conditions and the following disclaimers in the
22+
# documentation and/or other materials provided with the distribution.
23+
#
24+
# * Neither the names of the SIGBot, ACM@UIUC, nor the names of its
25+
# contributors may be used to endorse or promote products derived from
26+
# this Software without specific prior written permission.
27+
#
28+
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
29+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
30+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
31+
# CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
32+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
33+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH
34+
# THE SOFTWARE.
35+
36+
37+
from threading import Thread, Condition, Lock
38+
39+
40+
class TaskQueue(Thread):
41+
'''Task Queue that runs in a separate "thread"'''
42+
class Promise():
43+
'''An object that can be waited on'''
44+
def __init__(self):
45+
self.condition = Condition()
46+
self.wait_done = False
47+
48+
def wait(self):
49+
'''Wait for the work to be done'''
50+
with self.condition:
51+
while not self.wait_done:
52+
self.condition.wait()
53+
54+
def notify(self):
55+
'''Wake waiters'''
56+
with self.condition:
57+
self.wait_done = True
58+
self.condition.notifyAll()
59+
60+
class Work():
61+
'''Represents a piece of work to be done'''
62+
def __init__(self, func):
63+
self.func = func
64+
self.promise = TaskQueue.Promise()
65+
66+
def __call__(self):
67+
self.run()
68+
69+
def run(self):
70+
self.func()
71+
self.promise.notify()
72+
73+
def __init__(self):
74+
super(TaskQueue, self).__init__()
75+
self.work_queue = []
76+
# Condition variable to protect the work queue
77+
# In the threading library, this acts as both a lock and a condition
78+
# variable
79+
self.work_condition = Condition()
80+
81+
self.shutdown_lock = Lock()
82+
self.shutdown_ = False
83+
def __del__(self):
84+
self.shutdown()
85+
86+
def run(self):
87+
'''Start doing work in a separate thread'''
88+
89+
self.shutdown_lock.acquire()
90+
while not self.shutdown_:
91+
self.shutdown_lock.release()
92+
93+
work = None
94+
# Make sure to handle the work queue with the lock
95+
with self.work_condition:
96+
while len(self.work_queue) == 0:
97+
self.shutdown_lock.acquire()
98+
if self.shutdown_:
99+
self.shutdown_lock.release()
100+
return
101+
self.shutdown_lock.release()
102+
# Wait for values to be available
103+
self.work_condition.wait()
104+
105+
# I just recently found out that this is an atomic operation...
106+
work = self.work_queue.pop(0)
107+
108+
if work:
109+
# Do the work. Arguments should be bound to the function object
110+
work()
111+
112+
# Reacquire the lock before we check its value in the loop
113+
self.shutdown_lock.acquire()
114+
self.shutdown_lock.release()
115+
116+
def add_work(self, func):
117+
'''Add work to the queue
118+
119+
Arguments:
120+
work -- a function to be called by the work queue. If the function to
121+
be called has arguments, use partial application
122+
(`from functools import partial`)
123+
'''
124+
with self.work_condition:
125+
work = TaskQueue.Work(func)
126+
self.work_queue.append(work)
127+
128+
# We're notifying all waiters, but there should only be one
129+
self.work_condition.notifyAll()
130+
return work.promise
131+
132+
def shutdown(self):
133+
'''Shut down the work queue'''
134+
with self.shutdown_lock:
135+
self.shutdown_ = True
136+
with self.work_condition:
137+
self.work_condition.notifyAll()

0 commit comments

Comments
 (0)