25

I am interfacing with a USB-to-serial port that can be inserted or removed at any time. I've found that I can use WMI (particularly with the use of WMI Code Creator) to query for device changes in the PC.

In the generated snippet below, the Win32_DeviceChangeEvent is subscribed to. However, this event doesn't reveal which device (e.g. USB, serial port, etc) caused the event. Is there a way to only receive notifications when serial ports are inserted or removed?

To clarify, the point of the code is not to detect opening/closing of serial ports, it is to detect whether a new port has been added to the machine or a previous port was removed.

using System;
using System.Management;
using System.Windows.Forms;

namespace WMISample
{
    public class WMIReceiveEvent
    {
        public WMIReceiveEvent()
        {
            try
            {
                WqlEventQuery query = new WqlEventQuery(
                    "SELECT * FROM Win32_DeviceChangeEvent");

                ManagementEventWatcher watcher = new ManagementEventWatcher(query);
                Console.WriteLine("Waiting for an event...");

                watcher.EventArrived += 
                    new EventArrivedEventHandler(
                    HandleEvent);

                // Start listening for events
                watcher.Start();

                // Do something while waiting for events
                System.Threading.Thread.Sleep(10000);

                // Stop listening for events
                watcher.Stop();
                return;
            }
            catch(ManagementException err)
            {
                MessageBox.Show("An error occurred while trying to receive an event: " + err.Message);
            }
        }

        private void HandleEvent(object sender,
            EventArrivedEventArgs e)
        {
            Console.WriteLine("Win32_DeviceChangeEvent event occurred.");
        }

        public static void Main()
        {
            WMIReceiveEvent receiveEvent = new WMIReceiveEvent();
            return;
        }

    }
}
Pat
  • 16,515
  • 15
  • 95
  • 114

5 Answers5

24

I ended up using WMI and @Hans' advice to check what serial ports are new/missing.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Diagnostics.Contracts;
using System.IO.Ports;
using System.Management;

public static class SerialPortService
{
    private static SerialPort _serialPort;

    private static string[] _serialPorts;

    private static ManagementEventWatcher arrival;

    private static ManagementEventWatcher removal;

    static SerialPortService()
    {
        _serialPorts = GetAvailableSerialPorts();
        MonitorDeviceChanges();
    }

    /// <summary>
    /// If this method isn't called, an InvalidComObjectException will be thrown (like below):
    /// System.Runtime.InteropServices.InvalidComObjectException was unhandled
    ///Message=COM object that has been separated from its underlying RCW cannot be used.
    ///Source=mscorlib
    ///StackTrace:
    ///     at System.StubHelpers.StubHelpers.StubRegisterRCW(Object pThis, IntPtr pThread)
    ///     at System.Management.IWbemServices.CancelAsyncCall_(IWbemObjectSink pSink)
    ///     at System.Management.SinkForEventQuery.Cancel()
    ///     at System.Management.ManagementEventWatcher.Stop()
    ///     at System.Management.ManagementEventWatcher.Finalize()
    ///InnerException: 
    /// </summary>
    public static void CleanUp()
    {
        arrival.Stop();
        removal.Stop();
    }

    public static event EventHandler<PortsChangedArgs> PortsChanged;

    private static void MonitorDeviceChanges()
    {
        try
        {
            var deviceArrivalQuery = new WqlEventQuery("SELECT * FROM Win32_DeviceChangeEvent WHERE EventType = 2");
            var deviceRemovalQuery = new WqlEventQuery("SELECT * FROM Win32_DeviceChangeEvent WHERE EventType = 3");

            arrival = new ManagementEventWatcher(deviceArrivalQuery);
            removal = new ManagementEventWatcher(deviceRemovalQuery);

            arrival.EventArrived += (o, args) => RaisePortsChangedIfNecessary(EventType.Insertion);
            removal.EventArrived += (sender, eventArgs) => RaisePortsChangedIfNecessary(EventType.Removal);

            // Start listening for events
            arrival.Start();
            removal.Start();
        }
        catch (ManagementException err)
        {

        }
    }

