Skip to content

Commit e0ddce3

Browse files
authored
Prevent using same port multiple times (#121)
* Prevent using same port multiple times * Use WeakMethod for callbacks, to avoid bumping up refcount of Device. Also make sure we wait for callback loop to terminate to avoid _enter_buffered_busy error
1 parent 345022e commit e0ddce3

File tree

10 files changed

+155
-96
lines changed

10 files changed

+155
-96
lines changed

buildhat/color.py

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -143,27 +143,31 @@ def get_color_hsv(self):
143143
hue = int((math.degrees((math.atan2(s,c))) + 360) % 360)
144144
sat = int(sum([hsv[1] for hsv in readings]) / len(readings))
145145
val = int(sum([hsv[2] for hsv in readings]) / len(readings))
146-
return (hue, sat, val)
146+
return (hue, sat, val)
147+
148+
def _cb_handle(self, lst):
149+
self._data.append(lst[:4])
150+
if len(self._data) == self.avg_reads:
151+
r, g, b, _ = self._avgrgbi(self._data)
152+
seg = self.segment_color(r, g, b)
153+
if self._cmp(seg, self._color):
154+
with self._cond:
155+
self._old_color = seg
156+
self._cond.notify()
147157

148158
def wait_until_color(self, color):
149159
"""Waits until specific color
150160
151161
:param color: Color to look for
152162
"""
153163
self.mode(5)
154-
cond = Condition()
155-
data = deque(maxlen=self.avg_reads)
156-
def cb(lst):
157-
data.append(lst[:4])
158-
if len(data) == self.avg_reads:
159-
r, g, b, _ = self._avgrgbi(data)
160-
seg = self.segment_color(r, g, b)
161-
if seg == color:
162-
with cond:
163-
cond.notify()
164-
self.callback(cb)
165-
with cond:
166-
cond.wait()
164+
self._cond = Condition()
165+
self._data = deque(maxlen=self.avg_reads)
166+
self._color = color
167+
self._cmp = lambda x, y: x == y
168+
self.callback(self._cb_handle)
169+
with self._cond:
170+
self._cond.wait()
167171
self.callback(None)
168172

169173
def wait_for_new_color(self):
@@ -173,20 +177,13 @@ def wait_for_new_color(self):
173177
if self._old_color is None:
174178
self._old_color = self.get_color()
175179
return self._old_color
176-
cond = Condition()
177-
data = deque(maxlen=self.avg_reads)
178-
def cb(lst):
179-
data.append(lst[:4])
180-
if len(data) == self.avg_reads:
181-
r, g, b, _ = self._avgrgbi(data)
182-
seg = self.segment_color(r, g, b)
183-
if seg != self._old_color:
184-
self._old_color = seg
185-
with cond:
186-
cond.notify()
187-
self.callback(cb)
188-
with cond:
189-
cond.wait()
180+
self._cond = Condition()
181+
self._data = deque(maxlen=self.avg_reads)
182+
self._color = self._old_color
183+
self._cmp = lambda x, y: x != y
184+
self.callback(self._cb_handle)
185+
with self._cond:
186+
self._cond.wait()
190187
self.callback(None)
191188
return self._old_color
192189

buildhat/colordistance.py

Lines changed: 24 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -127,25 +127,29 @@ def get_color_rgb(self):
127127
reads.append(self.get())
128128
return self._avgrgb(reads)
129129

130+
def _cb_handle(self, lst):
131+
self._data.append(lst)
132+
if len(self._data) == self.avg_reads:
133+
r, g, b = self._avgrgb(self._data)
134+
seg = self.segment_color(r, g, b)
135+
if self._cmp(seg, self._color):
136+
with self._cond:
137+
self._old_color = seg
138+
self._cond.notify()
139+
130140
def wait_until_color(self, color):
131141
"""Waits until specific color
132142
133143
:param color: Color to look for
134144
"""
135145
self.mode(6)
136-
cond = Condition()
137-
data = deque(maxlen=self.avg_reads)
138-
def cb(lst):
139-
data.append(lst)
140-
if len(data) == self.avg_reads:
141-
r, g, b = self._avgrgb(data)
142-
seg = self.segment_color(r, g, b)
143-
if seg == color:
144-
with cond:
145-
cond.notify()
146-
self.callback(cb)
147-
with cond:
148-
cond.wait()
146+
self._cond = Condition()
147+
self._data = deque(maxlen=self.avg_reads)
148+
self._color = color
149+
self._cmp = lambda x, y: x == y
150+
self.callback(self._cb_handle)
151+
with self._cond:
152+
self._cond.wait()
149153
self.callback(None)
150154

151155
def wait_for_new_color(self):
@@ -155,20 +159,13 @@ def wait_for_new_color(self):
155159
if self._old_color is None:
156160
self._old_color = self.get_color()
157161
return self._old_color
158-
cond = Condition()
159-
data = deque(maxlen=self.avg_reads)
160-
def cb(lst):
161-
data.append(lst)
162-
if len(data) == self.avg_reads:
163-
r, g, b = self._avgrgb(data)
164-
seg = self.segment_color(r, g, b)
165-
if seg != self._old_color:
166-
self._old_color = seg
167-
with cond:
168-
cond.notify()
169-
self.callback(cb)
170-
with cond:
171-
cond.wait()
162+
self._cond = Condition()
163+
self._data = deque(maxlen=self.avg_reads)
164+
self._color = self._old_color
165+
self._cmp = lambda x, y: x != y
166+
self.callback(self._cb_handle)
167+
with self._cond:
168+
self._cond.wait()
172169
self.callback(None)
173170
return self._old_color
174171

buildhat/devices.py

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from .serinterface import BuildHAT
2-
from .exc import DeviceNotFound, DeviceChanged, DeviceInvalidMode, DeviceInvalid
2+
from .exc import DeviceNotFound, DeviceChanged, DeviceInvalidMode, DeviceInvalid, PortInUse
33
import weakref
44
import time
55
import os
@@ -29,29 +29,48 @@ class Device:
2929
75: "Motor",
3030
76: "Motor"
3131
}
32+
_used = { 0: False,
33+
1: False,
34+
2: False,
35+
3: False
36+
}
3237

