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

1 comments:

  1. Alex,

    I am trying to install the drivers for Temper and cannot get it to work. It shows the device in device manager as "USB-SERIAL(COM4)" and with the following message in the properties window "Windows successfully loaded the device driver for this hardware but cannot find the hardware device. (Code 41)".

    Also there is a black and yellow exclamation next to this name in the device manager. T
    he instruction details on the cd and on Amazon for its product description don't line up with the way the setup files work. Any inputs or suggestions would be great.

    Thank you.

    ReplyDelete