Chapter 4. Advanced Service Concepts

The Decentralized Software Services (DSS) are responsible for controlling the basic operations of robotics applications. As explained in the previous chapter, DSS is built on top of the CCR. This chapter moves on to more advanced features of services, including subscriptions and user interfaces. It is very likely that you will need to use the material in this chapter at some stage.

This chapter develops a service, called TeleOperation, that can be used to drive a robot with a web camera attached so you can see where the robot is going. The code introduces you to using Windows Forms as well as Web Forms with MRDS services, and demonstrates how to display live video. Don't worry if you don't have a robot with a camera because the faithful simulator comes to the rescue and you can still work through the code.

You might want your user interface to look like the cockpit of a 747, but that is not the objective in this chapter. It covers the basics of using Windows Forms (WinForms) and how to format web pages using XSLT. You can add fancy stuff yourself later.

These concepts are used in the Dashboard service that is included with this chapter. However, the discussion in the text uses the TeleOperation service because it contains a small subset of the Dashboard functions.

Setting Up for This Chapter

Although MRDS is full of examples, a new service is provided in this chapter to perform teleoperation: driving a robot remotely using an onboard camera. This is a common scenario, used, for example, in bomb disposal robots. In this case, you are the brains of the robot. It is basically just a camera on wheels, and requires only two services: Generic Differential Drive and Webcam.

You can use the TeleOperation service in this chapter with any robot that has a Generic Differential Drive service. In addition, if you can attach a camera to the robot somehow and it is accessible via the Webcam service, then you can see what the robot sees. The simplest solution is a small wireless "spy camera" with a video receiver that can be connected to your PC via a video capture device. This setup is shown diagrammatically in Figure 4-1.

Figure 4-1

Figure 4.1. Figure 4-1

Figure 4-2 shows a Swann wireless camera mounted on the front of a LEGO NXT Tribot. The camera is smaller than the 9V battery that powers it. Because the camera is not a LEGO part, liberal use of Blutac is required to fasten the camera and the battery onto the Tribot.

Figure 4-2

Figure 4.2. Figure 4-2

Even if you do not have a wireless camera, you can still try out the service using the Maze Simulator (which is in Chapter 9) with a simulated Pioneer 3DX robot. By default, when you run the TeleOperation service from Visual Studio, it starts the Maze Simulator. To run the service with a real robot, you need to edit the manifest: ProMRDSChapter4TeleOperationTeleOperation.manifest.xml.

You can also edit the command-line parameters in the Debug properties for the Solution and add another manifest, but you still need to remove the Maze Simulator from the existing manifest in this case.

The design of the TeleOperation service allows you to connect to a DSS node anywhere on the network to use the robot and camera. It connects to the first Differential Drive service that it finds, and the first WebCam. This should rarely be a problem because you are unlikely to be trying to teleoperate two robots at the same time!

Figure 4-3 shows a screenshot of the TeleOperation service (left). And look, there's R2-D2!

Figure 4-3

Figure 4.3. Figure 4-3

The TeleOperation control window is similar to the Dashboard, which is also included in the Chapter4 folder. You can use a game controller or joystick, or use the mouse with the onscreen "trackball." You can also use the arrow keys on the keyboard to drive the robot around. The arrow buttons on the left WinForm in Figure 4-3 use the DriveDistance and RotateDegrees operations, so the robot only moves a defined distance. However, you can uncheck the Fixed Moves checkbox and then the arrow buttons just turn the motors on as appropriate and you have to use the Stop button to stop the robot. This is similar to Robotics Tutorial 4, called "Drive by Wire." This option is provided because some robot services do not implement the DriveDistance and RotateDegrees requests.

The second WinForm (on the right in Figure 4-3) is the WebCam View, which shows live video from the camera mounted on the robot. Setting up the camera can be a little bit of a compromise. It needs to be tilted down to see the floor immediately in front of the robot, but this can limit how far it can see into the distance.

Hardware Setup

Your particular hardware might be quite different from the hardware used during the development of this chapter. For example, a Corobot has a web camera mounted on the front as a standard component, but you might have to jury-rig a camera onto your robot somehow. Regardless of your hardware setup, the key point is that the robot must have a Differential Drive service and the camera must be supported by the Webcam or IP Camera services.

The wireless camera used for testing in this chapter is a Swann Microcam with a Belkin USB Video Capture Device (VCD) to grab frames from the camera. The camera transmits in the 2.4GHz range. This is the same unregulated frequency band that is used by WiFi and Bluetooth. Consequently, there is a little interference between the camera and Bluetooth, even though Bluetooth uses frequency hopping.

Bluetooth causes streaks to appear in the video; but more important, the wireless camera causes some Bluetooth packets sent to or from the robot to be lost or corrupted. If the robot's communication protocol is not robust, then the service might hang waiting on packets that will never arrive because of "collisions." Even without direct interference, the presence of another transmitter close to the Bluetooth module on the robot "desensitizes" the Bluetooth receiver.

The Boe-Bot and LEGO NXT (shown in Figure 4-2) appear to operate OK with the Swann wireless camera attached. From time to time they miss a command, but they recover. However, the Stinger robot (in Figure 4-1) has some problems. The Stinger Serializer service hangs sometimes. This is being investigated.

The Surveyor SRV-1 has an onboard camera, but the video does not work with the TeleOperation service using the MRDS services available from Surveyor at the time of writing (version 061216) because these services do not implement the generic Webcam contract. However, a modified version of the camera service available from the book's website does implement the Webcam contract. The authors are working on services for the new Blackfin version of the SRV-1 that uses WiFi for communications and will therefore be much faster.

In addition, the SRV-1 Differential Drive service suffers from a flooding problem that makes using a gamepad almost impossible because the robot continues to move long after you release the joystick. You can drive it by carefully using the arrow keys on the keyboard. This problem has been addressed by a new Differential Drive service that is also available from the book's website (www.proMRDS.com). Unfortunately, this uncovered a different problem, which is that the robot is "deaf" while it is transmitting an image. Keep watching the book's website for updates.

Creating the Service

By now, you should have a good idea of how to create a service, so the following instructions are very brief. In addition, the TeleOperation service contains a lot of code, so some code is not covered in the book.

You should open the TeleOperation service in Visual Studio and look at the code as you follow through the text. The completed service is available in the ProMRDSChapter4 folder. The Dashboard service is also included in the Chapter4 folder, but it is not discussed here.

Quite a lot of references are used by the TeleOperation service. Rather than introduce them as they are needed, the following is a complete list. Remember that whenever you add a reference, look in the properties for the reference and change the Copy Local and Specific Version properties to false. If you forget to do this you might have problems with versioning.

  • RoboticsCommon.Proxy: This is almost always required for robot services, so you should get in the habit of adding it whenever you create a new service. Note that this reference is to the Proxy.

  • Ccr.Adapters.WinForms and System.Windows.Forms: These are required to use a Windows Form in your service, which is discussed in detail later in the chapter.

  • GameController.Y2006.M09.Proxy: This enables you to use a joystick or gamepad to drive the robot.

  • System.Drawing: This is required so you can manipulate bitmaps from the Webcam.

You do not need to add references to the Differential Drive or Webcam services because they are included in RoboticsCommon. The code establishes partnerships with these services dynamically.

At the top of the main source file, TeleOperation.cs, are appropriate using statements:

// Added references
// For Forms
using Microsoft.Ccr.Adapters.WinForms;
using System.Windows.Forms;
// For handling webcam images
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
// Game Controller
using game = Microsoft.Robotics.Services.GameController.Proxy;
// Generic Differential Drive
using drive = Microsoft.Robotics.Services.Drive.Proxy;
// Webcam
using webcam = Microsoft.Robotics.Services.WebCam.Proxy;

// For locating services in the Directory
using ds = Microsoft.Dss.Services.Directory;

// For HttpGet
using Microsoft.Dss.Core.DsspHttp;
// For HttpStatusCode
using System.Net;
// Specifically for HttpPost handling
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using Microsoft.Dss.Core.DsspHttpUtilities;

Knowing which references you need is something that comes with experience. Alternatively, you can look at other similar services and see what they use. In particular, you can use the tool tips (IntelliSense) in another service to find out where a certain data type comes from.

For example, the TeleOperation Windows Form (DriveControl.cs) also requires the following statement so that it can draw the "trackball":

using System.Drawing.Drawing2D;

Because TeleOperation is based on the Dashboard, you could figure this out by looking at the Dashboard code.

Some of the using statements that are automatically added when you create a new Windows Form are not required. However, they do no harm so you can leave them there. In any case, using statements are merely a convenience. Provided that you know which assembly a particular class is in (and you have added an appropriate reference), you can always specify its full name.

Throughout the rest of the chapter, various message types are mentioned. The Start method sets up the necessary receivers in the main interleave. The code is reviewed here in full, rather than line by line scattered throughout the chapter:

// Hook up all of the Form events
            MainPortInterleave.CombineWith(Arbiter.Interleave(
                new TeardownReceiverGroup
                (
                ),
                new ExclusiveReceiverGroup
                (
                    // Form handling
                    Arbiter.ReceiveWithIterator<OnLoad>(true, _eventsPort, 
Creating the Service
OnDriveControlLoadHandler), Arbiter.Receive<OnClosed>(true, _eventsPort,
Creating the Service
OnDriveControlClosedHandler), Arbiter.ReceiveWithIterator<OnLoad>(true,
Creating the Service
webCamEventsPort, OnWebCamFormLoadHandler), Arbiter.Receive<OnClosed>(true, _webCamEventsPort,
Creating the Service
OnWebCamFormClosedHandler), // Connection request Arbiter.ReceiveWithIterator<OnConnect>(true, _eventsPort, OnConnectHandler) ), new ConcurrentReceiverGroup ( // Game Controller-Added later // Drive notifications—Not currently used // Movement commands Arbiter.ReceiveWithIterator<OnMove>(true,
Creating the Service
_webCamEventsPort, OnMoveHandler), Arbiter.ReceiveWithIterator<OnMove>(true, _eventsPort,
Creating the Service
OnMoveHandler), Arbiter.ReceiveWithIterator<OnMotionCommand>(true,
Creating the Service
_eventsPort, OnMotionCommandHandler), // WebCam—Be careful because this can lock up if Exclusive Arbiter.ReceiveWithIterator<webcam.UpdateFrame>(true,
Creating the Service
_webCamNotify, CameraUpdateFrameHandler) ) ));

Briefly, the _eventsPort and the _webCamEventsPort are used for messages from the main Form (called DriveControl) and the Webcam Form, respectively. General Windows Form messages are OnLoad and OnClosed. The messages types OnMove and OnMotionCommand are used to control the robot. UpdateFrame messages come from the web camera when new frames are ready for retrieval.

Setting up these receivers ahead of time has no adverse effects. If no messages are ever sent to a particular port, then a receiver is wasted, but it does no harm to the service.

The handlers for the game controller are set up separately once a subscription is successfully established, as discussed in the next section.

Subscribing and Notifying

A common requirement in robotics is to "listen" for updates to sensor information. In MRDS you do this by subscribing to a service. The service then sends you notification messages, either at regular intervals or only when something changes.

The alternative to subscribing is to poll the sensors by periodically sending Get requests to the service that controls the robot (probably the "Brick" service). This is an inefficient approach because you might find that nothing has changed, especially if you are polling faster than new data arrives.

To summarize, whenever you want to get data, you have two options:

  • Send a request to the robot and wait for the data to be sent back.

  • Let another service get the data and tell you when it is available.

The remainder of this section discusses the second option—using subscriptions, i.e., letting somebody else do the polling for you.

Subscribing to State Changes

Subscribing to a partner service is quite simple. Partners that offer a subscription service must implement the Subscribe operation, so you simply send a Subscribe message. This is one of the fundamental message types defined in the DSSP service model.

The steps to subscribe to a partner service are:

  1. Set up a partnership, which can be done declaratively or dynamically in the code.

  2. Send a Subscribe message with appropriate parameters.

  3. Set up receivers and the corresponding handlers for notification messages.

Each of these steps is explained in more detail in the rest of this section using the Game Controller service as an example.

Consider the following code fragment from the TeleOperation service, which establishes a partnership with a game controller. As usual, you declare the partnership using an attribute and create a port to access the Game Controller service. (Remember that game is an alias declared in one of the using statements above).

[Partner("GameController", Contract = game.Contract.Identifier,
    CreationPolicy = PartnerCreationPolicy.UseExistingOrCreate)]
game.GameControllerOperations _gameControllerPort =
    new game.GameControllerOperations();
game.GameControllerOperations _gameControllerNotify =
    new game.GameControllerOperations();

This code is usually added at the top of your service class. It establishes a partnership that must always exist (because the policy is UseExistingOrCreate), although if there are no game controllers then the partner service is not very useful. Notice here that a second port is also created to receive notification messages—_gameControllerPort is for sending and _gameControllerNotify is for receiving. Later in the code you can subscribe to the Game Controller service by posting a Subscribe message. You also need to set up a receiver to handle the incoming notification messages.

The following two statements basically show what is required to subscribe to the Game Controller service. However, the actual code is a little more sophisticated. (You can see the full subscription process in the SubscribeToGameController method shown later.)

_gameControllerPortPort.Subscribe(_gameControllerNotify);
Activate(Arbiter.Receive<game.Replace>(true, _gameControllerNotify,
            GameReplaceHandler));

This is the absolute minimum amount of code required to set up a subscription, and it is not well written—you should check for a successful subscription by looking at the response message from the Subscribe; and you should not create an entirely separate receiver because it does not participate in the main interleave. Look at SubscribeToGameController below.

Referring back to the partner declaration in the code again, there is a third port:

Port<Shutdown> _gameControllerShutdown = new Port<Shutdown>();

This port is used for unsubscribing, but it is optional. (Unsubscribing is covered in the next section.) If you want to be able to unsubscribe, then you must specify a valid shutdown port in the NotificationShutdownPort property of the Subscribe message. To unsubscribe later, you send a Shutdown message to this shutdown port, hence the need for yet another port.

Lastly, the Game Controller service can send three types of notifications, not just one, and you need to set up receivers for each of them.

Taking all of this into account, the subscription code can now be rewritten in a more robust way:

