0
$\begingroup$

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]
$\endgroup$
0
$\begingroup$

The general name for what you're looking for is dependency injection

The pattern is to ensure that the sensors ("the dependencies") are provided to the main control system at configuration time, instead of being hardcoded deep-down. You can then have two toplevel paths. One creates the objects that connect to the actual devices and passes them when creating the main control system. The other one creates the mock objects.

Generally speaking, if you start having these kind of issues, consider using the frameworks that already exist (ROS, Rock, ...).

$\endgroup$
  • $\begingroup$ Cool, I will read into that try to apply it. $\endgroup$ – Stowoda Mar 7 '17 at 19:14

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.