Delphi World - Асинхронный режим чтения из Com-порта
Delphi World - это проект, являющийся сборником статей и малодокументированных возможностей  по программированию в среде Delphi. Здесь вы найдёте работы по следующим категориям: delphi, delfi, borland, bds, дельфи, делфи, дэльфи, дэлфи, programming, example, программирование, исходные коды, code, исходники, source, sources, сорцы, сорсы, soft, programs, программы, and, how, delphiworld, базы данных, графика, игры, интернет, сети, компоненты, классы, мультимедиа, ос, железо, программа, интерфейс, рабочий стол, синтаксис, технологии, файловая система...
Асинхронный режим чтения из Com-порта

Оформил: DeeCo

Автор: Терехов Александр

Вступление

Порядок запуска и работы "службы" (назовем все описываемое ниже так) Com-портов состоит из нескольких достаточно хорошо описанных шагов ( см. статьи по теме ):
  1. Инициализация Com-порта посредством вызова функции CreateFile.
  2. Установка параметров Com-порта посредством последовательного вызова функций GetCommState и SetCommState, а также SetupComm.
  3. Установка параметров тайм-аутов для чтения и записи - GetCommTimeouts и SetCommTimeouts.
  4. Собственно записи в Com-порт - WriteFile и чтения из него - ReadFile.
  5. Закрытие порта по окончанию работ CloseHandle.
Очень большой сложности описанные выше шаги не представляют, однако реализация чтения данных из порта в асинхронном (неблокирующем) режиме заставляет почесать затылок. Об этом и поговорим.

Чтение из Com-порта.

Судя по контексту справки, касающейся функции CreateFile, для "отлова" момента поступления данных в Com-порт следует использовать функцию WaitCommEvent. Предварительно установив маску SetCommMask на то событие, которое хотелось бы отследить. Нужное событие наступает - вызываем функцию ReadFile для чтения поступающих данных.

Казалось бы все в порядке, но... Вызов функции WaitCommEvent насмерть тормозит приложение, пока какие-либо данные не поступят в Com-порт.

Можно конечно, просто взять и запустить в непрерывном цикле ReadFile, однако приложение хотя и будет как-то шевелиться, но это шевеление скорее всего будет напоминать предсмертные судороги.

Как выход из ситуации многие предлагают использовать потоки (thread), забывая при этом описать как это делать :)

Итак потоки.

В модуле Classes для потоков определен специальный класс TThread. Для создания потоков специалисты рекомендуют использовать именно его, а не создавать потоки используя BeginThread и EndThread, т.к. библиотека VCL не является защищенной для потоков в такой реализации. Следуя советам экспертов, для организации контроля поступающих данных в Com-порт и будем использовать готовый класс TThread.

В раздел interface определим тип переменных этого класса, переопределив только один метод класса - Execute, ну и дополнительно объявим свой метод, который и займется опросом Com-порта.

