Skip to content

Commit 365e8da

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

File tree

4 files changed

+142
-10
lines changed

4 files changed

+142
-10
lines changed

buildhat/__init__.py

+1-1
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

+9-9
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

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

test/track_target_motor.py

+42
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)