Robotics Tutorial 6 (C#) - Remotely Connected Robots

Glossary Item Box

Microsoft Robotics Developer Studio Send feedback on this topic

Robotics Tutorial 6 (C#) - Remotely Connected Robots

Microsoft Robotics Developer Studio (RDS) allows you to create applications on your PC to remotely control simple robots. This is necessary when the robotics platform you have is not capable of running the .NET framework.

Figure 1

Figure 1 - Robot remotely connected to a PC

This tutorial teaches you how to write an interface for your own remotely connected (wired or wireless) robot. This interface implements the generic contracts defined in Robotics Common. This lets you abstract the details of your robot for higher level control.

This tutorial is provided in the C# language. You can find the project files for this tutorial at the following location under the Microsoft Robotics Developer Studio installation folder:

Samples\RoboticsTutorials\Tutorial6\CSharp

This tutorial teaches you how to:

  • Create an Onboard Remote Communication Interface.
  • Create a Hardware Interface.
  • Use Brick Service.
  • Implement Generic Services.

See Also:

  • Getting Started
  • Overview

Prerequisites

Hardware

This tutorial is intended for users who wish to write services for unsupported hardware. That being said, you may still find that using one of the platforms listed below is beneficial when working through this tutorial.

  • LEGO MINDSTORMS NXT
  • iRobot Create

Other platforms are also supported by the hardware vendors. Check the discussion forums before you start writing your own services because your robot might already have services available for it.

Software

This tutorial is designed for use with Microsoft Visual C#. You can use:

  • Microsoft Visual C# Express Edition
  • Microsoft Visual Studio Standard, Professional, or Team Edition.

You will also need Microsoft Internet Explorer or another conventional web browser.

Getting Started

You are not going to create a service in this tutorial. Instead, we walk you through several existing services to help you create your own. A Miscrosoft Visual Studio Platform (VSP) solution file which loads all the projects discussed in this tutorial is located in the samples\RoboticsTutorials\Tutorial6\CSharp directory of your RDS installation folder.

Start your development environment (Visual Studio Express, etc.) and load RoboticsTutorial6.sln. If you are using Visual studio, you should see the following in the Solution Explorer of your VS window:

Figure 2

Figure 2 - RoboticsTutorial6 projects

Overview

This tutorial walks through the LEGO NXT services, which exemplifies a common and useful architecture. This architecture is shown in the diagram below.

Figure 3

Figure 3 - LEGO NTX Services Architecture

Step 1: Create an Onboard Remote Communication Interface

Your robot requires code that supports remote communication interface with the state of the robot's sensors and motors. This code must run on the robot itself. If your robot already supports a communication interface, you can skip this step.

If your robot does not include a communication interface, you need to create this code using the development tools available for your robot. This code should monitor the sensors for changes and send a message back through its remote communications interface with the PC accordingly. It should also handle incoming motor requests properly. This code should run in a tight loop.

Step 2: Create a Hardware Interface

Now focus on the code that runs on the PC. That code must communicate with the remote interface of your robot. Some of our services perform the hardware interface in a helper service or C++/CLI library. This tutorial assumes that this is not the case for your robot and the hardware interface occurs in the same service as your "brick" service (called MyBrickService). A brick service can be thought of as the Robotics Developer Studio abstraction of your robot. Its state should contain the latest motor and sensor information for your robot.

The LEGO NXT uses a Bluetooth interface. When the Bluetooth connection is made, it appears as another serial port to the PC. Snippets of code involved in reading and writing to the serial port appear below. The two most important parts of this step are to make sure your permissions are set properly, and handle incoming messages from the serial port in the proper location.

Set up the serial port.

SerialPort serialPort = new System.IO.Ports.SerialPort();

void Open(int comPort, int baudRate)
{
    serialPort = new SerialPort("COM" + comPort.ToString(), baudRate);
    serialPort.Encoding = Encoding.Default;
    serialPort.Parity = Parity.None;
    serialPort.DataBits = 8;
    serialPort.StopBits = StopBits.One;
    serialPort.DataReceived += new SerialDataReceivedEventHandler(serialPort_DataReceived);
    serialPort.Open();
}

//Send data that your robot understands
void SendData(byte[] buffer)
{
    ...
    serialPort.Write(buffer, 0, buffer.Length);
}

If you are using Bluetooth, you may need to add a small header that includes the length of the message.

When you receive data on the COM port, you should post the data on an internal port. Then, when you receive this data in an exclusive handler, you can safely modify your state.

void serialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
    ...
    //Do not modify your state yet
    _myRobotInboundPort.Post(sensorMsg);
}

...

protected override void Start()
{
    Interleave mainInterleave = ActivateDsspOperationHandlers();

    mainInterleave.CombineWith(new Interleave(
        new TeardownReceiverGroup(),
        new ExclusiveReceiverGroup(
            Arbiter.ReceiveWithIterator<sensornotification>(true, _myRobotInboundPort, MyRobotSensorMessageHandler)
        ),
        new ConcurrentReceiverGroup()
    ));
}

private IEnumerator<itask> MyRobotSensorMessageHandler(SensorNotification sensorMessage)
{
    //update state here
    _state.sensor = sensorMessage.sensor;
    ...
}

...

Step 3: Use Brick Service

The brick service arbitrates access to the robot. It forwards actuator requests (in this case motor request) to the robot and it publishes sensor messages sent from the robot to subscribing services.

The generic subscriptions described Service Tutorial 4 (C#) - Supporting Subscriptionsand Service Tutorial 5 (C#) - Subscribing are not entirely appropriate here. A sensor service that subscribes to the brick is not interested in every sensor change on the robot. It is interested in a specific subset of sensor changes. A contact sensor service is only interested in changes to contact sensors (bumpers). It isn't concerned with ticks on a wheel encoder. This subset can be specified using custom subscriptions.

Custom Subscriptions

Just like traditional subscriptions, custom subscriptions use the subscription manager to deal with notification messages. When a custom Subscribe Request is sent, it also sends a description of the sensors to subscribe to. This description is usually a list of strings. The publishing service can support either logical disjunction (OR type) or logical conjunction (AND type) of these filter strings.

The code below demonstrates a subscription using a disjunction. That is to say it notifies the subscriber if any of the filter strings is matched. If a conjunction is implemented, a subscriber is only notified when all filter strings are matched.

First, add the custom subscription operation to the Types file:

public class MyBrickServiceOperations : PortSet
<
    DsspDefaultLookup,
    DsspDefaultDrop,
    Get,
    //IMPORTANT: Because SelectiveSubscribe inherits from Subscribe, it must go on top.
    SelectiveSubscribe,
    Subscribe
> {}

//The standard subscription
public class Subscribe : Subscribe
<
    SubscribeRequestType,
    PortSet
    <
        subscriberesponsetype
    >
> {}

//The custom subscription
public class SelectiveSubscribe : Subscribe
<
    MySubscribeRequestType,
    PortSet
    <
        SubscribeResponseType,
        Fault
    >
> { }

[DataContract]
public class MySubscribeRequestType : SubscribeRequestType
{
    //The list of sensors to subscribe to
    [DataMember]
    public List<string> Sensors;
}

Now add the new handlers to the implementation file:

// General Subscription
[ServiceHandler(ServiceHandlerBehavior.Exclusive)]
public IEnumerator<itask> SubscribeHandler(Subscribe subscribe)
{
    base.SubscribeHelper
    (
        subMgrPort,
        subscribe.Body,
        subscribe.ResponsePort
    );
    yield break;
}

// Custom Subscription
[ServiceHandler(ServiceHandlerBehavior.Exclusive)]
public IEnumerator<itask> SelectiveSubscribeHandler(SelectiveSubscribe subRequest)
{
    submgr.InsertSubscription selectiveSubscription = new submgr.InsertSubscription
    (
        new submgr.InsertSubscriptionMessage
        (
            subRequest.Body.Subscriber,
            subRequest.Body.Expiration,
            0
        )
    );

    selectiveSubscription.Body.NotificationCount = subRequest.Body.NotificationCount;

    List<submgr.QueryType> subscribeFilter = new List<submgr.QueryType>();

    //items in this loop are OR'ed together in the subscription
    foreach (string s in subRequest.Body.Sensors)
    {
        LogInfo("Adding subscription for: " + s.ToUpper());

        //you can achieve an AND behavior by adding a list of strings in the new QueryType
        subscribeFilter.Add(new submgr.QueryType(s.ToUpper()));
    }

    selectiveSubscription.Body.QueryList = subscribeFilter.ToArray();
    subMgrPort.Post(selectiveSubscription);

    yield return Arbiter.Choice
    (
        selectiveSubscription.ResponsePort,
        delegate(dssp.SubscribeResponseType response)
        {
            subRequest.ResponsePort.Post(response);
        },
        delegate(Fault fault)
        {
            subRequest.ResponsePort.Post(fault);
        });
        yield break;
    }

    selectiveSubscription.Body.NotificationCount = subRequest.Body.NotificationCount;

    List<submgr.querytype> subscribeFilter = new List<submgr.querytype>();

    //items in this loop are OR'ed together in the subscription
    foreach (string s in subRequest.Body.Sensors)
    {
        LogInfo("Adding subscription for: " + s.ToUpper());

        //you can achieve an AND behavior by adding a list of strings in the new QueryType
        subscribeFilter.Add(new submgr.QueryType(s.ToUpper()));
    }

    selectiveSubscription.Body.QueryList = subscribeFilter.ToArray();
    subMgrPort.Post(selectiveSubscription);

    yield return Arbiter.Choice
    (
        selectiveSubscription.ResponsePort,
        delegate(dssp.SubscribeResponseType response)
        {
            subRequest.ResponsePort.Post(response);
        },
        delegate(Fault fault)
        {
            subRequest.ResponsePort.Post(fault);
        }
    );
    yield break;
}

Finally, post messages to your custom subscribers in your previously defined sensor notification handler.

private IEnumerator<itask> MyRobotSensorMessageHandler(SensorNotification sensorMessage)
{
    //update state here
    _state.sensor = sensorMessage.sensor;
    ...

    //Build notification list
    List<string> notify = new List<string>();

    notify.Add(sensorMessage.Name.ToUpper());
    ...

    // notify general subscribers
    subMgrPort.Post
    (
        new submgr.Submit(_state, dssp.DsspActions.ReplaceRequest)
    );

    // notify selective subscribers
    subMgrPort.Post
    (
     new submgr.Submit(_state, dssp.DsspActions.ReplaceRequest, notify.ToArray())
    );

    yield break;
}
Bb483043.hs-note(en-us,MSDN.10).gif

The service author defines the sensor names and behavior for subscribers. Be careful to be consistent in your naming.

Step 4: Implement Generic Services

Now, write the services that interfaces with your brick service. These services implement the generic contracts defined in RoboticsCommon. This project will be called MyRobotServices.

Alternate Contracts

As in Robotics Tutorial 3 (C#) - Creating Reusable Orchestration Services, a service can implement an alternate contract. This allows your service to pose as another service by implementing its operations port. Perhaps the simplest service you need in your MyRobotServices project is the Motor service. This service is simple because it does not need to subscribe to the brick service. It only sends it motor commands.

DssNewService can generate a service that implements an alternate contract from another assembly, in this case RoboticsCommon.dll. To discover the contract for the generic motor run the utility, DssInfo.exe, to see the contents of RoboticsCommon.dll. Alternatively, the same information can be discovered through the Control Panel service on a running Node.

In a DSS Command Prompt run the following command.

<b>DssInfo /verbosity:minimal bin\services\RoboticsCommon.dll</b>

In the list of contracts that this outputs (without the -verbosity:minimal flag, it will output much more information) will be the following entry:

Contract Only:              Generic Motor
   DssContract:              https://schemas.microsoft.com/robotics/2006/05/motor.html
   Namespace:                Microsoft.Robotics.Services.Motor

The contract from this can be used in the command line of DssNewService as follows.

DssNewService.exe /service:MyRobotMotor /dir:samples\MyRobotMotor /alt:"https://schemas.microsoft.com/robotics/2006/05/motor.html" /implement:bin\services\RoboticsCommon.dll

This generates a service called MyRobotMotor in the directory samples\MyRobotMotor that implements the alternate contracthttps://schemas.microsoft.com/robotics/2006/05/motor.html found in the assembly bin\services\RoboticsCommon.dll.

This service now only needs the following changes to be functional.

  • Add a reference to the brick service proxy to your project, and note that your project has a reference to the RoboticsCommon proxy.

    Figure 4

    Figure 4 - Add reference proxies

  • Add a using declaration for the brick service proxy namespace

    using brick = Robotics.MyBrickService.Proxy;
    
  • Add the brick service as a partner

    [Partner("MyBrickService",
    Contract = brick.Contract.Identifier,
    CreationPolicy = PartnerCreationPolicy.UseExistingOrCreate,
    Optional = false)]
    brick.MyBrickServiceOperations _myBrickPort = new brick.MyBrickServiceOperations();
    
  • Implement the SetMotorPower message

    [ServiceHandler(ServiceHandlerBehavior.Exclusive)]
    public IEnumerator<itask> SetMotorPowerHandler(motor.SetMotorPower setMotorPower)
    {
        //flip direction if necessary
        double revPow = setMotorPower.Body.TargetPower;
        if (_state.ReversePolarity)
        {
            revPow *= -1.0;
        }
    
        //update state
        _state.CurrentPower = revPow;
    
        //convert to native units
        int power = (int)Math.Round(revPow * _state.PowerScalingFactor);
    
        //send hardware specific motor data
        brick.SetMotor motordata = new brick.SetMotor();
        motordata.PowerSetpoint = power;
    
        yield return Arbiter.Choice(
            _myBrickPort.SendMotorCommand(motordata),
            delegate(DefaultUpdateResponseType success)
            {
                setMotorPower.ResponsePort.Post(success);
            },
            delegate(Fault failure)
            {
                setMotorPower.ResponsePort.Post(failure);
            }
        );
    
        yield break;
    }
    

Subscribing

The majority of services you create in your MyRobotServices project will be for sensors that will need to subscribe to your brick service. They should use the custom subscription you created above. Here we show the relevant parts of the MyRobotBumper service. This service implements the ContactSensorArray contract in Robotics Common, and can be created in a similar fashion to the motor service above.

using bumper = Microsoft.Robotics.Services.ContactSensor.Proxy;
using brick = Robotics.MyBrickService.Proxy;
using submgr = Microsoft.Dss.Services.SubscriptionManager;

...

private void SubscribeToNXT()
{
    // Create a notification port
    brick..MyBrickServiceOperations _notificationPort = new brick.MyBrickServiceOperations();

    //create a custom subscription request
    brick.MySubscribeRequestType request = new brick.MySubscribeRequestType();

    //select only the sensor and ports we want
    //NOTE: this name must match the names you define in MyBrickService

    request.Sensors = new List<string>();

    foreach (bumper.ContactSensor sensor in _state.Sensors)
    {
        //Use Identifier as the port number of the sensor
        request.Sensors.Add("TOUCH" + sensor.Identifier);
    }

    //Subscribe to the brick and wait for a response
    Activate(
        Arbiter.Choice(_myBrickPort.SelectiveSubscribe(request, _notificationPort),
        delegate(SubscribeResponseType Rsp)
        {
            //update our state with subscription status
            subscribed = true;

            LogInfo("MyRobotBumper subscription success");

            //Subscription was successful, start listening for sensor change notifications
            Activate(
                Arbiter.Receive<brick.replace>(true, _notificationPort, SensorNotificationHandler)
            );
        },
        delegate(Fault F)
        {
            LogError("MyRobotBumper subscription failed");
        })
    );
}

private void SensorNotificationHandler(brick.Replace notify)
{
    //update state
    foreach (bumper.ContactSensor sensor in _state.Sensors)
    {
        bool newval = notify.Body.SensorPort[sensor.Identifier - 1] == 1 ? true : false;
        bool changed = (sensor.Pressed != newval);
        sensor.TimeStamp = DateTime.Now;
        sensor.Pressed = newval;

        if (changed)
        {
            //notify subscribers on any bumper pressed or unpressed
            _subMgrPort.Post(new submgr.Submit(sensor, DsspActions.UpdateRequest));
        }
    }
}

Extending State

The previous pattern is easy to implement and works in most cases because the state and operations in Robotics Common are very general. However, there are times when you want to add to the state or operations port. A good example of this is a "sonar as bumper" service. This service uses an ultrasonic range finder as a touch sensor. This service implements the ContactSensorArray contract as above, except you will add some state of your own. You need to do this because your state needs to include the distance reading and the threshold value, something the contact sensor array does not have.

First, we need a "types" file:

using bumper = Microsoft.Robotics.Services.ContactSensor.Proxy;

namespace Robotics.MyRobotSonarAsBumper
{
    public sealed class Contract
    {
        public const string Identifier = "https://schemas.microsoft.com/robotics/2006/07/myrobotsonarasbumper.html";
    }

    [DataContract]
    public class MyRobotSonarAsBumperState : bumper.ContactSensorArrayState
    {
        private List<int> _distanceMeasurements;
        private List<int> _thresholds;

        /// <summary>
        /// "Bumper" will be triggered when sonar is less than this value
        /// </summary>
        [DataMember]
        public List<int> Thresholds
        {
            get { return _thresholds; }
            set { _thresholds = value; }
        }

        /// <summary>
        /// Distance Measurement
        /// </summary>
        [DataMember]
        public List<int> DistanceMeasurements
        {
            get { return _distanceMeasurements; }
            set { _distanceMeasurements = value; }
        }
    }

    [ServicePort]
     public class MyRobotSonarAsBumperOperations : PortSet<
         DsspDefaultLookup,
         DsspDefaultDrop,
         Get,
         Replace
    >{}

    public class Get : Get<
         GetRequestType,
         PortSet<
             MyRobotSonarAsBumperState,
             Fault
        >
    > { }

public class Replace : Replace<
    MyRobotSonarAsBumperState,
    PortSet<
        DefaultReplaceResponseType,
        Fault
        >
    > { }
}

Notice how your state inherits from ContactSensorArrayState.

Now you need to modify the implementation file.

...
using bumper = Microsoft.Robotics.Services.ContactSensor.Proxy;
using brick = Robotics.MyBrickService.Proxy;
using submgr = Microsoft.Dss.Services.SubscriptionManager;

namespace Robotics.MyRobotSonarAsBumper
{

    [Contract(Contract.Identifier)]
    [AlternateContract(bumper.Contract.Identifier)]
    [PermissionSet(SecurityAction.PermitOnly, Name="Execution")]
    public class MyRobotSonarAsBumperService : DsspServiceBase
    {

        [InitialStatePartner(Optional = true)]
        private MyRobotSonarAsBumperState _state;

        [ServicePort("/MyRobotSonarAsBumper", AllowMultipleInstances = true)]
        private MyRobotSonarAsBumperOperations _mainPort = new MyRobotSonarAsBumperOperations();

        [AlternateServicePort(
            "/MyRobotBumper",
            AllowMultipleInstances = true,
            AlternateContract=bumper.Contract.Identifier
        )]

        private bumper.ContactSensorArrayOperations
            _bumperPort = new bumper.ContactSensorArrayOperations();

        [Partner(
            "MyRobotBrick",
            Contract = brick.Contract.Identifier,
            CreationPolicy = PartnerCreationPolicy.UseExistingOrCreate,
            Optional = false
        )]

        private brick.MyBrickServiceOperations _brickPort = new brick.MyBrickServiceOperations();

        [Partner(
            "SubMgr",
            Contract = submgr.Contract.Identifier,
            CreationPolicy = PartnerCreationPolicy.CreateAlways,
            Optional = false)]
        private submgr.SubscriptionManagerPort _subMgrPort = new submgr.SubscriptionManagerPort();

...

Note that the main port handles your messages and your alternate port handles, ContactSensorArrayOperations.

Now you need to listen on your alternate port:

// Listen on the main port for requests and call the appropriate handler.
Interleave mainInterleave = ActivateDsspOperationHandlers();

//listen on alternate service port for requests and call the appropriate handler.

mainInterleave.CombineWith(new Interleave(
    new TeardownReceiverGroup(
        Arbiter.Receive<dsspdefaultdrop>(
            false,
            _bumperPort,
            DefaultDropHandler
        )
    ),
    new ExclusiveReceiverGroup(
        Arbiter.ReceiveWithIterator<bumper.>(
            true,
            _bumperPort,
            ReplaceHandler
        ),
        Arbiter.ReceiveWithIterator<bumper.>(
            true,
            _bumperPort,
            SubscribeHandler
        ),
        Arbiter.ReceiveWithIterator<bumper.>(
            true,
            _bumperPort,
            ReliableSubscribeHandler
        )
    ),
    new ConcurrentReceiverGroup(
        Arbiter.ReceiveWithIterator<bumper.>(
            true,
            _bumperPort,
            GetHandler
        ),
        Arbiter.Receive<dsspdefaultlookup>(
            true,
            _bumperPort,
            DefaultLookupHandler
        )
    )
));

Note that you now have to implement two versions of some handlers such as Get , Replace, and Subscribe. The difference between Get handlers is shown below:

[ServiceHandler(ServiceHandlerBehavior.Concurrent)]
public IEnumerator<itask> MyGetHandler(Get get)
{
    get.ResponsePort.Post(_state);
    yield break;
}


public IEnumerator<itask> GetHandler(bumper.Get get)
{
    get.ResponsePort.Post((bumper.ContactSensorArrayState)_state.Clone());
    yield break;
}
Bb483043.hs-note(en-us,MSDN.10).gif

In the Get handler for the alternate contract, it is necessary to down cast from your state type to the ContactSensorArrayState. This is because when messages are serialized they are serialized as their actual type, this causes an error if the sender of the message is expecting the base type. However, all proxy types implement the Clone() method, so simply calling Clone() on your type will create a cloned copy of the base type.

That's all there is to it. The subscription to the brick service is the same, (except you subscribe to a sonar sensor). When you get sensor notifications and determine that your sonar bumper is "pressed", send the bumper state.

Summary

In this tutorial, you learned how to:

  • Create an Onboard Remote Communication Interface.
  • Create a Hardware Interface.
  • Use Brick Service.
  • Implement Generic Services.

 

 

© 2012 Microsoft Corporation. All Rights Reserved.