7
votes

To communicate with micro controllers I use the serial port. I use TCommPortDriver 2.1 which works fine. However, it lacks the ability to detect the addition or removal of new comports. This happens regularly during a session.

Is there an event that tells when a comport has been added or removed?

Update 1

I tried the first suggestion of RRUZ and turned it into a standalone program. It reacts on a WM_DEVICECHANGE when the cable is plugged in or out, but WParam does not show arrival or removal of the device. Results are:

msg = 537, wparam = 7, lparam = 0
msg = 537, wparam = 7, lparam = 0
msg = 537, wparam = 7, lparam = 0

The first message is sent when the USB cable is plugged out and the next two when it is plugged in. The message part shows the WM_DEVICECHANGE message (537) but WParam is 7, which is not WM_DEVICECHANGE or DBT_DEVICEARRIVAL. I modified the code somewhat in order the message to be processed but as LParam is zero this is no use. Results are identical to VCL and FMX. As a check see code below.

Update 2

I now got the WMI code running. It only fires when a COM port is added, no reaction when one is removed. Results:

TargetInstance.ClassGuid      : {4d36e978-e325-11ce-bfc1-08002be10318} 
TargetInstance.Description    : Arduino Mega ADK R3 
TargetInstance.Name           : Arduino Mega ADK R3 (COM4) 
TargetInstance.PNPDeviceID    : USB\VID_2341&PID_0044\64935343733351E0E1D1 
TargetInstance.Status         : OK 

Might this explain the fact that in the other code this is not seen as the addition of a COM port? It appears to see the new connection as a USB port (what it actually is). The Arduino driver translates this into a COM port but that is not recognized by WMI. Windows messaging 'sees' a COM port change but cannot detect whether it is added or removed.

Anyhow: the device change works. I only need to enumerate the COM ports to see which one are actually present and that was something I already did manually. Now I can do that automatically with WM_DEVICECHANGE. I just add an event to the CPDrv component.