Далее в разделе глобальных переменных определим поток-переменную полученного выше типа
CommThread:TCommThread; //наш поток, в котором будет работать процедура опроса порта
Затем в разделе implementation начинаем ваять.
ВНИМАНИЕ!!!
К этому времени порт уже должен быть инициализирован функцией CreateFile.
  1. 1. Инициализируем поток, используя метод Create.
    type
      //определим тип TComThread - наследника класса TThread
      TCommThread = class(TThread)
      private
        //процедура, занимающаяся опросом порта
        procedure QueryPort;
      protected
        //переопределим метод запуска потока
        procedure Execute; override;
      end;
    
    procedure StartComThread;
    //инициализация нашего потока
    begin {StartComThread}
      //пытаемся инициализировать поток
      CommThread := TCommThread.Create(False);
      //проверяем получилось или нет
      if CommThread = nil then
      begin {Nil}
        //ошибка, все выключаем и выходим
        SysErrorMessage(GetLastError);
        fmMain.btnStop.Click;
        Exit;
      end; {Nil}
    end; {StartComThread}
    Куски кода взяты из файла проекта, поэтому нажимание на кнопку btnStop главной формы fmMain - это "примочки" примера, не обращайте внимания.

  2. Запускаем процедуру опроса порта в нашем потоке.
    procedure TCommThread.Execute;
    begin {Execute}
      repeat
        QueryPort; //процедура опроса порта будет производиться пока поток не будет прекращен
      until Terminated;
    end; {Execute}
  3. Реализуем асинхронные опрос порта и чтение из него данных
    procedure TCommThread.QueryPort;
    var
      MyBuff: array[0..1023] of Char; //буфер для чтения данных
      ByteReaded: Integer; //количество считанных байт
      Str: string; //вспомогательная строка
      Status: DWord; //статус устройства (модема)
    begin {QueryPort}
      //получим статус COM-порта устройства (модема)
      if not GetCommModemStatus(hPort, Status) then
      begin {ошибка при получении статуса модема}
        //ошибка, все выключаем и выходим
        SysErrorMessage(GetLastError);
        fmMain.btnStop.Click;
        Exit;
      end; {ошибка при получении статуса модема}
      //Обработаем статус устройства (модема) и будем включать(выключать) лампочки
      //готовность устройства (модема) получать данные
      fmMain.imgCTSOn.Visible := ((Status and MS_CTS_ON) = MS_CTS_ON);
      //готовность устройства (модема) к сеансу связи
      fmMain.imgDSROn.Visible := ((Status and MS_DSR_ON) = MS_DSR_ON);
      //принимаются данные с линии сигнала
      fmMain.imgRLSDOn.Visible := ((Status and MS_RLSD_ON) = MS_RLSD_ON);
      //входящий звонок
      fmMain.imgRingOn.Visible := ((Status and MS_RING_ON) = MS_RING_ON);
    
      //читаем буфер из Com-порта
      FillChar(MyBuff, SizeOf(MyBuff), #0);
      if not ReadFile(hPort, MyBuff, SizeOf(MyBuff), ByteReaded, nil) then
      begin {ошибка при чтении данных}
        //ошибка, все закрываем и уходим
        SysErrorMessage(GetLastError);
        fmMain.btnStop.Click;
        Exit;
      end; {ошибка при чтении данных}
      //данные пришли
      if ByteReaded > 0 then
      begin {ByteReaded>0}
        //посчитаем общее количество прочитанных байтов
        ReciveBytes := ReciveBytes + ByteReaded;
        //преобразуем массив в строку
        Str := string(MyBuff);
        //отправим строку на просмотр
        fmMain.Memo1.Text := fmMain.Memo1.Text + Str;
        //покажем количество считанных байтов
        fmMain.lbRecv.Caption := 'recv: ' + IntToStr(ReciveBytes) + ' bytes...';
      end; {ByteReaded>0}
    end; {QueryPort}

На этом по поводу использования потоков для считывания данных из Com-порта, пожалуй, все.

Прилагающийся пример

Следуя правилам хорошего тона, прикладываю ко всему написанному работающий пример.
В примере используется самое доступное устройство для пользователей интернет - модем (на Com-порту). В качестве "примочек" я использовал лампочки, которые включаются (или выключаются) при изменении статуса модема. Можно было прикрутить лампочки-детекторы входящих-выходящих сигналов, но вместо них используются счетчики байтов.
Реализация кода включения-выключения не самая лучшая: можно было бы использовать TImageList для хранения изображений лампочек. Но почему-то ??? (кто знает почему - напишите) использование ImageList.GetBitmap при наличии запущенного потока "подвешивает" приложение насмерть. Причем это происходит под Windows'98, если тоже самое делать под Windows'95, то все в порядке.

Для проверки работоспособности примера попробуйте понабирать AT-команды
  • ATZ - инициализировать модем
  • ATH - положить трубку
  • ATH1 - поднять трубку
  • ATS0=1 - включить автоподнятие трубки на первый сигнал
  • ATS0=0 - выключить автоподнятие трубки
  • ATDP_номер_телефона_интернет_провайдера - мне нравится больше всего :)
  • ATDP - набор в импульсном режиме, ATDT - набор в тоновом режиме


Да, еще. Проект написан под Delphi3, при использовании Delphi более свежих версий возможны ошибки "несовпадения типов".
В этим случае поменяйте типы "ошибочных" переменных с Integer на Cardinal.

Скачать проект — ComPort.zip (17K)

Проект Delphi World © Выпуск 2002 - 2017
Автор проекта: Эксклюзивные курсы программирования