воскресенье, 30 июля 2017 г.

SetupApiMOD. Автоматическое определение портов с подключенным оборудованием

При эксплуатации промышленного оборудования важна не только надежность подключения и стабильность канала связи, но и минимизация ручных настроек и как следствие потерь времени. Как часто вам приходится лезть в диспетчер устройств и выяснять на каком порту сидит то или иное устройство, назначенное вашей ОС? Когда их один или два можно воспользоваться уникальными идентификаторами физического подключения (Serial number, Frandly name), как в случае виртуальных UART over USB конверторов интерфейса RS-485, RS-232, Ethernet. А представьте себе, что эти идентификаторы одинаковые или их нет вовсе (тот же физический RS-232), как различить что вот этот датчик выдает данные по COM1, этот по COM16, а те гаджеты по COM31 и COM32? "Данные", - вот ключевое слово. При известных протоколах и формате передачи данных всегда можно узнать, что идет и по какому каналу. Даже в случае, если при следующем сеансе связи было произведено физическое переподключение на совсем другие порты и/или конверторы интерфейсов. При этом инсталлятору оборудования вообще не нужно задумываться к какому из конверторов или плат сбора данных на объекте подключаться, главное - чтобы их первичные физические интерфейсы совпадали. Интересно? Тогда добро пожаловать под кат...

Основная идея заключается в том, что по каждому из последовательных портов (виртуальному или физическому) идут свои DATA со своими заголовками/шапкой. Назовем их сигнатурами SIG1 (скажем, AAh 55h AAh 55h) и SIG2 (FFh FFh FFh FFh AAh 55h AAh 55h). Причем количество сигнатур может быть любым, как и их длина. Вот их то и будем парсить.

Задача
  1. Обеспечить автоподключение бенчмарка к нужным каналам (COM-портам) за счет селекции заголовков данных без участия оператора (неважно в какой порт что подключили).
  2. Обеспечить отслеживание извлечения USB-конвертора (пропадание порта) и автопереподключение к нужному порту.
В качестве базиса для поиска портов, как всегда, мы решили использовать функции SetupAPI. Почему?
  1. Открытие портов через CreateFile() при использовании драйверов некоторых вендоров-производителей конверторов приводит к дикому торможению определения.
  2. Сканирование реестра может быть заблокировано политиками безопасности.
  3. WMI служба может быть отключена.
Функционал модуля SetupApiMOD следующий
  1. Инсталляции и настройки не требует (см.секции initialization, finalization).
  2. В потоке таймера (1000 мс), определяющим время ожидания и накопления данных производится перебор всех найденных портов с однократными попытками приема данных.
  3. В событии приема данных осуществляется селекция заголовков пакетов "плавающим окном" с накоплением (данные могут быть разорваны или склеены).
  4. При обнаружении заголовка-1, -2, -n производится сохранение номера порта, с которого они поступили (возвращает структура tInfo).
  5. При завершении перебора производится отключение таймера и освобождение хэндлов портов для последущей работы.
  6. В потоке таймера реализован механизм отслеживания ('enports2') и автопереподключение выбранного COM-порта при отключении и подключении конвертора интерфесов.
  7. Функция 'get_num_comname2' обеспечивает определение номера/ов COM портов, сопоставленным заданному Friendly Name устройства или Friendly Name устройств/а по номеру порта. Т.е. в случае уникального имени устройства есть возможность определения к какому порту подключено.
type tf = class

 protected
  constructor Create;
  destructor Destroy;
 public
 procedure reinit(temp: string);
 procedure recSIG(Sender: TObject; count: integer);
 procedure ontmr(Sender: TObject);
end;

type
 TInfo = packed record
    gadget_name,
    gadget_numport: ansistring;
    cnt: integer;
    Section: array [0..100] of ansistring;
    signatura: array [0..100] of ansistring;
  end;
  pinfo = ^TInfo;


var vgt: tf;                     // общий класс
    tcom: tinfo;                 // данные СКАНИРОВАНИЯ
    comn:  TBComPort;
    ftimer: ttimer;
    life: boolean = false;       // объект comn существует
    gl_cnt: integer = 0;
    on_trigger: boolean = false; // триггер открытого COM

    sig1: array[0..3] of byte = ($AA, $55, $AA, $55); // заголовок 1
    sig2: array[0..7] of byte = ($FF, $FF, $FF, $FF, $AA, $55, $AA, $55); // заголовок 2

    procedure get_num_comname2(node: ansistring; _res: PInfo);
    function enports2(com: ansistring): boolean;

// --- end MY ----------------------------------------------------------------

implementation

procedure get_num_comname2(node: ansistring; _res: PInfo);
var
  _i:DWORD;
  Res: CONFIGRET;
  GUID: PGUID;
  Buffer: array [0..1023] of ansiCHAR;
  BufSize: DWORD;
  PnPHandle: HDEVINFOS;
  DevData: TSPDevInfoDataS;
  RS: LongBool;
  Devn: Integer;
  _DN,_PN:ULONG;
  s: ansistring;
  t1, t2, kkk: integer;
