Needed to get a local serial port list. System.IO.Ports.SerialPort.GetPortNames()
returns names but not the descriptions like you can see in Device Manager: COM6
vs. USB Serial Port (COM6)
…
There is also no way to subscribe to plug-and-play port list changes.
Here comes the solution. Repository at GitHub.
To consume the lib:
static void Main(string[] args)
{
using (var ports = new ComPortList())
{
WriteLine(Join("\n", ports));
ports.Subscribe(WriteLine);
ReadLine();
}
}
Sample output on my machine is:
USB Serial Port (COM6)
USB Serial Port (COM6) was removed.
USB Serial Port (COM6) was added.
Where library code is:
public class ComPortList :
Enumerable<ComPort>, IObservable<ComPortUpdate>, IDisposable
{
public ComPortList()
{
Watcher = new PnPWatcher();
Items = new List<ComPort>();
Subject = new Subject<ComPortUpdate>();
Watcher.Change += (s, e) => Sync();
Sync();
}
PnPWatcher Watcher { get; }
List<ComPort> Items { get; }
Subject<ComPortUpdate> Subject { get; }
public void Dispose()
{
Watcher.Dispose();
Subject.Dispose();
}
public override IEnumerator<ComPort> GetEnumerator()
{
lock (Items)
return Items.ToList().GetEnumerator();
}
public IDisposable Subscribe(IObserver<ComPortUpdate> observer) =>
Subject.Subscribe(observer);
void Sync()
{
lock (Items)
{
var items = ComPort.Detect();
foreach (var port in Items.Except(items).ToArray())
{
Items.Remove(port);
Subject.OnNext(new ComPortRemoval(port));
}
foreach (var port in items.Except(Items).ToArray())
{
Items.Add(port);
Subject.OnNext(new ComPortAddition(port));
}
}
}
}
Using:
public class ComPort : ValueObject<ComPort>
{
public static ComPort[] Detect()
{
using (var searcher = new ManagementObjectSearcher
("SELECT * FROM Win32_PnPEntity WHERE ClassGuid='{4d36e978-e325-11ce-bfc1-08002be10318}'"))
{
return searcher.Get().Cast<ManagementBaseObject>()
.Select(p => new ComPort($"{p["Caption"]}"))
.ToArray();
}
}
ComPort(string description)
{
Description = description;
Name = new string(
description
.Reverse()
.SkipWhile(c => c != ')')
.TakeWhile(c => c != '(')
.Reverse()
.ToArray())
.TrimStart('(')
.TrimEnd(')');
}
public string Description { get; }
public string Name { get; }
public override string ToString() => Description;
protected override IEnumerable<object> EqualityCheckAttributes =>
new object[] { Name, Description };
}
And:
class PnPWatcher : IDisposable
{
const string Query =
"SELECT * FROM Win32_DeviceChangeEvent";
public PnPWatcher()
{
Watcher = new ManagementEventWatcher(Query);
Watcher.EventArrived += (s, e) => Change(this, EventArgs.Empty);
Watcher.Start();
}
ManagementEventWatcher Watcher { get; }
SynchronizationContext Context { get; }
public void Dispose() => Watcher.Dispose();
public event EventHandler Change = delegate { };
}
Event classes are:
public abstract class ComPortUpdate
{
public ComPortUpdate(ComPort port) => Port = port;
public ComPort Port { get; }
}
public class ComPortAddition : ComPortUpdate
{
public ComPortAddition(ComPort port) : base(port) { }
public override string ToString() => $"{Port} was added.";
}
public class ComPortRemoval : ComPortUpdate
{
public ComPortRemoval(ComPort port) : base(port) { }
public override string ToString() => $"{Port} was removed.";
}
Utility classes are:
public abstract class Enumerable<T> : IEnumerable<T>
{
public abstract IEnumerator<T> GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() =>
GetEnumerator();
}
And:
public abstract class ValueObject<T> : IEquatable<ValueObject<T>>
where T : ValueObject<T>
{
protected abstract IEnumerable<object> EqualityCheckAttributes { get; }
public override int GetHashCode() =>
EqualityCheckAttributes
.Aggregate(0, (hash, a) => hash = hash * 31 + (a?.GetHashCode() ?? 0));
public override bool Equals(object obj) =>
Equals(obj as ValueObject<T>);
public virtual bool Equals(ValueObject<T> other) =>
other != null &&
GetType() == other.GetType() &&
EqualityCheckAttributes.SequenceEqual(other.EqualityCheckAttributes);
public static bool operator ==(ValueObject<T> left, ValueObject<T> right) =>
Equals(left, right);
public static bool operator !=(ValueObject<T> left, ValueObject<T> right) =>
!Equals(left, right);
}
ComPortList
not implementingIList
- hadn't expected this from you who recommended me the book on framwork design :-P It should be anObservableComPortCollection
or at leastComPortCollection
. Also why does it have theList<ComPort> Items { get; }
property if it's already a list? Are you sure this is your code? ;-) \$\endgroup\$ComPortList
sounds more natural - one would not say Collection about COM ports. From that book: "For example, a type used in mainline scenarios to submit print jobs to print queues should be named Printer, rather than PrintQueue. Even though technically the type represents a print queue and not the physical device (printer), from the scenario point of view, Printer is the ideal name because most people are interested in submitting print jobs and not in other operations related to the physical printer device (e.g., configuring the printer)." 2) ReadOnlyCollection<T>.Items? \$\endgroup\$