48

I'd like to know if there's a way to trigger an event when a device is added or removed from the system. I want to be able to detect if say, a USB flash drive has been added, or a mouse, or whatever else. I tried searching around, but I can't find anything that say's how to do this.

Any ideas?

Andi Jay
  • 5,882
  • 13
  • 51
  • 61
  • I made a NuGet packet that works on Windows, MacOS and Linux: https://github.com/Jinjinov/Usb.Events – Jinjinov Apr 28 '20 at 06:28

6 Answers6

75

If you have a window in your application, you can use something like this:

using System;
using System.Runtime.InteropServices;

internal static class UsbNotification
{
    public const int DbtDevicearrival = 0x8000; // system detected a new device        
    public const int DbtDeviceremovecomplete = 0x8004; // device is gone      
    public const int WmDevicechange = 0x0219; // device change event      
    private const int DbtDevtypDeviceinterface = 5;
    private static readonly Guid GuidDevinterfaceUSBDevice = new Guid("A5DCBF10-6530-11D2-901F-00C04FB951ED"); // USB devices
    private static IntPtr notificationHandle;

    /// <summary>
    /// Registers a window to receive notifications when USB devices are plugged or unplugged.
    /// </summary>
    /// <param name="windowHandle">Handle to the window receiving notifications.</param>
    public static void RegisterUsbDeviceNotification(IntPtr windowHandle)
    {
        DevBroadcastDeviceinterface dbi = new DevBroadcastDeviceinterface
        {
            DeviceType = DbtDevtypDeviceinterface,
            Reserved = 0,
            ClassGuid = GuidDevinterfaceUSBDevice,
            Name = 0
        };

        dbi.Size = Marshal.SizeOf(dbi);
        IntPtr buffer = Marshal.AllocHGlobal(dbi.Size);
        Marshal.StructureToPtr(dbi, buffer, true);

        notificationHandle = RegisterDeviceNotification(windowHandle, buffer, 0);
    }

    /// <summary>
    /// Unregisters the window for USB device notifications
    /// </summary>
    public static void UnregisterUsbDeviceNotification()
    {
        UnregisterDeviceNotification(notificationHandle);
    }

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr RegisterDeviceNotification(IntPtr recipient, IntPtr notificationFilter, int flags);

    [DllImport("user32.dll")]
    private static extern bool UnregisterDeviceNotification(IntPtr handle);

    [StructLayout(LayoutKind.Sequential)]
    private struct DevBroadcastDeviceinterface
    {
        internal int Size;
        internal int DeviceType;
        internal int Reserved;
        internal Guid ClassGuid;
        internal short Name;
    }
}

Here's how you use it from a WPF Window (Windows Forms is similar):

    protected override void OnSourceInitialized(EventArgs e)
    {
        base.OnSourceInitialized(e);

        // Adds the windows message processing hook and registers USB device add/removal notification.
        HwndSource source = HwndSource.FromHwnd(new WindowInteropHelper(this).Handle);
        if (source != null)
        {
            windowHandle = source.Handle;
            source.AddHook(HwndHandler);
            UsbNotification.RegisterUsbDeviceNotification(windowHandle);
        }
    }

    /// <summary>
    /// Method that receives window messages.
    /// </summary>
    private IntPtr HwndHandler(IntPtr hwnd, int msg, IntPtr wparam, IntPtr lparam, ref bool handled)
    {
        if (msg == UsbNotification.WmDevicechange)
        {
            switch ((int)wparam)
            {
                case UsbNotification.DbtDeviceremovecomplete:
                    Usb_DeviceRemoved(); // this is where you do your magic
                    break;
                case UsbNotification.DbtDevicearrival:
                    Usb_DeviceAdded(); // this is where you do your magic
                    break;
            }
        }

        handled = false;
        return IntPtr.Zero;
    }

Here's the use example for Windows Forms (even simpler):

public Form1()
{
    InitializeComponent();
    UsbNotification.RegisterUsbDeviceNotification(this.Handle);
}

