Chapter 3. Decentralized Software Services (DSS)

The Decentralized Software Services (DSS) are responsible for controlling the basic functions of robotics applications. As explained in the previous chapter, DSS is built on top of the CCR. There is a DSS base class from which all services are derived, and this draws heavily on the features of the CCR.

DSS is responsible for starting and stopping services and managing the flow of messages between services via service forwarder ports. In fact, DSS itself is composed of several services that load service configurations; manage security; maintain a directory of running services; control access to local files and embedded resources such as icons; and provide user interfaces through web pages that are accessible using a web browser.

DSS uses a protocol called, not surprisingly, DSS Protocol (DSSP). The specification for DSSP can be used for free under the Microsoft Open Software Promise. DSSP is based on the Representational State Transfer (REST) model, which should be a familiar model for anyone who has worked in web development.

In some ways, such as handling errors using faults, DSSP is similar to SOAP (Simple Object Access Protocol), which is used by web services. However, unlike web services, DSSP separates state from behavior and represents all access to a service as operations on the state. In contrast, the state of a web service is hidden. Making the state of a robot fully visible, especially sensor values, is essential to writing control behaviors for the robot. A key feature of both models is asynchronous event notifications, which are important in the dynamic environment of robotics.

In this chapter, you learn how to build services, including the fundamental concepts. Like the last chapter on the CCR, this one is full of new terminology, and it may take you a little while to become completely familiar with DSS.

Overview of DSS

As you read through this chapter, you might find that there is too much information to absorb in one go. Don't panic! Read through the chapter in its entirety, and then come back and hit the highlights later. The repetition throughout the chapter is deliberate. Over time you will begin to absorb the concepts by osmosis.

The basic building block in MRDS is a service. Services can be combined (or composed) as partners to create applications. This process is referred to as orchestration.

An example of a complete application is shown diagrammatically in Figure 3-1. This Visual Explorer application uses a camera for the robot to explore its environment. The robot hardware is accessed via three generic contracts that describe the application programming interfaces (APIs) for a differential drive (two independently driven wheels), a set of bumpers, and a web camera. The Visual Explorer service does not have to know anything about the hardware in order to use these services.

Figure 3-1

Figure 3.1. Figure 3-1

At the next level down, the motor service talks to the robot brick service, which in turn communicates directly with the robot. (A brick service is the core component for a robot.) In this example, the web camera is mounted on the robot and controlled via the brick. However, image processing, which can be a very CPU-intensive task, is carried out on a different computer (DSS Node B). Computer vision packages for MRDS such as RoboRealm (www.roborealm.com) or VOLTS-IQ from Braintech (www.volts-iq.com) could be used for this task.

The services that make up an application can be created and destroyed dynamically. However, in most cases they are specified at startup through a manifest. This is an XML file that lists the required services and their partner relationships. Figure 3-1 shows the Visual Explorer service partnered with the Vision Processing, Differential Drive, Bumper Array, and Web Camera services. The dotted connection to the Robot "Brick" is intended to indicate that sometimes you have to bypass other services and go directly to the hardware to access devices such as LEDs, buzzers, and so on, which are not exposed through generic contracts.

One of the objectives of MRDS is to provide an environment where services are loosely coupled. In Figure 3-1, a different type of robot could be substituted for the existing one and Visual Explorer would not even know, as long as the new robot implemented the same generic contracts. Of course, in practice it is a little more complicated than this because the new robot might have motors that drive faster, a different configuration of bumpers, and a higher-resolution web camera. Nonetheless, the principle still stands.

A service consists of several components:

  • Contract: This defines the messages you can send to a service, as well as a globally unique reference, called the contract identifier, which identifies the service and is expressed in the form of a URI (Universal Resource Identifier)

  • Internal state: Information that the service maintains to control its own operation

  • Behaviors: The set of operations that the service can perform and that are implemented by handlers

  • Execution context: The partnerships that the service has with other services, and its initial state

Figure 3-2 shows all these components diagrammatically. They are discussed in more detail in the following section, and then covered again later in the chapter when you build your first service. Repetition is the key to learning!

Figure 3-2

Figure 3.2. Figure 3-2

Contracts

A contract identifier is used to uniquely identify different services. When you create a new service, as explained next, a contract identifier is created for you. For example, here is the contract identifier from the Dance service in Chapter 14:

/// <summary>
/// Dance Contract class
/// </summary>
public sealed class Contract
{
    /// <summary>
    /// The Dss Service contract
    /// </summary>
    public const String Identifier =
            "http://www.promrds.com/contracts/2007/10/dance.html";
}

Every service must have a class called Contract, and it must contain a field called Identifier. MRDS uses URIs in the following format:

http://somehost.domain/path/year/month/servicename.html

This looks like a valid URL, but it does not have to exist on the Internet. By default, the host name is www.promrds.com, but most of the Microsoft services use the host and path schemas.microsoft.com/robotics. The convention of using the year and month in which the service was created helps to keep the URIs unique and enables new versions of the same service to be built.

The important point to note is that the URI must be unique. You can use any host name or/path directory, that you want. This book uses a real web prefix, www.promrds.com/contracts/. Be aware, however, that the URI should be all lowercase or there can be problems with case sensitivity.

The programmatic interface to a service is defined as part of the contract via a set of classes that describe the types of messages to which a service can respond. The objective is to keep this interface clean by only using data structure definitions, and no methods. In particular, there must be at least one PortSet that is public and contains ports for all of the available message types. (Remember that messages are just data types, i.e., classes).

For example, here is the main operations port for the Dance service (although it is called a port, it is actually a PortSet):

/// <summary>
/// Dance Main Operations Port
/// </summary>
[ServicePort()]
public class DanceOperations: PortSet<DsspDefaultLookup, DsspDefaultDrop, Get>
{
}

Notice that this PortSet is "decorated" with the [ServicePort] attribute, which marks it is as an operations port. MRDS makes extensive use of such attributes to help build the necessary infrastructure for services declaratively. The compiler takes care of generating any code that might be necessary, so you don't have to worry about it.

DanceOperations contains ports for three types of messages: DsspDefaultLookup, DsspDefaultDrop, and Get. A Lookup operation enables other services to find out information about this service; Drop is used to shut down the service; and Get is used for retrieving a copy of the service's state. This is the set of message types created automatically when you create a new service.

State

The internal state of a service is a class containing properties that are important to the operation of the service. Some properties are effectively constants for the duration of the service execution. This can include parameters such as the COM port that is used to communicate with a robot. By making this available externally, it is possible to reconfigure the service without recompiling it.

Other state properties change over the lifetime of the service. For example, you might implement a finite state machine (FSM) to control a robot as it wanders around (see Chapter 16). Then you can define the current state of the robot as Drive Straight, Turn Right, Back Up, and so on, and store this in a variable in the service state. (Do not confuse "state" in the FSM with the "state" of a service. In general, the FSM state is a small subset of the overall service state.) In this case, the state changes in response to external events, such as detecting an obstacle in the path of the robot.

Lastly, some properties in the state can represent things happening in the real world, e.g., values obtained from sensors, user input, and so on.

Here is the state from the Stinger Drive By Wire service in Chapter 16:

[DataContract]
public class StingerDriveByWireState
{
    // The Headless field indicates if we should run without a GUI
    private bool _headless;

    [DataMember]
    public bool Headless
    {
        get { return _headless; }
        set { _headless = value; }
    }

    // Can run with the Motor disabled for testing
    private bool _motorEnabled;

    [DataMember]
    public bool MotorEnabled
    {
        get { return _motorEnabled; }
        set { _motorEnabled = value; }
    }
private bool _wanderEnabled;

    [DataMember]
    public bool WanderEnabled
    {
        get { return _wanderEnabled; }
        set { _wanderEnabled = value; }
    }

    private WanderModes _wanderMode;

    [DataMember]
    public WanderModes WanderMode
    {
        get { return _wanderMode; }
        set { _wanderMode = value; }
    }

    private int _wanderCounter;

    [DataMember]
    public int WanderCounter
    {
        get { return _wanderCounter; }
        set { _wanderCounter = value; }
    }

    private double _irLeft;

    [DataMember]
    public double IRLeft
    {
        get { return _irLeft; }
        set { _irLeft = value; }
    }

    private double _irFront;

    [DataMember]
    public double IRFront
    {
        get { return _irFront; }
        set { _irFront = value; }
    }
    private double _irRight;

    [DataMember]
    public double IRRight
    {
        get { return _irRight; }
        set { _irRight = value; }
    }
 }

The StingerDriveByWireState class defines the internal state of the service. Notice that it is also part of the service contract because it has the [DataContract] attribute. When another service issues a Get request, it knows from the contract exactly what information it will receive in the response message.

DSS nodes implement a web server so you can use a web browser and enter the URL for a service to see its state. Requesting a web page from the service executes an HttpGet operation, which returns an XML file. Being able to examine the state of a service remotely using a web browser gives you some insight into the service without having to run it in the debugger.

The state can be stored in a saved state or config file, which is an XML file. This can be reloaded the next time that the service is run so it "remembers" parameter settings. An example of a config file is shown here for the Stinger Drive By Wire service:

<?xml version="1.0" encoding="utf-8"?>
<StingerDriveByWireState xmlns:s="http://www.w3.org/2003/05/soap-envelope"
xmlsn:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlsn:d="http://schemas.microsoft.com/xw/2004/10/dssp.html"
xmlsn="http://schemas.tempuri.org/2007/11/stingerdrivebywire.html">
  <Headless>false</Headless>
  <MotorEnabled>true</MotorEnabled>
  <WanderEnabled>false</WanderEnabled>
  <WanderMode>None</WanderMode>
  <WanderCounter>0</WanderCounter>
  <IRLeft>61.3</IRLeft>
  <IRFront>80</IRFront>
  <IRRight>79.8</IRRight>
</StingerDriveByWireState>

The state has values that are supposed to be set by editing the saved state file: Headless, MotorEnabled, and WanderEnabled. It also contains internal information that provides some insight into how the Wander behavior is running: WanderMode and WanderCounter. The last three fields are the IR sensor values, obtained from other services but included here to make them easier to see.

The WanderMode controls the internal FSM during wandering. It uses an enum that is also part of the contract (because it has a [DataContract] attribute too):

// Modes that the robot can be in while wandering
[DataContract]
public enum WanderModes
{
    None,
    DriveStraight,
    VeerLeft,
    VeerRight,
    TurnLeft,
    TurnRight,
    BackUp
}

When the state is serialized (into XML), the WanderMode appears as one of the names in the enum. There is no need to memorize "magic" numbers so you can tell what is happening when you view the state. In your code, you simply use the symbolic values, such as WanderModes.DriveStraight.

You can poll a service by requesting its state on a regular basis using a Get operation. This places the responsibility on your service to request the updates (and wait for them to arrive). Determining an appropriate polling interval is one of the challenges of robotics. If you poll too fast, you might overload the other service, or simply receive the same old stale information. If you don't poll often enough, you might miss important information, such as a looming wall, and then crash!

One of the key features of DSSP is that messages can be sent asynchronously, i.e., unsolicited. These are referred to as notifications. Services can offer subscriptions to their state information, so you can avoid polling. (Subscriptions are covered in Chapter 4).

