Automating the Download of a Run-Time Image

Posted by: Brent Bishop

So far I've shown how to automate the creation of an OS design, modify it and build it. The next step is to demonstrate how to automate the process of downloading the run-time image to a device.

Step 1: Create a new Visual Studio add-in. For this example I created a new project similar to the one described for Automating the Creation of an OS Design Project. Only this time I named the project DownloadSample and gave the addin the following name and description.

Name: Platform Builder - Download Sample
Description: Shows how to use the Platform Builder object model to download a run-time image to a device

Step 2: Add a reference to System.Windows.Forms.dll and Interop.Microsoft.PlatformBuilder.Diagnostics.dll (found here C:\Program Files\Microsoft Platform Builder\6.00\cepb\IdeVS on my computer).

Step 3: Add a new class to the project for all the code that we will be adding for the add-in. Add a new class to the project by right clicking on the project in the Solution Explorer and clicking Add -> Class. Next name the class, I named my class DownloadSample, and click Add.

Step 4: Open Connect.cs (created by Visual Studio) and find the OnConnection method. I modified this to add two menu items to the tools menu. One for attaching and the other for detaching. Note that AddNamedCommand2 has a lot of parameters, I just copied the parameters generated by Visual Studio. Check out the MSDN Library for more information on those parameters if you are interested.

    //Add a command to the Commands collection:
Command command1 = commands.AddNamedCommand2(_addInInstance, "DownloadSampleAttach", "PB - Download Sample (Attach)", "Downloads a run-time image to a device.", true, 59, ref contextGUIDS, (int)vsCommandStatus.vsCommandStatusSupported + (int)vsCommandStatus.vsCommandStatusEnabled, (int)vsCommandStyle.vsCommandStylePictAndText, vsCommandControlType.vsCommandControlTypeButton);
Command command2 = commands.AddNamedCommand2(_addInInstance, "DownloadSampleDetach", "PB - Download Sample (Detach)", "Detach from the device.", true, 59, ref contextGUIDS, (int)vsCommandStatus.vsCommandStatusSupported + (int)vsCommandStatus.vsCommandStatusEnabled, (int)vsCommandStyle.vsCommandStylePictAndText, vsCommandControlType.vsCommandControlTypeButton);

//Add a control for the command to the tools menu:
if ((command1 != null) && (toolsPopup != null))
{
command1.AddControl(toolsPopup.CommandBar, 1);
}

//Add a control for the command to the tools menu:
if ((command2 != null) && (toolsPopup != null))
{
command2.AddControl(toolsPopup.CommandBar, 2);
}

Step 5: Next scroll to the bottom to the Exec method, which is called when one of the add-in's menu items is clicked. Here we can add code to handle the two menu items. As in previous examples, add a call to two new static methods in our newly created DownloadSample class.

    public void Exec(string commandName, vsCommandExecOption executeOption, ref object varIn, ref object varOut, ref bool handled)
{
handled = false;
if(executeOption == vsCommandExecOption.vsCommandExecOptionDoDefault)
{
if (commandName == "DownloadSample.Connect.DownloadSampleAttach")
{
DownloadSample.Attach(_applicationObject);
handled = true;
return;
}
else if (commandName == "DownloadSample.Connect.DownloadSampleDetach")
{
DownloadSample.Detach(_applicationObject);
handled = true;
return;
}
}
}

Step 6: Next find QueryStatus so we can add code to enable and disable the menu items.  In this case we will disable the detach menu item when not connected and disable the attach when connected.  This code uses a static method Connected that will we need to impliment in the DownloadSample class.

    public void QueryStatus(string commandName, vsCommandStatusTextWanted neededText, ref vsCommandStatus status, ref object commandText)
{
if(neededText == vsCommandStatusTextWanted.vsCommandStatusTextWantedNone)
{
if (commandName == "DownloadSample.Connect.DownloadSampleAttach")
{
if (DownloadSample.Connected(_applicationObject))
{
status = (vsCommandStatus)vsCommandStatus.vsCommandStatusSupported;
}
else
{
status = (vsCommandStatus)vsCommandStatus.vsCommandStatusSupported | vsCommandStatus.vsCommandStatusEnabled;
}
return;
}
else if (commandName == "DownloadSample.Connect.DownloadSampleDetach")
{
if (DownloadSample.Connected(_applicationObject))
{
status = (vsCommandStatus)vsCommandStatus.vsCommandStatusSupported | vsCommandStatus.vsCommandStatusEnabled;
}
else
{
status = (vsCommandStatus)vsCommandStatus.vsCommandStatusSupported;
}
return;
}
}
}