begin
 _i:=0;
 kkk:= 0;

  repeat
    GetMem(GUID, SizeOf(TGUID));

    Res := CM_Enumerate_ClassesS(_i, GUID^, 0);
    if Res <> $25 then begin
      SetupDiGetClassDescriptionS(GUID^, @Buffer[0], Length(Buffer), BufSize);
      // проверяем узел
      if pos(uppercase(node), Pansichar(@Buffer[0]))>0 then begin

       PnPHandle := SetupDiGetClassDevsS(@GUID^, nil, 0, DIGCF_PRESENT);
       if PnPHandle = INVALID_HANDLE_VALUE then Exit;

       Devn := 0;
       repeat
        DevData.cbSize := SizeOf(DevData);
        RS := SetupDiEnumDeviceInfoS(PnPHandle, Devn, DevData);
        if (RS) and (_DN<>$1) then begin
         s:= '';
         s:= GetDevName(PnPHandle, DevData);

         // ищем все порты
         if  (_res.gadget_numport = '')and(_res.gadget_name='') then begin
           //com0com - serial port emulator (COM6)
           t1:= pos(' (COM', uppercase(s)); // виртуальный COM
           if t1=0 then t1:= pos(' (LPT', uppercase(s)); // виртуальный LPT
           t2:= posex(')', s, t1) - t1-2;

           if pos(' (LPT', uppercase(s))=0 then begin
            _res.Section[kkk]:= copy(s, t1+2, t2);
            inc(kkk);
           end;
         end;
         // сопоставляем номер порта по имени устройства
         if  (_res.gadget_numport = '')and(_res.gadget_name<>'') then begin
          // проверяем субкласс
          if pos(uppercase(_res.gadget_name), uppercase(s))>0 then begin
           //com0com - serial port emulator (COM6)
           t1:= pos(' (COM', uppercase(s)); // виртуальный COM
           if t1=0 then t1:= pos('LPT', s); // виртуальный LPT
           t2:= posex(')', s, t1) - t1-2;
           _res.Section[kkk]:= copy(s, t1+2, t2);
           inc(kkk);
          end;
         end;
         // сопоставляем имя устройства по номеру порта
         if  (_res.gadget_numport <> '')and(_res.gadget_name='') then begin
          if pos(uppercase(_res.gadget_numport), uppercase(s))>0 then begin
           t1:= pos('(', s);
           _res.Section[kkk]:= pansichar(copy(s, 1, t1-2));
           inc(kkk);
          end;
         end;

         Inc(Devn);
        end;
        if Devn=0 then break;

       until not RS;
       SetupDiDestroyDeviceInfoListS(PnPHandle)
      end;
     end;
     Inc(_i);

     FreeMem(GUID);
   until Res = $25;

 _res.cnt:= kkk;
end;


function enports2(com: ansistring): boolean;
var
  _i:DWORD;
  Res: CONFIGRET;
  GUID: PGUID;
  Buffer: array [0..1023] of ansiCHAR;
  BufSize: DWORD;
  PnPHandle: HDEVINFOS;
  DevData: TSPDevInfoDataS;
  RS: LongBool;
  Devn: Integer;
  _DN,_PN:ULONG;
  s: ansistring;
  t1, t2, kkk: integer;
  tmp: boolean;
begin
 _i:=0;
 kkk:= 0;
 tmp:= false;

  repeat
    GetMem(GUID, SizeOf(TGUID));

    Res := CM_Enumerate_ClassesS(_i, GUID^, 0);
    if Res <> $25 then begin
      SetupDiGetClassDescriptionS(GUID^, @Buffer[0], Length(Buffer), BufSize);
      // проверяем узел
      if pos('COM', Pansichar(@Buffer[0]))>0 then begin

       PnPHandle := SetupDiGetClassDevsS(@GUID^, nil, 0, DIGCF_PRESENT);
       if PnPHandle = INVALID_HANDLE_VALUE then Exit;

       Devn := 0;
       repeat
        DevData.cbSize := SizeOf(DevData);
        RS := SetupDiEnumDeviceInfoS(PnPHandle, Devn, DevData);
        if (RS) and (_DN<>$1) then begin
         s:= GetDevName(PnPHandle, DevData);

         if pos(uppercase(com),s)>0
          then tmp:= true;

         Inc(Devn);
        end;
        if Devn=0 then break;

       until not RS;
       SetupDiDestroyDeviceInfoListS(PnPHandle)
      end;
     end;
     Inc(_i);

     FreeMem(GUID);
   until Res = $25;

  result:= tmp;
end;


// ----------------------------------------------------------------------------
// REC DATA - селекция заголовков
// ----------------------------------------------------------------------------
procedure Tf.recSIG(Sender: TObject; count: integer);
var buf: array[0..255] of byte;
    i,j: integer;
    pp1, pp2: integer;
