Some Assembly Required - It's Hot in Here. Connecting to Diverse Sources of Temperature Data using Visual Studio 2005 Express

Description

Scott Hanselman

Summary: In the second installment of the "Some Assembly Required" column, Scott Hanselman explains how to use Visual 2005 Express Edition and the .NET Framework 2.0 to retrieve weather information not only from a Web Service, but also from a local "Phidget" analog thermometer. He'll discuss interfaces, dynamic assembly loading, as well as some simple graphing and painting with WinForms.

Interfacing with the World

Welcome to the second installment of my new MSDN Hobbyist column "Some Assembly Required." This column is all about using C# and the .NET Framework to interface with gadgets and hardware - from your cell phone to your MP3 player, from your ReplayTV to your Windows Media Center PC, from your COM ports to a GPS device.

Phidget1

Phidgets

One of these days I'm going to make a 'poor man's' robot with C#. It'll be cool to get it interfacing with the outside world, maybe use an old QuickCam for its eyes, and maybe a Roomba body. But, first things first. I discovered this fantastic hobbyist site called Phidgets. From their site: "Phidgets are an easy to use set of building blocks for low cost sensing and control from your PC. Using the Universal Serial Bus (USB) as the
basis for all Phidgets, the complexity is managed behind an easy to use and robust Application Programming Interface (API).
" Sounds like the kind of thing I could use to start using hardware sensors with .NET.

When I read that marketing description, being the hobbyist and cheapskate I am, I see "Phidgets blah blah easy blah blah low cost blah blah API." Music to my ears. When I checked out their site they say "Applications can be developed quickly in Visual Basic [6], with beta support for C, C++, Delphi and Foxpro." Hm, no .NET Framework support. Well, if they support Visual Basic 6, that means they support COM which means I can use them in the .NET Framework. Sweet. Now what can I build?

It's Getting Hot In Here, Let's Take Off All Our Codes

I keep killing plants at my office. I have a suspicion that the temperature isn't very well maintained and that it's getting very cold at night and very hot in the weekend days.  I'll build a temperature monitor that charts the temperature in my office. However, it'd be nice to see what the temperature is outside at the same time, for context. I picked up a Temperature Sensor and an Interface Kit from the Phidgets folks. The kit is extendable with all sorts of things like RFID, Motion Detectors and Sliders.

The Phidgets software installs a classic COM DLL and that's it. Simple. I'll get into the details later, first I need to figure out how I'm going to get the temperature from outside and inside.

Interface-Oriented Programming

I want to get the temperature both outside and inside. I'll handle inside with the Phidget, why not a Web Service to get the outside? It's simple and the information is already out there. I searched around and found that CapeClear has a lovely Airport Weather Web Service with WSDL available. Visual Studio will be able to consume the WSDL and give us data types and an 'endpoint' to use. That'll get us the temperature at the Portland Airport (PDX or whatever airport you like).

There's commonality here. Even though the inside weather will come from a mysterious unmanaged COM library and the outside weather will come from a mysterious foreign WSDL-described Web Service, they are both giving me the current temperature in Celsius.

An "Interface" is a contract that can be described in code, and classes can be written that promise to play by that interface's rules. For example, I'll make an ICurrentTemperature ("I" for Interface) contract.

namespace ICurrentTemperature
{
    public interface ICurrentTemperatureProvider
    {
        double GetCurrentTemperatureInCelcius();
    }
}

This interface doesn't describe exactly HOW the temperature will be retrieve, just that it will be. The implementation is a detail left up to us. So, I'll make two classes, one that talks to the Phidget and one that talks to the Weather Service. Both will return the temperature in Celsius. Then I'll make a WinForms application that dynamically loads and runs all available ICurrentTemperatureProviders and charts the temperature.