Thanks RRUZ for your code and help!

  unit dev_change;

  interface

  uses
    Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
    Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

  type
    TProc = procedure (text: string) of object;

    BroadcastHdr  = ^DEV_BROADCAST_HDR;
    DEV_BROADCAST_HDR = packed record
      dbch_size: DWORD;
      dbch_devicetype: DWORD;
      dbch_reserved: DWORD;
    end;
    TDevBroadcastHdr = DEV_BROADCAST_HDR;

  type
    PDevBroadcastDeviceInterface  = ^DEV_BROADCAST_DEVICEINTERFACE;
    DEV_BROADCAST_DEVICEINTERFACE = record
      dbcc_size: DWORD;
      dbcc_devicetype: DWORD;
      dbcc_reserved: DWORD;
      dbcc_classguid: TGUID;
      dbcc_name: Char;
    end;
    TDevBroadcastDeviceInterface = DEV_BROADCAST_DEVICEINTERFACE;

  const
    DBT_DEVICESOMETHING        = $0007;
    DBT_DEVICEARRIVAL          = $8000;
    DBT_DEVICEREMOVECOMPLETE   = $8004;
    DBT_DEVTYP_DEVICEINTERFACE = $00000005;

  type
    TDeviceNotifyProc = procedure(Sender: TObject; const DeviceName: String) of Object;
    TDeviceNotifier = class
    private
      hRecipient: HWND;
      FNotificationHandle: Pointer;
      FDeviceArrival: TDeviceNotifyProc;
      FDeviceRemoval: TDeviceNotifyProc;
      FOnWin: TProc;

      procedure WndProc(var Msg: TMessage);

    public
      constructor Create(GUID_DEVINTERFACE : TGUID);
      property OnDeviceArrival: TDeviceNotifyProc read FDeviceArrival write FDeviceArrival;
      property OnDeviceRemoval: TDeviceNotifyProc read FDeviceRemoval write FDeviceRemoval;
      destructor Destroy; override;

      property OnWin: TProc read FOnWin write FOnWin;
    end;

    TForm1 = class(TForm)
      Memo: TMemo;
      procedure FormCreate(Sender: TObject);
      procedure FormDestroy(Sender: TObject);
    private
      { Private declarations }
      DeviceNotifier : TDeviceNotifier;
    public
      { Public declarations }
      procedure arrival(Sender: TObject; const DeviceName: String);
      procedure report (text: string);
    end;

  var
    Form1: TForm1;

  implementation

  {$R *.dfm}

  constructor TDeviceNotifier.Create(GUID_DEVINTERFACE : TGUID);
  var
    NotificationFilter: TDevBroadcastDeviceInterface;
  begin
    inherited Create;
    hRecipient := AllocateHWnd(WndProc);
    ZeroMemory (@NotificationFilter, SizeOf(NotificationFilter));
    NotificationFilter.dbcc_size := SizeOf(NotificationFilter);
    NotificationFilter.dbcc_devicetype := DBT_DEVTYP_DEVICEINTERFACE;
    NotificationFilter.dbcc_classguid  := GUID_DEVINTERFACE;
    //register the device class to monitor
    FNotificationHandle := RegisterDeviceNotification(hRecipient, @NotificationFilter, DEVICE_NOTIFY_WINDOW_HANDLE);
  end;

  procedure TDeviceNotifier.WndProc(var Msg: TMessage);
  var
    Dbi: PDevBroadcastDeviceInterface;
  begin
    OnWin (Format ('msg = %d, wparam = %d, lparam = %d', [msg.Msg, msg.WParam, msg.LParam]));
    with Msg do
    if (Msg = WM_DEVICECHANGE) and ((WParam = DBT_DEVICEARRIVAL) or (WParam = DBT_DEVICEREMOVECOMPLETE) or
                                    (WParam = DBT_DEVICESOMETHING)) then
    try
      Dbi := PDevBroadcastDeviceInterface (LParam);
      if Dbi.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE then
      begin
        if WParam = DBT_DEVICEARRIVAL then
        begin
          if Assigned(FDeviceArrival) then
            FDeviceArrival(Self, PChar(@Dbi.dbcc_name));
        end
        else
        if WParam = DBT_DEVICEREMOVECOMPLETE then
        begin
          if Assigned(FDeviceRemoval) then
            FDeviceRemoval(Self, PChar(@Dbi.dbcc_name));
        end;
      end;
    except
      Result := DefWindowProc(hRecipient, Msg, WParam, LParam);
    end
    else
      Result := DefWindowProc(hRecipient, Msg, WParam, LParam);
  end;

  destructor TDeviceNotifier.Destroy;
  begin
    UnregisterDeviceNotification(FNotificationHandle);
    DeallocateHWnd(hRecipient);
    inherited;
  end;

  procedure TForm1.arrival(Sender: TObject; const DeviceName: String);
  begin
     report (DeviceName);

     ShowMessage(DeviceName);
  end;

  procedure TForm1.FormCreate(Sender: TObject);
  const
    GUID_DEVINTERFACE_COMPORT  : TGUID = '{86E0D1E0-8089-11D0-9CE4-08003E301F73}';
  begin
    DeviceNotifier:=TDeviceNotifier.Create(GUID_DEVINTERFACE_COMPORT);
    DeviceNotifier.FDeviceArrival:=arrival;
    DeviceNotifier.OnWin := report;
  end;

  procedure TForm1.FormDestroy(Sender: TObject);
  begin
    DeviceNotifier.Free;
  end;

  procedure TForm1.report (text: string);
  begin
     Memo.Lines.Add (text);
  end;

  end.
2
You might have some luck using WMI events, just use DEVICE_CHANGE event and look for newly added serial portsPeter Hagström
The posted WMI code does not include the removal device detection, for that you must change the WQL sentence to Select * From __InstanceDeletionEvent Within 1 Where TargetInstance ISA "Win32_PnPEntity" AND TargetInstance.ClassGuid="{4d36e978-e325-11ce-bfc1-08002be10318}"RRUZ
or for detect the arrival or remove of the device using a single WQL sentence you can use the __InstanceOperationEvent WMI event.RRUZ
+1 for your thorough explanations. I tested __InstanceOperationEvent which works, but one needs some postprocessing as it is not possible to distinguish between deletion and addition from the reponse. I got my component ready and it really is a pleasure to see it reacting on removals and additions. Thanks for your code and help!Arnold

2 Answers

10
votes

You can use the RegisterDeviceNotification WinAPI function passing the DEV_BROADCAST_DEVICEINTERFACE structure with the GUID_DEVINTERFACE_COMPORT device interface class.

Try this sample.

type
  PDevBroadcastHdr  = ^DEV_BROADCAST_HDR;
  DEV_BROADCAST_HDR = packed record
    dbch_size: DWORD;
    dbch_devicetype: DWORD;
    dbch_reserved: DWORD;
  end;
  TDevBroadcastHdr = DEV_BROADCAST_HDR;

type
  PDevBroadcastDeviceInterface  = ^DEV_BROADCAST_DEVICEINTERFACE;
  DEV_BROADCAST_DEVICEINTERFACE = record
    dbcc_size: DWORD;
    dbcc_devicetype: DWORD;
    dbcc_reserved: DWORD;
    dbcc_classguid: TGUID;
    dbcc_name: Char;
  end;
  TDevBroadcastDeviceInterface = DEV_BROADCAST_DEVICEINTERFACE;

