How to Create Remote Tools for Windows CE .NET

 

Mike Hall
Microsoft Corporation

Steve Maillet
Entelechy Consulting

February 5, 2002

Building an embedded system based on Microsoft® Windows® CE .NET can be broken into four major sections:

  • Configure the operating system using Platform Builder.
  • Build the operating system using Platform Builder.
  • Download the operating system to a reference board (or to the Windows CE .NET Emulator).
  • Debug the operating system image (this could include debugging OAL, drivers, and applications).

Platform Builder ships with a number of remote tools that can aid in developing and debugging a platform. These tools run on your desktop and communicate with your development platform over the same debug connection as Platform Builder. This is somewhat different from the usual desktop development process, where the development tool (Microsoft® Visual Studio® .NET) and applications are running on the same PC.

The remote tools can be divided into three groups: verification/performance, debugging, and informational/utility. The following table lists each of the tools and the group in which they belong.

Group Tool
Verification/performance Kernel Tracker
  Remote Call Profiler
  Remote Performance Monitor
Debugging Remote Spy ++
  Remote Heap Walker
  Remote Process Viewer
Informational/utility Remote Zoomin
  Remote File Viewer
  Remote Registry Viewer
  Remote System Information

Previous versions of Platform Builder (and remote tools) were tied to use serial/parallel/Ethernet as the transports for downloading/debugging a platform. The debug transport was exposed using the Platform Manager APIs. This provided a level of abstraction between the tools and underlying debug transport. Windows CE .NET has further extended this model by introducing a layer beneath Platform Manager (Platman) called KITL, the Kernel Independent Transport Layer. KITL provides a debug/download transport plug-in model; reference board developers can now build reference boards that support 1394, SCSI, USB, or other transports for downloading/debugging.

There are in fact two levels to KITL—you create both desktop and Windows CE device-side transport mechanisms. On the desktop, the transport is a separate DLL that exports certain API functions that KITL relies on, and it is also registered in the system, so KITL knows that it is a functional transport. This sits below Platform Manager. Remote tools simply call the Platform Manager APIs, which then map down to the appropriate KITL transport. On the device, the transport is built into the OEM adaptation layer (OAL). Reference board designers therefore need to write the appropriate functions, which then add support for KITL to their platform.

Device-side KITL needs to communicate with the underlying hardware, including the ability to encode and decode a packet, send and receive a frame/packet, enable and disable the transport interrupt, and get and set configuration information. To enable this support on your reference platform, you must support the following functions on the embedded device: TransportEncode, TransportDecode, TransportSend, TransportRecv, TransportEnableInt, TransportGetDevCfg, TransportSetHostCfg.

Platform Builder online help describes the purpose of each of these functions, and explains how to add KITL support to your platform OAL.

This article will concentrate on showing how to build a remote tool using the Platform Manager APIs. Platform Manager exposes a COM-based object model that can easily be used from desktop development tools, including Microsoft® Visual C++® and Microsoft Visual Basic®. In the following examples, I will be using Visual Studio .NET to build the sample code.

Platform Manager

Platform Manager (PM) is a set of tools used to manage the connectivity between the target CE device and the development workstation. Windows CE .NET extends PM to include support for the operating system download and Kernel transport used by Platform Builder tools. This means that now all of the tools including the IDE itself use platform manager for all connections to a target device. To account for the different types of devices and hardware in the real world, PM supports plug-ins for a startup server and the actual data transport.

Startup Server

The startup server is used to start the transport on the device. Something has to start the code running on the device that will then communicate to the workstation. (Which came first, the chicken or the egg?) The primary role of the startup server is to start the device-side application (cemgrc.exe) that will connect to the workstation and inform it as to which protocol to use to communicate. There are 3 basic types of startup servers detailed below:

  1. Manual: The server is started manually (from the registry at boot or manually from a command line [GUI, serial, telnet]). Platform manager provides command-line arguments you need to enter in order to start cemgrc.exe.
  2. CESH: The server is started through the Windows CE debug shell (CESH). This requires that PB is up and running and connected to the target device through CESH. The server is launched by sending a start command through the Platform Builder's current CESH connection.
  3. ActiveSync: The server is started by sending a start through an existing ActiveSynch connection.

It is theoretically possible to develop and implement custom startup servers; it's not officially documented at this point. If you are interested in that option, let us know and we may tackle that in a future article.