3338
def __init__(self, port):
3439
if not isinstance(port, str) or len(port) != 1:
3540
raise DeviceNotFound("Invalid port")
3641
p = ord(port) - ord('A')
3742
if not (p >= 0 and p <= 3):
3843
raise DeviceNotFound("Invalid port")
44+
if Device._used[p]:
45+
raise PortInUse("Port already used")
3946
self.port = p
40-
if not Device._instance:
41-
data = os.path.join(os.path.dirname(sys.modules["buildhat"].__file__),"data/")
42-
firm = os.path.join(data,"firmware.bin")
43-
sig = os.path.join(data,"signature.bin")
44-
ver = os.path.join(data,"version")
45-
vfile = open(ver)
46-
v = int(vfile.read())
47-
vfile.close()
48-
Device._instance = BuildHAT(firm, sig, v)
49-
weakref.finalize(self, self._close)
47+
Device._setup()
5048
self._simplemode = -1
5149
self._combimode = -1
5250
self._typeid = self._conn.typeid
5351
if (self._typeid in Device._device_names and Device._device_names[self._typeid] != type(self).__name__) or self._typeid == -1:
5452
raise DeviceInvalid('There is not a {} connected to port {} (Found {})'.format(type(self).__name__, port, self.name))
53+
Device._used[p] = True
54+
55+
def _setup(device="/dev/serial0"):
56+
if Device._instance:
57+
return
58+
data = os.path.join(os.path.dirname(sys.modules["buildhat"].__file__),"data/")
59+
firm = os.path.join(data,"firmware.bin")
60+
sig = os.path.join(data,"signature.bin")
61+
ver = os.path.join(data,"version")
62+
vfile = open(ver)
63+
v = int(vfile.read())
64+
vfile.close()
65+
Device._instance = BuildHAT(firm, sig, v, device=device)
66+
weakref.finalize(Device._instance, Device._instance.shutdown)
67+
68+
def __del__(self):
69+
if hasattr(self, "port") and Device._used[self.port]:
70+
Device._used[self.port] = False
71+
self._conn.callit = None
72+
self.deselect()
73+
self.off()
5574

5675
@property
5776
def _conn(self):
@@ -83,9 +102,6 @@ def name(self):
83102
else:
84103
return "Unknown"
85104

86-
def _close(self):
87-
Device._instance.shutdown()
88-
89105
def isconnected(self):
90106
if not self.connected:
91107
raise DeviceNotFound("No device found")
@@ -163,4 +179,7 @@ def callback(self, func):
163179
self.select()
164180
else:
165181
self.deselect()
166-
self._conn.callit = func
182+
if func is None:
183+
self._conn.callit = None
184+
else:
185+
self._conn.callit = weakref.WeakMethod(func)

buildhat/exc.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ class DeviceNotFound(Exception):
2222
class DeviceChanged(Exception):
2323
pass
2424

25+
class PortInUse(Exception):
26+
pass
27+
2528
class DistanceSensorException(Exception):
2629
pass
2730

