I am coding a little robot controlled by a raspberry pi zero. Disclaimer: This is a more general development question, and only indirectly related to raspberry pi.
Background: I don't want to test everything on the robot directly, this is too time consuming. Therefore I am trying to implement mock interfaces. For example, I'd am using a set of fake sensor readings and feed them to the class responsible for sensing and driving. Running this code in command line should for example print out "driving left" instead of starting the right motor and so on.
Question: How to mock a device so that I can use the main code without modification on my robot.
Code examples I have currently the following file structure:
- main.py: running the loop so the robot is doing something and currently holding mock sensor data.
- acc.py: AutonomousCruiseControl class: taking care of the process of measuring and steering
- hcsr04.py: a class mocking the sonar distance reader device
- servo.py: currently doing nothing: Should contain a mock device writing to console what motor is turned into what direction
In the following code examples, you can see e.g. in the drive() method, that I have code purely for testing mixed with the code destined for production.
main.py
#!/usr/bin/env python3
# coding=UTF-8
from navigation import AutonomousCruiseControl
def main():
#self.objectColisionRange = [10,15,20,15,10]
readings = [
[11, 16, 21, 16, 11], # path free
[100, 100, 19, 100, 100], # front blocked
[9, 100,20, 100, 100], # center & left blocked
[100, 100, 10, 100, 10], # center & right blocked
[10, 100, 10, 100, 10], # center, left & right blocked
[100, 10, 100, 100, 100], # center free, left front object to avoid
[100, 100, 100, 10, 100], # center free, right front object to avoid
[100, 10, 100, 10, 100] # center free, but left and right (45°) objects too narrow
]
ACC = AutonomousCruiseControl()
# range second parameter is numbers to generate not the right boundary
for i in range(0,8):
ACC.front_sonar.readings = readings[i]
print("{}. reading: {}".format(i, ACC.read_front_sonar()))
print("{}. FOS: {}".format(i, ACC.get_front_object_status()))
ACC.drive()
if __name__ == '__main__':
main()
acc.py
#!/usr/bin/env python3
# coding=UTF-8
from navigation.hcsr04 import Hcsr04
from navigation.servo import Servo
from random import randint
class AutonomousCruiseControl:
"""helping to steer a robot trough obstacles"""
def __init__(self, object_colision_range=None, front_sonar_angles=None):
# initiate sensors
self.front_sonar = Hcsr04()
self.front_sonar_servo = Servo()
self.collision_distance = 20
if object_colision_range is None:
self.object_colision_range = [10, 15, 20, 15, 10]
else:
self.object_colision_range = object_colision_range
if front_sonar_angles is None:
self.front_sonar_angles = [0, 45, 90, 135, 180]
else:
self.front_sonar_angles = front_sonar_angles
self.front_sonar_distances = [200, 200, 200, 200, 200]
self.read_front_sonar()
print(self.front_sonar_distances)
self.front_object_status = [0, 0, 0, 0, 0]
def read_front_sonar(self):
""" Setting the angle and calling the read_distance method. """
# TODO implement reading from right to left and left to right, so both servo swings can be used
# go through all angles and read distance put it into front_sonar_distances
# initiate only once for test cases, to not mix values
for i in range(0, len(self.front_sonar_angles)):
# set servo to angle
angle = self.front_sonar_angles[i]
# read distance
distance = self.read_distance(self.front_sonar, angle)
# print("distance is {}\n".format(distance))
self.front_sonar_distances[i] = distance
return self.front_sonar_distances
def get_front_object_status(self):
""" Creating a simplified form of the distance measures. Currently if a reading is equal or below the
reading angle specific collision range, the status is set to 1. Zero means, no obstacle. """
for i in range(0, len(self.front_sonar_distances)):
if self.front_sonar_distances[i] <= self.object_colision_range[i]:
self.front_object_status[i] = 1
else:
self.front_object_status[i] = 0
return self.front_object_status
def read_distance(self, sensor, angle):
""" Remove angle later from this method and from hcsr04. """
if sensor is self.front_sonar:
# TODO: implement servo, set angle
# print("Set servo to angle {} °".format(angle))
# angle/index won't be necessary after the real servo routine is working
# this just helps the dry run tests
index = self.front_sonar_angles.index(angle)
return self.front_sonar.get_distance(index)
def drive(self):
""" Contolling the motors with obstacle avoidance switch.
Light search is not yet implemented. The robot should aimlessly
cruise around without hitting detectable obstacles. """
stat = self.front_object_status
dist = self.front_sonar_distances
if stat[1] + stat[2] + stat[3] is 0:
# road is free
print("Road free: L(eft) & R(ight) forward")
elif stat[1] is 1:
# left blocked
print("Obstacle left: L(eft) faster & R(ight) slower forward")
elif stat[3] is 1:
# right blocked
print("Obstacle right: L(eft) slower & R(ight) faster forward")
elif stat[2] is 1 or (stat[1] + stat[3] is 2):
# front blocked
print("obstacle front: stop all")
dist_sum_left = dist[0] + dist[1]
dist_sum_right = dist[3] + dist[4]
if dist_sum_left == dist_sum_right:
# both sides are free and distance the same
# choose randomly left and right 0 is left and 1 is right
rand = randint(0, 1)
if rand is 0: # left
print("Random turn left, L backw, R forw")
else: # right
print("Random turn right, R backw, L forw")
elif dist_sum_left > dist_sum_right:
# left has more leeway
print("Left leeway, turn left")
elif dist_sum_left < dist_sum_right:
# right has more leeway
print("Right leeway, turn right")
hcsr04.py
#!/usr/bin/env python3
# coding=UTF-8
# Test method
class Hcsr04:
'this just fakes the reading from sonar it needs the servo angle where the reading should come from'
def __init__(self):
self.readings = [88, 88, 88, 88, 88]
def get_distance(self, index):
return self.readings[index]