CmdMessenger - CodeProject

:

: 9

Introduction

I've recently been building an omni directional robot, which i'm documenting here. Since I don't want this article to become too long. I'm going to split the documentation across a number of articles.

In the first article I'm going to discuss the comms protocol used to communicate with the robot. You can see a picture and video of the robot in action below: 

 

Background

I've worked on a number of projects around the Arduino platform in the past which required Serial communication. I often came up against a hurdle when using Serial comms once the messages sent to the Arduino became more complex. I began looking into solutions to this problem and soon came across CmdMessenger on the Arduino playground.

From the libraries homepage:

Quote:

CmdMessenger is a messaging library for the Arduino Platform (and .NET/Mono platform). It uses the serial port as transport layer. To use CmdMessenger, we define a list of command identifiers, then attach callback / handler functions for received messages.

The library was a great solution the Serial comms problem I was trying to solve. However the .Net implementation of the protocol didn't meet my requirements. The existing .Net library was tightly coupled to using a serial port for the transport layer. Since many devices such as the Raspberry Pi and Arduino Yun now support WiFi, I wanted to be able to send the messages over TCP/IP.

The messages sent over TCP/IP could can then be proxied to the Arduino using a simple Python script. See diagram below:

I'm using a python script called tcp_serial_redirect.py which is run as a daemon process on the Linux device. I won't cover the steps involved in configuring the device in this article though. Before the Arduino Yun was released i was using a Raspberry Pi as the Linux device coupled with a Arduino Uno.

Protocol

The protocol defined by CmdMessenger is simple and very flexible. The format of the protocol is as follows:

String Escape parameters

Since the strings could contain a Parameter or Command separator the protocol must support escaping of these special characters. The special characters are escaped by suffixing them with the escape characters. By default the escape characters is a back slash. For example:

  • 5,Hello,World; - Would appear as one command with three parameters.
  • 5,Hello\,World; - Would appear as one command with two parameters. 

This is all handled by the library and therefore the client does not need to know these details.

Architecture

The following class diagram shows the basic overview of the library:

CmdMessenger

CmdMessenger is the main entry point into the library.

  • Start - opens the connection and starts processing incoming commands.
  • Stop - closes the connection and stops reading incoming commands.
  • Send - Sends an ISendCommand and blocks until a response is received.
  • SendAsync - Sends an ISendCommand and returns a Taks<IRecievedCommand>.
  • Register - allows a method or ICommandObserver to subscribe to incoming commands with a specified ID.

IReceivedCommand

IReceivedCommand provides an interface for reading incoming commands.

  • ReadInt16 - reads a 16 bit integer from the command.
  • ReadBool - reads a boolean from the command where 1, 0 represents true, false respectively.
  • ReadInt32 - reads a 32 bit integer from the command.
  • ReadString - reads a string from the command.

ReceivedCommand

ReceivedCommand is the concrete implementation of IReceivedCommand. There shouldn't be a need for clients to implement the interface.

ISendCommand

ISendCommand provides an interface for sending outgoing commands.

SendCommand

SendCommand provides a concrete implementation of the the ISendCommand interface. The class can be used by clients directly or the library can be extended by creating command which derive from this class.

ICmdComms

ICmdComms provides a common interface for transport layers.

CmdComms

Provides a common implementation for reading commands from a stream. Other stream based transport layers can derive from this base class. Two implementation of the ICmdComms are provided with the library:

  • SerialCmdClient
  • TcpCmdClient

Simple demo

The source code contains a simple demo application which demonstrates sending a command and handling its response. The demo shows the following steps:

  • CmdMessenger is first instantiated along with a transport layer.
  • A command handler is registered to handle the response command.
  • The start method is called to to open the connection to the transport layer and start processing the commands.
  • A command is sent to the CmdMessenger server and waits until the server responds with a CommandID of 1.
// Create an instance of CmdMessenger and provide an implementation of the transport layer.
var cmdClient = new CmdMessenger.CmdMessenger(new TcpCmdClient("127.0.0.1", 5000));
// Register one or more command handlers.
cmdClient.Register(1, r => Console.WriteLine("Response received"));
// Open the connection and begin processing incoming commands.
cmdClient.Start();

while (Console.ReadLine() != "x")
{
      // Send a simple command with no arguments.
      cmdClient.Send(new SendCommand(0, 1));
}

Commands can be uni or bi directional. If the send command defines an AckCommandID the send command will block until a response is received or the time-out is reached. 