const
  DBT_DEVICEARRIVAL          = $8000;
  DBT_DEVICEREMOVECOMPLETE   = $8004;
  DBT_DEVTYP_DEVICEINTERFACE = $00000005;

type
  TDeviceNotifyProc = procedure(Sender: TObject; const DeviceName: String) of Object;
  TDeviceNotifier = class
  private
    hRecipient: HWND;
    FNotificationHandle: Pointer;
    FDeviceArrival: TDeviceNotifyProc;
    FDeviceRemoval: TDeviceNotifyProc;
    procedure WndProc(var Msg: TMessage);
  public
    constructor Create(GUID_DEVINTERFACE : TGUID);
    property OnDeviceArrival: TDeviceNotifyProc read FDeviceArrival write FDeviceArrival;
    property OnDeviceRemoval: TDeviceNotifyProc read FDeviceRemoval write FDeviceRemoval;
    destructor Destroy; override;
  end;

type
  TForm17 = class(TForm)
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    { Private declarations }
    DeviceNotifier : TDeviceNotifier;
  public
    { Public declarations }
    procedure arrival(Sender: TObject; const DeviceName: String);
  end;

var
  Form17: TForm17;

implementation

{$R *.dfm}

constructor TDeviceNotifier.Create(GUID_DEVINTERFACE : TGUID);
var
  NotificationFilter: TDevBroadcastDeviceInterface;
begin
  inherited Create;
  hRecipient := AllocateHWnd(WndProc);
  ZeroMemory(@NotificationFilter, SizeOf(NotificationFilter));
  NotificationFilter.dbcc_size := SizeOf(NotificationFilter);
  NotificationFilter.dbcc_devicetype := DBT_DEVTYP_DEVICEINTERFACE;
  NotificationFilter.dbcc_classguid  := GUID_DEVINTERFACE;
  //register the device class to monitor
  FNotificationHandle := RegisterDeviceNotification(hRecipient, @NotificationFilter, DEVICE_NOTIFY_WINDOW_HANDLE);
end;

procedure TDeviceNotifier.WndProc(var Msg: TMessage);
var
  Dbi: PDevBroadcastDeviceInterface;
begin
  with Msg do
  if (Msg = WM_DEVICECHANGE) and ((WParam = DBT_DEVICEARRIVAL) or (WParam = DBT_DEVICEREMOVECOMPLETE)) then
  try
    Dbi := PDevBroadcastDeviceInterface(LParam);
    if Dbi.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE then
    begin
      if WParam = DBT_DEVICEARRIVAL then
      begin
        if Assigned(FDeviceArrival) then
          FDeviceArrival(Self, PChar(@Dbi.dbcc_name));
      end
      else
      if WParam = DBT_DEVICEREMOVECOMPLETE then
      begin
        if Assigned(FDeviceRemoval) then
          FDeviceRemoval(Self, PChar(@Dbi.dbcc_name));
      end;
    end;
  except
    Result := DefWindowProc(hRecipient, Msg, WParam, LParam);
  end
  else
    Result := DefWindowProc(hRecipient, Msg, WParam, LParam);
end;

destructor TDeviceNotifier.Destroy;
begin
  UnregisterDeviceNotification(FNotificationHandle);
  DeallocateHWnd(hRecipient);
  inherited;
end;



procedure TForm17.arrival(Sender: TObject; const DeviceName: String);
begin
  ShowMessage(DeviceName);
end;

procedure TForm17.FormCreate(Sender: TObject);
const
  GUID_DEVINTERFACE_COMPORT  : TGUID = '{86E0D1E0-8089-11D0-9CE4-08003E301F73}';
begin      
  DeviceNotifier:=TDeviceNotifier.Create(GUID_DEVINTERFACE_COMPORT);
  DeviceNotifier.FDeviceArrival:=arrival;
end;

procedure TForm17.FormDestroy(Sender: TObject);
begin
  DeviceNotifier.Free;
end;

end.
6
votes

Another option is use the WMI Events, on this case using the __InstanceCreationEvent Event and the Win32_PnPEntity WMI class you can filter the serial ports added using the {4d36e978-e325-11ce-bfc1-08002be10318} class GUID, writting a WQL sentence like so

Select * From __InstanceCreationEvent Within 1 Where TargetInstance ISA "Win32_PnPEntity" AND TargetInstance.ClassGuid="{4d36e978-e325-11ce-bfc1-08002be10318}"

Try this sample

{$APPTYPE CONSOLE}

{$R *.res}