Protocol

The protocol component manages all of the data communications between the workstation and the target device after the server (cemgrc.exe) is started. There are a few basic protocols provided with Platform Builder, and you can create your own if needed. (We won't be covering that in this article, but as always, if you are interested in that, let us know and we can cover it in the future.)

  1. ActiveSync: Uses an existing ActiveSync connection for all communication with the device side of the tool.
  2. PPP: Uses a PPP connection with the device (established by remnet.exe on the device and RAS on the workstation).
  3. TCP/IP: Uses an existing TCP/IP connection over Ethernet. (Requires that WINS is set up on the device to resolve the workstation's name. Normally this is done by setting the WINS Server option on the device to point to the workstation's IP address.)
  4. KITL: Uses the existing Platform Builder IDE KITL connection. This requires that PB is connected to the device.

Figure 1. Platform Manager object model

The following diagram illustrates the relationships of the various parts of the communications:

Click here for larger image.

Figure 2. Communications relationships (click thumbnail for larger image)

The left side of the diagram represents components on the development workstation, and the right side shows components running on the target device. The numbered "messages" illustrate the general sequence of interaction between the components when establishing an initial connection to the target device. Once the connection is established with the assistance of the startup server, all communications go through the protocol handler.

Creating Your Own Tools

Microsoft ships a number of samples that can be used as starting points for building new remote tools. These can be found in the following folder on your desktop PC: C:\Program Files\Common Files\Microsoft Shared\Windows CE Tools\Platman\sdk\samples.

Okay, onto sample code... The following Visual Basic .NET sample shows how to use the Platform Manager APIs to enumerate platforms and devices, make a connection to a device, and then copy a file to a folder on the device. At this point you're probably asking yourself what the difference is between a platform and a device. The Pocket PC can be considered to be a platform. When you install the Pocket PC SDK, this installs support for two devices: a Pocket PC (real hardware), and the Pocket PC Emulator (which runs on your desktop).

Here's how the final application will look when it's running:

Figure 3. Visual Basic .NET "Platman" application

There are two list boxes. The first contains a list of platforms; the second contains a list of devices. I've also added an edit control, which points to a file on my desktop PC's hard drive. Clicking Copy File To Device will (surprisingly) copy the file to the device.

And just to show that this does work, here's a snapshot of the Windows CE .NET emulator showing the Foo.txt file.

Figure 4. File copied to emulation

Here is the Visual Basic .NET code. (Note that this application could have been written in C/C++, Visual Basic, or C#—or any other Windows development language. I just chose to use Visual Basic because it's fairly easy to read.)

I declare an application-wide variable, tPlatman, which is used to create the initial Platman interface.

    Dim tPlatman As Object

On Form Load I create the Platman object. The Platman object exposes a number of interfaces, including enumeration of platforms, adding new devices, enumerating CPUs, and so on. You can clearly see this from the Platform Manager object model diagram above, Figure 1. For a complete listing of the tPlatform interfaces, take a look at the Platform Builder documentation; search for IPMPlatformManager.

    Private Sub Form1_Load(ByVal sender As Object,
        ByVal e As System.EventArgs) Handles MyBase.Load
        tPlatman = CreateObject("PlatformManager.PlatformManager.1")
    End Sub

I've added a button_click handler for the "Get Platforms" button—here's the code. This enumerates the known platforms and adds these to the topmost list box.

    ' Get a list of available Platforms
    Private Sub Button1_Click(ByVal sender As System.Object, 
ByVal e As System.EventArgs) Handles Button1.Click
        Dim tPlatform As Object
        For Each tPlatform In tPlatman.EnumPlatforms
            ListBox1.Items.Add(tPlatform.Name)
        Next tPlatform
        tPlatform = Nothing
    End Sub

Once the user has selected a Platform, they can click the second button. This enumerates the devices within the selected platform, and adds these to the second list box. We could display this information in a tree control, without requiring the user to select a platform (similar to the standard Windows CE Remote Tools connection dialog). In the sample code below, I also capture the name of the selected platform by calling tPlatform.Name. This could be used to prompt the user to confirm whether they've selected the correct platform.

The tPlatform object can be used to enumerate devices, transports, and startup servers. Take a look at the Platform Builder documentation for the complete list of exposed interfaces; search for IPMPlatform.

    ' Get a list of Devices associated with the current selected platform
    Private Sub Button5_Click(ByVal sender As System.Object,
        ByVal e As System.EventArgs) Handles Button5.Click
        Dim tPlatform As Object
        Dim tDevice As Object
        Dim DeviceName As String

        tPlatform = tPlatman.GetPlatform(ListBox1.SelectedItem)

        ' Get the Platform Name - we could use this to prompt the user
        ' to determine if this is the correct platform to use.
        DeviceName = tPlatform.Name()

        ' tPlatform.Enumdevices
        For Each tDevice In tPlatform.EnumDevices
            ListBox2.Items.Add(tDevice.Name)
        Next tDevice
    End Sub

So at this point we know the selected platform and device—we can make a connection to this and then copy the file. I call tPlatman.GetPlatform to get the currently selected Platform (this returns an IPMPlatform interface) and tPlatform.GetDevice to get the currently selected device (which returns an IPMRemoteDevice interface).

We make the connection by calling Attach on the tRemoteDevice (IPMRemoteDevice) object. This gets me a tConnection (IPMConnection) object, which supports a number of functions, including FileCopy (the function we're interested in) and FileDelete, CreateStream (more on this later), and more. Again, refer to the Platform Builder online documentation to get more information about the IPMConnection interface.

    ' Copy the file to the device.
    Private Sub Button2_Click(ByVal sender As System.Object,
        ByVal e As System.EventArgs) Handles Button2.Click
        Dim tConnection As Object
        Dim tPlatform As Object
        Dim tRemoteDevice As Object

        tPlatform = tPlatman.GetPlatform(ListBox1.SelectedItem)

        tRemoteDevice = tPlatform.GetDevice(ListBox2.SelectedItem)
        tConnection = tRemoteDevice.Attach("cemgrc", 1000)
        tConnection.FileCopy(TextBox1.Text, "\windows\Foo.txt", 1)

    End Sub

So, that's about as hard as it gets—20 or so lines of code to connect to a remote device, and copy a file from the desktop. That's not too shabby by any standard.

That's all well and good, I hear you say—being able to attach to a device and then copy a file or application to the device (perhaps even launch the application, which is also exposed through the IPMConnection interface). Yet I may need code on my desktop and device so they can communicate. Most of the remote tools that ship with Windows CE .NET have a desktop and device-side component. The RemoteRegistry editor, for example, has a device-side component that reads/writes to the device registry (since this functionality isn't exposed directly through Platman).

If you're a curious type you're probably wondering how this works…

<Curious Type>

We already know how to make a connection to the remote device and copy/launch an application. One of the exposed functions on IPMConnection (tConnection in our Visual Basic sample) is CreateStream. This takes a GUID as its first parameter (these can be easily created by using the GUIDGEN tool, or by calling the IPMPlatformManager->CreateGuid( ) function). The device-side process also makes a call to CreateStream. This function is exposed out of CETLSTUB.DLL on the device. Note that the DLL needs to be loaded by a call to LoadLibrary( ). By coincidence, the device-side function also takes a GUID as its first parameter. If we match the host-side and device-side GUIDs, then we have in effect created a pipe that we can use to post requests from the desktop application and get replies from the device. Kinda neat, huh?

</Curious Type>

Let's take a look at the TimeViewer desktop and device-side samples. The code can be found here: C:\Program Files\Common Files\Microsoft Shared\Windows CE Tools\Platman\sdk\samples\timeviewer.

There are three interesting folders:

  • Desktop: the desktop application.
  • Device: the Windows CE device application.
  • Include: include file "tvcommon.h". This contains the common GUID between the desktop and the device.

Here's the contents of tvcommon.h, which contains the common GUID and the list of commands that can be passed to the device from the desktop application. These are CMD_GET_DATA (get the current time), and CMD_FINISHED (close the device-side application).

// TimeViewerID - {5344F94F-E122-430c-A553-62EB86D7671C}
DEFINE_GUID(
    TimeViewerID,
    0x5344f94f, 0xe122, 0x430c, 0xa5, 0x53,
     0x62, 0xeb, 0x86, 0xd7, 0x67, 0x1c
    );

typedef enum _CMD_VALUES
{
    CMD_NONE,
    CMD_GET_DATA,
    CMD_FINISHED
} CMD_VALUES;

We can use the C/C++ equivalent of the Visual Basic .NET code above to make our connection to the device and copy/launch an application (take a look at the timeviewer desktop application code sample to see how to get that far). Creating and using the stream is the interesting bit…

TimerViewer.cpp contains a variable called g_piStream. This is of type IConnectionStream. An object of type IConnectionStream can be created by calling CreateStream on the IPMConnection object (tConnection in our Visual Basic sample).

IConnectionStream exposes the following functions:

  • Send: used to post a byte array (request) down the pipe.
  • Receive: used to determine how many bytes have been posted in the reply by the device-side application.
  • ReadBytes: used to read the byte stream from the remote device into the PC-side application.
  • Close: used to close the stream.

Here's the code from the desktop application that creates the stream. (Note that we already have the tConnection [IPMConnection] object. The first parameter passed into the CreateStream function is the common GUID for the desktop and device-side applications.)

    hr = piConnection->CreateStream(TimeViewerID,
                                    0,
                                    &g_piStream,
                                    NULL);

And here's the code that makes the request to get the time, and then reads the response and the breakout of what's actually happening.

The device-side application is calling the GetSystemTime function. This fills a SYSTEMTIME structure, which is then returned to the PC-side application.

  1. The PC-side application creates a SYSTEMTIME structure called systemTime.
  2. We check g_piStream to make sure that we have a valid stream.
  3. We call g_piStream->Send( ) and write a DWORD, CMD_GET_DATA, to the device, or CMD_FINISHED if we want to close the device-side application.
  4. We call g_piStream->Receive(&dwSizeSent) to determine how many bytes have been sent by the device-side application. (Note that this should be the size of a SYSTEMTIME structure.)
  5. We call g_piStream->ReadBytes( ) to read the SYSTEMTIME structure back from the device.
  6. We display the string to the user.
//**********************************************************************
HRESULT Refresh(HWND hWnd)
//**********************************************************************
{
    DWORD dwCmd = CMD_NONE;
    DWORD dwSizeSent = 0;
    HRESULT hr = E_FAIL;
    SYSTEMTIME systemTime;


    if (!g_piStream)
    {
        MessageBox(hWnd, "Not Connected", "Refresh", MB_OK);
        return hr;
    }

    lstrcpy(g_szMessage, "Error trying to read data");

    //------------------------------------------------------------------
    // Send a command to get the device system time
    //------------------------------------------------------------------
    dwCmd = CMD_GET_DATA;
    dwSizeSent = sizeof(DWORD);
    hr = g_piStream->Send(sizeof(DWORD), (BYTE *)&dwCmd, &dwSizeSent);

    if (FAILED(hr))
    {
        char szText[80];
        wsprintf(szText, 
        "Error occurred when trying to send the get command: 0x%x", hr);
        MessageBox(hWnd, szText, "Refresh", MB_OK);
        return hr;
    }

    //------------------------------------------------------------------
    // Wait for a response
    //------------------------------------------------------------------
    dwSizeSent = sizeof(SYSTEMTIME);
    hr = g_piStream->Receive(&dwSizeSent);  // Find out how many bytes
                                            // are being sent
    if (SUCCEEDED(hr))
    {
        // Get the data from ITL
        hr = g_piStream->ReadBytes(sizeof(SYSTEMTIME),
                                   (BYTE *)&systemTime,
                                   &dwSizeSent);
        if (SUCCEEDED(hr))
        {
            // Format the time for display
            wsprintf(g_szMessage, 
            "The CE time is %2d:%2.2d:%2.2d",
            systemTime.wHour, systemTime.wMinute, systemTime.wSecond);
            ::InvalidateRect(hWnd, NULL, TRUE);
        }
    }
    else
    {
        char szText[80];
        wsprintf(szText,
        "Unable to obtain data from the device: hr = 0x%x", hr);
        ::MessageBox(hWnd, szText, "Refresh", MB_OK);
    }

    return hr;
}

That's the desktop application taken care of. Now lets take a look at the device-side (Windows CE) application.

The TimeViewer sample CE application is a standard Win32 application. Here's the WinMain (entry point) code:

int WINAPI WinMain(HINSTANCE hInstance,
                   HINSTANCE hPrev,
                   LPTSTR lpCmdLine,
                   int nCmdShow)
//**********************************************************************
{
    CConnectionStream *pStream = NULL;
    BOOL bDone = FALSE;
    DWORD dwCmd = CMD_NONE;
    DWORD dwSizeSent = 0;
    HRESULT hr = S_OK;
   HMODULE hModule = NULL;
    SYSTEMTIME systemTime;


    // Initialize ITL and establish a stream
    bDone = InitializeITL(&hModule, &pStream);
    bDone = !bDone;

    // -----------------------------------------------------------------
    // This is the main processing loop.  It waits for a command to be
    // sent from the desktop and then performs the appropriate action.
    // -----------------------------------------------------------------
    while (!bDone)
    {
       hr = pStream->ReadBytes((BYTE *)&dwCmd,
          sizeof(DWORD), &dwSizeSent);

        if (hr == ERROR_SUCCESS)
        {
            switch (dwCmd)
            {
                case CMD_GET_DATA:
                    GetSystemTime(&systemTime);
                    hr = pStream->WriteBytes((BYTE *)&systemTime,
                                  sizeof(SYSTEMTIME));
                    if (FAILED(hr))
                    {
                        bDone = TRUE;
                    }
                    break;

                case CMD_FINISHED:
                    bDone = TRUE;
                    break;

                default: // Unknown command
                    break;
            }
        }
        else
        {
            bDone = TRUE;
        }
    }

    // Cleanup and exit
    Cleanup(hModule, pStream);

    return 0;
}

This should by now look very familiar. We define a CConnectionStream pointer *pStream, we also define a SYSTEMTIME structure systemTime and call a function called InitializeITL. This is a local function that initializes the pStream interface; we will show this function later in the article.

From this point forward, it's a simple WHILE loop. We read a DWORD from the desktop application; this can be CMD_GET_DATA or CMD_FINISHED.

If we get a CMD_GET_DATA request from the desktop application, then we simply call GetSytemTime and then call pStream->WriteBytes((BYTE *)&systemTime, sizeof(SYSTEMTIME)) to write the contents of the SYSTEMTIME structure back to the desktop application.

If we get a CMD_FINISHED, then we break the loop and the application quits.

Here's the InitializeITL function from the device-side application, and here's what's happening:

  1. We declare a function pointer pfnCreateStream.
  2. We call LoadLibrary on CETLSTUB.DLL.
  3. We call GetProcAddress to get the function pointer to the CreateStream function.
  4. We call the CreateStream function and return.
//**********************************************************************
BOOL InitializeITL(HMODULE *phModule,
                   CConnectionStream **ppStream)
//**********************************************************************
{
    BOOL brc = FALSE;
    CREATESTREAMFUNC pfnCreateStream = NULL;

   // Load the Transport Library
   *phModule = LoadLibrary( L"CETLSTUB.DLL");

   if (*phModule)
    {
      pfnCreateStream = (CREATESTREAMFUNC)GetProcAddress(*phModule ,
                                               L"CreateStream");

      /*----------------------------------------------------------*/
      // Create the stream that matches up to the desktop (GUID)
      /*----------------------------------------------------------*/
      *ppStream = NULL;
       *ppStream = pfnCreateStream(TimeViewerID, 0);

        if (*ppStream)
        {
            brc = TRUE;
        }
    }

    return brc;
}

This is an extremely simple (but powerful) example of how to use the Platform Manager APIs—a few extra lines and we could be setting the time of the remote device.

The Platform Manager object model exposes more interfaces than we have time to cover in this month's article. All of the Platform Manager interfaces, though, are documented in the Platform Builder online help. Please let us know if you would like to see additional articles that cover the object model in more depth. We'd also be interested in hearing about and showcasing any remote tools that you create!

 

Get Embedded

Mike Hall is a Product Manager in the Microsoft Embedded and Appliance Platform Group (EAPG). Mike has been working with Windows CE since 1996—in developer support, Embedded System Engineering, and the Embedded product group. When not at the office, Mike can be found with his family, working on Skunk projects, or riding a Honda ST1100.

Steve Maillet is the Founder and Senior Consultant for Entelechy Consulting. Steve has provided training and has developed Windows CE solutions for clients since 1997, when CE was first introduced. Steve is a frequent contributor to the Microsoft Windows CE development newsgroups. When he's not at his computer burning up the keys, Steve can be found jumping out of airplanes at the nearest drop zone.