如何检测添加新的串口?

Posted

技术标签:

【中文标题】如何检测添加新的串口?【英文标题】:How to detect addition of new serial port? 【发布时间】:2012-12-22 10:53:37 【问题描述】:

为了与微控制器通信,我使用串行端口。我使用工作正常的 TCommPortDriver 2.1。但是,它缺乏检测新组件的添加或删除的能力。这在会话期间经常发生。

是否有一个事件表明组件何时被添加或删除?

更新 1

我尝试了RRUZ的第一个建议并将其变成了一个独立的程序。当电缆插入或拔出时,它会对WM_DEVICECHANGE 作出反应,但WParam 不会显示设备的到达或移除。结果是:

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

第一条消息在 USB 数据线拔出时发送,接下来的两条消息在插入时发送。 消息部分显示WM_DEVICECHANGE 消息(537),但WParam 是7,而不是WM_DEVICECHANGEDBT_DEVICEARRIVAL。我稍微修改了代码以便处理消息,但由于LParam 为零,这没有用。结果与 VCL 和 FMX 相同。作为检查,请参见下面的代码。

更新 2

我现在运行了 WMI 代码。它仅在添加 COM 端口时触发,删除时没有反应。结果:

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 

这是否可以解释在其他代码中这不被视为添加 COM 端口的事实?它似乎将新连接视为 USB 端口(实际上是什么)。 Arduino 驱动程序将其转换为 COM 端口,但 WMI 无法识别。 Windows 消息“看到”COM 端口更改,但无法检测到它是添加还是删除。

无论如何:设备更改有效。我只需要枚举 COM 端口即可查看实际存在的 COM 端口,这是我已经手动完成的。现在我可以使用WM_DEVICECHANGE 自动执行此操作。我只是在 CPDrv 组件中添加一个事件。

感谢 RRUZ 提供代码和帮助!

  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.

【问题讨论】:

使用 WMI 事件可能会有一些运气,只需使用 DEVICE_CHANGE 事件并寻找新添加的串行端口 贴出的WMI代码不包含移除设备检测,需要将WQL语句改为Select * From __InstanceDeletionEvent Within 1 Where TargetInstance ISA "Win32_PnPEntity" AND TargetInstance.ClassGuid="4d36e978-e325-11ce-bfc1-08002be10318" 或使用单个 WQL 语句检测设备的到达或移除,您可以使用 __InstanceOperationEvent WMI 事件。 +1 为您提供详尽的解释。我测试了 __InstanceOperationEvent ,但它需要一些后处理,因为无法从响应中区分删除和添加。我准备好了我的组件,很高兴看到它对删除和添加做出反应。感谢您的代码和帮助! 【参考方案1】:

您可以将DEV_BROADCAST_DEVICEINTERFACE 结构与GUID_DEVINTERFACE_COMPORT 设备接口类一起使用RegisterDeviceNotification WinAPI 函数。

试试这个示例。

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.

【讨论】:

哇,非常感谢!我使用您提供的代码向我当前的应用程序添加了一个表单。更改 com 端口时,调用 WndProc 但未到达。仔细检查Msg 表明它的Msg 确实是WM_DEVICECHANGE. WParam 是7,而不是DBT_DEVICEARRIVAL(所有其他都是0)。更改条件使其在WParam 为7 时执行,Dbi := PDevBroadcastDeviceInterface (LParam); 返回nil,我应该知道因为LParam = 0。WndProc 在更改com 端口时只调用一次。有什么想法吗? @Arnold,你要添加什么样的串口?因为我尝试使用 USB serial port adapter 并添加 virtual serial ports 并且在这两种情况下代码都可以正常工作(Windows 7 x64)。 我添加了一根 USB 电缆,它(也)被 ComPortDriver 视为串行端口。 可能是您的应用程序中的某些东西干扰了设备通知,您可以尝试使用普通 vcl 表单应用程序中的代码吗? 这不会改变结果。在问题更新中查看我的报告。【参考方案2】:

另一个选项是使用 WMI 事件,在这种情况下使用 __InstanceCreationEvent 事件和 Win32_PnPEntity WMI 类,您可以过滤使用 4d36e978-e325-11ce-bfc1-08002be10318 类 GUID 添加的串行端口,像这样编写 WQL 语句

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

试试这个示例

$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.

【讨论】:

我喜欢你玩 WMI 的方式,RRUZ! +1

以上是关于如何检测添加新的串口?的主要内容,如果未能解决你的问题,请参考以下文章

51单片机做串口通信接收缓存区没反应

C# 从串口读取数据 - 如何只获取新的/当前数据?

串口工具 list_ports windows 无法检测到串口

linux下检测可用串口并使用minicom打开

检测串口插入/移除

Linux下如何测试及使用USB转串口线