uses
  Windows,
  {$IF CompilerVersion > 18.5}
  Forms,
  {$IFEND}
  SysUtils,
  ActiveX,
  ComObj,
  WbemScripting_TLB;

type
  TWmiAsyncEvent = class
  private
    FWQL      : string;
    FSink     : TSWbemSink;
    FLocator  : ISWbemLocator;
    FServices : ISWbemServices;
    procedure EventReceived(ASender: TObject; const objWbemObject: ISWbemObject; const objWbemAsyncContext: ISWbemNamedValueSet);
  public
    procedure  Start;
    constructor Create;
    Destructor Destroy;override;
  end;

//Detect when a key was pressed in the console window
function KeyPressed:Boolean;
var
  lpNumberOfEvents     : DWORD;
  lpBuffer             : TInputRecord;
  lpNumberOfEventsRead : DWORD;
  nStdHandle           : THandle;
begin
  Result:=false;
  nStdHandle := GetStdHandle(STD_INPUT_HANDLE);
  lpNumberOfEvents:=0;
  GetNumberOfConsoleInputEvents(nStdHandle,lpNumberOfEvents);
  if lpNumberOfEvents<> 0 then
  begin
    PeekConsoleInput(nStdHandle,lpBuffer,1,lpNumberOfEventsRead);
    if lpNumberOfEventsRead <> 0 then
    begin
      if lpBuffer.EventType = KEY_EVENT then
      begin
        if lpBuffer.Event.KeyEvent.bKeyDown then
          Result:=true
        else
          FlushConsoleInputBuffer(nStdHandle);
      end
      else
      FlushConsoleInputBuffer(nStdHandle);
    end;
  end;
end;

{ TWmiAsyncEvent }

constructor TWmiAsyncEvent.Create;
const
  strServer    ='.';
  strNamespace ='root\CIMV2';
  strUser      ='';
  strPassword  ='';
begin
  inherited Create;
  CoInitializeEx(nil, COINIT_MULTITHREADED);
  FLocator  := CoSWbemLocator.Create;
  FServices := FLocator.ConnectServer(strServer, strNamespace, strUser, strPassword, '', '', wbemConnectFlagUseMaxWait, nil);
  FSink     := TSWbemSink.Create(nil);
  FSink.OnObjectReady := EventReceived;
  FWQL:='Select * From __InstanceCreationEvent Within 1 '+
        'Where TargetInstance ISA "Win32_PnPEntity" AND TargetInstance.ClassGuid="{4d36e978-e325-11ce-bfc1-08002be10318}" ';

end;

destructor TWmiAsyncEvent.Destroy;
begin
  if FSink<>nil then
    FSink.Cancel;
  FLocator  :=nil;
  FServices :=nil;
  FSink     :=nil;
  CoUninitialize;
  inherited;
end;

procedure TWmiAsyncEvent.EventReceived(ASender: TObject;
  const objWbemObject: ISWbemObject;
  const objWbemAsyncContext: ISWbemNamedValueSet);
var
  PropVal: OLEVariant;
begin
  PropVal := objWbemObject;
  Writeln(Format('TargetInstance.ClassGuid      : %s ',[String(PropVal.TargetInstance.ClassGuid)]));
  Writeln(Format('TargetInstance.Description    : %s ',[String(PropVal.TargetInstance.Description)]));
  Writeln(Format('TargetInstance.Name           : %s ',[String(PropVal.TargetInstance.Name)]));
  Writeln(Format('TargetInstance.PNPDeviceID    : %s ',[String(PropVal.TargetInstance.PNPDeviceID)]));
  Writeln(Format('TargetInstance.Status         : %s ',[String(PropVal.TargetInstance.Status)]));
end;

procedure TWmiAsyncEvent.Start;
begin
  Writeln('Listening events...Press Any key to exit');
  FServices.ExecNotificationQueryAsync(FSink.DefaultInterface,FWQL,'WQL', 0, nil, nil);
end;

var
   AsyncEvent : TWmiAsyncEvent;
begin
 try
    AsyncEvent:=TWmiAsyncEvent.Create;
    try
      AsyncEvent.Start;
      //The next loop is only necessary in this sample console sample app
      //In VCL forms Apps you don't need use a loop
      while not KeyPressed do
      begin
          {$IF CompilerVersion > 18.5}
          Sleep(100);
          Application.ProcessMessages;
          {$IFEND}
      end;
    finally
      AsyncEvent.Free;
    end;
 except
    on E:EOleException do
        Writeln(Format('EOleException %s %x', [E.Message,E.ErrorCode]));
    on E:Exception do
        Writeln(E.Classname, ':', E.Message);
 end;
end.