protected override void WndProc(ref Message m)
{
    base.WndProc(ref m);
        if (m.Msg == UsbNotification.WmDevicechange)
    {
        switch ((int)m.WParam)
        {
            case UsbNotification.DbtDeviceremovecomplete:
                Usb_DeviceRemoved(); // this is where you do your magic
                break;
            case UsbNotification.DbtDevicearrival:
                Usb_DeviceAdded(); // this is where you do your magic
                break;
        }
    }
}   
Darko Kenda
  • 4,781
  • 1
  • 28
  • 31
  • 4
    If you do not have a window, you need to create one. A [message-only window](http://msdn.microsoft.com/en-us/library/windows/desktop/ms632599.aspx#message_only) works perfectly well. You can use the [`NativeWindow` class](http://msdn.microsoft.com/en-us/library/system.windows.forms.nativewindow.aspx) for that in WinForms. – Cody Gray - on strike Apr 26 '13 at 22:40
  • I do have a window. However, I'm a little confused by the second part of the code you have. OnSourceInitialized gives me an error that there is no object defined. Also, WindowInteropHelper say's there's no using directive. However, "using System.Windows.Interop" gives me an error. Last, HwndSource seems to "not exist under the current context." – Andi Jay Apr 29 '13 at 18:42
  • FYI, I'm creating my windows form like this "mainForm = new MainForm();" – Andi Jay Apr 29 '13 at 18:53
  • I added an example for WinForms – Darko Kenda Apr 29 '13 at 21:11
  • 3
    WOAH!! that works. I'm going ot mark this as the answer. Now that I have something that works, it's much easier to dig in and understand how it all works. :) I do have a question though. Is there a way to get some information about the device that was added? Like a product ID or vendor ID? – Andi Jay Apr 30 '13 at 14:26
  • Also, is there any documentation that might help me understand some of this? – Andi Jay Apr 30 '13 at 15:09
  • Actually, rather than the PID and VID of the device, just a string description would be nice – Andi Jay Apr 30 '13 at 16:23
  • My last comment I have is that the friendly ID is never retrieved. It always uses the DeviceDesc property in the registry. – Andi Jay May 20 '13 at 20:04
  • @AndiJay you will probably need libUsbDotNet to get information about a usb device – Ali.Rashidi Apr 30 '14 at 20:04
  • Anyway to filter what device type has been plugged in? For example can I just detect removable media? – Martyn Ball Jul 23 '14 at 09:58
  • @DarkoKenda Hate bringing up old questions, but will a BT device trigger this as well? My app is set to interface with a device that can be connected either BT or USB. – Wobbles Aug 02 '14 at 15:54
  • 4
    how can I handle device change on windows service? please can you explain. – Hubeyb Özkul Jan 01 '15 at 23:57
  • 3
    this code works but I get the callback twice... how can I avoid this? – ΦXocę 웃 Пepeúpa ツ Sep 21 '16 at 07:59
  • 3
    Question about 'IntPtr buffer', should we call Marshal.FreeHGlobal(buffer) after we called RegisterDeviceNotification()? Would there be a memory leak with 'buffer' if not released? – Santiago Villafuerte Jan 29 '17 at 17:09
  • @CodyGray I tried message-only window but I didn't get `WM_DEVICECHANGE` message. It was stated in documentation that message-only windows doesn't receive broadcast message. Does it concern to this? @ΦXocę웃Пepeúpaツ I have the same problem too. – witoong623 Mar 03 '17 at 01:35
  • I always get an DBT_DEVNODES_CHANGED (0x7) message, anyone know why? Never get device arrival or removecomplete – Renato Pereira Feb 15 '18 at 21:55
  • 1
    Great answer, but I wish it was using native constant names instead of C# style names – AaA Apr 09 '18 at 11:02
  • 3
    @ΦXocę웃Пepeúpaツ I know this is years late but could be helpful to others as well: If you get the event twice it is likely because you didn't need to use RegisterDeviceNotification() -- see Microsoft documentation for said method, "Remarks" section. – DatuPuti May 17 '19 at 13:58
  • @AaA I agree with you. My answer below is even worse as I'm just using number literals. I'm new to C# -- is there an easy way to access Win32 / WinAPI constants in C# without rewriting them all from scratch as is shown here: https://social.msdn.microsoft.com/Forums/vstudio/en-US/a6ce27b8-374f-4d0a-87a8-3fe01c833c20/using-winapi-in-c?forum=csharpgeneral – DatuPuti May 17 '19 at 14:17
  • @DatuPuti As far as I know, it is not possible. I always copied them from Win.h to my C# code – AaA May 21 '19 at 01:48
  • How to differentiate between COM port disconnect and USB disconnect. – Sid133 Jan 11 '22 at 08:47
  • @Sid133, you mean the actual COM port or a device connected to COM port? If the latter, you can not detect it automatically, as COM port is not plug&play. You have to poll the device manually. – Darko Kenda Mar 01 '22 at 12:01
  • @DarkoKenda a device connected to COM, a microcontroller like Aurdino – Sid133 Mar 02 '22 at 07:04
