DPWS stands for
Devices Profile for Web Services. When I read up on it, it got me really excited with possibilities of network Plug-n-Play type devices. So, when I decided to test it out, I was really surprised to find very little help with the whole idea. My biggest hurdle was my lack of familiarity with Web Services in general.
I set out to write a simple service host running on the device, exporting a status of a button. The client would be a windows application running on a computer my FEZ Spider is hooked into through an Ethernet cable. In order to help me out, I decided to look into .NETMF samples: SimpleService and SimpleServiceClient. This gave me a starting point to learn about WebServices on the device. Unfortunately, it was quite some time until I learned that a better example of what I was trying to achieve can be found in samples: HelloWorldServer_MF and HelloWorldClient_WCF. So for anybody following my footsteps, those are the samples you want to disect, along with their complimentary host-on-WCF and client-on-MF samples.
To ease creation of the Web Services, we can write a service definition file, or a WSDL file. This file is then fed into existing utilities that auto-generate code necessary for the service operation. Unfortunately, a different tool needs to be used for different .NET Frameworks. A tool used by .NETMF is called MFSvcUtil.exe and is found in the Tools folder of your current .NETMF installation. At the time of this writing, mine was installed in: C:\Program Files (x86)\Microsoft .NET Micro Framework\v4.2\Tools. Regular .NET framework tool is called SvcUtil.exe and can be found in the Windows SDK bin folder. At the time of this writing, mine was installed in: C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1\Bin. Be careful with the latter one. If you have multiple SDKs and/or development tools installed, your path variable may be pointing to a different SDK. In that case, the auto-generated code will potentially point to a wrong version of the framework.
Another tool that may eventually become useful is called SvcConfigEditor.exe. There is one in the same Bin directory as SvcUtil.exe, but I would suggest not using that one. In that Bin directory, there is a subdirectory called "NETFX 4.0 Tools" and within it is a newer, better version of the said tool. I'll get back to that tool when I talk about configuring the client application.
Ok, onto the business at hand. The first thing we need to do is define a WSDL file. I can't get into the specifics of it--it would take too long--but if you search for "WSDL tutorial" or some-such you'll be up and running in no time. One thing to keep in mind is: there are two versions of WSDL definitions. For what we are doing we need to use the older version. The one with <types/>, <message/>, <portType/>, and <binding/> required sections. Another note I picked up somewhere is that you should always define types for your operations. Even if you don't want to pass any data. This, the argument goes, serves as both the place holder if in the future you do wish to pass some data, and as a safety to make sure everything is standardized. Personally I subscribed to the second of these two sentiments. There is so much that can (and did) go wrong with this, that I don't need to wrestle such trivial matters of how do I define a parameterless message and why it doesn't work.
Alright, onto the actual code. To start my example WSDL file, I define a bunch of namespaces in the top level definitions element.
<definitions name= "gadgeteerStatuses"
targetNamespace= "GadgeteerReportingService/gadgeteerStatuses"
xmlns:tns= "GadgeteerReportingService/gadgeteerStatuses"
xmlns= "http://schemas.xmlsoap.org/wsdl/"
xmlns:soap= "http://schemas.xmlsoap.org/wsdl/soap12/"
xmlns:xs= "http://www.w3.org/2001/XMLSchema"
xmlns:wsa= "http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:wsp= "http://schemas.xmlsoap.org/ws/2004/09/policy">
Next come the type definitions. This is a normal XML based data schema for each of the data types you wish to communicate. Based upon some of the suggestions I encountered while researching that, it was suggested that all atomic data types like
ints and
strings be wrapped into complex types. I went with that suggestion, but cannot vouch for its merits. Since I only wish to return a status of a button, I just need a simple
int return value. But based on all of the suggestions I covered so far, I have a place-holder type serving as a dummy value for my "parameterless" query. In addition, my button value integer is wrapped into a button value type.
<types>
<xs:schema targetNamespace= "GadgeteerReportingService/gadgeteerStatuses">
<xs:element name="EmptyValue" type="tns:EmptyValueType"/>
<xs:complexType name="EmptyValueType"/>
<xs:element name="ButtonValue" type="tns:ButtonValueType"/>
<xs:complexType name="ButtonValueType">
<xs:sequence>
<xs:element minOccurs="1" maxOccurs="1" name="Value"
nillable="false" type="xs:int"/>
</xs:sequence>
</xs:complexType>
</xs:schema>
</types>
Once the data types are defined, we can define messages that use them.
<message name="getButtonValueRequest">
<part name="empty" element="tns:EmptyValue"/>
</message>
<message name="getButtonValueResponse">
<part name="value" element="tns:ButtonValue"/>
</message>
With the messages, we can use messages to define operations. In our case we have a two way operation, so we need an input and an output message. Operations are a function of a port. Therefore we define a portType, and an associated operation, with the required input and output messages. In addition, we assigned soap actions. To be quite honest, I'm not certain if those actions are necessary, or even used. In all my digging through this material, I haven't been able to figure out where exactly this definition comes into play. If you are reading this, and happen to know the answer, please share it in the comments.
<portType name="GadgeteerStatuses">
<operation name="getButtonValue">
<input wsa:Action="GadgeteerReportingService/gadgeteerStatuses/getButtonValue"
message="tns:getButtonValueRequest" />
<output wsa:Action="GadgeteerReportingService/gadgeteerStatuses/getButtonValueResponse"
message="tns:getButtonValueResponse" />
</operation>
</portType>
Once the portType is defined, we need to generate a binding. It's
type is the name of the portType you defined above. You define the soap transport here as well as additional details of how messages are going to be passed through this port. In our case, we are trying to pass data as
literal, rather than
encoded, and as
documents rather than
RPCs. To be honest with you, I don't think this detail of information is used by the aforementioned tools, but it works, so I'll leave it in as is.
<binding type="tns:GadgeteerStatuses" name="gadgeteerStatusesBinding">
<soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http" />
<operation name ="getButtonValue">
<soap:operation soapAction="GadgeteerReportingService/gadgeteerStatuses/getButtonValue" style="document"/>
<input>
<soap:body use="literal"/>
</input>
<output>
<soap:body use="literal"/>
</output>
</operation>
</binding>
Finally, we can define our service information. I am not certain if this section is necessary. This sort of data will be defined in code, once we are setting up our service. I do know it doesn't hurt, so I'll put it here for completeness sake.
<service name="gadgeteerStatusesService">
<documentation>Service for querying gadgeteer hardware statuses.</documentation>
<port name="gadgeteerStatusesPort" binding="tns:gadgeteerStatusesBinding">
<soap:address location="GadgeteerReportingService/gadgeteerStatuses/"/>
</port>
</service>
Don't forget to close off definitions element if you were copy/pasting the above code.
Once the WSDL file is written, you generate the micro framework code by using the MFSvcUtil.exe utility. I opened Windows 7 SDK Command Prompt and executed the utility with only the GadgeteerReportingService.wsdl file as an argument. It auto-generated for me three additional files: GadgeteerReportingService.cs, GadgeteerReportingServiceClientProxy.cs, and GadgeteerReportingServiceHostedService.cs. ClientProxy file is not of interest for this example, so I added the remaining two to the VS project.
GadgeteerReportingService.cs file contains all of our data definitions, as well as the serializers necessary to serialize the complex data types we defined. It also contains all of the data and service contracts necessary for the hosted Web Service (WS). At this point, we hit our first snag. We are lacking some assembly references in our project. In order to be a DPWS host, we need to add references to:
MFWsStack,
MFDpwsDevice, and
MFDpwsExtensions assemblies. In order to deal with XML serialization we need to add:
System.XML assembly. If we were to implement client behaviour as well, we would need MFDpwsClient assembly as well.
We are almost done for the host set-up. We have to implement the actual code that will generate our response. We do this by implementing a new class that derives from auto-generated service contract interface
IGadgeteerStatuses. It must implement one method as defined in the interface. The method
getButtonValue takes a dummy argument and returns the status of a button (0 or 1) wrapped in
ButtonValue object. In this particular example, I added a reference to the button in question to this class. This was purely for brevity of this example--in an actual system this would have to be handled much more elegantly.
namespace GadgeteerDpwsHostExample
{
/// <summary>
/// Implements Gadgeteer Reporting Service contract interface.
/// </summary>
class GadgeteerReportingServiceImplementation : IGadgeteerStatuses
{
Button externalButton;
public Button ExternalButton
{
get { return externalButton; }
set { externalButton = value; }
}
/// <summary>
/// Fetch button value.
/// </summary>
/// <param name="req">dummy</param>
/// <returns>1 if button pressed, 0 otherwise</returns>
public ButtonValue getButtonValue( EmptyValue req )
{
ButtonValue bv = new ButtonValue();
bv.Value = externalButton.IsPressed ? 1 : 0;
return bv;
}
}
}
In essence those are all the necessary elements required to hook up and start a new service. But, as it turns out, a device host uses one service as a face to the world, while additional services then may be added to its repertoire. To keep things clean and add as much functionality to this example, while still keeping things clean, we'll define a new hosted service to do nothing but serve as a public face. So, we create a new class:
namespace GadgeteerDpwsHostExample
{
class GadgeteerReportingServiceDeviceHost : DpwsHostedService
{
public GadgeteerReportingServiceDeviceHost( ProtocolVersion v )
: base( v )
{
// Add ServiceNamespace. Set ServiceID and ServiceTypeName
ServiceNamespace = new WsXmlNamespace( "gad", "GadgeteerReportingService/gadgeteerStatuses" );
ServiceID = "urn:uuid:2CC239A5-78CA-4D5A-A310-FED281330010";
ServiceTypeName = "GadgeteerDeviceService";
}
public GadgeteerReportingServiceDeviceHost()
: this( new ProtocolVersion10() )
{
}
}
}
If you look closely, you'll see that it is a simple version of the
GadgeteerStatuses hosted service auto-generated in the
GadgeteerReportingServiceHostedService.cs file. And this brings us to the final step of hosting our service: actually instantiating and started all of the elements. So, in the main
Program object, I have the following initialization method:
private void InitializeDpwsStack()
{
Debug.Print( "Initializing DPWS stack." );
// Initialize the binding
string urn = "urn:uuid:2CC239A5-78CA-4D5A-A310-FED281330000";
ProtocolVersion version = new ProtocolVersion10();
Device.Initialize( new WS2007HttpBinding( new HttpTransportBindingConfig( urn, 8084 ) ), version );
// Set device information
Device.ThisModel.Manufacturer = "Example Corporation";
Device.ThisModel.ManufacturerUrl = "http://GadgeteerReportingService.org/manufacturer.html";
Device.ThisModel.ModelName = "GadgeteerReportingService Test Device";
Device.ThisModel.ModelNumber = "1.0";
Device.ThisModel.ModelUrl = "http://GadgeteerReportingService.org/model.html";
Device.ThisModel.PresentationUrl = "http://GadgeteerReportingService.org/device.html";
Device.ThisDevice.FriendlyName = "Gadgeteer Device";
Device.ThisDevice.FirmwareVersion = "0.0.0";
Device.ThisDevice.SerialNumber = "12345678";
// Add a Host service type
Device.Host = new GadgeteerReportingServiceDeviceHost();
// Add Dpws hosted service(s) to the device
GadgeteerReportingServiceImplementation implementation = new GadgeteerReportingServiceImplementation();
implementation.ExternalButton = button;
Device.HostedServices.Add( new GadgeteerReportingService.gadgeteerStatuses.GadgeteerStatuses( implementation ) );
// Set this device property if you want to ignore this clients request
Device.IgnoreLocalClientRequest = true;
Debug.Print( "Start DPWS device service with endpoint address: '" + Device.EndpointAddress + "'" );
// Start the device
ServerBindingContext ctx = new ServerBindingContext( version );
Device.Start( ctx );
}
There is much to do about nothing in the above method. First step is to initialize the binding. We are using a WS2007HttpBinding. Don't ask me why or what alternatives there are. I picked this from an example and got it to work with the client. That's as far as I can justify that particular binding type. We are setting it up to use HTTP with the given urn and a port of 8084. Most of this won't matter for the client because it will all be auto-detected. Just make sure you don't conflict with any other ports you may have running on the device (e.g. web-server). Protocol version is also one of the things that I picked up from an example and got it to work. Not really sure what the difference between 10 and 11 really is.
Next block of code just sets up some human readable information for the hosted service. Once your service is up and running, your device will show up in the Networking tab of your Windows Explorer, and the data you enter here will pop up as Properties of that device. This is relevant if you are delivering production devices to customers, but completely moot for our example.
Next, we assign the world facing service host. Since, for this example, we are only running one meaningful service, we could assign *it* as the device host service, but to keep with what I said before, we'll assign the object we made for this purpose.
Then we add the actual useful service to the device. Before we added the service host object, we gave it a reference to the button we want to report on.
Finally, we ignore local requests since we are not intending to talk to ourselves. And then start the service itself. That's it. Compile. Deploy. And if you set up networking between the computer and the device properly, after a few seconds you should see your device pop up under "Other Devices" settings in the Networking pane.
The easy half of the job is done. I will continue the client talk in the next post.