First, make a small class library containing only the ICurrentTemperatureProvider interface. Keeping it separate from all the other assemblies means that the contract can be freely shared and referred to by the other projects we'll make. At the end we'll have a total of four assemblies:

  • ICurrentTemperature.dll - The interface that's shared by all projects
  • PhidgetTemp.dll - The assembly that implements ICurrentTemperatureProvider and talks COM Interop to the actual Phidget Device
  • WebServiceTemp.dll - The assembly that implements ICurrentTemperatureProvider and talks Web Services to the Global Weather Service
  • TemperatureMonitor.exe - The WinForms application that will dynamically execute all the assemblies in the current directory and the GetCurrentTemperatureInCelcius method an chart the results

Calling the Phidget COM Library with the .NET Framework

1114Every COM library is different, and hardware-specific ones may provide a view on top of the hardware that isn't initially obvious. To add a reference to a COM Library and automatically generate a .NET Framework-based view or "Interop Assembly" for that library, right-click on the References section of your PhidgetsTemp assembly and click "Add Reference." Select the COM tab and select the Phidgets COM library. A new Interop.PHIDGET.dll will be created for you and will appear in your project's output directory when you compile. When your .NET Framework-based project is calling what appear to be methods on the underlying COM library, you're acutally calling this Runtime Callable Wrapper (RCW) that handles all the COM Interop goo. The result is a seemless experience where the Microsoft .NET developer can use whatever libraries are available. It'd be a shame if I couldn't use the Phidgets Hardware just because there isn't a native Microsoft .NET library available!

The Phidget COM Library has a PhidgetManager class that lets us initialize the system and get to the IPhidgetInterfaceKit and its sensors. There's a one-time initialization of the system then we'll call the library's get_SensorValue on the analog port the temperature monitor is plugged into.

The Phidget Interface Kit measures 1000 steps between 0 Volts and 5 Volts when returning values. This means one unit for a sensor's value is 5mV (0.005V).  Looking at the hardware datasheet for the analog temperature sensor gives the following equation to get the temperature from voltage to Celcius:

TempInCelcius = (SensorValue - 200)/4

namespace PhidgetTemp
{
    public class PhidgetTemp : ICurrentTemperatureProvider
    {
        public double GetCurrentTemperatureInCelcius()
        {
            if(initialized == false)
            {
                PhidgetTemp.Initialize();
            }
            double sensorValue = (double)kit.get_SensorValue(analogPort);
            double temp = ((sensorValue - 200) / 4);
            return temp;
        }
        //SNIP...
    }
}

Calling the Global Weather Service

AddwebreferenceAdding a reference to a Web Services is very similar to adding an ordinary library reference, except of course, that the actual work is happening elsewhere. When we added the reference to the Phidgets COM library, the Phidgets.dll provided enough information to the .NET Framework to generate a suitable "proxy" that looks and smells like a .NET Framework-based Library. This enables us to use that Phidgets library in any .NET-supported language. The "Add Web Reference" dialog asks for an URL and we'll enter the location of the Web Services Description Language (WSDL) file for the Global Weather service. When we click Add Reference, Visual Studio will create a callable proxy for the service.

Now, we'll create another implementation of ICurrentTemperatureProvider, this time calling the Global Weather Service rather than the Phidgets.

namespace WebServiceTemp
{
    public class WebServiceTemp : ICurrentTemperatureProvider
    {
        public double GetCurrentTemperatureInCelcius()
        {
            com.capescience.live.GlobalWeather ws = new com.capescience.live.GlobalWeather();
            com.capescience.live.WeatherReport report = ws.getWeatherReport(System.Configuration.ConfigurationSettings.AppSettings["AirportCode"]);
            return report.temperature.ambient;
        }
    }
}

 

At this point, we should have three assemblies. One assembly with just the ICurrentTemperatureProvider interface, one for the Phidget implementation and one for the Web Service implementation.

It's About Contracts

Just a small side note here - certainly we could just write a monolithic WinForms application that called the Phidgets library directly and Weather Web Service directly, but there's a benefit in this seemingly round-about method. Besides learning about interfaces, we are making our application more modular by spliting it into 'services' or functional areas. Not only can you apply these method to your own projects and hobbies, but you can also start watching for these patterns in applications you use every day. What would Excel or Word be without plug-ins? How would a graphics application work without pluggable filters and formats? By using an external contract like ITemperatureProvider (or any contract) we are being more explicit in expressing our intent as programmers. That's the whole point of programming, clearly expressing your exact intent - not just to the computer, but to other programmers.