15

The accepted answer is excellent, however it only works with USB devices.

To make it work with all devices (and optionally filter USB), use the following slightly modified class:

static class DeviceNotification {
    //https://msdn.microsoft.com/en-us/library/aa363480(v=vs.85).aspx
    public const int DbtDeviceArrival = 0x8000; // system detected a new device        
    public const int DbtDeviceRemoveComplete = 0x8004; // device is gone     
    public const int DbtDevNodesChanged = 0x0007; //A device has been added to or removed from the system.

    public const int WmDevicechange = 0x0219; // device change event      
    private const int DbtDevtypDeviceinterface = 5;
    //https://msdn.microsoft.com/en-us/library/aa363431(v=vs.85).aspx
    private const int DEVICE_NOTIFY_ALL_INTERFACE_CLASSES = 4;
    private static readonly Guid GuidDevinterfaceUSBDevice = new Guid("A5DCBF10-6530-11D2-901F-00C04FB951ED"); // USB devices
    private static IntPtr notificationHandle;

    /// <summary>
    /// Registers a window to receive notifications when devices are plugged or unplugged.
    /// </summary>
    /// <param name="windowHandle">Handle to the window receiving notifications.</param>
    /// <param name="usbOnly">true to filter to USB devices only, false to be notified for all devices.</param>
    public static void RegisterDeviceNotification(IntPtr windowHandle, bool usbOnly = false) {
        var dbi = new DevBroadcastDeviceinterface {
            DeviceType = DbtDevtypDeviceinterface,
            Reserved = 0,
            ClassGuid = GuidDevinterfaceUSBDevice,
            Name = 0
        };

        dbi.Size = Marshal.SizeOf(dbi);
        IntPtr buffer = Marshal.AllocHGlobal(dbi.Size);
        Marshal.StructureToPtr(dbi, buffer, true);

        notificationHandle = RegisterDeviceNotification(windowHandle, buffer, usbOnly ? 0 : DEVICE_NOTIFY_ALL_INTERFACE_CLASSES);
    }

    /// <summary>
    /// Unregisters the window for device notifications
    /// </summary>
    public static void UnregisterDeviceNotification() {
        UnregisterDeviceNotification(notificationHandle);
    }

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr RegisterDeviceNotification(IntPtr recipient, IntPtr notificationFilter, int flags);

    [DllImport("user32.dll")]
    private static extern bool UnregisterDeviceNotification(IntPtr handle);

    [StructLayout(LayoutKind.Sequential)]
    private struct DevBroadcastDeviceinterface {
        internal int Size;
        internal int DeviceType;
        internal int Reserved;
        internal Guid ClassGuid;
        internal short Name;
    }
}

