Tuesday, March 10, 2009

Using reflection to hack .NET Web Services (Adding backwards compatibility)

ASP.NET Web Services provide a quick and easy way to implement a Web Service but they are not without their shortcomings. The high level nature of ASP.NET Web Services sadly means that some of the low level functionality required to produce a supportable and maintainable Web Service is missing. Because this missing functionality, small changes actually turn out to be breaking changes.

Here a few breaking changes that this post will address:

1. Renaming of the .asmx Web Service - Product names change, one product becomes two and sometimes developers just get bored. Regardless of the reason, changing the .asmx Web Service name/filename will break all existing Web Service consumers.

2. Renaming a specific Web Service method - Typically the easiest solution here is to add a wrapper with the original name that can be phased out after existing Web Service consumers have made the transition. To cover all basis(and because some of you out there might not have the luxury of exposing two methods that have the same functionality) I've included this scenario in this post.

3. Renaming a parameter of a Web Service method - As Web Services as essentially APIs, developers tend to name Web Service method parameters to coincide with their internal parameter names. While this obviously makes the developer's lives a bit easier (consistency is crucial for a developer), consumers of the Web Services may know those parameters by different names (a business web application developer will often use different terminologies for metrics than the business web site developer -- which of course if fine unless the web developer writes a web service intended for the business developer).

