This series of articles focuses on creating a scalable object oriented modular software architecture based on services you can reuse in all your robots, without needing to start from scratch every time you create a new robot.
Maybe the most cost effective approach to start in robotics is with the Smart Robot Car you can buy on any e-commerce (aliexpress, banggood, etc). But of course buying it is the easiest part... You don't need to buy specifically that, and even with the description Smart Robot Car You'll find many different variants. I'll be creating "services" for all the most common modules you get with this kit, so you can choose which service you need, and use them together to create your own Smart robot, without the need to start from scratch on every robot you make.
IndexThis is the index of the series or articles Taibot Robot Services that I will be creating.
About the HC-SR04 Distance SensorThis cheap sensor provides 2 cm to 400 cm of ultrasonic distance measurement functionality with a ranging accuracy that can reach up to 3 mm. Each HC-SR04 module includes an ultrasonic transmitter, a receiver and a control circuit.
There are only four pins that you need to worry about on the HC-SR04:
- VCC (Power),
- Trig (Trigger),
- Echo (Receive), and
- GND (Ground).
As an extra detail, if you connect Trig and Echo pins toghether, you can control dis sensor using only one pin on the Arduino. (Really useful if you have many sensors). You can find more info on this here.
Features:
- Operating Voltage: 5V DC
- Operating Current: 15mA
- Measure Angle: 15°
- Ranging Distance: 2cm - 4m
We will be using the base Service class created in the previous article. If you didn't read it, please do so, so you can understand what we will be creating here.
The DistanceSensorService base classWe are going to create a layer of abstraction, that will define the behavior that any Distance Sensor Service should have. Doing this, allows us to create different distance sensor services for any hardware we buy, and use any distance sensor service we have, in the same way (so with really few changes in the code).
The DistanceSensorService class should allow us to:
- Get the Distance measured by the device
- Get the Averaged Distance measured by the device
- And of course, the functionalities inherited from the base Service class
The DistanceSensorService class header file (DistanceSensorService.h) looks like this:
#pragma once
#include "Service.h"
namespace Taibot
{
class DistanceSensorService : public Service
{
public:
DistanceSensorService(bool isEnabled, bool isVerbose, unsigned int maxDistance);
unsigned int GetMaxDistance();
virtual unsigned int GetDistance() = 0;
virtual unsigned int GetAverageDistance() = 0;
private:
unsigned int _maxDistance;
};
};
And its implementation (DistanceSensorService.h) :
#include "DistanceSensorService.h"
using namespace Taibot;
DistanceSensorService::DistanceSensorService(bool isEnabled, bool isVerbose, unsigned int maxDistance) : Service(isEnabled, isVerbose)
{
_maxDistance = maxDistance;
}
unsigned int DistanceSensorService::GetMaxDistance()
{
return _maxDistance;
}
As you can see, DistanceSensorService class doesn't do anything other that defining the contract for the DistanceSensorServices implementations (Now we will work on its implementation for the HC-SR04 Distance Sensor module).
The HCSR04DistanceSensorService classThis class is the implementation of a DistanceSensorService, that controls a HC-SR04 Distance Sensor module. It follows the layout we specified in the previous article.
To communicate with the sensor we will be using the NewPing library, if you don't have installed already, please download it and install it in your Arduino IDE libraries folder. (you'll find more details on how to do this and the download link here).
First the header file (HCSR04DistanceSensorService.h):
#pragma once
#include <Arduino.h>
#include <NewPing.h>
#include "DistanceSensorService.h"
#include "MovingAverage.h"
#define MAX_READING_FREQUENCY 30 //The maximum frequency the HC-SR04 can be queried at
namespace Taibot
{
class HCSR04DistanceSensorService : public DistanceSensorService
{
public:
HCSR04DistanceSensorService(bool isEnabled, bool isVerbose, unsigned int maxDistance, unsigned int pinTrigger, unsigned int pinEcho);
unsigned int GetDistance();
unsigned int GetAverageDistance();
void Setup();
void Update();
private:
// The NewPing sensor instance
NewPing sensor;
// The last time we made a reading
unsigned long _lastReadingTime = 0;
// The last reading we made
unsigned int _latestDistance;
// The HC-SR04 returns fake readings from time to time.
// We will be averaging the last 3 reading of the sensor, to get a more reallistic reading
MovingAverage<unsigned int, 3> average;
};
};
As you can see, we added the #include <NewPing.h> to allow us to use this library. We also defined the MAX_READING_FREQUENCY that we will use to query the sensor. This value was taken from the NewPing library documentation.
And its implementation (HCSR04DistanceSensorService.cpp):
#include "HCSR04DistanceSensorService.h"
using namespace Taibot;
HCSR04DistanceSensorService::HCSR04DistanceSensorService(bool isEnabled, bool isVerbose, unsigned int maxDistance, unsigned int pinTrigger, unsigned int pinEcho)
: DistanceSensorService(isEnabled, isVerbose, maxDistance),
sensor(pinTrigger, pinEcho, maxDistance), // create the NewPing sensor instance
average(maxDistance) // Start the average readings at the maxDistance value
{
// Set the latest distance to the maximum, so the sensor does not start with a fake super close reading
_latestDistance = GetMaxDistance();
}
unsigned int HCSR04DistanceSensorService::GetDistance()
{
if (IsEnabled())
{
// We can only ping the sensor every 30ms, so we will do that, and the rest of the time,
if (_lastReadingTime + MAX_READING_FREQUENCY <= millis())
{
// As enough time have passed, we can update the reading
_latestDistance = sensor.ping_cm();
// The NewPing library returns 0 when it does not detect an object.
// We want this service to return the MaxDistance, so we dont get confuse this with a super close object
if (_latestDistance == 0)
{
_latestDistance = GetMaxDistance();
}
average.Add(_latestDistance);
if (IsVerbose())
{
// We will only print the distance when we got a real update
Serial.print(F("HCSR04: d="));
Serial.print(_latestDistance);
Serial.println(F(" cm"));
}
}
return _latestDistance;
}
}
unsigned int HCSR04DistanceSensorService::GetAverageDistance()
{
// Make the ping only if we shold
GetDistance();
// Now get the latest average
unsigned int latestAverage = average.Get();
if (IsVerbose())
{
// We will only print the distance when we got a real update
Serial.print(F("HCSR04: a="));
Serial.print(latestAverage);
Serial.println(F(" cm"));
}
// return the average of the latest pings
return latestAverage;
}
void HCSR04DistanceSensorService::Setup()
{
// Nothing to do here for this service
}
void HCSR04DistanceSensorService::Update()
{
// Nothing to do here for this service
}
In the constructor, we are creating the instance of the NewPing sensor, sending to it the pins that we want to use. (this can be two different arduino pins, or the same pin, if both trigger and echo are connected to each other (more details on this here).
We are also initializing an instance of the Average class. I didn't talk about that yet. I will later in this article. But that allows anyone creating an instance of it to manage a moving average, as we need here.
GetDistance method:
As usual, you'll notice that we are using the base IsEnabled() and IsVerbose() methods to determine if we should really do something, or log somethig.
As we can only ping the sensor every 30 ms, we are checking that enough time have passed since the last query.
if (_lastReadingTime + MAX_READING_FREQUENCY <= millis())
If it did, we query the sensor...
_latestDistance = sensor.ping_cm();
and update the average.
average.Add(_latestDistance);
We want also to return the maximum distance when no object was found, instead of 0, as the sensor returns. we do this to avoid thinking something is too close, when there's really nothing ahead..
if (_latestDistance == 0)
{
_latestDistance = GetMaxDistance();
}
GetAverageDistance method:
This method calls the GetDistance method, so we update the reading in case enough time have passed...
GetDistance();
And after doing that, we return the latest calculated average:
latestAverage = average.Get();
The Average template classThis class allow us to keep track of the average of a succession of values. I'll show you the code first and after that, what it does:
The MovingAverage.h file looks like this:
template <typename V, int N> class MovingAverage
{
public:
/*
* @brief Class constructor.
* @param n the size of the moving average window.
* @param def the default value to initialize the average.
*/
MovingAverage(V def = 0) : _sum(0), p(0)
{
for (int i = 0; i < N; i++)
{
_samples[i] = def;
_sum += _samples[i];
}
}
/*
* @brief Add a new sample.
* @param new_sample the new sample to add to the moving average.
* @return the updated average.
*/
V Add(V newSample)
{
_sum = _sum - _samples[p] + newSample;
_samples[p++] = newSample;
if (p >= N)
{
p = 0;
}
_lastAverage = _sum / N;
return _lastAverage;
}
V Get() const
{
return _lastAverage;
}
private:
V _samples[N];
V _sum;
unsigned int p;
V _lastAverage;
};
The implementation was added to the .h file, since its really small.
As it is a template, you can provide as V any numeric type you want to average (int, long, float, byte, etc). The parameter N lets you configure how many values you want to consider in the average.
template <typename V, int N> class MovingAverage
The constructor creates the array that holds N values of the V datatype, and initialize this values with the default (def) value that you provided if any.
Add(newSample) method:
This method adds a new sample to the circular buffer, and removes the oldest one. In the process it also calculates the new average, for the updated values and returns this value.
Get() method:
This method is a lot more simple, it just returns the latest calculated average. As you can see it's defined as const, since it doesn't change the internal state of the Average object.
Average class usage example:
This is how you would use the Average class:
MovingAverage<unsigned int, 3> average;
In this case, the average object will be averaging the latest 3 elements of the type unsigned int we add() to it.
Testing the ServiceNow we will make a sketch to test the service behavior. This is how the Arduino Sketch should look:
/*
Name: Taibot.ino
Created: 12/13/2016 10:27:53 AM
Author: Nahuel Taibo savagemakers.com
*/
#include "HCSR04DistanceSensorService.h"
using namespace Taibot;
// We will use this variables to change the robot speed on after some seconds (without using delays)
unsigned long previousTime = millis();
unsigned int updateFreq = 500;
#define MAX_RANGEFINDER_DISTANCE 200
#define RANGEFINDER_1_TRIG 34 // If both pins (Triger and Echo) ar connected beween them,
#define RANGEFINDER_1_ECHO 34 // you can use only one Arduino pin to control the sensor
HCSR04DistanceSensorService distanceSensor(true, false, MAX_RANGEFINDER_DISTANCE, RANGEFINDER_1_TRIG, RANGEFINDER_1_ECHO);
void setup()
{
// Configure the Serial to get the logging output
Serial.begin(115200);
Serial.println("Taibot Started.");
}
void loop()
{
// Call the Update method of all the service we are using...
// Do anything else we want to do...
if ((previousTime + updateFreq) < millis())
{
previousTime = millis();
unsigned int distance = distanceSensor.GetDistance();
unsigned int average = distanceSensor.GetAverageDistance();
Serial.print("Distance=");
Serial.print(distance);
Serial.print(" Average=");
Serial.println(average);
}
}
As you see, we defined the MAX_RANGEFINDER_DISTANCE constant the distance sensor service needs and the two pins (same pin to be truth) that the HC-SR04 sensor needs. RANGEFINDER_1_TRIG
and RANGEFINDER_1_ECHO
. I used the pin 34 of my Arduino Mega, but you can use any pin you want.
Inside the loop, we are logging every 500 ms we are asking the sensor service for the distance measured and the average of the last 3 distances measured.
ConclusionYou'll find the code repository attached to this article. I created a branch that holds only this service implementation, so you can get only this if that is your intention.
If you don't understand any part of this article, were unable to make this code work, found a bug, or have any suggestion, please let me know, I'm willing to improve this and make it a clean code base for anyone that wants to reuse it.
Comments