Skip to content

Commit c06bea7

Browse files
authored
Test ordering of motor run_to_position commands (#176)
* Test ordering of motor run_to_position commands * Restructure to use Future * Use Future to obtain voltage of build HAT * Support multiple non-blocking commands simultaneously * Enable 'del' to function correctly * Bump threshold temporarily * Refactor variable names
1 parent 74d7644 commit c06bea7

File tree

5 files changed

+115
-40
lines changed

5 files changed

+115
-40
lines changed

buildhat/devices.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import os
44
import sys
55
import weakref
6+
from concurrent.futures import Future
67

78
from .exc import DeviceError
89
from .serinterface import BuildHAT
@@ -202,11 +203,10 @@ def get(self):
202203
idx = self._combimode
203204
else:
204205
raise DeviceError("Not in simple or combimode")
206+
ftr = Future()
207+
self._hat.portftr[self.port].append(ftr)
205208
self._write(f"port {self.port} ; selonce {idx}\r")
206-
# wait for data
207-
with Device._instance.portcond[self.port]:
208-
Device._instance.portcond[self.port].wait()
209-
return self._conn.data
209+
return ftr.result()
210210

211211
def mode(self, modev):
212212
"""Set combimode or simple mode

buildhat/hat.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""HAT handling functionality"""
22

3+
from concurrent.futures import Future
4+
35
from .devices import Device
46

57

@@ -45,11 +47,10 @@ def get_vin(self):
4547
:return: Voltage on the input power jack
4648
:rtype: float
4749
"""
50+
ftr = Future()
51+
Device._instance.vinftr.append(ftr)
4852
Device._instance.write(b"vin\r")
49-
with Device._instance.vincond:
50-
Device._instance.vincond.wait()
51-
52-
return Device._instance.vin
53+
return ftr.result()
5354

5455
def _set_led(self, intmode):
5556
if isinstance(intmode, int) and intmode >= -1 and intmode <= 3:

buildhat/motors.py

+13-13
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import threading
44
import time
55
from collections import deque
6+
from concurrent.futures import Future
67
from enum import Enum
78
from threading import Condition
89

@@ -205,9 +206,10 @@ def _run_positional_ramp(self, pos, newpos, speed):
205206
cmd = (f"port {self.port}; combi 0 {self._combi} ; select 0 ; selrate {self._interval}; "
206207
f"pid {self.port} 0 1 s4 0.0027777778 0 5 0 .1 3; "
207208
f"set ramp {pos} {newpos} {dur} 0\r")
209+
ftr = Future()
210+
self._hat.rampftr[self.port].append(ftr)
208211
self._write(cmd)
209-
with self._hat.rampcond[self.port]:
210-
self._hat.rampcond[self.port].wait()
212+
ftr.result()
211213
if self._release:
212214
time.sleep(0.2)
213215
self.coast()
@@ -228,9 +230,7 @@ def run_for_degrees(self, degrees, speed=None, blocking=True):
228230
if not (speed >= -100 and speed <= 100):
229231
raise MotorError("Invalid Speed")
230232
if not blocking:
231-
th = threading.Thread(target=self._run_for_degrees, args=(degrees, speed))
232-
th.daemon = True
233-
th.start()
233+
self._queue((self._run_for_degrees, (degrees, speed)))
234234
else:
235235
self._run_for_degrees(degrees, speed)
236236

@@ -251,9 +251,7 @@ def run_to_position(self, degrees, speed=None, blocking=True, direction="shortes
251251
if degrees < -180 or degrees > 180:
252252
raise MotorError("Invalid angle")
253253
if not blocking:
254-
th = threading.Thread(target=self._run_to_position, args=(degrees, speed, direction))
255-
th.daemon = True
256-
th.start()
254+
self._queue((self._run_to_position, (degrees, speed, direction)))
257255
else:
258256
self._run_to_position(degrees, speed, direction)
259257

@@ -262,9 +260,10 @@ def _run_for_seconds(self, seconds, speed):
262260
cmd = (f"port {self.port} ; combi 0 {self._combi} ; select 0 ; selrate {self._interval}; "
263261
f"pid {self.port} 0 0 s1 1 0 0.003 0.01 0 100; "
264262
f"set pulse {speed} 0.0 {seconds} 0\r")
263+
ftr = Future()
264+
self._hat.pulseftr[self.port].append(ftr)
265265
self._write(cmd)
266-
with self._hat.pulsecond[self.port]:
267-
self._hat.pulsecond[self.port].wait()
266+
ftr.result()
268267
if self._release:
269268
self.coast()
270269
self._runmode = MotorRunmode.NONE
@@ -283,9 +282,7 @@ def run_for_seconds(self, seconds, speed=None, blocking=True):
283282
if not (speed >= -100 and speed <= 100):
284283
raise MotorError("Invalid Speed")
285284
if not blocking:
286-
th = threading.Thread(target=self._run_for_seconds, args=(seconds, speed))
287-
th.daemon = True
288-
th.start()
285+
self._queue((self._run_for_seconds, (seconds, speed)))
289286
else:
290287
self._run_for_seconds(seconds, speed)
291288

@@ -444,6 +441,9 @@ def release(self, value):
444441
raise MotorError("Must pass boolean")
445442
self._release = value
446443

444+
def _queue(self, cmd):
445+
Device._instance.motorqueue[self.port].put(cmd)
446+
447447

448448
class MotorPair:
449449
"""Pair of motors

buildhat/serinterface.py

+41-17
Original file line numberDiff line numberDiff line change
@@ -84,23 +84,24 @@ def __init__(self, firmware, signature, version, device="/dev/serial0", debug=Fa
8484
self.cond = Condition()
8585
self.state = HatState.OTHER
8686
self.connections = []
87-
self.portcond = []
88-
self.pulsecond = []
89-
self.rampcond = []
87+
self.portftr = []
88+
self.pulseftr = []
89+
self.rampftr = []
90+
self.vinftr = []
91+
self.motorqueue = []
9092
self.fin = False
9193
self.running = True
92-
self.vincond = Condition()
93-
self.vin = None
9494
if debug:
9595
tmp = tempfile.NamedTemporaryFile(suffix=".log", prefix="buildhat-", delete=False)
9696
logging.basicConfig(filename=tmp.name, format='%(asctime)s %(message)s',
9797
level=logging.DEBUG)
9898

9999
for _ in range(4):
100100
self.connections.append(Connection())
101-
self.portcond.append(Condition())
102-
self.pulsecond.append(Condition())
103-
self.rampcond.append(Condition())
101+
self.portftr.append([])
102+
self.pulseftr.append([])
103+
self.rampftr.append([])
104+
self.motorqueue.append(queue.Queue())
104105

105106
self.ser = serial.Serial(device, 115200, timeout=5)
106107
# Check if we're in the bootloader or the firmware
@@ -150,6 +151,11 @@ def __init__(self, firmware, signature, version, device="/dev/serial0", debug=Fa
150151
self.cb.daemon = True
151152
self.cb.start()
152153

154+
for q in self.motorqueue:
155+
ml = threading.Thread(target=self.motorloop, args=(q,))
156+
ml.daemon = True
157+
ml.start()
158+
153159
# Drop timeout value to 1s
154160
listevt = threading.Event()
155161
self.ser.timeout = 1
@@ -266,6 +272,8 @@ def shutdown(self):
266272
self.running = False
267273
self.th.join()
268274
self.cbqueue.put(())
275+
for q in self.motorqueue:
276+
q.put((None, None))
269277
self.cb.join()
270278
turnoff = ""
271279
for p in range(4):
@@ -278,6 +286,20 @@ def shutdown(self):
278286
self.write(f"{turnoff}\r".encode())
279287
self.write(b"port 0 ; select ; port 1 ; select ; port 2 ; select ; port 3 ; select ; echo 0\r")
280288

289+
def motorloop(self, q):
290+
"""Event handling for non-blocking motor commands
291+
292+
:param q: Queue of motor functions
293+
"""
294+
while self.running:
295+
func, data = q.get()
296+
if func is None:
297+
break
298+
else:
299+
func(*data)
300+
func = None # Necessary for 'del' to function correctly on motor object
301+
data = None
302+
281303
def callbackloop(self, q):
282304
"""Event handling for callbacks
283305
@@ -330,11 +352,11 @@ def loop(self, cond, uselist, q, listevt):
330352
if uselist and listevt.is_set():
331353
count += 1
332354
elif cmp(msg, BuildHAT.RAMPDONE):
333-
with self.rampcond[portid]:
334-
self.rampcond[portid].notify()
355+
ftr = self.rampftr[portid].pop()
356+
ftr.set_result(True)
335357
elif cmp(msg, BuildHAT.PULSEDONE):
336-
with self.pulsecond[portid]:
337-
self.pulsecond[portid].notify()
358+
ftr = self.pulseftr[portid].pop()
359+
ftr.set_result(True)
338360

339361
if uselist and count == 4:
340362
with cond:
@@ -362,11 +384,13 @@ def runit():
362384
if callit is not None:
363385
q.put((callit, newdata))
364386
self.connections[portid].data = newdata
365-
with self.portcond[portid]:
366-
self.portcond[portid].notify()
387+
try:
388+
ftr = self.portftr[portid].pop()
389+
ftr.set_result(newdata)
390+
except IndexError:
391+
pass
367392

368393
if len(line) >= 5 and line[1] == "." and line.endswith(" V"):
369394
vin = float(line.split(" ")[0])
370-
self.vin = vin
371-
with self.vincond:
372-
self.vincond.notify()
395+
ftr = self.vinftr.pop()
396+
ftr.set_result(vin)

test/motors.py

+52-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
class TestMotor(unittest.TestCase):
1111
"""Test motors"""
1212

13+
THRESHOLD_DISTANCE = 15
14+
1315
def test_rotations(self):
1416
"""Test motor rotating"""
1517
m = Motor('A')
@@ -19,18 +21,66 @@ def test_rotations(self):
1921
rotated = (pos2 - pos1) / 360
2022
self.assertLess(abs(rotated - 2), 0.5)
2123

24+
def test_nonblocking(self):
25+
"""Test motor nonblocking mode"""
26+
m = Motor('A')
27+
last = 0
28+
for delay in [1, 0]:
29+
for _ in range(3):
30+
m.run_to_position(90, blocking=False)
31+
time.sleep(delay)
32+
m.run_to_position(90, blocking=False)
33+
time.sleep(delay)
34+
m.run_to_position(90, blocking=False)
35+
time.sleep(delay)
36+
m.run_to_position(last, blocking=False)
37+
time.sleep(delay)
38+
# Wait for a bit, before reading last position
39+
time.sleep(7)
40+
pos1 = m.get_aposition()
41+
diff = abs((last - pos1 + 180) % 360 - 180)
42+
self.assertLess(diff, self.THRESHOLD_DISTANCE)
43+
44+
def test_nonblocking_multiple(self):
45+
"""Test motor nonblocking mode"""
46+
m1 = Motor('A')
47+
m2 = Motor('B')
48+
last = 0
49+
for delay in [1, 0]:
50+
for _ in range(3):
51+
m1.run_to_position(90, blocking=False)
52+
m2.run_to_position(90, blocking=False)
53+
time.sleep(delay)
54+
m1.run_to_position(90, blocking=False)
55+
m2.run_to_position(90, blocking=False)
56+
time.sleep(delay)
57+
m1.run_to_position(90, blocking=False)
58+
m2.run_to_position(90, blocking=False)
59+
time.sleep(delay)
60+
m1.run_to_position(last, blocking=False)
61+
m2.run_to_position(last, blocking=False)
62+
time.sleep(delay)
63+
# Wait for a bit, before reading last position
64+
time.sleep(7)
65+
pos1 = m1.get_aposition()
66+
diff = abs((last - pos1 + 180) % 360 - 180)
67+
self.assertLess(diff, self.THRESHOLD_DISTANCE)
68+
pos2 = m2.get_aposition()
69+
diff = abs((last - pos2 + 180) % 360 - 180)
70+
self.assertLess(diff, self.THRESHOLD_DISTANCE)
71+
2272
def test_position(self):
2373
"""Test motor goes to desired position"""
2474
m = Motor('A')
2575
m.run_to_position(0)
2676
pos1 = m.get_aposition()
2777
diff = abs((0 - pos1 + 180) % 360 - 180)
28-
self.assertLess(diff, 10)
78+
self.assertLess(diff, self.THRESHOLD_DISTANCE)
2979

3080
m.run_to_position(180)
3181
pos1 = m.get_aposition()
3282
diff = abs((180 - pos1 + 180) % 360 - 180)
33-
self.assertLess(diff, 10)
83+
self.assertLess(diff, self.THRESHOLD_DISTANCE)
3484

3585
def test_time(self):
3686
"""Test motor runs for correct duration"""

0 commit comments

Comments
 (0)