    private static void RaisePortsChangedIfNecessary(EventType eventType)
    {
        lock (_serialPorts)
        {
            var availableSerialPorts = GetAvailableSerialPorts();
            if (!_serialPorts.SequenceEqual(availableSerialPorts))
            {
                _serialPorts = availableSerialPorts;
                PortsChanged.Raise(null, new PortsChangedArgs(eventType, _serialPorts));
            }
        }
    }

    public static string[] GetAvailableSerialPorts()
    {
        return SerialPort.GetPortNames();
    }
}

public enum EventType
{
    Insertion,
    Removal,
}

public class PortsChangedArgs : EventArgs
{
    private readonly EventType _eventType;

    private readonly string[] _serialPorts;

    public PortsChangedArgs(EventType eventType, string[] serialPorts)
    {
        _eventType = eventType;
        _serialPorts = serialPorts;
    }

    public string[] SerialPorts
    {
        get
        {
            return _serialPorts;
        }
    }

    public EventType EventType
    {
        get
        {
            return _eventType;
        }
    }
}

The MonitorDeviceChanges method actually sees all device changes (like Device Manager), but checking the serial ports allows us to only raise an event when those have changed.

To use the code, simply subscribe to the PortsChanged event, e.g. SerialPortService.PortsChanged += (sender1, changedArgs) => DoSomethingSerial(changedArgs.SerialPorts);

Oh, and the .Raise method is just an extension method I picked up somewhere:

/// <summary>
/// Tell subscribers, if any, that this event has been raised.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="handler">The generic event handler</param>
/// <param name="sender">this or null, usually</param>
/// <param name="args">Whatever you want sent</param>
public static void Raise<T>(this EventHandler<T> handler, object sender, T args) where T : EventArgs
{
    // Copy to temp var to be thread-safe (taken from C# 3.0 Cookbook - don't know if it's true)
    EventHandler<T> copy = handler;
    if (copy != null)
    {
        copy(sender, args);
    }
}
Pat
  • 16,515
  • 15
  • 95
  • 114
  • 3
    Hi, I've tried to use this code and I found that the removal part is not the best you can get. When the application has connection with the device and you disconnect the cable your code will notice this. However, you cannnot see any changes because the program did not clean the ports yet and the `GetPortNames` will return the comport which is not available anymore. I am only interested in the removal event, so I am checking whether the `SerialPort` is open or not. If the port is closed, a removal event has occured. – 2pietjuh2 Apr 29 '13 at 13:14
  • 1
    @2pietjuh2 If I understand you, you're correct. The point of the code isn't to detect opening/closing of serial ports, it is to detect whether a *new* port has been added to the machine or a previous port was *removed*. So could it be that you are looking at a different problem? – Pat May 02 '13 at 00:04
  • Can you explain what eventtype 2 and 3 are and what other event types exist? – John Demetriou Dec 01 '15 at 15:16
  • 2
    @JohnDemetriou see https://msdn.microsoft.com/en-us/library/windows/desktop/aa394124(v=vs.85).aspx, specifically: Configuration Changed (1) Device Arrival (2) Device Removal (3) Docking (4) – Pat Dec 09 '15 at 22:51
  • Is anyone is facing multiple event firing issue using above code? – Manish Dubey Nov 03 '17 at 07:07
2

NB: I tried to post this as a comment on @Pat's answer, but don't have enough reputation to do that.

Further to @2pietjuh2's comment, the RaisePortsChangedIfNecessary() can be changed to the following:

private static void RaisePortsChangedIfNecessary(EventType eventType)
{
    lock (_serialPorts)
    {
        var availableSerialPorts = GetAvailableSerialPorts();
        if (eventType == EventType.Insertion)
        {
            var added = availableSerialPorts.Except(_serialPorts).ToArray();
            _serialPorts = availableSerialPorts;
            PortsChanged.Raise(null, new PortsChangedArgs(eventType, added));
        }
        else if (eventType == EventType.Removal)
        {
            var removed = _serialPorts.Except(availableSerialPorts).ToArray();
            _serialPorts = availableSerialPorts;
            PortsChanged.Raise(null, new PortsChangedArgs(eventType, removed));
        }
    }
}

