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.
1 2 3 4 5 6 7 8
<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">
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
<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>
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.
1 2 3 4 5 6 7 8 9
<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>
1 2 3 4 5 6 7 8 9 10 11 12 13 14
<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>
1 2 3 4 5 6 7
<service name="gadgeteerStatusesService">
<documentation>Service for querying gadgeteer hardware statuses.</documentation>
<port name="gadgeteerStatusesPort" binding="tns:gadgeteerStatusesBinding">
<soap:address location="GadgeteerReportingService/gadgeteerStatuses/"/>
</port>
</service>
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
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;
}
}
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
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() )
{
}
}
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
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 );
}
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.
This is an excellent tutorial/overview.
ReplyDeleteCan you comment on "SocketException ErrorCode = 10049" ?
Is this error caused by the router's security?
Thank you kindly for your words. I'm glad someone found some use for it.
DeleteUnfortunately, I have never encountered such an error.