MAC, PC, USB,
and User Frendliness

Preface

This story begins in August, 2010, in Budva, Montenegro. I was consuming some alcoholic beverages in a place called Casper DJ Bar. I've encountered a guy (don't remember the name), who's a musician, and a Mac fanboy (BTW that's common amongs musicians, for some good reasons I'm not going to discuss in this article).

We had a tiny Mac-vs-PC holywar; "tiny" because it was severily eased by the climate and alcohol. Among other things, he told me something like "when I use MAC with an extarnal audio interface, if I happen to occasionally disconnect the USB, I only have to connect it back; while on Windows, I have to restart my whole DAW software."

Of course I replied it's not the Windows' fault, but the fault of the creators of the DAW software.

That time I've been working on a software that uses USB-attached modems.

I've recalled the discussion with that musician, and tried to do my best to make my software work reliably even while the USB modem is being re-plugged into my customer's PC.

WM_DEVICECHANGE message

This windows message notifies an application of a change to the hardware configuration of a device or the computer. This message comes to the main window of your application.

Be warned however, that when you plug a composite USB device, you'll get several WM_DEVICECHANGE notifications. Moreover, you'll get several WM_DEVICECHANGE notifications for the same device during the different stages of plug & play process. That's why I've implemented the following logic:

  1. On WM_DEVICECHANGE message, I start a 2 seconds timer.
  2. If a subsequent WM_DEVICECHANGE message is received before the timer is elapsed, the timer end duration is reset so it'll elapse 2 seconds after the currently received message.
  3. When the timer is elapsed, I use WMI to query the system devices.
  4. If it happens too early (i.e. no drivers are installed yet), no big deal: the WMI will find no new suitable devices, however as soon as the driver installation will be finished, the application will receive one more WM_DEVICECHANGE message and will re-query the WMI.

The WMI Part

Here's some C# routines to search for USB devices:

static IEnumerable<ManagementObject> QueryWmi( string q )
{
      using( ManagementObjectSearcher mos = new ManagementObjectSearcher( q ) )
      using( ManagementObjectCollection moc = mos.Get() )
            foreach( ManagementObject mo in moc )
                  yield return mo;
}

static IEnumerable<ManagementObject> GetUsbDevices()
{
      return QueryWmi( @"select Dependent from Win32_USBControllerDevice" )
            .Select( mo => (string)mo[ "Dependent" ] )
            .Select( dep => new ManagementObject( dep ) );
}

I was only interested in the GSM modems: I didn't care about mice, keyboards, gaming controllers, printers, or any other USB peripherals you might have attached to your PC. Here's how I've filtered devices by the device class:

static readonly HashSet<Guid> s_hsUsefulDeviceClasses = new HashSet<Guid>()
{
      PInvoke.GUID_DEVCLASS.GUID_DEVCLASS_MODEM,
      PInvoke.GUID_DEVCLASS.GUID_DEVCLASS_USB,
      PInvoke.GUID_DEVCLASS.GUID_DEVCLASS_PORTS,
      new Guid("4f919108-4adf-11d5-882d-00b0d02fe381") // Wireless Communication Devices
};

static Guid getDeviceClassGuid( this ManagementObject mo )
{
      object cg = mo[ "ClassGuid" ];
      if( null == cg || !( cg is string ) )
            return Guid.Empty;
      return new Guid( (string)cg );
}

/// <summary>Filter out USB devices of wrong device classes.</summary>
///
<param name="mo"></param>
///
<returns></returns>
static bool FilterOutWrongDevices( ManagementObject mo )
{
      Guid DevClass = mo.getDeviceClassGuid();
      if( !s_hsUsefulDeviceClasses.Contains( DevClass ) )
            return false;
      return true;
}

See this link for the definition of class GUID_DEVCLASS.

Then I used the exact vendor and product IDs to look for the specific models of GSM modems supported by my software:

/// <summary></summary>
///
<param name="mo"></param>
///
<param name="strId">e.g. pass @"USB\VID_12D1&PID_1001" to look for Huawei E1550</param>
///
<returns></returns>
static bool checkPnpId( this ManagementObject mo, string strId )
{
      string strDevId = mo[ "PNPDeviceID" ] as string;
      if( String.IsNullOrEmpty( strDevId ) )
            return false;

      strId = strId.ToUpperInvariant();
      strDevId = strDevId.ToUpperInvariant();

      if( !strDevId.StartsWith( strId ) )
            return false;
      return true;
}

And after you've detected the device you was looking for, don't forget it might have changed severely. Re-initialize your stuff that works with the device. SMS control center software queries the newly discovered modems for the IMSI of the installed SIM card, then uses IMSI value to bind the device to identity.

Final Words

I'm not saying this is the right way to implement the functionality I wanted; what I'd like to say is "it works". My users are able to re-plug the GSM modems (or the whole USB hub hosting several modems) any time, and my software instantly detects it, and starts to use.

January, 2011.