Programming a loosely coupled system: Gadgeteer Robot-F6
After I made my first steps in programming a Gagdgeteer project. See also:
Gadgeteer GHI Extender Module, combining functionality. I was not happy about always ending up with one Program.cs with all code in it. Therefore I decided to improve the code. Below is a picture of the system: the controls, sensors and brain of my robot:
Cerberus motherboard Wifi module RN171 Extender module for:Motor control distance measurement power supply when not programming Humidity/temperature module USBclientSP (only for programming)
Basic setup
Robot web control
The robot has a web interface and via ajax calls to the "web server", the robot can be controlled. I created a class called WebRequestHandler.cs which handles all web requests, but I shouldn't have any knowledge about the motor controls, humidity sensor, etc. This can be achieved by using the command-pattern. All the necessary commands must implement the ICommand-interface:
using System;
using Microsoft.SPOT;
namespace Robot_F6
{
public interface ICommand
{
void Execute();
}
}
The WebRequestHandler handler has no knowledge about the different implementations. It just checks whether a command has been supplied when a specific WebRequest is handled. If a command is available, the WebRequestHandler calls the ICommand.Execute() method.
using System;
using System.Collections;
using System.Threading;
using Microsoft.SPOT;
using Microsoft.SPOT.Presentation;
using Microsoft.SPOT.Presentation.Controls;
using Microsoft.SPOT.Presentation.Media;
using Microsoft.SPOT.Touch;
using Gadgeteer.Networking;
using GT = Gadgeteer;
using GTM = Gadgeteer.Modules;
using Gadgeteer.Interfaces;
using Gadgeteer.Modules.GHIElectronics;
using Gadgeteer;
using Gadgeteer.Modules.Seeed;
namespace Robot_F6
{
public class WebRequestHandler : IWebRequestHandler
{
public ICommand NorthCommand { get; set; }
public ICommand NorthEastCommand { get; set; }
public ICommand EastCommand { get; set; }
public ICommand SouthEastCommand { get; set; }
public ICommand SouthCommand { get; set; }
public ICommand SouthWestCommand { get; set; }
public ICommand WestCommand { get; set; }
public ICommand NorthWestCommand { get; set; }
public ICommand StopCommand { get; set; }
public ICommand StartCommand { get; set; }
public ICommand FireCommand { get; set; }
public ICommand FastCommand { get; set; }
public ICommand SlowCommand { get; set; }
public ICommand BreakCommand { get; set; }
public ITemperatureMeasurer TemperatureMeasurer { get; set; }
public IHumidityMeasurer HumidityMeasurer { get; set; }
public WebRequestHandler()
{
}
public void HandleRequest(GTM.GHIElectronics.HttpStream request)
{
string requestedURL = request.Request.URL;
if (requestedURL.IndexOf("/_") == 0)
{
HandleAjaxHttpRequest(request, requestedURL);
}
else
{
HandleHttpRequest(request, requestedURL);
}
}
private void HandleHttpRequest(GTM.GHIElectronics.HttpStream request, string requestedURL)
{
string response = "Unkown action";
if (requestedURL == "/index.html" || requestedURL == "/") { response = "Welcome"; }
else { response = "Unknown action"; }
response = "HTML HERE" + response + "HTML HERE"; // see below
SendResponse(request, response);
}
private void HandleAjaxHttpRequest(GTM.GHIElectronics.HttpStream request, string requestedURL)
{
bool responseWasHandled = false;
string response = "";
switch (requestedURL)
{
case "/_index":
response = "Welcome";
break;
case "/_northwest":
response += "north west";
if(NorthWestCommand != null){ NorthWestCommand.Execute();}
break;
case "/_north":
response += "north";
if(NorthCommand != null){ NorthCommand.Execute();}
break;
case "/_northeast":
response += "north east";
if(NorthEastCommand != null){ NorthEastCommand.Execute();}
break;
case "/_west":
response += "west";
if(WestCommand != null){ WestCommand.Execute();}
break;
case "/_east":
response += "east";
if (EastCommand != null) { EastCommand.Execute(); }
break;
case "/_southwest":
if( SouthWestCommand!= null){ SouthWestCommand.Execute();}
break;
case "/_south":
response = "We're going down south";
if(SouthCommand != null){ SouthCommand.Execute();}
break;
case "/_southeast":
if(SouthEastCommand != null){ SouthEastCommand.Execute();}
break;
case "/_start":
response = "Let's go";
if(StartCommand != null){ StartCommand.Execute();}
break;
case "/_fast":
response = "Fast";
if (FastCommand != null) { FastCommand.Execute(); }
break;
case "/_slow":
response = "Slow";
if (SlowCommand != null) { SlowCommand.Execute(); }
break;
case "/_stop":
response = "Hold it, right there!";
if(StopCommand != null){ StopCommand.Execute();}
break;
case "/_fire":
if(TemperatureMeasurer != null)
{
response = "Temp: " + TemperatureMeasurer.GetTemperature().ToString("f2") + "C ";
}
if(HumidityMeasurer != null)
{
response += " Humidity: " + HumidityMeasurer.GetHumidity().ToString("f2") + "%";
}
if (FireCommand != null) { FireCommand.Execute(); }
break;
case "/_getdataX":
if(TemperatureMeasurer != null)
{
response = "Temp: " + TemperatureMeasurer.GetTemperature().ToString("f2") + "C ";
}
if(HumidityMeasurer != null)
{
response += " Humidity: " + HumidityMeasurer.GetHumidity().ToString("f2") + "%";
}
break;
default:
response = "Unknown action";
break;
}
if (!responseWasHandled)
{
SendResponse(request, response);
}
}
private void SendResponse(GTM.GHIElectronics.HttpStream request, string response)
{
byte[] document = System.Text.Encoding.UTF8.GetBytes(response);
request.Response.HeaderData["Content-type"] = "text/html; charset=utf-8";
request.Response.HeaderData["Connection"] = "close";
request.Response.HeaderData["Cache-Control"] = "no-cache";
request.Response.HeaderData["Content-Length"] = document.Length.ToString();
request.Response.StatusCode = GTM.GHIElectronics.HttpResponse.ResponseStatus.OK;
request.Response.Send(document);
}
}
}
The example response html did mess up the 'code view' a little. There fore I removed it and copied in my 'normal text'-view. Hope to fix that some other time.
response = "<!DOCTYPE html>\n<html lang=\"en\" xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n <meta charset=\"utf-8\" />\n <title>Robot F6</title>\n</head>\n<body>\n <style>\n td {\n width:160px;\n vertical-align:central\n }\n input[type=\"button\"] {\n font-size:144px;\n background-color:beige;\n text-align:center;\n color:black;\n font-weight:bold;\n width:150px;\n height:150px;\n vertical-align:central\n\n }\n\n input[type=\"button\"].x {\n font-size:12px;\n }\n\n #btnStop {\n background-color:red;\n font-size:32px;\n }\n\n #btnStart {\n background-color:green;\n font-size:32px;\n }\n \n #btnFast {\n background-color:midnightblue;\n color:white;\n font-size:32px;\n }\n \n #btnSlow {\n background-color:yellow;\n font-size:32px;\n }\n #lbInfo {\n display: block;\n float: right;\n width:300px;\n background-color:white;\n font-size:24px;\n text-align:center\n }\n\n </style>\n <script type=\"text/javascript\">\n var xmlhttp;\n if (window.XMLHttpRequest) {\n xmlhttp = new XMLHttpRequest();\n }\n else {\n xmlhttp = new ActiveXObject(\"Microsoft.XMLHTTP\");\n }\n\n function submitAction(action) {\n\n xmlhttp.onreadystatechange = function () {\n if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {\n document.getElementById(\"lbInfo\").innerHTML = xmlhttp.responseText;\n }\n }\n xmlhttp.open(\"GET\", \"http://192.168.1.1/_\" + action, true);\n xmlhttp.send();\n }\n\n\n window.setInterval(submitAction('getdataX'), 1000); </script>\n <table>\n <tr>\n <td><input type=\"button\" class=\"x\" value=\"x\" id=\"btnNorthWest\" onclick=\"submitAction('northwest');\"/></td>\n <td><input type=\"button\" value=\"/\\\" id=\"btnUp\" onclick=\"submitAction('north');\"/></td>\n <td><input type=\"button\" class=\"x\" value=\"x\" id=\"btnNorthEast\" onclick=\"submitAction('northeast');\"/></td>\n <td><input type=\"button\" value=\"START\" id=\"btnStart\" onclick=\"submitAction('start');\"/></td>\n <td><input type=\"button\" value=\"STOP\" id=\"btnStop\" onclick=\"submitAction('stop');\"/></td>\n </tr>\n <tr>\n <td><input type=\"button\" value=\"<\" id=\"btnWest\" onclick=\"submitAction('west');\"/></td>\n <td><input type=\"button\" class=\"x\" value=\"x\" id=\"btnCenter\" onclick=\"submitAction('fire');\"/></td>\n <td><input type=\"button\" value=\">\" id=\"btnEast\" onclick=\"submitAction('east');\"/></td>\n <td colspan=2><label id=\"lbInfo\">Welcome to Robot-F6. Please tell me what to do.</label></td>\n\n </tr>\n <tr>\n <td><input type=\"button\" class=\"x\" value=\"x\" id=\"btnSouthWest\" onclick=\"submitAction('southwest');\"/></td>\n <td><input type=\"button\" value=\"\\/\" id=\"btnSouth\" onclick=\"submitAction('south');\"/></td>\n <td><input type=\"button\" class=\"x\" value=\"x\" id=\"btnSouthEast\" onclick=\"submitAction('southeast');\"/></td>\n <td><input type=\"button\" value=\"SLOW\" id=\"btnFast\" onclick=\"submitAction('slow');\"/></td>\n <td><input type=\"button\" value=\"FAST\" id=\"btnFast\" onclick=\"submitAction('fast');\"/></td>\n </tr>\n </table>\n</body>\n</html>";
As you can see, the WebRequestHandler has no knowledge of the command implementations. Besides the ICommand interface there are two other interfaces that are used by the WebRequestHandler: ITemperatureMeasurer and IHumidityMeasurer
using System;
using Microsoft.SPOT;
namespace Robot_F6
{
public interface ITemperatureMeasurer
{
double GetTemperature();
}
}
using System;
using Microsoft.SPOT;
namespace Robot_F6
{
public interface IHumidityMeasurer
{
double GetHumidity();
}
}
Both interfaces are implemented by the TemperatureHumidityModuleWrapper class which wraps the Gadgeteer.Modules.Seeed.TemperatureHumidity module. So if you want to use different temperature and/or humidity modules, you don't have to change the WebRequestHandler. Just create another implementation of the IHumidityMeasurer and/or ITemperatureMeasurer and tie them together (loosely).
using System;
using Microsoft.SPOT;
using Gadgeteer.Modules.Seeed;
namespace Robot_F6
{
public class TemperatureHumidityModuleWrapper : IHumidityMeasurer, ITemperatureMeasurer
{
private double _humidity = 0;
private double _temperature = 0;
private TemperatureHumidity _temperatureHumidityModule;
public TemperatureHumidityModuleWrapper(TemperatureHumidity temperatureHumidityModule)
{
_temperatureHumidityModule = temperatureHumidityModule;
}
public double GetHumidity()
{
return _humidity;
}
public void SetHumidity(double humidity)
{
_humidity = humidity;
}
public double GetTemperature()
{
return _temperature;
}
public void SetTemperature(double temperature)
{
_temperature = temperature;
}
}
}
For the ICommand implementation I created different classes: one for each command. I could have combined some, but there's not much functionality in the implementations. For example the MoveLeftBackwardsCommand looks like this:
using System;
using Microsoft.SPOT;
namespace Robot_F6
{
public class MoveLeftBackwardsCommand : ICommand
{
private IDualMotorDriver _dualMotorDriver;
public MoveLeftBackwardsCommand(IDualMotorDriver dualMotorDriver)
{
_dualMotorDriver = dualMotorDriver;
}
public void Execute()
{
_dualMotorDriver.MoveLeftBackwards();
}
}
}
As you can see, there's not much functionality in it. All movement commands that I created use an IDualMotorDriver interface. This way the ICommand implementations are loosely coupled to the DualMotorDriver implementation.
using System;
using Microsoft.SPOT;
namespace Robot_F6
{
public interface IDualMotorDriver
{
/// <summary>
/// Set M1 and M2 to move left forwards
/// </summary>
void MoveForwards();
/// <summary>
/// Set M1 and M2 to move right backwards
/// </summary>
void MoveBackwards();
/// <summary>
/// Set M1 and M2 to turn clockwise: M1 backwards, M2 forwards
/// </summary>
void TurnClockwise();
/// <summary>
/// Set M1 and M2 to turn counter clockwise: M1 forwards, M2 backwards
/// </summary>
void TurnCounterClockwise();
/// <summary>
/// Set M1 and M2 to move left forwards: M1 moves slower than M2
/// </summary>
void MoveLeftForwards();
/// <summary>
/// Set M1 and M2 to move left forwards: M1 moves faster than M2
/// </summary>
void MoveRightForwards();
/// <summary>
/// Set M1 and M2 to move left backwards: M1 moves slower than M2
/// </summary>
void MoveLeftBackwards();
/// <summary>
/// Set M1 and M2 to move left backwards: M1 moves faster than M2
/// </summary>
void MoveRightBackwards();
/// <summary>
/// Set M1 and M2 to stop
/// </summary>
void Stop();
/// <summary>
/// Set M1 and M2 to break
/// </summary>
void Break();
/// <summary>
/// Move M1 and M2 one step
/// </summary>
void Step();
/// <summary>
/// Change to 'fast motion'
/// </summary>
void Fast();
/// <summary>
/// Change to 'slow motion'
/// </summary>
void Slow();
void NormalSpeed();
}
}
So how do we tie things together? Partially by creating a factory that instantiates the implementations of specific interfaces.
using System;
using Microsoft.SPOT;
using Gadgeteer.Modules.GHIElectronics;
using GT = Gadgeteer;
using GTM = Gadgeteer.Modules;
using Gadgeteer.Interfaces;
using Gadgeteer.Modules.Seeed;
namespace Robot_F6
{
public class F6Factory
{
public static IMotor CreateMotor(GT.Interfaces.DigitalOutput contact1, GT.Interfaces.DigitalOutput contact2)
{
return new Motor(contact1, contact2);
}
public static IDualMotorDriver CreateDualMotorDriver(int socketNumber, DigitalOutput m1a, DigitalOutput m1b, DigitalOutput m2a, DigitalOutput m2b)
{
return new L298_H_BridgeDriver(socketNumber, m1a, m1b, m2a, m2b);
}
public static IWebRequestHandler CreateWebRequestHandler()
{
return new WebRequestHandler();
}
}
}
All other parts are tied together in Program.cs. First the extender module is configured, then the temperaturehumiditywrapper and the webRequestHandler. To prevent the ProgramStarted method from locking, a run once timer is intialized and started.
void ProgramStarted()
{
wifi_RN171.Reboot();
int socketNumber = 4;
// Distance_US3 uses pin3 and pin4. Other IO pins are unused.
_distanceMeterFront = new Distance_US3(socketNumber);
DigitalOutput m1a = extender.SetupDigitalOutput(Socket.Pin.Nine, false);
DigitalOutput m1b = extender.SetupDigitalOutput(Socket.Pin.Eight, false);
DigitalOutput m2a = extender.SetupDigitalOutput(Socket.Pin.Seven, false);
DigitalOutput m2b = extender.SetupDigitalOutput(Socket.Pin.Six, false);
_iDualMotorDriver = F6Factory.CreateDualMotorDriver(socketNumber, m1a, m1b, m2a, m2b);
_temperatureHumidityModuleWrapper = new TemperatureHumidityModuleWrapper(temperatureHumidity);
_webRequestHandler = F6Factory.CreateWebRequestHandler();
GT.Timer timer = new GT.Timer(1000, GT.Timer.BehaviorType.RunOnce);
timer.Tick += timer_FirstTick;
timer.Start();
Debug.Print("Robot F6 started...");
}
In the timer_FirstTick, all components are configured: WebRequestHandler, Wifi, Sensors and a Helloworld is given to the user: Robot spins short Clockwise and Counter Clockwise. After that an interval timer is started. Each timer tick the Robot can 'think and act' about what to do in the current state.
private void timer_FirstTick(GT.Timer timer)
{
timer.Stop();
timer.Tick -= timer_FirstTick;
SetupWebRequestHandler();
SetupWifi();
SetupTemperatureAndHumidityMeasurements();
HelloWorld();
StartIntervalTimer();
}
private void StartIntervalTimer()
{
GT.Timer intervalTimer = new GT.Timer(_intervalmsec, GT.Timer.BehaviorType.RunContinuously);
intervalTimer.Tick += intervalTimer_Tick;
intervalTimer.Start();
}
private void HelloWorld()
{
_iDualMotorDriver.TurnClockwise();
Thread.Sleep(1000);
_iDualMotorDriver.Stop();
Thread.Sleep(1000);
_iDualMotorDriver.TurnCounterClockwise();
Thread.Sleep(1000);
_iDualMotorDriver.Stop();
}
In the SetupWebRequestHandler all the commands and the humidity and temperature measurers are created and attached to the WebRequestHandler.
private void SetupWebRequestHandler()
{
_webRequestHandler.NorthCommand = new MoveForwardsCommand(_iDualMotorDriver);
_webRequestHandler.NorthEastCommand = new MoveRightForwardsCommand(_iDualMotorDriver);
_webRequestHandler.EastCommand = new TurnClockwiseCommand(_iDualMotorDriver);
_webRequestHandler.SouthEastCommand = new MoveRightBackwardsCommand(_iDualMotorDriver);
_webRequestHandler.SouthCommand = new MoveBackwardsCommand(_iDualMotorDriver);
_webRequestHandler.SouthWestCommand = new MoveLeftBackwardsCommand(_iDualMotorDriver);
_webRequestHandler.WestCommand = new TurnCounterClockwiseCommand(_iDualMotorDriver);
_webRequestHandler.NorthWestCommand = new MoveLeftForwardsCommand(_iDualMotorDriver);
_webRequestHandler.FireCommand = new DelegatedCommand(new DelegatedCommand.DoSomething(this.ToggleAutoPilot));
<br/>
_webRequestHandler.SlowCommand = new SlowCommand(_iDualMotorDriver);
_webRequestHandler.FastCommand = new FastCommand(_iDualMotorDriver);
_webRequestHandler.StopCommand = new StopCommand(_iDualMotorDriver);
_webRequestHandler.BreakCommand = new BreakCommand(_iDualMotorDriver);
_webRequestHandler.StartCommand = new StartCommand(_iDualMotorDriver);
_webRequestHandler.TemperatureMeasurer = _temperatureHumidityModuleWrapper;
_webRequestHandler.HumidityMeasurer = _temperatureHumidityModuleWrapper;
}
The SetupWifi enables the HttpServer and subscribes to the HttpRequestReceived event.
private void SetupWifi()
{
wifi_RN171.SetDebugLevel(GTM.GHIElectronics.WiFi_RN171.DebugLevel.DebugAll);
wifi_RN171.Initialize(GTM.GHIElectronics.WiFi_RN171.SocketProtocol.TCP_Server);
wifi_RN171.EnableHttpServer(); //Enable HTTP Parsing
wifi_RN171.HttpRequestReceived += new GTM.GHIElectronics.WiFi_RN171.HttpRequestReceivedHandler(wifi_RN171_HttpRequestReceived);
<br/>
}
public void wifi_RN171_HttpRequestReceived(GTM.GHIElectronics.HttpStream request)
{
_webRequestHandler.HandleRequest(request);
}
The SetupTemperatureAndHumidityMeasurements starts continuous measurements and subscribes to MeasurementComplete events of the 'TemperatureHumidity' module.
private void SetupTemperatureAndHumidityMeasurements()
{
temperatureHumidity.MeasurementComplete += temperatureHumidity_MeasurementComplete;
temperatureHumidity.StartContinuousMeasurements();
}
When the Robot is in auto pilot mode, the intervaltimertick will change the robot's direction based on the measured distance. When in manual mode, the Step method of the dualmotordriver is called.
private void intervalTimer_Tick(GT.Timer intervalTimer)
{
intervalTimer.Stop();
int distance = _distanceMeterFront.GetDistanceInCentimeters();
if (autoPilot)
{
_counter++;
if (_counter * _intervalmsec < 5000)
{
if (distance < 0 || distance > mininumDistance)
{
_iDualMotorDriver.MoveForwards();
}
else
{
_iDualMotorDriver.TurnClockwise();
}
}
else if (_counter * _intervalmsec < 7500)
{
_iDualMotorDriver.Stop();
}
else
{
_counter = 0;
}
}
//else if (distance > 0 && distance < mininumDistance)
//{
// autoPilot = true;
//}
else
{
_iDualMotorDriver.Step();
}
intervalTimer.Start();
}
One could argue to create all commands in the factory as well, but then I would suggest to move all logic from Program.cs as well. The source code can be downloaded
here .