Raised events then include the serial port inserted/removed, rather than the list of serial ports available after the insertion/removal.

Journeyman
  • 95
  • 9
1

No. Go find out what happened to SerialPort.GetPortNames(). Listening for the WM_DEVICECHANGE message in a window can give you better info.

Wolf
  • 9,679
  • 7
  • 62
  • 108
Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
1

Here is a stripped down version of a DeviceChangeEvents notification class I wrote some time ago, though I never fully completed it. I stripped out everything except the PortArrived event as it's quite fugly otherwise.

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Forms;

public sealed class PortArrivalEventArgs : EventArgs
{
    public string Name { get; private set; }
    public PortArrivalEventArgs(string name) { Name = name; }
}

public static class DeviceChangeEvents
{
    #region Events

    #region PortArrived
    private static object PortArrivedEvent = new Object();
    public static event EventHandler<PortArrivalEventArgs> PortArrived
    {
        add { AddEvent(PortArrivedEvent, value); }
        remove { RemoveEvent(PortArrivedEvent, value); }
    }
    private static void FirePortArrived(IntPtr lParam)
    {
        EventHandler<PortArrivalEventArgs> handler
            = (EventHandler<PortArrivalEventArgs>)events[PortArrivedEvent];
        if (handler != null)
        {
            string portName = Marshal.PtrToStringAuto((IntPtr)((long)lParam + 12));
            handler(null, new PortArrivalEventArgs(portName));
        }
    }
    #endregion

    #endregion

    #region Internal

    private static EventHandlerList events = new EventHandlerList();
    private static MessageWindow messageWindow = null;

    private static void AddEvent(object key, Delegate value)
    {
        events.AddHandler(key, value);
        if (messageWindow == null)
            messageWindow = new MessageWindow();
    }

    private static void RemoveEvent(object key, Delegate value)
    {
        events.RemoveHandler(key, value);

        // In the more complete version of DeviceChangedEvents, System.ComponentModel.EventHandlerList
        //  is replaced by an identical event storage object which exposes a count of the number of
        //  handlers installed. It also removes empty handler stubs. Both of these are required
        //  to safely destroy the message window when the last handler is removed.

        //if (messageWindow != null && events.Count == 0)
        //    messageWindow.DestroyHandle();
    }

    #endregion

    private sealed class MessageWindow : NativeWindow
    {
        public MessageWindow()
        {
            CreateParams cp = new CreateParams();
            cp.Caption = GetType().FullName;
            // NOTE that you cannot use a "message window" for this broadcast message
            //if (Environment.OSVersion.Platform == PlatformID.Win32NT)
            //  cp.Parent = (IntPtr)(-3); // HWND_MESSAGE
            //Debug.WriteLine("Creating MessageWindow " + cp.Caption);
            CreateHandle(cp);
        }

        const int WM_DESTROY = 0x02;
        const int WM_DEVICECHANGE = 0x219;

        enum DBT
        {
            DEVICEARRIVAL = 0x8000,
        }