The key change is the Flags parameter when calling RegisterDeviceNotification (see https://msdn.microsoft.com/en-us/library/aa363431(v=vs.85).aspx), which if set to 4 instead of 0 will ignore the ClassGuid parameter and register for all devices.

Erwin Mayer
  • 18,076
  • 9
  • 88
  • 126
  • 1
    I've just used this code as a basis for detected an audio device being removed. However I've noticed that the handler is called for every device in the class of the device removed. So I have 11 audio devices, I unplug one device and that causes this to be invoked 11 times. Is that an expected behaviour? – Jammer Sep 23 '17 at 16:03
  • @Jammer, this might happen because all devices are in same category and there is a use priority for audio devices. if one removed all have to refresh to get new sequence. – AaA Apr 09 '18 at 11:22
3

Here is better version, because it can get port name. In case of DBT_DEVTYP_PORT device type, lParam points to DEV_BROADCAST_PORT, that contains DEV_BROADCAST_HDR and then name in Unicode of the device being removed or added ending with zeroes.

    protected override void WndProc(ref Message m)
    {
        switch ((WndMessage) m.Msg)
        {
            case WndMessage.WM_DEVICECHANGE:
                DEV_BROADCAST_HDR dbh;

                switch ((WM_DEVICECHANGE) m.WParam)
                {
                    case WM_DEVICECHANGE.DBT_DEVICEARRIVAL:
                    case WM_DEVICECHANGE.DBT_DEVICEREMOVECOMPLETE:
                        dbh = (DEV_BROADCAST_HDR) Marshal.PtrToStructure(m.LParam, typeof(DEV_BROADCAST_HDR));
                        if ((WM_DEVICECHANGE) dbh.dbch_devicetype == WM_DEVICECHANGE.DBT_DEVTYP_PORT)
                        {
                            var portNameBytes = new byte[dbh.dbch_size - (int) WM_DEVICECHANGE.SIZE_OF_DBH];
                            Marshal.Copy(m.LParam + (int) WM_DEVICECHANGE.SIZE_OF_DBH, portNameBytes, 0, portNameBytes.Length);
                            string portName = Encoding.Unicode.GetString(portNameBytes).TrimEnd('\0');
                            if (portName == Settings.Instance.PortName)
                            {
                                if ((WM_DEVICECHANGE) m.WParam == WM_DEVICECHANGE.DBT_DEVICEREMOVECOMPLETE)
                                {
                                    if (!_port.IsOpen)
                                    {
                                        ClosePort();
                                    }
                                }
                                else
                                {
                                    BeginInvoke((Action) (() => OpenPort()));
                                }
                            }
                        }
                        break;
                }
                break;
        }

        base.WndProc(ref m);
    }

public enum WndMessage
{
    WM_DEVICECHANGE = 0x0219, // device change event   
}

public enum WM_DEVICECHANGE
{
    // full list: https://learn.microsoft.com/en-us/windows/win32/devio/wm-devicechange
    DBT_DEVICEARRIVAL = 0x8000,             // A device or piece of media has been inserted and is now available.
    DBT_DEVICEREMOVECOMPLETE = 0x8004,      // A device or piece of media has been removed.

    DBT_DEVTYP_DEVICEINTERFACE = 0x00000005,    // Class of devices. This structure is a DEV_BROADCAST_DEVICEINTERFACE structure.
    DBT_DEVTYP_HANDLE = 0x00000006,             // File system handle. This structure is a DEV_BROADCAST_HANDLE structure.
    DBT_DEVTYP_OEM = 0x00000000,                // OEM- or IHV-defined device type. This structure is a DEV_BROADCAST_OEM structure.
    DBT_DEVTYP_PORT = 0x00000003,               // Port device (serial or parallel). This structure is a DEV_BROADCAST_PORT structure.
    DBT_DEVTYP_VOLUME = 0x00000002,             // Logical volume. This structure is a DEV_BROADCAST_VOLUME structure.

    SIZE_OF_DBH = 12,   // sizeof(DEV_BROADCAST_HDR)
}

public struct DEV_BROADCAST_HDR
{
    internal UInt32 dbch_size;
    internal UInt32 dbch_devicetype;
    internal UInt32 dbch_reserved;
};
iglk
  • 56
  • 2
2

I came to this post for a more specific case than the original question in that I want to be notified anytime a port is added or removed. For this situation the answer is much more simple and does not require calling RegisterDeviceNotification:

The DBT_DEVICEARRIVAL and DBT_DEVICEREMOVECOMPLETE events are automatically broadcast to all top-level windows for port devices. Therefore, it is not necessary to call RegisterDeviceNotification for ports....

https://learn.microsoft.com/en-us/windows/desktop/api/Winuser/nf-winuser-registerdevicenotificationa

So the solution becomes something like:

using System.Runtime.InteropServices;

//Put all of the following code inside your Form's partial class:

private struct DEV_BROADCAST_HDR {
    internal UInt32 dbch_size;
    internal UInt32 dbch_devicetype;
    internal UInt32 dbch_reserved;
};

protected override void WndProc(ref Message m) {
    base.WndProc(ref m);        //This allows window default behavior of base class to be executed
    if (m.Msg == 0x0219) {      //WM_DEVICECHANGE = 0x0219
        DEV_BROADCAST_HDR dbh;
        switch ((int)m.WParam) {                    
            case 0x8000:        //DBT_DEVICEARRIVAL = 0x8000
                dbh = (DEV_BROADCAST_HDR)Marshal.PtrToStructure(m.LParam, typeof(DEV_BROADCAST_HDR));
                if (dbh.dbch_devicetype == 0x00000003) {     //DBT_DEVTYP_PORT = 0x00000003
                    Console.WriteLine("Port added!");
                    //TODO
                }
                break;
            case 0x8004:        //DBT_DEVICEREMOVECOMPLETE = 0x8004                     
                dbh = (DEV_BROADCAST_HDR)Marshal.PtrToStructure(m.LParam, typeof(DEV_BROADCAST_HDR));
                if (dbh.dbch_devicetype == 0x00000003) {     //DBT_DEVTYP_PORT = 0x00000003
                    Console.WriteLine("Port removed!");
                    //TODO
                }
                break;
        }
    }
}
DatuPuti
  • 629
  • 7
  • 17
  • 1
    Man this is amazing. I added `#pragma warning disable CS0649` and `#pragma warning restore` around the fields in DEV_BROADCAST_HDR to silence the warnings :) – Wayne Uroda Jun 20 '19 at 06:32