4. Adding a new parameter to a Web Service method - .NET Web Services require all parameters to supplied when the method is called. To add a new parameter to a Web Service method without breaking existing deployments requires creating a wrapper with the original to overload the new method with the new parameters. As with renaming a Web Service method, phasing out method wrappers created for compatibility can be a documentation and supportability nightmare. A better alternative (and the #1 feature I wish ASP.NET Web Services had) is to implement default values for Web Service parameters. The overloaded method you create will ultimately pass in a default value anyways, so why not have a formal way of providing that same default to the Web Service method (therefore eliminating the need for the wrapper)?


NOTE: This post is meant purely for educational purposes. Any decisions/code modifications/problems that develop as a result of this post are 100% not my responsibility (it's best to fully understand what the code below is doing before implementing it).


This post will only cover one potential solution to the aforementioned issues. While I won't go into the details of how I determined what fields of the ASP.NET objects needed to be touched, what I will say (as it might provide extremely useful to you) is that if you are using a .NET library (ASP.NET is basically just a large 3rd-party library sitting atop IIS) and are either having issues or simply want to know how something is implemented, give .NET Reflector a try. With .NET Reflector, you can decompile .NET DLLs into a human readable (and compilable) form.



A simple Web Service (a starting point):

The following is a sample HTTP GET request and response.
The placeholders shown need to be replaced with actual values.

GET /SampleWebService/SimpleService.asmx/SimpleMethod?
StrParam=string&IntParam=string&BoolParam=string&EnumParam=string HTTP/1.1

public enum ENUM_ITEMS
{
ITEM1 = 1,
ITEM2 = 2,
ITEM3 = 3
}

[WebMethod]
public string SimpleMethod(String StrParam, int IntParam, bool BoolParam, ENUM_ITEMS EnumParam)
{
return "The Params: "
+ "StrParam=" + StrParam + ", "
+ "IntParam=" + IntParam + ", "
+ "BoolParam=" + BoolParam + ", "
+ "EnumParam=" + EnumParam;
}

A standard request:
http://localhost/SampleWebService/SimpleService.asmx/SimpleMethod?StrParam=TestString&IntParam=9&BoolParam=false&EnumParam=ITEM3
The return value:
    <?xml version="1.0" encoding="utf-8" ?> 
<string xmlns="http://tempuri.org/">The Params: StrParam=TestString, IntParam=9, BoolParam=False, EnumParam=ITEM3</string>


What the follow code will allow:

1. Request to an alternate Web Service (OldWebService.asmx instead of SimpleService.asmx):
http://localhost/SampleWebService/OldWebService.asmx/SimpleMethod?StrParam=TestString&IntParam=9&BoolParam=false&EnumParam=ITEM3

2. Request to an alternate Web Service method (TheOldMethod instead of SimpleMethod):
http://localhost/SampleWebService/SimpleService.asmx/TheOldMethod?StrParam=TestString&IntParam=9&BoolParam=false&EnumParam=ITEM3

3. Request with an alternate parameter name (StringParam instead of StrParam):
http://localhost/SampleWebService/SimpleService.asmx/SimpleMethod?StringParam=TestString&IntParam=9&BoolParam=false&EnumParam=ITEM3

4. Request using the default StrParam (DefaultString) and EnumParam (ITEM2) values:
http://localhost/SampleWebService/SimpleService.asmx/SimpleMethod?IntParam=9&BoolParam=false
The return value:
    <?xml version="1.0" encoding="utf-8" ?> 
<string xmlns="http://tempuri.org/">The Params: StrParam=DefaultString, IntParam=9, BoolParam=False, EnumParam=ITEM2</string>

5. Putting it all together - a request to an alternate Web Service, an alternate Web Service method and with no parameters (using all default parameter values):
http://localhost/SampleWebService/OldWebService.asmx/TheOldMethod
The return value:
    <?xml version="1.0" encoding="utf-8" ?> 
<string xmlns="http://tempuri.org/">The Params: StrParam=DefaultString, IntParam=11, BoolParam=True, EnumParam=ITEM2</string>


The following solution intercepts the ASP.NET request object before ASP.NET reads the request variables to determine what Web Service and Web Service method to call and modifies the request so that ASP.NET will call the Web Service and Web Service method with the parameters of our choosing.


NOTE: Scroll to the bottom of this post to download the entire solution (including the Web Service and preprocessor code).


This snippet is the preprocessor initialization code that will create a Web Service alias, a method alias, parameter aliases and parameter defaults.

private static object m_InitLock = new object();

private static Dictionary<String, String> m_WebServiceMappings = null;
private static Dictionary<String, Dictionary<String, String>> m_MethodMappings = null;
private static Dictionary<String, Dictionary<String, Dictionary<String, String>>> m_ParameterMappings = null;
private static Dictionary<String, Dictionary<String, Dictionary<String, String>>> m_ParameterDefaults = null;

private static void Init()
{
if (m_ParameterMappings != null) return;

lock (m_InitLock)
{
if (m_ParameterMappings != null) return;

m_WebServiceMappings = new Dictionary<String, String>();
m_MethodMappings = new Dictionary<String, Dictionary<String, String>>();
m_ParameterMappings = new Dictionary<String, Dictionary<String, Dictionary<String, String>>>();
m_ParameterDefaults = new Dictionary<String, Dictionary<String, Dictionary<String, String>>>();

String RawWebServiceName, WebServiceName, RawMethodName, MethodName;

// The following will:
// 1. Allow Web Service requests the Web Service 'OldWebService.asmx' to be remapped to 'SimpleService.asmx'
// 2. Allow Web Service requests to the method 'TheOldMetod' to be remapped to method 'SimpleMethod'
// 3. Allow calls to 'SimpleMethod' with the parameter 'StringParameter' to interpret that parameter as 'StrParam'
// 4. Will apply defaults to 'SimpleMethod' parameters not supplied
#region 'SimpleService' mappings/defaults

RawWebServiceName = "SimpleService.asmx";
WebServiceName = RawWebServiceName.ToUpper();

//Allow for 'OldWebService.asmx' calls to be remapped to 'SimpleService.asmx'
m_WebServiceMappings["OldWebService.asmx".ToUpper()] = RawWebServiceName;


RawMethodName = "SimpleMethod";
MethodName = RawMethodName.ToUpper();

//Add alternate method mappings for SimpleMethod
m_MethodMappings[WebServiceName] = new Dictionary<String, String>();
m_MethodMappings[WebServiceName]["TheOldMethod".ToUpper()] = RawMethodName;

//Add alternate parameter mappings for SimpleMethod
m_ParameterMappings[WebServiceName] = new Dictionary<String, Dictionary<String, String>>();
m_ParameterDefaults[WebServiceName] = new Dictionary<String, Dictionary<String, String>>();
m_ParameterMappings[WebServiceName][MethodName] = new Dictionary<String, String>();
m_ParameterDefaults[WebServiceName][MethodName] = new Dictionary<String, String>();

//Allow for an alterate input (StringParameter) for
//the StrParam parameter of SimpleMethod
m_ParameterMappings[WebServiceName][MethodName]["StringParameter"] = "StrParam";

//Init the SimpleMethod defaults
//(cannot override params, only set params that don't already exist)
m_ParameterDefaults[WebServiceName][MethodName]["StrParam"] = "DefaultString";
m_ParameterDefaults[WebServiceName][MethodName]["IntParam"] = "11";
m_ParameterDefaults[WebServiceName][MethodName]["BoolParam"] = "true";
m_ParameterDefaults[WebServiceName][MethodName]["EnumParam"] = "ITEM2";

#endregion
}
}

How the preprocessor is called (via the Global.asax file):

protected void Application_BeginRequest(object sender, EventArgs e)
{
Preprocessor.PreprocessRequest(Server, Request, Response);
}

The preprocessor code (take a deep breath):

public static void PreprocessRequest(HttpServerUtility Server, HttpRequest Request, HttpResponse Response)
{
if (Request == null || Request.Url == null || Request.Url.Segments == null
|| Request.Url.Segments.Length < 3) return;

String RawWebServiceName = Request.Url.Segments[Request.Url.Segments.Length - 2];
if (RawWebServiceName != null && RawWebServiceName.EndsWith("/"))
RawWebServiceName = RawWebServiceName.Substring(0, RawWebServiceName.Length - 1);

if (!RawWebServiceName.EndsWith(".asmx", StringComparison.InvariantCultureIgnoreCase))
return;

String RawMethodName = Request.Url.Segments[Request.Url.Segments.Length - 1];
if (RawMethodName.EndsWith(".asmx", StringComparison.InvariantCultureIgnoreCase))
return;


//Ensure everything has been initialized
Init();

String WebServiceName = RawWebServiceName.ToUpper();
String RawNewWebServiceName = null;
String NewWebServiceName = null;

String MethodName = RawMethodName.ToUpper();
String RawNewMethodName = null;
String NewMethodName = null;

//Determine if the current Web Service (.asmx) call needs to be remapped
if (m_WebServiceMappings.ContainsKey(WebServiceName))
{
RawNewWebServiceName = m_WebServiceMappings[WebServiceName];
NewWebServiceName = RawNewWebServiceName.ToUpper();
}

String WebServiceKey = NewWebServiceName == null ? WebServiceName : NewWebServiceName;

//Determine if the current method call needs to be remapped
if (m_MethodMappings.ContainsKey(WebServiceKey)
&& m_MethodMappings[WebServiceKey].ContainsKey(MethodName))
{
RawNewMethodName = m_MethodMappings[WebServiceKey][MethodName];
NewMethodName = RawNewMethodName.ToUpper();
}

String MethodKey = NewMethodName == null ? MethodName : NewMethodName;

//POST (Form/SOAP) requests need to be handled a bit differently
bool IsPost = Request.RequestType.Trim().ToUpper() == "POST";

//Determine if any new parameter values need to be added
Dictionary<String, String> NewParamValues = GetNewParamValues(Request, WebServiceKey, MethodKey, IsPost);
//if (NewParamValues == null || NewParamValues.Count == 0) return;

//Use reflection to manually insert parameters into the request
RewriteRequest(Request, RawWebServiceName, RawNewWebServiceName,
RawMethodName, RawNewMethodName, NewParamValues, IsPost);
}

private static Dictionary<String, String> GetNewParamValues(HttpRequest Request,
String WebServiceKey, String MethodKey, bool IsPost)
{
Dictionary<String, String> NewParamValues = new Dictionary<String, String>();

//Determine if any parameters need remapping
if (m_ParameterMappings.ContainsKey(WebServiceKey)
&& m_ParameterMappings[WebServiceKey].ContainsKey(MethodKey))
{
String NewParameter;
foreach (String Param in m_ParameterMappings[WebServiceKey][MethodKey].Keys)
{
//Determine if the parameter needs to be remapped (exists)
if (Request.Params[Param] != null)
{
NewParameter = m_ParameterMappings[WebServiceKey][MethodKey][Param];
//Verify that the parameter that it would map to does not already exist
if (NewParameter != null && Request.Params[NewParameter] == null)
NewParamValues[NewParameter] = Request.Params[Param];
}
}
}

//Determine if any parameters need a default value applied
if (m_ParameterDefaults.ContainsKey(WebServiceKey)
& m_ParameterDefaults[WebServiceKey].ContainsKey(MethodKey))
{
foreach (KeyValuePair<String, String> DefaultValue in m_ParameterDefaults[WebServiceKey][MethodKey])
{
if (IsPost)
{
if (Request.Form[DefaultValue.Key] == null)
NewParamValues[DefaultValue.Key] = DefaultValue.Value;
}
else if (Request.Params[DefaultValue.Key] == null)
NewParamValues[DefaultValue.Key] = DefaultValue.Value;
}
}

return NewParamValues;
}

private static void RewriteRequest(HttpRequest Request, String CurrentWebService, String NewWebService,
String CurrentMethod, String NewMethod, Dictionary<String, String> NewParamValues, bool IsPost)
{
try
{
MethodInfo WritableMethod;
MethodInfo ReadOnlyMethod;

BindingFlags Flags = BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;

//If needed, rewrite a few internal values of HttpWorkerRequest and HttpRequest
//that ASP.NET will use when trying to determine the Web Service/method to call
try
{
bool UpdateWebService = NewWebService != null;
bool UpdateMethod = NewMethod != null && Request.PathInfo.EndsWith(CurrentMethod);

if (UpdateWebService || UpdateMethod)
{
FieldInfo WorkerField = Request.GetType().GetField("_wr", Flags);

HttpWorkerRequest WorkerRequest = (HttpWorkerRequest)WorkerField.GetValue(Request);

FieldInfo FilePathField = null;
FieldInfo PathField = null;
FieldInfo PathInfoField = null;

String filePath = null;
String path = null;
String pathInfo = null;

if (UpdateWebService)
{
FilePathField = WorkerRequest.GetType().GetField("_filePath", Flags);
filePath = (String)FilePathField.GetValue(WorkerRequest);
}

PathField = WorkerRequest.GetType().GetField("_path", Flags);
path = (String)PathField.GetValue(WorkerRequest);

if (UpdateMethod)
{
PathInfoField = WorkerRequest.GetType().GetField("_pathInfo", Flags);
pathInfo = (String)PathInfoField.GetValue(WorkerRequest);
}

if (UpdateWebService)
{
filePath = path.Substring(0, filePath.Length - CurrentWebService.Length);
filePath += NewWebService;

path = path.Replace("/" + CurrentWebService + "/",
"/" + NewWebService + "/");
}

if (UpdateMethod)
{
path = path.Substring(0, path.Length - CurrentMethod.Length);
path += NewMethod;

pathInfo = pathInfo.Substring(0, pathInfo.Length - CurrentMethod.Length);
pathInfo += NewMethod;
}

if (UpdateWebService)
{
FilePathField.SetValue(WorkerRequest, filePath);
FilePathField = Request.GetType().GetField("_filePath", Flags);
FilePathField.SetValue(Request, null);
}

PathField.SetValue(WorkerRequest, path);
PathField = Request.GetType().GetField("_path", Flags);
PathField.SetValue(Request, null);

if (UpdateMethod)
{
PathInfoField.SetValue(WorkerRequest, pathInfo);
PathInfoField = Request.GetType().GetField("_pathInfo", Flags);
PathInfoField.SetValue(Request, null);
}
}
}
catch { }

//Remap or set defaults for Web Service parameters
if (NewParamValues.Count > 0)
{
if (IsPost)
{
WritableMethod = Request.Form.GetType().GetMethod("MakeReadWrite", Flags);
ReadOnlyMethod = Request.Form.GetType().GetMethod("MakeReadOnly", Flags);

FieldInfo FormField = Request.GetType().GetField("_form", Flags);

WritableMethod.Invoke(Request.Form, null);

foreach (String NewParam in NewParamValues.Keys)
Request.Form[NewParam] = NewParamValues[NewParam];

FormField.SetValue(Request, Request.Form);
ReadOnlyMethod.Invoke(Request.Form, null);
}
else
{
WritableMethod = Request.QueryString.GetType().GetMethod("MakeReadWrite", Flags);
ReadOnlyMethod = Request.QueryString.GetType().GetMethod("MakeReadOnly", Flags);

FieldInfo QueryStringField = Request.GetType().GetField("_queryStringText", Flags);
FieldInfo QueryStringOverridenField = Request.GetType().GetField("_queryStringOverriden", Flags);

MethodInfo ResetMethod = Request.QueryString.GetType().GetMethod("Reset", Flags);
MethodInfo FillMethod = Request.GetType().GetMethod("FillInQueryStringCollection", Flags);

String QueryString = Request.QueryString.ToString();
WritableMethod.Invoke(Request.QueryString, null);

QueryString = QueryString.Replace(CurrentMethod, NewMethod);

foreach (String NewParam in NewParamValues.Keys)
Request.QueryString[NewParam] = NewParamValues[NewParam];

QueryStringField.SetValue(Request, Request.QueryString.ToString());
QueryStringOverridenField.SetValue(Request, true);

ResetMethod.Invoke(Request.QueryString, null);
FillMethod.Invoke(Request, null);
ReadOnlyMethod.Invoke(Request.QueryString, null);
}
}
}
catch (Exception) { }
}


Download the source code: SampleWebService.zip


Sunday, February 15, 2009

New Domain - nofeature.com

As I've finally found the motivation to start regularly posting on this blog, I thought it was only appropriate that it grow up a bit. As I was a bit late in the domain name registration race, I've been forced to settle for 'no-feature.com' as opposed to the more appropriate (not that it really matters) 'nofeature.com'. As I don't speak German, I suspect it'll be a while (if ever) before I acquire both domains (although in a weeks time, I highly doubt I'll even care).

All that side.. stay tuned.. there's more to come!

Saturday, February 23, 2008

A C# Web Service Wrapper For Your TEMPer 1.0 Device

In my previous post Taking advantage of your TEMPer 1.0 USB device, I provided a C# library for accessing your TEMPer 1.0 device (I say TEMPer 1.0 because my TEMPer 2.0 device has not arrived and I'm not sure if the current library will be compatible) and in this post I will provide a .NET Web Service to simplify accessing your devices.

My reason for writing this web service: COM sucks. I decided to write a Windows Vista Sidebar gadget capable of displaying the temperature from my TEMPer device and found creating an exe for the widget to call and using COM both a royal pain. Creating an exe for the widget to call was of course the easiest and fastest solution, but it essentially meant that when I had the sidebar widget activated, the sidebar widget and only the sidebar widget would only ever be able to use the TEMPer device it was reading from. I then turned to COM and found COM's limitations (COM hates statics and for the life of me, I couldn't get a String[] to return correctly) were enough for me to throw in the towel (COM is sometimes useful, but if there's another path.. I'm taking it). I avoided the dedicated exe because I didn't want to have the TEMPer device restricted to only one application, so I needed a solution that would allow multiple applications to access the data and a solution that was language (and even OS) independent. The obvious solution: a web service. I won't go through the effort of writing a .NET Web Service tutorial because a Google search for 'C# web service' will give you everything you need to get things up and running.

A web service provided two things: 1. a central place for all my applications to grab temperature data 2. a nice clean way for my sidebar widget to get data without the need of executing an application on disk or loading up a custom ActiveX control.

The biggest issue with writing a web service to communicate with a hardware device is that only one connection to the device can be created/used/accessed at any given time. With that in mind, I wrote the TEMerData class that represents that since instance. Each time TEMPerData is called (via GetCurrentTEMPerValue), the curent time is recorded and the TEMPerData instance is locked. If your request has the lock, you'll make the actual ReadTemp on the TEMPerData's TEMPerInterface instance. If your request was blocking on the TEMPerData instance (someone else had already initiated a request for temperature data) but now has the lock all for itself, the current request's start time is used to determine if the temperature data in TEMPerData is more recent than the start of your request (the TEMPerData's time stamp will be updated by the whomever did have the lock). If the TEMPerData instance's time stamp is more recent than the request's start time stamp, the last temperature retrieved is returned, otherwise the new temperature is read and the TEMPerData's time stamp is updated.



using System;
using System.Collections.Generic;
using System.Web;
using System.Web.Services;
using TEMPer.Communication;

namespace TEMPer.WebService
{
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class TEMPerService : System.Web.Services.WebService
{
internal const String Version = "2008.02.24.1";

private const String m_TEMPerContainer_Key = "TEMPerContainer";
private static object m_TEMPerContainer_Lock = new object();

[WebMethod(EnableSession=false, Description="Lists the available TEMPer devices.")]
public String[] FindDevices()
{
List<String> Devices = new List<String>();
Devices.AddRange(TEMPerInterface.FindDevices());

lock (m_TEMPerContainer_Lock)
{
foreach (TEMPerData data in TEMPerContainer.Values)
if (!Devices.Contains(data.COMPort))
Devices.Add(data.COMPort);
}

Devices.Sort();

return Devices.ToArray();
}

[WebMethod(EnableSession = false, Description = "Reads the temperature (in Celsius) from the specified port. Returns double.MinValue (-1.79769e+308 / -1.7976931348623157E+308) on error.")]
public double ReadTemp(String COMPort)
{
return GetCurrentTEMPerValue(COMPort);
}

private static double GetCurrentTEMPerValue(String COMPort)
{
TEMPerData Data = GetTEMPerDataContainer(COMPort);
if (Data == null) return double.MinValue;

lock (Data)
{
double temp = Data.LastUpdate < DateTime.Now ? Data.ReadTemp() : Data.LastValue;
if (temp == double.MinValue) RemoveTEMPerDataContainer(COMPort);
return temp;
}
}

private static TEMPerData GetTEMPerDataContainer(String COMPort)
{
if (COMPort == null) return null;

String Key = COMPort.Trim().ToUpper();
if (Key.Length == 0) return null;

TEMPerData Data = null;

lock (m_TEMPerContainer_Lock)
{
Dictionary<String, TEMPerData> Dict = TEMPerContainer;

if(Dict.ContainsKey(Key))
Data = Dict[Key];
else
{
Data = new TEMPerData(Key);
Dict[Key] = Data;
}
}

return Data;
}

private static void RemoveTEMPerDataContainer(String COMPort)
{
TEMPerContainer.Remove(COMPort);
}

private static Dictionary<String, TEMPerData> TEMPerContainer
{
get
{
HttpApplicationState App = HttpContext.Current.Application;

Dictionary<String, TEMPerData> dict = null;

lock (m_TEMPerContainer_Lock)
{
dict = (Dictionary<String, TEMPerData>)App[m_TEMPerContainer_Key];
if (dict == null)
{
dict = new Dictionary<String, TEMPerData>();
App[m_TEMPerContainer_Key] = dict;
}
}

return dict;
}
set {
HttpApplicationState App = HttpContext.Current.Application;

lock (m_TEMPerContainer_Lock)
{
if (value == null)
App.Remove(m_TEMPerContainer_Key);
else
App[m_TEMPerContainer_Key] = value;
}
}
}
}

internal class TEMPerData
{
private String m_COMPort;
private DateTime m_LastUpdate;
private double m_LastValue;

private TEMPerInterface m_Interface;

public TEMPerData(String COMPort)
{
m_COMPort = COMPort;
m_LastUpdate = DateTime.MinValue;
m_LastValue = double.MinValue;
m_Interface = null;
}

public String COMPort
{
get { return m_COMPort; }
}

public DateTime LastUpdate
{
get { return m_LastUpdate; }
set { m_LastUpdate = value; }
}

public double LastValue
{
get { return m_LastValue; }
set { m_LastValue = value; }
}

public double ReadTemp()
{
try
{
if(m_Interface == null)
m_Interface = new TEMPerInterface(COMPort);

m_LastValue = m_Interface.ReadTEMP();
}
catch (Exception)
{
m_LastValue = double.MinValue;
}

m_LastUpdate = DateTime.Now;

return m_LastValue;
}
}
}


FindDevices output:

<?xml version="1.0" encoding="utf-8" ?>
<ArrayOfString xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://tempuri.org/">
<string>COM19</string>
</ArrayOfString>


Successful ReadTemp output:

<?xml version="1.0" encoding="utf-8" ?>
<double xmlns="http://tempuri.org/">24.125</double>


Failed ReadTemp output:

<?xml version="1.0" encoding="utf-8" ?>
<double xmlns="http://tempuri.org/">-1.7976931348623157E+308</double>


Download the source code: TEMPer.asmx.cs (v. 2008.02.24.1)


Changes
------------
v.2008.02.24.1
- Initial release

Saturday, January 26, 2008

Taking advantage of your TEMPer 1.0 USB device

I recently purchased a TEMPer (http://www.usbfever.com/index_eproduct_view.php?products_id=257) device to assist me in yet another time wasting home improvement project and was shocked to see all the wasted potential in this device. After being blinded by the .NET application that came with the device, I decided to take it upon myself to write something better. I had heard about Reflector (http://www.aisto.com/roeder/dotnet/) but had never actually used it for anything.. man was I impressed.. dropping TEMPer.exe into Reflector gave nicely formatted source code for literally every method required to use the TEMPer device. So here it is folks, a .NET (C#) dell you can use to access your TEMPer device (the download link is at the bottom).



using System;
using TEMPer.Communication;

namespace TEMPerDemo
{
class Program
{
const String m_CallNumFormat = "000000";
const String m_TempFormat = "00.0000";

static void Main(string[] args)
{
double TempC;
double TempF;
int i;

String[] ComPorts = TEMPerInterface.FindDevices();
if (ComPorts.Length == 0) return;

TEMPerInterface[] Devices = new TEMPerInterface[ComPorts.Length];

for (i = 0; i < ComPorts.Length; i++)
Devices[i] = new TEMPerInterface(ComPorts[i]);

for (int CallNum = 1; CallNum <= 1000; CallNum++)
{
for(i = 0; i < Devices.Length; i++)
{
TempC = Devices[i].ReadTEMP();
TempF = TEMPerInterface.CtoF(TempC);

Console.WriteLine(" "
+ CallNum.ToString(m_CallNumFormat)
+ " - "
+ Devices[i].PortName
+ " - "
+ TempC.ToString(m_TempFormat) + " C"
+ " - "
+ TempF.ToString(m_TempFormat) + " F");
}
}
}
}
}


The results:


000001 - COM19 - 22.3750 C - 72.2750 F
000002 - COM19 - 22.3750 C - 72.2750 F
000003 - COM19 - 22.3750 C - 72.2750 F
000004 - COM19 - 22.3750 C - 72.2750 F
....
000049 - COM19 - 22.5000 C - 72.5000 F
000050 - COM19 - 22.5000 C - 72.5000 F

Requirements:
As of version 2008.01.29.1, the DLL no longer requires USBRDXP.DLL (thanks to Bob for the code that removed this dependency).

Notes:
The device speaks Celsius.. and as such only returns degrees in 0.1250 increments.
If you decide to use this library or the above code, please feel free to contact me with questions (not that I completely understand everything their original code is doing.. trust me, the decompiled code lacks a lot of context) or comments.. but please understand that I'm not responsible for any problems/issues that might crop up as a result of using this library.

The library only does a few things:
1. Reads the registry to list the system's com ports
2. Queries each COM port to find those that have a TEMPer device
3. Reads/writes to the COM port to get temperature data

That's it.. there's no other magic or trickery going on inside the library.

Download the DLL: TEMPer.Communication.dll (v. 2008.02.23.1)
Download the source code: TEMPer.cs (v. 2008.02.23.1)

Changes
------------
v.1
- Initial release

v.2008.01.29.1
- Removed static reference to SerialPort (I suspect this is why reading from multiple devices would fail when multiple instances were created. I'll have to wait for my new devices to arrive before I can verify this).
- Removed the dependency on the TEMPer driver/dll. Thanks to Bob (see comments below) for the C++ code to implement the TEMPer device verification manually. The code below was ported to C# to make things a bit easier on my end.
- Added a version to the DLL

v.2008.02.23.1
- GetPortNames() now uses the managed SerialPort.GetPortNames() instead of querying the windows registry.
- I experimented with the idea of making ReadTemp a static call (removing the need to create an instance of the TEMPerInterface class) but the overhead for initializing the device added more than 1 second to each read call (something I was not willing to live with). Instead, I cleaned up a bit of the code I borrowed form the original TEMPer exe which shaved a few hundred milliseconds off of each call.
- The SerialPort is only kept open for the initialization of the device and for each ReadTemp (previously, the SerialPort was not being closed at the end of every ReadTemp call). Leaving the SerialPort open (eliminating the overhead of opening/closing the SerialPort each time) caused major freezing issues on my Vista laptop. The performance gain was insignificant, so the SerialPort is now closed at the end of each ReadTemp call. As the SerialPort is closed at the end of each read, there is no longer a need to manually call the Close function on the TEMPerInterface class (therefore that function has been removed).