        protected override void WndProc(ref Message m)
        {
            if (m.Msg == WM_DESTROY)
            {
                messageWindow = null;
            }
            else if (m.Msg == WM_DEVICECHANGE)
            {
                DBT changeType = (DBT)m.WParam;
                int deviceType = m.LParam == IntPtr.Zero ? 0 : Marshal.ReadInt32(m.LParam, 4);

                Debug.WriteLine(String.Format("WM_DEVICECHANGE changeType = {0}, deviceType = {1}", changeType, deviceType));

                switch (changeType)
                {
                    case DBT.DEVICEARRIVAL:
                        switch (deviceType)
                        {
                            case 3: // DBT_DEVTYP_PORT
                                FirePortArrived(m.LParam);
                                break;
                        }
                        break;
                }
            }

            base.WndProc(ref m);
        }
    }
}
Tergiver
  • 14,171
  • 3
  • 41
  • 68
  • Thanks for clarifying what @Hans meant by "Listening for the WM_DEVICECHANGE message in a window" - I had no idea. But having to have a NativeWindow and unmanaged code doesn't really appeal to me. – Pat Nov 16 '10 at 23:29
  • 1
    Every single line of your code makes calls to unmanaged code. There is no such thing as a 'pure' .NET application. No application can perform useful work without interacting with the operating system. – Tergiver Nov 17 '10 at 03:41
  • If you don't like the NativeWindow (which makes no sense as all System.Windows.Forms.Control objects are based on NativeWindow), you can simply override your main window's WndProc. The purpose of the above class is to encapsulate the message on its own. – Tergiver Nov 17 '10 at 03:43
  • I wasn't meaning to offend, but using anything from the Marshal class means using unmanaged code (http://msdn.microsoft.com/en-us/library/system.runtime.interopservices.marshal.aspx). Also, I am working with a Console app, so there isn't a "main window". – Pat Nov 17 '10 at 19:41
  • If you want to use WM_DEVICECHANGE in a console app, you can spin up a thread with a message pump and a window. I didn't take offense to your comment, what I have a problem with is people's aversion to 'unsafe' code. The .NET Framework is chock full of unsafe code. It cannot be avoided, therefore any aversion to it is illogical. – Tergiver Nov 18 '10 at 14:09
  • I don't exactly remember the context when I asked this question, but I think what I was trying to avoid was having to check the "unsafe code" checkbox somewhere in the project properties. I can't remember why, but at the time it seemed important. – Pat Oct 09 '12 at 18:01
  • @Pat There is nothing in the code above that would require the unsafe keyword or compiler switch. It's impossible to interact with the OS without running unmanaged code and it's impossible to perform useful work without the OS. What WinForms does is hide the unmanaged bits from you by encapsulating it. The problem is, like any wrapper/encapsulation, there are always missing bits. Win32 is *massive*. A 100% complete wrapper would be nearly impossible to build. WinForms covers all of the features that are commonly used. For the rest you have to manage the interop yourself. – Tergiver Oct 10 '12 at 14:48
0

Your device change event can be used with the WMI - PNP Entity. The following will return device details - in the code below it shows the device name.

Dim moReturn As Management.ManagementObjectCollection
Dim moSearch As Management.ManagementObjectSearcher
Dim mo As Management.ManagementObject
moSearch = New Management.ManagementObjectSearcher("Select * from Win32_PnPEntity")
moReturn = moSearch.Get

For Each mo In moReturn
If CStr(mo.Properties.Item("Name").Value).Contains("Prolific") Then
    returns something like: "Prolific USB-to-Serial Comm Port (COM17)"
    txtStatus.Text &= CStr(mo.Properties.Item("Name").Value) & vbCrLf
End If
Next

Also see code to access other PNP properties that could be used to filtered or monitored for change:

On Error Resume Next
strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colItems = objWMIService.ExecQuery("Select * from Win32_PnPEntity",,48)
For Each objItem in colItems
    "Availability: " & objItem.Availability
    "Caption: " & objItem.Caption
    "ClassGuid: " & objItem.ClassGuid
    "ConfigManagerErrorCode: " & objItem.ConfigManagerErrorCode
    "ConfigManagerUserConfig: " & objItem.ConfigManagerUserConfig
    "CreationClassName: " & objItem.CreationClassName
    "Description: " & objItem.Description
    "DeviceID: " & objItem.DeviceID
    "ErrorCleared: " & objItem.ErrorCleared
    "ErrorDescription: " & objItem.ErrorDescription
    "InstallDate: " & objItem.InstallDate
    "LastErrorCode: " & objItem.LastErrorCode
    "Manufacturer: " & objItem.Manufacturer
    "Name: " & objItem.Name
    "PNPDeviceID: " & objItem.PNPDeviceID
    "PowerManagementCapabilities: " & objItem.PowerManagementCapabilities
    "PowerManagementSupported: " & objItem.PowerManagementSupported
    "Service: " & objItem.Service
    "Status: " & objItem.Status
    "StatusInfo: " & objItem.StatusInfo
    "SystemCreationClassName: " & objItem.SystemCreationClassName
    "SystemName: " & objItem.SystemName
Next
alyeomans
  • 21
  • 3