buildhat/hat.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,8 @@
77
class Hat:
88
"""Allows enumeration of devices which are connected to the hat
99
"""
10-
def __init__(self, device="/dev/serial0"):
11-
if not Device._instance:
12-
data = os.path.join(os.path.dirname(sys.modules["buildhat"].__file__),"data/")
13-
firm = os.path.join(data,"firmware.bin")
14-
sig = os.path.join(data,"signature.bin")
15-
ver = os.path.join(data,"version")
16-
vfile = open(ver)
17-
v = int(vfile.read())
18-
vfile.close()
19-
Device._instance = BuildHAT(firm, sig, v, device=device)
20-
weakref.finalize(self, self._close)
10+
def __init__(self):
11+
Device._setup()
2112

2213
def get(self):
2314
"""Gets devices which are connected or disconnected

buildhat/motors.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,6 @@ def set_default_speed(self, default_speed):
8585
raise MotorException("Invalid Speed")
8686
self.default_speed = default_speed
8787

88-
def _isfinishedcb(self, speed, pos, apos):
89-
self._bqueue.append(pos)
90-
9188
def run_for_rotations(self, rotations, speed=None, blocking=True):
9289
"""Runs motor for N rotations
9390
@@ -266,22 +263,21 @@ def when_rotated(self):
266263
"""
267264
return self._when_rotated
268265

269-
def _intermediate(self, value, speed, pos, apos):
266+
def _intermediate(self, data):
267+
speed, pos, apos = data
270268
if self._oldpos is None:
271269
self._oldpos = pos
272270
return
273271
if abs(pos - self._oldpos) >= 1:
274-
value(speed, pos, apos)
272+
if self._when_rotated is not None:
273+
self._when_rotated(speed, pos, apos)
275274
self._oldpos = pos
276275

277276
@when_rotated.setter
278277
def when_rotated(self, value):
279278
"""Calls back, when motor has been rotated"""
280-
if value is not None:
281-
self._when_rotated = lambda lst: [self._intermediate(value, lst[0], lst[1], lst[2]),self._isfinishedcb(lst[0], lst[1], lst[2])]
282-
else:
283-
self._when_rotated = lambda lst: self._isfinishedcb(lst[0], lst[1], lst[2])
284-
self.callback(self._when_rotated)
279+
self._when_rotated = value
280+
self.callback(self._intermediate)
285281

286282
def plimit(self, plimit):
287283
if not (plimit >= 0 and plimit <= 1):

buildhat/serinterface.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from .exc import DeviceNotFound, DeviceChanged, DeviceInvalid, HatNotFound
2-
import importlib
32
import threading
43
import gpiozero
54
import serial
@@ -186,6 +185,8 @@ def shutdown(self):
186185
self.fin = True
187186
self.running = False
188187
self.th.join()
188+
self.cbqueue.put(())
189+
self.cb.join()
189190
turnoff = ""
190191
for p in range(4):
191192
conn = self.connections[p]
@@ -199,7 +200,13 @@ def shutdown(self):
199200
def callbackloop(self, q):
200201
while self.running:
201202
cb = q.get()
202-
cb[0](cb[1])
203+
# Test for empty tuple, which should only be passed when
204+
# we're shutting down
205+
if len(cb) == 0:
206+
continue
207+
if not cb[0]._alive:
208+
continue
209+
cb[0]()(cb[1])
203210
q.task_done()
204211

205212
def loop(self, cond, uselist, q):

test/distance.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import unittest
2+
from buildhat import DistanceSensor
3+
from buildhat.exc import PortInUse
4+
5+
class TestDistance(unittest.TestCase):
6+
7+
def test_properties(self):
8+
d = DistanceSensor('A')
9+
self.assertIsInstance(d.distance, int)
10+
self.assertIsInstance(d.threshold_distance, int)
11+
12+
def test_distance(self):
13+
d = DistanceSensor('A')
14+
self.assertIsInstance(d.get_distance(), int)
15+
16+
def test_eyes(self):
17+
d = DistanceSensor('A')
18+
d.eyes(100, 100, 100, 100)
19+
20+
def test_duplicate_port(self):
21+
d = DistanceSensor('A')
22+
self.assertRaises(PortInUse, DistanceSensor, 'A')
23+
24+
def test_del(self):
25+
d = DistanceSensor('A')
26+
del d
27+
d = DistanceSensor('A')
28+
29+
if __name__ == '__main__':
30+
unittest.main()

test/vin.py renamed to test/hat.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@
22
from buildhat import Hat
33

44

5-
class TestVin(unittest.TestCase):
5+
class TestHat(unittest.TestCase):
66

77
def test_vin(self):
88
h = Hat()
99
vin = h.get_vin()
1010
self.assertGreaterEqual(vin, 7.2)
1111
self.assertLessEqual(vin, 8.5)
1212

13+
def test_get(self):
14+
h = Hat()
15+
self.assertIsInstance(h.get(), dict)
1316

1417
if __name__ == '__main__':
1518
unittest.main()

0 commit comments

Comments
 (0)