If you’re building an IoT (Internet of Things)-based gadget, one thing you might want to know about is when USB devices are attached to or removed from your platform.
Specifically, as we’ll see in this post, you might be interested to know when a USB Thumb drive or other USB-based storage media is plugged in, or removed.
To achieve this, there are a few different techniques you can use. If, for example, you want to get the USB device’s GUID and Vendor & Product IDs, you’ll want to use the WMI (Windows Management Interface) to do this. Typically, WMI is quite complicated and very, very code heavy, so we’ll leave this one for a future post.
In this post, we’re going to look at a slightly easier method of performing this task by using the standard Windows message pump.
Because we’re going to be overriding the message pump, this means you can’t do this in a console application (there are ways, however), so we’ll be creating a Windows Forms program to demonstrate the technique. I should imagine that it’s also possible to do this in WPF too, but I don’t know how to override the message pump in a WPF app. (If you do, feel free to leave a note in the comments.)
Let’s Write Some Code….
Start up Visual Studio and create a standard Windows Forms application.
Press F7 to go into the code editor, and add the following to your Form1 class:
protected override void WndProc(ref Message m) { base.WndProc(ref m); }
By doing this, you’re overriding the normal “Windows Proc” that handles all the messages for your form. You MUST put the base call in first; if you don’t, none of the regular Windows messages will be acted upon, essentially freezing your form.
Once you have this in place, press F5 run your app and make sure the blank form behaves as normal. When you’re satisfied that it does, stop the app and add a list box to the form as the Figure 1 shows:
Figure 1: Our form with ‘listBox1’ on it
Switch back to code view for your form.
The next thing we need to do is to handle a custom message that gets sent to our application when a device is added or removed. This message is called “WM_DEVICECHANGED” and has the value 0x219 (in hexadecimal).
Alter our overridden message handler, so that it now looks like this:
protected override void WndProc(ref Message m) { base.WndProc(ref m); switch(m.Msg) { case WM_DEVICECHANGE: switch((int)m.WParam) { } break; } }
This now allows us to have a point in our handler when every time we receive one of these events, our code will be called.
In C#, you’ll also need to add a
private const int WM_DEVICECHANGE = 0x219;
somewhere near the top of the class, most likely just after the opening ‘public partial class Form1’ line.
Once we know we have a WM_DEVICECHANGE event, we then need to examine the m.WParam value because this will tell us the actual device action that has happened. If you place a break point on this code, you’ll find that you get way more than one notification for addition and one for removal as you might expect. In fact, you’ll get a lot of events with the value 7 in WParam, which you can safely ignore.
The only two message codes you need to be concerned with are “DBT_DEVICEARRIVAL” and “DBT_DEVICEREMOVECOMPLETE”. Just underneath the “WM_DEVICECHANGE” const you defined a moment ago, add the following three constants:
private const int DBT_DEVICEARRIVAL = 0x8000; private const int DBT_DEVICEREMOVECOMPLETE = 0x8004; private const int DBT_DEVTYP_VOLUME = 0x00000002;
I’ll explain what the third one is for in just a moment.
Inside the “WParam” switch, add the following:
case DBT_DEVICEARRIVAL: listBox1.Items.Add("New Device Arrived"); break; case DBT_DEVICEREMOVECOMPLETE: listBox1.Items.Add("Device Removed"); break;
If you run your application now, and add/remove a thumb drive or other removable media to your PC, you should see messages telling you what’s happening appearing on your form.
Figure 2: Our app now knows when a device is plugged in and removed.
At this point, if all you wanted to do was know something had been plugged in, that would be great. Our application, as it stands, will tell you about every device addition and removal, no matter what type of device it is.
If we want to know only about storage media, we need to do a further check for “DBT_DEVTYPE_VOLUME” which is the second parameter of the “DEV_BROADCAST_HDR” structure, pointed to by ‘m.LParam’ in your message handler.
You can think of “DEV_BROADCAST_HDR” as being a super class to a number of different structures, one of which is “DBT_DEVTYP_VOLUME”; this is the one we are interested in.
To check the type of the structure, the second 32-bit integer in the memory pointed to by LParam will be a value indicating which of the five types (all of which are documented here) the DBT_DEVTYPE_VOLUME value of 2 we defined a moment ago, is the one we’re interested in finding.
Once we know we have a value of 2, we then need to copy the pointer contents into a C# class that mirrors the native Windows structure, so we can read the values and act upon them.
Add a new class to your project and call it “DevBroadcastVolume.cs”; then, make sure it has the following code in it:
using System; using System.Runtime.InteropServices; namespace UsbDriveDetector { [StructLayout(LayoutKind.Sequential)] public struct DevBroadcastVolume { public int Size; public int DeviceType; public int Reserved; public int Mask; public Int16 Flags; } }
Switch back to the code you have in Form1, and in your “DBT_DEVICEARRIVAL” case, add the following just after the listBox1 item add call:
int devType = Marshal.ReadInt32(m.LParam, 4); if(devType == DBT_DEVTYP_VOLUME) { DevBroadcastVolume vol; vol = (DevBroadcastVolume) Marshal.PtrToStructure(m.LParam, typeof (DevBroadcastVolume)); listBox1.Items.Add("Mask is " + vol.Mask); }
If you run your app now, and watch the form, you’ll see an extra line has been added; it contains the device mask:
Figure 3: Our app now displays the inserted device mask, but ONLY if it’s a storage volume.
In Figure 3, you can see the mask value is 131072, which if you convert to binary, gives you the following value:
00000000000000100000000000000000
Starting at the left, and giving each column of the number a single letter of the alphabet, you should get
ZYXWVUTSRQPONMLKJIHGFEDCBA 00000000000000100000000000000000
You’ll see that the ‘1’ appears under the ‘R’ column, and if I look in my Explorer view:
Figure 4: Explorer view showing inserted drive
You’ll see the key chain drive I plugged in was mounted as Drive R.
For reference, the full code for your Form1 class should look something like the following:
using System.Runtime.InteropServices; using System.Windows.Forms; namespace UsbDriveDetector { public partial class Form1 : Form { private const int WM_DEVICECHANGE = 0x219; private const int DBT_DEVICEARRIVAL = 0x8000; private const int DBT_DEVICEREMOVECOMPLETE = 0x8004; private const int DBT_DEVTYP_VOLUME = 0x00000002; public Form1() { InitializeComponent(); } protected override void WndProc(ref Message m) { base.WndProc(ref m); switch(m.Msg) { case WM_DEVICECHANGE: switch((int)m.WParam) { case DBT_DEVICEARRIVAL: listBox1.Items.Add("New Device Arrived"); int devType = Marshal.ReadInt32(m.LParam, 4); if(devType == DBT_DEVTYP_VOLUME) { DevBroadcastVolume vol; vol = (DevBroadcastVolume) Marshal.PtrToStructure(m.LParam, typeof (DevBroadcastVolume)); listBox1.Items.Add("Mask is " + vol.Mask); } break; case DBT_DEVICEREMOVECOMPLETE: listBox1.Items.Add("Device Removed"); break; } break; } } } }
And that’s all there is to this. What you do with the event data is entirely up to you, and if you look on MSDN at the different structures, you’ll see you easily can get the same information for things like COM ports, modems, and other USB-based communication devices.
Got a tricky .NET problem that’s nagging you? Drop me a note in the comments below, or come hunt me down on Twitter as @shawty_ds and I’ll see if I can write it up in a post.