0

As DatuPuti mentioned:

The DBT_DEVICEARRIVAL and DBT_DEVICEREMOVECOMPLETE events are automatically broadcast to all top-level windows for port devices. Therefore, it is not necessary to call RegisterDeviceNotification for ports

So I took his implementation and adapted it for Windows Forms:

using System;
using System.Runtime.InteropServices;

private enum WM_DEVICECHANGE
{
    // full list: https://learn.microsoft.com/en-us/windows/win32/devio/wm-devicechange
    DBT_DEVICEARRIVAL = 0x8000,             // A device or piece of media has been inserted and is now available.
    DBT_DEVICEREMOVECOMPLETE = 0x8004,      // A device or piece of media has been removed.
}
private int WmDevicechange = 0x0219; // device change event   

protected override void WndProc(ref Message m)
{
    base.WndProc(ref m);        //This allows window default behavior of base class to be executed

    if (m.Msg == WmDevicechange)
    {
        switch ((WM_DEVICECHANGE)m.WParam)
        {
            case WM_DEVICECHANGE.DBT_DEVICEREMOVECOMPLETE:
                Console.WriteLine("USB Device removed");
                break;
            case WM_DEVICECHANGE.DBT_DEVICEARRIVAL:
                Console.WriteLine("USB Device added");
                break;
        }
    }
}
Jay_Nitzel
  • 11
  • 4
0

If you do not have a Window, this solution creates a generic window with no class name and its handle:

using System;
using System.Windows.Forms;

internal class UsbService : NativeWindow, IDisposable
{
    internal UsbService(IEventAggregator eventAggregator)
    {
        _eventAggregator = eventAggregator;
        base.CreateHandle(new CreateParams());
    }

    protected override void WndProc(ref Message msg)
    {
        base.WndProc(ref msg);
        if (msg.Msg == 0x0219) // Device change event
        {
            switch (msg.WParam.ToInt32())
            {
                case 0x8000: // Device added
                case 0x8004: // Device removed
                    _eventAggregator.Publish(...);
                    break;
            }
        }
    }

    public void Dispose()
    {
        if (!_isDisposed)
        {
            base.DestroyHandle();
            _isDisposed = true;
            GC.SuppressFinalize(this);
        }
    }

    private bool _isDisposed;
    private IEventAggregator _eventAggregator;
}

I think it is cleaner to use this in a WPF project. In this example I have used DI to inject an event aggregator which will publish an event to which I am subscribed in a different class. This can be removed and replaced with events to notify the change. To get basic and fast info about connected volumes, I would use DriveInfo.GetDrives() method.

Alexandru Dicu
  • 1,151
  • 1
  • 16
  • 24