Data Collection

Now, to the real work. Our new TemperatureMonitor WinForms application will need to scan the current directory for assemblies that implement ICurrentTemperatureProvider. That is, look for assemblies it can call. Remember that our application doesn't know anything about Phidgets or Web Services; it only knows about ICurrentTemperatureProvider.

Using the new Generics functionality in the .NET Framework 2.0, we'll create a list of ICurrentTemperatureProviders. We'll load the list by loading all the assemblies and scaning them for Types that implement ICurrentTemperatureProvider. If we find one, we'll create it and store the instances in our list. Later, we'll spin through the list and actually all getCurrentTemperatureInCelcius. (NOTE: There are reasons to avoid using Assembly.LoadFrom, but it met our needs here. Alternatively, as an enhancement we could forgo the dynamic scanning and put th ethe fully-qualfied names of the assemblies we want to load in our application's configuration file.)

List<ICurrentTemperatureProvider> TemperatureImplementations = new List<ICurrentTemperatureProvider>(); 

private List<ICurrentTemperatureProvider> GetTemperatureImplementations(string dir)
{
    List<ICurrentTemperatureProvider> retVal = new List<ICurrentTemperatureProvider>();
     foreach(string f in Directory.GetFiles(dir,"*.dll"))
    {
        try
        {
            Assembly a = Assembly.LoadFrom(f);
            foreach (Type t in a.GetTypes())
            {
                if (null != t.GetInterface(typeof(ICurrentTemperatureProvider).FullName))
                {
                    ICurrentTemperatureProvider i = Activator.CreateInstance(t) as ICurrentTemperatureProvider;
                    if (null != i)
                    {
                        retVal.Add(i);
                    }
                }
            }
        }
        catch (BadImageFormatException)
        {
            //skip bad assemblies.
        }
    }
    return retVal;
}

Later, when we want to all our implementations (in this case, we'll expect to find two, the Phidget and the Web Service) we can just spin through them and call them like this:

foreach (ICurrentTemperatureProvider temp in TemperatureImplementations)
{
    try
    {
        double i = temp.GetCurrentTemperatureInCelcius();
        listBox1.Items.Add(i + " from " + temp.GetType().Name);
    }
    catch (Exception ex)
    {
        listBox1.Items.Add(ex.ToString());
    }
}

Notice that no casting is needed in the foreach loop, because the TemperatureImplementations variable was declared using generics so we're sure of the type of its contents.

List<ICurrentTemperatureProvider> TemperatureImplementations = new List<ICurrentTemperatureProvider>();

I'll store the records we've collected in an internal TemperatureRecord data structure and call our loop on an interval. We could use the new .NET Framework 2.0 BackgroundWorker component, but the standard Timer control serves our purposes here well.

As the collected TemperatureRecords are stored, we'll draw them on a PictureBox making what I'm calling a "poor man's chart." I'm doing my own drawing for two reasons: one, it's fun and I have simple needs and two, I didn't want to buy a chart component. I'll leave upgrading to a commercial chart as an excercise to the reader. I don't want anyone adding records to my collection while I'm pulling from it, so I'll lock before I start painting.  This method says in psuedocode:

For each kind of TemperatureRecord, get a unique color
   For each TemperaureRecord of this kind, starting from the most recent
      Draw it as a point on a graph, starting from the right
      If you reach the left edge, stop drawing the old records

private void UpdatePanel(Graphics g)
 {
    lock (objectLock)
    {
        using (SolidBrush white = new SolidBrush(Color.WhiteSmoke))
        using (SolidBrush black = new SolidBrush(Color.Black))
        using (SolidBrush inside = new SolidBrush(Color.Blue))
        using (SolidBrush outside = new SolidBrush(Color.Red))
        using (Pen twoWidePen = new Pen(black, 2))
        {
            g.FillRectangle(white, 0, 0, pictureBox1.Width, pictureBox1.Height);
            int colorIndex = 0;
            //draw each  plugin in a different color
            foreach (string name in TemperatureRecordsDictionary.Keys)
            {
                using (Pen coloredPen = new Pen(colors[colorIndex], 2))
                {
                    int x = pictureBox1.Width;
                    //start slightly off screen
                    Point previousPoint = new Point(pictureBox1.Width+1, pictureBox1.Height);

                    //draw the most recent records first
                    for (int i = TemperatureRecordsDictionary[name].Count - 1; i >= 0; i--)
                    {
                        TemperatureRecord record = TemperatureRecordsDictionary[name][i];
                        //y-scale is 0 to 100 Celcius
                        // This was trivial, although I should have used a Matrix transform
                        int y = pictureBox1.Height - (int)((record.tempCelcius * pictureBox1.Height) / 100);
                        Point newPoint = new Point(x, y);
                        g.DrawLine(coloredPen, previousPoint, newPoint);
                        previousPoint = newPoint;
                        x -= 10;
                        if (x <= 0)
                        {
                            //we're reaching the left edge, stop drawing
                            break;
                        }
                    }
                }
                colorIndex++;
                if (colorIndex > 12) throw new ArgumentOutOfRangeException("I've only got 12 colors to use, orry!");
            }
            //x-axis
            int smallOffset = 1;
            g.DrawLine(twoWidePen, smallOffset, pictureBox1.Height - smallOffset, pictureBox1.Width - smallOffset, pictureBox1.Height - smallOffset);
            //y-axis
            g.DrawLine(twoWidePen, smallOffset, pictureBox1.Height - smallOffset, smallOffset, smallOffset);
        }
    }
 }

TemperaturemonitorA few things are worth noting in this snippet. I'm from the old-school GDI days and back then one had to clean up after themselves. Any time you make a Pen, you delete it. You make a Brush, you delete it. In the world of GDI+ and the .NET Framework, I'm told this is less important, but old habits die hard. I like to use the using statement a LOT. It's a great statement that's built into the languages. It says, "I'm using this within this scope. When I'm done, call Dispose() for me." Types that implement IDisposable have a Dispose() statement and will clean up any physical resources they may be holding on when Dispose() is called. If I didn't call Dispose(), it'd eventually be called for me when the object was finalized (garbage collected). However, I don't have any control over when that happens, but I DO have control over when Dispose() is called. I will almost aways vote for being explicit over implicit if I can.

Conclusion

There's lot of things that could be extended, added, and improved on with this project. Here are some ideas to get you started:

  • Create an interface of your own to collect diverse information and chart it
    • A dynamically charting stock ticker via Web Services
    • Visits to your blog or website
    • Network access via PerfMon or WMI
  • Add logging of details to a text file
  • Get a Phidgets motion detector and chart/log when people sneak into your office
  • Change the use of the Timer control to BackgroundWorker to make the UI more responsive
  • Chart the weather via the Global Weather service from multiple locations
  • Expand (heh, make prettier) the chart to use a 3rd party chart like Dundas Chart
  • Improve the chart on your own by adding axis tick marks and a legend

Have fun and have no fear when faced with the words - Some Assembly Required!


Scott Hanselman is the Chief Architect at the Corillian Corporation, an eFinance enabler. He has twelve years experience developing software in C, C++, VB, COM, and most recently in VB.NET and C#. Scott's proud to be a Microsoft RD and MVP. He is co-author of a new book on ASP.NET 2.0 with Bill Evjen and company, which will be released later in 2005. His thoughts on the Zen of .NET, Programming and Web Services can be found on his blog at http://www.computerzen.com 

The Discussion

  • User profile image
    Tosin

    can you please send vb.net codes for motion detector to my mail(hero9081@yahoo.com)

Comments closed

Comments have been closed since this content was published more than 30 days ago, but if you'd like to send us feedback you can Contact Us.