Creating a Temperature-Sensor Gadget for Windows Sidebar with C#

This article was written for application developers who are interested in displaying portable device data in the Windows Sidebar. It describes a Microsoft .Net application written in C# as well as a gadget for Windows Sidebar. The application was written using Microsoft Visual Studio 8. The WPD gadget was written in HTML and JScript.

The .Net application retrieves temperature data from a Parallax temperature-sensor device and writes this data to a file on the local disk. A script in the gadget's HTML file reads the contents of this local file and displays the temperature in the Sidebar. 

The temperature sensor used by this application is based on the BASIC Stamp Activity Kit for Windows Portable Devices. This kit is offered by the Parallax Corporation in Rocklin, California. You can order the kit from the company’s website.

Running the Application and Viewing the Gadget

The application and gadget described in this article require that you install the WPD temperature sensor driver (WpdTempSensorDriver.dll) and that you connect the temperature sensor device over a standard RS232 port.

The WPD temperature-sensor driver exposes a Temperature Sensor object. This object, in turn, exposes three programmatic elements: two properties and an event. These elements are described in the following table.

Programmatic Element

Description

Interval property

This read/write property specifies the frequency at which the device should return the current temperature. (This property is specified in milliseconds.)

Temperature property

This read-only property specifies the current temperature in degrees Kelvin.

Temperature-reading event

This event is fired each time the device retrieves the current temperature. (The Interval property specifies the frequency at which the device will fire this event.)

Building and Installing the Temperature Sensor Device

The temperature sensor device, referenced in this article, is based on a Parallax BS2 microcontroller and an AD592 Temperature Sensor transducer.

Parallax supplies a complete hardware kit that includes all of the required components. To order the kit, see this page on the Parallax site.

You can download the Basic Stamp source code for the temperature sensor device from the WHDC site.

Building and Installing the WPD Temperature Sensor Driver

In order to install the driver, you’ll first need to download the source code and build it. For information about downloading the driver, see the following topic on the WHDC site.

Installing the Gadget

The sidebar gadget is found here at the Microsoft Download Center.

Download this file to your local machine, open Windows Explorer, and double-click on the icon. Vista will ask you whether or not you want to install the gadget. Choose the Install button.

The gadget will be installed under the Desktop\<user name>\AppData\Local\Microsoft\Windows Sidebar\Gadgets folder.

Installing the Windows SDK

Because there’s no native .Net support for the WPD API, you’ll need to use the WPD COM interfaces if you are writing a .Net application. Your application will access these interfaces through the COM Interoperability (interop) feature of .Net.

When working with the type libraries for COM objects, it’s sometimes necessary to make minor revisions to the type library metadata. Microsoft provides tools you can use to make these changes in the Windows SDK; so, you’ll want to download this SDK onto your development machine.

You can download the Windows SDK here.

A Client/Server Model

The .Net application described in this article is a server while the gadget acts as its client. The server monitors the temperature sensor device for event notifications. Each time it receives a notification, this application writes corresponding temperature data to a file on disk. The data is formatted as HTML. The client (or gadget) retrieves the data from the file created by the application and renders it in the sidebar.

The .Net application sets a timer event to fire every three seconds. Each time the event fires, the application retrieves the current temperature from the WPD temperature-sensor device. The application then writes the temperature as a string of HTML to an ASCII text file on the local disk. This string has the following appearance:

<P>Office Temperature<P>70&deg; Farenheit

The gadget’s script executes every 3,000 milliseconds; this script reads the string of HTML created by the application and inserts the string into the body of the gadget’s HTML

The Temperature-Sensor Application

The temperature-sensor application described in this article is a simple application that accomplishes the following tasks:

· It searches for the temperature-sensor device and opens a connection if it’s found.

· It sets a timer event which is fired ever 3 seconds.

· Each time the event fires, it retrieves the current temperature from the connected device and writes it to local disk.

A Windows temperature-sensor application project, written in C# and .Net, might consist of the following files:

File

Description

Form1.cs

Searches for the temperature-sensor device and opens a connection if it’s found. Sets the timer event and retrieves the current temperature from the device, and, writes the temperature value to a file on local disk.

Form1.Designer.cs

Defines the InitializeComponent method (a method describing the application’s form and its contents).

Form1.resx

Defines the resources found on the form (a single timer resource).

Interop.PortableDeviceApiLib.dll

