Skip to content

Commit 2896324

Browse files
committed
add new motor class for tracking angles without queuing or threading (see issue #195)
1 parent 9c1dbb9 commit 2896324

File tree

4 files changed

+140
-10
lines changed

4 files changed

+140
-10
lines changed

buildhat/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@
88
from .hat import Hat
99
from .light import Light
1010
from .matrix import Matrix
11-
from .motors import Motor, MotorPair, PassiveMotor
11+
from .motors import Motor, MotorPair, PassiveMotor, TargetTrackerMotor
1212
from .serinterface import BuildHAT
1313
from .wedo import MotionSensor, TiltSensor

buildhat/devices.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@ class Device:
2424
62: ("DistanceSensor", "Distance Sensor"), # 45604
2525
63: ("ForceSensor", "Force Sensor"), # 45606
2626
64: ("Matrix", "3x3 Color Light Matrix"), # 45608
27-
38: ("Motor", "Medium Linear Motor"), # 88008
28-
46: ("Motor", "Large Motor"), # 88013
29-
47: ("Motor", "XL Motor"), # 88014
30-
48: ("Motor", "Medium Angular Motor (Cyan)"), # 45603
31-
49: ("Motor", "Large Angular Motor (Cyan)"), # 45602
32-
65: ("Motor", "Small Angular Motor"), # 45607
33-
75: ("Motor", "Medium Angular Motor (Grey)"), # 88018
34-
76: ("Motor", "Large Angular Motor (Grey)")} # 88017
27+
38: (["Motor", "TargetTrackerMotor"], "Medium Linear Motor"), # 88008
28+
46: (["Motor", "TargetTrackerMotor"], "Large Motor"), # 88013
29+
47: (["Motor", "TargetTrackerMotor"], "XL Motor"), # 88014
30+
48: (["Motor", "TargetTrackerMotor"], "Medium Angular Motor (Cyan)"), # 45603
31+
49: (["Motor", "TargetTrackerMotor"], "Large Angular Motor (Cyan)"), # 45602
32+
65: (["Motor", "TargetTrackerMotor"], "Small Angular Motor"), # 45607
33+
75: (["Motor", "TargetTrackerMotor"], "Medium Angular Motor (Grey)"), # 88018
34+
76: (["Motor", "TargetTrackerMotor"], "Large Angular Motor (Grey)")} # 88017
3535

3636
_used = {0: False,
3737
1: False,
@@ -63,7 +63,7 @@ def __init__(self, port):
6363
self._interval = 10
6464
if (
6565
self._typeid in Device._device_names
66-
and Device._device_names[self._typeid][0] != type(self).__name__ # noqa: W503
66+
and type(self).__name__ not in Device._device_names[self._typeid][0] # noqa: W503
6767
) or self._typeid == -1:
6868
raise DeviceError(f'There is not a {type(self).__name__} connected to port {port} (Found {self.name})')
6969
Device._used[p] = True

buildhat/motors.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,3 +596,91 @@ def release(self, value):
596596
self._release = value
597597
self._leftmotor.release = value
598598
self._rightmotor.release = value
599+
600+
601+
class TargetTrackerMotor(Device):
602+
"""Motor device which can be used to track a target. This type of motor is non-blocking by default and doesn't wait
603+
for a command to be finished. An actual running command will be overwritten. If you need sequential execution which
604+
wait's for a movement to finish, then use the ~Motor class.
605+
606+
:param port: Port of device
607+
:raises DeviceError: Occurs if there is no motor attached to port
608+
"""
609+
610+
def __init__(self, port):
611+
"""Initialise motor
612+
613+
:param port:
614+
"""
615+
super().__init__(port)
616+
if self._typeid in {38}:
617+
self.mode([(1, 0), (2, 0)])
618+
self._combi = "1 0 2 0"
619+
self._noapos = True
620+
else:
621+
self.mode([(1, 0), (2, 0), (3, 0)])
622+
self._combi = "1 0 2 0 3 0"
623+
self._noapos = False
624+
self.plimit(0.7)
625+
self.bias(0.3)
626+
self._init_pid()
627+
628+
def _init_pid(self):
629+
"""Initialize the pid controller"""
630+
cmd = f"pid {self.port} 0 5 s2 0.0027777778 1 5 0 .1 3\r "
631+
self._write(cmd)
632+
633+
def plimit(self, plimit):
634+
"""Limit power
635+
636+
:param plimit: Value 0 to 1
637+
:raises MotorError: Occurs if invalid plimit value passed
638+
"""
639+
if not (0 <= plimit <= 1):
640+
raise MotorError("plimit should be 0 to 1")
641+
self._write(f"port {self.port} ; plimit {plimit}\r")
642+
643+
def bias(self, bias):
644+
"""Bias motor
645+
646+
:param bias: Value 0 to 1
647+
:raises MotorError: Occurs if invalid bias value passed
648+
"""
649+
if not (0 <= bias <= 1):
650+
raise MotorError("bias should be 0 to 1")
651+
self._write(f"port {self.port} ; bias {bias}\r")
652+
653+
def run_to_position(self, degrees):
654+
"""Run motor to position (in degrees)
655+
656+
:param degrees: Position in degrees from -180 to 180
657+
"""
658+
cmd = f"port {self.port}; set {1 / 360 * degrees}\r"
659+
self._write(cmd)
660+
661+
def get_position(self):
662+
"""Get position of motor with relation to preset position (can be negative or positive)
663+
664+
:return: Position of motor in degrees from preset position
665+
:rtype: int
666+
"""
667+
return self.get()[1]
668+
669+
def get_aposition(self):
670+
"""Get absolute position of motor
671+
672+
:return: Absolute position of motor from -180 to 180
673+
:rtype: int
674+
"""
675+
if self._noapos:
676+
raise MotorError("No absolute position with this motor")
677+
else:
678+
return self.get()[2]
679+
680+
def get_speed(self):
681+
"""Get speed of motor
682+
683+
:return: Speed of motor
684+
:rtype: int
685+
"""
686+
return self.get()[0]

test/track_target_motor.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""Test motors with ~TargetTrackerMotor"""
2+
3+
import time
4+
import unittest
5+
6+
from buildhat import TargetTrackerMotor
7+
8+
9+
class TestTargetTrackerMotor(unittest.TestCase):
10+
"""Test ~TargetTrackerMotor"""
11+
12+
def test_command_overwrites(self):
13+
"""Test motor rotating"""
14+
m = TargetTrackerMotor('A')
15+
16+
# position motor at 0 degrees
17+
pos1 = m.get_aposition()
18+
t0 = time.time()
19+
while pos1 < -5 or pos1 > 5:
20+
m.run_to_position(0)
21+
time.sleep(0.1)
22+
pos1 = m.get_aposition()
23+
if time.time() - t0 > 3:
24+
self.fail("Motor could not reach start position in given time!")
25+
26+
angles = [0, 90, 0, 90, 0]
27+
28+
max_angle_reached = 0
29+
30+
for _ in range(5):
31+
for angle in angles:
32+
m.run_to_position(angle)
33+
time.sleep(0.1)
34+
pos2 = m.get_aposition()
35+
if max_angle_reached < pos2:
36+
max_angle_reached = pos2
37+
38+
self.assertLess(max_angle_reached, 45)
39+
40+
41+
if __name__ == '__main__':
42+
unittest.main()

0 commit comments

Comments
 (0)