// Will block until the device responds with a commandID of 1
cmdClient.Send(new SendCommand(0, 1);

// Will return imedietly since no AckCommandID has been defiened.
cmdClient.Send(new SendCommand(0);

The library supports C#5 with the SendAsync methods. 

Task<IReceivedCommand> received = cmdClient.SendAsync(new SendCommand(0, 1);

// Do some other work

// Wait for the response 
await received;

Robot Controller Application

The source code contains a WPF application for controlling the robot. This application takes things a little further by adding an extra layer of abstraction:

There are four components which make up the Robot Controller:

  • CmdMessenger - The CmdMessenger library.
  • PiBot.Common - A class library which adds a layer of abstraction on top of CmdMessenger.
  • ControlPad - A Custom Control (I might document this in a subsequent article)
  • PiBot.Gui - The main application.

PiBot.Common - Robot Commands

PiBot.Common contains a number of commands. I plan to add sensors and a pan/tilt camera to the robot at some stage. I havent added these features yet and therefore I have only fully implemented a few commands.

  • SetMotorSpeed
  • SendMotorSpeed

PitBot.Gui - Application

The Robot Controller application is simple to use:

  • Select "Address = 127.0.0.1, Port 5000" from the combo box.
  • Click connect, the controller will wait until a connection can be established.
  • Once a connection is established the control pad will be enabled allowing you to click the control directions.

Since you won't have my robot to hand I've created a test harness which can be used in conjunction with the robot controller application.

Arduino Code

The following is a code dump of the Arduino code. 

Key Points

I'm using four motor controllers from VEX robotics to drive the robot Motor Controller 29. The motor controllers require a PWM signal in the range of 1ms to 2ms with 1ms being full reverse and 2ms full forward, 1.5 ms is neutral. From testing I was able to use the Arduino Servo library to generate the desired PWM signal. I found an angle of 40 gave me full reverse and 140 gave me full forward, therefore the range of available speeds is 0 - 100. 

#include <CmdMessenger.h>
#include <Base64.h>
#include <Streaming.h>
#include <Servo.h>

char field_separator = ',';
char command_separator = ';';

CmdMessenger cmdMessenger = CmdMessenger(Serial, field_separator, command_separator);

Servo servo1;
Servo servo2;
Servo servo3;
Servo servo4;

enum
{
    //Request the device to send it's motor speed.
    GetMotorSpeed = 1,
    // Response message to a GetMotorSpeed message.
    GetMotorSpeedResponse,
    // Sets the motor speed on the device.
    SetMotorSpeed,
    // Message send when an external device changes the motor speed.
    SendMotorSpeed,
    // Request the current pan tilt position from the device.
    GetPanTiltPos,
    // Response message to a GetPanTiltPos message.
    GetPanTiltPosResponse,
    // Set the pan tilt position on the device.
    SetPanTiltPos,
    // Message sent when an external device modifies the pan tilt position.
    SendPanTiltPos,
    // Get the current value for a specified sensor.
    GetSensorValue,
    // Response message to a GetSensorValue.
    GetSensorValueResponse,
    //Set sensor value 
    SetSensorValue,
    // Message sent when a sensor value changes.
    SendSensorValue,
    // A fault has occurred.
    Fault
};

// Commands we send from the PC and want to receive on the Arduino
// We must define a callback function in our Arduino program for each entry in the list.
messengerCallbackFunction messengerCallbacks[] = 
{
    HandleGetMotorSpeed,
    unknownCmd,
    HandleSetMotorSpeed,
    unknownCmd,
    NULL
};

// ---------------------- C A L L B A C K  M E T H O D S ----------------------------------

void HandleGetMotorSpeed()
{
    int leftSpeedF  = servo1.read() - 40;
    int rightSpeedF = servo2.read() - 40;
    int leftSpeedR  = servo3.read() - 40;
    int rightSpeedR = servo4.read() - 40;
    
    String response = "Send pan tilt pos" + String(leftSpeedF) + "," + String(rightSpeedF) + 
                                        "," + String(leftSpeedR) + "," + String(leftSpeedR);
    
    char* stdStr = new char[response.length() + 1];
    response.toCharArray(stdStr,response.length());
  
    cmdMessenger.sendCmd(GetMotorSpeedResponse, "Get mototr speed");
}

void HandleSetMotorSpeed()
{
    int leftSpeedF  = cmdMessenger.readInt();
    int rightSpeedF = cmdMessenger.readInt();
    int leftSpeedR  = cmdMessenger.readInt();
    int rightSpeedR = cmdMessenger.readInt();
    
    cmdMessenger.sendCmd(SendMotorSpeed, "Send motor speed");
  
    servo1.write(40 + leftSpeedF); 
    servo2.write(40 + rightSpeedF); 
    servo3.write(40 + leftSpeedR); 
    servo4.write(40 + rightSpeedR); 
}

// ----------------------- D E F A U L T   C A L L B A C K S --------------------------------------

void unknownCmd()
{
    // Default response for unknown commands and corrupt messages
    cmdMessenger.sendCmd(Fault,"Unknown command");
}

// ------------------ E N D  C A L L B A C K  M E T H O D S ------------------


// ------------------ S E T U P ----------------------------------------------

void attach_callbacks(messengerCallbackFunction* callbacks)
{
    int i = 0;
    int offset = 1;
    while(callbacks[i])
    {
        cmdMessenger.attach(offset+i, callbacks[i]);
        i++;
    }
}

void setup()
{
    // Listen on the serial connection for messages from pc
    Serial.begin(57600);
    
    servo1.attach(8);
    servo2.attach(9);
    servo3.attach(10);
    servo4.attach(11); 
    
    // Pront line break between messages.
    cmdMessenger.print_LF_CR();
    
    cmdMessenger.attach(unknownCmd);
    
    attach_callbacks(messengerCallbacks);
    
}

// ------------------ E N D  S E T U P ----------------------------------

void loop()
{
    cmdMessenger.feedinSerialData();
  
}

Points of Interest

Support For Binary Form (efficient)

The original C# implementation of the CmdMessenger library supports sending messages in binary format. I decided not to implemented this feature in my library. I think the advantage of protcol is  the fact that messages can be  sent using a simple serial or telnet session. Messages can be sent to a device using simple ASCII commands. Implementing this feature would complicate the library. The main purpose for sending messages in binary would be to reduce latency which is not the main purpose of this library.

Source Code 

If you would like to view the libraries source code and demo applications the code can be found on my Bit Bucket site. 

https://bitbucket.org/chrism233/pibot

The git repository address is as follows:

https://chrism233@bitbucket.org/chrism233/pibot.git

History

Date Changes
25/08/2014 Initial release.