Step 7: Add the code to attach and detach. The easiest approach would be to just call dte.ExecuteCommand(). But that wouldn't be any different than clicking on the Platform Builder menu items to attach and detach.  In order to know when the download is complete we must register a download complete event handler and download through the Platform Builder object model. The following code does that:

    // Get the debugger object model
CCeSystemDiagnostics diagnostics = (CCeSystemDiagnostics)dte.GetObject("Yamazaki-OM");
csdConnection connection = (csdConnection)diagnostics.GetConnection();

// Add an event handler
diagnostics.DebuggerConnectionNotification += new csdEvents_DebuggerConnectionNotificationEventHandler(OnDownloadComplete);

// Attach to the device
connection.Attach();

That seems simple enough, but that will only attach to the currently configured and selected device. So we need to add some code to create and configure a new device in the CoreCon data store. Note: The GUIDs included in the code below are specific to the version of Platform Builder that you are working with and these GUIDs can be found in C:\Documents and Settings\<user name>\Local Settings\Application Data\Microsoft\CoreCon\1.0\Microsoft.PlatformBuilder.ServiceCategory.xsl. One additional complexity is that Visual Studio doesn't like menu item handlers that take too long to run and has a tendency to kill them. That means that we need to move all of the code into an thread and have the menu item launch that thread.  Here's the completed class with the logic to add a Device Emulator device to the data store plus the threads and event handler:

    // Standard namespaces
using System;
using System.IO;
using System.Windows.Forms;

// PlatformBuilder and Visual Studio namespaces
using EnvDTE;
using EnvDTE80;
using Interop.Microsoft.PlatformBuilder.Diagnostics;

