Skip to content

Commit d659701

Browse files
committed
Merge branch 'main' of github.com:RaspberryPiFoundation/build-hat
2 parents f9a32a8 + 3d5affd commit d659701

File tree

14 files changed

+196
-1056
lines changed

14 files changed

+196
-1056
lines changed

buildhat/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@
44
from .color import ColorSensor
55
from .colordistance import ColorDistanceSensor
66
from .matrix import Matrix
7+
from .hat import Hat
78
from .serinterface import BuildHAT
89
from .exc import *

buildhat/color.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ def get_color_rgbi(self):
116116
"""Returns the color
117117
118118
:return: RGBI representation
119-
:rtype: tuple
119+
:rtype: list
120120
"""
121121
self.mode(5)
122122
reads = []

buildhat/colordistance.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ def get_color_rgb(self):
116116
"""Returns the color
117117
118118
:return: RGBI representation
119-
:rtype: tuple
119+
:rtype: list
120120
"""
121121
self.mode(6)
122122
reads = []

buildhat/hat.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from .serinterface import BuildHAT
2+
from .devices import Device
3+
import os
4+
import sys
5+
import weakref
6+
7+
class Hat:
8+
"""Allows enumeration of devices which are connected to the hat
9+
"""
10+
def __init__(self):
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)
20+
weakref.finalize(self, self._close)
21+
22+
def get(self):
23+
"""Gets devices which are connected or disconnected
24+
25+
:return: Dictionary of devices
26+
:rtype: dict
27+
"""
28+
devices = {}
29+
for i in range(4):
30+
name = "Other"
31+
if Device._instance.connections[i].typeid in Device._device_names:
32+
name = Device._device_names[Device._instance.connections[i].typeid]
33+
elif Device._instance.connections[i].typeid == -1:
34+
name = "Disconnected"
35+
devices[chr(ord('A')+i)] = {"typeid" : Device._instance.connections[i].typeid,
36+
"connected" : Device._instance.connections[i].connected,
37+
"name" : name}
38+
return devices
39+
40+
def _close(self):
41+
Device._instance.shutdown()

buildhat/motors.py

Lines changed: 62 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,50 @@ def run_for_rotations(self, rotations, speed=None, blocking=True):
6060
raise MotorException("Invalid Speed")
6161
self.run_for_degrees(int(rotations * 360), speed, blocking)
6262

63-
def _run_for_degrees(self, newpos, origpos, speed):
64-
dur = abs((newpos - origpos) / speed)
63+
def _run_for_degrees(self, degrees, speed):
64+
mul = 1
65+
if speed < 0:
66+
speed = abs(speed)
67+
mul = -1
68+
pos = self.get_position()
69+
newpos = ((degrees*mul)+pos)/360.0
70+
pos /= 360.0
71+
speed *= 0.05
72+
dur = abs((newpos - pos) / speed)
73+
cmd = "port {} ; combi 0 1 0 2 0 3 0 ; select 0 ; pid {} 0 1 s4 0.0027777778 0 5 0 .1 3 ; set ramp {} {} {} 0\r".format(self.port,
74+
self.port, pos, newpos, dur)
75+
self._write(cmd)
76+
with self._hat.rampcond[self.port]:
77+
self._hat.rampcond[self.port].wait()
78+
if self._release:
79+
time.sleep(0.2)
80+
self.coast()
81+
82+
def _run_to_position(self, degrees, speed, direction):
83+
data = self.get()
84+
pos = data[1]
85+
apos = data[2]
86+
diff = (degrees-apos+180) % 360 - 180
87+
newpos = (pos + diff)/360
88+
v1 = (degrees - apos)%360
89+
v2 = (apos - degrees)%360
90+
mul = 1
91+
if diff > 0:
92+
mul = -1
93+
diff = sorted([diff, mul * (v2 if abs(diff) == v1 else v1)])
94+
if direction == "shortest":
95+
pass
96+
elif direction == "clockwise":
97+
newpos = (pos + diff[1])/360
98+
elif direction == "anticlockwise":
99+
newpos = (pos + diff[0])/360
100+
else:
101+
raise DirectionInvalid("Invalid direction, should be: shortest, clockwise or anticlockwise")
102+
pos /= 360.0
103+
speed *= 0.05
104+
dur = abs((newpos - pos) / speed)
65105
cmd = "port {} ; combi 0 1 0 2 0 3 0 ; select 0 ; pid {} 0 1 s4 0.0027777778 0 5 0 .1 3 ; set ramp {} {} {} 0\r".format(self.port,
66-
self.port, origpos, newpos, dur)
106+
self.port, pos, newpos, dur)
67107
self._write(cmd)
68108
with self._hat.rampcond[self.port]:
69109
self._hat.rampcond[self.port].wait()
@@ -83,20 +123,12 @@ def run_for_degrees(self, degrees, speed=None, blocking=True):
83123
speed = self.default_speed
84124
if not (speed >= -100 and speed <= 100):
85125
raise MotorException("Invalid Speed")
86-
mul = 1
87-
if speed < 0:
88-
speed = abs(speed)
89-
mul = -1
90-
origpos = self.get_position()
91-
newpos = ((degrees*mul)+origpos)/360.0
92-
origpos /= 360.0
93-
speed *= 0.05
94126
if not blocking:
95-
th = threading.Thread(target=self._run_for_degrees, args=(newpos, origpos, speed))
127+
th = threading.Thread(target=self._run_for_degrees, args=(degrees, speed))
96128
th.daemon = True
97129
th.start()
98130
else:
99-
self._run_for_degrees(newpos, origpos, speed)
131+
self._run_for_degrees(degrees, speed)
100132