begin
 pp1:= 0;
 pp2:= pp1;
 comn.Read(buf, count);

 // проверка идентификаторов байт 1-сигнатуры
 for i:= 0 to count-high(sig1)-1 do begin // ищем плавающим окном
  for j:= low(sig1) to high(sig1) do
   if buf[i+j] = sig1[j] then inc(pp1);
  if pp1=length(sig1) then break;
 end;

 // проверка идентификаторов байт 2-сигнатуры

 for i:= 0 to count-high(sig2)-1 do begin // ищем плавающим окном
  for j:= low(sig2) to high(sig2) do
   if buf[i+j] = sig2[j] then inc(pp2);
  if pp2=length(sig2) then break;
 end;

 if pp1=length(sig1) then tcom.signatura[gl_cnt]:= '1';
 if pp2=length(sig2) then tcom.signatura[gl_cnt]:= '2';
end;


procedure tf.reinit(temp: string);
begin
  if life then begin
   life:= false;
   // если порт был открыт, закрываем
   if on_trigger then begin
    on_trigger:= false;
    comn.Close;
   end;
   // уничтожаем объект
   freeandnil(comn);
  end;

  if not life then begin
   // динамическое создание
   comn:= TBComPort.Create(nil);
   comn.BaudRate  := br115200;
   comn.ByteSize  := bs8;
   comn.Parity    := paNone;
   comn.StopBits  := sb1;
   comn.SyncMethod:= smNone;
   comn.InBufSize := 1024;
   comn.OutBufSize:= 1024;

   comn.timeouts.ReadInterval:= 25; // время между окончанием приема пред. и началом след. байта
   comn.timeouts.ReadTotalMultiplier:= 100; // период = длительность байта + длительность интервала до следующего байта
   comn.timeouts.ReadTotalConstant:= 100;   // сколько добавить на всякий случай к (ReadTotalMultiplier * кол-во_ожидаемых_байт)
   //comn.timeouts.WriteTotalMultiplier:=50;
   //comn.timeouts.WriteTotalConstant:=100;
   comn.OnRxChar:= recSIG;    // пробуем принять сигнатуры (заголовки)
   comn.Port:= '\\.\' + temp; // номер порта

   life:= true;
   try  // пытаемся активировать
    comn.Open;

    on_trigger:= true; // флаг однозначного открытия порта
    comn.ClearBuffer(true, true);
   except on_trigger:= false; end
  end;

end;


// поток обработчика MM-таймера
//procedure ontmr(uTimerID, uMessage: uint;dwUser, dw1, dw2: dword) stdcall;
procedure TF.Ontmr(sender: tobject); // раз в 1000 мс
var p: boolean;
    s: ansistring;
    i: integer;
begin
 s:= tcom.Section[gl_cnt];
 // проверка существования порта методами SetupAPI
 p:= enports2(s);

 // если порт открыт и выдернули адаптер (порт не существует), то закрыть порт
 IF (life)and(not p) then begin
  life:= false;
  try
   freeandnil(comn);
  except end;
 end;
 // если порт существует и не подключен, то настроить и подключить
 if p then reinit(s);


 // перебираем найденные порты 1 раз/сек
 // ищем сигнатуры, останавливаем, отключаем сканирование
 inc(gl_cnt); if gl_cnt > tcom.cnt-1 then begin
  ftimer.Enabled:= false;
  gl_cnt:= 0; life:= false;
  freeandnil(comn);
 end;
end;


constructor TF.Create;
begin
 inherited Create;

 // скан COM-портов
 // реализован на SetupAPI без открытия портов (мгновенно), поскольку
 // CreateFile() тормозит с некоторыми драйверами виртуальных COM over USB
 // tcom.cnt - содержит кол-во портов
 // tcom.Section[xx] - содержит имя-номер порта СOMxx
 tcom.gadget_name:= '';
 tcom.gadget_numport:= '';
 get_num_comname2('com', @tcom); // будет осуществлен поиск ноды COM с любым subclass-name

 // задаем секундное ожидание DATA
 // для каждого из найденных COM-портов
 //_tmr:= timesetevent(1000, 0, ontmr, 0, 1);
 ftimer:= ttimer.Create(nil);
 ftimer.Interval:= 1000;
 ftimer.OnTimer:= ontmr;
 ftimer.Enabled:= true;
end;


destructor TF.Destroy;
begin
 inherited destroy;
 //timeKillEvent(_tmr);
 freeandnil(ftimer);
end;

initialization
 vgt:= tf.Create;
finalization
 vgt.Destroy;

end.

Комментариев нет:

Отправить комментарий

В комментариях уважайте собеседника, внимательно читайте посты и не додумывайте. Просьбы и предложения из разряда: «можно ваш Skype/Viber/телефон», «напишите мне в vk/FB», а также другие им подобные — игнорируются. Выход новых версий ПО, внешняя ссылка, переставшая работать с течением времени и т.п. не является основанием для претензий. Желающие спокойно подискутировать и высказаться — Welcome. Желающие спонсировать блог — Donate. Нарушение этих простых правил ведет к бану и удалению комментариев без предупреждения.