private IEnumerator<ITask> SubscribeToGameController()
         {
            bool success = false;

            LogInfo("Subscribing to Game Controller");

            // Create a subscription message to subscribe to the Game 
Subscribing to State Changes
Controller service game.Subscribe msg = new game.Subscribe(); msg.NotificationPort = _gameControllerNotify; // Specify a Shutdown port so we can unsubscribe later msg.NotificationShutdownPort = _gameControllerShutdown; // Post the message _gameControllerPort.Post(msg); // Wait for a response yield return Arbiter.Choice( msg.ResponsePort, delegate(SubscribeResponseType response) { success = true; }, delegate(Fault fault) { LogError(fault); success = false; } ); if (!success) yield break; // Subscription failed // Add receivers to the main interleave for each of the possible // notification messages from the game controller. // If there are no game controllers, then there will be no messages! MainPortInterleave.CombineWith(new Interleave( new ExclusiveReceiverGroup(), new ConcurrentReceiverGroup ( Arbiter.ReceiveWithIterator<game.Replace>(true, _gameControllerNotify, GameReplaceHandler), Arbiter.ReceiveWithIterator<game.UpdateAxes>(true, _gameControllerNotify, GameUpdateAxesHandler), Arbiter.ReceiveWithIterator<game.UpdateButtons>(true, _gameControllerNotify, GameUpdateButtonsHandler) ) )); LogInfo("Game Controller subscription successful"); }

As a matter of interest, a subscription to the Game Controller service always succeeds regardless of whether you have a gamepad or joystick connected to your PC. However, if you try to enumerate the game controllers, you will not find any. TeleOperation doesn't enumerate the controllers because the Game Controller service automatically selects the first controller (if there is one). The TeleOperation service does not give you any way to select a game controller if you have more than one.

Three receivers are set up here and merged with the Concurrent receiver group of the main interleave. (You need to write three iterator handlers for each of the different types of notification messages).

Most services just send a Replace message as a notification, but it is also possible to define additional messages that send subsets of the service state. These are based on the Update message type. For example, the game controller can send changes to the button states separately from the axes (movement of the joystick). However, the settings of the buttons are also included in a Replace message (which contains the whole of the game controller state).

In addition to a Subscribe operation, some services offer a ReliableSubscribe operation. With a normal subscription, if the receiving service dies or is dropped, the sender just continues to send notifications. With a reliable subscription, however, the sender stops sending notifications if the receiver becomes unreachable. The subscription is added to a suspended list, and every so often an attempt is made to ping the receiver. If the receiver comes back to life (perhaps it was temporarily overloaded), then notification messages resume.

Because of this additional feature, there is a parameter called suspensionInterval that can be specified using ReliableSubscribe. You won't always need reliable subscriptions, so don't just use it because it is there. The following example shows how to use ReliableSubscribe:

// Subscribe to the drive
_driveShutdown = new Port<Shutdown>();
drive.ReliableSubscribe subscribe = new drive.ReliableSubscribe(
    new ReliableSubscribeRequestType(10)
);
subscribe.NotificationPort = _driveNotify;
subscribe.NotificationShutdownPort = _driveShutdown;

_drivePort.Post(subscribe);

yield return Arbiter.Choice(
    subscribe.ResponsePort,
    delegate(SubscribeResponseType response)
    {
        LogInfo("Subscribed to " + service);
    },
    delegate(Fault fault)
    {
        _driveShutdown = null;
        LogError(fault);
    }
);

Unsubscribing from State Change Notifications

It is always a good idea to unsubscribe from your partners in your service's Drop handler. Otherwise, your service might not be able to shut down cleanly. In addition, if you are dynamically connecting to services and disconnecting again, then you need to be able to unsubscribe. (Otherwise, you might have the strange situation where you are no longer talking to a particular service but it is still talking to you! There is no actual connection between services, just messages traveling back and forth.)

The following code fragment unsubscribes from the Webcam service (if one is in use):

// Already connected?
if (_webCamPort != null)
{
    // Unsubscribe
    if (_webCamShutdown != null)
        yield return PerformShutdown(ref _webCamShutdown);
}

Because unsubscribing is done from several places in the code, a function is defined in the TeleOperation service to do this:

Choice PerformShutdown(ref Port<Shutdown> port)
{
     Shutdown shutdown = new Shutdown();
     port.Post(shutdown);
     port = null;

     return Arbiter.Choice(
         shutdown.ResultPort,
         delegate(SuccessResult success) { },
         delegate(Exception e)
         {
             LogError(e);
         }
     );
}

That's all there is to unsubscribing—just send a Shutdown message to the port you supplied when you subscribed. Of course, you should wait for the response to ensure that the unsubscribe has completed.

Building in Support for Subscriptions and Notifications

The TeleOperation service has no need to handle subscriptions from other partners, so an example is required from elsewhere in the book code. The following code snippets are from BSBumper.cs, which is in Chapter 14. It implements the "bumpers" for the Boe-Bot, and consists of two infrared sensors (which only register on and off) and two "whiskers."

Microsoft provides a Subscription Manager Service as part of MRDS. This makes it easy to handle subscriptions because you don't have to keep track of all the services that have subscribed to your service or worry about how to send notification messages to all of them. The following discussion outlines the steps that a service must follow in order to accept subscriptions:

  1. You need a using statement to simplify access to the Subscription Manager:

    using submgr = Microsoft.Dss.Services.SubscriptionManager;
  2. Add a Subscription Manager partner at the top of your service class:

    [Partner("SubMgr", Contract=submgr.Contract.Identifier,
        CreationPolicy=PartnerCreationPolicy.CreateAlways, Optional=false)]
    private submgr.SubscriptionManagerPort _subMgrPort =
        new submgr.SubscriptionManagerPort();
  3. You need a handler for Subscribe messages:

    /// <summary>
    /// Subscribe Handler
    /// </summary>
    /// <param name="subscribe"></param>
    /// <returns></returns>
    [ServiceHandler(ServiceHandlerBehavior.Exclusive)]
    public virtual void SubscribeHandler(bumper.Subscribe subscribe)
    {
        base.SubscribeHelper(_subMgrPort, subscribe.Body,
                subscribe.ResponsePort);

    The SubscribeHelper method takes care of the subscription process for you. Any number of other services can subscribe to your service, but you do not need to keep track of them.

  4. It is a good idea at this stage to immediately send a notification message to the new subscriber. This initializes its state. Otherwise, it might have to wait a while before the first notification message.

    foreach (bumper.ContactSensor bumper in _state.Sensors)
         {
         SendNotification<bumper.Update>(_subMgrPort,
                 subscribe.Body.Subscriber,
                 new bumper.Update(bumper));
         }
    }

    This overload of SendNotification specifies a particular subscriber, rather than sending to all subscribers. This example is a little complicated because the individual bumpers in the contact sensor array are sent one at a time. In general, you would send the entire state using a Replace message.

  5. In order for the SubscribeHandler to be called, you must add the Subscribe type to your main operations port. The message type in this case is bumper.Subscribe because this service implements the generic Contact Sensor Array service. When you implement a generic service, you use the operations that are defined in the generic contract. Consequently, the Subscribe message type is already declared. If you look carefully at the top of the service class declaration, you will see that it implements an alternate contract:

    [Contract(Contract.Identifier)]
        [AlternateContract(bumper.Contract.Identifier)]
        [DisplayName("Boe-Bot Generic Contact Sensor")]
        [Description("Provides access to the Parallax BASIC Stamp 2 Boe-Bot 
    Building in Support for Subscriptions and Notifications
    infrared sensor used as a bumper. (Uses Generic Contact Sensors contract.)")] public class BumperService: DsspServiceBase

    Generic contracts are discussed toward the end of this chapter in "Inheriting from Abstract Services."

  6. You can optionally define a ReliableSubscribe handler as well:

    /// <summary>
            /// ReliableSubscribe Handler
            /// </summary>
            /// <param name="subscribe"></param>
            /// <returns></returns>
            [ServiceHandler(ServiceHandlerBehavior.Exclusive)]
            public virtual void ReliableSubscribeHandler(
                    bumper.ReliableSubscribe subscribe)
            {
                base.SubscribeHelper(_subMgrPort, subscribe.Body,
                        subscribe.ResponsePort);
                foreach (bumper.ContactSensor bumper in _state.Sensors)
                {
                    SendNotification<bumper.Update>(_subMgrPort,
    subscribe.Body.Subscriber, new bumper.Update(bumper));
                }
            }

    Although this looks identical to the Subscribe handler, note that the message type is different and the SubscribeHelper acts accordingly. Because the code looks the same at a quick glance, it is easy to overlook the difference between the two operations.

The preceding steps cover the process for supporting subscriptions in the Bumper service. However, the updates to the sensor information have to come from somewhere. The Bumper service subscribes to the BASICStamp2 "Brick" service (the Boe-Bot's brain) for SensorsChanged messages. There is a partner declaration at the top of the code, and a method called SubscribeToBasicStamp2 does the subscribing. The process is similar to the game controller described earlier, so it is not repeated here.

When a notification arrives from the Boe-Bot brick, the Bumper service checks whether any of the sensors have changed since the last update; if so, it issues a notification to all of its subscribers. The last step in the SensorsChangedHander is as follows:

if (changed)
    this.SendNotification<bumper.Update>(_subMgrPort,
        new bumper.Update(bumper));

That's it. The Subscription Manager sends a bumper.Update message to all the subscribers (or does nothing if no other services have subscribed).

Make sure that you don't flood your partners with notification messages. Always check incoming data to see if anything has changed. If there are no changes, don't send a notification! This is particularly important for the Game Controller service, for example. Imagine that you let go of the joystick and it springs back to the (0,0) position. If the Game Controller service kept sending updates with axis values of (0,0), then you would not be able to use the buttons on the TeleOperation Form because the game controller would continually override them and stop the robot. Therefore, the game controller only sends notifications when you move the joystick.

If you have implemented a Replace message handler in your service, then you must modify it to send new state information to all of the subscribers using SendNotification. Replace messages are discussed in Chapter 3. The code is not shown here, but you can look at it in Visual Studio.

If your service state is quite large, and especially if it can be broken into logical subgroupings, then consider having more than one type of notification message. This Boe-Bot example is trivial, but it is conceivable that there could be a message type for only the IR sensors and a different message type for just the whiskers. A subscriber might choose to listen only to the IR data, and ignore messages about the whiskers. In fact, the updated firmware from Parallax for use with a SpinStamp microcontroller stops the motors whenever a whisker is pressed. The MRDS service has no say in the matter because this happens aboard the robot.

User Interfaces

Although a primary objective of the robotics field is to create autonomous robots, almost all robots have to interact with humans. Therefore, user interfaces are an essential element of the equation. You have two different approaches available to you for creating user interfaces for MRDS services:

  • Windows Forms (WinForms)

  • Web Forms

Which approach you take depends a lot on the amount of user interaction that is required. In general, more complex or frequently used interfaces are best written using Windows Forms. However, a Windows Form will only be visible on the local computer that is running the DSS node. If you want to allow users to make changes to service parameters remotely, then you have to use a Web Form. You could write yet another service that displays a WinForm and run it as a client on another computer to talk to the main service, but this is getting ridiculous—where do you draw the line? In any case, this would require users to have MRDS installed on their computer instead of just a web browser.

In the example for this chapter, both types of interface are used. However, as you will see when you use it, the TeleOperation service would probably be easier to use if the option settings were in a Windows Form, rather than a Web Form. For comparison, the Dashboard service, also included with this chapter, uses a Windows Form for option settings.

In terms of "best practice," it is a good idea to implement a web page to display the service state, i.e., a HttpGet operation using an XSLT transform. This makes the state information much easier to read, and it looks more professional. Whether you decide to implement a Web Form, i.e., support for the HttpPost operation, to allow users to update fields in the state is a different issue. Once you have worked through the next few sections you will be able to make an informed decision based on your users' needs and your own programming skills.

Using Windows Forms

This section assumes that you are familiar with Windows Forms (or WinForms, for short) in the same way that it is assumed you are already a C# programmer.

The MRDS Robotics Tutorial 4 (Drive-By-Wire) uses a simple form with four buttons to control a robot. The TeleOperation service in this chapter is much more sophisticated than this. However, you should read Robotics Tutorial 4 in conjunction with this section of the book.

If you plan to use a Windows Form on a CF (Compact Framework) device, e.g. a PDA, then you should read Chapter 16. There are some considerations that are specific to the CF environment. The details are omitted here in order to keep the discussion as simple as possible at this stage. A slimmed-down version of TeleOperation, called Drive-By-Wire, is provided with the code for this chapter, which includes a CF version (not discussed here).

To see how the WinForms work, follow these steps:

  1. Start the TeleOperation service in the debugger. It takes a little while because the default manifest is set up to start the simulator.

  2. Select localhost as the node name and 50001 as the port number. (These values are stored in the config file, so they should already appear in the window.)

  3. Click the Connect button. You should see another window appear with the view from the robot's camera, as shown in Figure 4-4. You can move the two windows around independently. The service was deliberately designed to use a second WinForm for the camera view so that you can still use TeleOperation when the robot has no camera.

    Figure 4-4

    Figure 4.4. Figure 4-4

    If you don't see the simulated camera view in the webcam window, make sure that you have installed the V1.5 Refresh.

  4. You can drive the robot around using the arrow buttons on the main form, the arrow keys on the keyboard, a gamepad, or a joystick. If you close the WebCam View window, you can reopen it by clicking the Connect button again. If you close the TeleOperation window, then the service should shut down and take the DSS node with it. However, the DSS node sometimes doesn't shut down for reasons that are not apparent.

How WinForms Work under MRDS

A quick overview of how Windows Forms work with DSS services is appropriate at this point. In a normal Windows Forms application, all of the relevant code is often included directly in the form. However, for use with MRDS, "control" of the service is done in the main service implementation code and not the form.

Windows Forms operate off the main Win32 message queue for the application. They handle events such as MouseMove, ButtonClick, KeyDown, and so on. These events need to be sent back to the main DSS service (possibly after some pre-processing, or perhaps not at all if they only affect the internal state of the form).

Therefore, a port is created by the main service to allow the WinForm to send messages back to the main service. The message types have to be defined in the same way as they would for any service. In this sense, the WinForm acts something like an internal partner service, but it does not have a Proxy.

When it is necessary to execute some code in the context of the WinForm, the main service has to use an approach that is similar to PlatformInvoke for calling unmanaged code. This is done by sending a FormInvoke message to the WinFormsServicePort specifying a delegate to execute.

Windows Forms are in a sense "legacy" code. (Eventually, WinForms might disappear and be replaced by the new Windows Presentation Foundation, WPF. However, as of V1.5 of MRDS the WPF is not supported.) WinForms run in a Single-Threaded Apartment model and do not fit nicely into the multi-threaded model of the CCR. Because WinForms have thread affinity—i.e., they store state information into the thread's local store—they require special treatment. Therefore, Microsoft defined a WinFormsServicePort in the DsspServiceBase class that is used to control Windows Forms.

Following is a summary of the steps for adding a WinForm to your service (steps 2-4 are similar to setting up a new service):

  1. Create a new Form in your service project.

  2. Define the request messages that the Form can send and a PortSet containing these message types. These messages usually correspond to each of the event handlers in the Form. You can place these message classes in the Form source file if they are declared as public, or add them to the ServiceTypes.cs file for the service.

  3. Define a port in the main service (using the Form's PortSet) to receive the messages from the Form.

  4. Write handlers for each of the Form message types and add appropriate receivers to the main interleave.

  5. Modify the Form's constructor to accept a port as a parameter and create a variable to store it in.

  6. Edit the Form source code to add appropriate public properties and methods to enable information to be passed back to the Form from the main service.

  7. Update the Start method of the service to post a RunForm message to the WinFormsServicePort. This creates a new instance of the Form. The Form creation code should pass the Form port to the Form constructor. Save the handle (pointer) to the new Form instance so that you can access it later.

  8. To send messages from the Form to the service, post them to the port that was supplied to the constructor.

  9. To send information from the service to the Form, either copy data into public variables inside the Form or post a FormInvoke message to the WinFormsServicePort so that a delegate can execute public methods inside the Form using the Form handle.

This might seem like a daunting task, but once you get the hang of it you will find that it is not that complicated. These steps are explained in more detail in the following sections.

Creating a WinForm

Creating a Windows Form and then setting it up to interact with your main service is a fairly involved process. However, once you have done it a couple of times you should not have any trouble.

As you saw in the last section, you need to add a reference to System.Windows.Forms to your project, and a using statement at the top of your code.

You create a new Windows Form for your service in exactly the same way as you do for any Visual Studio project, i.e., click Project Add Windows Form. Figure 4-5 shows the Add New Item dialog for adding the DriveControl Form to the TeleOperation project.

Figure 4-5

Figure 4.5. Figure 4-5

The TeleOperation service has two forms, each of which is associated with a source file where you place the event handlers:

  • DriveControl: This enables you to control the robot. The DriveControl Form is discussed in this section. It is the window on the left in Figure 4-4.

  • WebCamForm: This displays the live video feed. This is covered in the section on using web cameras. It is the window on the right in Figure 4-4.

For the DriveControl Form, then, at the top of the main service, you declare a variable to hold a handle (a reference to the class instance) for the Form, _driveControl, as well as a port for messages from the Form, _eventsPort:

// Handle to the main WinForm UI
DriveControl _driveControl;
// Port for the UI to send messages back to here (main service)
DriveControlEvents _eventsPort = new DriveControlEvents();

The DriveControlEvents class is discussed in the next section and is defined in DriveControl.cs. For now, you only need to know that it is a PortSet that enables the DriveControl Form to send information back to the main service.

There is nothing magical about these classes or variables. You can name them whatever you like, and you can change the messages that are accepted by the DriveControlEvents PortSet. This is just one example of how to implement communication between a service and a WinForm. Other samples in MRDS use different approaches. However, the authors believe this is a consistent and manageable approach.

In the Start method, you have to create a new instance of the DriveControl Form. (The WebCamForm is created only after a Webcam service has been found). This is done as follows:

// Create the WinForm UI
WinFormsServicePort.Post(new RunForm(CreateForm));

The RunForm message specifies a delegate to execute to create the new Form. This delegate should return a handle to the Form as follows:

System.Windows.Forms.Form CreateForm()
{
     // NOTE: Modify the constructor in the Form to pass these parameters
     return new DriveControl(_eventsPort, _state);
}

This routine is trivial and could be specified as an anonymous delegate in the RunForm message instead of as a separate method. If you are an experienced Windows Forms programmer, you might have noticed that the code does not save the handle to the new Form instance. It can be done here, but for illustrative purposes it is done later.

Note that when the DriveControl Form is created it is passed two parameters. Normally the constructor for a WinForm does not take any parameters. You must modify the automatically generated code for the Form to accept these parameters (in DriveControl.cs):

public partial class DriveControl: Form
{
    // This port is passed across to the constructor
    // It allows messages to be sent back to the main service
    DriveControlEvents _eventsPort;
    // This is part of the main service State
    GUIOptions options;
public DriveControl(DriveControlEvents EventsPort,
            TeleOperationState state)
    {
        InitializeComponent();

        // Remember the port to use to send back messages
        _eventsPort = EventsPort;

        // Copy the option settings
        options = state.Options;

All the constructor needs to do is copy the parameters to variables inside the Form. Other code within the Form can then use these variables as necessary. Clearly, the EventsPort is required so that the Form can post back messages to the main service. The reason for passing across the service state is not so obvious—it enables the Form to access the option settings that are stored in the config file.

You have not entirely finished with Form creation because you still need the Form handle. This last step is covered in the next section.

Passing Information Between a WinForm and a Service

Information needs to be passed in both directions between the main service and the WinForm. This section discusses how to pass information in each of these directions and the different approaches required:

  • Sending to the Form: A WinForm is a separate module, not a service in its own right. Because it operates as a Single-Threaded Apartment model, it cannot wait on CCR ports to receive messages. However, the main service needs to update information on the Form in response to notification messages such as game controller updates. Sending information from the main service to the Form is done using FormInvoke.

  • Receiving from the Form: The Form needs to pass back commands to the main service. When you interact with the Form, events fire inside the Form code. These WinForm events are not related to the CCR in any way, but the event handlers in the Form can send CCR messages back to the main service by posting to the _eventsPort, which was created especially for this purpose in the code in the previous section.

What FormInvoke does is execute code from the main service in the context of a WinForm. It does this by posting a message to the WinFormsServicePort, which means that the delegate executes asynchronously with respect to the caller. In other words, any code that follows a FormInvoke in the same routine cannot assume that the FormInvoke has completed execution.

A good example is the handler for axes updates from the game controller. This handler sends the new axes information to the DriveControl Form so that the onscreen trackball and the associated X, Y, and Z values can be updated:

IEnumerator<ITask> GameUpdateAxesHandler(game.UpdateAxes update)
{
      if (_driveControl != null)
      {
          WinFormsServicePort.FormInvoke(
delegate()
          {
               _driveControl.UpdateGameControllerAxes(update.Body);
          }
      );
  }
  yield break;
}

Note the following regarding the preceding code:

  • The code checks to make sure that the DriveControl Form is active and then calls FormInvoke on the WinFormsServicePort. The delegate takes advantage of its access to local variables in the handler to pass update.Body to the UpdateGameControllerAxes method in DriveControl.cs, i.e., inside the Form. Notice the use of _driveControl, which is a pointer to the Form instance, to execute a public method inside the Form.

  • UpdateGameControllerAxes is a public method in the Form code (DriveControl.cs). It is too involved to explain the details of how it works here. In short, it updates the current position of the trackball and then processes the new position in exactly the same way as when you use the mouse to move the trackball. This usually results in a new power setting for the drive motors, and UpdateGameControllerAxes posts an OnMove message back to the _eventsPort. The OnMove message is discussed later, but basically it causes the main service to send a SetDrivePower message to the differential drive.

If you only want to read data, you can also extract information from the Form by grabbing the values of public properties in the Form. (Technically, you can also write to public variables, but then you might have concurrency issues). Visual Studio creates all controls on a Form as private properties, so you cannot access the controls directly. However, you can declare your own public properties, and many properties at the Form level are public.

The following code snippet is from the handler that saves the state to a config file:

// Grab the current window location
if (_driveControl != null)
{
    _state.Options.WindowStartX = _driveControl.Location.X;
    _state.Options.WindowStartY = _driveControl.Location.Y;
}

The Location.X and Location.Y properties are the screen coordinates of the top-left corner of the window. This enables the window to be started at the same position the next time the service is run. Note that gathering data this way is an on-demand approach, rather than an event-driven approach.

The basic process for communication from the Form back to the main service is via the Form's event handlers, which generate messages that are posted to the _eventsPort. Some events in the Form might not result in a message being sent, but instead set internal variables inside the Form, so there is no one-to-one correspondence between events and messages sent to the _eventsPort.

Strictly speaking, the _eventsPort is not necessary. You could define the requests that the DriveControl Form requires as additional operations on the main operations port for the service. However, it makes sense to separate WinForm interactions from the operations that the service provides because you don't want to expose these operations via the service Proxy. It also helps to make the WinForm handling code reusable.

Windows Forms often have dozens of controls. If you defined a message type for each possible event that could occur on the Form, it would quickly get out of hand. A better approach is to define a general class to handle WinForms interaction and then subclass this as necessary for message types that are specific to particular Forms. It is also advisable to try to group messages together to share message types.

Look in TeleOperationTypes.cs in the General Form Operations region at the bottom of the file:

#region General Form Operations
// This is the base class for all Form event messages
public class FormEvent
{
    private Form _theForm;

    public Form Form
    {
        get { return _theForm; }
        set { _theForm = value; }
    }

    public FormEvent(Form form)
    {
        _theForm = form;
    }
}

public class OnLoad: FormEvent
{
    public OnLoad(Form form)
        : base(form)
    {
    }
}

public class OnClosed: FormEvent
{
    public OnClosed(Form form)
        : base(form)
    {
    }
}

#endregion

The class called FormEvent is the base class for any event information that is passed back from a WinForm to the main service. This base class contains a property that identifies the WinForm by its handle. In the TeleOperation example, there are two WinForms.

Two other subclasses are also defined: OnLoad and OnClosed. These events are common to all WinForms so it is sensible to define them here. In the code for a WinForm, you add event handlers similar to the following. Notice that these handlers simply post a message of the appropriate type to the _eventsPort for the WinForm:

private void DriveControl_Load(object sender, EventArgs e)
{
    _eventsPort.Post(new OnLoad(this));
}

private void DriveControl_FormClosed(object sender, FormClosedEventArgs e)
{
    _eventsPort.Post(new OnClosed(this));
}

Note

Do not insert these routines by typing in the code because they won't work unless you edit the Designer-generated code and manually hook them to the events. You should allow Visual Studio to create the empty methods for you because then they will be hooked up to the events properly. Once you have the empty routines, you can insert code inside them.

If you are not familiar with adding event handlers, follow these steps:

  1. Go to the Design View for the Form.

  2. Click the background of the Form to select the entire Form.

  3. In the Properties panel, shown in Figure 4-6, click the lightning bolt icon. This shows all the possible events that can be associated with the Form.

    Figure 4-6

    Figure 4.6. Figure 4-6

  4. If you double-click one of the event handler names in the Properties panel, an empty method is added to your WinForm code.

Figure 4-6 shows event handlers defined for FormClosed, KeyDown, KeyUp and Load. Note that KeyPress is not used. The key events are discussed in the next section.

For buttons and other controls on the Form, you don't need to go to this much trouble because you can simply double-click the control in the Design View and the default event handler is added automatically. However, the keypress events cannot be added this way because there are no controls to click for keypress.

The OnLoad handler in the main service for OnLoad messages from the DriveControl Form is as follows (there is a different OnLoad handler for the WebCamForm):

/// <summary>
/// On Load Handler for completion of Form Load
/// </summary>
/// <param name="onLoad"></param>
/// <returns></returns>
IEnumerator<ITask> OnDriveControlLoadHandler(OnLoad onLoad)
{
    // Save a handle to the form
    _driveControl = (DriveControl)onLoad.Form;

    LogInfo("Drive Control Form Loaded");

    // Subscribe to the joystick
    yield return Arbiter.ExecuteToCompletion(Environment.TaskQueue,
        Arbiter.FromIteratorHandler(SubscribeToGameController));
}

This handler saves the Form handle at last. It is not necessary to do this here—it could have been saved when the Form was initially created, but it is done here as an example.

Then the handler tries to subscribe to a game controller. Other initialization can also be included at this point, such as updating labels or textbox controls on the Form with initial information. The main point to note is that this handler is not executed until the OnLoad message has been received, which is an indication that the Form has been created and is running.

The OnClosed event fires if the user clicks the Close button in the Form's title bar. There is no need to provide an Exit button on the Form, but one has been included as a convenience. The Exit button handler simply closes the Form, which fires the OnClosed event anyway, and this causes an OnClosed message to be sent to the main service.

The OnClosed handler in the main service attempts to shut down by posting a Drop message to itself. (The Drop handler is not discussed here).

/// <summary>
/// Form Closed Handler for when Drive Control Form has closed
/// </summary>
/// <param name="onClosed"></param>
void OnDriveControlClosedHandler(OnClosed onClosed)
{
    if (onClosed.Form == _driveControl)
    {
        LogInfo("Main Form Closed");

        // Send a Drop message to ourselves
        _mainPort.Post(new DsspDefaultDrop(DropRequestType.Instance));
    }
}

Notice here that the code checks the handle in the message against the handle in the Form to ensure that the message has been received from the correct Form. This isn't actually necessary, but it illustrates the point that one handler can potentially handle multiple Forms by using the Form handle to identify the Form.

Keyboard Handling

If you refer back to Figure 4-6 you can see that there are event handlers for the KeyDown and KeyUp events. These handlers are used to enable you to drive the robot using the arrow keys on the keyboard. However, there are a couple of tricks involved in setting this up.

The keyboard handlers are in a region called Keyboard Handlers toward the bottom of the Form code, DriveControl.cs. Note that the event handlers were first created in the Properties panel by double-clicking the events, and then the code was added to each of the routines.

Start by looking at the KeyDown event handler:

#region Keyboard Handlers

// NOTE: The arrow keys will not normally appear in a KeyDown event.
// This is because they will be pre-processed. However, the operation
// under CF seems to be different and it does no harm to leave them
// in here anyway.

private void DriveControl_KeyDown(object sender, KeyEventArgs e)
{
    switch ((Keys)e.KeyValue)
    {
        case Keys.Up:
            Forward();
            e.Handled = true;
            break;

        case Keys.Down:
            Backward();
            e.Handled = true;
            break;
case Keys.Left:
            TurnLeft();
            e.Handled = true;
            break;

        case Keys.Right:
            TurnRight();
            e.Handled = true;
            break;

        default:
            break;
     }
  }

It turns out that this code is not responsible for the movements of the robot, even though it looks like it should be. It is called for keypresses in general, but not for the arrow keys. You could add other case statements to the switch to use alternative keys. For example, the A, S, D, and W keys are commonly used in first-person shooter games to move around.

Are you being misled? No. When you get to Chapter 16, you will find that Windows Mobile operates a little differently. In that case, the preceding code is called. The "arrow keys" on a PDA correspond to the "rocker switch" that is used to move the cursor around.

With desktop operating systems, you have to take a different approach to process the arrow keys. You first need to set the KeyPreview property on the Form to True. This indicates that you want to handle the "special" keys, rather than use the default handling. The arrow keys are normally used to move from one control to another, or to move side to side in a TextBox. By setting the KeyPreview flag, your code can take over.

Next, you must create a handler to override the default ProcessDialogKey method. Your handler can do whatever it likes with incoming keystrokes before they are passed on to the Form. If you wanted to be really nasty, you could throw away all "2" keystrokes and the poor users would think they had a broken key. Let's not do that.

ProcessDialogKey has to process the keys that it is interested in and return true to indicate that the keystrokes have been used up. Any keys that it is not interested in can just be passed to the original ProcessDialogKey handler, where they are processed as usual. In other words, you are intercepting keystrokes before they arrive at the Form:

// Overriding ProcessDialogKey allows us to trap the arrow keys
protected override bool ProcessDialogKey(Keys keyData)
{
    switch (keyData)
    {
        case Keys.Up:
            Forward();
            return true;

        case Keys.Down:
Backward();
            return true;

        case Keys.Left:
            TurnLeft();
            return true;

        case Keys.Right:
            TurnRight();
            return true;

        default:
            break;
    }

    return base.ProcessDialogKey(keyData);
}

Notice that the last step in the code is to pass the keyData back to the normal ProcessDialogKey handler and return whatever result it gives you. It is quite likely that the original ProcessDialogKey handler won't do anything with the keystroke (because it is not a special character), in which case it returns false and the keyData is sent to the Form for processing.

The functions that are called in response to each of the arrow keys are trivial, so only the Forward function is shown here:

void Forward()
{
    _eventsPort.Post(new OnMove(this,
      (int)options.MotionSpeed, (int)options.MotionSpeed));
}

It is worth noting here that MotionSpeed is an option setting. This is part of the service state, so it is saved in the config file. The Form doesn't have direct access to the service state, but the state is passed to the Form when it is created.

OnMove messages are processed in the main service by the following handler. As you would expect, it issues a SetDrivePower request to the differential drive:

/// <summary>
/// Handle basic moves (motor power settings)
/// </summary>
/// <param name="onMove"></param>
/// <returns></returns>
IEnumerator<ITask> OnMoveHandler(OnMove onMove)
{
    if (_drivePort != null)
    {
        // Create a drive request
        // There is a more concise syntax, but we need to access some
        // additional properties
drive.SetDrivePowerRequest request =
                new drive.SetDrivePowerRequest();
        request.LeftWheelPower =
                (double)onMove.Left * MOTOR_POWER_SCALE_FACTOR;
        request.RightWheelPower =
                (double)onMove.Right * MOTOR_POWER_SCALE_FACTOR;
        drive.SetDrivePower sdp = new drive.SetDrivePower(request);
        // Set a timeout so that this does not wait forever
        sdp.TimeSpan = TimeSpan.FromMilliseconds(1000);
        _drivePort.Post(sdp);

        yield return Arbiter.Choice(
                sdp.ResponsePort,
                delegate(DefaultUpdateResponseType response)
                {
                    //Console.WriteLine("Power updated");
                },
                delegate(Fault f)
                {
                    // Log an error (most probably a timeout)
                    LogError(f);
                    if (f.Code.Subcode.Value ==
                                DsspFaultCodes.ResponseTimeout)
                        Console.WriteLine("Timeout on Move");
                    else
                        Console.WriteLine(f.Detail);
                }
        );
    }
}

Another point to note about the SetDrivePower request is that a timeout is set on it. All DSS requests have a TimeSpan property. If you use a shorthand method for sending a request, rather than explicitly creating the request object, then you do not have the opportunity to set the TimeSpan. The code is therefore a little more involved.

It is important to have a timeout here so that the TeleOperation service doesn't hang. Some robot services are not well-behaved when something goes wrong with the robot. For example, try turning your robot off and see whether your service stops responding to requests.

The last point about this code is that the motor power settings from the Form are scaled by the MOTOR_POWER_SCALE_FACTOR, which is 0.001. This is because the Game Controller service sends axis values in the range −1000 to +1000. However, the SetDrivePower request must use values in the range −1.0 to +1.0.

At this point, the code gets the robot moving, but you also need to stop it. The simplest way to do this is to stop the robot as soon as the user releases the key. This means that as long as the arrow key is held down, the robot continues to move in that direction, which is intuitively easy to use.

The KeyUp event handler therefore calls Stop and is trivial:

private void DriveControl_KeyUp(object sender, KeyEventArgs e)
{
    Stop();
}

A KeyUp event occurs when any key is released. The handler for KeyUp immediately sends a Stop command without even determining which key was released.

If you "tap" a key, the robot might jerk a little, but it won't keep driving. You must hold a key down. Pounding on the keyboard will not achieve anything, and it certainly won't make the robot go faster.

While you are holding a key down, the keyboard automatically repeats the keystroke, typically a few times per second. These additional "keystrokes" simply execute the same KeyDown code, but they are not necessary for the process to work—the first KeyDown event starts the robot moving, and the key release stops it.

As noted above, if you are working with a PDA running Windows Mobile, only two event handlers (KeyDown and KeyUp) are required provided that you change the KeyPreview property for the Form to true. The ProcessDialogKey method is not required for Windows Mobile.

Button Handling

There are arrow buttons on the DriveControl Form for controlling the robot. The way that these work is actually quite simple: The event handlers for the buttons post messages to the main service, and the service posts appropriate messages to the differential drive.

Consider the event handler for the "Forward" button (the up arrow):

private void btnForward_Click(object sender, EventArgs e)
        {
            if (chkFixedMoves.Checked)
                _eventsPort.Post(new OnMotionCommand(this,
MOTION_COMMANDS.Translate, options.DriveDistance / 1000, options.MotionSpeed));
            else
                Forward();
        }

The code first checks whether the Fixed Moves checkbox is enabled or not. If it is, then DriveDistance and RotateDegrees are used to control the robot. These functions are handy because they only move the robot by a fixed amount and it can't run away from you. However, some robot services don't implement these functions, which would make the buttons useless.

Notice that there is another message type for the fixed motions called OnMotionCommand. The enum called MOTION_COMMANDS has three possible values: Translate, Rotate, and Stop. The second parameter to this request is a double that is either a distance or an angle (depending on the request type) and the last parameter is the drive power.

If the Fixed Moves checkbox is not clicked, the Forward method shown earlier is called. This just turns on the motors; you have to click the Stop button to stop the robot. In this mode, the buttons behave like they do in Robotics Tutorial 4, but they are much prettier buttons.

Using Web Forms

This section explains how to set up your service to handle HttpGet and HttpPost requests. Both of these require you to write an XSLT (Extensible Stylesheet Language Transformation) file, which is used to format the data. Because XSLT might be new to some readers, also included here is a brief explanation of how it works and how to set up your development environment.

Following that are examples demonstrating how to display service state on a web page and how to update service state using a Web Form. The final section discusses using JavaScript to do client-side validation of input data before it is submitted to update the state. This is not essential, but it makes your Web Form much more user-friendly.

As you should know by now, DssHost implements a web server, and you can examine the state of services using a web browser. You can also start and stop services via a browser. If you don't know how this works, read Chapter 3.

The TeleOperation service uses several option settings that are stored in its state. These can be changed by editing the config file prior to running the service. However, if you are trying to adjust the motor settings for your particular robot, it is very annoying to have to edit the config, run the service, shut down, edit the config, run the service, and so on.

There are two solutions to this problem: use a Windows Form or a Web Form. You can add another Windows Form to your service to update the configuration. This is how the Dashboard works. Although there are some extra steps involved in passing the configuration data between the Form and the main service, you have already learned how to create a Windows Form and exchange information between the service and the Form.

The alternative is to use a Web Form. This section explains the process. You should also look at the MRDS Service Tutorial 6.

Accessing Services from Web Pages

When you enter a URL that refers to a service into the address bar of a web browser, DssHost gets the state information from the service on your behalf and sends back a web page. You will see either some raw XML code or a nicely formatted web page.

If the service does not implement the HttpGet request type, then DssHost makes a Get request. If you have declared the HttpGet message type in your main operations port but you did not supply a handler, then the handler in the DsspServiceBase class is used instead.

Note

If you add a HttpGet handler to your code but forget to add the operation to the operations port, you get a run-time error something like the following:

*** Dssp Operation handler has been marked with the ServiceHandlerAttribute but
its operation type is not on the operations
port.Method:System.Collections.
Generic.IEnumerator`1[Microsoft.Ccr.Core.ITask] HttpGetHandler(Microsoft.Dss.Core.Dss
pHttp.HttpGet) [01/16/2008 17:42:56][http://koala:50000/teleoperation]

To see the difference between a formatted page and raw XML, follow these steps:

  1. Start the TeleOperation service in the debugger again, enter localhost and 50001, and click Connect. Note that TeleOperation connects to the TCP port, not the HTTP port (which is 50000).

  2. Open a web browser and enter the following URL: http://localhost:50000/teleoperation.

    This requests the current state of the TeleOperation service. Because the TeleOperation service has an XSLT (Extensible Stylesheet Language Transformation) file that defines how to display the data, you see a formatted web page, as shown in Figure 4-7.

    Figure 4-7

    Figure 4.7. Figure 4-7

    Figure 4-7 shows the values of all the state properties. It indicates that the service is connected and lists the full URIs of the Differential Drive and Webcam services that it is using. Although they are both simulated services, they implement the generic contracts so TeleOperation can connect to them.

    If you have investigated MRDS in a web browser before, then you should recognize that this page is in the standard format for MRDS web pages. This is because it uses the Master Page (template) that is provided by MRDS for consistency.

  3. Enter the following URL in the address bar of the web browser: http://localhost:50000/teleoperation/raw.

This displays the state as an XML file without using the XSLT file to format it. (It is the same as clicking the orange XML button in the top right-hand corner of the window in Figure 4-7). The XML code is as follows:

<?xml version="1.0" encoding="utf-8" ?>
<TeleOperationState
    xmlns:s="http://www.w3.org/2003/05/soap-envelope"
    xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing"
    xmlns:d="http://schemas.microsoft.com/xw/2004/10/dssp.html"
    xmlns="http://www.promrds.com/contracts/2008/01/teleoperation.html">
  <Host>localhost</Host>
  <Port>50001</Port>
  <Connected>true</Connected>
  <DriveService>dssp.tcp://koala:50001/simulateddifferentialdrive/ 
Figure 4-7
68d363c7-39a9-41b2-b90d-2e24a966015b/drive</DriveService> <WebCamService>dssp.tcp://koala:50001/simulatedwebcam/53f5c3a7-3f08-
Figure 4-7
4f64-800f-8edddeabe68a/webcamservice</WebCamService> <Options> <WindowStartX>78</WindowStartX> <WindowStartY>51</WindowStartY> <WebCamStartX>415</WebCamStartX> <WebCamStartY>11</WebCamStartY> <DeadZoneX>200</DeadZoneX> <DeadZoneY>200</DeadZoneY> <TranslateScaleFactor>0.75</TranslateScaleFactor> <RotateScaleFactor>0.5</RotateScaleFactor> <MotionSpeed>500</MotionSpeed> <DriveDistance>300</DriveDistance> <RotateAngle>45</RotateAngle> </Options> </TeleOperationState>

Which of these two versions, a formatted web page or raw XML, would you prefer to see as a user of the TeleOperation service? Obviously, a nicely formatted screen gives a more professional look to your service and is much easier to use.

Now that you have seen both the raw state information and a formatted web page, here is a summary of the steps for using a Web Form:

  1. Create an XSLT file that transforms the XML service state into a more readable HTML format.

  2. Add the XSLT file to your assembly as an embedded resource, or place it into the appropriate directory under MRDS.

  3. Add or modify the HttpGet handler in your service to use the transform. To simply display the state, you don't need to go any further.

  4. If you want to use a Web Form to update the state, add appropriate input tags to the XSLT file, and a Submit button.

  5. Add an HttpPost handler to your service and write code for it to process the input data and update the state (and maybe save the state too).

  6. Optionally, add JavaScript code to your XSLT file to do client-side validation before the data is submitted to the service.

If you are an experienced web developer, you should find this process easy to follow. If not, please keep reading.

XSLT Overview

If you don't have a background in web development, then you might find this section useful. If you are already an experienced web developer, skim through to the next section (and ignore the little "white lies").

As you know, data is transferred in messages between services using XML (Extensible Markup Language). This is a much stricter language than HTML (Hypertext Markup Language), which is used for writing web pages. In particular, XML is case sensitive. It is assumed that you have some basic familiarity with HTML.

An XSLT file defines how to take an XML file as input and reformat it to create a new output file. Note that the output does not have to be HTML, although this is usually the case. In theory, XSLT can be used to create a PDF file, a Word document, and so on. The definition of XSLT can be found on the World Wide Web Consortium website at www.w3.org/TR/xslt.

A lot of the code in an XSLT file is just HTML, which is destined to be displayed on the output page. Embedded in this are <xsl> elements that control what information from the XML data stream is displayed, such as the <xsl:value-of> element, which displays a value by name.

Visual Studio understands XSL syntax, so when you open an XSLT file you can use IntelliSense and the online help. For example, when you enter <xsl:, Visual Studio pops up a list of available XSL elements (as soon as you press the ":" key).

XSL includes logic and repetition elements, such as <xsl:if>, <xsl:choose>, and <xsl:for-each>. You can even create variables in XSL. Place the cursor over one of these commands in the editor in Visual Studio and press F1 to view the online help.

Setting Up Your Development Environment for XSLT

Before you start working with XSLT, you should follow the instructions provided in this section. They will help you to set up your environment, and they explain a little bit about how XSLT is used by MRDS.

  1. Start up DssHost with no manifest:

    C:Microsoft Robotics Studio (1.5)>dsshost /p:50000 /t:50001
  2. Open a web browser and browse to the following:

    http://localhost:50000/resources/dss/Microsoft.Dss.Runtime.Home.Template.xml

    You should see a message that says that this page is a template. Click the link to Template.xslt. The full URL is as follows:

    http://localhost:50000/resources/dss/Microsoft.Dss.Runtime.Home.Template.xslt
  3. Assuming that your copy of MRDS is installed on the C: drive, create a directory called C:Resources. Under this, create a directory called DSS. (Substitute the appropriate drive letter if your MRDS installation is on a different drive).

  4. Save the XSLT file from the web browser into this new directory. The path C:ResourcesDSS corresponds to the path in the URL. The entire template is shown here:

    <!—
    This file is a template for xslt files that use the default
    Microsoft Robotics Studio layout to represent a DSSP service state.
    -->
    <?xml version="1.0" encoding="utf-8" ?>
    <xsl:stylesheet
        version="1.0"
        xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
        xmlns:soap="http://www.w3.org/2003/05/soap-envelope"
        xmlns:dssp="http://schemas.microsoft.com/xw/2004/10/dssp.html"
        xmlns:svc="http://schemas.tempuri.org/app/2007/1/template.html"
        >
    
    <xsl:import href="/resources/dss/Microsoft.Dss.Runtime.Home.MasterPage.xslt" />
    
    <xsl:template match="/">
      <xsl:comment><!—Service Header Info—></xsl:comment>
      <xsl:variable name="title">
        Service Page Title
      </xsl:variable>
      <xsl:variable name="serviceName">
        Service Name
      </xsl:variable>
      <xsl:variable name="description">
        Service Description
      </xsl:variable>
    
      <xsl:call-template name="MasterPage">
        <xsl:with-param name="serviceName" select="$serviceName" />
        <xsl:with-param name="description" select="$description" />
        <!--If title is not provided, serviceName will be used instead.-->
        <xsl:with-param name="title">
          <xsl:value-of select="$serviceName" />
          <xsl:if test="$title != "">
            <xsl:text> - </xsl:text>
            <xsl:value-of select="$title" />
          </xsl:if>
        </xsl:with-param>
      <!-- Possible values for navigation are: 'Open', 'Closed', and 'None'
           'Open' is the default value. -->
    <xsl:with-param name="navigation" select="'Open'" />
          <!-- The contents of head param will be placed just before the </head> tag in html. -->
          <xsl:with-param name="head">
            <style type="text/css">
              /* Service-specific stylesheet goes here */
            </style>
            <script language="javascript" type="text/javascript">
              <![CDATA[<!—
    
    /* Service-specific script goes here */
    
    dssRuntime.init = function()
    {
       // Add page initialization code here.
       // This function is attached to the window.onload event.
       // Do not override window.onload.
    }
    
    //-->   ]]>
             </script>
           </xsl:with-param>
         </xsl:call-template>
       </xsl:template>
    
       <!-- Match service state's document element. -->
       <xsl:template match="/svc:Template">
         <xsl:comment><! --Service State Contents --></xsl:comment>
         <form name="DssForm" method="post">
           <xsl:copy-of select="." />
         </form>
       </xsl:template>
    
    </xsl:stylesheet>
  5. If you open this file in Visual Studio, you might see an error:

    Unexpected XML declaration. The XML declaration must be the first node in the
    document and no white space characters are allowed to appear before it.

    If you see this error, delete the comment lines at the top of the file just before the <?xml> line and save the file.

    Now when you want to create a new XSLT file for MRDS in Visual Studio, you can easily use this template. This is the approach that Microsoft recommends. Bear in mind that you might need to update this template whenever a new version of MRDS is released.

  6. Looking at the XML code, note the <xsl:import> element that refers to the following:

    http://localhost:50000/resources/dss/Microsoft.Dss.Runtime.Home.MasterPage.xslt

    Enter this URL into your web browser. Again, you should see an XML file. Save this file to the C:ResourcesDSS folder along with the template.

  7. This master page refers to yet another file:

    http://localhost:50000/resources/dss/Microsoft.Dss.Runtime.Home.Navigation.xslt

    Enter this URL and then save the XML file to C:ResourceDSS too.

By saving these files to your hard drive, you avoid some other error messages that appear if you try to edit an XSLT file. This step is not essential, but it gets rid of some annoying errors.

If you are interested, there are other embedded resources for MRDS that you can examine using a web browser:

http://localhost:50000/resources/dss/Microsoft.Dss.Runtime.Home.Styles.Common.css
http://localhost:50000/resources/dss/Microsoft.Dss.Runtime.Home.JavaScript
.Common.js

There are also some images that are not listed here, but you don't need them. Unfortunately, you cannot simply browse /resources/dss to view the available embedded resources. In order to see an embedded resource, you must know its full URI. Otherwise, DssHost presents you with a blank page.

Using XSLT for Enhanced Information Display

To use an XSLT file to display your service state you have two options:

  • Use an external file: Under the MRDS root directory is a store folder containing a transforms folder. You can place your XSLT files here if you wish. However, this makes deployment a little more complicated and it's possible for the XSLT file to become separated from the service. Conversely, it is much easier to edit an external XSLT file because you don't need the service source code and you don't have to recompile.

  • Use an embedded resource: Embedded resources are linked into the service assembly. When the service starts, these resources are "mounted" into the /resources pseudo-folder on the DSS node so that they are accessible via URIs. The advantages of this approach are that nobody can mess with your XSLT and it can never get lost because it is part of the service DLL. Conversely, it is more difficult to change the XSLT file and it requires recompilation of the service.

From here on, the examples use embedded resources because this is the authors' preferred approach. The format of the URI for embedded resources is as follows:

/resources/Assembly-Name/Default-Namespace.Path.Filename

You can open the project properties for your service and look on the Application tab to find the assembly name and the default namespace. If your XSLT file is in the same folder as the rest of your source files, then there is no Path. The Filename is just the name of the XSLT file. The URI is not case sensitive.

It is common practice to locate XSLT files in a Resources subfolder under your source directory. The TeleOperation service follows this convention, so the full URI for the XSLT file is as follows (ignoring the wrap-around):

/resources/TeleOperation.y2008.m01/ProMRDS.Robotics.TeleOperation.
Using XSLT for Enhanced Information Display
Resources.TeleOperation.xslt

Building an XSLT File from Scratch

To create a new XSLT file you have two options:

  • Create the entire XSLT file yourself: Building your own XSLT file gives you complete flexibility in the design. You can develop your own "look and feel," but it won't be compatible with the existing MRDS pages.

  • Use the MRDS master page and template: Using the MRDS template saves you some work and ensures that the layout and formatting of the page match the other MRDS pages.

Obviously, this section covers building the XSLT file from scratch; the next section discusses using the MRDS template. If you want to start with a blank file and embed it in your service assembly, proceed as follows:

  1. Click Visual Studio ? Project ? Add New Item and then select XSLT file from the dialog. The new XSLT file should appear in the Solution Explorer.

  2. Click on the new file in Solution Explorer and then look in the Properties panel. Change the Build Action from Content to Embedded Resource.

    This is an important step. If you don't set the Build Action to Embedded Resource, you won't be able to see your embedded XSLT file in a web browser.

  3. The XSLT file that you create can contain a complete web page, giving you total flexibility to format the page any way you want. The basic layout in this case is similar to the following example:

    <?xml version="1.0" encoding="UTF-8" ?>
    <xsl:stylesheet version="1.0"
            xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
            xmlns:svc="http://schemas.tempuri.org/yyyy/mm/servicename.html">
      <xsl:output method="html"/>
    
      <xsl:template match="/svc:ServiceState">
        <html>
          <head>
            <title>Service Name</title>
            <link rel="stylesheet" type="text/css" 
    Building an XSLT File from Scratch
    href="/resources/dss/Microsoft.Dss.Runtime.Home.Styles.Common.css" /> </head> <body style="margin:10px"> <h1>Service Name</h1> <table border="1"> <tr class="odd"> <th colspan="2">Service State</th> </tr> <tr class="even"> <th>Property 1:</th> <td> <xsl:value-of select="svc:property1"/> </td> </tr> <tr class="odd"> <th>Property 2:</th> <td>
    <xsl:value-of select="svc:property2"/>
                </td>
              </tr>
            </table>
          </body>
        </html>
      </xsl:template>
    </xsl:stylesheet>

The service contract identifier is listed as one of the xmlns (XML namespace) parameters at the top of the file using the alias svc so that you can easily refer to it throughout the rest of the file. In the <xsl:template> element, the match attribute specifies the ServiceState. This must be the actual class name of your service state. Remember that the XML file output by your service is a serialized version of the service state.

Be very careful with capitalization in the contract identifier and the names of the service state and the properties. XML is case sensitive.

Inside the template section is a complete HTML page. Property values are displayed from the state using the <xsl:value-of> element. All properties must be referred to using a full URI. This is why they are shown as svc:property1 and svc:property2. If you use nested structures within your state, then you cannot just use the "dot notation" that you use in C#. For example, the state for the TeleOperation service contains a class called Options, which in turn contains a number of other properties. To access Options.MotionSpeed you must write the following:

<xsl:value-of select="svc:Options/svc:MotionSpeed"/>

Notice that a slash (/) replaces the dot (.) in the property name and that the contract identifier (svc:) is repeated. If you make a mistake in the syntax of the select attribute, the value quietly disappears. No errors are reported—the value is just missing from the web page.

In addition to formatting the layout of your state properties using XSLT, you can use a Cascading Style Sheet (CSS) file to control the formatting of the HTML tags, i.e., the appearance of the elements on the page.

It is beyond the scope of this book to explain how to write CSS code, so it is assumed that you already know how, or you can learn from a tutorial website on the Internet. The full definition of CSS is also available on the World Wide Web Consortium website, but it is quite complex to read.

A CSS file can be embedded in your service in exactly the same way as an XSLT file. (Add it to your project and then mark it as an embedded resource). Then you just need to know the appropriate URI. In the preceding example, the built-in MRDS Cascading Style Sheet is used for formatting. The full URI for this is in the href attribute of the <link> tag. This MRDS CSS file defines the odd and even classes that are used on the table rows to make them easier to read by changing the background highlighting.

Creating an XSLT file Using the MRDS Template

If you want to save yourself some time and promote page uniformity, instead of building an XSLT file from scratch as shown in the last section, you can always create one using the MRDS template. To use the MRDS template, follow these steps:

  1. Copy the template from the C:ResourcesDSS folder (where you saved it earlier in the section "Setting Up Your Development Environment for XSLT") into your project directory and give it an appropriate name. Click Project Add Existing Item to add it to your project.

    Remember that once you have added the XSLT file to your project, you must change the properties so that the Build Action is set to Embedded Resource.

  2. Edit the XSLT file. You need to insert your service's contract identifier at the top of the file:

    <?xml version="1.0" encoding="utf-8" ?>
    <xsl:stylesheet
        version="1.0"
        xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
        xmlns:soap="http://www.w3.org/2003/05/soap-envelope"
        xmlns:dssp="http://schemas.microsoft.com/xw/2004/10/dssp.html"
        xmlns:svc="http://www.promrds.com/contracts/2008/01/teleoperation.html"
    >

    For convenience, the alias for your service is svc. You can change it if you like, but it is easier to leave it alone.

  3. In the first <xsl:template> section, change the Service Title, Name, and Description as appropriate for your service:

    <xsl:template match="/">
        <xsl:comment><!—Service Header Info—></xsl:comment>
        <xsl:variable name="title">
          TeleOperation Service
        </xsl:variable>
        <xsl:variable name="serviceName">
          <img
    src="/resources/teleoperation.y2008.m01/ProMRDS.Robotics.TeleOperation.Resources.
    Creating an XSLT file Using the MRDS Template
    icon_32x32.gif" align="middle" /> TeleOperation </xsl:variable> <xsl:variable name="description"> Allows you to drive a Differential Drive robot using live video from a WebCam on the robot </xsl:variable>
  4. Adding images to your page is also relatively easy. You can use embedded resources once again. The file format is not particularly important as long as it is one that the web browser understands. Note that BMP files are not understood by all web browsers. (This is a Windows format; and in any case it is not compressed, so the images can be quite large). Use the PNG, GIF, or JPG file format for your images.

Notice that the TeleOperation service displays an icon in the page header (in the preceding code) by adding an image to the serviceName variable. (Ignore the wrap-around in the URI).

In the MRDS template, you can either define CSS styles directly (between the <style> tags) or include a style sheet file. Look at the comments carefully. They indicate that the head parameter is placed in the HTML <head> section of the web page, and there is also a comment showing where to insert style definitions directly.

In the following example, an embedded style sheet file is used:

<!-- The contents of head param will be placed just before the </head> tag in 
Creating an XSLT file Using the MRDS Template
html. -->
<xsl:with-param name="head">

   <link rel="stylesheet" href="/resources/teleoperation.y2008.m01/ProMRDS.Robotics 
Creating an XSLT file Using the MRDS Template
.TeleOperation.Resources.TeleOperation.css" type="text/css" /> <style type="text/css"> /* Service-specific stylesheet goes here */ </style>

Using your own style sheet means that your page might not have the same appearance as other MRDS pages. Notice here that the CSS file is also an embedded resource in the Resources folder of the project. The standard MRDS style sheet is automatically included by the master page.

At the bottom of the first template is another <xsl:template> section. You need to change the match criterion from /svc:Template to use the correct name for your service state class. This is shown in the following example (ignore the <form> tag for now, as it is discussed later):

<xsl:template match="/svc:TeleOperationState">
   <form name="DssForm" method="post" onsubmit="return checkform(this);">
     <div class="Content">
   <table width="100%" border="0" cellpadding="5" cellspacing="5">
     <tr>
       <th>Host:</th>
       <td>
         <xsl:value-of select="svc:Host"/>
       </td>
     </tr>
     <tr>
       <th>Port:</th>
       <td>
         <xsl:value-of select="svc:Port"/>
       </td>
     </tr>
     <tr>
       <th>Connected:</th>
       <td>
         <xsl:choose>
           <xsl:when test="svc:Connected = 'true'">
             Connection established
           </xsl:when>
           <xsl:otherwise>
             Not connected
             (<b>Tip:</b>
             <span class="greyText">
               Enter a Host name and a Port and click on Connect
             </span>)
           </xsl:otherwise>
         </xsl:choose>
       </td>
     </tr>
...
     <tr>
       <th colspan="2">Option Settings</th>
</tr>
          <tr>
            <th>Main Window Start Position</th>
            <td><xsl:value-of select="svc:Options/svc:WindowStartX"/>,
Creating an XSLT file Using the MRDS Template
<xsl:value-of select="svc:Options/svc:WindowStartY"/> (Saved automatically) </td> </tr> <tr> ... </table> </div> </form> </xsl:template> </xsl:stylesheet>

For this code, note the following:

  • Usually the data is displayed in a table with two columns—the left column consists of Table Headings (th) and the right column is Table Data (td).

  • Values are inserted into the page using the <xsl:value-of> element. Remember to use the correct syntax for nested structures in your state, such as for the WindowStartX property.

  • Notice the use of <xsl:choose> to display different text depending on the state of the Connected property. You can have several <xsl:when> elements as well as <xsl:otherwise>, so it is conceptually similar to a switch statement. Alternatively, you could also use <xsl:if>. However, there is no else part, so you would have to use two if statements.

The best way to learn how to create XSLT code is probably to look at existing files. Search the MRDS samples folder for XSLT files.

Displaying the State Using XSLT

You are not finished yet. To display your state using XSLT, you must create a handler for HttpGet requests and add this message type to your main operations port (if it is not there already). The handler specifies the XSLT file when it returns the state.

To refer to your embedded XSLT file in the HttpGet handler, you define a string variable at the top of the source file. It is usually called _transform, but it doesn't have to be. You can use the [EmbeddedResource] attribute to construct the full URI for the XSLT file as shown here for the TeleOperation service:

/// <summary>
    /// Embedded XSLT file for formatting State on a web page
    /// </summary>
[EmbeddedResource("ProMRDS.Robotics.TeleOperation.Resources.TeleOperation.xslt")]
    string _transform = null;

The appropriate prefix is added to the URI and the resulting string is assigned to _transform when you compile the code.

The code for the HttpGet handler is quite simple:

/// <summary>
/// HttpGet Handler
/// </summary>
/// <param name="get"></param>
/// <returns></returns>
[ServiceHandler(ServiceHandlerBehavior.Concurrent)]
public virtual IEnumerator<ITask> HttpGetHandler(HttpGet httpGet)
{
    // Format the response using a transform
    httpGet.ResponsePort.Post(new HttpResponseType(
        HttpStatusCode.OK,
        _state,
        _transform)
    );
    yield break;
}

The response posted back is created using _state and _transform.

That's all there is to formatting your state information. However, if you make a mistake in the URI of your XSLT file in the [EmbeddedResource] attribute, you might see an error like the one shown in Figure 4-8.

Figure 4-8

Figure 4.8. Figure 4-8

You can check whether your embedded XSLT file is present in your assembly by entering the full URI into the address bar of the web browser. If it is not found, a blank page is displayed. In that case, you need to double-check your spelling, the path, and the placement of dots. The URI is not case sensitive, so that cannot be the problem.

Creating a Web Form for Data Input

Creating a Web Form is not much different from what you have already seen except that you wrap your HTML code in a <form> tag and put the property values into <input> tags. You also have to add a Submit button; otherwise, there is no way to post the Form! If you are familiar with web development, then you should not have any trouble:

  1. Looking at the TeleOperation.xslt file again, there is a <form> tag at the top of the template:

    <xsl:template match="/svc:TeleOperationState">
    <form name="DssForm" method="post" onsubmit="return checkform(this);">

    Notice that the method is post. You cannot (easily) use the get method because the HttpGet and HttpPost handlers are separate. The Form name is not important, and the onsubmit attribute is discussed in the next section.

  2. In the main part of the HTML table, you can use <input> tags to get values from the user. The following example displays the current value of the DeadZoneX property in a textbox:

    <tr>
     <th>Dead Zone X</th>
       <td>
         <input type="text" name="DeadZoneX" class="TextBox">
           <xsl:attribute name="value">
             <xsl:value-of select="svc:Options/svc:DeadZoneX"/>
           </xsl:attribute>
         </input>
         (Game Controller range is 1000, so typical value is 100)
       </td>
    </tr>

    Note that the XSL code sets the value attribute of the <input> tag to show the current value.

    The dead zone is a region at the center of the joystick's movement where the drive speed is set to zero. Most joysticks do not return exactly to the zero position when you release them. The small amount by which they are off-center causes very small set power commands to be sent to the robot's wheels, but it is unlikely to move due to inertia. In some cases, this causes the motors to squeal, which can be quite annoying!

  3. The last row in the table is a Submit button. The Form data is sent to the service (as a HttpPost request) when you click the button, which is labeled "Save":

    <tr>
      <td>
        <input type="submit" name="Save" value="Save" />
      </td>
    </tr>

The Submit (Save) button is visible at the very bottom of Figure 4-9.

Figure 4-9

Figure 4.9. Figure 4-9

Notice in Figure 4-9 that the values above the Option Settings heading cannot be edited; they are for informational purposes only. The window positions cannot be edited either. They are obtained directly from the windows, so you just need to position the windows where you want them before you save the state.

All of the remaining properties have textboxes for entering new values. It is possible to make them right-aligned, but this has not been done. It is simply a matter of changing the TextBox style in the CSS file.

You can have more than one <form> tag on a web page, and you can have more than one Submit button in a Form (with different names). In that case, the code in your HttpPost handler needs to check the Form name and/or the Submit button name.

After you click Save, the data is sent in an HttpPost request to your handler. Before looking at the handler though, you need to add some support utilities. At the top of the service, add the following statement:

DsspHttpUtilitiesPort _httpUtilities;

This port can be used to request support services for processing HTTP forms. Do not be tempted to create a new port like this:

DsspHttpUtilitiesPort _httpUtilities = new DsspHttpUtilitiesPort();

Using new does not work. The new port must be created in the Start method:

// Needed for HttpPost
_httpUtilities = DsspHttpUtilitiesService.Create(Environment);

If you forget to create the HTTP utilities port and simply use new, you will fall into a common trap of MRDS—sending messages when there is nobody home! You can send as many messages as you like to a port, but if no service is listening on the other end then your requests will go unanswered. You will wait a very long time for the HTTP utilities to do their job!

The TeleOperation HttpPost handler begins like this:

/// <summary>
/// Http Post Handler for Web Form inputs
/// </summary>
[ServiceHandler(ServiceHandlerBehavior.Concurrent)]
public virtual IEnumerator<ITask> HttpPostHandler(HttpPost httpPost)
{
    string ErrorMessage = String.Empty;
    Fault fault = null;
    NameValueCollection parameters = new NameValueCollection();

    // Use helper to read form data
    ReadFormData readForm = new ReadFormData(httpPost.Body.Context);
    _httpUtilities.Post(readForm);

    // Wait for result
    yield return Arbiter.Choice(
        readForm.ResultPort,
        delegate(NameValueCollection col)
        {
            parameters = col;
        },
        delegate(Exception e)
        {
            fault = Fault.FromException(e);
            LogError(null, "Error processing form data", fault);
            ErrorMessage += e.Message;
        }
    );

    if (fault != null)
    {
        httpPost.ResponsePort.Post(fault);
        yield break;
    }

The first thing the handler does is send a ReadFormData request to the HTTP utilities. Because the utilities operate like a service, you have to wait for a response and be prepared to handle a Fault.

There is an alternative to using these utilities: You can write your own code to extract the form parameters. The following code is not seriously proposed as a replacement, but it shows how the parameters are obtained. The code performs no error handling, and it fails if the total amount of data exceeds 10K:

NameValueCollection parameters = new NameValueCollection();
            // Extract the parameters from the Request data stream
            int length = 0;
            byte[] data = new byte[10240];
            length = httpPost.Body.Context.Request.InputStream.Read(
Figure 4-9
data, 0, 10240); StringBuilder sb = new StringBuilder(); for (int i = 0; i < length; i++) sb.Append((char)data[i]); string request = sb.ToString(); char[] ampersand = { '&' }; char[] equals = { '=' }; string[] pairs = request.Split(ampersand); for (int i = 0; i < pairs.Length; i++) { string[] namevalue = pairs[i].Split(equals); parameters.Add(namevalue[0], namevalue[1]); }

Each of the parameters is checked in turn to see if it is valid. You must do this even if you use client-side validation in JavaScript because users can turn off JavaScript in their web browsers. For example, the following code confirms that the user entered a valid number for MotionSpeed:

double MotionSpeed = _state.Options.MotionSpeed;
bool validValue = false;

if (!string.IsNullOrEmpty(parameters["MotionSpeed"]))
{
    try
    {
        MotionSpeed = double.Parse(parameters["MotionSpeed"]);
        validValue = true;
    }
    catch (Exception e)
    {
        string msg = "Could not parse Motion Speed: " + e.Message;
        LogError(msg);
        ErrorMessage += msg;
    }
}

if (validValue && MotionSpeed >= 10 && MotionSpeed <= 1000)
{
    _state.Options.MotionSpeed = MotionSpeed;
}

The code also checks whether MotionSpeed is within a reasonable range, and finally assigns the new value to the property in the state. (Because it modifies the state, the HttpPost handler should be Exclusive. However, most implementations mark it as a Concurrent handler. Make sure that you mark it as Exclusive if it changes the state.)

Similar checks are performed for the rest of the properties. Notice that any error messages are appended to the ErrorMessage variable. Once all of the updates have completed, it is a simple matter to determine whether the ErrorMessage is empty and call the appropriate routine to post success or failure:

// Finally, process the result
    if (ErrorMessage == string.Empty)
    {
        HttpPostSuccess(httpPost);
    }
    else
    {
       HttpPostFailure(httpPost, ErrorMessage);
    }

    yield break;
}

The HttpPostSuccess routine does not require much comment. Just note that it saves the option settings back into public variables in the WinForms if they are active. In effect, it passes information from the main service to the WinForms. Then it saves the state to a config file:

/// <summary>
/// Send Http Post Success Response
/// </summary>
private void HttpPostSuccess(HttpPost httpPost)
{
    // Grab the current window location
    if (_driveControl != null)
    {
        _state.Options.WindowStartX = _driveControl.Location.X;
        _state.Options.WindowStartY = _driveControl.Location.Y;
    }
    if (_cameraForm != null)
    {
        _state.Options.WebCamStartX = _cameraForm.Location.X;
        _state.Options.WebCamStartY = _cameraForm.Location.Y;
    }

    // Update the Forms with the new option settings
    // This is another implicit method of communication with WinForms
    if (_driveControl != null)
        _driveControl.options = _state.Options;
    if (_cameraForm != null)
        _cameraForm.options = _state.Options;

    // Post a response
    HttpResponseType rsp =
        new HttpResponseType(HttpStatusCode.OK, _state, _transform);
httpPost.ResponsePort.Post(rsp);

    // Save the state now because it might have changed
    SaveState(_state);
}

Conversely, the HttpPostFailure routine does require some explanation. If it simply posts back a Fault (as in the code that is commented out), then XML code is displayed on the screen. Instead, it posts a Fault response but specifies an XSLT file (_faultTransform):

/// <summary>
        /// Send Http Post Failure Response
        /// </summary>
        private void HttpPostFailure(HttpPost httpPost, string failureReason)
        {
            // Create a new Fault based on the error message
            Fault fault = Fault.FromCodeSubcodeReason(FaultCodes.Receiver, 
Figure 4-9
DsspFaultCodes.OperationFailed, failureReason); //HttpResponseType rsp = new HttpResponseType(fault); //httpPost.ResponsePort.Post(rsp); // Post it back but use the Fault Transform httpPost.ResponsePort.Post(new HttpResponseType( HttpStatusCode.OK, fault, _faultTransform ) ); }

The Fault.xslt file isn't covered here. It formats a standard SOAP fault (which is an XML message) and provides a couple of buttons so that users are not left at a dead end with nowhere to go. You can examine this file yourself.

Using JavaScript for Client-Side Scripting

A Web Forms approach whereby all the data on the Form is sent to the web server for validation does not result in a good user experience. It is much better to have JavaScript code in the Form to do validation prior to submitting the Form.

If you do not already know JavaScript, it might be a good investment in your job skills because it is a key component of web development. Numerous websites on the Internet offer tutorials on JavaScript, such as www.w3schools.com/js/default.asp. Because it is a C-like language, you should not have trouble learning enough to do basic validation. There are also websites that provide JavaScript code for a variety of tasks, such as www.dynamicdrive.com.

The position in the XSLT file where you place your JavaScript code is indicated quite clearly in the comments in the template. You can either enter the code directly into the XSLT file or create another embedded resource and add your own <script> tag to reference it as follows:

<script language="javascript" type="text/javascript" src="URI-for-JS-file" />

In TeleOperation.xslt you can see the validation code, which is split across two routines. The first routine, simply called validate, is used to confirm that a value is numeric and falls within a specified range:

// Check that a value is numeric and within a specified range
// Returns a string which is either empty or an error message
function validate(val, fieldname, minval, maxval)
{
   var msg = "";
   var num;
   if (val.length == 0)
   {
       msg += "Please enter a value for " + fieldname + "
";
   }
   else
   {
     num = parseFloat(val);
     if (isNaN(num))
       msg += fieldname + " must be a number
";
     else if (num < minval || num >= maxval)
      msg += fieldname + " is out of range (" + minval + " to " + maxval +")
";
   }
   return msg;
}

The validate function returns an error message if there is one, or an empty string. It is called by the checkform routine:

// Check the form before submission
// NOTE: Return true if OK to proceed, or false if there are errors
function checkform(f)
{
   var msg = "";

   // Accumulate all of the error messages (if there are any)
   msg += validate(f.DeadZoneX.value, "Dead Zone X", 0, 900);
   msg += validate(f.DeadZoneY.value, "Dead Zone Y", 0, 900);
   msg += validate(f.TranslateScaleFactor.value, "Translate Scale Factor", 
Using JavaScript for Client-Side Scripting
0.01, 100); msg += validate(f.RotateScaleFactor.value, "Rotate Scale Factor", 0.01, 100); msg += validate(f.MotionSpeed.value, "Motion Speed", 10, 1000); msg += validate(f.DriveDistance.value, "Drive Distance", 10, 2000); msg += validate(f.RotateAngle.value, "Rotate Angle", 5, 360); // If there were any errors at all, display them if (msg != "") { // Pop up a dialog to tell the user what is wrong alert(msg); return false; } // If we got here, then the data is OK return true; }

In addition, you need to modify the <form> tag so that the validation routine is called prior to the Form being submitted. To do this, add the onsubmit attribute to the tag:

<form name="DssForm" method="post" onsubmit="return checkform(this);">

Note the syntax used in the onsubmit attribute—you must return the Boolean value from checkform so that the Form submission can either proceed or be suppressed. In addition, note that JavaScript is case sensitive, so if you write "return CheckForm(this);", you will receive an error when you try to submit the Form.

Validating Form data this way cannot catch every possible type of error. There might still be errors that occur only after the Form has been submitted and the service has had a chance to process it. In any case, the user can turn off JavaScript in the web browser, so you always have to check on the server side. This is just basic Web Development 101.

Using a Camera

This section discusses how to set up a webcam and display live video in a WinForm. You have already seen in Figures 4-3 and 4-4 that TeleOperation displays a separate window for the video feed.

Before you start working with a webcam, it is a good idea to test it. Some webcams don't work with MRDS because they must support DirectX. In particular, older camera drivers might not be suitable if they support only the obsolete Video for Windows (VFW) standard.

To test the camera, plug it in and make sure that the drivers are loaded. (Read the manufacturer's instructions for this. There might even be a test program included with the software so that you can see if the camera works.)

In Chapter 3, you learned how to start DssHost without a manifest and manually start up a Webcam service. If you need detailed instructions, refer back to Chapter 3. In summary:

  1. Start up DssHost from a MRDS Command Prompt window without a manifest.

  2. Open a web browser and browse to the Control Panel.

  3. Locate the Webcam service in the list of services.

  4. Click the Create button beside the Webcam service.

  5. Browse to the Service Directory.

  6. Click the Webcam service in the list of running services.

This displays a page that shows an image from your webcam. You can click the Start button to get a continuous video feed. If you worked through Chapter 3, then you might have already tried out your webcam in the section "Service Directory."

If your camera requires a particular service to be running—for example, the Surveyor SRV-1 with the modified service supplied with this book—then you have to start DssHost with the appropriate manifest. However, the Swann Microcam2 plugs into a USB Video Capture Device (VCD) and is automatically detected by the standard Webcam service.

If you change webcams, you might encounter a problem trying to get the new one to work. In this case, try renaming (or deleting) the config file, which is samplesConfigwebcam.Config.xml. This file contains the last selected webcam, so if you switch cameras, the Webcam service sometimes gets confused. This seems to be the case with the Belkin USB VCD.

You can change the settings on your webcam via the Webcam state page even while the TeleOperation service is running. TeleOperation does not provide facilities to do this. If the default resolution is not appropriate, then you have to change it manually. Once you have made the change, it is recorded in the config file and you should not have to worry about it again.

Adding a Camera to a Service

The following is a quick summary of the steps that are required to add a webcam to a new service. You should first test the camera as outlined above and make sure that it works with MRDS.

  1. Set up the webcam as a partner, or write code to locate it in the service directory and connect to it dynamically.

  2. If you want to use an existing WinForm to display the video images, skip to step 4.

  3. Create a WinForm (see the section "Creating a WinForm"). Set up the necessary communication with the main service as explained earlier in the chapter. This Form does not need to send messages back to the main service, so the standard OnLoad and OnClosed messages (discussed above) should be sufficient.

  4. Add a PictureBox to the WinForm with an appropriate size. Cameras usually have an aspect ratio of 4:3, e.g., 160 ? 120, 320 ? 240, etc. Set the SizeMode property to Zoom so that the image aspect ratio is preserved (unless you like stretched images).

  5. Add a public property or method to the WinForm to set the bitmap in the PictureBox.

  6. Add ports to your main service to communicate with the webcam and receive notifications. You should add a shutdown port as well.

  7. Add a receiver to the main interleave to handle notification messages from the webcam. This receiver listens for UpdateFrame messages from the webcam.

  8. Write a handler for UpdateFrame messages. This handler sends QueryFrame messages to the webcam to get the actual bitmap data, and then puts it into the PictureBox on the Form using FormInvoke.

  9. Add code to your main service to subscribe to the webcam either when you connect or in the Start method if the partnership is established in the manifest.

  10. Optionally, add code to your Drop handler to unsubscribe from the webcam before shutting down. (This is best practice).

Most of the code you can just copy and paste from another service such as TeleOperation or the Dashboard.

Setting Up a WinForm for the Video Feed

If you require a particular web camera for your service to operate, you can declare the generic Webcam service as a partner at the top of your main service source file. Then add a partner in the manifest, and when your service starts it should find the appropriate camera.

However, for the TeleOperation service there are two problems with this approach:

  • This would lead to a static definition and you would have to modify the manifest or the source code to use a different DSS node or a different camera.

  • The service might not start if the webcam partner cannot be found.

The TeleOperation service does not connect to a webcam until you click the Connect button. The button click event handler in DriveControl.cs posts a message to the main service requesting a connection to the host and port specified on the WinForm. The OnConnect message contains the URI of the appropriate directory service in the Service property.

The code to handle the connection request in TeleOperation.cs is in two parts. The OnConnectHandler is called first in response to the message from the DriveControl Form; then the ConnectDrive and/or ConnectWebCam routines are called to make the actual connections.

Handling Connection Requests

OnConnectHandler searches the directory on the specified remote host (it does not have to be localhost) for generic Differential Drive and Webcam services. It is possible that there are no matching services, so this must be taken into account:

/// <summary>
        /// Connect Handler
        /// </summary>
        /// <param name="onConnect"></param>
        /// <returns></returns>
        IEnumerator<ITask> OnConnectHandler(OnConnect onConnect)
        {
            if (onConnect.Form == _driveControl)
            {
                string ErrorMessage = null;

                // The service here is the Directory on the specified host:port
                UriBuilder builder = new UriBuilder(onConnect.Service);
                builder.Scheme = new Uri(ServiceInfo.Service).Scheme;

                ds.DirectoryPort port = 
Handling Connection Requests
ServiceForwarder<ds.DirectoryPort>(builder.Uri); ds.Get get = new ds.Get(); port.Post(get); ServiceInfoType[] list = null; yield return Arbiter.Choice(get.ResponsePort, delegate(ds.GetResponseType response)
{
                    list = response.RecordList;
                },
                delegate(Fault fault)
                {
                    list = new ServiceInfoType[0];
                    LogError(fault);
                }
        );

If you did not have to allow for the possibility of remote hosts, then it would not be necessary to create a ServiceForwarder for the directory service. Instead, you could just use the DirectoryPort, which is defined in the DsspServiceBase class.

A simple Get request to the directory service returns an array of ServiceInfoType records. The code extracts the host name and port number from the first record, although this is not really necessary. It also handles the situation where no services are found:

ServiceInfoType driveInfo = null;
ServiceInfoType webcamInfo = null;

try
{
    if (list.Length > 0)
    {
        UriBuilder node = new UriBuilder(list[0].Service);
        node.Path = null;
        string nodestring = node.Host + ":" + node.Port;
        LogInfo(nodestring);
    }
    else
    {
        LogError("No services found!");
        ErrorMessage = "No services found!
";
        _state.Connected = false;
    }

Next, the code loops through all of the services in the list, comparing the Contract ID with the IDs for the Differential Drive service and the Webcam service. If either of these is found, the service info is remembered:

string driveUriPath = null;
                    string webcamUriPath = null;
                    foreach (ServiceInfoType info in list)
                    {
                        if (driveInfo == null && info.Contract == 
Handling Connection Requests
drive.Contract.Identifier) { driveInfo = info; } if (webcamInfo == null && info.Contract ==
Handling Connection Requests
webcam.Contract.Identifier)
{
                            webcamInfo = info;
                        }

                        if (driveInfo != null && webcamInfo != null)
                            break;
                    }
               }
               catch (Exception ex)
               {
                   string msg = "Service search error: " + ex.Message;
                   LogError(msg);
                   ErrorMessage += msg + "
";
                   _state.Connected = false;
               }

An error message is constructed based on the outcome of the search. If either or both of the services were not found, a MessageBox displays. The MessageBox is invoked via a public method in the DriveControl Form. The ShowErrorMessage method is trivial—it contains a single statement, which displays a MessageBox using the string parameter it was given. This illustrates one way to display message boxes from within a service:

if (driveInfo == null)
    ErrorMessage += "No Drive service found
";
if (webcamInfo == null)
    ErrorMessage += "No WebCam service found
";

if (ErrorMessage != null)
{
    if (_driveControl != null)
    {
        WinFormsServicePort.FormInvoke(
            delegate()
            {
                _driveControl.ShowErrorMessage(ErrorMessage);
            }
        );
    }
}

Lastly, the appropriate connect routine is called for each of the services (if found):

if (driveInfo != null)
      SpawnIterator<string>(driveInfo.Service, ConnectDrive);

  if (webcamInfo != null)
      SpawnIterator<string>(webcamInfo.Service, ConnectWebCam);

  // We are "connected" if either service was found
  if (driveInfo != null || webcamInfo != null)
      _state.Connected = true;
  }
}

Connecting a Webcam

Before looking at the ConnectWebCam routine, there are several global variables declared at the top of TeleOperation.cs that are used in this routine:

// Ports for the Web Camera
webcam.WebCamOperations _webCamPort;
webcam.WebCamOperations _webCamNotify = new webcam.WebCamOperations();
Port<Shutdown> _webCamShutdown = null;
// Form to display the video in
WebCamForm _cameraForm;
// Port for the WebCam Form to communicate on
WebCamFormEvents _webCamEventsPort = new WebCamFormEvents();
// Flag to indicate that the form is loaded and ready
bool _webCamFormLoaded = false;

For this code, note the following:

  • _webCamPort is used to send requests to the camera service.

  • The purpose of _webCamNotify should be obvious: It receives UpdateFrame messages from the camera. These messages do not contain the actual bitmap data—you must make a request to the camera to get the data in response to a notification. This avoids sending a lot of data around if there is a backlog.

  • Likewise, the purpose of _webCamShutdown is obvious, as is the _cameraForm handle for the WinForm instance.

  • The _webCamEventsPort is used for communication from the Webcam Form. In order to ensure that you can drive the robot using the keyboard regardless of which window has the input focus, the keyboard handling code is duplicated in the Webcam Form. It is a different port from the DriveControl Form, but it uses the same handler.

  • _webCamFormLoaded is an important flag. It indicates whether the Webcam Form is active or not. Due to timing issues, it is possible for frames to arrive from the camera before the Form is properly initialized, or after the user has closed down the Form. The point of this flag is to prevent access violations caused by trying to update the PictureBox when it does not exist.

The rest of the code is in the Camera region at the bottom of TeleOperation.cs.

ConnectWebCam takes the name of the service as a string. If a connection is already open to a camera, then it unsubscribes:

// Handler for connecting to WebCam
IEnumerator<ITask> ConnectWebCam(string camera)
{
    //ServiceInfoType info = null;
    Fault fault = null;
    SubscribeResponseType s;
    //String camera = Opt.Service;

    // Already connected?
    if (_webCamPort != null)
    {
// Unsubscribe
    if (_webCamShutdown != null)
        yield return PerformShutdown(ref _webCamShutdown);
}

Next, it creates a new operations port and subscribes to the webcam:

// Create a new port
_webCamPort = ServiceForwarder<webcam.WebCamOperations>(camera);

// Subscribe to the webcam
webcam.Subscribe subscribe = new webcam.Subscribe();
subscribe.NotificationPort = _webCamNotify;
subscribe.NotificationShutdownPort = _webCamShutdown;

_webCamPort.Post(subscribe);

yield return Arbiter.Choice(
    //_webCamPort.Subscribe(_webCamNotify),
    subscribe.ResponsePort,
    delegate(SubscribeResponseType success)
    { s = success; },
    delegate(Fault f)
    {
        fault = f;
    }
);

if (fault != null)
{
    LogError(null, "Failed to subscribe to webcam", fault);
    yield break;
}

If the subscription is successful, then the state is updated with the full URI of the Webcam service and a new Webcam View Form is created:

// Put the service URI into the state for visibility
_state.WebCamService = camera;
LogInfo("Connected WebCam to " + camera);

// Now that we have found the service and subscribed,
// create a form to display the video
RunForm runForm = new RunForm(CreateWebCamForm);

WinFormsServicePort.Post(runForm);

yield return Arbiter.Choice(
    runForm.pResult,
    delegate(SuccessResult success) { },
    delegate(Exception e)
    {
        fault = Fault.FromException(e);
    }
);

    if (fault != null)
    {
        LogError(null, "Failed to Create WebCam window", fault);
        yield break;
    }

    yield break;
}

At this stage, the TeleOperation service is waiting for messages to arrive from the webcam and a new WinForm should be visible on the screen, as shown in Figures 4-3 and 4-4.

Processing Video Frames

When an UpdateFrame message arrives from the webcam, the handler must issue a QueryFrame request to get the image data. Notice that a timeout is set on the request so that the handler does not get bogged down if the Webcam service dies or is too slow responding:

// Handler for new frames from the camera
        IEnumerator<ITask> WebCamUpdateFrameHandler(webcam.UpdateFrame update)
        {
            webcam.QueryFrameResponse frame = null;
            Fault fault = null;

            // Don't do anything if the form has not loaded or has been closed!
            // Race conditions can arise when the form is first created, or if
            // the user closes the form. These result in access violations unless
            // we are careful not to execute the rest of the code.
            if (!_webCamFormLoaded)
                yield break;

            // Throw away the backlog
            // This does no harm because we are throwing away notifications,
            // not webcam images
            Port<webcam.UpdateFrame> p = 
Processing Video Frames
(Port<webcam.UpdateFrame>)_webCamNotify[typeof(webcam.UpdateFrame)]; if (p.ItemCount > 2) { Console.WriteLine("Webcam backlog: " + p.ItemCount); p.Clear(); } webcam.QueryFrame query = new webcam.QueryFrame(); // Set a timeout so that this cannot wait forever query.TimeSpan = TimeSpan.FromMilliseconds(1000); _webCamPort.Post(query); // Wait for response yield return Arbiter.Choice( query.ResponsePort,
delegate(webcam.QueryFrameResponse success)
                {
                    frame = success;
                },
                delegate(Fault f)
                {
                    fault = f;
                }
            );

            if (fault != null)
            {
                LogError(null, "Failed to get frame from camera", fault);
                yield break;
            }

Once a response is received successfully, the data (which is a raw array of bytes) can be turned into a Bitmap and inserted into the PictureBox on the Form:

// Create a bitmap from the webcam response and display it
            Bitmap bmp = MakeBitmap(frame.Size.Width, frame.Size.Height, 
Processing Video Frames
frame.Frame); // Display the image in the WinForm SpawnIterator<Bitmap>(bmp, DisplayImage); yield break; }

The code for creating a Bitmap from a byte array is general and can be used anywhere that you need to do this type of conversion. Note that the image dimensions must be supplied separately because a byte array does not contain this information:

Bitmap MakeBitmap(int width, int height, byte[] imageData)
{
      // NOTE: This code implicitly assumes that the width is a multiple
      // of four bytes because Bitmaps have to be longword aligned.
      // We really should look at bmp.Stride to see if there is any padding.
      // However, the width and height come from the webcam and most cameras
      // have resolutions that are multiples of four.

      Bitmap bmp = new Bitmap(width, height, PixelFormat.Format24bppRgb);

      BitmapData data = bmp.LockBits(
           new Rectangle(0, 0, bmp.Width, bmp.Height),
           ImageLockMode.WriteOnly,
           PixelFormat.Format24bppRgb
      );

      Marshal.Copy(imageData, 0, data.Scan0, imageData.Length);

      bmp.UnlockBits(data);

      return bmp;
}

Finally, the new Bitmap image is copied into the PictureBox using a FormInvoke. This code explicitly creates a FormInvoke message, and then posts it:

// Display an image in the WebCam Form
IEnumerator<ITask> DisplayImage(Bitmap bmp)
{
    Fault fault = null;

    // Insurance in case the form was closed
    if (!_webCamFormLoaded)
        yield break;

    FormInvoke setImage = new FormInvoke(
        delegate()
        {
            if (_webCamFormLoaded)
                _cameraForm.CameraImage = bmp;
        }
    );

    WinFormsServicePort.Post(setImage);

    yield return Arbiter.Choice(
        setImage.ResultPort,
        delegate(EmptyValue success) { },
        delegate(Exception e)
        {
            fault = Fault.FromException(e);
        }
    );

    if (fault != null)
    {
        LogError(null, "Unable to set camera image on form", fault);
    }
    else
    {
        // LogInfo("New camera frame");
    }
    yield break;
}

There are only a couple of lines of code in the FormInvoke delegate. First, a test is done to make sure that the Webcam Form is still valid. Then the Bitmap is assigned to a public property in the Form.

The code inside the Webcam Form that handles the Bitmap is as follows:

private Bitmap _cameraImage;

public Bitmap CameraImage
{
    get { return _cameraImage; }
    set
    {
_cameraImage = value;

          Image old = picCamera.Image;
          picCamera.Image = value;

          // Dispose of the old bitmap to save memory
          // (It will be garbage collected eventually, but this is faster)
          if (old != null)
          {
              old.Dispose();
          }
    }
}

As well as keeping a private copy of the Bitmap, the code assigns it to the PictureBox and then disposes of the previous Bitmap to save memory.

That completes the processing of a camera frame. The result is that you see images updating continuously on the screen, unless the Webcam service has a problem.

Inheriting from Abstract Services

There has been a fair amount of discussion on the MRDS Forum about object-oriented concepts and inheritance. The design of DSS does not really allow for traditional inheritance, but DSS has a form of inheritance through generic contracts.

Generic contracts have been mentioned several times previously. The concept is quite simple: You define a service state, a set of message types, and an operations port. (You can also include enum data types as part of the data contract). Note that there is no executable code included in a generic contract.

Developers can implement generic contracts for different brands and models of robots so that they all have a common interface. This enables applications like the Dashboard and TeleOperation to work on a variety of robots. In fact, the applications do not even know what type of robot they are talking to because all robots look the same from an API point of view.

When a developer implements a generic contract, the new service is like a device driver in an operating system—it hides the details about how to control a physical device and presents a "virtual" device to the operating system that accepts a standard set of commands. Generic contracts are covered in the MRDS Service Tutorials 8 and 9. You should read these tutorials for further information.

You are more likely to implement a generic contract than you are to define one, so the next section covers implementing contracts; building generic contracts is covered in the following section.

Implementing a Generic Service

In the ProMRDSChapter16 folder is a service called StingerPWMDrive. This example implements the Generic Differential Drive service for the Stinger robot. The original services from RoboticsConnection for the Stinger did not support the generic interface, which meant that it would not work with the TeleOperation service.

StingerPWMDrive is a "wrapper" that translates generic drive operations into requests to the Serializer Services. (The Serializer is the on-board brains of a Stinger robot). By using StingerPWMDrive instead of the drive service supplied by RoboticsConnection, the TeleOperation service can treat a Stinger like other types of robots because StingerPWMDrive has a generic interface. In fact, TeleOperation is not even aware that it is talking to a Stinger. Without this service, you cannot control the Stinger using TeleOperation.

The StingerPWMDrive service uses the Pulse Width Modulation (PWM) interface on the Stinger, not the Proportional, Integral, and Derivative (PID) interface. Therefore, it does not make use of the wheel encoders. The DriveDistance and RotateDegrees operations just use a timer, which is not very accurate. However, it is still a useful example.

To create a new service based on a generic contract, you use DssNewService with the /alt parameter to specify the alternate contract. You must also include the /i parameter to specify which assembly to look at for the alternate service. (The bold text in the following code indicates what you type.)

C:Microsoft Robotics Studio (1.5)ProMRDSChapter16>dssnewservice
/service:"StingerPWMDrive" /namespace:"ProMRDS.Robotics.Stinger.PWMDrive" 
Implementing a Generic Service
/year:"2008" /month:"01"
Implementing a Generic Service
/alt:"http://schemas.microsoft.com/robotics/2006/05/drive.html"
Implementing a Generic Service
/i:"....inRoboticsCommon.dll"

The new service has the [AlternateContract] attribute with the contract identifier of the generic service:

/// <summary>
    /// Provides access to a differential drive (that coordinates two motors that 
Implementing a Generic Service
function together). /// </summary> [DisplayName("Stinger Generic Differential Drive")] [Description("Provides access to the Stinger Drive (Uses the Generic
Implementing a Generic Service
Differential Drive contract) (Partners with Stinger 'brick')")] [Contract(Contract.Identifier)] [AlternateContract("http://schemas.microsoft.com/robotics/2006/05/drive.html")] public class StingerPWMDriveService: DsspServiceBase {

Because it is implementing an existing service, the main port and the state both use data types from the generic service:

/// <summary>
      /// Main Port
      /// </summary>
      /// <remarks>Note: The main port is an instance of the Generic Differential 
Implementing a Generic Service
Drive Operations Port</remarks> [ServicePort("/stingerpwmdrive", AllowMultipleInstances=false)] private drive.DriveOperations _mainPort = new drive.DriveOperations(); /// <summary> /// Stinger PWMDrive Service State
/// </summary>
      /// <remarks>Note: The State is an instance of the Generic Differential 
Implementing a Generic Service
Drive State</remarks> [InitialStatePartner(Optional=true,
Implementing a Generic Service
ServiceUri="Stinger.PWMDrive.Config.xml")] private drive.DriveDifferentialTwoWheelState _state = new drive.DriveDifferentialTwoWheelState();

Stubs are created for all of the operations in the generic contract, but they throw exceptions saying that they are not implemented, as shown in this example:

/// <summary>
         /// HttpPost Handler
         /// </summary>
         /// <param name="submit"></param>
         /// <returns></returns>
         [ServiceHandler(ServiceHandlerBehavior.Concurrent)]
         public virtual IEnumerator<ITask> HttpPostHandler(dssphttp.HttpPost submit)
         {
             // TODO: Implement Submit operations here.
             throw new NotImplementedException("TODO: Implement Submit operations 
Implementing a Generic Service
here."); }

The StingerPWMDriveTypes.cs file only contains the contract identifier. There is no need to define a main operations port or request types because these are all in the generic contract.

However, if you want to extend the state or add more operations, then you can subclass your state or PortSet off the generic ones. It is therefore possible to add more fields to the state, or more operations to the PortSet. The Microsoft Tutorials refer to this as "extending" a generic contract.

What if you find yourself writing very similar services, e.g., one for simulation and one for real hardware, and you want to avoid duplicating code? The simplest approach is to use a Helper DLL or service.

If you abstract the common routines and place them into a separate DLL, then you avoid the maintenance nightmare of keeping two copies of the code in sync. Whether you choose to use a DLL with a conventional library interface or a service is up to you, although your decision should be guided by the need to implement queuing, which ports are very good at.

Building Virtual Services

Generic contracts are intended to be used across a range of different hardware. The most common example is the Generic Differential Drive contract. If you are building services for a single robot and have no plans to support other robots, you probably don't need to create any generic contracts. However, you should try to use existing generic contracts in this case.

The source files for most of the generic contracts in MRDS are located under the MRDS root directory in the folder samplesCommon. These are good examples of how to create generic services. You should become familiar with these generic services and try to use them whenever possible, rather than define your own.

Creating a generic contract is very easy, assuming that you have already planned what data you need in the state and the types of operations you want to perform. Note that Chapter 17 walks through the steps for creating a new Generic Brick contract, but they are outlined here for completeness.

To create a generic contract, follow these steps:

  1. Create a new service, such as MyGenericContract. There are no special requirements for creating the new service. Note that there is no need to make any changes to the Contract class that is automatically generated when you create a new service.

  2. Open AssemblyInfo.cs and modify the ServiceDeclaration attribute. The ServiceDeclaration should initially contain the following:

    [assembly: ServiceDeclaration(DssServiceDeclaration.ServiceBehavior)]

    This indicates an implementation of a service. However, this new service is a generic service so it has no implementation. Change this declaration to the following:

    [assembly: ServiceDeclaration(DssServiceDeclaration.DataContract)]

    This revised declaration says that the service contains only a DataContract, i.e., it is generic.

    If you want to reduce the number of DLLs in your applications, you might choose to have generic services and service implementations in the same solution. If so, you can modify the ServiceDeclaration so that both types of service can coexist in the same DLL:

    [assembly: ServiceDeclaration(DssServiceDeclaration.DataContract | DssServiceDeclaration.ServiceBehavior)]

    If you combine both types of services in a single DLL, then you must use different namespaces for each of the services.

  3. Remove the service implementation source file, MyGenericContract.cs, from the solution and delete the file. A generic contract, as its name implies, is simply a data contract; therefore, it does not contain any executable code, i.e., there is no actual implementation of the service.

  4. Update the state in MyGenericContractTypes.cs. Defining the generic service state is no different from a normal service. You use the [DataContract], [DataMember], and [DataMemberConstructor] attributes in the same way as usual.

    If you want, you can split this file into two parts and create MyGenericContractState.cs. This is not required but it might be easier for users of your contract to understand. Many of the standard MRDS generic contracts are organized this way.

  5. Define all the necessary data types for your generic service operations in the file MyGenericContractTypes.cs. This process is identical to how you define operations for a normal service, as explained in Chapter 3.

  6. Add a main operations port that lists all of the operations. The main operations port is similar to a normal service.

If you plan to use the generic service to create services to run under the .NET Compact Framework (CF), then you have to be careful about how you declare the operations port. In particular, if there are more than eight data types in the PortSet declaration, then you must use typeof. Refer to Chapter 16 for more information on CF services.

Generic services do not explicitly include a version number. (It is possible to specify a version number in the Assembly Information, but this is not used for locating services.) You can change the year and month in the contract identifier if you make substantial changes to a generic contract. However, existing services cannot use the new generic contract without being modified and recompiled. Therefore, try to design your generic services carefully to avoid possible changes in the future.

The MRDS Service Tutorial 9 discusses how to extend existing generic contracts. The procedure is straightforward, so it is not discussed here.

More on Debugging

Debugging is discussed briefly in Chapter 3. This section provides some more tips on how to debug services under MRDS.

Read the Documentation First

It sounds obvious, but read the documentation. Also read all of the messages that are displayed in the Console window, and the Debug and Trace messages in a web browser. Often the answer is right under your nose. Even if the problem is not clear, you can use part of an error message to search the MRDS Discussion Forum or even Google it.

The MRDS Discussion Forum contains a wealth of information. You might not get an immediate answer if you post a question there, but in general you will find the information you need, often from an expert or one of the people on the MRDS Development Team.

As with all forums, make sure you can clearly define your problem, and if possible narrow it down to a small code snippet. (You can mark code when you post it to the Discussion Forum so that it doesn't end up with smiley faces all through it). You are more likely to get a response if your posting demonstrates that you have made an effort to solve the problem yourself and you list the things you have already tried. If you post a question like "Why doesn't this code work?" followed by 100 lines of code, or you ask a question that has been discussed several times before, then you might not get an answer.

Use the Visual Studio Debugger

The obvious way to debug a service is using the Visual Studio Debugger! What might not be so obvious is that you can actually set breakpoints inside multiple services so you can see the effect of messages bouncing back and forth. All you have to do is open the relevant source code files from the other projects and set breakpoints. (This assumes that you have the source code and that the current assemblies in the bin folder were compiled from those sources).

The debugger is not always the solution to finding bugs. Some bugs occur due to subtle timing issues, and as soon as you stop in the debugger you change the timing. In addition, because the environment is multi-threaded, stopping at a breakpoint might leave other code running, with the result that messages pile up in a port.

Finally, remember that there is a Threads window in the Debugger. This might help you to identify what is happening.

Examine the State of a Node and Services

You have already seen how to examine the state of a service, even if it is displayed in XML. This works across the network, so you can examine services on remote hosts.

Don't be afraid to add debugging information to your state. The values of key variables, especially counters such as the number of packets sent and the number of packet errors, can be invaluable for verifying that the service is operating correctly or locating the source of errors. Instrumenting your service in this way is a good idea. You can always use conditional compilation to remove these variables later.

Traditional Debugging Techniques

Of course, if you really want to, you can resort to the classic debugging technique called "debug print statements" whereby you place Console.WriteLine statements at strategic places in your code. However, this is the lazy approach.

You should be aware that Console.WriteLine is quite slow, and it can actually hold up your services, resulting in behavior that might not be typical. You should not use it in time-critical code, inside loops, or in handlers that are called frequently. However, it is useful during initialization to indicate progress and for catastrophic errors that force the service to close down.

Using Trace Level and Logging

You might be familiar with the .NET Debug and Trace classes in System.Diagnostics that can be used to conditionally output messages. The DsspServiceBase class has a set of built-in methods that you can use to log information. In addition to the general-purpose Log method, specific methods are associated with each of the trace levels. These methods also output to the Debug and Trace Messages page shown in Chapter 3.

The methods and their associated trace levels are shown in the following table:

Method

Trace Level

LogError

1

LogWarning

2

LogInfo

3

LogVerbose

4

Tracing is a feature of .NET, so the trace levels are set in the .NET application configuration file for DssHost, which is bindsshost.exe.config. Enabling tracing at one level also enables all trace messages at a lower level. The default trace levels should be appropriate. If you are interested in changing the tracing behavior, read the Visual Studio help on the subject.

Each of these methods has several overloads. You can use the Object Browser to look at their definitions. Alternatively, type LogInfo into a Visual Studio source window, place the cursor over it, and press F1. This invokes the online help for Visual Studio, but since the V1.5 Refresh, the MRDS Class Reference is also available via this method, so you will see a description of LogInfo. However, most of the Class Reference is automatically generated and it might not give you much more information than the Object Browser.

The primary advantage of using tracing is that it can be enabled or disabled without recompiling the code.

Where to Go from Here

The TeleOperation service covers a wide variety of tasks that a service can perform, but it is by no means complete. You can use it as a starting point for building your own applications, or continue to improve it.

Another service called Drive-By-Wire is included with the code for the book. It is an abbreviated version of TeleOperation designed to run on a PDA. For example, it works with the Boe-Bot if you have a PDA with built-in Bluetooth. However, it won't work with a LEGO NXT on a Dell Axim 50v because the NXT requires a baud rate of 115200, and Bluetooth on the Axim will not run that fast.

Here are a few more enhancements for the TeleOperation service that you might consider:

  • Add controls to one of the WinForms (and the necessary code) to change the camera resolution, or select a different camera. Remember to add this information to the state so it is written into the config file. On startup, set the resolution based on the config file.

  • Add a drop-down list to select the game controller. (This is already implemented in the Dashboard service, so you can cheat and look in there).

  • Allow the Webcam View window to be resized, and adjust the size of the PictureBox so that the aspect ratio is maintained. You could send a message to the main service to change the camera resolution to try to match the window size.

  • Add some color or blob tracking code so the robot can follow an object. A sample Blob Tracker service is included with MRDS—look in samplesMiscBlobTracker.

  • Incorporate some vision processing using the MRDS services from RoboRealm (www.roborealm.com).

The possibilities are endless! Let your imagination run free, and remember that you can use the simulator if you don't have real hardware.

Summary

Many services do not require a user interface. Those that do have two options: Windows Forms or Web Forms (using XSLT to format the state information). Both of these were covered in detail in this chapter. This chapter has also shown you how to use a web camera as part of the TeleOperation service for remotely driving a robot.

This is the end of the introductory part of the book. It has covered all the basics of MRDS, and by now you should be comfortable writing your own services.

The next part of the book discusses using the MRDS simulator. It offers a great environment for testing services without a real robot. This is a significant benefit if you don't have a lot of money. It also means that if you make a mistake and crash your robot, it doesn't matter.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.220.16