101133
def run_to_position(self, degrees, speed=None, blocking=True, direction="shortest"):
102134
"""Runs motor to position (in degrees)
@@ -110,35 +142,12 @@ def run_to_position(self, degrees, speed=None, blocking=True, direction="shortes
110142
raise MotorException("Invalid Speed")
111143
if degrees < -180 or degrees > 180:
112144
raise MotorException("Invalid angle")
113-
pos = self.get_position()
114-
apos = self.get_aposition()
115-
diff = (degrees-apos+180) % 360 - 180
116-
newpos = (pos + diff)/360
117-
118-
v1 = (degrees - apos)%360
119-
v2 = (apos - degrees)%360
120-
mul = 1
121-
if diff > 0:
122-
mul = -1
123-
diff = sorted([diff, mul * (v2 if abs(diff) == v1 else v1)])
124-
125-
if direction == "shortest":
126-
pass
127-
elif direction == "clockwise":
128-
newpos = (pos + diff[1])/360
129-
elif direction == "anticlockwise":
130-
newpos = (pos + diff[0])/360
131-
else:
132-
raise DirectionInvalid("Invalid direction, should be: shortest, clockwise or anticlockwise")
133-
134-
pos /= 360.0
135-
speed *= 0.05
136145
if not blocking:
137-
th = threading.Thread(target=self._run_for_degrees, args=(newpos, pos, speed))
146+
th = threading.Thread(target=self._run_to_position, args=(degrees, speed, direction))
138147
th.daemon = True
139148
th.start()
140149
else:
141-
self._run_for_degrees(newpos, pos, speed)
150+
self._run_to_position(degrees, speed, direction)
142151

143152
def _run_for_seconds(self, seconds, speed):
144153
cmd = "port {} ; combi 0 1 0 2 0 3 0 ; select 0 ; pid {} 0 0 s1 1 0 0.003 0.01 0 100; set pulse {} 0.0 {} 0\r".format(self.port, self.port, speed, seconds);
@@ -298,9 +307,9 @@ def run_for_degrees(self, degrees, speedl=None, speedr=None):
298307
speedl = self.default_speed
299308
if speedr is None:
300309
speedr = self.default_speed
301-
th1 = threading.Thread(target=self._leftmotor.run_for_degrees, args=(degrees,), kwargs={'speed': speedl, 'blocking': True})
310+
th1 = threading.Thread(target=self._leftmotor._run_for_degrees, args=(degrees, speedl))
311+
th2 = threading.Thread(target=self._rightmotor._run_for_degrees, args=(degrees, speedr))
302312
th1.daemon = True
303-
th2 = threading.Thread(target=self._rightmotor.run_for_degrees, args=(degrees,), kwargs={'speed': speedr, 'blocking': True})
304313
th2.daemon = True
305314
th1.start()
306315
th2.start()
@@ -318,9 +327,9 @@ def run_for_seconds(self, seconds, speedl=None, speedr=None):
318327
speedl = self.default_speed
319328
if speedr is None:
320329
speedr = self.default_speed
321-
th1 = threading.Thread(target=self._leftmotor._run_for_seconds, args=(seconds,), kwargs={'speed': speedl})
330+
th1 = threading.Thread(target=self._leftmotor._run_for_seconds, args=(seconds, speedl))
331+
th2 = threading.Thread(target=self._rightmotor._run_for_seconds, args=(seconds, speedr))
322332
th1.daemon = True
323-
th2 = threading.Thread(target=self._rightmotor._run_for_seconds, args=(seconds,), kwargs={'speed': speedr})
324333
th2.daemon = True
325334
th1.start()
326335
th2.start()
@@ -344,16 +353,22 @@ def stop(self):
344353
self._leftmotor.stop()
345354
self._rightmotor.stop()
346355

347-
def run_to_position(self, degreesl, degreesr, speed=None):
356+
def run_to_position(self, degreesl, degreesr, speed=None, direction="shortest"):
348357
"""Runs pair to position (in degrees)
349358
350359
:param degreesl: Position in degrees for left motor
351360
:param degreesr: Position in degrees for right motor
352361
:param speed: Speed ranging from -100 to 100
353362
"""
354363
if speed is None:
355-
self._leftmotor.run_to_position(degreesl, self.default_speed)
356-
self._rightmotor.run_to_position(degreesr, self.default_speed)
364+
th1 = threading.Thread(target=self._leftmotor._run_to_position, args=(degreesl, self.default_speed, direction))
365+
th2 = threading.Thread(target=self._rightmotor._run_to_position, args=(degreesr, self.default_speed, direction))
357366
else:
358-
self._leftmotor.run_to_position(degreesl, speed)
359-
self._rightmotor.run_to_position(degreesr, speed)
367+
th1 = threading.Thread(target=self._leftmotor._run_to_position, args=(degreesl, speed, direction))
368+
th2 = threading.Thread(target=self._rightmotor._run_to_position, args=(degreesr, speed, direction))
369+
th1.daemon = True
370+
th2.daemon = True
371+
th1.start()
372+
th2.start()
373+
th1.join()
374+
th2.join()

docs/buildhat/colordistancesensor.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
ColorDistanceSensor
22
===================
33

4+
|location_link|
5+
6+
.. |location_link| raw:: html
7+
8+
<a href="https://www.lego.com/en-gb/product/color-distance-sensor-88007" target="_blank">LEGO Color and Distance Sensor 45605</a>
9+
10+
11+
The LEGO® Color and Distance Sensor can sort between six different colors and objects within 5 to 10 cm range
12+
13+
NOTE: Support for this device is experimental and not all features are available yet.
14+
415
.. autoclass:: buildhat.ColorDistanceSensor
516
:members:
617
:inherited-members:

docs/buildhat/hat.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from buildhat import Hat
2+
3+
hat = Hat()
4+
print(hat.get())

docs/buildhat/hat.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Hat
2+
===
3+
4+
Gets list of devices connected to the hat
5+
6+
.. autoclass:: buildhat.Hat
7+
:members:
8+
:inherited-members:
9+
10+
Example
11+
-------
12+
13+
.. literalinclude:: hat.py

docs/buildhat/index.rst

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
Library
44
=======
55

6-
The Build HAT library has been created to support the Raspberry Pi Build HAT,
7-
an add-on board for the Raspberry Pi computer, which allows control of up to four LEGO® Technic™ motors and sensors included in the SPIKE™ Portfolio.
6+
The Build HAT library has been created to support the `Raspberry Pi Build HAT`_,
7+
an add-on board for the Raspberry Pi computer, which allows control of up to four LEGO® TECHNIC™ motors and sensors included in the SPIKE™ Portfolio.
8+
9+
.. _Raspberry Pi Build HAT: http://raspberrypi.com/products/build-hat
810

911
.. image:: images/BuildHAT_closeup.jpg
1012
:width: 300
@@ -18,7 +20,9 @@ Other LEGO® devices may be supported if they use the LPF2 connector:
1820
:alt: The LEGO LPF2 connector
1921

2022
In order to drive motors, your Raspberry Pi and Build HAT will need an external 7.5V
21-
power supply. For best results, use the official Raspberry Pi Build HAT power supply.
23+
power supply. For best results, use the `official Raspberry Pi Build HAT power supply`_.
24+
25+
.. _official Raspberry Pi Build HAT power supply: http://raspberrypi.com/products/build-hat-power-supply
2226

2327
.. warning::
2428

@@ -35,3 +39,4 @@ power supply. For best results, use the official Raspberry Pi Build HAT power su
3539
matrix.rst
3640
motor.rst
3741
motorpair.rst
42+
hat.rst

docs/buildhat/motor.rst

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
Motor
22
=====
33

4-
LEGO® Technic™ motors from the LEGO® Education SPIKE™ portfolio have an integrated rotation sensor (encoder) and can be positioned with 1-degree accuracy. The encoders can be queried to find the current
5-
position of the motor with respect to a 'zero' mark shown on the motor itself. Other motors without encoders will report a 0 value if queried.
4+
LEGO® Technic™ motors from the LEGO® Education SPIKE™ portfolio have an integrated rotation sensor (encoder) and can be positioned with 1-degree accuracy. The encoders can be queried to find the current position of the motor with respect to a 'zero' mark shown on the motor itself.
65

76
|location_link1|
87

@@ -16,7 +15,25 @@ position of the motor with respect to a 'zero' mark shown on the motor itself. O
1615

1716
<a href="https://education.lego.com/en-gb/products/lego-technic-medium-angular-motor/45603" target="_blank">LEGO® Medium angular motor 45603</a>
1817

18+
Other motors without encoders will report a 0 value if queried.
1919

20+
|location_link3|
21+
22+
.. |location_link3| raw:: html
23+
24+
<a href="https://www.lego.com/en-gb/product/medium-linear-motor-88008" target="_blank">LEGO® Medium Linear motor 88008</a>
25+
26+
|location_link4|
27+
28+
.. |location_link4| raw:: html
29+
30+
<a href="https://www.lego.com/en-gb/product/technic-large-motor-88013" target="_blank">Technic™ Large Motor 88013</a>
31+
32+
|location_link5|
33+
34+
.. |location_link5| raw:: html
35+
36+
<a href="https://www.lego.com/en-gb/product/technic-xl-motor-88014" target="_blank">Technic™ XL Motor 88014</a>
2037

2138
.. autoclass:: buildhat.Motor
2239
:members:

0 commit comments

Comments
 (0)