Revised interop assembly that correctly handles the call to the IPortableDeviceManager::GetDevices method. (See https://blogs.msdn.com/dimeby8 and the Enumerating WPD devices in C# topic.)

Portabledeviceconstants.cs

A file containing definitions for the property-keys used by the WPD API as well as the property key for the TEMP_SENSOR_READING property.

Program.cs

Defines the application’s main entry point.

WpdSidebarGadget.csproj

The Visual Studio 8 project file.

Accessing the WPD API through .Net

Because there’s no native .Net support for the WPD API, you’ll need to use the WPD COM interfaces through the COM Interoperability (interop) feature of .Net. This feature lets your .Net application “think” that it’s accessing a .Net version of WPD, when, in fact, it’s accessing the unmanaged objects instead.

COM objects (like the WPD API) are described by type libraries. These libraries give the Visual Studio 8 compiler a definition of the types, interfaces, and methods that WPD supports.

When working with the type libraries for COM objects, it’s sometimes necessary to make minor revisions to the metadata associated with these libraries. Microsoft provides the tools necessary to make these changes in the Windows SDK, so, you’ll want to download this SDK onto your development machine.

Your application will use the COM interfaces found in the WPD API to accomplish tasks like: enumerating connected devices, opening a device, closing a device, enumerating objects on a device, reading and writing properties on a device, sending a command to a device, and registering to receive events from a device.

In order to access these interfaces and their methods from your C# .Net application, you’ll need to add references to the PortableDeviceApiLib and the PortableDeviceTypes Type Libraries in your Visual Studio 8 .Net project. You’ll add these references through the Project/Add Reference menu and the COM tab found on the associated dialog.

Once you add the references to these two type libraries, you’ll need to resolve several marshalling restrictions in the Interop assembly that Visual Studio generates for the PortableDeviceApiLib. (You’ll use a couple of tools found in the Windows SDK to resolve these restrictions.)

Resolving the Marshalling Restrictions

Create a Visual Studio 8 project for a C# Windows application and add the references to the two type libraries listed above (PortableDeviceApiLib and PortableDeviceTypes). Next, build your application.

When you built your application, Visual Studio invoked the type library importer and created what’s called an Interop Assembly for each of the type libraries that you’d added. You’ll find these assemblies in the \bin\debug folder of your project (if you built a debug version of your application); otherwise, you’ll find these assemblies in the \bin\release folder.

An Interop Assembly lists the COM types that your application can access via COM interoperability. These assemblies contain metadata that the .Net compiler uses to resolve method calls.

Unfortunately, some of the metadata generated by the type library importer is incorrect for C# applications. For example, if you examine the metadata for the IPortableDeviceManager::GetDevices method, you’ll see that the first argument (an array of string pointers) is specified to be:

[in][out] string& marshal( lpwstr) pPnPDeviceIDs

You’ll need to replace unary & operator with the square brackets which are used to specify an array type in C#. And, you’ll need to replace the lpwstr type after the marshal keyword with the [] operator. So, the new description of this argument would appear as below:

[in][out] string[] marshal([]) pPnPDeviceIDs

In general, for WPD methods that take an array or buffer as an argument, you’ll want to replace any occurrence of the unary & operator found in the type-libraries metadata with the square brackets. (And, you may also need to add the marshal keyword and arguments.)

To edit the metadata in the interop assembly, you’ll need to use the IL Disassembler (ILDASM.EXE) and the IL Assembler (ILASM.EXE). The disassembler creates an editable text file from an interop assembly file. After you’ve edited this text file (and revised the metadata), you’ll rebuild the interop assembly using the assembler.

The following steps describe how you would create an editable text file from the PortableDeviceApi interop assembly, how you would revise the metadata for the GetDevices and GetDeviceFriendlyName methods, and how you would create a new, revised assembly.

1. Open an instance of the Windows SDK command shell.

2. Use the “cd” command to change directory to your project’s \debug folder that contains the file Interop.PortableDeviceApiLib.dll

3. Use the following command to disassemble the PortableDeviceAPI interop assembly into an editable text file:
ildasm Interop.PortableDeviceApiLib.dll /out:pdapi.il

4. Open the text file pdapil.il in notepad and replace all occurrences of the pPnPDeviceIDs argument for the GetDevices method with the correct metadata. Replase the following string:
instance void GetDevices([in][out] string& marshal( lpwstr) pPnPDeviceIDs, with the revised string:
instance void GetDevices([in][out] string[] marshal([]) pPnPDeviceIDs,

5. For the sample described in this article, you’ll also want to make the following changes to the description of the pDeviceFriendlyName argument for the GetDeviceFriendlyName method. Replace the following string:
[in][out] uint16& pDeviceFriendlyName
with the revised string:
[in][out] uint16[] marshal( []) pDeviceFriendlyName

6. Assemble the revised metadata text file into a new interop assembly using the following command:
ilasm pdapi.il /dll /output=Interop.PortableDeviceApiLib.dll

7. Move the newly created assembly to the root folder of your project. (If you leave in the \debug folder, Visual Studio will overwrite it the next time you build.)

8. Remove the reference in our Visual Studio project to the original interop assembly and create a new reference to the newly modified assembly that resides in the root folder of your project.

If your application invokes other WPD methods that use arrays or buffers as arguments, you would repeat the revisions described in steps 4. and 5. above before you created the new interop assembly.

Accessing the WPD Properties and Commands

WPD properties and commands are identified by a PROPERTYKEY structure. This structure has two parts: a GUID and a DWORD. (For more information, see the discussion of WPD PROPERTYKEYS on MSDN).

In order to use the common WPD PROPERTYKEYs, you’ll need to add a file containing their definition to your C# project. For a complete description of how this is done, refer to the following link on the dimeby8 blog.

In the sample code found in this article, we refer to a file portabledeviceconstants.cs which we created using the guidelines in the dimeby8 blog (above).

Creating the WPD Objects

One of the first tasks a .Net will accomplish is the creation of the WPD objects that are used throughout the application to invoke methods, retrieve property values, and so on. The following snippet demonstrates how an application might create instances of the device-manager object, a device-values collection, and a portable device object.

namespace WpdSidebarGadget

{

    public partial class Form1 : Form

    {

        //

        // Get an instance of the device manager

        //

        PortableDeviceApiLib.PortableDeviceManagerClass devMgr

            = new PortableDeviceApiLib.PortableDeviceManagerClass();

        // Create our client information collection

        PortableDeviceApiLib.IPortableDeviceValues pValues =

       (PortableDeviceApiLib.IPortableDeviceValues)

                new PortableDeviceTypesLib.PortableDeviceValuesClass();

        // Create a new IPortableDevice instance

        PortableDeviceApiLib.PortableDeviceClass pPortableDevice =

                new PortableDeviceApiLib.PortableDeviceClass();

The following table lists the variable corresponding to each object and describes its use later in the application.

Variable

Description

devMgr

Used to retrieve a count of connected devices, to retrieve Plug and Play identifiers for those devices, and to retrieve friendly names for those devices,

pValues

Used to specify client information when opening a connection to the device.

pPortableDevice

Used to open a connection to the temperature-sensor device. Also used to retrieve the value of the TEMP_SENSOR_READING property.

Opening a Connection to the Temperature-Sensor Device

The process of opening a connection to a device is done in two phases: in the first phase we check to see if the device is connected to the PC; if it’s found, we open the connection in the second phase.

Searching for a Specific Device

The IPortableDeviceManager::GetDevices method serves two purposes. If the first parameter is set to null, it returns a count of connected devices. Otherwise, it returns a list of Plug and Play identifiers for each connected device.

The Plug and Play names are used internally by WPD to identify devices. The Plug and Play name for the temperature sensor device is: “\\?\root#wpd#0000#{6ac27878-a6fa-4155-ba85-f98f491d4f33}”. This string uniquely identifies the device instance and the device interface and is defined as part of the Windows Driver Model (WDM). For more information, refer to this topic on MSDN.

The IPortableDeviceManager::GetFriendlyName method maps a given Plug and Play identifier back to the given device’s friendly name. This is the name commonly associated with a device by the user. For example: “Parallax BS2 Temperature Sensor”. For more details about the use of the GetFriendlyName method, see the following section titled: “Retrieving the Friendly Name for a Given Device”.

The following code demonstrates how an application used the GetDevices method to retrieve a count of connected devices and then, subsequently, retrieve the Plug and Play identifier for each device. This code then retrieves the friendly name for each connected device (by calling the RetrieveFriendlyName helper function), and checks to see if any of the connected devices have a friendly name that matches the string: “Parallax BS2 Temperature Sensor”. If such a device exists, its Plug and Play identifier is passed to the StartProcessing helper function which opens a connection to the device and begins retrieving temperature data.

private void Form1_Load(object sender, EventArgs e)

{

    uint i = 0;

    uint cDevices = 0;

    string strDeviceID = String.Empty;

    string strFriendlyName = String.Empty;

    // Retrieve a count of connected WPD devices.

    devMgr.GetDevices(null, ref cDevices);

    // Iterate through the connected WPD devices--searching for

    // a TemperatureSensor device. If it's found, open the device

    // and begin retrieving the temperature property.

    if (cDevices > 0)

    {

        // Retrieve the PnP identifiers for each

        // connected device.

        string[] strPnPDeviceIDs = new string[cDevices];

        devMgr.GetDevices(strPnPDeviceIDs, ref cDevices);

         // For each connected device, retrieve the friendly

         // name and compare it to the TempSensor's name.

         for (i = 0; i < cDevices; i++)

         {

             strFriendlyName = RetrieveFriendlyName(devMgr, strPnPDeviceIDs[i]);

             if (strFriendlyName == "Parallax BS2 Temperature Sensor")

             {

                 // Open the device and begin retrieving the temperature

                 StartProcessing(pPortableDevice, pValues, strPnPDeviceIDs[i]);

              }

          }

      }

  }

Retrieving the Friendly Name for a Given Device

As noted in the previous section, the IPortableDeviceManager::GetFriendlyName method maps a given Plug and Play identifier back to the given device’s friendly name. In addition to retrieving a device’s friendly name, this method also returns a count of characters in the friendly-name string--if the second parameter is set to null.

The following helper function, RetrieveFriendlyName, demonstrates how your application could use the GetFriendlyName method to retrieve the count of characters in the friendly-name string, allocate memory for the string, and then retrieve it.

string RetrieveFriendlyName(

                        PortableDeviceApiLib.PortableDeviceManagerClass PortableDeviceManager,

                        string PnPDeviceID)

{

    uint cFriendlyName = 0;

    ushort[] usFriendlyName;

    string strFriendlyName = String.Empty;

    // First, pass NULL as the LPWSTR return string parameter to get the total number

    // of characters to allocate for the string value.

    PortableDeviceManager.GetDeviceFriendlyName(PnPDeviceID, null, ref cFriendlyName);

           

    // Second allocate the number of characters needed and retrieve the string value.

    usFriendlyName = new ushort[cFriendlyName];

    if (usFriendlyName.Length > 0)

    {

        PortableDeviceManager.GetDeviceFriendlyName(PnPDeviceID, usFriendlyName, ref cFriendlyName);

        // We need to convert the array of ushorts to a string, one

        // character at a time.

        foreach (ushort letter in usFriendlyName)

            if (letter != 0)

                strFriendlyName += (char)letter;

      // Return the friendly name

        return strFriendlyName;

    }

    else

        return null;

}

Opening a Connection to the Device

The IPortableDevice::Open method opens a connection to a device that’s attached to a PC. This method takes two arguments: the first is the Plug and Play identifier for the target device; the second is an IPortableDeviceValues object that specifies client information for the device. At a minimum, the client information must include the following:

Client Information

Description

Name

A friendly-name for the device.

Major Version Number

The device’s major version number.

Minor Version Number

The device’s minor version number.

Revision Number

The device’s most recent revision number.

The following StartProcessing helper function configures the client information in an IPortableDeviceValues object and then calls the Open method. Once the connection is opened, the StartProcessing method calls a RetrieveTemp method to retrieve the current temperature from the connected temperature-sensor device. After calling the RetrieveTemp method, a timer is enabled so that it fires an event every 3 seconds.

void StartProcessing(

                    PortableDeviceApiLib.PortableDeviceClass pPortableDevice,

                    PortableDeviceApiLib.IPortableDeviceValues pValues,

                    string PnPDeviceID)

{

 

   // We have to provide at the least our name, version, revision

   pValues.SetStringValue(

   ref PortableDevicePKeys.WPD_CLIENT_NAME, "Temp-Sensor Client");

   pValues.SetUnsignedIntegerValue(

   ref PortableDevicePKeys.WPD_CLIENT_MAJOR_VERSION, 1);

   pValues.SetUnsignedIntegerValue(

   ref PortableDevicePKeys.WPD_CLIENT_MINOR_VERSION, 0);

   pValues.SetUnsignedIntegerValue(

   ref PortableDevicePKeys.WPD_CLIENT_REVISION, 2);

   // We are now ready to open a connection to the device

   // We'll assume deviceID contains a valid WPD device path and connect to it

   pPortableDevice.Open(PnPDeviceID, pValues);

   // Retrieve and display the initial temperature

   strVal = RetrieveTemp(pPortableDevice, "Temperature");

   iVal = Convert.ToInt32(strVal);

   // Enable the timer for UI

   timer1.Interval = 3000; // 3 second intervals

   timer1.Enabled = true;

 

}

Retrieving the Temperature Property

You can retrieve a collection of readable properties for a WPD device by calling the IPortableDeviceProperties::GetValues method. This method takes a collection of property keys as input and returns a collection of property values as output. Once the WPD driver returns the collection of values, you can retrieve an individual property’s value by calling one of the Getxxx methods on the IPortableDeviceValues interface. In the case of the temperature property, we’ll call the IPortableDeviceValues::GetSignedIntegerValue method. And, when we call this method, we’ll pass the corresponding property key (TEMP_SENSOR_CURRENT_VALUE) which we’ve added to the file portabledeviceconstants.cs.

The new entry in portabledeviceconstants.cs has the following form:

namespace PortableDeviceConstants

{

    class PortableDevicePKeys

    {

        static PortableDevicePKeys()

        {

          TEMP_SENSOR_READING.fmtid = new Guid(0xA7EF4367, 0x6550, 0x4055, 0xB6, 0x6F, 0xBE, 0x6F, 0xDA, 0xCF, 0x4E, 0x9F);

          TEMP_SENSOR_READING.pid = 2;

The following helper function, RetrieveTemp, retrieves the temperature property from the temperature sensor device and returns that value in string form.

string RetrieveTemp(

        PortableDeviceApiLib.PortableDeviceClass pPortableDevice,

                    string objectID)

{

    string strTemp = String.Empty; // Receives current temp

    //

    // Retrieve IPortableDeviceContent interface required to

    // obtain the IPortableDeviceProperties interface

    //

    PortableDeviceApiLib.IPortableDeviceContent pContent;

    pPortableDevice.Content(out pContent);

    //

    // Retrieve IPortableDeviceProperties interface required

    // to get all the properties

    //

  PortableDeviceApiLib.IPortableDeviceProperties pProperties;

    pContent.Properties(out pProperties);

    //

    // Create a key collection

    //

    PortableDeviceApiLib.IPortableDeviceKeyCollection pKeyCollection =

        (PortableDeviceApiLib.IPortableDeviceKeyCollection)

        new PortableDeviceTypesLib.PortableDeviceKeyCollectionClass();

    // Add the TEMP_SENSOR_READING property to the key collection

    // This value is defined in PortableDeviceConstans.cs

     pKeyCollection.Add(ref PortableDevicePKeys.TEMP_SENSOR_READING);

    //

    // Call the GetValues to retrieve the prop values for the given

    // key. Then, retrieve the TEMP_SENSOR_CURRENT_VALUE property

    // by calling the GetStringValue method.

    //

    PortableDeviceApiLib.IPortableDeviceValues pPropValues;

    pProperties.GetValues(objectID, pKeyCollection, out pPropValues);

    int iTemp = 0;

    pPropValues.GetSignedIntegerValue(ref PortableDevicePKeys.TEMP_SENSOR_READING, out iTemp);

          

    if (iTemp > 0)

  return iTemp.ToString();

    else

        return "Invalid temperature!";

}

There are two approaches to retrieving the current temperature for the temperature-sensor device: an application can register to receive a temperature-update event from the device, or, it can retrieve the temperature value directly. This article focuses on the latter. In this article we’ve used an application-defined timer event to retrieve the temperature property.

The first step in enabling an application-defined timer event requires that you add a timer object to your application’s form. (You’ll find the timer in the Components section of the Visual Studio Toolbox when viewing your application’s form in Design mode.)

Once you add the timer to your form, you can specify the interval at which the corresponding event will be fired, and you can enable the timer object. Both of these tasks were accomplished in the final lines of the StartProcessing method which was described previously in the Open a Connection to the Device section.

   // Enable the timer for UI

   timer1.Interval = 3000; // 3 second intervals

   timer1.Enabled = true;

Each time the timer event fires, it calls the application’s Tick callback function. In our sample, the code for the timer1_Tick method retrieves the temperature from the device, converts it from degrees Kelvin to degrees Farenheit and then writes a string of HTML to disk. (The gadget script will read the contents of this file and use it to render the current temperature in the Sidebar.)

private void timer1_Tick(object sender, EventArgs e)

{

    // Use DateTime structure to retrieve timestamp

    DateTime dtNow = DateTime.Now;

    // Retrieve the temp prop

    strVal = RetrieveTemp(pPortableDevice, "Temperature");

    iVal = KelvinToFarenheit(strVal);

     // Use the StreamWriter object for file I/O

    StreamWriter sw = new StreamWriter("c:\\temp\\tempdata.txt");

    // Write the data to the file

    sw.WriteLine("<P>Office Temperature<P>" + Convert.ToString(iVal) + "&deg; Farenheit");

           

    // Close the streamwriter object

    sw.Close();

 

    // Update the Window text

    this.Text = dtNow.Hour + ":" + dtNow.Minute + ":" + dtNow.Second + " " + Convert.ToString(iVal) + "°";

          

}

The following snippet contains the code for the KelvinToFarenheit method which converts the temperature value.

private int KelvinToFarenheit(string strTemp)

{

    double dTemp = 0.0;

    int iVal = 0;

    dTemp = Convert.ToDouble(strTemp);

    iVal = (int)(((dTemp - 273.15) * 1.8) + 32);

    return iVal;

}

The Temperature-Sensor Gadget

The temperature-sensor gadget displays the current office temperature in degrees Farenheit in the Windows Sidebar.

If you’ve never created a gadget for Windows Sidebar before, you should refer to the Gadget Development Overview.

The Temperature-Sensor Gadget Files

The temperature-sensor gadget consists of three files: an HTML file, an XML file, and a .jpg file. These files are described in the following table.

File Name

Description

UpdateTemperatureReading.htm

An HTML file that contains the script which retrieves the updated temperature data from the local disk every 3,000 milliseconds.

gadget.xml

The gadget manifest. This file specifies properties such as the name of the gadget, the name of its HTML file, a string of descriptive text, and so on.

background.jpg

A 163 x 58 pixel image that produces the gadget’s two-tone blue background color.

Retrieving the Current Temperature

The gadget’s HTML file, UpdateTemperatureReading.htm, contains a script which opens the file created by the application (c:\temp\tempdata.txt) and copies the contents of this file into a span element identified as “temperature_data” within the body of the gadget’s HTML.

This script creates a FileSystemObject which it uses to open and read the text file. It uses the OpenTextFile method to open the file and the ReadAll method to retrieve its contents.

The script uses the setTimeout method on the window object to recursively execute the update function every 3,000 milliseconds. (This function contains the code that retrieves the contents of c:\temp\tempdata.txt.)

<html>

<head>

<script>

var fileSystemObject = null;

function loaded()

{

       fileSystemObject = new ActiveXObject("Scripting.FileSystemObject");

       update();

}

function update()

{

   var textStream = fileSystemObject.OpenTextFile("c:\\temp\\tempdata.txt");

   try

          {

             temperature_data.innerHTML = textStream.ReadAll();

           }

         finally

           {

             textStream.Close();

             window.setTimeout(update, 3000);

           }

}

</script>

<style>

   body

        {

               margin: 0;

               width: 125px;

               height: 62px;

               font: 'normal 8pt Arial';

               line-height: 12pt;

   text-align:center;

   }

</style>

</head>

<body onload="loaded()">

<g:background src="background.png">

<span id="temperature_data"></span>

</g:background>

</body>

</html>

The HTML file also contains a description of the gadget’s window in the <style></style> tags that follow the script. Note that the width and height attributes match the width and height of the background image (background.png). In addition to specifying the size of the gadget’s window, the attributes found here specify the font used to render text, the line-height (which creates padding between the two lines of text), and the text alignment.

The g:background element specifies the source file (background.png) for the gadget’s background image.

The span element “temperature_data” identifies the location of the HTML which is copied from c:\temp\tempdata.txt by the Update function.

 

This posting is provided "AS IS" with no warranties, and confers no rights.