Skip to content

Commit 90111d2

Browse files
authored
Feature/easy libraries (#35)
* Move Distance Sensor and Line Follower in here * remove dependency on mock_package * remove alias for read_sensors until we know if we need it (probably not)
1 parent 539d39e commit 90111d2

File tree

4 files changed

+402
-4
lines changed

4 files changed

+402
-4
lines changed
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
#######################################################################
2+
#
3+
# DistanceSensor
4+
#
5+
# under try/except in case the Distance Sensor is not installed
6+
#######################################################################
7+
try:
8+
from di_sensors import distance_sensor
9+
10+
except:
11+
print("DI Sensors library not found")
12+
# try:
13+
# from mock_package import distance_sensor
14+
# print ("Loading library without distance sensor")
15+
# except:
16+
# pass
17+
18+
# import easy_sensors
19+
from I2C_mutex import Mutex
20+
import time
21+
22+
mutex = Mutex(debug=False)
23+
24+
def _ifMutexAcquire(mutex_enabled=False):
25+
"""
26+
Acquires the I2C if the ``use_mutex`` parameter of the constructor was set to ``True``.
27+
"""
28+
if mutex_enabled:
29+
mutex.acquire()
30+
31+
def _ifMutexRelease(mutex_enabled=False):
32+
"""
33+
Releases the I2C if the ``use_mutex`` parameter of the constructor was set to ``True``.
34+
"""
35+
if mutex_enabled:
36+
mutex.release()
37+
38+
39+
class EasyDistanceSensor(distance_sensor.DistanceSensor):
40+
"""
41+
Class for the `Distance Sensor`_ device.
42+
43+
We can create this :py:class:`~easygopigo3.DistanceSensor` object similar to how we create it in the following template.
44+
45+
.. code-block:: python
46+
47+
# create an EasyGoPiGo3 object
48+
gpg3_obj = EasyGoPiGo3()
49+
50+
# and now let's instantiate a DistanceSensor object through the gpg3_obj object
51+
distance_sensor = gpg3_obj.init_distance_sensor()
52+
53+
# read values continuously and print them in the terminal
54+
while True:
55+
distance = distance_sensor.read()
56+
57+
print(distance)
58+
59+
"""
60+
def __init__(self, port="I2C",gpg=None, use_mutex=False):
61+
"""
62+
Creates a :py:class:`~easygopigo3.DistanceSensor` object which can be used for interfacing with a `distance sensor`_.
63+
64+
:param str port = "I2C": Port to which the distance sensor is connected.
65+
:param easygopigo3.EasyGoPiGo3 gpg = None: Object that's required for instantianting a :py:class:`~easygopigo3.DistanceSensor` object.
66+
:param bool use_mutex = False: When using multiple threads/processes that access the same resource/device, mutexes should be enabled.
67+
:raises IOError: If :py:class:`di_sensors.distance_sensor.DistanceSensor` can't be found. Probably the :py:mod:`di_sensors` module isn't installed.
68+
:raises TypeError: If the ``gpg`` parameter is not a :py:class:`~easygopigo3.EasyGoPiGo3` object.
69+
70+
To see where the ports are located on the `GoPiGo3`_ robot, please take a look at the following diagram: :ref:`hardware-ports-section`.
71+
72+
"""
73+
self.descriptor = "Distance Sensor"
74+
self.use_mutex = use_mutex
75+
76+
_ifMutexAcquire(self.use_mutex)
77+
try:
78+
distance_sensor.DistanceSensor.__init__(self)
79+
except Exception as e:
80+
print("Distance Sensor init: {}".format(e))
81+
raise
82+
finally:
83+
_ifMutexRelease(self.use_mutex)
84+
85+
# Returns the values in cms
86+
def read_mm(self):
87+
"""
88+
Reads the distance in millimeters.
89+
90+
:returns: Distance from target in millimeters.
91+
:rtype: int
92+
93+
.. note::
94+
95+
1. Sensor's range is **5-8,000** millimeters.
96+
2. When the values are out of the range, it returns **8190**.
97+
98+
"""
99+
100+
# 8190 is what the sensor sends when it's out of range
101+
# we're just setting a default value
102+
mm = 8190
103+
readings = []
104+
attempt = 0
105+
106+
# try 3 times to have a reading that is
107+
# smaller than 8m or bigger than 5 mm.
108+
# if sensor insists on that value, then pass it on
109+
while (mm > 8000 or mm < 5) and attempt < 3:
110+
_ifMutexAcquire(self.use_mutex)
111+
try:
112+
mm = self.read_range_single()
113+
except Exception as e:
114+
print(e)
115+
mm = 0
116+
finally:
117+
_ifMutexRelease(self.use_mutex)
118+
attempt = attempt + 1
119+
time.sleep(0.001)
120+
121+
# add the reading to our last 3 readings
122+
# a 0 value is possible when sensor is not found
123+
if (mm < 8000 and mm > 5) or mm == 0:
124+
readings.append(mm)
125+
if len(readings) > 3:
126+
readings.pop(0)
127+
128+
# calculate an average and limit it to 5 > X > 3000
129+
if len(readings) > 1: # avoid division by 0
130+
mm = round(sum(readings) / float(len(readings)))
131+
if mm > 3000:
132+
mm = 3000
133+
134+
return mm
135+
136+
def read(self):
137+
"""
138+
Reads the distance in centimeters.
139+
140+
:returns: Distance from target in centimeters.
141+
:rtype: int
142+
143+
.. note::
144+
145+
1. Sensor's range is **0-800** centimeters.
146+
2. When the values are out of the range, it returns **819**.
147+
148+
"""
149+
150+
cm = self.read_mm()//10
151+
return (cm)
152+
153+
def read_inches(self):
154+
"""
155+
Reads the distance in inches.
156+
157+
:returns: Distance from target in inches.
158+
:rtype: float with one decimal
159+
160+
.. note::
161+
162+
1. Sensor's range is **0-314** inches.
163+
2. Anything that's bigger than **314** inches is returned when the sensor can't detect any target/surface.
164+
165+
"""
166+
cm = self.read()
167+
return round(cm / 2.54, 1)
168+
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import easy_sensors
2+
from I2C_mutex import Mutex
3+
import time
4+
from os import path
5+
# from numpy import mean, std
6+
7+
try:
8+
from di_sensors import line_follower
9+
except:
10+
# think about what we should do
11+
# goal is to support both line followers seamlessly
12+
raise ImportError("Line Follower library not found")
13+
exit()
14+
15+
class EasyLineFollower(Sensor):
16+
"""
17+
Class for interacting with the `Line Follower`_ sensor.
18+
With this sensor, you can make your robot follow a black line on a white background.
19+
20+
The `Line Follower`_ sensor has 5 IR sensors.
21+
Each IR sensor is capable of diferentiating a black surface from a white one.
22+
23+
In order to create an object of this class, we would do it like in the following example.
24+
25+
.. code-block:: python
26+
27+
# initialize an EasyGoPiGo3 object
28+
gpg3_obj = EasyGoPiGo3()
29+
30+
# and then initialize the LineFollower object
31+
line_follower = gpg3_obj.init_line_follower()
32+
33+
# use it however you want it
34+
line_follower.read_raw_sensors()
35+
36+
.. warning::
37+
38+
This class requires the :py:mod:`line_sensor` library.
39+
40+
"""
41+
42+
DIR_PATH="/home/pi/Dexter/"
43+
# If for some reason this got installed in a weird way,
44+
# then default to pi home dir
45+
if not path.isdir(DIR_PATH):
46+
DIR_PATH = "/home/pi/"
47+
48+
FILE_BLACK=dir_path+'black_line.txt'
49+
FILE_WHITE=dir_path+'white_line.txt'
50+
FILE_RANGE=dir_path+'range_line.txt'
51+
52+
def __init__(self, port="I2C", gpg=None, use_mutex=False):
53+
"""
54+
Constructor for initalizing a :py:class:`~easygopigo3.LineFollower` object.
55+
56+
:param str port = "I2C": The port to which we have connected the `Line Follower`_ sensor.
57+
:param easygopigo3.EasyGoPiGo3 gpg = None: The :py:class:`~easygopigo3.EasyGoPiGo3` object that we need for instantiating this object.
58+
:param bool use_mutex = False: When using multiple threads/processes that access the same resource/device, mutexes should be enabled.
59+
:raises ImportError: If the :py:mod:`line_follower` module couldn't be found.
60+
:raises TypeError: If the ``gpg`` parameter is not a :py:class:`~easygopigo3.EasyGoPiGo3` object.
61+
:raises IOError: If the line follower is not responding.
62+
63+
64+
The only value the ``port`` parameter can take is ``"I2C"``.
65+
66+
The I2C ports' location on the `GoPiGo3`_ robot can be seen in the following graphical representation: :ref:`hardware-ports-section`.
67+
68+
"""
69+
try:
70+
self.set_descriptor("Line Follower")
71+
easy_sensors.Sensor.__init__(self, port, "INPUT", gpg, use_mutex)
72+
if line_sensor.read_sensor() == [-1, -1, -1, -1, -1]:
73+
raise IOError("Line Follower not responding")
74+
except:
75+
raise
76+
77+
# Function for removing outlier values
78+
# For bigger std_factor_threshold, the filtering is less aggressive
79+
# For smaller std_factor_threshold, the filtering is more aggressive
80+
# std_factor_threshold must be bigger than 0
81+
# def _statisticalNoiseReduction(values, std_factor_threshold = 2):
82+
# if len(values) == 0:
83+
# return []
84+
85+
# calculated_mean = mean(values)
86+
# standard_deviation = std(values)
87+
88+
# # just return if we only got constant values
89+
# if standard_deviation == 0:
90+
# return values
91+
92+
# # remove outlier values which are less than the average but bigger than the calculated threshold
93+
# filtered_values = [element for element in values if element > mean - std_factor_threshold * standard_deviation]
94+
# # the same but in the opposite direction
95+
# filtered_values = [element for element in filtered_values if element < mean + std_factor_threshold * standard_deviation]
96+
97+
# return filtered_values
98+
99+
def read_raw_sensors(self):
100+
"""
101+
Read the 6 IR sensors of the `Line Follower`_ sensor.
102+
Note: the old line follower (with red board) has 5 sensors, not 6.
103+
104+
:returns: A list with 5 10-bit numbers that represent the readings from the line follower device.
105+
:rtype: list[int]
106+
:raises IOError: If the line follower is not responding.
107+
108+
"""
109+
110+
try:
111+
sensor_vals = line_sensor.read_sensor()
112+
except IOError as e:
113+
print(e)
114+
sensor_vals = [-1]*6
115+
116+
if five_vals != [-1, -1, -1, -1, -1]:
117+
return five_vals
118+
else:
119+
raise IOError("Line Follower not responding")
120+
121+
def get_white_calibration(self):
122+
"""
123+
Place the `GoPiGo3`_ robot on top of a white-colored surface.
124+
After that, call this method for calibrating the robot on a white surface.
125+
126+
:returns: A list with 5 10-bit numbers that represent the readings of line follower sensor.
127+
:rtype: int
128+
129+
Also, for fully calibrating the sensor, the :py:class:`~easygopigo3.LineFollower.get_black_calibration` method also needs to be called.
130+
131+
"""
132+
return line_sensor.get_white_line()
133+
134+
def get_black_calibration(self):
135+
"""
136+
Place the `GoPiGo3`_ robot on top of a black-colored surface.
137+
After that, call this method for calibrating the robot on a black surface.
138+
139+
:returns: A list with 5 10-bit numbers that represent the readings of line follower sensor.
140+
:rtype: int
141+
142+
Also, for fully calibrating the sensor, the :py:class:`~easygopigo3.LineFollower.get_white_calibration` method also needs to be called.
143+
144+
"""
145+
return line_sensor.get_black_line()
146+
147+
def read(self):
148+
"""
149+
Reads the 5 IR sensors of the `Line Follower`_ sensor.
150+
151+
:returns: A list with 5 numbers that represent the readings of the line follower device. The values are either **0** (for black) or **1** (for white).
152+
:rtype: list[int]
153+
154+
.. warning::
155+
156+
If an error occurs, a list of **5 numbers** with values set to **-1** will be returned.
157+
This may be caused by bad calibration values.
158+
159+
Please use :py:meth:`~easygopigo3.LineFollower.get_black_calibration` or :py:meth:`~easygopigo3.LineFollower.get_white_calibration` methods before calling this method.
160+
161+
"""
162+
five_vals = scratch_line.absolute_line_pos()
163+
164+
return five_vals
165+
166+
def read_position(self):
167+
"""
168+
Returns a string telling to which side the black line that we're following is located.
169+
170+
:returns: String that's indicating the location of the black line.
171+
:rtype: str
172+
173+
The strings this method can return are the following:
174+
175+
* ``"center"`` - when the line is found in the middle.
176+
* ``"black"`` - when the line follower sensor only detects black surfaces.
177+
* ``"white"`` - when the line follower sensor only detects white surfaces.
178+
* ``"left"`` - when the black line is located on the left of the sensor.
179+
* ``"right"`` - when the black line is located on the right of the sensor.
180+
181+
.. note::
182+
183+
This isn't the most "intelligent" algorithm for following a black line, but it proves the point and it works.
184+
185+
"""
186+
five_vals = [-1, -1, -1, -1, -1]
187+
188+
189+
five_vals = self.read()
190+
191+
if five_vals == [0, 0, 1, 0, 0] or five_vals == [0, 1, 1, 1, 0]:
192+
return "center"
193+
if five_vals == [1, 1, 1, 1, 1]:
194+
return "black"
195+
if five_vals == [0, 0, 0, 0, 0]:
196+
return "white"
197+
if five_vals == [0, 1, 1, 0, 0] or \
198+
five_vals == [0, 1, 0, 0, 0] or \
199+
five_vals == [1, 0, 0, 0, 0] or \
200+
five_vals == [1, 1, 0, 0, 0] or \
201+
five_vals == [1, 1, 1, 0, 0] or \
202+
five_vals == [1, 1, 1, 1, 0]:
203+
return "left"
204+
if five_vals == [0, 0, 0, 1, 0] or \
205+
five_vals == [0, 0, 1, 1, 0] or \
206+
five_vals == [0, 0, 0, 0, 1] or \
207+
five_vals == [0, 0, 0, 1, 1] or \
208+
five_vals == [0, 0, 1, 1, 1] or \
209+
five_vals == [0, 1, 1, 1, 1]:
210+
return "right"
211+
return "unknown"
212+
213+
def read_position_str(self):
214+
"""
215+
returns a string of five letters indicating what the line sensor is seeing.
216+
'b' indicates that specific sensor has detected a black line.
217+
'w' indicates that specific sensor has not detected a black line.
218+
219+
:returns: String indicating what the line follower just read.
220+
:rtype: str
221+
222+
Here's an example of what could get returned:
223+
* ``'bbbbb'`` - when the line follower reads black on all sensors.
224+
* ``'wwbww'`` - when the line follower is perfectly centered.
225+
* ``'bbbww'`` - when the line follower reaches an intersection.
226+
"""
227+
five_vals = self.read()
228+
out_str = "".join(["b" if sensor_val == 1 else "w" for sensor_val in five_vals])
229+
return out_str
230+
##########################

0 commit comments

Comments
 (0)