For example, a laser range finder (LRF) produces range scans about once a second. Services that are interested in this data can subscribe to the LRF service and receive notifications when new range scan data becomes available. (The LRF data is part of the service's state, so the LRF service is sending state update notifications.) These updates continue to arrive until one of the services is shut down or the subscriber decides to unsubscribe.

Note that the LRF service sends the range data as part of the notification message. However, a Webcam service simply sends a message to say that a new frame is available because of the large amount of data involved. It is then up to the recipient to request the frame. If the recipient is slow and misses a couple of updates, very little bandwidth is wasted because the images are not sent in notifications anyway.

Putting this all together, the diagram shown in Figure 3-3 depicts a sequence of messages passing between hypothetical services.

Figure 3-3

Figure 3.3. Figure 3-3

In Figure 3-3, note the following:

  • Service A generates new information and sends it to Service B as an Update request. Perhaps Service A has obtained a new sensor reading. An Update only changes part of the state, as opposed to Replace, which overwrites the entire state.

  • Service B realizes that in order to complete the update, it needs another piece of information from Service C, so it makes a Query request. (This is somewhat artificial, but just play along.) A Query is used to retrieve part of the state, whereas a Get returns the whole state.

  • Service C responds to Service B, so then Service B can respond to Service A. The Update transaction is now complete. However, the state of Service B has changed and it has two subscribers: Service C and Service D. Therefore, it sends notification messages to both of these services. Note that (apart from the original subscription message) Services C and D just wait patiently and do not send any messages.

Note two distinct patterns in Figure 3-3. One is the classic request/response pattern. You have the option to wait for the response or ignore it. The response could be a Fault if the other service encountered an error processing the request. You can also explicitly set the request's TimeSpan property so that it times out if the other service doesn't respond.

The other pattern is asynchronous notifications to multiple subscribers. The subscribers first request notifications (these messages are not shown in Figure 3-3) and then they continue to receive updates whenever they become available. No acknowledgment is required for notifications.

Behavior

Service behavior describes what the service actually does. Behavior includes the algorithms that a service uses to achieve its purpose, such as wandering around without bumping into obstacles. Orchestration is a form of behavior. Behavior also includes simple functions such as returning the state when it is requested.

All services implement a certain minimum set of operations that are defined when you create a new service (as shown earlier in "Contracts"), so even an "empty" service has some behavior. An operation is a function that a service can perform. Operations are defined by the ports that represent them, and ports use particular classes (or data types) for their messages. This is discussed in more detail below.

This terminology gets a little messy. Sometimes operations are also referred to as requests or responses—a request occurs when a message is sent to a service (using a specified data type associated with a port), and a response is sent back by the service (using another data type).

Note that operations are not required to send a response; it is up to you as the programmer to define whether or not a response is sent. At runtime, you can also set ResponsePort to null in the request—that is, you can explicitly say that you don't want a response. DSSP uses TCP/IP or HTTP, both of which are reliable transports, so there is no need to acknowledge every message.

However, if you anticipate that a request might fail and you want to know about it, then you must specify a response port (and set up a receiver to read from the port) or set up a causality. Causalities are like structured exception handling on steroids. They allow exceptions that occur anywhere along the execution path of a message sequence to be posted back to a single port. These exceptions might even occur concurrently due to the multi-threaded nature of the CCR.

Internally, a service uses handlers to process operations. Handlers operate asynchronously and possibly in parallel. Most handlers are controlled by a CCR Interleave (called MainPortInterleave), which is created automatically for you. As shown in the previous chapter, the handlers attached to an interleave can belong to the Exclusive or Concurrent groups (or the special case called TearDown, which is usually just for Drop messages).

Here is a handler for a Get operation that retrieves the state from a service:

/// <summary>
/// Get Handler
/// </summary>
/// <param name="get"></param>
/// <returns></returns>
[ServiceHandler(ServiceHandlerBehavior.Concurrent)]
public virtual IEnumerator<ITask> GetHandler(Get get)
{
    get.ResponsePort.Post(_state);
    yield break;
}

This is a very simple handler—it just sends back a copy of the service state. It is annotated with the [ServiceHandler] attribute, which specifies that it belongs to the Concurrent group. This handler is allowed to execute simultaneously with other Concurrent handlers, but it is not allowed to execute while an Exclusive handler is running.

In effect, operations are the APIs that can be used to query and manipulate a service, e.g., retrieve (Get) or change (Replace, Update) the state. In a traditional programming environment, you link your code against a library of subroutines and use local procedure calls (the APIs for the library) to execute the functions in the library. In MRDS, operations are executed via SOAP requests, which means that they are similar to remote procedure calls and can therefore be executed on another computer anywhere in the network.

If an unhandled exception occurs during the execution of a handler, then DSS automatically creates a Fault and returns it as the response. However, if you are running the code in the debugger, it catches the exception and suspends execution. You can simply continue executing the code so DSS gets a chance to handle it.

Note that the contract is embodied in a Proxy DLL that is created automatically when you compile your service by a program called DssProxy. (Because .NET assemblies are self-describing, you can find out about the contract using reflection, but that is another topic.) You do not make calls directly to the service DLL; instead, you call routines with the same signatures in the Proxy DLL.

When you set up references to other services in Visual Studio, as a general rule you should always link to the Proxy DLL, rather than the service implementation DLL.

If you understand web services, then you know that parameters that are passed to and from services have to be marshaled, i.e., converted to a neutral format that both the client and the server understand. For web services, this format is XML. Because DSSP is based on SOAP, it also uses XML for requests over HTTP. However, for direct communication using TCP/IP, a binary serialization is used that is 10 to 20 times faster. (When you get to the "Running a DSS Node" section, you will see that two ports are specified when you start DSS: one for HTTP requests and one for direct TCP/IP connections.)

Execution Context

The context of a service includes all of its partners. These are other services that it relies on to do its job. The obvious examples for a robot are the motors that drive the wheels, infrared sensors, and so on. A service can have as many partners as it likes, and the partnerships can be dynamic.

By design, partners do not have to be on the same computer. Each computer runs an instance (or possibly multiple instances) of a DSS node. A service directory is maintained in each DSS node. As you have already seen, one of the possible operations on a service is a lookup.

In most cases, partnerships last for the duration of the service execution. Therefore, they are often established declaratively in the code (using attributes). However, MRDS has a concept of generic contracts. These types of contracts specify the operations that a conforming service must support. At runtime, any service that complies with the generic contract can be used as a partner. The association between the actual service and the generic service is done using a manifest, which is an XML file that uses a schema defined by Microsoft.

A common example is the generic differential drive service for two-wheeled robots. The manifest can connect a service to the drive on a Boe-Bot, or a LEGO NXT Tribot, or a Stinger, and so on, and the service should have no idea what type of robot it is talking to.

One case where partnerships are established on demand is when you use the Dashboard (which is described in Chapter 4). In order to do this, the Dashboard must query the service directory to find services that implement generic contracts that it understands, such as the differential drive, game controller, laser range finder, webcam, and so on. The execution context of the Dashboard, therefore, changes over time.

The following manifest starts a Boe-Bot and the Dashboard. It runs the BASICStamp2 service, which is the "brick" or "brain" on the robot; the BSDrive service, which controls the wheels; and the Dashboard:

<?xml version="1.0" encoding="utf-8"?>
<Manifest
     xmlns="http://schemas.microsoft.com/xw/2004/10/manifest.html"
     xmlns:dssp="http://schemas.microsoft.com/xw/2004/10/dssp.html"
     >

  <CreateServiceList>

     <!—Start BasicStamp2 Brick—>
     <ServiceRecordType>
       <dssp:Contract>http://schemas.microsoft.com/robotics/2007/06/basicstamp2.html
       </dssp:Contract>
       <dssp:PartnerList>
         <!—Initial BasicStamp2 config file—>
         <dssp:Partner>
           <dssp:Service>Parallax.BoeBot.Config.xml</dssp:Service>
           <dssp:Name>dssp:StateService</dssp:Name>
         </dssp:Partner>
       </dssp:PartnerList>
     </ServiceRecordType>

     <!—Start the BoeBot drive service—>
<ServiceRecordType>
       <dssp:Contract>http://schemas.microsoft.com/robotics/2007/06/bsdrive.html
       </dssp:Contract>
       <dssp:PartnerList>
         <!—Initial Drive Configuration File—>
         <dssp:Partner>
           <dssp:Service>Parallax.BoeBot.Drive.Config.xml</dssp:Service>
           <dssp:Name>dssp:StateService</dssp:Name>
         </dssp:Partner>
       </dssp:PartnerList>
     </ServiceRecordType>

     <!—Dashboard—>
     <ServiceRecordType>
       <dssp:Contract>http://schemas.microsoft.com/robotics/2006/10/dashboard.html</dssp:Contract>
     </ServiceRecordType>

   </CreateServiceList>

</Manifest>

You can clearly see each of the services listed inside a ServiceRecordType. The BASICStamp2 and BSDrive also partner with the StateService, which is responsible for loading the initial state from the specified config file.

Don't worry about manifests at this stage. MRDS includes a Manifest Editor that makes creating them relatively easy. This is discussed later in the chapter.

DSSP Service Operations

The DSSP service model defines many operation classes. Luckily, most of these you will never use in an MRDS service. (If you are interested, you can read the full DSSP specification, available at http://download.microsoft.com/download/5/6/B/56B49917-65E8-494A-BB8C-3D49850DAAC1/DSSP.pdf).

The full list of DSSP operations is shown in the following table. You do not need to know all of these operations, but they are included here for completeness.

Operation

Description

Create

Creates a new service

Delete

Deletes (part of) service state

Drop

Terminates a service (actually, requests the service to terminate itself)

Get

Retrieves a copy of the service state

Insert

Adds new information to the service state

Lookup

Retrieves information about the service and its context

Query

Similar to Get but with additional parameters to allow structured queries. (Returns a subset of the state.)

Replace

Replaces the entire service state

Subscribe

Requests notification of all state changes

Submit

Special case of an Update that does not necessarily change state (typically used with Web Forms)

Update

Retrieves information about the service and its context

Upsert

Performs an Update if the state information exists, otherwise an Insert

The only mandatory operation according to the DSSP specification is Lookup. However, for practical reasons, MRDS always creates services with Lookup, Drop, and Get. A service that only implemented Lookup would be pretty useless.

The DSS runtime provides a wrapper, CreateService, for the Create operation. Lookup operations are a little more complicated, but can be handled by requests to the Directory service.

All of the operations are based on the generic DsspOperation class:

public class DsspOperation<TBody, TResponse>

To define your service operations, you subclass one of the standard DSSP operations and supply your own body type (which is the request) and a PortSet for the response.

An example will help to clarify this. The Hemisson services in Chapter 16 have a request to read the IR sensors. It is defined as follows:

[DisplayName("GetSensors")]
[Description("Gets the state of the infrared sensors")]
public class QueryInfraRed: Query<SensorsRequest, PortSet<Sensors, Fault>>
{
}

[Description("Requests sensor data")]
[DataContract]
public class SensorsRequest
{
}

This new operation type, QueryInfraRed, is based on the Query class because it only retrieves part of the state. When a new instance is created, the Body field contains a variable of type SensorsRequest, and the ResponsePort field is a PortSet that returns either a Sensors object or a SOAP Fault. (The Sensors class is not shown here. It contains all of the IR sensor values.)

A key point to note is that the request type must be unique among all of the types in the main operations PortSet. If you tried to implement requests using an int for several of them, only the first one in the PortSet would ever get any messages.

This new class must be added to the main operations port for the Hemisson services, and then a handler can be written as follows:

/// <summary>
/// QueryInfraRed Handler
/// </summary>
/// <param name="query"></param>
/// <returns></returns>
[ServiceHandler(ServiceHandlerBehavior.Concurrent)]
public virtual IEnumerator<ITask> QueryInfraRedHandler(QueryInfraRed query)
{
    query.ResponsePort.Post(_state.Sensors);
    yield break;
}

Notice that this handler returns only the Sensors portion of the state. How this sensor information is updated is irrelevant here. It is the responsibility of the service to obtain this data from the robot and make it available.

The [ServiceHandler] attribute flags this method as being a handler, and the QueryInfraRed parameter indicates what type of operation it handles. The attribute also specifies which interleave group the handler should be in.

This behavior is in the Concurrent interleave group. This means that it is allowed to execute in parallel with other Concurrent handlers. However, it cannot execute while an Exclusive handler is running.

The third group of handlers are TearDown handlers. Usually this group only contains the Drop handler. TearDown handlers are not only exclusive; they also prevent further execution of any other handlers. The last thing that a service must do when it receives a Drop request is to post back a response.

The QueryInfraRed example shows a request that basically has an empty body and returns some information. The opposite is a request that sends information but does not expect any information in the response. The PlayTone operation from the Boe-Bot (see Chapter 14) is an example:

[DisplayName("PlayTone")]
[Description("Plays a tone on the speaker on the Boe-Bot.")]
public class PlayTone: Update<Tone, PortSet<DefaultUpdateResponseType, Fault>>
{
}

The PlayTone operation sends a Tone object as the request body, but it only receives a DefaultUpdateResponseType to indicate success (or a Fault for failure). There are pre-defined classes for the default responses to most of the operation classes so that you do not have to bother defining response types for all of your operations.

The Tone class is marked as part of the data contract. It also specifies two data members as the parameters for a constructor, Frequency and Duration:

[Description("Sound a Tone on the Speaker.")]
[DataContract]
public class Tone
{
    // Parameters
    private int _frequency;
    private int _duration;

    [DataMember, DataMemberConstructor(Order = 1)]
    [Description("Frequency in Hz (rounded to nearest 50Hz).")]
    public int Frequency
    {
        get { return this._frequency; }
        set { this._frequency = value; }
    }

    [DataMember, DataMemberConstructor(Order = 2)]
    [Description("Duration in Milliseconds (rounded to nearest 50ms).")]
    public int Duration
    {
        get { return this._duration; }
        set { this._duration = value; }
    }
}

As a convenience, the Proxy generator (discussed later in the section "Proxy Assemblies") will create appropriate helper methods on the proxy operations port so that you can write the following command (assuming _stampPort is already set up as the operations port):

_stampPort.PlayTone(3000, 500);

This constructs a new Tone object and sends it to the Boe-Bot.

Because the PlayTone operation returns a PortSet, you can also use it in a Choice:

yield return
      Arbiter.Choice(
          _stampPort.PlayTone(3000, 500),
          delegate(DefaultUpdateResponseType d)
          { },
          delegate(Fault f)
          {
              Console.WriteLine("Play Tone failed: " + f.Reason);
          }
      );

This has the added benefit that execution will not continue until the PlayTone request has been completed, and of course it is also defensive programming because it explicitly flags errors.

Generic Contracts

A generic contract is similar to an abstract class, but MRDS does not implement class inheritance in the usual sense. Generic contracts have no implementation of their own. They consist solely of the type definitions required for the contract identifier, the state, and the main operations port.

To implement a generic service, you must build a service based on the generic contract. This can be done most easily using the /alt qualifier to DssNewService to specify the generic contract that should be used. This is covered in more detail in Chapter 4.

The implementation service has two operations ports: one for the new service itself and one that receives requests based on the generic contract. In your source code for the new service, you must implement handlers for all of the operations defined in the generic service contract. There is no requirement to add more handlers to the new service (apart from Lookup, Drop, and Get, which all services have). The simplest implementation, therefore, consists only of handlers for the generic operations, in which case you do not need your own operations port.

The reason for defining generic contracts is to try to make MRDS services hardware-independent in the same way that device drivers hide hardware details from an operating system.

Running a DSS Node

A program called DssHost.exe is responsible for implementing the run-time environment for MRDS. In the text, references to DSS usually mean the environment created by DssHost. In Figure 3-1, the DSS nodes are instances of DssHost running on a Windows-based computer (or computers). The online documentation contains a full description of DssHost. You can also get abbreviated help from DssHost itself, as described in this section, which explains how to start DssHost and get information from it. DssHost implements a web server so that you can interrogate DSS and running services using a web browser. This is a novel approach that means you can examine and manipulate the state of services running in a DSS node across the network without installing any special client software to do it.

When you start a DSS node, several services are started automatically. You will explore most of these services in this chapter. They include the following:

  • Console Output: Captures and filters informational and error messages

  • Constructor: Creates new service instances

  • Control Panel: Provides an interface to start and stop services manually

  • Manifest Loader: Interprets manifests to set up the services comprising applications

  • Mount Service: Provides access to the local file system (only under the MRDS folder)

  • Embedded Resource Manager: Exposes icons, bitmaps, XSLT files, and other resources

  • Security Manager: Manages the security on the DSS node

  • Service Directory: Maintains a list of available services on the DSS node

  • State Partner Service: Reads configuration files when services start

Directory Structure of a DSS Node

The layout of a DSS node is as follows:

Root directory (or Mount point)
    Bin
    Store
        Logs
        Media
        Styles
        Transforms

When you deploy the MRDS runtime to another computer, this is the minimal directory structure that is created. If you have a full installation of MRDS on your computer, then there are many more directories but they are not required to run MRDS.

In a web browser, you can go to the Service Directory (as explained below) and click on the mountpoint service to see the local path where MRDS is installed, which is referred to as the root directory or the mount point. The mountpoint service can be used in your code to access local files, but it only allows you to access files below the MRDS root directory.

You can find out where your local DSS node is in your C# code using the LayoutPaths static class, as shown in this example:

string logdir = LayoutPaths.RootDir + LayoutPaths.LogDir;

This will give you the full path to the Logs directory if you want to write your own log file.

File I/O operations are synchronous—that is, they block the calling thread. Therefore, you should not usually perform file I/O directly unless you know that the operations are very quick. For long operations, you might need to create a separate thread.

Starting DssHost

Open an MRDS Command Prompt window by clicking Start

Starting DssHost
C:Microsoft Robotics Studio (1.5)>DssHost /?

Now start DssHost on its own (with no services) using the following command:

C:Microsoft Robotics Studio (1.5)>DssHost /p:50000 /t:50001

The output from DssHost should look like Figure 3-4, which shows that the Directory and Constructor services started, but no manifest was supplied (so nothing else started).

Figure 3-4

Figure 3.4. Figure 3-4

DssHost requires you to specify at least the port parameter (abbreviated to /p). In the example in Figure 3-4, the tcpport is also specified with /t. At first glance this is a little confusing. The "port" numbers you give to DssHost have nothing to do with CCR ports, and why do you need two ports?

The port parameter supplies what might more appropriately be called the HTTP port, and the tcpport parameter is the SOAP port. DssHost includes a web server that can be used to examine and control services. This web server uses the port parameter. In addition, a service receives requests (SOAP messages) through its service port, which is the tcpport parameter.

There is nothing magical about the numbers 50000 and 50001 other than the fact that they are above the range of "well-known" TCP/IP ports. In fact, in the early days of MRDS, the documentation used 40000 and 40001 in many cases.

Exploring the DSS Web Pages

Once DssHost is running, start up a web browser and enter the following URL in the address bar: http://localhost:50000.

Because you will make frequent use of a web browser to examine services, it is a good idea to save a shortcut (or favorite) for this web page once it is displayed. Notice that the port number in the URL is the (HTTP) port that you specified on the command line when you started DssHost.

You should see something like what is shown in Figure 3-5. Notice that there are several options in the menu at the left-hand side of the window. Each of these options is explained briefly below. Try them all out for yourself. You can investigate the Developer Resources and About Microsoft Robotics Studio sections of the menu on your own.

Figure 3-5

Figure 3.5. Figure 3-5

The Control Panel

The Control Panel displays all of the available services. When you select the Control Panel, it might take a little while before anything is displayed because it initiates a refresh of the directory cache. Eventually, the Control Panel displays a list of all the available services. You can manually start services from this list.

You can narrow down the list of services by typing in a search string. Figure 3-6 shows the results of entering "cam," which indicates all services with "cam" in their name or description, which are obviously cameras.

Figure 3-6

Figure 3.6. Figure 3-6

In the drop-down list beside each service, you can find a list of all of the manifests that DSS found that refer to the service. You can either select one of these manifests or just leave the drop-down set on <<Start without manifest>> and click the Create button to start a new instance of the service. If you have a web camera, make sure that it is plugged into your PC and click the Create button beside the Webcam service. (Don't select a manifest.)

The Service Directory

A new Webcam service will be started on your computer. You can verify this by selecting Service Directory from the menu (see Figure 3-7).

Figure 3-7

Figure 3.7. Figure 3-7

Every DSS node has a service directory that lists the currently running services. Two Webcam services are listed (the second one is cut off a little at the bottom of the window in Figure 3-7) because there is a generic Webcam contract and the service implements both the generic contract and its own contract. Notice that there are several other services as well, such as the manifest loader, console output, and the Control Panel.

Click the Webcam service in the Service Instance list to see what the output from the service looks like. An example is shown in Figure 3-8. If you look carefully in the address bar, you might be able to see that the URL is http://localhost:50000/webcam/c7354581-c28f-4b4c-b419-d5b7ea06aab9. Before you panic, assuming you have to know these magic numbers, it should be pointed out that they change every time you run a service—they are only there to make service URIs unique. The main point is that the Webcam service can be accessed just by appending its name to the end of the DSS node URL; the numbers are not necessary unless there are multiple instances of a service.

Figure 3-8

Figure 3.8. Figure 3-8

The Webcam service formats its output when you make a HttpGet request to the service and presents it as a Web Form. The form enables you to set the camera parameters, including refresh interval, display format, and capture format (image resolution). You can also select from several cameras if you have more than one connected to your PC. Finally, you can run the viewer continuously or refresh the image manually using the Start, Stop, and Refresh buttons. This is an advanced example of a web interface to a service.

The Debug and Trace Messages Page

Select Debug and Trace Messages from the menu. You should see something like what is shown in Figure 3-9.

Figure 3-9

Figure 3.9. Figure 3-9

The Console Output, as it is known (the URL is http://localhost:50000/console/output), lists all of the informational and error messages that are generated by services running in the DSS node. You can use this to assist you with debugging.

Don't be confused by the name "console output" because any messages that you write using Console.WriteLine do not appear on this page.

Notice that you can select the level of messages in the Filters section. This is one of the advantages of using LogInfo and the other methods: You can select at runtime what level of messages you want to see. Another advantage is that you can expand the information available by clicking the small, downward-pointing arrow in the View column beside each message. This shows you information from the stack, including the name of the source file and the line number. You might find this useful if you can't remember where a particular message comes from.

The default trace levels are set in the DssHost application configuration file, which is in the MRDS bin folder and is called dsshost.exe.config. (This is really a .NET feature, not MRDS.) Recall that the config file is an XML file, so you can open it in Notepad. It contains some helpful comments.

You can also enable timeout tracking on all messages in the config, so that you can detect services that are not responding to messages. You can also log all message traffic if you really want to!

The Manifest Load Results option is covered shortly because there is no manifest loaded at the moment.

The Contract Directory

Next, click the Contract Directory menu option. This displays a screen like the one shown in Figure 3-10. This is not very exciting; it just tells you where the services reside on the local hard drive.

Figure 3-10

Figure 3.10. Figure 3-10

The Security Manager Page

Select the Security Manager from the menu. This displays a Web Form. If you click the Edit button, the screen should look like the one shown in Figure 3-11.

Figure 3-11

Figure 3.11. Figure 3-11

Some users have experienced problems running DssHost due to firewall or security settings on their computers. This is most likely to happen if you are not logged in as an Administrator on your PC.

The Security Manager page shows the contents of the file storeSecuritySettings.xml, which you can edit if you wish. (If you cannot find the file, then the default security settings will be in force.) However, there is another part to the puzzle: The name of the security settings file is set in the .NET application configuration file for DssHost (which was mentioned above)—namely, bindsshost.exe.config. If you look in this file you should find a key called Security:

<!--Comment the line below to disable security-->
<add key="Security" value=".storeSecuritySettings.xml"/>

Security is discussed in more detail in the online documentation. You can also search the Discussion Forum if you have problems.

Resource Diagnostics

The last menu option is Resource Diagnostics. This page shows information about each of the dispatchers (and their queues) running in the DSS node. You might find this information helpful when trying to diagnose a problem. An example is shown in Figure 3-12.

When you have finished exploring DSS in the web browser, enter Ctrl+C in the MRDS Command Prompt window or simply close down the window. This is a nasty way to shut down DssHost, but it works.

Figure 3-12

Figure 3.12. Figure 3-12

Running a Robot Service

To run a robot service, you need to supply a manifest on the command line. Several manifests are supplied for you in the MRDS samplesConfig folder. After you have installed the code that comes with this book, you will also have a folder called ProMRDSConfig.

You will no doubt find yourself typing the same DssHost command over and over again as you test your services. You might want to create batch files to run various services. If you place them into the MRDS bin folder, then they will be on the search path and will be found automatically when you type a command in an MRDS Command Prompt window. These files are fairly simple, but they save you a lot of time.

For example, here is a batch file that runs a Boe-Bot (called RunBoeBot.cmd):

@ECHO ON
REM Run a Parallax Boe-Bot
REM Type Ctrl-C in this window when you want to stop the program.
dsshost -port:50000 -tcpport:50001 
Running a Robot Service
-manifest:"../ProMRDS/Config/Parallax.BoeBot.manifest.xml"
Running a Robot Service
-manifest:"../ProMRDS/Config/Dashboard.manifest.xml"

The DssHost command is all on one line in the batch file, not wrapped as it appears here in print.

Two manifests are specified on the command line: the Boe-Bot and the Dashboard. This is because the two services are not directly related—the Dashboard connects to other services dynamically.

Notice that the paths to the manifests are relative to the location of the batch file. If you place the batch file into a different folder, i.e., not the bin folder, then you will need a relative path to DssHost.exe, and the relative paths to the manifests will be different as well.

To run this batch file, you can just double-click it in Windows Explorer. Alternatively, to run it from an MRDS Command Prompt window, enter the following command:

C:Microsoft Robotics Studio (1.5) >RunBoeBot

Manifest Load Results

Once you have the Boe-Bot running (or your particular robot), start a web browser again and browse to the DSS node at http://localhost:50000. Select Manifest Load Results in the menu; the result should look similar to what is displayed in Figure 3-13. You can expand each manifest to see additional information. Notice that the Boe-Bot manifest loaded the BASICStamp2 and BSDrive services, each with a StateService partner (an initial config file).

Figure 3-13

Figure 3.13. Figure 3-13

Now select the Service Instance Directory (see Figure 3-14).

Figure 3-14

Figure 3.14. Figure 3-14

Viewing Service State

Figure 3-14 indicates that many different services are running. Click the basicstamp2 service to view its state. This output should look like the window shown in Figure 3-15.

Figure 3-15

Figure 3.15. Figure 3-15

Notice in Figure 3-15 that the right infrared sensor specifies true, which indicates an obstacle in the immediate vicinity of the Boe-Bot. This page is nicely formatted because it uses an XSLT (Extensible Stylesheet Language Transform) file to format the XML output from the service.

The state information on the page does not update automatically—you need to keep clicking on the browser's refresh button.

Go back to the Service Directory and click the bsdrive service. The output should look like Figure 3-16. This is raw XML code. If you look back at Figure 3-15, you will see an XML button in the window's top-right corner. You can click this to see the raw XML behind a formatted state page.

Figure 3-16

Figure 3.16. Figure 3-16

The BSDrive service does not have an associated XSLT file to format its output. However, the output is still intelligible. This is one of the advantages of XML—it is human-readable.

That completes the pictorial overview of DSS. It is hoped that the many pages it took to show the screenshots were worth more than several thousand words.

Creating a New Service

As an MRDS programmer, services are your bread and butter. Therefore, it makes sense to begin by discussing exactly what a service is and how it operates. The best way to learn is to make a new service.

In this section you will build two services, called ServiceA and ServiceB. For your convenience, completed versions of these services are supplied in the ProMRDSChapter3 folder.

ServiceA is the "master," and it partners with ServiceB so that it can get information from ServiceB. As you work through the exercises, make sure you add code to the correct service.

Building a Service from Scratch

In this section you build a brand-new service. It is advisable to keep your code separate from the MRDS distribution. The Service Tutorials suggest creating new services under the samples folder, but this results in your code being intermixed with the Microsoft code. All of the code for the book is under the ProMRDS folder, which makes it much easier to pick up and move elsewhere. Furthermore, there is no chance of a conflict if you update your MRDS installation, such as with the MRDS V1.5 Refresh.

If you have not done so already, create a new folder called Projects. In Windows Explorer, browse to the Microsoft Robotics Studio (1.5) folder and make a new folder under it called Projects.

Alternatively, open an MRDS Command Prompt window by running the Command Prompt option from the Microsoft Robotics Studio (1.5) folder in the Start menu. Create a new folder called Projects:

C:Microsoft Robotics Studio (1.5)>md Projects

This is where you should create your own MRDS projects.

Using Visual Studio

The previous chapter explained how to create a new service using Visual Studio. Now you need to create another service. The New Project dialog is shown in Figure 3-17.

Figure 3-17

Figure 3.17. Figure 3-17

To summarize the steps involved:

  1. Open Visual Studio and select File

    Figure 3-17
  2. Select the Robotics project type and the Simple Dss Service (1.5) template.

  3. Make sure that the location is your Projects folder.

  4. Give the service the name ServiceA.

When Visual Studio has finished creating your project, you will find the following source files listed in the Solution Explorer panel:

  • AssemblyInfo.cs: This is a standard file that contains information about the output assembly (DLL). You do not need to be concerned with it here, so you can ignore it.

  • ServiceA.cs: This is the main source file for the new service. Most of the work of creating a new service is done in this file.

  • ServiceA.manifest.xml: This is the manifest that is used by DSS to load the service.

  • ServiceATypes.cs: This contains a set of classes, also called types, that are used by the service and other services that wish to communicate with it.

(There are several more files that Visual Studio uses to manage the solution: ServiceA.csproj, ServiceA.csproj.user, ServiceA.sln and ServiceA.suo. You can ignore them; they don't show up in the Solution Explorer anyway.)

The source files are explained in more detail later in this chapter. In the meantime, it is instructive to look at the Visual Studio Project Properties.

Open the Project Properties and step through each of the tabs to look at the settings. The important points on each of the tabs are highlighted here:

  • Application tab: The application type is Class Library (DLL). All MRDS services are dynamic link libraries (DLLs). Notice that the Assembly name is ServiceA.Yyyyy.Mmm, where yyyy is the current year and mm is the current month. Using the date is a simple way to create different versions of a service with the same name. Recall from earlier in the chapter that contract identifiers also contain a year and a month. The assembly name and the contract identifier should match. (It is possible to have a different assembly name, but that can become a source of confusion!)

    The default namespace is Robotics.ServiceA. There is no need to change this unless you want to create assemblies for a particular organization.

  • Build tab: The output path should be ....in. All MRDS services are placed into the bin folder under the MRDS root directory.

  • Build Events tab: There is a post-build event command that is only executed after successful compilation:

    "C:Microsoft Robotics Studio (1.5)indssproxy.exe" 
    Figure 3-17
    /dll:"$(TargetPath)" /proxyprojectpath:"$(ProjectDir)Proxy "
    Figure 3-17
    /keyfile:"$(AssemblyOriginatorKeyFile)" $(ProxyDelaySign)
    Figure 3-17
    $(CompactFrameworkProxyGen) /binpath:". "
    Figure 3-17
    /referencepath:"C:Microsoft Robotics Studio (1.5)in "
    Figure 3-17
    /referencepath:"C:Microsoft Robotics Studio (1.5)in "

    You do not need to know what this command does for now, and you certainly should not change it. Its purpose is to create a proxy for your service. If you are familiar with web services and SOAP requests, then you know what a proxy is. Otherwise, it is explained later in the section "Proxy Assemblies."

  • Debug tab: When you run the debugger, it automatically starts an external program: C:Microsoft Robotics Studio (1.5)indsshost.exe.

    DssHost.exe runs a DSS node, which is effectively the MRDS runtime environment. Your service(s) run within this node. Although it is possible to start a DSS node from within your own application program, it isn't done that way for the examples in this book.

    Notice that the working directory is the MRDS root directory and some command-line options are passed to DssHost:

    -port:50000 -tcpport:50001 
    Figure 3-17
    -manifest:"C:Microsoft Robotics Studio (1.5)projects
    Figure 3-17
    ServiceAServiceA.manifest.xml"
  • Reference Paths tab: There should be two directories in the list of reference paths. The first directory is where all of the service DLLs reside, as well as the components that make up MRDS. This enables you to easily reference other services. The second directory is the V2.0 .NET Framework, which is required to run MRDS V1.5.

    c:microsoft robotics studio (1.5)in
    c:windowsmicrosoft.netframeworkv2.0.50727

You can ignore the Resources, Settings, and Signing tabs because they are not important at this stage. For now, just leave Visual Studio open while you create another service from the command line as an alternative to using Visual Studio. We want two services so that they can interact with each other.

Using the DssNewService Tool

Open an MRDS Command Prompt window from the MRDS menu by clicking Start All Programs Microsoft Robotics Studio (1.5). Note that you cannot use a normal MS-DOS command prompt because the MRDS Command Prompt window defines a lot of environment variables and paths to help you run MRDS commands. In addition, the MRDS command prompt automatically places you into the root directory of MRDS when it starts up.

To use the DSSNewService tool, follow these steps (what you should type appears in bold text):

  1. You should already have a folder called Projects. Change to this folder:

    C:Microsoft Robotics Studio (1.5)>cd Projects
  2. Execute DssNewService to create a service for you:

    C:Microsoft Robotics Studio (1.5)>DssNewService /service:ServiceB

    The only command-line parameter you need to provide is the service name. Note carefully that it is ServiceB. This is going to be the partner to ServiceA, which you created earlier.

  3. Once the service has been created, which only takes a couple of seconds, you can open it in Visual Studio. Either locate the .sln file in Windows Explorer and double-click it, or at the command prompt enter the following:

    C:Microsoft Robotics Studio (1.5)>cd ServiceB
    C:Microsoft Robotics Studio (1.5)>ServiceB.sln
  4. Look around in ServiceB. It is basically the same as ServiceA. The only slight difference you might find is that it has an additional reference path:

    c:microsoft robotics studio (1.5)incf

This is for the .NET Compact Framework, which is not relevant here, but is covered in Chapter 16.

The DssNewService tool has several command-line qualifiers. You can read about them in the online help or enter the following command:

C:Microsoft Robotics Studio (1.5)>DssNewService /?

We will not cover all of the options here, but a few of them are worth mentioning:

  • You can explicitly set the URI prefix that is used in the contract identifier with /org. The default is schemas.tempuri.org.

  • The /month and /year qualifiers affect the contract identifier and the assembly name. They default to the current month and year.

  • You can specify the service namespace using /namespace. This might be important if you are producing code for a particular organization. You might want to coordinate this with the /org qualifier.

  • The /clone qualifier enables you to copy an existing service. It does this using reflection, not by copying the source files. Therefore, when DssNewService has finished building your solution, all of the methods exist but there is no code. You can, of course, copy and paste the code yourself.

  • As you become more familiar with MRDS, you will want to create new services that implement generic contracts. The /alt qualifier specifies an alternate service, i.e., the generic contract, that you wish to implement in addition to your own set of operations. This is covered in the next chapter. Note that if you specify /alt, you also need to supply an /i qualifier.

Examining the Service Source Files

This section examines the various files that are generated when you create a new service. The source code for a new service is split into two files: the main code and the "types." You are not limited to using only these two files—you can split the code up any way you like. In fact, in the early days of MRDS, it was common to also have a "state" file that contained only the definition of the service state. This is now rolled into the types file, but you can still see many state files in the samples supplied with MRDS.

The Types File

Begin by opening ServiceATypes.cs in Visual Studio. At the very top of the file you can see the contract for ServiceA:

namespace Robotics.ServiceA
{
    /// <summary>
    /// ServiceA Contract class
    /// </summary>
public sealed class Contract
    {
        /// <summary>
        /// The Dss Service contract
        /// </summary>
        public const String Identifier = 
The Types File
"http://schemas.tempuri.org/2008/01/servicea.html"; }

The namespace is based on the service name. The Contract class is a mandatory part of the contract, and it must contain a string called Identifier. (Your contract identifier will be different unless you explicitly specified /year:2008 and /month:01 when you created the service, which is unlikely.)

Next is the service state. All services have a state class, but at this stage it is empty. Adding fields to the service state is one of the standard steps in creating a new service:

/// <summary>
/// The ServiceA State
/// </summary>
[DataContract()]
public class ServiceAState
{
}

Notice that the service state has the [DataContract] attribute applied to it. This is necessary so that the class will be copied across to the Proxy DLL when the code is compiled.

The next piece of code is the main operations port, which is a PortSet:

/// <summary>
    /// ServiceA Main Operations Port
    /// </summary>
    [ServicePort()]
    public class ServiceAOperations: PortSet<DsspDefaultLookup, 
The Types File
DsspDefaultDrop, Get> { }

A service's main operations port must have the [ServicePort] attribute. Note that even though it is called a port, it is really a PortSet that lists all of the operations supported by the service.

The default PortSet when you create a new service only contains DsspDefaultLookup, DsspDefaultDrop, and Get operations.

For an orchestration service, such as ServiceA, there is no need to add any operations because no other service will call ServiceA—it is the "master." However, ServiceB needs some additional operations (as explained later in the section "Update") because ServiceA needs to call ServiceB to perform certain functions.

The final section of the types file contains the definitions for the operation request types. In this case, there is only one for Get:

/// <summary>
    /// ServiceA Get Operation
    /// </summary>
    public class Get: Get<GetRequestType, PortSet<ServiceAState, Fault>>
    {
        /// <summary>
        /// ServiceA Get Operation
        /// </summary>
        public Get()
        {
        }

        /// <summary>
        /// ServiceA Get Operation
        /// </summary>
        public Get(Microsoft.Dss.ServiceModel.Dssp.GetRequestType body):
            base(body)
        {
        }

        /// <summary>
        /// ServiceA Get Operation
        /// </summary>
        public Get(Microsoft.Dss.ServiceModel.Dssp.GetRequestType body, 
The Types File
Microsoft.Ccr.Core.PortSet<ServiceAState,W3C.Soap.Fault> responsePort): base(body, responsePort) { } }

There are no definitions for DsspDefaultLookup and DsspDefaultDrop because these operations are implicitly handled by the DsspServiceBase class. You can override these operations if you wish; and you will do this for ServiceB, but in general it is not necessary—you can let the base class handle them for you.

Notice that Get uses two generic types (one for the request message type and the other for the response message or messages): GetRequestType and PortSet<ServiceAState, Fault>.

GetRequestType is known as the body of the message. In this case, it is a class that is already defined in the DSS service model. You can also define your own classes to use in messages, so you can determine exactly what the content of messages will be.

Get operations are service-specific because they return the state for the particular service. Therefore, every service needs to subclass Microsoft.Dss.ServiceModel.Dssp.Get and supply an appropriate PortSet for the response. Notice that in this case the PortSet contains ServiceAState.

The PortSet indicates that there are two possible responses to this message: ServiceAState or Fault. Obviously, the purpose of Get is to return ServiceAState. Because DSSP is based on SOAP, it uses the standard mechanism already defined by SOAP to return errors. If you look at the top of the file, you will see that the SOAP namespace is referenced because this is where Fault comes from:

using W3C.Soap;

Main Service Implementation File

Now open the main code file for ServiceA, ServiceA.cs. It contains the service initialization and the service operations (or behaviors). Scroll down below the using statements:

namespace Robotics.ServiceA
{
     /// <summary>
     /// Implementation class for ServiceA
     /// </summary>
     [DisplayName("ServiceA")]
     [Description("The ServiceA Service")]
     [Contract(Contract.Identifier)]
     public class ServiceAService: DsspServiceBase
     {

The namespace matches ServiceATypes.cs as you would expect. You can change the [DisplayName] and [Description] attributes as you see fit because they are only for documentation. The contract is specified using Contract.Identifier, which is in the ServiceATypes.cs file (as you saw earlier). The new service class is a subclass of DsspServiceBase. This gives you access to a whole host of helper functions.

Every service must have an instance of its service state, even if the state is empty, and a main operations port:

/// <summary>
/// _state
/// </summary>
private ServiceAState _state = new ServiceAState();

/// <summary>
/// _main Port
/// </summary>
[ServicePort("/servicea", AllowMultipleInstances=false)]
private ServiceAOperations _mainPort = new ServiceAOperations();

By convention, the service state is called _state, but you can call it anything you like. In addition, an operations port called _mainPort is created for sending messages to ServiceA. It might seem strange to send messages to yourself, but in larger services this is quite common. Again, you can use any name you like for the main port, but it is best to stick with convention.

Finally, there is the constructor for the class, which is always empty:

/// <summary>
/// Default Service Constructor
/// </summary>
public ServiceAService(DsspServiceCreationPort creationPort):
        base(creationPort)
{
}

Next is the Start method. All services have a Start method. It is called during service creation so that the service can initialize itself:

/// <summary>
/// Service Start
/// </summary>
protected override void Start()
{
    base.Start();
    // Add service specific initialization here.
}

A call to base.Start is inserted here automatically. This is explained further in the section "Service Initialization" later in the chapter. You should not remove it.

Lastly, there is a handler for the Get operation:

/// <summary>
/// Get Handler
/// </summary>
/// <param name="get"></param>
/// <returns></returns>
[ServiceHandler(ServiceHandlerBehavior.Concurrent)]
public virtual IEnumerator<ITask> GetHandler(Get get)
{
    get.ResponsePort.Post(_state);
    yield break;
}

This handler is very simple. All it does is return a copy of the service state. It is annotated with the [ServiceHandler] attribute, which specifies that it belongs to the Concurrent group.

Notice that there are no handlers for DsspDefaultLookup and DsspDefaultDrop because these are defined in the base class and there is no need to override them. In fact, you might even wonder why there is code here for Get. The short answer is that it is one of the handlers that is commonly modified.

The Service Manifest

The last source file to look at (we will ignore AssemblyInfo.cs for now) is the manifest. Open ServiceA.manifest.xml in Visual Studio:

<?xml version="1.0" ?>
<Manifest
    xmlns="http://schemas.microsoft.com/xw/2004/10/manifest.html"
    xmlns:dssp="http://schemas.microsoft.com/xw/2004/10/dssp.html"
    >
    <CreateServiceList>
        <ServiceRecordType> <dssp:Contract>http://schemas.tempuri.org/2008/01/servicea.html</dssp:Contract>
        </ServiceRecordType>
    </CreateServiceList>
</Manifest>

When you run the debugger, this manifest is passed to DssHost. The manifest describes the services to be started and any partnerships. The preceding one only has a single ServiceRecord with the ServiceA contract identifier. Manifests are discussed in detail later in the chapter in the section "Modifying Manifests."

Compiling and Running Services

In general, before you compile a service, you need to add references for the other services it uses and write some code to implement the service operations. However, in this case, you have a "chicken or the egg" situation because ServiceA relies on ServiceB, so you will start by compiling both services.

Compiling a Service

You don't need to do anything special to compile a service—just select Build Solution from the Build menu. If it is not open already, open ServiceA in Visual Studio. Locate the Start method in ServiceA.cs and add a line of code to announce the startup of the service, as shown here:

protected override void Start()
{
     base.Start();
     // Add service specific initialization here.
     Console.WriteLine("ServiceA starting");

Now compile ServiceA. It should compile without errors. You can run it if you want, but it just displays a message indicating that it is starting, which is not very exciting.

Open ServiceB in Visual Studio and add a Console.WriteLine command in ServiceB.cs, except obviously the message should say "ServiceB starting."

When you compile, you might notice in the Output panel that some other DLLs are created as well. These are the Proxy DLLs discussed later in the section "Proxy Assemblies."

Setting Up References

In Visual Studio for ServiceA, expand the References in the Solution Explorer panel. References are inserted automatically for Ccr.Core, DssBase, and DssRuntime when the service is created. In almost all services, you will also require RoboticsCommon.Proxy. You can add this reference to your services if you want, but because they do not involve a real robot, it is not necessary.

You also need to add references to other services that you intend to use. In the example in this chapter, ServiceA partners with ServiceB, so ServiceA must have a reference to the ServiceB proxy. (See "Proxy Assemblies," later in the chapter). This is why you compiled both of the services in the previous section.

When you add a reference, it might take a little while before the dialog is displayed. Eventually you should see a list of DLLs. Scroll down to ServiceB, as shown in Figure 3-18. Make sure you select the Proxy assembly. (The year and month in the assembly name will be different for you.)

Figure 3-18

Figure 3.18. Figure 3-18

Note that ServiceB does not need a reference to ServiceA because it never calls ServiceA—it only responds to requests from ServiceA.

At the top of ServiceA.cs underneath the existing using statements, add another one:

using serviceb = Robotics.ServiceB.Proxy;

You can now recompile ServiceA with the new reference. Clearly, you have to compile ServiceB before ServiceA so that the correct reference is used.

Proxy Assemblies

When you compile a service, a post-build event is triggered after a successful compilation. This runs DssProxy, which generates a Proxy DLL. You can see the command in the Project Properties on the Build Events tab, as mentioned earlier.

Service operations are executed "over the wire" by sending messages. These messages must first be serialized, i.e., converted to XML, so that they can be sent. This is necessary because the service that you are communicating with might not be on the same computer. It is not simply a matter of passing across a pointer reference to a location in memory—this will not work across the network!

This is an important point: You can't allocate a chunk of memory, e.g., create a new class instance, and send a pointer to it to another service. The contents of the area of memory have to be converted to XML, and then sent.

The Proxy DLL is responsible for performing the serialization and deserialization of messages. When you post a message, you pass a handle to the request message body (a pointer reference) to the Proxy and it creates an XML message from that. When a response message is returned, the Proxy converts the XML back into its binary representation and gives you a handle to a response message.

Therefore, the reference that you add to ServiceA is to the ServiceB proxy. You do not call ServiceB directly.

Running Services

Now that you have compiled the services, you can try running each of them using whatever method you normally use: press F5; click the Start Debugging icon in the toolbar; or click Debug Start Debugging.

This starts DssHost and runs the manifest that was generated for you when you created the service. The command that is executed can be found in the Project Properties on the Debug tab in the Start External Program textbox, and the command-line options are in the Command Line Arguments textbox.

The first time you run ServiceA, you should see something like what is shown in Figure 3-19. Notice that the message "ServiceA starting" is displayed but then nothing else happens. You must stop debugging or close the MRDS Command Prompt window to terminate the service.

Figure 3-19

Figure 3.19. Figure 3-19

Because this is a new service that has not been run before, DSS rebuilds the contract directory cache, as you can see from the message:

Rebuilding contract directory cache. This will take a few moments .
Contract directory cache refresh complete

The contract directory cache is populated using reflection on all of the DLLs in the MRDS bin folder. It consists of two files in the store folder:

  • contractDirectory.state.xml

  • contractDirectoryCache.xml

You can open these files and have a look at them if you like, but you will never need to edit them. If you want to force the cache to be refreshed, you can delete these two files. The next time DssHost starts, it will recreate them.

Also in Figure 3-19, you can see that the Manifest Loader starts up and then loads the ServiceA manifest. When you are creating your own manifest (explained below), you might make mistakes. If you do, this is where the errors will appear.

Now add some behavior to ServiceB. Open it in Visual Studio and then open ServiceB.cs. Scroll down in ServiceB until you find the Start method. Add the following code (shown highlighted):

/// <summary>
/// Service Start
/// </summary>
protected override void Start()
{
    base.Start();
    // Add service specific initialization here.

    SpawnIterator(MainLoop);
}
private IEnumerator<ITask> MainLoop()
{
    Random r = new Random();
    while (true)
    {
        // Generate a random number from 0-100 and write it out
        Console.WriteLine("B: " + r.Next(100));

        // Wait a while
        yield return Arbiter.Receive(
            false,
            TimeoutPort(1000),
            delegate(DateTime time)
            { }
        );
    }
}

The SpawnIterator in the Start method kicks off an infinite loop called MainLoop. ServiceB then displays a random number from 0-100 on the console, waits for a second, and then repeats forever (or until you shut it down). The point of this code is that it continually generates new random numbers that are used to simulate sensor readings.

Compile ServiceB and run it. You should see output like that in Figure 3-20.

Figure 3-20

Figure 3.20. Figure 3-20

Using the Debugger

Debugging services is just like debugging normal code—assuming that you normally debug multi-threaded code! You can set breakpoints and step through code just as usual. Try it out by setting a breakpoint inside MainLoop and running ServiceB again.

Figure 3-21 shows the debugger stopped at a breakpoint. Notice in the bottom-left corner that the Threads window is visible. Select it from the menu using Debug Windows Threads. This menu option is only available while the debugger is running. Two threads are assigned to the User Services Common Dispatcher. This dispatcher is set up for you by DssHost.

Figure 3-21

Figure 3.21. Figure 3-21

Be careful about where you set breakpoints. You can set breakpoints inside a delegate if you want to. If you try to single-step through the code, you might encounter strange behavior when you hit a yield return. Instead of stepping through a yield return, set a breakpoint on the statement after it and tell the debugger to continue execution.

It is important to understand that by setting up an infinite loop like this, one of the two threads created for the default dispatcher is unavailable to the CCR for scheduling service operations. In this case it is not a problem, but in general you should not tie up a thread like this, or you should specify more threads as explained in the previous chapter.

If you are calling other services, you can debug them too. Simply open the relevant source file from another project in the current Visual Studio window. Set breakpoints in this "external" source file as required, and then run the debugger.

For example, later in the chapter you will be running ServiceA with ServiceB as a partner. You can open ServiceA in Visual Studio and set breakpoints in ServiceA.cs. Then, from the menu, click ? File Open ? File and browse to ServiceB.cs and open it. You can set breakpoints inside ServiceB as well. That way, as you send messages backward and forward, you stop in the code on both sides.

Defining Service State

The concept of service state implies that it should contain all of the necessary information to enable you to save the state, restart the service some time later, and, by reloading the saved state, continue running from where the service left off. For this reason, any information that you retrieve from a service must be part of the state. Service state is exposed via the Get and HttpGet operations. The Get operation is intended to be used programmatically, whereas HttpGet is for human consumption, which enables you to observe the state using a web browser.

In the simplest form, the state is displayed as XML. Therefore, you can start working with a new service without having to write a UI. If you want to present the information in an easy-to-read form, you can supply an XSLT file and even some JavaScript code for the web page.

To define a service state, follow these steps:

  1. Open ServiceB in Visual Studio. (Note that this is "B," not "A.") Then open the source file called ServiceBTypes.cs.

  2. Scroll down through the source code to the service state, which is initially empty. Add two properties to the state, as shown here:

    /// <summary>
    /// The ServiceB State
    /// </summary>
    [DataContract()]
    public class ServiceBState
    {
        [DataMember]
        public int Interval;
        [DataMember]
        public int SensorValue;
    }

    Each of these fields, Interval and SensorValue, is declared as public and is decorated with the [DataMember] attribute. (The purpose of these two fields will become apparent shortly.) These fields are part of the service contract and will be serialized (to XML) when the state is saved or sent in a message. You can, of course, have private members in the state if you want, but they are ignored during serialization.

    Purists might prefer to use a property declaration like the following, rather than a public field, although this is not strictly necessary. There is, however, a subtle difference if you want to fully document your code—the [DisplayName] attribute can only be applied to a property, not a public field.

    private int _interval;
    [DataMember]
    public int Interval
    {
        get { return _interval; }
        set { _interval = value; }
    }
  3. Now that the state for ServiceB contains some fields, go back to ServiceB.cs and revise the MainLoop function as shown:

    // Global flag to allow terminating the Main Loop
    private bool _shutdown = false;
    
    private IEnumerator<ITask> MainLoop()
    {
        Random r = new Random();
    
        while (!_shutdown)
       {
            // Generate a random number from 0-100 and save it in the state
    
            _state.SensorValue = r.Next(100);
            Console.WriteLine("B: " + _state.SensorValue);
    
            // Wait a while
            yield return Arbiter.Receive(
                false,
                TimeoutPort(_state.Interval),
                delegate(DateTime time)
                { }
            );
       }
    }

The changes are as follows:

  • A global flag called _shutdown has been added.

  • The while loop now uses _shutdown so that it can be terminated (see below).

  • New random values are stored in the state SensorValue property.

  • The timer interval is read from the state Interval property.

Later in the chapter, in the section "Dropping Services," you will add a Drop handler, which can use _shutdown to terminate the MainLoop. For now, this variable has no effect on the operation of the service.

Storing the simulated sensor value into the state means that it can be accessed from other services using a Get operation.

Exposing the Interval property enables it to be changed without having to recompile the code. It is really a configuration parameter. There are two ways in which the interval can be changed: through the initial state (as explained in the next section) or via an update operation (covered later in the chapter).

Persisting Service State

Saving state is useful for remembering the configuration of a service. MRDS provides a service called the Initial State Partner service that can be used to read a configuration file and populate your service state when the service starts up.

In the case of the Simulator, you can save the entire state of the simulation. Using this saved state, you can restart a simulation later without having to construct the environment again programmatically. You can see an example of this in the MRDS Simulation Tutorials.

Loading State from a Configuration File

To load an initial state from a config file, you need to specify where it should come from. DSS can obtain a name for a config file in two ways:

  • You can specify it in your code in an [InitialStatePartner] declaration.

  • The filename can be supplied in the manifest.

Using Initial State Partner to Load a Configuration File

To load state using a service state partner, go to the top of ServiceB.cs and locate the declaration of the state instance. Add an [InitialStatePartner] attribute as shown here:

/// <summary>
/// _state
/// </summary>
// Set an OPTIONAL initial state partner
// NOTE: If the config file is NOT specified in the manifest, then the
// file will be created in the MRDS root directory. If it is listed in the
// manifest, then it will be created in the same directory as the manifest.
[InitialStatePartner(Optional = true, ServiceUri = "ServiceB.Config.xml")]
private ServiceBState _state = new ServiceBState();

The initial state partner declaration says that the config file is optional. This is a good idea because the service won't start if the file is required and it doesn't exist, such as the first time you run the service.

Notice the ServiceUri parameter, which specifies the filename. This is not required, but if you omit it, you must specify the filename in the manifest. You should get into the habit of naming your manifests and config files in a consistent fashion. Then you can easily pick up all of the files associated with a particular service. This is why we have called the config file ServiceB.Config.xml. However, MRDS does not care what the filename is.

If a config file is specified in a manifest without a path, then it is assumed to be in the same directory as the manifest. This is an easy way to keep the files together. However, if the config file is specified in the service code without a path (and not listed in the manifest), then it defaults to the MRDS root directory.

The ServiceUri is a URI, not a directory path on your hard drive. You can, however, specify the location of a config file using the ServicePaths class, as shown by the following example from the Dashboard in Chapter 4:

private const string InitialStateUri = ServicePaths.MountPoint + @"/ProMRDS/Config/Dashboard.Config.xml";

     // shared access to state is protected by the interleave pattern
     // when we activate the handlers
     [InitialStatePartner(Optional = true, ServiceUri = InitialStateUri)]
     StateType _state = null;

ServicePaths.MountPoint maps to "/mountpoint," but you should not make this assumption. The mountpoint is the root directory of the MRDS installation on the machine where the DSS node is running. Directory paths are specified relative to this. The ServicePaths class contains a number of other URI prefixes that you can use. You can investigate it using the Object Browser or reflection (by typing ServicePaths in the Visual Studio editor and then right-clicking it and selecting Go To Definition).

Specifying a Configuration File in a Manifest

If you want to specify the config file in the manifest, you must add a service state partner to ServiceB. Open ServiceB.manifest.xml in Visual Studio and update it by adding the following highlighted code:

<?xml version="1.0" ?>
<Manifest
    xmlns="http://schemas.microsoft.com/xw/2004/10/manifest.html"
    xmlns:dssp="http://schemas.microsoft.com/xw/2004/10/dssp.html"
    >
    <CreateServiceList>
        <ServiceRecordType>
    <dssp:Contract>http://schemas.tempuri.org/2008/01/serviceb.html</dssp:Contract>
            <dssp:PartnerList>
              <dssp:Partner>
                <dssp:Service>ServiceB.Config.xml</dssp:Service>
                <dssp:Name>dssp:StateService</dssp:Name>
              </dssp:Partner>
            </dssp:PartnerList>
        </ServiceRecordType>
    </CreateServiceList>
</Manifest>

The first time that you run ServiceB after completing the rest of the changes in this section, it will create a config file in the ProjectsServiceB directory because this is where the manifest is located and no path is specified in the manifest.

Your code, usually in the Start method, must be prepared to handle the situation where no state is defined. This might happen, for example, the first time you run a service. In this case, the global variable (usually called _state by convention) will be null because no config file was found.

Go to the Start method in ServiceB.cs and add the following code in the middle of the function to check the state:

protected override void Start()
{
     base.Start();
     // Add service specific initialization here.
     Console.WriteLine("ServiceB starting");

     // Make sure that we have an initial state!
     if (_state == null)
     {
         _state = new ServiceBState();
     }
     // Sanity check the values (or initialize them if empty)
     if (_state.Interval <= 0)
         _state.Interval = 1000;

     // Save the state now
     SaveState(_state);

     SpawnIterator(MainLoop);
}

As well as creating a new state if there isn't one, the code confirms that the information in the state is sensible. The value of Interval will be zero if the state has just been created. It also saves the state (config file), as explained in the next section.

Saving the State to a Config File

Saving the current state of a service is actually quite trivial—just call the SaveState helper function in DSS. However, you need to set up the name of the config file first, as explained in the previous section.

The following code from the previous code snippet saves the state each time the service runs:

// Save the state now
SaveState(_state);

SaveState is just a helper function that posts a Replace message to the Mount service. Because this happens asynchronously, you cannot assume when it returns that the state has been saved, or even that it was successful! However, SaveState returns a PortSet, so you can use it in a Choice to determine whether it returns a Fault. More important, if you are saving the state in your Drop handler, then you can wait until the save has completed before finally shutting down the service. The simplest way to wait is as follows:

yield return (Choice)SaveState(_state);

This assumes that you are inside an iterator.

In V1.5, the default output directory for saved config files was changed to the MRDS root directory (if no path is specified in the Partner attribute). For this reason, it is preferable to supply the filename in the manifest file. Then the file will be saved to the same directory as the manifest. Alternatively, as explained in the last section, you can specify an explicit path.

Saved state files are in XML format. In the case of ServiceB, the first time the state is saved it should look like the following:

<?xml version="1.0" encoding="utf-8"?>
<ServiceBState 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://schemas.tempuri.org/2008/01/serviceb.html">
  <Interval>1000</Interval>
  <SensorValue>0</SensorValue>
</ServiceBState>

You can edit the saved state and change the Interval. The file is in the MRDS root directory and is called ServiceB.Config.xml. Open this file in Notepad and change Interval to 200. Save the file.

Rerun ServiceB. The output should appear a lot faster. What you have done is to change the configuration of ServiceB without modifying the code and recompiling. Even though ServiceB rewrites the config file every time it runs, it simply propagates what is already there.

Modifying Service State

The DSS Protocol defines several types of operations that you can use to modify the state of a service, but only two of them are commonly used in MRDS: Replace and Update.

Replace messages are not used very often, so some services do not implement this operation. It is easy to understand why if you consider that a sophisticated service has a lot of properties in the state and these properties might not be directly related to one another. For example, you would rarely want to set the sensor polling interval, the serial COM port for communications, and the power to the motors at the same time.

This is where Update comes in. The Generic Brick contract in Chapter 17 defines separate Update messages for several tasks, including setting the drive power, changing the brick configuration parameters, and turning on LEDs.

Replace

A Replace message supplies a completely new copy of the state. Because the Body of the message contains an instance of the state that is specific to this service, you must define a Replace class in your service types file (where xxx is the service name):

public class Replace: Replace<xxxState, 
Replace
PortSet<DefaultReplaceResponseType, Fault>> { public Replace() { } public Replace(xxxState body)
: base(body)
        {
        }
    }

A basic Replace handler might look like the following:

[ServiceHandler(ServiceHandlerBehavior.Exclusive)]
public IEnumerator<ITask> ReplaceHandler(Replace replace)
{
    _state = replace.Body;
    replace.ResponsePort.Post(DefaultReplaceResponseType.Instance);
    yield break;
}

Notice that the handler is declared in the Exclusive group. This guarantees that the state is not updated by two different handlers at the same time, which could lead to an inconsistent state.

However, note also that the code does no checking of the incoming state. Most of the samples in the MRDS distribution are like this (if they have a Replace operation). It would be better to check crucial values in the state before making the replacement.

The handler sends an acknowledgment message to the ResponsePort when it has finished. It uses a pre-defined message called DefaultReplaceResponseType.Instance that makes your life easier because you don't need to create a response type. (Similar instances are available for other types of operations; use IntelliSense to look for them.) If the partner service cannot continue until the replacement is complete, then it can wait for this response message to arrive.

Update

You use the Update messages when only a portion of the state needs to change. You need to define an appropriate class to hold all the information you want to update, and then write a handler to perform the update. This must be an Exclusive handler to avoid potential conflicts.

Defining a Class for an Update Request

Go back to ServiceBTypes.cs in Visual Studio. At the bottom of the file below the Get class, add the following code:

/// <summary>
/// ServiceB Set Interval Operation
/// </summary>
public class SetInterval: Update<SetIntervalRequest,
            PortSet<DefaultUpdateResponseType,Fault>>
{
    public SetInterval()
    {
    }
}

/// <summary>
/// Set Interval Request
/// </summary>
[DataContract]
[DataMemberConstructor]
public class SetIntervalRequest
{
    [DataMember, DataMemberConstructor]
    public int Interval;
}

The new class, SetInterval, is based on Update and uses a request message type of SetIntervalRequest and returns a DefaultUpdateResponseType if it is successful.

The SetIntervalRequest class has only one member, which is the Interval. Notice that there is a data contract on SetIntervalRequest and it requests that DssProxy generate a helper to construct a new instance using the [DataMemberConstructor] attribute. Because of the constructor, you can execute this operation from another service using the following shorthand:

_servicebPort.SetInterval(1500);

This assumes, of course, that _servicebPort is a service forwarder port that points to ServiceB.

Adding a Handler to Process Update Requests

Now you need to go to ServiceB.cs to add the handler for the SetInterval operation. At the bottom of the file, add the following handler just before the end of the service class:

/// <summary>
/// Set Interval Handler
/// </summary>
/// <param name="request">SetIntervalRequest</param>
/// <returns></returns>
[ServiceHandler(ServiceHandlerBehavior.Exclusive)]
public virtual IEnumerator<ITask> SetIntervalHandler(SetInterval request)
{
    if (_state == null)
        // Oops! Return a failure
        request.ResponsePort.Post(new Fault());
    else
    {
        // Set the interval
        _state.Interval = request.Body.Interval;
        // Return a success response
        request.ResponsePort.Post(DefaultUpdateResponseType.Instance);
    }
    yield break;
}

This handler simply updates the _state.Interval from the request body and then sends back an acknowledgment using one of the pre-defined default response types.

It is worth mentioning at this point that this example demonstrates defensive programming: The code checks for a null state before using it. When services start, there is no guarantee in what order they will complete their initialization. A race condition is possible if another service sends a request before the initial state of ServiceB has been loaded. ServiceB must protect itself from an access violation by confirming that the state actually exists before updating it.

Note that the ServiceB GetHandler should also be modified to determine whether the state is null:

/// <summary>
/// Get Handler
/// </summary>
/// <param name="get"></param>
/// <returns></returns>
[ServiceHandler(ServiceHandlerBehavior.Concurrent)]
public virtual IEnumerator<ITask> GetHandler(Get get)
{
    if (_state == null)
        // Oops! Return a failure
        // This can happen due to race conditions if another service
        // issues a Get before the Initial State has been defined
        get.ResponsePort.Post(new Fault());
    else
        // Return the state
        get.ResponsePort.Post(_state);
    yield break;
}

Returning Errors

Notice in the preceding code that if there is no state, a Fault is returned. Errors are always returned this way via the response port. However, the code just creates an empty Fault object. This is not good practice, and the programmer should be reprimanded. A Fault should always include information about what caused the error.

There are several ways to create a Fault that contains useful information. Inside the catch block of a try/catch, you can use the Fault.FromException helper method to convert the Exception into a Fault.

If you want to use a string as the error message (called the reason), you can use the following:

// Create a new Fault based on the error message
Fault fault = Fault.FromCodeSubcodeReason(FaultCodes.Receiver,
        DsspFaultCodes.OperationFailed, "Some error message");

There are many different fault codes and DSSP fault codes to choose from. You can list them in the editor by using IntelliSense and find the ones that are most appropriate for your situation.

Service Initialization

You have already seen the Start method that must be present in every service. This is where you place your initialization code. When a new service is created, it includes a call to base.Start, which does the following:

  • Calls ActivateDsspOperationHandlers on all your main and alternate ports, trying to hook up handlers with the [ServiceHandler] attribute to ports in the operation PortSets

  • Sends a DirectoryInsert to add your service to the Service Directory

  • Does a LogInfo with the URI of the service

If you have a lot of initialization to do, don't call base.Start until after you have completed the initialization. This appears to be counter to the comment that is automatically inserted when you create a service, but it does not cause any problems:

protected override void Start()
{
     base.Start();
     // Add service specific initialization here.
}

In older code (from V1.0) you might see the following pattern instead of base.Start:

// Listen for each operation type and call its Service Handler
ActivateDsspOperationHandlers();
// Publish the service to the local Node Directory
DirectoryInsert();
// Display HTTP service Uri
LogInfo(LogGroups.Console, "Service uri: ");

Going back even further to older code, ActivateDsspOperationHandlers was not used; there was an explicit declaration of the main interleave—for example, if you look in ArcosCore.cs for the Pioneer 3DX robot:

Activate(Arbiter.Interleave(
            new TeardownReceiverGroup
            (
                Arbiter.Receive<DsspDefaultDrop>(false,_mainPort,DropHandler)
            ),
            new ExclusiveReceiverGroup
            (
                Arbiter.Receive<Replace>(true, _mainPort, ReplaceHandler),
Arbiter.ReceiveWithIterator<Subscribe>(true, _mainPort, SubscribeHandler),
            ),
            new ConcurrentReceiverGroup
            (
Arbiter.Receive<DsspDefaultLookup>(true,_mainPort,DefaultLookupHandler),
Arbiter.Receive<Get>(true, _mainPort, GetHandler),
       Arbiter.Receive<HttpGet>(true,_mainPort, HttpGetHandler),
       Arbiter.Receive<HttpQuery>(true,_mainPort, HttpQueryHandler),
       Arbiter.Receive<Query>(true, _mainPort, QueryHandler),
       Arbiter.Receive<Update>(true, _mainPort, UpdateHandler),
    )
));

You no longer have to do all of this because DSS uses reflection to find your handlers based on the [ServiceHandler] attribute and the method signature. However, it is helpful to understand what is actually happening when your service starts.

Note that if you want to add handlers to the main interleave after the service has started, you should use the Arbiter.CombineWith method. If you create a new interleave, it runs independently of the main interleave and therefore does not guarantee exclusivity. Likewise, if you use SpawnIterator or Activate to start a task, then the task runs outside of the main interleave.

A common thread in the discussion forums is about how to introduce delays during initialization to ensure that other services have started up properly. Using Thread.Sleep is not recommended because it blocks a thread. If possible, check the status of other services by sending a Get request to them (and waiting for the response) or looking in the directory to see whether they are visible.

Another alternative is to use SpawnIterator to start a new thread running an iterator. Then you can use the TimeoutPort to insert delays (if you really feel that you need to), which will release the thread until the timeout occurs. With Arbiter.ExecuteToCompletion, you can even call a series of other iterators, one after another.

In summary, best practice for initialization is to use iterators, and avoid blocking threads.

Composing and Coordinating Services

One of the benefits of writing everything as services is that you can combine, or orchestrate, services to create more complex applications. This section discusses how to start and stop services and establish partnerships.

Starting and Stopping Services Programmatically

In most cases, you do not need to explicitly start or stop a service because all the services you require will be started when the manifest is loaded. However, this section explains the procedure. The next section ("Using the Partner Attribute") shows you an easier approach.

Remember that ServiceA is the one that is in charge. It partners with ServiceB and makes requests to ServiceB. Therefore, open ServiceA.cs in Visual Studio.

Adding a Service Forwarder Port

You need a service forwarder port so that you can send requests to ServiceB. However, because the code will create a new instance of ServiceB, the service port must be null initially. Add the following code somewhere near the top of the service class:

// Create a port to access Service B,
// but we don't know where to send messages yet
serviceb.ServiceBOperations _servicebPort = null;

For the moment, ignore the question of how to establish a partnership between ServiceA and ServiceB. In the Start method of ServiceA, add a line of code to spawn a new task:

/// </summary>
protected override void Start()
{
    base.Start();
    // Add service specific initialization here.
    Console.WriteLine("ServiceA starting");

    // Start the main task on a separate thread and return
    SpawnIterator(MainTask);
}

This achieves the purpose of leaving a thread running but finishing the service initialization as far as DSS is concerned.

Main ServiceA Behavior

Underneath the Start method, add the MainTask. This routine creates a new ServiceB; calls MainLoop, which runs until it is satisfied; and then closes down the services and the DSS node:

private IEnumerator<ITask> MainTask()
{
      Port<EmptyValue> done = new Port<EmptyValue>();

      SpawnIterator<Port<EmptyValue>>(done, CreatePartner);

      // Wait for a message to say that ServiceB is up and running
      yield return Arbiter.Receive(
          false,
          done,
          EmptyHandler
      );

      // Check that we have a forwarder
      if (_servicebPort == null)
      {
          LogError("There is no ServiceB");
          yield break;
      }
      else
{
          SpawnIterator<Port<EmptyValue>>(done, MainLoop);
          yield return Arbiter.Receive(
              false,
              done,
              EmptyHandler
          );
      }

      // We no longer require ServiceB so shut it down
      // NOTE: This is not necessary—it is just here to illustrate that
      // other services can be shut down
      LogInfo(LogGroups.Console, "Dropping ServiceB .");
      _servicebPort.DsspDefaultDrop();

      Console.WriteLine("ServiceA finished");
      // Wait a while for ServiceB to exit
      yield return Arbiter.Receive(
          false,
          TimeoutPort(500),
          delegate(DateTime time)
          { }
      );

      // Pause for user input—
      // This is just so that the DSS node stays up for the time being
      Console.WriteLine("Press Enter to exit:");
      Console.ReadLine();
      // Shut down the DSS node
      ControlPanelPort.Post( 
Main ServiceA Behavior
new Microsoft.Dss.Services.ControlPanel.DropProcess()); yield break; }

Using Completion Ports for Synchronization

As an example, MainTask creates a port called done for signaling purposes. Because no information needs to be transferred, it uses the EmptyValue class. Then it spawns the CreatePartner iterator and waits on the done port. An alternative, and much neater way to do it without signaling on a port, is to use Arbiter.ExecuteToCompletion.

The first step in CreatePartner is to call CreateService using the ServiceB contract identifier. If this is successful, then a new URI is created based on the service information. Then a ServiceForwarder is created. This enables messages to be sent to the operations port of the newly created service. (The _servicebPort was declared at the beginning of this section as a global).

private IEnumerator<ITask> CreatePartner(Port<EmptyValue> p)
{
     // Create ServiceB instance
     Console.WriteLine("Creating new ServiceB");
     yield return Arbiter.Choice(CreateService(serviceb.Contract.Identifier),
         delegate(CreateResponse s)
{
                     // Create Request succeeded.
                     LogInfo(LogGroups.Console, "ServiceB created: " + s.Service);

                     Uri addr;
                     try
                     {
                          // Create URI from service instance string
                          addr = new Uri(s.Service);

                          // Create forwarder to ServiceB
                          _servicebPort = 
Using Completion Ports for Synchronization
ServiceForwarder<serviceb.ServiceBOperations>(addr); } catch (Exception ex) { LogError(LogGroups.Console,
Using Completion Ports for Synchronization
"Could not create forwarder: " + ex.Message); } }, delegate(W3C.Soap.Fault failure) { // Request failed LogError(LogGroups.Console, "Could not start ServiceB"); } ); // Signal that the create is finished p.Post(EmptyValue.SharedInstance); yield break; }

Assuming that a service forwarder port is created successfully, the MainLoop iterator is called (also using the done port to signify completion). MainLoop looks like this:

private IEnumerator<ITask> MainLoop(Port<EmptyValue> p)
{
         // Issue several Gets to ServiceB
         yield return Arbiter.ExecuteToCompletion(Environment.TaskQueue,
             Arbiter.FromIteratorHandler(GetData));

         // Change the update interval on ServiceB
         _servicebPort.SetInterval(1500);

         // Do some more Gets to see the effect of the changed interval
         yield return Arbiter.ExecuteToCompletion(Environment.TaskQueue,
             Arbiter.FromIteratorHandler(GetData));

         // Finally, post a message to say that we are finished
         p.Post(EmptyValue.SharedInstance);

         yield break;
}

MainLoop in turn calls another iterator to request the sensor data from ServiceB. Then it changes the update interval in ServiceB and again reads the sensor data several times using GetData:

// Request "sensor" data from ServiceB
        private IEnumerator<ITask> GetData()
        {
            for (int i = 0; i < 10; i++)
            {
                // Send a Get request to ServiceB
                yield return Arbiter.Choice(_servicebPort.Get(),
                    delegate(serviceb.ServiceBState s)
                    {
                        // Get request succeeded
                        LogInfo(LogGroups.Console, 
Using Completion Ports for Synchronization
"ServiceB Sensor: " + s.SensorValue); }, delegate(W3C.Soap.Fault failure) { // Get request failed LogError(LogGroups.Console,
Using Completion Ports for Synchronization
"Get to ServiceB failed" + failure.Reason); } ); // Wait for 1 second yield return Arbiter.Receive( false, TimeoutPort(1000), delegate(DateTime time) { } ); } yield break; }

This code uses the LogInfo method rather than Console.WriteLine. The output is therefore clearly differentiated from the Console.WriteLine output, as you can see in Figure 3-22.

Notice that GetData uses Get operations on ServiceB, repeated at one-second intervals. This timer interval does not change, but partway through, ServiceA changes the interval in ServiceB. You should be able to see the effects of these mismatched time intervals quite clearly in the output. This merely illustrates that sometimes the data you get might be a little stale.

Dropping Services

When the MainLoop is finished, a Drop message is sent to ServiceB (from MainTask). Then the code waits for the user to press Enter. This is not something that you would normally do. In particular, Console.ReadLine is not CCR-friendly and should not be used. However, it enables you to poke around inside the DSS node using a web browser before the whole thing is closed down.

To stop a service, you send it a Drop message. By default, the DSS runtime handles Drop requests for you, so you don't have to write any code. However, ServiceB does something a little nasty—it runs an infinite loop on a dedicated thread. All sorts of "nasties" can arise when you are programming MRDS services, so it is worthwhile to review Drop handlers here.

Go to the bottom of the ServiceB.cs file in Visual Studio. (If you have been following the instructions, there should be a SetIntervalHandler there). Insert the following code:

/// <summary>
/// Drop Handler
/// </summary>
/// <param name="drop"></param>
/// <returns></returns>
[ServiceHandler(ServiceHandlerBehavior.Teardown)]
public virtual IEnumerator<ITask> DropHandler(DsspDefaultDrop drop)
{
    // Tell the main loop to stop
    _shutdown = true;

    Console.WriteLine("ServiceB shutting down");

    // Make sure you do this or the sender might be blocked
    // waiting for a response. The base handler will send
    // a response for us.
    base.DefaultDropHandler(drop);

    // That's all folks!
    yield break;
}

Make sure that the Drop handler is in the TearDown group. If you inadvertently place it into the Concurrent group, very strange things happen. The service will disappear from the service directory, but it continues executing!

The only function that the DropHandler needs to perform is to set the _shutdown flag so that the main loop terminates on the next iteration. Then it calls base.DefaultDropHandler to finish the job.

Just declaring this handler is sufficient to override the default handler defined in DSS—there is nothing else you need to do to make it work.

Now ServiceA can send a message to ServiceB to tell it to terminate itself:

_servicebPort.DsspDefaultDrop();

The last step is for ServiceA to send a DropProcess message to the Control Panel, which shuts down the DSS node.

Running the Services Together

Recompile both ServiceA and ServiceB. Then run ServiceA in the debugger. If you have done everything properly, then Service B should also start up, as shown in Figure 3-22.

Figure 3-22

Figure 3.22. Figure 3-22

If you read through the output in Figure 3-22, you will see that ServiceB is created by ServiceA, but the first Get response from ServiceB is before it has output any "sensor" readings, and consequently the value is zero. (The first "reading" is when ServiceB displays B: 64).

After 10 iterations, the timer interval for ServiceB is changed by ServiceA. Then you see sensor readings doubling up because ServiceA continues to poll ServiceB at the same rate as before. This is now too fast for the incoming "sensor data" at the ServiceB end.

Lastly, ServiceA tells ServiceB to shut down by sending a Drop message.

Using the Partner Attribute to Start Services

Rather than create services explicitly, you can create a service partner declaratively using the [Partner] attribute. This is the usual approach. However, sometimes you do not know ahead of time what services you will have to partner with—for example, the Dashboard dynamically partners with Differential Drive services, Laser Range Finder services, and Webcam services. When it starts, it doesn't know which of these services might be available. More to the point, it doesn't know which DSS node you are going to point it at.

To specify a partner at compile time, go back to the top of ServiceA.cs and change the declaration of _servicebPort as follows:

// Partner with ServiceB
[Partner("ServiceB", Contract = serviceb.Contract.Identifier,
    CreationPolicy = PartnerCreationPolicy.CreateAlways, Optional = false)]
private serviceb.ServiceBOperations _servicebPort =
    new serviceb.ServiceBOperations();

Notice that the contract identifier for ServiceB is specified as a parameter to the [Partner] attribute. In addition, ServiceB is not optional. The CreationPolicy is set to CreateAlways. This causes the constructor service to create a new instance of ServiceB for you. It doesn't matter whether you create a new instance of the service operations port or leave it null if the config file exists because the State service will take care of it.

Four service creation policies are available:

  • CreateAlways

  • UseExistingOrCreate

  • UseExisting

  • UsePartnerListEntry

The simplest approach is CreateAlways. This is equivalent to creating the service explicitly but requires only a couple of lines of code. In some cases this makes sense, such as when a service uses another service that is never called directly by anyone else, i.e., there is a one-to-one partnership.

Now that you have declared the partnership with ServiceB, there is no longer any need to call the CreatePartner routine. However, for the purposes of illustrating a point, you will insert some different code.

The FindPartner routine replaces CreatePartner:

private IEnumerator<ITask> MainTask()
{
     Port<EmptyValue> done = new Port<EmptyValue>();

     SpawnIterator<Port<EmptyValue>>(done, FindPartner);

FindPartner issues a DirectoryQuery to try to find ServiceB, but with the default long timeout:

private IEnumerator<ITask> FindPartner(Port<EmptyValue> p)
        {
            // Find ServiceB in the local directory
            Console.WriteLine("Finding ServiceB in the Directory");
            yield return Arbiter.Choice(
                DirectoryQuery(serviceb.Contract.Identifier, 
Using the Partner Attribute to Start Services
DsspOperation.DefaultLongTimeSpan), delegate(ServiceInfoType s) { // Request succeeded LogInfo(LogGroups.Console, "Found ServiceB: " + s.Service); Uri addr; try { // Create URI from service instance string addr = new Uri(s.Service); // Now create service forwarder to ServiceB _servicebPort =
Using the Partner Attribute to Start Services
ServiceForwarder<serviceb.ServiceBOperations>(addr); } catch (Exception ex) { LogError(LogGroups.Console, "Could not create forwarder: " + ex.Message); _servicebPort = null; } }, delegate(W3C.Soap.Fault failure) { // Request failed LogError(LogGroups.Console, "Could not find ServiceB"); _servicebPort = null; } ); // Signal that the find is finished p.Post(EmptyValue.SharedInstance); yield break; }

Basically, FindPartner waits until ServiceB does a DirectoryInsert and becomes visible in the service directory. The rest of the code is the same as CreatePartner. This is one way to synchronize service startup. For it to work successfully, services must not call base.Start until they have completed all of their initialization. Otherwise, they appear in the directory prematurely. If you run this revised version of the code, there is not very much difference, so no screenshot is provided.

You can change the partner creation policy to UseExistingOrCreate, but you won't see any difference in this scenario because the "Or Create" option causes ServiceB to be created. In other words, ServiceA uses an existing instance of ServiceB if one exists; if not, it simply goes ahead and creates one anyway.

Modifying Manifests Manually

When you create a new service, a manifest is automatically created for you. This section steps you through the process of modifying a manifest using a text editor. However, as you will see later in "Modifying Manifests Using the DSS Manifest Editor," it is preferable to use the appropriate tool for this task. The purpose of this section is to give you some understanding of what manifests look like and how they work. It is not suggested that you always edit manifests by hand.

Use Existing Partner

In Visual Studio for ServiceA, open ServiceA.manifest.xml. It should have a single service record for ServiceA. Leave it alone for the moment.

Change the partner creation policy in ServiceA.cs to UseExisting:

// Partner with ServiceB
[Partner("ServiceB", Contract = serviceb.Contract.Identifier,
    CreationPolicy = PartnerCreationPolicy.UseExisting, Optional = false)]
private serviceb.ServiceBOperations _servicebPort = 
Use Existing Partner
new serviceb.ServiceBOperations();

This specifies that there must be a ServiceB running on the DSS node before ServiceA can start up properly. You can see from the ServiceA manifest that there is no mention of ServiceB.

Recompile the code and run ServiceA. Be patient—very patient! In fact, be patient for up to two minutes. Eventually, an error message will appear (displayed in red), as shown in Figure 3-23.

Figure 3-23

Figure 3.23. Figure 3-23

This error says, "Partner enumeration during service startup failed." Quite clearly, the error occurred because you asked for ServiceB but did not create a new instance of it. This is easy to fix.

Open the ServiceB.manifest.xml in Visual Studio and copy the Service Record. Then go to ServiceA.manifest.xml and paste it in there. The resulting ServiceA manifest should look like the following:

<?xml version="1.0" ?>
<Manifest
    xmlns="http://schemas.microsoft.com/xw/2004/10/manifest.html"
    xmlns:dssp="http://schemas.microsoft.com/xw/2004/10/dssp.html"
    >
  <CreateServiceList>
    <ServiceRecordType>
    <dssp:Contract>http://schemas.tempuri.org/2008/01/servicea.html</dssp:Contract>
    </ServiceRecordType>
    <ServiceRecordType>
    <dssp:Contract>http://schemas.tempuri.org/2008/01/serviceb.html</dssp:Contract>
      <dssp:PartnerList>
        <dssp:Partner>
          <dssp:Service>ServiceB.Config.xml</dssp:Service>
          <dssp:Name>dssp:StateService</dssp:Name>
        </dssp:Partner>
      </dssp:PartnerList>
    </ServiceRecordType>
  </CreateServiceList>
</Manifest>

Be very careful when you copy and paste pieces of code between XML files. If you end up with mismatched tags, the manifest won't work at all and you will just get a syntax error. This is one reason why it is better to use the Manifest Editor (discussed in "Modifying Manifests Using the DSS Manifest Editor"), rather than edit manifests manually.

Remember that in the section "Specifying a Configuration File in a Manifest" you added a state service partner to ServiceB. This is part of the Service Record as well. However, when you run ServiceA now, a new config file is created in the ProjectsServiceA folder because that is where the manifest is. It all gets a little confusing after a while.

This is the brute-force approach to partnering—it simply specifies two services in the manifest and they are both created. Save the manifest and run ServiceA again. The services should now run successfully.

Contract Identifiers and Case Sensitivity

If you have plenty of time and nothing better to do, edit the manifest again by changing the names of the services in the contract identifiers to ServiceA and ServiceB, i.e., capitalize them so that they look pretty. Run ServiceA. What happens? Look at Figure 3-24.

Figure 3-24

Figure 3.24. Figure 3-24

First, the contract directory cache is rebuilt because you requested two services that DSS has not seen before. However, this doesn't help and you get two error messages saying that there was an error creating the service, with a Fault.Subcode of UnknownEntry.

This is a small trap for beginners—contract identifiers are case-sensitive.

Use Partner List Entry

The last step is to change the partner creation policy to UsePartnerListEntry. Recompile and run ServiceA.

This time you get an error that partner enumeration failed, but ServiceB starts up and begins pumping out "sensor" data. ServiceA, however, does not start.

To fix this problem, you need to edit the manifest and explicitly make ServiceB a partner of ServiceA. The resulting manifest looks like the following, with the changes highlighted:

<?xml version="1.0" ?>
<Manifest
    xmlns="http://schemas.microsoft.com/xw/2004/10/manifest.html"
    xmlns:dssp="http://schemas.microsoft.com/xw/2004/10/dssp.html"
    xmlns:this="http://schemas.tempuri.org/2008/01/servicea.html"
    >
  <CreateServiceList>
    <ServiceRecordType>
    <dssp:Contract>http://schemas.tempuri.org/2008/01/servicea.html</dssp:Contract>
    <dssp:PartnerList>
      <dssp:Partner>
        <dssp:Name>this:ServiceB</dssp:Name>
      </dssp:Partner>
    </dssp:PartnerList>
    <Name>this:ServiceA</Name>
</ServiceRecordType>
  <ServiceRecordType>
    <dssp:Contract>http://schemas.tempuri.org/2008/01/serviceb.html</dssp:Contract>
      <dssp:PartnerList>
        <dssp:Partner>
          <dssp:Service>ServiceB.Config.xml</dssp:Service>
          <dssp:Name>dssp:StateService</dssp:Name>
        </dssp:Partner>
      </dssp:PartnerList>
      <Name>this:ServiceB</Name>
    </ServiceRecordType>
  </CreateServiceList>
</Manifest>

Both services have explicit names in the manifest, and ServiceB is in the PartnerList for ServiceA. ServiceB has a config file specified using the StateService as a partner. Note that there is no path, just a filename, so the config file is assumed to be in the same directory as the manifest.

When you run ServiceA with this revised manifest, both services start up and everybody is happy.

DSS Tools

Several tools are supplied with MRDS to assist you with various tasks. You have already seen DssNewService, DssHost, and DssProxy. This section briefly outlines some other tools that you should become familiar with, including the Manifest Editor, DssInfo, and DssProjectMigration. Because it is a significant tool in its own right, DssDeploy is covered in the following section.

Modifying Manifests Using the DSS Manifest Editor

The DSS Manifest Editor was introduced in MRDS V1.5 as a tool to help make the creation and editing of manifests easier. We do not intend to provide a tutorial on the Manifest Editor here, but you should be aware that it exists and read up on it in the online documentation. (Actually, the Manifest Editor is not a separate program but part of VPL. A batch file called dssme.cmd in the MRDS bin directory executes VPL in Manifest mode.)

To try out the Manifest Editor, use the very last manifest from the previous section. You can start the Manifest Editor by clicking Start ? Microsoft Robotics Studio (1.5) ? Microsoft DSS Manifest Editor. It might take a little while to start, because it is listing all of the available services.

When the Manifest Editor main screen appears, click File ? Open and browse to the ServiceA manifest to open it. Once the manifest has been loaded, it should look like what is shown in Figure 3-25.

Figure 3-25

Figure 3.25. Figure 3-25

You can select ServiceB and add an initial configuration if you want. This will add a state partner with a nominated config file.

Apart from that, just play around in the Manifest Editor until you are comfortable with it. You will find it is much easier to use than editing XML files. It is the recommended method for editing manifests, rather than opening them in a text editor.

DssInfo: Examining Contract Information

You can get information about contracts and the operations that a service supports using the DssInfo tool. For example, the following command displays information about the Dashboard service that is included with the code for Chapter 4:

C:Microsoft Robotics Studio (1.5)>dssinfo bindashboard.y2007.m10.dll

You can control the amount of information displayed using command-line qualifiers. The default information level shows the contract, partners, the operations that are supported, and so on, as shown here:

Reflecting:                         Dashboard.Y2007.M10.dll
DSS CONTRACT
Verbosity                           ShowWarnings
Assembly:                           c:microsoft robotics studio (1.5)indashboar
                                    d.y2007.m10.dll
Service:                            DashboardService
   DssContract:                     http://www.promrds.com/2007/10/dashboard.html
   Namespace:                       ProMRDS.Robotics.Services.Dashboard
   ServicePrefix:                   /dashboard
   Singleton:                       False
   Partner(s):
     [InitialStatePartner]          /mountpoint/ProMRDS/Config/Dashboard.Config.xml
     UseExistingOrCreate            http://schemas.microsoft.com/robotics/2006/09/g
                                    amecontroller.html
   ServicePort                      DashboardOperations
     Lookup:                        DsspDefaultLookup
     Drop:                          DsspDefaultDrop
     Get:                           Get
     Replace:                       Replace

If you run DssInfo and specify RoboticsCommon.dll, you will see several generic contracts and some actual service implementations.

DssProjectMigration: Migrating Services

At this stage, you do not need to migrate any services because all your code is on one computer and you have not upgraded MRDS. However, it is worth mentioning the DssProjectMigration tool so that you can file away it in the back of your mind for later use. This tool can be very useful in a couple of circumstances:

  • When V2.0 of MRDS arrives, possibly around the time this book is released, you might want to take some of your existing services over to the new version. This is the primary purpose of DssProjectMigration, and it can migrate entire hierarchies of projects. In this case, the new (V2.0) tool will look for known changes to the code and try to fix anything that might break from V1.5.

  • This tool ensures that a project is set up properly for its current location. If you move a project to a different folder, or if you transfer it to another computer where MRDS is installed on a different disk drive, then you need to update some of the project details. Rather than edit the .csproj and .sln files yourself, you can let DssProjectMigration take care of it for you.

For more information, you can refer to the online documentation when you need to use DssProjectMigration.

Deploying Services

Sooner or later, you will want to distribute your services to friends, colleagues, or customers. MRDS includes a tool specifically designed for this purpose: DssDeploy.

There are fundamentally two different ways to deploy a service:

  • Distribute only the service itself to a computer that already has MRDS installed, i.e., distribute the source code so other people can see it and modify it.

  • Distribute the service plus the core components of MRDS so that the packaged service can be run on any computer (assuming the computer is running Windows XP, Vista, Mobile, or CE), i.e., distribute executables only.

For full documentation on DssDeploy, see the online help. This section simply provides two examples illustrating how to use it, but they are two commonly used examples.

Sharing Source Code

If you are interested in sharing your code, you can package it so that other people can install it into their MRDS environment. This is what the authors have done with the ProMRDS code. In this case, the DssDeploy package contains only the source code for the relevant services and the compiled DLLs. All of the object files, debug symbol tables, and so on, are left behind because they can be recreated by compiling the source code.

To build a package for one of the chapters in this book, following these steps:

  1. Make a listing of the files from a command prompt using the following:

    Dir /b/o:n/s >dir.txt
  2. You can then edit the output file (dir.txt) in Notepad to remove all of the object files and anything else that should not be included. (Strange files sometimes sneak into source directories when you are not looking). The list of files is turned into a DssDeploy options file, which is just a sequence of DssDeploy command-line options. You can use a hash sign (#) to insert comments.

  3. Along with the source files, some other files should be included, such as the DLLs from the bin folder, the ReadMe file, etc. A standard template for an options file helps to ensure that you don't forget anything. The following example shows the options file for Chapter 2, which is relatively short. Called DeployChapter2Options.txt, it resides in the ProMRDSPackage folder:

    # Chapter 2 Deploy Options File
    # All Chapter files should follow the same basic format
    
    # Include the deploy options file for later repackaging
    /d:"..PackageDeployChapter2Options.txt"
    
    # Chapter Readme.htm and associated files
    /d:"..Chapter2Readme.htm"
    /d:"..Chapter2images*"
    /d:"..Chapter2styles*.css"
    
    # Add the Batch files to run the applications in this chapter
    #/d:"..BatchFilesxxxx.cmd"
    
    # Add the Config files for the applications (if any)
    # and the Manifests
    #/d:"..Configxxxx*"
    
    # Include any models/meshes
    
    # And the media files
    
    # Now start with the source files
    
    #/d:"..Chapter2Chapter2.sln"
    
    #----------CCR Examples----------
    
    # Add the Readme files for this application (if any)
    # Include the binaries
    /d:"....inCCRExamples.Y2007.M12.dll"
    /d:"....inCCRExamples.Y2007.M12.proxy.dll"
    /d:"....inCCRExamples.Y2007.M12.transform.dll"
    
    # Now the source code, but none of the compiled stuff
    /d:"..Chapter2CCRExamplesAssemblyInfo.cs"
    /d:"..Chapter2CCRExamplesCcrexamples.cs"
    /d:"..Chapter2CCRExamplesCCRExamples.csproj"
    /d:"..Chapter2CCRExamplesCCRExamples.csproj.user"
    /d:"..Chapter2CCRExamplesCCRExamples.manifest.xml"
    /d:"..Chapter2CCRExamplesCCRExamples.sln"
    /d:"..Chapter2CCRExamplesCcrexamplesTypes.cs"
  4. To build chapter packages, you use a batch script to avoid typing long command lines. This script, called BuildChapterPackage.cmd, contains the following commands:

    @echo off
    echo Build a single chapter for ProMRDS
    if "%1" == "" goto usage
    if not exist DeployChapter%1Options.txt goto noopts
    ....indssdeploy /p /e+ /cv+ /s- /n:"ProMRDS Chapter %1 Package"
    /r:"..Chapter%1Readme.htm" @DeployChapter%1Options.txt ProMRDSChapter%1.exe
    copy ProMRDSChapter%1.exe c:	emp
    copy /Y ProMRDSChapter%1.exe ProMRDSChapter%1.exe.safe
    goto end
    
    :usage
    echo Usage: BuildChapterPackage num
    echo where num is the Chapter number
    :noopts
    echo DeployChapternumOptions.txt file must already exist
    
    :end

    The /p qualifier asks DssDeploy to create a packed self-expanding executable. Deployment is restricted to only the same version of MRDS using the /cv+ qualifier, and security ACLs are turned off with /s-.

    The ReadMe file for the package (/r qualifier) should be a HTML file. It is displayed automatically when the package finishes installing on the target computer. Other options, such as for including a license, signing the file, and so on, might be relevant to you if you are creating a commercial package.

    The script automatically adds a name to the package, specifies the deploy options file, and creates a ".safe" version for sending via e-mail. It is not rocket science, and you might be able to come up with a more elegant solution. However, it works—as you have no doubt already seen!

  5. To deploy the package, copy it to the target PC and run it. It verifies that MRDS is installed and then creates the necessary folders and copies the files into them. Then it runs DssProjectMigration to ensure that the projects are correctly configured. Finally, it displays the ReadMe file in a web browser.

Distributing Executables

When you get to Chapters 16 and 17, you will be deploying services to a PDA running Windows Mobile or an eBox embedded PC running Windows CE. These environments run the .NET Compact Framework, so they are referred to as CF services for short.

In this situation you do not want to install the full MRDS environment. (In fact, you can't do this anyway.) What you want is a runtime-only environment.

The batch script to create the package, called BuildStingerCFPackage.cmd, contains the following commands:

REM Create a DssDeploy package
setlocal
call "......sdkenv.cmd"
cd "%~dp0"
DssDeploy /p /cf /n:"ProMRDS Stinger CF Example" /d:"../../BatchFiles/CF/*"
/m:../../Config/StingerCFDriveByWire.manifest.xml StingerCF.exe
pause

This DssDeploy command creates a package for the Compact Framework due to the /cf qualifier. It explicitly adds all the files from the BatchFilesCF folder to the package, and then uses a manifest to collect all of the necessary services. DssDeploy analyzes the manifest and walks down the dependency tree looking for service DLLs. The MRDS runtime DLLs are automatically included.

Deploying a runtime version of MRDS in this fashion is not restricted to just CF platforms. You can also create a package without the /cf qualifier and deploy it to Windows XP or Vista.

There is one restriction on the runtime-only version, however, and it is related to the simulator. In order to run Simulation services, you need to have the AGEIA PhysX engine installed, as well as the latest versions of Microsoft DirectX and XNA. In short, you must have MRDS installed on the target machine if you want to deploy a simulation.

Viewing the Contents of a Package

A tool called ViewDssDeployContents, written by Paul Roberts at Microsoft, is available from the Channel 9 website (http://channel9.msdn.com/ShowPost.aspx?PostID=368096), and you should definitely download a copy of it. Perhaps this tool will be included in MRDS V2.0.

This tool shows, in an Explorer-like tree, what files are included in the package. This is particularly handy if you want to check out a package before installing it. Otherwise, you might have no idea what files are going to be replaced. DssDeploy only lists the files that will be replaced if there are less than about 20 of them, but beyond that, it simply asks you something like "72 files will be replaced, OK?".

Summary

This chapter addresses the basics of using DSS: state, operations, and partners. You have seen the various components of a DSS service and how services interact. There is a lot to learn and you probably recognized some overlap with the previous chapter. If you have not done so already, you should at least browse through the MRDS online documentation. In addition, go through the tutorials.

As noted at the beginning of the chapter, it takes some time to get used to the MRDS terminology and way of doing things. The best way to learn is to get your hands dirty and start hacking services.

The next chapter elaborates on writing DSS services to enable you to perform more advanced functions, including using Windows Forms and a web camera.

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

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