/// <summary>
/// A sample that shows how to use the Platform Builder object model to download a run-time image to a device.
/// </summary>
internal static class DownloadSample
{
/// <summary>
/// The caption to use in message boxes.
/// </summary>
private const string Caption = "PB - Download Sample";

/// <summary>
/// The guid of the device to attach to.
/// </summary>
private static string device;

/// <summary>
/// A value indicating whether Platform Builder is done downloading.
/// </summary>
private static bool done = false;

/// <summary>
/// A value indicating whether Platform Builder is done downloading.
/// </summary>
private static DTE2 dte;

/// <summary>
/// A value indicating whether Platform Builder connected successfully.
/// </summary>
private static bool success = false;

#region Configuration GUIDs
/// <summary>
/// Ethernet Download
/// </summary>
static string DownloadEthernet = "83EE7228-08BF-4ECD-A54E-CB172C5BAC83";

/// <summary>
/// Serial Download
/// </summary>
static string DownloadSerial = "9A2D20B9-161E-46A4-8F1E-227FE00E575F";

/// <summary>
/// Device Emulator DMA Download
/// </summary>
static string DownloadDeviceEmulatorDMA = "15630029-F9F4-4765-AE8F-469AA3EC75B4";

/// <summary>
/// Image Update Download
/// </summary>
static string DownloadImageUpdate = "35AFF35B-5F24-437B-960C-52C02406C089";

/// <summary>
/// No Download
/// </summary>
static string DownloadNone = "00000000-0000-0000-0000-000000000000";

/// <summary>
/// Ethernet Kernel Transport
/// </summary>
static string TransportEthernet = "4646D3DD-6B46-4017-9714-177F68A60A15";

/// <summary>
/// Serial Kernel Transport
/// </summary>
static string TransportSerial = "B686C04E-E313-4848-B1A7-E507AFA3DF42";

/// <summary>
/// Device Emulator DMA Kernel Transport
/// </summary>
static string TransportDeviceEmulatorDMA = "4492D487-DD61-474f-9FF7-572503ED59AE";

/// <summary>
/// USB Kernel Transport
/// </summary>
static string TransportUSB = "61B58DD8-ACE7-48FE-8CA6-C0C43E4D57C6";

/// <summary>
/// No Kernel Transport
/// </summary>
static string TransportNone = "00000000-0000-0000-0000-000000000000";

/// <summary>
/// KDStub Debugger Service
/// </summary>
static string DebuggerKDStub = "999059D7-9896-47FD-959F-276E22998CB2";

/// <summary>
/// No Debugger Service
/// </summary>
static string DebuggerNone = "00000000-0000-0000-0000-000000000000";
#endregion

/// <summary>
/// Downloads a run-time image to a device.
/// </summary>
/// <param name="dte">A reference to the DTE for controlling Visual Studio.</param>
public static void Attach(DTE2 vsdte)
{
// Save the reference to the DTE
dte = vsdte;

// Start the thread
System.Threading.Thread thread = new System.Threading.Thread(AttachThread);
thread.Start();
}

/// <summary>
/// Downloads a run-time image to a device.
/// </summary>
public static void AttachThread()
{
try
{
// Initialize the variables
done = false;
success = false;

// Get the debugger object model
CCeSystemDiagnostics diagnostics = (CCeSystemDiagnostics)dte.GetObject("Yamazaki-OM");
csdConnection connection = (csdConnection)diagnostics.GetConnection();

// Add an event handler
diagnostics.DebuggerConnectionNotification += new csdEvents_DebuggerConnectionNotificationEventHandler(OnDownloadComplete);

// Add a device
string name = "Automation";
string platform = null;
device = connection.AddDevice(ref name, ref platform);

// Configure the device
connection.SetKernelBootStrap(ref device, ref DownloadDeviceEmulatorDMA);
connection.SetKernelTransport(ref device, ref TransportDeviceEmulatorDMA);
connection.SetKernelDebugger(ref device, ref DebuggerKDStub);

// Set the current device
connection.SetCurrentDevice(ref device);

// Make sure the current device is set before attaching
System.Threading.Thread.Sleep(2000);

// Attach to the device
connection.Attach();

// TO DO:
// - Add code to reset the physical device.
// - Or use this message box as a reminder to manual reset the device.
// MessageBox.Show("Reset your device now.", Caption, MessageBoxButtons.OK, MessageBoxIcon.Information);

// Activate the debugger output window pane
foreach (OutputWindowPane pane in dte.ToolWindows.OutputWindow.OutputWindowPanes)
{
if (pane.Name == "Windows CE Debug")
{
pane.Activate();
break;
}
}

// Wait for the download to complete
for (int i = 0; i < 60 && !done; i++)
{
System.Threading.Thread.Sleep(2000);
if (FindOutput(dte, "Booting kernel"))
{
done = true;
success = true;
dte.ToolWindows.OutputWindow.ActivePane.OutputString("FAILED GET THE DEBUGGER CALLBACK -- CONTINUING ANYWAY!!\r\n");
break;
}
}

// Make sure it was successful
if (success)
{
MessageBox.Show("Successfully downloaded the run-time image.", Caption, MessageBoxButtons.OK);
}
else
{
MessageBox.Show("ERROR: Failed to download the run-time image.", Caption, MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
catch (Exception e)
{
// Show the error message (NOTE: capturing all exceptions for debugging because Visual Studio will eat all unhandled exceptions thrown by an add-in)
MessageBox.Show(e.Message, Caption, MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}

/// <summary>
/// See if Platform Builder is connected to a device.
/// </summary>
/// <param name="dte">A reference to the DTE for controlling Visual Studio.</param>
public static bool Connected(DTE2 dte)
{
try
{
// Get the debugger object model
CCeSystemDiagnostics diagnostics = (CCeSystemDiagnostics)dte.GetObject("Yamazaki-OM");
csdConnection connection = (csdConnection)diagnostics.GetConnection();

// See if the device is attached
return connection.IsDeviceAttached;
}
catch (Exception e)
{
// Show the error message (NOTE: capturing all exceptions for debugging because Visual Studio will eat all unhandled exceptions thrown by an add-in)
MessageBox.Show(e.Message, Caption, MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
}

/// <summary>
/// Detach from the device.
/// </summary>
/// <param name="dte">A reference to the DTE for controlling Visual Studio.</param>
public static void Detach(DTE2 vsdte)
{
// Save the reference to the DTE
dte = vsdte;

// Start the thread
System.Threading.Thread thread = new System.Threading.Thread(DetachThread);
thread.Start();
}

/// <summary>
/// Detach from the device.
/// </summary>
public static void DetachThread()
{
try
{
// Get the debugger object model
CCeSystemDiagnostics diagnostics = (CCeSystemDiagnostics)dte.GetObject("Yamazaki-OM");
csdConnection connection = (csdConnection)diagnostics.GetConnection();

// Detach the device
connection.Detach();

// Make sure it's disconnected before deleting the device
System.Threading.Thread.Sleep(2000);

// Remove the device configuration
connection.DeleteDevice(ref device);
}
catch (Exception e)
{
// Show the error message (NOTE: capturing all exceptions for debugging because Visual Studio will eat all unhandled exceptions thrown by an add-in)
MessageBox.Show(e.Message, Caption, MessageBoxButtons.OK, MessageBoxIcon.Error);
}

// Kill the Device Emulator
System.Diagnostics.Process[] processes2 = System.Diagnostics.Process.GetProcessesByName("DeviceEmulator");
foreach (System.Diagnostics.Process process in processes2)
{
process.Kill();
}
}

/// <summary>
/// Find the text in the output window.
/// </summary>
/// <param name="dte">A reference to the DTE for controlling Visual Studio.</param>
/// <param name="text">The string to search for in the output window.</param>
/// <returns><c>true</c> if the text was found in the output window; otherwise, <c>false</c>.</returns>
public static bool FindOutput(DTE2 dte, string text)
{
// Variables
TextDocument document = dte.ToolWindows.OutputWindow.ActivePane.TextDocument;
EditPoint edit = document.StartPoint.CreateEditPoint();
EditPoint end = document.EndPoint.CreateEditPoint();
TextRanges ranges = null;

// Search for the text
return edit.FindPattern(text, (int)vsFindOptions.vsFindOptionsNone, ref end, ref ranges);
}

/// <summary>
/// The method that handles the download complete event.
/// </summary>
/// <param name="connected">A value indicating whether Platform Builder is connected.</param>
private static void OnDownloadComplete(bool connected)
{
done = true;
success = connected;
}
}

Step 8: Wait for the device to boot. What we have so far is good enough to get the image downloaded to a device. The problem is that we can't do anything useful with it until we know that the device has finished booting up. Unfortunately there is no event or call back that can tell us when the device is finished booting. So how can we detect that the device is finished booting? The simplest approach is to look at the output in the output window. If no new text has been written to the output window in a given amount of time then there is a high probability that the device is booted. So I added a methods to the class to do just that.

    /// <summary>
/// This retrieves the current text in the active pane of the output window.
/// </summary>
/// <param name="dte">A reference to the DTE for controlling Visual Studio.</param>
/// <returns>The contents of the output window.</returns>
public static string GetCurrentText(DTE2 dte)
{
// Variables
TextDocument document = dte.ToolWindows.OutputWindow.ActivePane.TextDocument;
EditPoint edit = document.StartPoint.CreateEditPoint();

// Get the current text in the debug output window
return edit.GetText(document.EndPoint);
}

/// <summary>
/// Wait till the Image has completely booted up.
/// </summary>
/// <param name="timeout">The number of minutes to wait for the device to finish booting.</param>
/// <param name="foo">The number of second to wait between checks of the output window.</param>
/// <returns><c>true</c> if the device successfully booted; otherwise, <c>false</c>.</returns>
public static bool WaitTillImageIsUpAndRunning(int timeout, int idletime)
{
// Get the current text in the active pane
string oldText = GetCurrentText(dte);
string newText = null;

// Calculate how many times to try
int attempts = timeout * 60 / idletime;

// Check to see if the output window text has changed
for (int i = 0; i < attempts; i++)
{
// Wait
System.Threading.Thread.Sleep(idletime * 1000);

// Get the latest text from Active Pane
newText = GetCurrentText(dte);

// If we have no new text, it means device has booted up completely
if (oldText == newText)
{
return true;
}

// Save the latest text
oldText = newText;
newText = null;
}

// Timed out
return false;
}

I also replaced the last block of code in the AttachThread method

    // Make sure it was successful
if (success)
{
MessageBox.Show("Successfully downloaded the run-time image.", Caption, MessageBoxButtons.OK);
}
else
{
MessageBox.Show("ERROR: Failed to download the run-time image.", Caption, MessageBoxButtons.OK, MessageBoxIcon.Error);
}

with the following so that it now waits for the device to be booted.

    // Wait for the device to finish booting
if (success)
{
if (WaitTillImageIsUpAndRunning(10, 20))
{
MessageBox.Show("Successfully booted the run-time image.", Caption, MessageBoxButtons.OK);
}
else
{
MessageBox.Show("ERROR: The device did not boot in 10 minutes.", Caption, MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}

This example is now ready to be extended to interface with the device through the debugger object model or the target control window. In future posts we will explore some of these possibilities.