7. Модемы и операционная система Windows

Данная глава книги будет посвящена программированию асинхронных последовательных адаптеров и модемов в среде операционной системы Windows. В ней вы найдете подробное описание функций программного интерфейса Windows, сообщений и структур данных, предназначенных для взаимодействия с устройствами (модемами, факс-модемами), подключенными к асинхронному последовательному адаптеру.

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

7.1. Драйвер асинхронного последовательного адаптера

Драйвер асинхронного последовательного адаптера представляет собой специальную библиотеку динамической компоновки. Он экспортирует набор функций, которые Windows может использовать для того, чтобы открыть порт, установить новый режим его работы, передать и принять данные, определить состояние порта и причину возникновения ошибок.

Однако приложения не обращаются непосредственно к этим функциям. Вместо этого они обращаются к функциям, экспортируемым из модуля USER, операционной системы Windows - OpenComm, CloseComm и т. д. Подробное описание данных функций представлено в следующем разделе книги.

Функции из модуля USER обращаются уже непосредственно к драйверу асинхронного последовательного адаптера.

В состав операционных систем Windows 3.1 и Windows for Worksgroups 3.11 входит драйвер асинхронного последовательного адаптера COMM.DRV. Этот драйвер позволяет получить доступ к портам COM1, COM2, COM3 и COM4. При этом драйвер использует для COM-портов адреса, записанные в области переменных (данных) BIOS.

Если область переменных BIOS не содержит информации о COM-портах, то базовые адреса и номера линии IRQ пользователь должен установить самостоятельно при помощи приложения Control Panel.

Введенная вами информация записывается в директивах COMxBase и COMxIRQ из раздела [386Enh] файла SYSTEM.INI. Данные из этих директив используются только в случае, когда в области переменных BIOS нет данных.

Драйвер асинхронного последовательного адаптера определяет тип микросхемы UART, на основе которой построен асинхронный последовательный адаптер. Если он обнаруживает UART 16550A, то включает аппаратный буфер FIFO, расположенный на нем.

В этом случае Windows устойчиво работает на скоростях 9600 бит/с и выше. Многие компьютеры, на которых не установлена эта микросхема, теряют информацию во время передачи данных и не могут установить связь на скорости выше 9600 бит/с.

7.2. Телекоммуникационные функции

Практически все функции, которые мы будем описывать в разделе "Телекоммуникационные функции", могут быть использованы для работы с параллельным адаптером. Однако из-за ограниченного объема книги мы не приводим сведений об использовании параллельных портов компьютера.

7.2.1. Функция OpenComm

Перед тем как приложение сможет начать работу с портом асинхронного последовательного адаптера, оно должно открыть этот порт при помощи функции OpenComm.

int OpenComm(LPCSTR lpszDevControl,
             UINT cbInQueue, UINT cbOutQueue);

Первый параметр функции lpszDevControl определяет открываемый порт и является указателем на строку, закрытую двоичным нулем, содержащую имя порта. Строка должна иметь формат "COMn" для асинхронного последовательного адаптера или "LPTn" для параллельного адаптера. Вместо символа n следует указать номер открываемого порта.

Как вы уже знаете из предыдущих разделов, COM-порт работает под управлением специального драйвера асинхронного последовательного адаптера. Драйвер принимает данные из порта и записывает их во входную очередь. Затем приложение, по мере надобности может прочитать данные из входной очереди. Когда приложение передает данные в COM-порт, они сначала попадают в выходную очередь драйвера, после чего драйвер передает их непосредственно асинхронному адаптеру.

При вызове функции OpenComm вы должны сами определить размер входной и выходной очереди драйвера асинхронного последовательного адаптера.

Параметр cbInQueue задает размер входной очереди COM-порта в байтах, а параметр cbOutQueue - размер выходной очереди в байтах.

В случае успешного выполнения функция возвращает число, определяющее открытый COM-порт. В дальнейшем оно будет использоваться практически всеми телекоммуникационными функциями. Мы будем называть его идентификатором COM-порта.

Если по какой-либо причине COM-порт не открыт, функция OpenComm возвращает отрицательное значение - код ошибки. В следующей таблице перечислены возможные значения, возвращаемые функцией в случае возникновения ошибки:

Значение Смысл
IE_BADID Неверный или неподдерживаемый идентификатор COM-порта
IE_BAUDRATE Установлена скорость передачи информации не поддерживается
IE_BYTESIZE Ошибка при определении размера передаваемых и принимаемых данных
IE_DEFAULT Не поддерживаются характеристики порта, принятые по умолчанию
IE_HARDWARE Порт недоступен. Возможно, порт уже используется другим приложением
IE_MEMORY Не хватает оперативной памяти для размещения входной и выходной очередей COM-порта
IE_NOPEN Порт не открыт
IE_OPEN Порт уже открыт

Функцию OpenComm можно использовать для того, чтобы узнать открыт ли данный COM-порт. Если перед вызовом функции OpenComm присвоить параметрам cbInQueue и cbOutQueue нулевые значения, то функция возвращает константу IE_OPEN, в случае, когда порт уже открыт или IE_MEMORY в противном случае.

В операционных системах Windows 3.1 и Windows for Worksgroups 3.11 можно использовать COM-порты с номерами от 1 до 9 (COM1-COM9) и параллельные порты от 1 до 3 (LPT1-LPT3). Если вы укажете номер порта, не поддерживаемый драйвером, функция OpenComm вернет код ошибки.

Сразу после открытия порта для него устанавливаются режим, принятый по умолчанию (скорость передачи информации, формат данных и т. д.). Чтобы изменить этот режим, необходимо воспользоваться функцией SetCommState.

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

//==========================================================
// Функция OpenCommPort
//==========================================================
int OpenCommPort(int nNumPort)
{
      // Временный буфер для создания имени порта
      char szTmpNamePort[10];

      wsprintf(szTmpNamePort, "COM%d", nNumPort);

      // Открываем COM-порт
      return OpenComm(szTmpNamePort, 8192, 8192);
}

Функция OpenCommPort самостоятельно формирует строку с текстовым именем открываемого COM-порта. Строка формируется с помощью функции wsprintf во временном буфере szTmpNamePort.

Размер входной и выходной очереди COM-порта задается равным 8192 байтам, чего вполне достаточно для простых приложений, не поддерживающих протоколы обмена файлами. Затем определенная нами функция возвращает идентификатор открытого порта или отрицательное число в случае ошибки.

7.2.2. Функция CloseComm

COM-порт является аппаратным ресурсом компьютера. Когда приложение открывает COM-порт, он становится недоступен для других приложений. Поэтому после использования порта, его следует освободить. Для этой цели нужно воспользоваться функцией CloseComm. Функция CloseComm имеет следующий прототип:

int CloseComm(int idComDev);

Единственный параметр idComDev должен содержать идентификатор порта асинхронного последовательного адаптера, который будет закрыт. Идентификатор порта асинхронного последовательного адаптера возвращает описанная выше функция OpenComm.

Функция возвращает ноль в случае успешного завершения или число, меньшее нуля, в случае ошибки. Одной из причин возникновения ошибки может быть неправильное значение параметра idComDev (т. е. если вы пытаетесь закрыть порт, который не был открыт.

Функция CloseComm закрывает определенный порт асинхронного последовательного адаптера и освобождает всю оперативную память, выделенную для входной и выходной очереди данных. Перед тем как порт будет закрыт, все символы из выходной очереди будут переданы в COM-порт. Чтобы закрыть порт, не дожидаясь передачи всех оставшихся в выходной очереди символов, можно воспользоваться функцией FlushComm, сбрасывающей содержимое очередей без передачи.

// Сбросить выходную очередь
FlushComm(nPortID,0);

// Сбросить входную очередь
FlushComm(nPortID,1);

// Закрыть COM-порт имеющий идентификатор nPortID
CloseComm(nPortID);

7.2.3. Первая программа

Теперь, когда мы рассмотрели первые две функции, предназначенные для управления портами асинхронного последовательного адаптера, приведем исходный текст приложения OPENCOMM, использующего эти функции.

Приложение OPENCOMM последовательно открывает и закрывает все порты асинхронного адаптера, начиная с COM1 до COM8. При этом оно определяет номера доступных портов и выводит на экран их список. Главный файл приложения OPENCOMM представлен в листинге 7.1.

Листинг 7.1. Файл OPENCOMM.CPP

// ============================================================
// Определение доступных COM-портов
// ============================================================

#define      STRICT
#include    <windows.h>

// Прототип функции
BYTE FAR FindCOMPorts( void );

// ============================================================
// Функция WinMain
// ============================================================
#pragma argsused

int PASCAL
WinMain( HINSTANCE hInstance,
                   HINSTANCE hPrevInstance,
                   LPSTR lpCmdLine,
                   int nCmdShow )
{
      char  szMsg[ 60 ] = "";
      BYTE  bFindPort;

      // Определяем доступные для использования COM-порты
      bFindPort = FindCOMPorts();

      // Формируем строку результата с названиями доступных портов
      for(int nBitNum = 0; nBitNum < 8; nBitNum++)
      {
             if (bFindPort & (BYTE) (1 << (BYTE)nBitNum))
             wsprintf( szMsg, "%sCOM%d ", (LPSTR) szMsg, nBitNum + 1 ) ;
      }

      // Отображаем на экране сообщение с названиями доступных
      // COM-портов
      MessageBox(NULL, szMsg, "Обнаружены COM-порты:",
                           MB_OK | MB_ICONINFORMATION);

      return 0;
}

// ============================================================
// Функция FindCOMPorts вызывает функцию OpenComm
// и определяет, какие COM-порты установлены в компьютере и
// доступны для использования
// ============================================================
BYTE FAR FindCOMPorts( void )
{
      // Буфер для подготовки имени порта
      char     szCommPattern[8];
      // Идентификатор COM-порта
      int  idComDev;
      // Переменная для формирования результата
      BYTE  bFindPort = 0;
      // Вспомогательная переменная nBitNum
      int  nBitNum = 1;

      // Пробуем открыть COM-порты COM1 - COM8
      for(int i = 0; i < 8; i++) {

             wsprintf( szCommPattern, "COM%d", i + 1 ) ;

             // Открываем COM-порт
             idComDev = OpenComm(szCommPattern, 1024, 1024);

             if (idComDev >= 0)
             {
                   // Если COM-порт n успешно открыт, тогда устанавливаем
                   // в переменной bFindPort n-ый бит
                   bFindPort = bFindPort | nBitNum;

                   // Закрываем COM-порт
                   CloseComm(idComDev);
             }
             nBitNum <<= 1;
      }

      // Возвращаем результат
      return bFindPort;
}

После запуска приложения OPENCOMM, функция WinMain вызывает функцию FindCOMPorts, определяющую список доступных COM-портов.

Функция FindCOMPorts содержит цикл, в котором открывается очередной COM-порт:

idComDev = OpenComm(szCommPattern, 1024, 1024);

Если OpenComm возвращает значение большее или равное нулю, значит COM-порт успешно открыт. В этом случае мы устанавливаем в байте bFindPort бит с номером, соответствующим номеру открытого порта. После этого мы закрываем только что открытый порт:

CloseComm(idComDev);

Затем мы переходим к проверке следующего порта. Если функция OpenComm возвращает значение, меньшее нуля, мы считаем, что порт недоступен.

Проверив в цикле COM-порты от COM1 до COM8, функция FindCOMPorts возвращает байт bFindPort. Каждый бит этого байта отвечает за свой COM-порт.

Установлен бит Порт доступен
0 COM1
1 COM2
2 COM3
3 COM4
4 COM5
5 COM6
6 COM7
7 COM8

Получив байт bFindPort, WinMain формирует в строке szMsg список доступных портов и выводит его на экран с помощью функции MessageBox (см. рис. 7.1).

Рис 7.1. Список доступных COM-портов

Файл определения модуля приложения OPENCOMM приведен в листинге 7.2.

Листинг 7.2. Файл OPENCOMM.DEF

; =============================================================
; Файл определения модуля
; =============================================================

NAME OPENCOMM
DESCRIPTION 'Приложение OPENCOMM, (C) 1994, Frolov G.V.'
EXETYPE windows
STUB 'winstub.exe'
STACKSIZE  5120
HEAPSIZE  1024
CODE preload moveable discardable
DATA preload moveable multiple

7.2.4. Определение доступных портов

Драйвер асинхронного последовательного адаптера VCD предоставляет приложениям Windows программный интерфейс через точку входа, адрес которой можно определить, вызвав мультиплексное прерывание INT 2Fh.

Реализация этих функций зависит от конкретного драйвера VCD. Если у вас установлен драйвер, не совместимый с драйвером VCD из DDK, то эти функции могут не работать.

В листинге 7.3. мы приводим исходный текст приложения FINDPORT, которое определяет порты асинхронного последовательного адаптера, доступные приложениям Windows, посредством вызова функции драйвера VCD.

Листинг 7.3. Файл FINDPORT.CPP

#define            STRICT
#include          <windows.h>
#include          <bwcc.h>

// Прототип функции
BYTE FAR FindCOMPorts( void );

// =========================================================
// Функция WinMain
// =========================================================
#pragma argsused

int PASCAL
WinMain( HINSTANCE hInstance,
                   HINSTANCE hPrevInstance,
                   LPSTR lpCmdLine,
                   int nCmdShow )
{
      char  szMsg[ 60 ] = "";
      BYTE  bFindPort;

      // Определяем работает ли Windows в расширенном режиме
      if(GetWinFlags() & WF_ENHANCED)
      {
             bFindPort = FindCOMPorts();

             for (int i = 0; i < 8; i++)
             {
                   if (bFindPort & (BYTE) (1 << (BYTE)i))
                                wsprintf( szMsg, "%sCOM%d ", (LPSTR) szMsg, i + 1 ) ;
             }

             BWCCMessageBox(NULL,
                   szMsg, "Обнаружены COM-порты:",
                   MB_OK | MB_ICONINFORMATION);
      }

      else
             BWCCMessageBox(NULL,
                   "Зпустите Windows в расширенном режиме", "Сообщение",
                   MB_OK | MB_ICONSTOP);

      return 0;
}

После запуска приложения, WinMain вызывает функцию GetWinFlags для определения режима работы Windows. Если Windows работает в стандартном режиме, приложение выводит соответствующее сообщение и завершает свою работу.

Если Windows работает в расширенном режиме, выполняется вызов функции FindCOMPorts, определяющей список доступных COM-портов. Затем полученный список доступных портов отображается на экране.

Функция FindCOMPorts возвращает байт, каждый бит которого отвечает за свой COM-порт. Младший бит соответствует порту COM1, а старший COM8. Исходный текст функции FindCOMPorts представлен в листинге 7.4.

Листинг 7.4. Файл COMM_DRV.CPP

#include    <windows.h>
#include    "comm_drv.h"

// Идентификатор драйвера асинхронного последовательного
// адаптера
#define      VCD_Device_ID  0x0E

// ===========================================================
// Функция FindCOMPorts вызывает драйвер асинхронного адаптера
// и определяет какие COM-порты установлены в компьютере и
// доступны для использования
// ===========================================================
BYTE FAR FindCOMPorts( void )
{
      BYTE     bResult;
      FARPROC  lpCOMM;

      asm {

             // Определяем адрес точки входа программного интерфейса
             // драйвера асинхронного последовательного адаптера
             mov   ax, 1684h
             mov   bx, VCD_Device_ID
             xor   di, di
             mov   es, di
             int   2Fh

             // Проверяем значение регистров ES:DI
             mov   ax, es
             or    ax, di

             // Если в регистрах ES:DI записаны нулевые значения,
             // драйвер не имеет программного интерфейса и не доступен
             jz    FunctionFailed

             // Драйвер доступен, сохраняем адрес его интерфейса в
             // переменной lpCOMM
             mov   word ptr lpCOMM,      di
             mov   word ptr lpCOMM+2, es

             // Вызываем функцию из драйвера COM-порта
             mov   dx, 1
             call  dword ptr lpCOMM

             // Сохраняем результат, полученный в регистре AL
             mov   bResult, al

             // Завершаем выполнение функции
             jmp   FunctionQuit
      }

      FunctionFailed:

      asm {

             // В случае возникновения ошибки возвращаем нулевое
             // значение
             mov   bResult, 0
      }

      FunctionQuit:

      return( bResult ) ;
}

В исходном тексте функции FindCOMPorts нет ни вызова функции OpenComm, ни вызовов других телекоммуникационных функций, она содержит только десяток строк на языке ассемблера. Рассмотрим функцию FindCOMPorts подробней.

Сначала вызывается функция 1684h мультиплексного прерывания INT 2Fh. Для нее в регистре BX передается константа VCD_Device_ID, означающая, что необходимо получить точку входа для драйвера VCD.

mov   ax, 1684h
mov   bx, VCD_Device_ID
xor   di, di
mov   es, di
int   2Fh

Если после вызова мультиплексного прерывания в регистрах ES:DI сохраняются нулевые значения, значит драйвер VCD не поддерживает программный интерфейс и, следовательно, мы не можем получить к нему доступ. В этом случае возвращаем нулевое значение.

Если содержимое регистров ES:DI не равно нулю, значит драйвер VCD доступен. Сохраняем адрес точки входа его интерфейса в переменной lpCOMM:

mov   word ptr lpCOMM,   di
mov   word ptr lpCOMM+2, es

Затем вызываем функцию из драйвера VCD, предварительно записав в регистр DX значение 1.

mov   dx, 1
call  dword ptr lpCOMM

После возвращения из этой функции по содержимому регистра AL можно определить, какие из портов асинхронного последовательного адаптера доступны для использования.

Затем мы завершаем выполнение функции FindCOMPorts и возвращаем значение регистра AL.

Файл определения модуля приложения FINDPORT приведен в листинге 7.5.

Листинг 7.5. Файл FINDPORT.DEF

; =============================================================
; Файл определения модуля
; =============================================================

NAME OPENCOMM
DESCRIPTION 'Приложение FINDPORT, (C) 1994, Frolov G.V.'
EXETYPE windows
STUB 'winstub.exe'
STACKSIZE  5120
HEAPSIZE  1024
CODE preload moveable discardable
DATA preload moveable multiple

7.2.5. Функция SetCommState

Итак, мы научились открывать и закрывать COM-порт. Однако при вызове функции OpenComm мы указали только его имя и размеры входной и выходной очереди. Ни одной характеристики COM-порта - скорости передачи или формата данных мы не указывали.

Дело в том, что функция OpenComm устанавливает для открываемого порта скорость обмена, формат данных и другие характеристики, принятые по умолчанию. Чтобы изменить эти характеристики, необходимо воспользоваться функцией SetCommState.

Функция SetCommState устанавливает новый режим COM-порта, задаваемый полями структуры DCB, передаваемой ей в качестве одного из параметров.

int SetCommState(const DCB FAR* lpdcb);

Параметр lpdcb является дальним указателем на структуру DCB, содержащую описание устанавливаемого режима COM-порта. Подробное описание полей этой структуры мы привели чуть ниже.

Если функция SetCommState успешно установила новый режим работы COM-порта, она возвращает нулевое значение. В противном случае функция возвращает отрицательное значение.

Функция проводит повторную инициализацию всего аппаратного обеспечения COM-порта, но не сбрасывает входную и выходную очереди. Поэтому символы, расположенные во входной и выходной очередях, не будут удалены. В том случае, если желательно удалить эти символы и очистить очереди, следует воспользоваться функцией FlushComm.

7.2.6. Структура DCB

Перейдем к подробному описанию структуры DCB (Device Control Block). Структура DCB содержит информацию, определяющую различные характеристики портов последовательного асинхронного адаптера. В структуре DCB определяется скорость передачи данных, количество бит данных и стоповых бит в передаваемых символах, устанавливается контроль четности и режим управления потоком.

Структура DCB определена в файле WINDOWS.H следующим образом:

typedef struct tagDCB
{
      BYTE  Id;
      UINT  BaudRate;
      BYTE  ByteSize;
      BYTE  Parity;
      BYTE  StopBits;

      UINT   RlsTimeout;
      UINT   CtsTimeout;
      UINT   DsrTimeout;

      UINT   fBinary       :1;
      UINT   fRtsDisable   :1;
      UINT   fParity       :1;
      UINT   fOutxCtsFlow  :1;
      UINT   fOutxDsrFlow  :1;

      UINT   fDummy        :2;
      UINT   fDtrDisable   :1;

      UINT   fOutX         :1;
      UINT   fInX          :1;
      UINT   fPeChar       :1;
      UINT   fNull         :1;
      UINT   fChEvt        :1;

      UINT   fDtrflow      :1;
      UINT   fRtsflow      :1;
      UINT   fDummy2       :1;

      char   XonChar;
      char   XoffChar;
      UINT   XonLim;
      UINT   XoffLim;
      char   PeChar;

      char   EofChar;
      char   EvtChar;
      UINT   TxDelay;
} DCB;

В файле WINDOWS.H определен также тип LPDCB - дальний указатель на структуру типа DCB:

typedef DCB FAR* LPDCB;

Опишем назначение отдельных полей структуры DCB.

Поле Id

Содержит идентификатор COM-порта. Значение поля устанавливается функцией BuildCommDCB, описанной ниже, и содержит то же самое значение, которое возвращает функция OpenComm при открытии этого порта.

Если старший бит поля Id установлен, данная структура DCB описывает порт параллельного адаптера.

Поле BaudRate

Поле BaudRate определяет скорость передачи, с которой работает COM-порт. Скорость можно задать двумя способами. Вы можете записать в это поле либо одну из констант CBR_, определенных в файле WINDOWS.H, либо абсолютное значение скорости передачи информации.

Список констант CBR_ и соответствующие им значения скорости перечислены в представленной ниже таблице.

Константа Значение Скорость передачи информации, бит/с
CBR_110 0xFF10 110
CBR_300 0xFF11 300
CBR_600 0xFF12 600
CBR_1200 0xFF13 1200
CBR_2400 0xFF14 2400
CBR_4800 0xFF15 4800
CBR_9600 0xFF16 9600
CBR_14400 0xFF17 14400
CBR_19200 0xFF18 19200
CBR_38400 0xFF1B 38400
CBR_56000 0xFF1F 56000
CBR_128000 0xFF23 128000
CBR_256000 0xFF27 256000

Как видите, старший байт для констант CBR_ содержит значение 0xFF. Поэтому, если вы желаете записать в поле BaudRate абсолютное значение скорости, указывайте числа, меньшие, чем значение константы CBR_110 (0xFF10), и большие 2.

Если вы запишите в поле BaudRate неправильное значение, функция SetCommState завершится с ошибкой IE_BAUDRATE.

Поле ByteSize

В поле ByteSize определяется количество бит в символах, передаваемых и принимаемых через COM-порт. Это поле может содержать любое значение от 4 до 8. Если вы запишете в него другое значение, то функция SetCommState завершится с ошибкой IE_BYTESIZE.

Поле Parity

Поле Parity управляет контролем четности и может содержать одну из пяти констант, перечисленных ниже:

Значение Смысл
EVENPARITY Выполняется проверка на четность
MARKPARITY Бит четности всегда установлен
NOPARITY Проверка на четность не выполняется
ODDPARITY Выполняется проверка на нечетность
SPACEPARITY Бит четности всегда сброшен

Если записать в это поле другое значение, функция SetCommState вернет код ошибки IE_DEFAULT.

Поле StopBits

Поле StopBits задает количество стоповых бит. Поле может содержать одну из констант, перечисленных ниже:

Значение Смысл
ONESTOPBIT 1 стоповый бит
ONE5STOPBITS 1.5 стоповых бита
TWOSTOPBITS 2 стоповых бита

Если записать в это поле другое значение, то функция SetCommState вернет код ошибки IE_DEFAULT.

Поле RlsTimeout

Поле RlsTimeout определяет максимальный интервал времени (тайм-аут), в течение которого функция WriteComm будет ожидать появления сигнала RLSD (ранее обозначался нами как DCD) перед тем, как записать данные в очередь передатчика. Интервал времени задается в миллисекундах.

Вы можете записать в эти поля константы INFINITE или IGNORE, определенные в файле WINDOWS.H как 0xFFFF и 0x0, соответственно.

INFINITE можно перевести как бесконечный, однако это не означает, что функция WriteComm будет бесконечно долго ждать сигнал RLSD. На самом деле эта константа задает интервал времени примерно 65 секунд.

IGNORE означает, что WriteComm не ожидает сигнал RLSD.

Если интервал ожидания истечет, а сигнал RLSD так и не установится, функция WriteComm вернет нулевое значение. Чтобы узнать причину ошибки, вызовите функцию GetCommError. Если функция WriteComm возвратила управление, после того как истек тайм-аут, GetCommError возвратит значение CE_RLSDTO.

Поле CtsTimeout

Поле CtsTimeout определяет максимальный интервал времени (тайм-аут), в течение которого функция WriteComm будет ожидать появления сигнала CTS перед тем, как записать данные в очередь передатчика. Интервал времени задается в миллисекундах.

Вы можете записать в эти поля константы INFINITE или IGNORE. Назначение этих констант уже было рассмотрено нами при описании поля RlsTimeout.

Если интервал ожидания истечет, а сигнал CTS не установится, функция WriteComm вернет нулевое значение. Чтобы узнать причину ошибки, вызовите функцию GetCommError. Если функция WriteComm возвратила управление, после того как истек тайм-аут, GetCommError возвратит значение CE_CTSTO.

Поле DsrTimeout

Поле DsrTimeout определяет максимальный интервал времени (тайм-аут), в течение которого функция WriteComm будет ожидать появления сигнала DSR перед тем, как записать данные в очередь передатчика. Интервал времени задается в миллисекундах.

Вы можете записать в эти поля константы INFINITE или IGNORE. Назначение этих констант было уже рассмотрено при описании поля RlsTimeout.

Если интервал ожидания истечет, а сигнал DSR не установится, функция WriteComm вернет нулевое значение. Чтобы узнать причину ошибки, вызовите функцию GetCommError. Если функция WriteComm возвратила управление, после того как истек тайм-аут, GetCommError возвратит значение CE_DSRTO.

Поле fBinary

Поле fBinary, которое используется как флаг, устанавливает режим передачи двоичной информации. Если это поле содержит значение FALSE (ноль), принятый символ EOF распознается как сигнал для окончания приема данных. Символ EOF определяется полем EofChar структуры DCB.

После того как функция ReadComm прочтет символ EOF из входной очереди, она возвращает управление, не прочитав остальные данные. Если во входном буфере остались непрочитанные данные, они распознаются как переполнение. В этом случае функция GetCommError вернет код ошибки CE_RXOVER.

При получении символа EOF в структуре COMSTAT устанавливается флаг CSTF_EOF.

Если в поле fBinary записано значение TRUE (единица), символ EOF обрабатывается так же, как и остальные символы. Это позволяет беспрепятственно передавать через COM-порт не только текстовые, но также и двоичные данные.

Поле fRtsDisable

Поле fRtsDisable определяет, будет ли использоваться сигнал RTS. Если это флаг установлен, сигнал RTS не используется и все время остается равен нулю. Если поле fRtsDisable содержит нулевое значение (FALSE), сигнал RTS устанавливается, когда COM-порт открывается и сбрасывается, когда порт закрывается.

Чтобы использовать сигнал RTS для управления потоком, необходимо записать в поле fRtsDisable нулевое значение, а в поле fRtsFlow - единицу. Другие комбинации значений fRtsDisable и fRtsFlow блокируют управление потоком по линии RTS.

Ниже перечислены различные комбинации полей fRtsDisable и fRtsFlow:

Поле fRtsDisable Поле fRtsFlow Состояние сигнала RTS
0 0 Установлен
0 1 Разрешено управление потоком по линии RTS
1 0 Сброшен
1 1 Сброшен

Если сигналы RTS и DTR не используются для управления потоком, их можно устанавливать и сбрасывать с помощью функции EscapeCommFuntion.

Поле fParity

Поле fParity определяет, будет ли выполняться проверка принимаемой и передаваемой информации на четность. Если в этом поле записано значение TRUE, проверка на четность выполняется.

Если функция ReadComm обнаружит ошибку четности, она возвращает отрицательную величину. В этом случае вы должны воспользоваться функцией GetCommError, чтобы определить причину и сбросить флаг ошибки CE_RXPARITY.

Чтобы при обнаружении ошибки по четности в функцию окна поступало сообщение EV_ERR, воспользуйтесь функцией SetCommEventMask.

Поле fOutxCtsFlow

Поле fOutxCtsFlow определяет, что сигнал CTS используется для управления выходным потоком данных. Если это поле содержит значение TRUE и сигнал CTS выключен, передача информации приостанавливается до тех пор, пока сигнал CTS не установится снова.

Если поле CtsTimeout содержит величину, отличную от нуля, функция WriteComm будет ожидать появления сигнала CTS вне зависимости от значения поля fOutxCtsFlow.

Поле fOutxDsrFlow

Поле fOutxDsrFlow определяет, что сигнал DSR используется для управления выходным потоком. Если это поле содержит значение TRUE и сигнал DSR выключен, передача информации приостанавливается до тех пор, пока сигнал CTS снова не установится.

Если поле DsrTimeout содержит величину, отличную от нуля, функция WriteComm будет ожидать появления сигнала DSR вне зависимости от значения поля fOutxDsrFlow.

Поле fDummy

Поле fDummy зарезервировано.

Поле fDtrDisable

Поле fDtrDisable определяет, будет ли использоваться сигнал DTR. Если это флаг установлен, сигнал DTR не используется и остается равен нулю. Если поле fDtrDisable содержит нулевое значение (FALSE), сигнал DTR устанавливается, когда COM-порт открыт, и сбрасывается, когда порт закрывается.

Обычно поле fDtrDisable используют совместно с fDtrFlow. Их использование аналогично полям fRtsDisable и fRtsFlow.

Поле fOutX

Поле fOutX включает протокол управления потоком XON/XOFF. Если это поле установлено, передача информации приостанавливается, когда принимается символ XoffChar, и возобновляется при получении сигнала XonChar.

Если передача данных прерывается в результате приема символа XOFF, в структуре COMSTAT устанавливается флаг CSTF_XOFFHOLD.

Поле fInX

Поле fInX устанавливает протокол управления потоком XON/XOFF при приеме данных. Если это поле установлено, то символ XoffChar передается, когда в очереди приемника становится больше, чем XoffLim символов и символ XonChar, когда в очереди приемника становится меньше, чем XonLim символов.

Если передача данных прерывается в результате передачи символа XOFF, в структуре COMSTAT устанавливается флаг CSTF_XOFFSENT.

Поле fPeChar

Если поле fPeChar содержит значение TRUE, каждый символ, полученный с ошибкой по четности, будет заменен на символ, заданный полем PeChar.

Поле fNull

Если поле fNull содержит значение TRUE, все принятые из COM-порта символы, имеющие нулевое значение, будут пропускаться.

Поле fChEvt

При установке поля fChEvt, получение символа EvtChar отмечается как событие EV_RXFLAG. В действительности поле fChEvt не используется. Необходимо вызвать функцию SetCommEventMask, чтобы разрешить это событие.

Поле fDtrflow

Поле fDtrflow определяет, что сигнал DTR используется для протокола управления потоком (при приеме данных). Если этот флаг установлен, сигнал DTR выключается, когда в приемной очереди становится больше чем XoffLim символов, и включается снова, когда в приемной очереди становится меньше, чем XonLim символов.

Такой механизм позволяет предотвратить переполнение входной очереди COM-порта, которое может вызвать потерю принятых данных.

Поле fRtsflow

Поле fRtsflow определяет, что сигнал RTS используется для управления потоком (при приеме данных). Если это поле установлено, сигнал RTS выключается, когда в приемной очереди становится больше чем XoffLim символов и включается, когда в приемной очереди становится меньше, чем XonLim символов.

Поле fDummy2

Поле fDummy2 зарезервировано.

Поле XonChar

Поле XonChar содержит значение символа XON для передачи и для приема. Смотри описание полей fInX и fOutX.

Функция BuildCommDCB записывает в это поле значение 0x11 (<CTRL+Q>).

Поле XoffChar

Поле XoffChar определяет значение символа XOFF для передачи и для приема. Смотри описание полей fInX и fOutX.

Функция BuildCommDCB, описанная ниже, записывает в это поле значение 0x13 (<CTRL+S>).

Если вы решили изменить принятые по умолчанию значения полей XonChar и XoffChar, следует обратить внимание на то, что они должны содержать различные величины. В противном случае управление потоком при помощи символов XON/XOFF будет работать неправильно и обмен данными может прекратиться.

Поле XonLim

Поле XonLim определяет минимально допустимое число символов в приемной очереди, при принижении которого передается символ XON (если включен протокол XON/XOFF) и устанавливается сигнал DTR (если включен протокол DTR).

Функция BuildCommDCB записывает в это поле значение 0x10.

Поле XoffLim

Поле XoffLim определяет максимально допустимое число символов в приемной очереди, при превышении которого передается символ XOFF (если включен протокол) и сбрасывается сигнал DTR (если включен протокол).

Чтобы определить максимально допустимое количество символов в очереди, надо вычесть из размера очереди приемника значение поля XoffLim.

Функция BuildCommDCB записывает в это поле значение 0x10. Для изменения значения, записанного в поле, можно воспользоваться функцией SetCommState.

Поле PeChar

Поле PeChar задает значение символа, используемого для замещения символов, принятых с ошибкой по четности.

Функция BuildCommDCB записывает в это поле значение 0x0. Смотри поле fPeChar и fParity.

Поле EofChar

Поле EofChar определяет символ EOF, используемый при передаче сигнала "конец данных".

Функция BuildCommDCB записывает в это поле значение 0x0. Смотри поле fBinary.

Поле EvtChar

Поле EvtChar определяет символ, используемый для передачи сигнала событие (EV_RXFLAG). Когда принимается символ EvtChar, генерируется событие EV_RXFLAG. В действительности флаг fChEvt не используется. Необходимо вызвать функцию SetCommEventMask, чтобы разрешить это событие.

Функция BuildCommDCB записывает в это поле значение 0x0.

Поле TxDelay

Поле TxDelay не используется в Windows 3.1 и Windows for Workgroups 3.11.

Как заполнить или модифицировать структуру DCB

На этом мы заканчиваем описание структуры DCB. В заключение приведем несколько советов по модификации полей этой структуры.

Структура DCB заполняется двумя функциями - OpenComm и BuildCommDCB. Однако существует много полей в структуре DCB, которые нельзя изменить с помощью этих функций. Для этого надо непосредственно записать новые значения в поля структуры, а затем воспользоваться функцией SetCommState, чтобы внесенные изменения отразились на работе COM-порта.

Если вам нужно изменить какое-либо поле этой структуры, следует сначала узнать текущее значение полей структуры DCB, с помощью функции GetCommState. Затем внесите все необходимые изменения в поля структуры и вызовите функцию SetCommState, чтобы соответственно изменился режим COM-порта.

Такой алгоритм гарантирует, что остальные поля структуры DCB перед вызовом SetCommState будут содержать правильные значения.

В качестве альтернативного способа изменения структуры DCB можно воспользоваться функцией BuildCommDCB. Этой функции передается в качестве параметра строка, имеющая формат команды MODE операционной системы MS-DOS. Данная строка может содержать значения скорости передачи данных, количества бит данных и стоповых бит в слове.

Функция BuildCommDCB заполняет структуру DCB, но не изменяет состояния COM-порта. Для этого следует воспользоваться функцией SetCommState.

В начале работы с COM-портом приложение должно открыть его с помощью функции OpenComm. Функция OpenComm с помощью функции BuildCommDCB заполняет временный, недоступный программисту, блок DCB значениями по умолчанию. Скорость обмена устанавливается равной 9600 бит/с., проводится проверка на четность, передаваемые символы содержат 7 информационных и один стоповый бит. Затем функция OpenComm выполняет внутренний вызов функции SetCommState с подготовленной структурой DCB, устанавливая начальный режим COM-порта. Затем можно создать собственную структуру DCB и установить новый режим COM-порта, вызвав функцию SetCommState.

Функция SetCommState принимает в качестве параметра указатель на заполненную структуру DCB и устанавливает режим работы COM-порта в соответствии с данными из этой структуры.

Вы также можете узнать текущее состояние COM-порта с помощью функции GetCommState.

7.2.7. Функция BuildCommDCB

Функция BuildCommDCB заполняет структуру DCB в соответствии с переданной ей строкой параметров. Структура DCB содержит управляющую информацию, необходимую для установки режима работы портов последовательного асинхронного адаптера (см. раздел "Структура DCB").

int BuildCommDCB(LPCSTR lpszDef, DCB FAR* lpdcb);

Параметр lpszDef является дальним указателем на строку символов, закрытую двоичным нулем, которая должна содержать команды установки режима COM-порта. Формат этой строки полностью соответствует параметрам команды MODE операционной системы MS-DOS.

Например, параметр lpszDef может указывать на строку "COM1:9600,n,8,1". В этом случае после вызова функции BuildCommDCB заполненная структура DCB позволит установить порт COM1 в режим обмена данными со скоростью 9600 бит/с, без проверки на четность с форматом передаваемых символов из 8 бит данных и одним стоповым битом.

Параметр lpdcb является дальним указателем на структуру типа DCB. В нее будет записана информация, полученная после интерпретации строки lpszDef.

Функция BuildCommDCB возвращает ноль, если команды, указанные в параметре lpszDef, успешно интерпретированы и структура DCB заполнена. В случае возникновения ошибки, например, в формате команд, функция возвращает -1.

Функция BuildCommDCB только заполняет поля структуры DCB. Чтобы установить режим порта последовательного асинхронного адаптера в соответствии с заполненной структурой DCB, необходимо воспользоваться функцией SetCommState.

С помощью функции BuildCommDCB вы можете управлять далеко не всеми характеристиками COM-порта, определяемыми структурой DCB. Так как формат команд строки lpszDef соответствует команде MODE MS-DOS, то с помощью BuildCommDCB можно изменить только скорость передачи информации, режим проверки на четность и формат передаваемых данных (число стоповых бит и бит данных в передаваемых символах).

Остальные поля структуры DCB функция BuildCommDCB заполняет по собственному усмотрению (см. описание полей структуры DCB в разделе "Структура DCB"). Так, BuildCommDCB запрещает управление потоком данных между модемом и компьютером на аппаратном уровне (сигналы CTS и RTS) и на программном уровне (с помощью символов XON и XOFF).

Кроме того, функция BuildCommDCB позволяет определить только символы длиной 7 или 8 бит. Другие значения вызывают ошибку.

Таким образом, функция BuildCommDCB может быть полезна только в самых простых случаях или для начального заполнения структуры DCB. В большинстве случаев вам потребуется самостоятельно заполнять поля этой структуры.

7.2.8. Функция SetCommBreak

Функция SetCommBreak прекращает передачу данных и переводит COM-порт в состояние BREAK. COM-порт находится в состоянии BREAK до тех пор, пока приложение не вызовет функцию ClearCommBreak.

int SetCommBreak(int idComDev);

Параметр idComDev должен содержать идентификатор COM-порта, переводимого в состояние BREAK. В качестве параметра idComDev необходимо использовать величину, возвращаемую функцией OpenComm.

При успешном завершении функция возвращает ноль. Если функция возвращает значение меньшее нуля, произошла ошибка.

7.2.9. Функция ClearCommBreak

Функция ClearCommBreak восстанавливает обмен данными через порт асинхронного последовательного адаптера, прерванный ранее вызовом функции SetCommBreak.

int ClearCommBreak(int idComDev);

Единственный параметр функции idComDev должен содержать идентификатор порта асинхронного последовательного адаптера, для которого вызывается функция.

Функция возвращает ноль в случае успешного завершения или -1, если параметр idComDev задан неправильно, то есть не соответствует ни одному открытому порту асинхронного последовательного адаптера.

7.2.10. Функция EnableCommNotification

Функция EnableCommNotification разрешает или запрещает передачу сообщения WM_COMMNOTIFY в функцию окна приложения. Более подробную информацию о сообщении WM_COMMNOTIFY можно получить, прочитав раздел "Сообщение WM_COMMNOTIFY".

Прототип функции EnableCommNotification представлен ниже.

BOOL EnableCommNotification(int idComDev, HWND hwnd,
               int cbWriteNotify, int cbOutQueue);

Параметр idComDev является идентификатором порта, который будет передавать сообщения WM_COMMNOTIFY в функцию окна. Для параметра idComDev необходимо использовать значение, полученное от функции OpenComm.

Параметр hwnd определяет окно, функции которого будет передаваться сообщение WM_COMMNOTIFY. Если этот параметр содержит значение NULL, функция EnableCommNotification запрещает передачу сообщения WM_COMMNOTIFY в функцию текущего окна.

Параметр cbWriteNotify определяет количество байт, которое драйвер асинхронного последовательного адаптера должен записать во входную очередь перед тем, как будет передано сообщение WM_COMMNOTIFY, у которого в младшем слове параметра lParam записан код извещения CN_RECEIVE . Данное сообщение служит сигналом приложению, что пора прочитать данные из входной очереди COM-порта.

Последний параметр cbOutQueue задает минимальное количество символов (байт) в очереди передатчика. Если в очереди передатчика становится меньше байт, чем определено параметром cbOutQueue, драйвер асинхронного последовательного адаптера передает приложению сообщение WM_COMMNOTIFY, у которого в младшем слове параметра lParam записан код извещения CN_TRANSMIT. Это сообщение означает, что выходная очередь практически пустая и можно поместить в нее очередную порцию данных.

В случае успешного завершения, функция возвращает значение отличное от нуля. Если функция завершилась с ошибками, она возвращает ноль. Причиной возникновения ошибки может послужить вызов функции EnableCommNotification с неправильным идентификатором COM-порта (COM-порт не открыт) или то, что драйвер асинхронного последовательного адаптера не поддерживает функцию EnableCommNotification.

Так, например, драйвер асинхронного последовательного адаптера COMM.DRV операционной системы Windows версии 3.0 не поддерживает эту функцию.

Вы можете запретить передачу функции окна сообщения WM_COMMNOTIFY с кодами извещения CN_RECEIVE и CN_TRANSMIT.

Если задать для параметра cbWriteNotify значение -1, драйвер не будет передавать функции окна сообщение WM_COMMNOTIFY с кодом извещения CN_RECEIVE.

Аналогично, если задать для параметра cbOutQueue значение -1, драйвер не будет передавать функции окна сообщение WM_COMMNOTIFY с кодом извещения CN_TRANSMIT.

Может возникнуть резонный вопрос: что произойдет, если разрешить генерацию сообщения WM_COMMNOTIFY с кодом извещения CN_RECEIVE, задать пороговое значение для передачи сообщения 20 байт, а из COM-порта поступит только 15 байт? Будет ли драйвер ожидать прихода остальных 5 байт и как долго? Не прекратится ли обмен данными?

Драйвер асинхронного последовательного адаптера периодически, с интервалом около 100 миллисекунд, опрашивает все очереди COM-портов. Если при очередном опросе входной очереди в ней будет находится меньше чем cbWriteNotify байт, функции окна все равно передается сообщение WM_COMMNOTIFY с кодом извещения CN_RECEIVE. Таким образом, если во входной очереди находится меньше байт, чем надо для генерации сообщения, функция окна все равно получит сообщение WM_COMMNOTIFY по истечении 100 миллисекунд.

7.2.11. Функция FlushComm

Функция FlushComm удаляет все символы из входной или выходной очереди COM-порта. Прототип функции имеет следующий вид:

int FlushComm(int idComDev, int fnQueue);

Параметр idComDev является идентификатором COM-порта, очередь которого необходимо очистить (сбросить).

Параметр fnQueue определяет очередь, из которой будут удалены все символы. Если значение этого параметра равно нулю, очищается выходная очередь. Если же значение параметра равно единице, удаляются все символы из приемной очереди.

Функция возвращает ноль в случае нормального завершения. Если значение, возвращаемое функцией, меньше нуля, параметр idComDev определяет неоткрытый COM-порт или параметр fnQueue задает несуществующую очередь. Возвращаемое функцией число принимает положительное значение, если возникла ошибка COM-порта. Подробный список возможных ошибок приведен в описании функции GetCommError.

7.2.12. Функция GetCommError

Функция GetCommError позволяет определить текущее состояние COM-порта и причину ошибки при предыдущем вызове коммуникационной функции.

Когда случается ошибка при передаче или приеме данных, Windows блокирует COM-порт до тех пор, пока не будет вызвана функция GetCommError.

int GetCommError(int idComDev, COMSTAT FAR* lpStat);

Параметр idComDev должен содержать идентификатор порта, который будет проверяться.

Через параметр lpStat передается дальний указатель на структуру типа COMSTAT. В поля этой структуры будет записано состояние COM-порта. В случае если вы вызовете GetCommError с параметром lpStat, равным NULL, функция вернет только значение ошибки.

Структура COMSTAT определена в файле WINDOWS.H следующим образом:

#if (defined(STRICT) || (WINVER >= 0x030a))

// Основное определение структуры COMSTAT
      typedef struct tagCOMSTAT
      {
             BYTE status;
             UINT cbInQue;
             UINT cbOutQue;
      } COMSTAT;

      // Определение вспомогательных констант
      #define CSTF_CTSHOLD    0x01
      #define CSTF_DSRHOLD    0x02
      #define CSTF_RLSDHOLD   0x04
      #define CSTF_XOFFHOLD   0x08
      #define CSTF_XOFFSENT   0x10
      #define CSTF_EOF        0x20
      #define CSTF_TXIM       0x40

#else   /* (STRICT || WINVER >= 0x030a) */

// Альтернативное определение структуры COMSTAT
      typedef struct tagCOMSTAT
      {
             BYTE fCtsHold  :1;
             BYTE fDsrHold  :1;
             BYTE fRlsdHold :1;
             BYTE fXoffHold :1;
             BYTE fXoffSent :1;
             BYTE fEof      :1;
             BYTE fTxim     :1;
             UINT cbInQue;
             UINT cbOutQue;
      } COMSTAT;

#endif  /* !(STRICT || WINVER >= 0x030a */

Из приведенного выше листинга видно, что структура COMSTAT определяется по-разному в зависимости от следующего условия:

#if(defined(STRICT) || (WINVER >= 0x030a))
      // Основное определение структуры COMSTAT
      // ....
#else   /* (STRICT || WINVER >= 0x030a) */
      // Альтернативное определение структуры COMSTAT
      // ....
#endif

Из приведенного выше листинга видно, что структура COMSTAT определяется по-разному в зависимости от следующего условия:

#if(defined(STRICT) || (WINVER >= 0x030a))
      // Основное определение структуры COMSTAT
      // ....
#else /* (STRICT || WINVER >= 0x030a) */
      // Альтернативное определение структуры COMSTAT
      // ....
#endif

Если вы создаете загрузочный модуль для операционной системы Windows 3.1 или устанавливаете жесткий режим контроля синтаксиса программы, определив константу STRICT, используется основное определение структуры COMSTAT. Опишем поля структуры COMSTAT при использовании основного определения:

Поле Описание
status Определяет состояние передачи данных. Оно может содержать один или несколько флагов:
CSTF_CTSHOLD Передача данных приостановлена в ожидании сигнала CTS
CSTF_DSRHOLD Передача данных приостановлена в ожидании сигнала DSR
CSTF_RLSDHOLD Передача данных приостановлена в ожидании сигнала RLSD
CSTF_XOFFHOLD Передача данных приостановлена после приема символа XOFF
CSTF_XOFFSENT Передача данных приостановлена при передаче символа XOFF. Передача приостанавливается, когда символ XOFF передан. CSTF_XOFFSENT используется системами, которые принимают следующий символ XON вне зависимости от настоящего принятого символа
CSTF_EOF Получен символ конца файла EOF
CSTF_TXIM Произошла задержка при передаче символа
cbInQue Количество символов, находящихся во входной очереди
cbOutQue Количество символов в выходной очереди

Функция GetCommError возвращает значение, являющееся комбинацией флагов. Флаги определяют ошибки, возникшие при последнем вызове коммуникационной функции.

Возвращаемое значение может быть комбинацией из следующих флагов:

Флаг Описание
CE_BREAK Обнаружено состояние разрыва связи (BREAK)
CE_CTSTO Тайм-аут CTS. Во время передачи символа сигнал CTS отсутствовал дольше промежутка времени, определенного полем fCtsHold структуры COMSTAT
CE_DNS Параллельный порт не был выбран
CE_DSRTO Тайм-аут DSR. Во время передачи символа сигнал DSR отсутствовал промежуток времени, определенный полем fDsrHold структуры COMSTAT
CE_FRAME Обнаружена ошибка формата кадра (framing error)
CE_IOE Во время попытки взаимодействия с параллельным портом произошла ошибка ввода/вывода
CE_MODE Запрашиваемый режим не поддерживается или идентификатор COM-порта недействителен. Если установлен этот флаг, другие флаги следует игнорировать
CE_OOP Параллельный адаптер передал компьютеру сигнал "out of paper" - конец бумаги
CE_OVERRUN Символ не был прочитан из регистров COM-порта до прихода следующего символа. В результате этот символ был потерян
CE_PTO Истекло время (тайм-аут) при выполнении попытки взаимодействия с параллельным устройством
CE_RLSDTO Тайм-аут RLSD. Во время передачи символа сигнал RLSD отсутствовал промежуток времени, определенный полем fRlsdHold структуры COMSTAT
CE_RXOVER Очередь приемника переполнена. Либо очередь приемника переполнена, либо символ был получен после получения символа конца файла
CE_RXPARITY Обнаружена ошибка четности
CE_TXFULL Очередь передатчика полностью заполнена, но функция пытается записать новые данные в эту очередь

7.2.13. Функция SetCommEventMask

Для каждого COM-порта Windows поддерживает слово событий. Оно состоит из набора бит, которые устанавливаются при изменении состояния COM-порта (например, при изменении состояния сигнала CTS). Чтобы при изменении состояния COM-порта соответствующим образом менялось слово состояния, необходимо сначала разрешить регистрацию этих изменений.

Функция SetCommEventMask разрешает регистрацию изменений состояния COM-порта в слове событий данного COM-порта:

UINT FAR* SetCommEventMask(int idComDev, UINT fuEvtMask);

Параметр idComDev является идентификатором COM-порта. Это значение возвращает функция OpenComm.

Параметр fuEvtMask определяет, какие события будут разрешены. Этот параметр может быть комбинацией из любых констант, перечисленных в следующей таблице:

Константа Смысл
EV_BREAK Устанавливается при обнаружении состояния BREAK на входной линии
EV_CTS Устанавливается, когда сигнал CTS изменяет свое состояние
EV_CTSS Указывает текущее состояние сигнала CTS
EV_DSR Устанавливается, когда сигнал DSR изменяет свое состояние
EV_ERR Устанавливается при возникновении ошибки на линии. Такими ошибками являются CE_FRAME, CE_OVERRUN, и CE_RXPARITY (см. описание функции GetCommError)
EV_PERR Устанавливается при возникновении ошибки принтера для параллельного порта. Такими ошибками являются CE_DNS, CE_IOE, CE_LOOP и CE_PTO (см. описание функции GetCommError)
EV_RING Указывает текущее состояние сигнала RI во время последнего прерывания от модема
EV_RLSD Устанавливается, когда сигнал RLSD изменяет свое состояние
EV_RLSDS Указывает текущее состояние сигнала RLSD
EV_RXCHAR Устанавливается, когда символ принимается и помещается во входную очередь
EV_RXFLAG Устанавливается, когда определенный в структуре DCB символ принимается и помещается во входную очередь COM-порта
EV_TXEMPTY Устанавливается, когда последний символ из выходной очереди отправлен в COM-порт и выходная очередь становится пустой

Функция SetCommEventMask возвращает указатель на слово событий данного COM-порта. Каждый бит этого слова отвечает за определенное событие. Если бит установлен, значит соответствующее событие произошло.

В слово событий записываются только события, разрешенные функцией SetCommEventMask.

Воспользовавшись функцией EnableCommNotification, можно разрешить передачу в функцию окна приложения сообщения WM_COMMNOTIFY с кодом извещения CN_EVENT. Это сообщение будет передаваться при изменении слова состояния COM-порта.

7.2.14. Функция GetCommEventMask

Функция GetCommEventMask получает и затем очищает слово события данного COM-порта.

UINT GetCommEventMask(int idComDev, int fnEvtClear);

Параметр idComDev является идентификатором COM-порта.

Параметр fnEvtClear определяет события, которые будут сброшены в слове событий.

Если функция завершается успешно, она возвращает значение слова событий для COM-порта, определенного параметром idComDev. Каждый бит этого слова отвечает за одно конкретное событие. Если данное событие произошло, соответствующий бит равен 1.

Перед тем как функция GetCommEventMask может зарегистрировать возникновение события, приложение должно разрешить данное событие при помощи функции SetCommEventMask.

При возникновении таких событий, как изменение состояния линии или ошибка принтера, вы должны вызвать функцию GetCommError.

7.2.15. Функция GetCommState

Функция GetCommState позволяет узнать режим работы порта асинхронного последовательного адаптера. Прототип функции представлен ниже:

int GetCommState(int idComDev, DCB FAR* lpdcb);

Параметр idComDev должен содержать идентификатор COM-порта. Это значение возвращает функция OpenComm.

Параметр lpdcb является дальним указателем на структуру DCB, в которую будет записана информация о режиме работы COM-порта, указанного в параметре idComDev.

В случае успешного завершения функция GetCommState возвращает нулевое значение. Если при работе функции возникла ошибка, возвращаемая величина меньше нуля.

7.2.16. Функция ReadComm

Функция ReadComm позволяет прочитать данные из входной очереди COM-порта. Прототип функции представлен ниже:

int ReadComm(int idComDev, void FAR* lpvBuf, int cbRead);

Параметр idComDev является идентификатором COM-порта, из которого будут прочитаны данные.

Параметр lpvBuf содержит дальний указатель на буфер, в который будут записаны прочитанные из COM-порта данные.

Последний параметр cbRead задает количество символов, которое следует прочитать из входной очереди порта.

В случае успешного выполнения функции возвращаемое значение определяет количество символов, прочитанное из COM-порта. Если во время выполнения функции произошла ошибка, функция возвращает отрицательное значение. При этом абсолютное значение возвращенной величины определяет количество символов, успешно прочитанных из COM-порта.

В случае возникновения ошибки ее причину можно выяснить с помощью функции GetCommError. Так как ошибка может произойти и в том случае, когда ни один символ из входной очереди не прочитан, то, если ReadComm возвращает нулевую величину, следует вызвать функцию GetCommError, чтобы удостоверится в отсутствии ошибок.

Если величина, возвращаемая функцией меньше чем значение параметра cbRead, это означает, что на момент вызова функции во входной очереди COM-порта находилось символов меньше, чем определено параметром cbRead.

Если величина, возвращаемая функцией равна значению параметра cbRead, то возможно, что в очереди приемника еще находятся данные. Поэтому можно сразу повторно вызвать функцию ReadComm.

В качестве примера использования функции ReadComm мы приводим исходный текст функции ReadCommChar, считывающей из входной очереди один символ:

//==========================================================
// Функция ReadCommChar
//==========================================================
int ReadCommChar(int nPortID)
{
      int iResult = 0;
      int iErr = 0;

      // Считываем из COM-порта один символ
      iErr = ReadComm(nPortID, (LPSTR)&iResult, 1);

      // Если символ не прочитан, обрабатываем ошибку
      if(iErr != 1)
      {
             iResult = -1;

             // Сбрасываем флаги ошибок
             GetCommError(nPortID, NULL);
      }
      return iResult;
}

В качестве параметра для функции ReadCommChar необходимо передать идентификатор COM-порта. Функция ReadCommChar вызывает ReadComm и пытается считать из входной очереди COM-порта один символ.

Если ReadComm возвращает единицу, символ успешно прочитан и функция ReadCommChar возвращает код полученного символа. В противном случае для того, чтобы сбросить флаги ошибок, вызывается функция GetCommError. Затем функция ReadCommChar возвращает -1.

7.2.17. Функция TransmitCommChar

Функция TransmitCommChar помещает символ, заданный параметром chTransmit, в начало выходной очереди COM-порта, определенного параметром idComDev. Прототип функции представлен ниже.

int TransmitCommChar(int idComDev, char chTransmit);

Функция возвращает нулевое значение в случае успешного завершения или отрицательное значение, если символ не записан в выходную очередь. Функцию нельзя вызывать повторно, если COM-порт не производит передачу данных. После того как функция TransmitCommChar записала символ в начало выходной очереди, необходимо, чтобы перед повторным вызовом функции этот символ был передан.

Если предыдущий символ не был передан, перед повторным вызовом функции TransmitCommChar, функция завершается с ошибкой.

В следующем примере функция TransmitCommChar используется для передачи символов, набранных на клавиатуре в COM-порт.

case WM_CHAR:
      ch = (char)wParam;

      // Помещаем код нажатой клавиши в начало выходной очереди
      TransmitCommChar(idComDev, ch);

      // Добавляем символ перевода строки LF
      // для каждого символа возврата каретки
      if (ch == 0x0d)
             TransmitCommChar(idComDev, 0x0a);

      return TRUE;

7.2.18. Функция UngetCommChar

Функция позволяет поместить символ в начало входной очереди. При выполнении последующей операции чтения функция ReadComm прочитает этот символ. Прототип функции UngetCommChar представлен ниже:

int UngetCommChar(int idComDev, char chUnget);

Первый параметр функции idComDev должен содержать идентификатор COM-порта.

Параметр chUnget должен содержать символ, помещаемый во входную очередь COM-порта.

Функция возвращает нулевое значение в случае успешного завершения или отрицательное значение при возникновении ошибки.

Последовательные вызовы функции UngetCommChar не допускаются. Перед тем как вы сможете вызвать функцию UngetCommChar еще раз, символ, помещенный в очередь приемника, должен быть прочитан.

7.2.19. Функция WriteComm

Функция WriteComm записывает символы в выходную очередь COM-порта. Прототип функции представлен ниже:

int WriteComm(int idComDev, const void FAR* lpvBuf,
                       int cbWrite);

Параметр idComDev содержит идентификатор COM-порта, используемого для передачи данных.

Параметр lpvBuf является дальним указателем на буфер, содержащий передаваемые данные.

Последний параметр функции cbWrite определяет количество символов в буфере lpvBuf, предназначенных для передачи.

В случае успешного выполнения функции возвращаемое ей значение определяет количество символов, записанное в выходную очередь COM-порта. Если во время выполнения функции произошла ошибка, функция возвращает отрицательное значение. Абсолютное значение возвращаемой величины определяет количество символов, успешно записанных в выходную очередь.

В случае возникновения ошибки ее причину можно выяснить с помощью функции GetCommError.

Если в очереди передатчика недостаточно места, функция WriteComm может удалить данные из этой очереди. Поэтому перед вызовом функции WriteComm приложение должно проверить наличие достаточного свободного пространства в очереди передатчика. Для этого следует воспользоваться функцией GetCommError.

При открытии COM-порта функцией OpenComm следует указать размер выходной очереди не меньше, чем максимальный предполагаемый размер передаваемых строк.

В качестве примера использования функции WriteComm мы приводим исходный текст функции WriteCommChar, записывающей в выходную очередь один символ:

//==========================================================
// Функция WriteCommChar
//==========================================================
BOOL WriteCommChar(int nPortID, int nChar)
{
      int iErr = 0;

      iErr = WriteComm(nPortID, (LPSTR)&nChar, 1))

      if(iErr != 1)
      {
             GetCommError(nPortID, NULL);
             return FALSE;
      }
      return TRUE;
}

В качестве первого параметра функции WriteCommChar передается идентификатор COM-порта, в который необходимо передать символ. Второй параметр должен содержать код передаваемого символа.

Функция WriteCommChar вызывает функцию WriteComm и пытается записать в выходную очередь COM-порта один символ.

Если WriteComm возвращает единицу, значит символ успешно записан и функция WriteCommChar возвращает значение TRUE. В противном случае для того чтобы сбросить флаги ошибок вызывается функция GetCommError и WriteCommChar возвращает FALSE.

7.2.20. Сообщение WM_COMMNOTIFY

Сообщение WM_COMMNOTIFY отправляется драйвером COM-порта в функцию окна и служит извещением об изменении состояния COM-порта. Такое сообщение может поступать в функцию окна при наполнении входной очереди до определенного порогового значения, изменении состояния сигналов DTR, RI, RTS и т. д.

Параметр wParam сообщения WM_COMMNOTIFY определяет идентификатор COM-порта, вызвавшего отправку сообщения.

Через младшее слово параметра lParam функция окна получает число, по которому можно судить о причине посылки сообщения WM_COMMNOTIFY. Это число может быть комбинацией следующих кодов извещения:

Код извещения Описание
CN_EVENT Произошло событие, разрешенное в слове событий COM-порта. Эти события разрешаются вызовом функции SetCommEventMask. После прихода сообщения WM_COMMNOTIFY с кодом извещения CN_EVENT приложение должно вызвать функцию GetCommEventMask, чтобы определить, какое именно событие произошло и сбросить событие
CN_RECEIVE Очередь приемника содержит больше, чем cbWriteNotify байт. Пороговое значение cbWriteNotify задается одним из параметров функции EnableCommNotification
CN_TRANSMIT Очередь передатчика содержит меньше, чем cbOutQueue байт. Пороговое значение cbOutQueue задается одним из параметров функции EnableCommNotification

Если приложение обработало поступившее ему сообщение WM_COMMNOTIFY, оно должно возвратить нулевое значение.

Сообщение WM_COMMNOTIFY с кодом извещения CN_EVENT передается только тогда, когда слово состояния событий изменяет свое значение. Приложение, которое принимает сообщение WM_COMMNOTIFY, должно каждый раз очищать слово состояния событий, для того чтобы гарантировать получение сообщений в дальнейшем.

Для решения этой проблемы можно использовать следующие методы:

Событие CN_RECEIVE формируется в тех случаях, когда количество байт во входной очереди превышает пороговое значение cbWriteNotify, установленное функцией EnableCommNotification, или когда истекло время (тайм-аут). После формирования события CN_RECEIVE в случае превышения пороговой величины cbWriteNotify другие сообщения CN_RECEIVE не будут генерироваться до тех пор, пока количество байт во входной очереди не станет меньше значения cbWriteNotify и не превысит ее снова.

Сообщение CN_TRANSMIT создается аналогично CN_RECEIVE. Порог устанавливается параметром cbOutQueue функции EnableCommNotify. Когда количество символов в выходной очереди становится меньше, чем cbOutQueue, формируется сообщение CN_TRANSMIT. Другие сообщения CN_TRANSMIT не будут посылаться до тех пор, пока в буфере не станет больше, чем cbOutQueue символов.

Однако, если прерывания поступают достаточно быстро, дополнительные сообщения CN_RECEIVE (или CN_TRANSMIT) могут посылаться до того, как количество символов в выходной очереди станет больше, чем cbWriteNotify. Эти сообщения можно пропускать (игнорировать), однако они могут послужить причиной перезагрузки системы.

Ниже приведен фрагмент обработчика сообщения WM_COMMNOTIFY:

//==========================================================
// Функция окна WndProc
//==========================================================
LRESULT CALLBACK _export
WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
      switch( message )
      {
             case WM_COMMNOTIFY:
             {
                   if(CN_EVENT & LOWORD( lParam ) == CN_EVENT)
                   {
                         GetCommEventMask(COMDEV( npTTYInfo ), EV_RXCHAR);
                         return(TRUE);
                   }

                   else if(CN_RECEIVE & LOWORD( lParam ) == CN_RECEIVE)
                   {
                         return(TRUE);
                   }

                   else if(CN_TRANSMIT & LOWORD( lParam ) == CN_TRANSMIT)
                   {
                         return(TRUE);
                   }

                   else if(LOWORD( lParam ) == 0)
                         return(TRUE);
             }
             default:
                   return(DefWindowProc(hwnd, message, wParam, lParam));
      }
}

При загрузке драйвера последовательного асинхронного адаптера он вызывает функцию CreateSystemTimer и создает таймер, посылающий сообщения драйверу каждые 100 миллисекунд. Обрабатывая сообщения таймера, драйвер просматривает состояние всех открытых COM-портов и проводит проверку тайм-аута. Период таймера изменить нельзя.

7.3. Приложение EASYTTY

Наше первое телекоммуникационное приложение EASYTTY демонстрирует использование функций Windows, предназначенных для работы с портами асинхронного последовательного адаптера и модемами.

Приложение EASYTTY выполняет все основные функции, которые должна поддерживать любая телекоммуникационная программа. EASYTTY позволяет передавать модему AT-команды, принимать от него ответ и отображать его на экране.

Перед тем как запускать приложение EASYTTY на вашем компьютере, следует создать в каталоге Windows файл EASYTTY.INI (см. листинг 7.6). Если вместе с книгой вы приобрели дискету, то скопируйте файл EASYTTY.INI из каталога WIN\EASYTTY в каталог Windows.

Листинг 7.6. Файл EASYTTY.INI

[Port]
Mode=COM2:9600,N,8,1
>

Файл EASYTTY.INI должен состоять из одного раздела [Port], содержащего единственную строку Mode. В этой строке определяется номер COM-порта, к которому подключен модем, скорость обмена и формат передаваемых данных. Формат строки Mode совпадает с форматом команды MODE операционной системы MS-DOS.

Запустите EASYTTY. Набирая на клавиатуре AT-команды модема, можно перевести его в любой режим. Например, можно сбросить текущую конфигурацию. Для этого введите команду ATZ и нажмите клавишу <Enter>. В ответ на введенную команду модем загрузит конфигурацию, принятую по умолчанию и вернет компьютеру ответ OK (см. рис. 7.2).

Чтобы приложение EASYTTY могло автоматически отвечать на вызов по телефонной линии, передайте модему команду ATS0=1 и нажмите клавишу <Enter>. Поэкспериментируйте с приложением EASYTTY, передавая модему различные команды и наблюдая на экране ответные сообщения.

Рис. 7.2. Приложение EASYTTY

Вы можете передать модему команду набора номера удаленного абонента. Чтобы набрать номер 777-77-77, достаточно ввести команду ATDP 777-77-77 и нажать клавишу <Enter>.

После установления связи с удаленным модемом вы можете обменяться с ним текстовыми сообщениями. Набирайте передаваемый текст на клавиатуре и просматривайте ответ от удаленного компьютера в главном окне приложения.

Когда вы закончите сеанс связи с удаленным компьютером, переведите модем в командный режим и передайте ему команду ATH0. Модем повесит трубку и отключится от телефонной линии.

Чтобы перевести модем из режима передачи данных в командный режим, подождите 2-3 секунды, наберите на клавиатуре три знака '+' и дождитесь от модема ответа OK.

Главный файл приложения EASYTTY приведен в листинге 7.7.

Листинг 7.7. Файл EASYTTY.CPP

#include    <windows.h>
#include    <string.h>
#include    <conio.h>
#include    <stdio.h>

// Определение констант
#define      QUEUE_SIZE 1024
#define      CTRL_Q          17
#define      CTRL_S          19
#define      ESC                  27

// Прототипы функций
int InitCommPort(void);
int CloseCommPort(int idCommPort);
int ProcessExchange(int idCommPort);

// ============================================================
// Функция WinMain
// ============================================================
#pragma argsused

int PASCAL
WinMain( HANDLE hInstance,
                   HANDLE hPrevInstance,
                   LPSTR lpszCmdLine,
                   int cmdShow )
{
      int  idCommPort;         // идентификатор COM-порта

      // Позволяем одновременно запустить только
      // одну копию приложения
      if( hPrevInstance )
             return( FALSE );

      // Инициализация интерфейса EasyWin
      _InitEasyWin();

      // Открываем COM-порт и устанавливаем новый режим работы
      idCommPort = InitCommPort();

      // В случае возникновения ошибки при инициализации COM-порта
      // завершаем работу приложения
      if(idCommPort < 0)
             return FALSE;

      puts( "Для завершения программы нажмите клавишу <ESC>" );

      while( TRUE )
      {
             MSG    msg;

             // Организуем цикл обработки сообщений
             if( PeekMessage( ( LPMSG )&msg, NULL, 0, 0, PM_REMOVE ) )
             {
                   // При получении сообщения WM_QUIT завершаем приложение
                   if ( msg.message == WM_QUIT )
                         return( msg.wParam );

                   TranslateMessage( &msg );
                   DispatchMessage ( &msg );
             }

             // Если в очереди приложения нет сообщений, начинаем обмен
             // данными через COM-порт
             else
             {
                   if( !ProcessExchange( idCommPort ))
                   {
                         // Закрываем главное окно приложения
                         DestroyWindow(GetFocus());
                   }
             }
      }
}

// ============================================================
// Функция InitCommPort
// ============================================================
int InitCommPort()
{
      DCB    dcbCommPort;            // структура DCB
      int  idCommPort;         // идентификатор COM-порта

      char     szMsg[144];                             // временный буфер
      char     szCommSettings[20];       // режим работы COM-порта
      char     szPortName[6];                        // имя COM-порта

      // Получаем из раздела [Port] файла PHONE.INI строку Mode,
      // определяющую режим работы COM-порта и записываем ее в
      // буфер szCommSettings

      GetPrivateProfileString("Port", "Mode", "COM1:9600,n,8,1",
             szCommSettings, sizeof(szCommSettings) - 1, "easytty.ini");

      // Выделяем из полученной строки первые четыре символа,
      // задающие номер COM-порта, для последующей передачи его
      // функции OpenComm

      lstrcpyn(szPortName, szCommSettings, 5);
      szPortName[4] = '\0';

      // Открываем COM-порт szPortName
      if (( idCommPort =
                         OpenComm( szPortName, QUEUE_SIZE, QUEUE_SIZE )) < 0 )
      {
             // В случае ошибки отображаем сообщение и
             // завершаем работу приложения
             wsprintf( szMsg,
                   "Ошибка открытия порта.\nФункция OpenComm возвратила %d",
                   idCommPort );

             MessageBox( NULL, szMsg,
                                      "Ошибка", MB_OK | MB_ICONEXCLAMATION );

             return( idCommPort );
      }

      // Удаляем все символы из выходной очереди
      FlushComm( idCommPort, 0 );

      // Удаляем все символы из входной очереди
      FlushComm( idCommPort, 1 );

      // Заполняем структуру DCB в соответствии с командной строкой
      // Mode из раздела [Port] файла PHONE.INI

      if( BuildCommDCB( szCommSettings, &dcbCommPort ) != 0 )
      {
             // В случае ошибки отображаем сообщение и
             // завершаем работу приложения
             MessageBox( NULL, "Ошибка при заполнении структуры DCB",
                                      "Ошибка", MB_OK | MB_ICONEXCLAMATION );
             return( -1 );
      }

      // Устанавливаем новый режим COM-порта в соответствии с
      // подготовленной структурой DCB
      if ( SetCommState( &dcbCommPort ) != 0 )
      {
             // В случае ошибки отображаем сообщение и
             // завершаем работу приложения
             MessageBox( NULL, "Ошибка установки режима COM-порта",
                                      "Ошибка", MB_OK | MB_ICONEXCLAMATION );

             return( -1 );
      }

      // Возвращаем идентификатор открытого COM-порта
      return idCommPort;
}

// ============================================================
// Функция ProcessExchange
// ============================================================
int ProcessExchange( int idCommPort )
{
      int nCharWaiting, nCharWriting;
      COMSTAT     ComStat;
      char     inBuff[ QUEUE_SIZE ];

      // Определяем текущее состояние открытого COM-порта
      GetCommError( idCommPort, &ComStat );

      // Если во входной очереди уже есть данные, считываем их и
      // выводим на экран
      if ((nCharWaiting = ComStat.cbInQue ) > 0 )
      {
             // Считываем данные из входной очереди и
             // помещаем их в буффер inBuff
             nCharWaiting = ReadComm( idCommPort, inBuff,
                   ( nCharWaiting > QUEUE_SIZE ?
                   QUEUE_SIZE : nCharWaiting ));

             // Отображаем полученные символы на экране
             if ( nCharWaiting > 0 )
                   for( int i = 0; i < nCharWaiting; i++ )
                         putch(inBuff[i]);

             else
                   return( FALSE );
      }

      // Узнаем, нажата ли какая-нибудь клавиша на клавиатуре
      else if ( kbhit() )
      {
             // Если клавиша нажата, определяем ее код
             char keyHit  = ( char )getch();

             if ( !keyHit )
                   keyHit = ( char )getch();

             // Если нажата клавиша <Esc>, закрываем COM-порт и завершаем
             // работу приложения
             if ( keyHit == ESC )
             {
                   CloseCommPort( idCommPort );
                   return( FALSE );
            }

             // Записывем код нажатой клавиши в выходную очередь
             // COM-порта
             else
             {
                   nCharWriting =
                                WriteComm( idCommPort, ( LPSTR )&keyHit, 1 );

                   // При возникновении ошибки завершаем приложение
                   if( nCharWriting < 0 )
                         return( FALSE );
            }
      }
      return( TRUE );
}

// ============================================================
// Функция CloseCommPort
// ============================================================
int CloseCommPort( int idCommPort )
{
      // Удаляем все символы из входной и выходной очереди
      // COM-порта
      FlushComm( idCommPort, 0 );
      FlushComm( idCommPort, 1 );

      // Закрываем COM-порт
      CloseComm( idCommPort );

      return 0;
}

Особенностью приложения EASYTTY является использование интерфейса EasyWin, предоставляемого средой разработки Borland C++ for Windows версии 3.1. Интерфейс EasyWin позволяет сократить до минимума код, требуемый для создания окна, вывода в него принимаемых данных и получения ввода с клавиатуры.

После запуска приложения EASYTTY, функция WinMain выполняет инициализацию интерфейса EasyWin. Для этого вызывается функция _InitEasyWin, описанная во включаемом файле STDIO.H:

_InitEasyWin();

После вызова этой функции появляется главное окно приложения. Теперь можно вызывать стандартные функции консольного ввода/вывода - puts, kbhit, getch, putch.

Затем функция WinMain вызывает функцию InitCommPort, определенную в приложении. Эта функция считывает из раздела [Port] файла PHONE.INI строку Mode, которая определяет номер COM-порта, к которому подключен модем и его режим работы. Потом InitCommPort открывает соответствующий порт и устанавливает его режим. Затем функция завершает свою работу и возвращает идентификатор открытого COM-порта.

Если COM-порт не открыт, то идентификатор открытого COM-порта равен нулю и приложение сразу завершает работу.

После того как порт открыт, вызывается функция puts:

puts( "Для завершения приложения нажмите клавишу <Esc>" );

Она выводит в окне приложения строку "Для завершения приложения нажмите клавишу <Esc>". Затем следует цикл while в котором вызывается функция PeekMessage и функция ProcessExchange, определенная в нашем приложении:

while( TRUE )
{
      MSG    msg;

      // Организуем цикл обработки сообщений
      if( PeekMessage( ( LPMSG )&msg, NULL, 0, 0, PM_REMOVE ) )
      {
             // При получении сообщения WM_QUIT завершаем приложение
             if ( msg.message == WM_QUIT )
                   return( msg.wParam );

             TranslateMessage( &msg );
             DispatchMessage ( &msg );
      }

      // Если в очереди приложения нет сообщений, начинаем обмен
      // данными через COM-порт
      else
      {
             if( !ProcessExchange( idCommPort ))
             {
                   // Закрываем главное окно приложения
                   DestroyWindow(GetFocus());
             }
      }
}

Функция PeekMessage образует цикл обработки сообщений, благодаря которому одновременно могут работать и другие приложения Windows.

Функция ProcessExchange является сердцем приложения EASYTTY. Она организует весь диалог пользователя с приложением. Для этого она считывает данные из входного буфера COM-порта, поступающие в него от модема, и отображает их в окне приложения. Если вы нажимаете на клавиши, функция ProcessExchange передает код клавиши в выходной буфер COM-порта.

Если вы нажмете клавишу <Esc>, функция ProcessExchange вызывает функцию CloseCommPort, определенную в приложении, а затем закрывает главное окно приложения, вызывая функцию DestroyWindow.

Теперь рассмотрим более подробно функции InitCommPort, ProcessExchange и CloseCommPort, определенные в приложении.

Функция InitCommPort считывает из раздела [Port] файла EASYTTY.INI строку Mode, определяющую режим работы COM-порта и записывает ее в буфер szCommSettings. Если файл EASYTTY.INI не обнаружен в каталоге Windows или в нем не определена строка Mode, в буфер szCommSettings записывается строка "COM1:9600,n,8,1".

Затем из строки в буфере szCommSettings выделяются первые четыре символа, задающие номер COM-порта для последующей передачи его функции OpenComm. Функция OpenComm открывает этот COM-порт.

if((idCommPort =
             OpenComm(szPortName, QUEUE_SIZE, QUEUE_SIZE)) < 0 )
{
...
}

Если COM-порт не открыт, OpenComm возвращает отрицательное значение. Функция отображает предупреждающее сообщение "Ошибка открытия порта..." и завершает работу.

Если COM-порт успешно открыт, удаляем все символы из входной и выходной очередей:

// Удаляем все символы из выходной очереди
FlushComm( idCommPort, 0 );

// Удаляем все символы из входной очереди
FlushComm( idCommPort, 1 );

Затем заполняем структуру dcbCommPort типа DCB в соответствии с командной строкой Mode из раздела [Port] файла EASYTTY.INI. Для этого используем функцию BuildCommDCB, передав ей строку szCommSettings:

if( BuildCommDCB( szCommSettings, &dcbCommPort ) != 0 )
{
      // В случае ошибки отображаем сообщение и
      // завершаем работу приложения
      MessageBox( NULL, "Ошибка при заполнении структуры DCB",
                                "Ошибка", MB_OK | MB_ICONEXCLAMATION );
      return( -1 );
}

Если BuildCommDCB возвращает значение, не равное нулю, значит произошла ошибка. В этом случае выводим сообщение "Ошибка при заполнении структуры DCB" и завершаем функцию, возвращая число -1.

В случае успешного выполнения функции BuildCommDCB устанавливаем новый режим COM-порта в соответствии с подготовленной структурой DCB:

if( SetCommState( &dcbCommPort ) != 0 )
{
      // В случае ошибки отображаем сообщение и
      // завершаем работу приложения
      MessageBox( NULL, "Ошибка установки режима COM-порта",
                                      "Ошибка", MB_OK | MB_ICONEXCLAMATION );
      return( -1 );
}

Если SetCommState возвращает ненулевое значение, значит произошла ошибка. В этом случае выводим сообщение "Ошибка установки режима COM-порта" и завершаем функцию InitCommPort, возвращая число -1.

Если функция SetCommState завершилась успешно, функция InitCommPort завершает работу и возвращает идентификатор открытого COM-порта. Позже полученный идентификатор COM-порта передается функциям ProcessExchange и CloseCommPort.

Функция ProcessExchange вызывает GetCommError, заполняющую структуру ComStat типа COMSTAT:

COMSTAT     ComStat;

// Определяем текущее состояние открытого COM-порта
GetCommError( idCommPort, &ComStat );

Поле cbInQue структуры ComStat будет определять количество символов во входной очереди используемого нами COM-порта. Если во входной очереди есть данные, считываем их и выводим их в окно приложения:

nCharWaiting = ReadComm( idCommPort, inBuff,
                   ( nCharWaiting > QUEUE_SIZE ? QUEUE_SIZE :
                                                  nCharWaiting ));

// Отображаем полученные символы на экране
if ( nCharWaiting > 0 )
      for( int i = 0; i < nCharWaiting; i++ )  putch(inBuff[i]);

else
      return( FALSE );

Если входная очередь COM-порта пуста, с помощью стандартной функции консольного ввода/вывода kbhit проверяем, нажата ли какая-нибудь клавиша на клавиатуре.

В случае, если клавиша нажата, определяем ее код:

// Если клавиша нажата, определяем ее код
char keyHit  = ( char )getch();

if ( !keyHit )
      keyHit = ( char )getch();

Проверяем, нажата ли клавиша <Esc>. Если нажата клавиша <Esc>, закрываем COM-порт с помощью функции CloseCommPort и возвращаем значение FALSE:

// Закрываем COM-порт и возвращаем значение FALSE
CloseCommPort( idCommPort );
return( FALSE );

Если пользователь нажал любую другую клавишу, записываем ее код в выходную очередь COM-порта:

nCharWriting = WriteComm( idCommPort, ( LPSTR )&keyHit, 1 );

На этом работа функции завершена, и мы переходим к рассмотрению функции CloseCommPort.

Функция CloseCommPort наиболее простая из функций приложения EASYTTY. Она удаляет все символы из входной и выходной очереди COM-порта, а затем закрывает COM-порт и возвращает управление:

FlushComm( idCommPort, 0 );
FlushComm( idCommPort, 1 );

CloseComm( idCommPort );
return 0;

Файл определения модуля для приложения EASYTTY приведен в листинге 7.8.

Листинг 7.8. Файл EASYTTY.DEF

; ==========================================================
; Файл определения модуля
; ==========================================================
NAME EASYTTY
DESCRIPTION 'Приложение EASYTTY, (C) 1994, Frolov G.V.'
EXETYPE windows
STUB 'winstub.exe'
STACKSIZE  16384
HEAPSIZE  16384
CODE preload moveable discardable
DATA preload moveable multiple

7.4. Приложение PHONE

Телекоммуникационное приложение EASYTTY демонстрирует основные приемы использования функций Windows, предназначенных для работы с портами асинхронного последовательного адаптера.

Приложение PHONE создано специально для работы с модемами и демонстрирует приемы передачи модему AT-команд и автоматического реагирования на ответ модема. Главная диалоговая панель приложения представляет собой панель телефонного аппарата и показана на рисунке 7.3.

Рис. 7.3. Приложение PHONE

Вы можете набрать на клавиатуре, расположенной в левой части диалоговой панели "Телефон", любой телефонный номер. Введенный номер отображается в специальном поле справа вверху. Если цифра номера введена неправильно, ее можно стереть, нажав на кнопку "<-".

Чтобы модем приступил к набору номера, нажмите кнопку "Набор". Если после набора номера произошло соединение с удаленным модемом, вы можете разорвать связь и повесить трубку, нажав кнопку "Сброс". По окончании работы с приложением, его можно завершить, нажав на кнопку "Выход". Во время инициализации модема, набора номера и попытки установления связи с удаленным модемом завершение приложения не разрешается и кнопка "Выход" блокируется.

Перед тем как запустить приложение PHONE на вашем компьютере, следует создать в каталоге Windows файл PHONE.INI (см. листинг 7.9).

Листинг 7.9. Файл PHONE.INI

[Port]
Mode=COM2:19200,N,8,1
DsrDtrFlow=1
CtsRtsFlow=1
RtsDisable=0
DtrDisable=0

[Modem]
Init=ATQ0V1X4&C1&D2M1
Dial=ATDP
HangUp=ATH0
ModemTimeout=5000
DialTimeout=60000
HangUpTimeout=2000

Файл PHONE.INI должен содержать раздел [Port]. В этом разделе расположены строки, определяющие используемый модемом COM-порт, скорость обмена, формат передаваемых данных, режим управления потоком.

Строка Mode определяет используемый COM-порт, скорость обмена и формат передаваемых данных. Формат строки Mode совпадает с форматом команды MODE операционной системы MS-DOS.

Строки DsrDtrFlow, CtsRtsFlow, RtsDisable и DtrDisable используется для указания режима управления потоком. Числа, указанное вами в этих строках, будут записаны в соответствующие поля структуры DCB.

Далее должен находиться раздел [Modem]. В нем расположены строки, содержащие AT-команды инициализации модема и определены интервалы времени, отведенные на выполнение различных операций.

Строка Описание
Init Команда инициализации модема. Инициализация выполняется один раз при запуске приложения PHONE
Dial Команда набора номера. Передается модему по нажатию кнопки "Набор"
HangUp Команда разрыва связи. Передается модему по нажатию кнопки "Сброс"
ModemTimeout Промежуток времени, в течение которого модем должен отреагировать на передачу ему AT-команды
DialTimeout Промежуток времени, в течение которого модем должен набрать номер и установить связь с удаленным модемом
HangUpTimeout Величина задержки перед и после передачи модему Escape-последовательности. Используется для переключения модема в командный режим

Временные интервалы, определяемые строками ModemTimeout, DialTimeout и HangUpTimeout, должны быть указаны в миллисекундах.

Исходный текст главного файла приложения PHONE приведен в листинге 7.10.

Листинг 7.10. Файл PHONE.CPP

#define      STRICT

#include    <windows.h>
#include    <string.h>
#include    <stdlib.h>
#include    <mem.h>
#include    <bwcc.h>
#include    "phone.h"

// Глобальные переменные
HINSTANCE  hInst;                     // Идентификатор приложения
int                     idComDev;            // Идентификатор COM-порта
char                        sBufNum[20]; // Буфер для телефонного номера
BOOL                     WaitFlag = FALSE;

// ============================================================
// Функция WinMain
// ============================================================
#pragma argsused

int PASCAL
WinMain( HINSTANCE  hInstance,
                         HINSTANCE  hPrevInstance,
                         LPSTR       lpszCmdLine,
                         int        nCmdShow)
{
      static DLGPROC lpfnDlgProc;

      hInst = hInstance;

      // Переходник для функции диалоговой панели
      lpfnDlgProc =
             (DLGPROC)MakeProcInstance((FARPROC)DlgProc, hInst);

      // Создаем модальную диалоговую панель
      DialogBox( hInstance, "PHONE", NULL, lpfnDlgProc );

      return 0;
}

// ============================================================
// Функция DldProc
// ============================================================
#pragma argsused

BOOL CALLBACK _export
DlgProc(HWND hdlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
      int  iErr;

      switch(msg)
      {
             // Инициализация диалоговой панели
             case WM_INITDIALOG:
             {
                   // Посылаем сообщение WM_CONNECT
                   PostMessage( hdlg,WM_CONNECT,0,0L );
                   return TRUE;
             }

             case WM_CONNECT:
             {
                   EnableWindow(GetDlgItem(hdlg, IDCANCEL), FALSE);

                   // Открываем COM-порт и инициализируем модем
                   iErr = InitLine();

                   EnableWindow(GetDlgItem(hdlg, IDCANCEL), TRUE);

                   // Если возникла ошибка, отображаем сообщение
                   // и закрываем диалоговую панель
                   if( iErr < 0 )
                   {
                         ShowError( iErr );
                         EndDialog( hdlg, 0 );
                   }
                   return TRUE;
             }

             case WM_COMMAND:
             {
                   switch(wParam)
                   {
                         // Нажата кнопка номеронаберателя
                         case     ID_1: case       ID_2: case       ID_3: case       ID_4: case       ID_5:
                         case     ID_6:   case     ID_7: case       ID_8: case       ID_9: case       ID_0:
                         {
                                int  iNum;
                                char     sTmp[10];

                                // Определяем набранную цифру
                                iNum = wParam - ID_0;

                                wsprintf( sTmp, "%d", iNum );

                                // Добавляем набранную цифру к номеру
                                lstrcat(sBufNum, sTmp);

                                // Обновляем номер на экране
                                SetDlgItemText(hdlg, ID_NUMBER, sBufNum);

                                return TRUE;
                         }

                         // Нажата кнопка "Сброс"
                         case ID_CLEAR:
                         {
                                EnableWindow(GetDlgItem(hdlg, IDCANCEL), FALSE);

                                // Вешаем трубку
                                HangUpPhone();

                                EnableWindow(GetDlgItem(hdlg, IDCANCEL), TRUE);

                                return TRUE;
                         }

                         // Удаляем последную цифру номера
                         case ID_BACK:
                         {
                                if( lstrlen( sBufNum ) != 0 )
                                {
                                      sBufNum[lstrlen( sBufNum ) - 1] = '\0';
                                      SetDlgItemText( hdlg, ID_NUMBER, sBufNum );
                                }
                                else MessageBeep( MB_ICONASTERISK );

                                return TRUE;
                         }

                         // Нажата кнопка "Набор"
                         case ID_DIAL:
                         {
                                EnableWindow(GetDlgItem(hdlg, IDCANCEL), FALSE);

                                // Набираем номер
                                iErr = DialPhone();

                                // Если возникла ошибка, отображаем сообщение
                                if( iErr < 0 )
                                      ShowError( iErr );

                                EnableWindow(GetDlgItem(hdlg, IDCANCEL), TRUE);
                                return TRUE;
                         }

                         // Нажата кнопка "Выход"
                         case IDCANCEL:
                         {
                                // Закрываем COM-порт
                                CloseLine();

                                // Закрываем диалоговую панель
                                EndDialog( hdlg, 0 );

                                return TRUE;
                         }
                   }
             }
      }
      return FALSE;
}

// ============================================================
// Функция InitLine
// ============================================================
int  InitLine()
{
      BYTE  bSet;
      DCB    dcb;
      int  iErr;
      char     sTmp[200];
      char     szModemAnswer[200];

      char     szInitMode[80];
      char     szTmpStrBool[3];
      char     szInitModem[100];
      long     lnModemTimeout;

      // Определяем режим работы COM-порта. Для этого считываем
      // строку Mode из раздела Port файла PHONE.INI
      GetPrivateProfileString( "Port", "Mode", "COM1:9600,n,8,1",
                   szInitMode, sizeof(szInitMode), "phone.ini" );

      // Открываем COM-порт, заданный в строке szInitMode
      wsprintf( sTmp, "COM%c", szInitMode[3] );

      idComDev = OpenComm( sTmp, 4915, 4915 );
      if (idComDev < 0)
             return ERR_NO_OPEN;

      // Заполняем структуру DCB в соответствии с szInitMode
      iErr = BuildCommDCB( szInitMode, &dcb );
      if(iErr < 0)
             return ERR_DCB_BUILD;

      // Изменяем отдельные поля структуры DCB в
      // соответствии с файлом phone.ini

      // Заполняем поля fOutxDsrFlow, fDtrflow, DsrTimeout
      GetPrivateProfileString( "Port", "DsrDtrFlow", "1",
                                                          szTmpStrBool, sizeof(szTmpStrBool),
                                                          "phone.ini" );

      dcb.fOutxDsrFlow = dcb.fDtrflow = bSet =
                                                         (BYTE) atoi(szTmpStrBool);

      dcb.DsrTimeout = (bSet) ? 3 : 0 ;

      // Заполняем поля fOutxCtsFlow, fRtsflow, CtsTimeout
      GetPrivateProfileString( "Port", "CtsRtsFlow", "1",
              szTmpStrBool, sizeof(szTmpStrBool), "phone.ini" );

      dcb.fOutxCtsFlow = dcb.fRtsflow = bSet =
                                                         (BYTE) atoi(szTmpStrBool);

      dcb.CtsTimeout = (bSet) ? 3 : 0 ;

      // Заполняем поле fRtsDisable
      GetPrivateProfileString( "Port", "RtsDisable", "0",
              szTmpStrBool, sizeof(szTmpStrBool), "phone.ini" );

      dcb.fRtsDisable = (BOOL) atoi(szTmpStrBool);

      // Заполняем поле fDtrDisable
      GetPrivateProfileString( "Port", "DtrDisable", "0",
              szTmpStrBool, sizeof(szTmpStrBool), "phone.ini" );

      dcb.fDtrDisable = (BOOL) atoi(szTmpStrBool);

      // Устанавливаем новый режим работы COM-порта
      iErr = SetCommState( &dcb );
      if( iErr < 0 )
             return ERR_DCB_SET;

      // Удаляем данные из входной и выходной очередей COM-порта
      FlushComm(idComDev, 1);
      FlushComm(idComDev, 0);

      // Подаем сигнал DTR
      EscapeCommFunction(idComDev, SETDTR);

      // Определяем команду инициализации модема, для этого
      // считываем строку Init из раздела Modem файла phone.ini
      GetPrivateProfileString( "Modem", "Init", "ATZ",
              szInitModem, sizeof(szInitModem), "phone.ini" );

      // Добавляем к команде символ перевода строки
      wsprintf( sTmp, "%s\r", szInitModem );

      // Передаем команду инициализации модему
      iErr = WriteComm( idComDev, sTmp, lstrlen( sTmp ) );
      if( iErr < 0 )
             return ERR_WRITE;

      // Определяем время ожидания ответа от модема
      GetPrivateProfileString( "Modem", "ModemTimeout", "1000",
              sTmp, sizeof(sTmp), "phone.ini" );

      lnModemTimeout = atol( sTmp );

      // Ожидаем от модема ответ "OK"
      iErr = WaitModemAnswer(   idComDev, (LPSTR*)szOkString,
                                                   szModemAnswer, 200, (DWORD)lnModemTimeout);
      if( iErr < 0 )
             return iErr;

      return 0;
}

// ============================================================
// Функция DialPhone
// ============================================================
int  DialPhone()
{
      int  iErr;
      char     sTmp[80];
      char     szModemAnswer[200];
      char     szDialModem[80];
      long     lnModemTimeout;

      // Определяем команду набора номера. Для этого считываем
      // строку Dial из раздела Modem файла phone.ini
      GetPrivateProfileString( "Modem", "Dial", "ATDP",
              szDialModem, sizeof(szDialModem), "phone.ini" );

      // Формируем во временном буфере sTmp команду набора номера
      wsprintf( sTmp, "%s%s\r", szDialModem, sBufNum );

      // Удаляем данные из входной и выходной очередей COM-порта
      FlushComm(idComDev, 1);
      FlushComm(idComDev, 0);

      // Передаем модему команду набора номера
      iErr = WriteComm(idComDev, sTmp, lstrlen(sTmp) );
      if( iErr < 0 )
             return ERR_WRITE;

      // Определяем время ожидания ответа от модема
      GetPrivateProfileString( "Modem", "DialTimeout", "30000",
              sTmp, sizeof(sTmp), "phone.ini" );

      lnModemTimeout = atol( sTmp );

      // Ожидаем ответ от модема
      iErr = WaitModemAnswer(   idComDev, (LPSTR*)szAnswer,
                                            szModemAnswer, 200, (DWORD)lnModemTimeout);

      BWCCMessageBox(NULL, szModemAnswer, "Ответ модема", MB_OK );

      return iErr;
}

// ============================================================
// Функция CloseLine
// ============================================================
int  CloseLine()
{
      // Сбрасывааем сигнал DTR
      EscapeCommFunction(idComDev, CLRDTR);

      // Удаляем данные из входной и выходной очередей COM-порта
      FlushComm(idComDev, 1);
      FlushComm(idComDev, 0);

      // Закрываем COM-порт
      CloseComm(idComDev);

      return 0;
}

// ============================================================
// Функция HangUp
// ============================================================
int  HangUpPhone()
{
      DWORD   StartTick;
      char           szHangUpString[80], sTmp[80];
      long           lnModemTimeout;

      // Определяем время задержки перед подачей команды "+++"
      GetPrivateProfileString( "Modem", "HangUpTimeout", "2000",
              sTmp, sizeof(sTmp), "phone.ini" );

      lnModemTimeout = atol( sTmp );

      // Определяем команду разрыва связи. Для этого считываем
      // строку Dial из раздела Modem файла PHONE.INI
      GetPrivateProfileString( "Modem", "HangUp", "ATH0",
              szHangUpString, sizeof(szHangUpString), "phone.ini" );

      // Формируем во временном буфере sTmp команду
      // разрыва соединения
      wsprintf( sTmp, "%s\r", szHangUpString );

      // Определяем текущий момент времени
      StartTick = GetTickCount();

      // Формируем задержку
      while( StartTick + (DWORD)lnModemTimeout > GetTickCount() )
             Idle();

      // Передаем модему последовательность "+++" для перевода его
      // в командный режим
      WriteComm( idComDev, "+++", 3);

      // Определяем текущий момент времени
      StartTick = GetTickCount();

      // Формируем задержку
      while( StartTick + (DWORD)lnModemTimeout > GetTickCount() )
             Idle();

      // Передаем модему команду разорвать соединение и
      // повесить трубку
      WriteComm( idComDev, sTmp, lstrlen( sTmp ) );

      EscapeCommFunction(idComDev, CLRDTR);

      return 0;
}

// ============================================================
// Функция ShowError
// ============================================================
void     ShowError( int iErr )
{
      int  iNum;
      char     szMsgBuff[40];

      // Неизвестная ошибка
      if(( iErr < -6 ) || ( iErr >= 0 ))
             return;

      // Загружаем из ресурсов приложения строку
      // с идентификатором iErr
      LoadString( hInst, iErr, szMsgBuff, 40 );

      // Отображаем на экране сообщение об ошибке
      BWCCMessageBox( NULL, szMsgBuff, "Ошибка",
                                       MB_OK | MB_ICONSTOP   );

      // Подаем звуковой сигнал
      MessageBeep( MB_ICONASTERISK );

      return;
}

// ============================================================
// Функция ReadComPort
// ============================================================
int ReadComPort(int idComDev, LPSTR szDest, int nLength)
{

      COMSTAT ComStat;
      int nTotalRead = 0, nRead = 0;

      while(nLength > nTotalRead)
      {
             // Определяем состояние COM-порта
             GetCommError(idComDev,&ComStat);

             // Во входной очереди нет данных
             if (ComStat.cbInQue == 0)
                   break;

             // Считываем данные в буфер szDest
             else
                   nRead = ReadComm(idComDev,&(szDest[nTotalRead]),
                                    nLength - nTotalRead);
             // Если функция ReadComm завершилась с
             // ошибкой, возвращаем -1
             if (nRead < 0)
                   return -1;

             nTotalRead += nRead;
      }

      // Возвращаем количество байт, прочитанных из COM-порта
      return nTotalRead;
}

// ============================================================
// Функция WaitModemAnswer
// ============================================================
int WaitModemAnswer(int idComDev, LPSTR *szWaitDest,
                                        LPSTR szModemAnswer, int nLength,
                                        DWORD dwTimeOut)
{
      int  nRead;
      int  nTotalChar = 0, i;
      DWORD   dwStartTick;
      BOOL  fFind = FALSE;

      // Определяем текущий момент времени
      dwStartTick = GetTickCount();

      do
      {
             // Считываем данные из COM-порта
             nRead = ReadComPort(idComDev, &szModemAnswer[nTotalChar],
                                            nLength - lstrlen(szModemAnswer));

             // В случае ошибки возвращаем ERR_READ
             if(nRead < 0)
                   return ERR_READ;

             else if(nRead > 0)
             {
                   nTotalChar += nRead;

                   // Добавляем в конец полученных данных двоичный ноль
                   szModemAnswer[nTotalChar] = '\0';

                   // Проверяем, получен ли от модема ответ из
                   // массива szWaitDest
                   for(int i = 0; szWaitDest[i]; i++)
                   {
                         if(strstr(szModemAnswer, szWaitDest[i]))
                         {
                                // Если ответ получен, завершаем чтение данных
                                // и возвращаем управвление
                                fFind = TRUE;
                                break;
                         }
                   }
             }

             // Разрешаем обработку сообщений для других приложений
             else
                   Idle();

      // Выходим из цикла, если от модема получен ожидаемый ответ,
      // возникла ошибка при чтении данных, или истекло время
      // ожидания ответа (nTimeOut * 1000)

      } while (!fFind && nRead >= 0 &&
                         dwStartTick + dwTimeOut > GetTickCount() );

      if(nRead >= 0 && !fFind)
             return ERR_TIMEOUT;

      // Возвращаем количесттво пррочитанных символов
      return lstrlen( szModemAnswer );
}

// ============================================================
// Функция Idle
// ============================================================
void     Idle(void)
{
      MSG msg;

      while( PeekMessage( &msg, NULL, NULL, NULL, PM_REMOVE ) )
      {
             TranslateMessage( &msg );
             DispatchMessage( &msg );
      }

      return;
}

Функция WinMain, приложения PHONE, содержит всего несколько строк. В ней нет привычного вызова функций регистрации класса окна и создания окна. Вместо этого WinMain сразу выводит на экран модальную диалоговую панель PHONE, определенную в файле ресурсов приложения PHONE.RC.

Для создания модальной диалоговой панели мы воспользовались функцией DialogBox. Перед вызовом функции DialogBox вызывается функция MakeProcInstance. Она создает переходник для функции диалога DlgProc. Более подробную информацию о создании диалоговых панелей можно получить в 12 томе "Библиотеки системного программиста".

#pragma argsused

int PASCAL
WinMain(HINSTANCE hInstance,
      HINSTANCE hPrevInstance,
      LPSTR     lpszCmdLine,
      int       nCmdShow)
{
      static DLGPROC lpfnDlgProc;

      hInst = hInstance;

      // Переходник для функции диалоговой панели
      lpfnDlgProc =
             (DLGPROC)MakeProcInstance((FARPROC)DlgProc, hInst);

      // Создаем модальную диалоговую панель
      DialogBox( hInstance, "PHONE", NULL, lpfnDlgProc );

      return 0;
}

Функция диалоговой панели DlgProc обрабатывает сообщения WM_INITDIALOG, WM_CONNECT, WM_COMMAND.

Обработчик сообщения WM_INITDIALOG посылает функции диалоговой панели DlgProc сообщение WM_CONNECT и возвращает значение TRUE:

PostMessage( hdlg,WM_CONNECT,0,0L );
return TRUE;

Сообщение WM_CONNECT определено во включаемом файле PHONE.H следующим образом:

#define WM_CONNECT  WM_USER

Обработчик сообщения WM_CONNECT блокирует кнопку "Сброс" на время инициализации модема, вызывая функцию EnableWindow. После блокировки кнопки "Сброс" вызывается функция InitLine.

// Блокируем кнопку "Сброс"
EnableWindow(GetDlgItem(hdlg, IDCANCEL), FALSE);

// Открываем COM-порт и инициализируем модем
iErr = InitLine();

// Разблокируем кнопку "Сброс"
EnableWindow(GetDlgItem(hdlg, IDCANCEL), TRUE);

Функция InitLine, определенная в приложении, открывает COM-порт, устанавливает режим его работы. После этого InitLine передает модему команду инициализации и ждет ответ в течении некоторого времени.

Если модем не возвратил ответ OK, функция InitLine возвращает отрицательную величину. В этом случае приложение сначала вызывает функцию ShowError, которая отображает сообщение об ошибке, а затем функцию EndDialog, которая закрывает диалоговую панель.

// Если возникла ошибка отображаем сообщение
// и закрываем диалоговую панель
if( iErr < 0 )
{
      ShowError( iErr );
      EndDialog( hdlg, 0 );
}

Когда пользователь нажимает на любую кнопку диалоговой панели PHONE, вызывается обработчик сообщения WM_COMMAND. Параметр wParam сообщения WM_COMMAND содержит идентификатор нажатой кнопки. В зависимости от его значения обработчик WM_COMMAND выполняет различные действия.

Если нажата одна из цифровых кнопок "1", "2", "3" ... "0", соответствующая цифра добавляется в конец буфера sBufNum. В этом буфере подготавливается строка с телефонным номером.

Чтобы стереть последнюю цифру номера, можно нажать кнопку "<-". Эта кнопка имеет идентификатор ID_BACK. Обработчик сообщений WM_COMMAND, имеющий параметр wParam, равный ID_BACK, стирает последнюю цифру из буфер sBufNum. Если все символы из буфера sBufNum удалены, вызывается функция MessageBeep, подающая звуковой сигнал.

Набор телефонного номера, записанного в буфере sBufNum, происходит после нажатия на кнопку "Набор". В этом случае в функцию окна приходит сообщение WM_COMMAND с параметром wParam, равным ID_DIAL. Обработчик этого сообщения блокирует кнопку "Выход" и вызывает функцию DialPhone, определенную в приложении, которая и производит набор номера.

Если во время набора номера возникла ошибка, функция DialPhone возвращает отрицательное значение. В этом случае вызывается функция ShowError, которая отображает на экране диалоговую панель с кратким описанием возникшей ошибки. Перед окончанием обработки сообщения разблокируется кнопка "Выход".

case ID_DIAL:
{
      EnableWindow(GetDlgItem(hdlg, IDCANCEL), FALSE);
      // Набираем номер
      iErr = DialPhone();

      // Если возникла ошибка, отображаем сообщение
      if( iErr < 0 )
             ShowError( iErr );

      EnableWindow(GetDlgItem(hdlg, IDCANCEL), TRUE);
      return TRUE;
}

Чтобы завершить приложение, надо нажать кнопку "Выход". В этом случае в функцию окна приходит сообщение WM_COMMAND с параметром wParam равным IDCANCEL. Обработчик этого сообщения закрывает COM-порт, вызывая функцию CloseLine.

Затем он вызывает функцию EndDialog, которая закрывает диалоговую панель и завершает приложение.

case IDCANCEL:
{
      // Закрываем COM-порт
      CloseLine();

      // Закрываем диалоговую панель
      EndDialog( hdlg, 0 );

      return 0;
}

После того как модем набрал телефонный номер и соединился с удаленным компьютером, вы можете разорвать это соединение, нажав кнопку "Сброс". При этом вызывается соответствующий обработчик:

case ID_CLEAR:
{
      EnableWindow(GetDlgItem(hdlg, IDCANCEL), FALSE);

      // Вешаем трубку
      HangUpPhone();

      EnableWindow(GetDlgItem(hdlg, IDCANCEL), TRUE);

      return TRUE;
}

Он блокирует кнопку "Выход", а затем вызывает функцию HangUpPhone. Функция HangUpPhone вешает трубку и разрывает связь с удаленным модемом.

Коды ошибок, идентификаторы диалоговой панели PHONE, а также два массива строк szOkString и szAnswer определены в файле PHONE.H (см. листинг 7.11).

Листинг 7.11. Файл PHONE.H

// Прототипы функций
BOOL  CALLBACK _export DlgProc(HWND, UINT, WPARAM, LPARAM);

int  InitLine(void);
int  DialPhone(void);
int  CloseLine(void);
int  HangUpPhone(void);
void     ShowError(int iErr);
void     Idle(void);
int  WaitModemAnswer(int idComDev, LPSTR *szWaitDest,
                                   LPSTR szModemAnswer, int nLength,
                                   DWORD dwTimeOut);

// Определение констант
#define      WM_CONNECT WM_USER

#define      ERR_NO_OPEN      (-1)
#define      ERR_DCB_BUILD    (-2)
#define      ERR_DCB_SET      (-3)
#define      ERR_WRITE        (-4)
#define      ERR_TIMEOUT      (-5)
#define      ERR_READ         (-6)

#define ID_0 100
#define ID_1 101
#define ID_2 102
#define ID_3 103
#define ID_4 104
#define ID_5 105
#define ID_6 106
#define ID_7 107
#define ID_8 108
#define ID_9 109
#define ID_REPEAT 111
#define ID_CLEAR  130
#define ID_BACK   123
#define ID_NUMBER 120
#define ID_DIAL 122

// Определение массивов строк szOkString и szAnswer
char *szOkString[] = { "OK", NULL };

char *szAnswer[] = {
                     "OK",    "CONNECT",
                     "RING",  "NO CARRIER",
                     "ERROR", "NO DIAL TONE",
                     "BUSY",  "NO ANSWER",
                     NULL
                   };

В листинге 7.12 представлен исходный текст файла PHONE.RC, содержащего описание ресурсов приложения. В нем описаны диалоговая панель PHONE, пиктограмма PHONE и таблица строк.

Листинг 7.12. Файл PHONE.RC

#include "phone.h"

PHONE DIALOG 59, 29, 131, 79
STYLE WS_POPUP | WS_VISIBLE | WS_BORDER
CLASS "BorDlg"
CAPTION "Телефон"
BEGIN
      PUSHBUTTON "1", ID_1, 8, 9, 14, 14,
             WS_CHILD | WS_VISIBLE | WS_TABSTOP
      PUSHBUTTON "2", ID_2, 24, 9, 14, 14,
             WS_CHILD | WS_VISIBLE | WS_TABSTOP
      PUSHBUTTON "3", ID_3, 40, 9, 14, 14,
             WS_CHILD | WS_VISIBLE | WS_TABSTOP
      PUSHBUTTON "4", ID_4, 8, 25, 14, 14,
             WS_CHILD | WS_VISIBLE | WS_TABSTOP
      PUSHBUTTON "5", ID_5, 24, 25, 14, 14,
             WS_CHILD | WS_VISIBLE | WS_TABSTOP
      PUSHBUTTON "6", ID_6, 40, 25, 14, 14,
             WS_CHILD | WS_VISIBLE | WS_TABSTOP
      PUSHBUTTON "7", ID_7, 8, 41, 14, 14,
             WS_CHILD | WS_VISIBLE | WS_TABSTOP
      PUSHBUTTON "8", ID_8, 24, 41, 14, 14,
             WS_CHILD | WS_VISIBLE | WS_TABSTOP
      PUSHBUTTON "9", ID_9, 40, 41, 14, 14,
             WS_CHILD | WS_VISIBLE | WS_TABSTOP
      PUSHBUTTON "0", ID_0, 24, 57, 30, 14,
             WS_CHILD | WS_VISIBLE | WS_TABSTOP
      PUSHBUTTON "#", ID_REPEAT, 8, 57, 14, 14,
             WS_CHILD | WS_VISIBLE | WS_TABSTOP
      PUSHBUTTON "Выход", IDCANCEL, 78, 57, 32, 14,
             WS_CHILD | WS_VISIBLE | WS_TABSTOP
      PUSHBUTTON "Сброс", ID_CLEAR, 66, 41, 28, 14,
             WS_CHILD | WS_VISIBLE | WS_TABSTOP
      PUSHBUTTON "Набор", ID_DIAL, 78, 25, 32, 14,
             WS_CHILD | WS_VISIBLE | WS_TABSTOP
      CONTROL "", ID_NUMBER, "EDIT",   ES_LEFT | ES_READONLY |
             WS_CHILD | WS_VISIBLE | WS_BORDER, 66, 10, 58, 12
      PUSHBUTTON "<--", ID_BACK, 96, 41, 28, 14,
             WS_CHILD | WS_VISIBLE | WS_TABSTOP
      CONTROL "", 110, "BorShade", 1 | WS_CHILD | WS_VISIBLE,
             4, 4, 54, 72
      CONTROL "", 112, "BorShade", 1 | WS_CHILD | WS_VISIBLE,
             62, 4, 66, 72
END

PHONE ICON "phone.ico"

STRINGTABLE
BEGIN
      ERR_NO_OPEN,      "COM-порт не открыт"
      ERR_DCB_BUILD,    "Ошибка DCB"
      ERR_DCB_SET,      "Ошибка при установке режимма COM-порта"
      ERR_WRITE,        "Ошибка при записи в COM-порта"
      ERR_TIMEOUT,      "Модем не отвечает"
      ERR_READ,         "Ошибка чтения из COM-порта"
END

В листинге 7.13 приведено изображение пиктограммы, расположенной в файле PHONE.ICO, на который ссылается оператор ICON в файле PHONE.RC.

Листинг 7.13. Файл PHONE.ICO

Файл определения модуля для приложения PHONE приведен в листинге 7.14.

Листинг 7.14. Файл PHONE.DEF

; ==========================================================
; Файл определения модуля
; ==========================================================

NAME PHONE
DESCRIPTION 'Приложение PHONE, (C) 1994, Frolov G.V.'
EXETYPE windows
STUB 'winstub.exe'
STACKSIZE   16384
HEAPSIZE    16384
CODE preload moveable discardable
DATA preload moveable multiple

7.5. Приложение TELETYPE

Приложение TELETYPE демонстрирует использование телекоммуникационных функций Windows и сообщения WM_COMMNOTIFY для работы с портами асинхронного последовательного адаптера и модемами.

Приложение TELETYPE выполняет все основные функции, которые должна поддерживать любая телекоммуникационная программа. TELETYPE позволяет передавать модему AT-команды, принимать от него ответ и отображать его на экране.

Перед тем как запускать приложение TELETYPE на вашем компьютере, следует создать в каталоге Windows файл TELETYPE.INI (см. листинг 7.15).

Листинг 7.15. Файл TELETYPE.INI

[Port]
Mode=COM2:9600,N,8,1

Файл TELETYPE.INI должен содержать раздел [Port], состоящий из единственной строки Mode. В этой строке определяется номер COM-порта, к которому подключен модем, скорость обмена и формат передаваемых данных. Формат строки Mode совпадает с форматом команды MODE операционной системы MS-DOS.

Запустите TELETYPE. Набирая на клавиатуре AT-команды модема, можно перевести его в любой режим. Можно сбросить текущую конфигурацию. Для этого введите команду ATZ и нажмите клавишу <Enter>. В ответ на введенную команду модем загрузит принятую по умолчанию конфигурацию и вернет компьютеру ответ OK.

Чтобы приложение TELETYPE могло автоматически отвечать на вызов по телефонной линии, передайте модему команду ATS0=1 и нажмите клавишу <Enter>. Поэкспериментируйте с приложением TELETYPE, передавая модему различные команды и наблюдая на экране ответные сообщения.

Рис. 7.4. Приложение TELETYPE

Вы можете передать модему команду набора номера удаленного абонента. Чтобы набрать номер 987-65-43 достаточно ввести команду ATDP 987-65-43 и нажать клавишу <Enter>.

После установления связи с удаленным модемом вы можете обменяться с ним текстовыми сообщениями. Набирайте передаваемый текст на клавиатуре и просматривайте ответ от удаленного компьютера в главном окне приложения.

Когда вы закончите сеанс связи с удаленным компьютером, переведите модем в командный режим и передайте ему команду ATH0. Модем повесит трубку и отключится от телефонной линии.

Чтобы перевести модем из режима передачи данных в командный режим, подождите 2-3 секунды, наберите на клавиатуре три знака '+' и дождитесь от модема ответа OK. Внешний вид главного окна приложения TELETYPE представлен на рисунке 7.4.

Главный файл приложения TELETYPE приведен в листинге 7.16. После запуска TELETYPE управление получает главная функция приложения WinMain.

Если параметр hPrevInstance функции WinMain равен нулю, WinMain вызывает функцию инициализации приложения InitApp. В противном случае на экран выводится сообщение о невозможности запуска второй копии приложения.

Функция InitApp регистрирует класс окна WMODEM и возвращает управление функции WinMain. На базе зарегистрированного класса окна создается и отображается главное окно приложения, после чего запускается обычный цикл обработки сообщений.

При создании окна в функцию окна передается сообщение WM_CREATE. Получив это сообщение, функция окна вызывает функцию InitTTY, которая определяет различные параметры окна и сохраняет их в глобальных переменных. Затем вызывается функция PostMessage, которая отправляет сообщение WM_CONNECT функции главного окна приложения. Сообщение WM_CONNECT определено во включаемом файле TELETYPE.H следующим образом:

#define WM_CONNECT  WM_USER

Константа WM_USER специально предназначена для определения приложениями своих собственных кодов сообщений.

Обработчик сообщения WM_CONNECT вызывает функцию InitCommPort. Эта функция открывает и инициализирует COM-порт в соответствии с данными из файла TELETYPE.INI, разрешает генерацию драйвером COM-порта сообщений WM_COMMNOTIFY и устанавливает сигнала DTR.

После обработки сообщения WM_CONNECT можно набирать на клавиатуре текст. Для получения ввода с клавиатуры наше приложение обрабатывает сообщение WM_CHAR, вызывая функцию UserChat, которая передает код введенного символа драйверу COM-порта и отображает его на экране.

Меню приложения TELETYPE содержит две строки: "Информация" и "Выход". При выборе строки "Информация" на экране отображается короткая информация о приложении.

Когда вы закончите работать с TELETYPE, завершите приложение, выбрав из меню строку "Выход". В этом случае вызывается функция PostMessage, передающая функции окна сообщение WM_CLOSE.

Обработчик сообщения WM_CLOSE вызывает функцию CloseCommPort, которая закрывает COM-порт и завершает приложение.

Листинг 7.16. Файл TELETYPE.CPP

// Определяем константу MAIN_MODULE. Она используется
// в файле TELE.H
#define MAIN_MODULE

#include <windows.h>
#include <mem.h>
#include <bwcc.h>
#include "tele.h"

// Имя класса главного окна приложения
char     szClassName[] = "WMODEM";

// Заголовок главного окна приложения
char     szWindowTitle[] = "Телетайп";

//=============================================================
// Функция WinMain
//=============================================================
#pragma argsused

int PASCAL
WinMain( HINSTANCE hInstance,
                   HINSTANCE  hPrevInstance,
                   LPSTR       lpszCmdLine,
                   int        nCmdShow)
{
      MSG  msg;   // структура для работы с сообщениями
      HWND hwnd;  // идентификатор главного окна приложения

      // Проверяем, не запускалось ли это приложение ранее
      if(!hPrevInstance)
      {
             // Сохраняем в глобальной переменной hInst идентификатор
             // приложения
             hInst = hInstance;

             // Если не запускалось, вызываем функцию InitApp
             if(!InitApp(hInstance))
                   return FALSE;
      }
      else
      {
             MessageBeep(MB_ICONASTERISK);

             BWCCMessageBox( NULL,
                   "Можно запускать только одну копию приложения",
                   "Ошибка", MB_OK | MB_ICONSTOP);

             return FALSE;
      }

      // Создаем главное окно приложения
      hwnd = CreateWindow(
             szClassName,
             szWindowTitle,
             WS_OVERLAPPED | WS_VISIBLE,
             CW_USEDEFAULT, CW_USEDEFAULT,
             CW_USEDEFAULT, CW_USEDEFAULT,
             NULL, NULL,
             hInstance,
             NULL);

      // Если создать окно не удалось, завершаем приложение
      if(!hwnd)
      {
             MessageBeep(MB_ICONASTERISK);

             BWCCMessageBox( NULL,
                   "Ошибка при создании главного окна приложения",
                   "Ошибка", MB_OK | MB_ICONSTOP);

             return FALSE;
      }

      // Рисуем окно
      ShowWindow(hwnd, nCmdShow);

      // Передаем функции окна сообщение WM_PAINT
      UpdateWindow(hwnd);

      // Запускаем цикл обработки сообщений
      while (GetMessage(&msg,NULL,0,0))
      {
             TranslateMessage(&msg);
             DispatchMessage(&msg);
      }

      return msg.wParam;
}

// ============================================================
// Функция InitApp
// ============================================================
BOOL
InitApp(HINSTANCE hInstance)
{
      ATOM aWndClass;        // атом для кода возврата
      WNDCLASS wndclass;  // структура для регистрации
                                                               // класса окна

      // Запмсываем нулевые значения во все поля структуры
      memset(&wndclass, 0, sizeof(wndclass));

      wndclass.style                        = CS_HREDRAW | CS_VREDRAW;
      wndclass.lpfnWndProc         = (WNDPROC) WndProc;
      wndclass.cbClsExtra        = 0;
      wndclass.cbWndExtra           = 0;
      wndclass.hInstance         = hInstance;
      wndclass.hIcon                       = LoadIcon(hInstance, "PHONE");
      wndclass.hCursor                   = LoadCursor( NULL, IDC_ARROW );
      wndclass.hbrBackground     = GetStockObject( WHITE_BRUSH );
      wndclass.lpszMenuName     = (LPSTR) "APP_MENU";
      wndclass.lpszClassName      = (LPSTR) szClassName;

      // Регистрируем класс окна szClassName
      aWndClass = RegisterClass(&wndclass);

      // Возвращаем результат регистрации класса
      return(aWndClass != 0);
}

// ============================================================
// Функция окна WndProc
// ============================================================
LRESULT CALLBACK _export
WndProc( HWND hwnd,
                         UINT message,
                         WPARAM wParam,
                         LPARAM lParam )
{
      switch( message )
      {
             case WM_CREATE:
             {
                   // Определяем параметры окна
                   InitTTY( hwnd );

                   // Отправляем сообщение WM_CONNECT
                   PostMessage( hwnd,WM_CONNECT,0,0L );
                   return 0;
             }

             case WM_CONNECT:
             {
                   // Открываем и инициализируем COM-порт
                   idOpenCommPort = InitCommPort(hwnd);

                   // В случае ошибки отображаем сообщение и завершаем
                   // приложение
                   if (idOpenCommPort < 0)
                   {
                         MessageBeep(MB_ICONASTERISK);
                         BWCCMessageBox(hwnd,"COM-порт не открыт","Ошибка",
                                MB_ICONSTOP | MB_OK);

                         PostMessage(hwnd,WM_CLOSE,0,0L);
                   }
                   return 0;
             }

             case WM_COMMNOTIFY:
             {
                   // Вызываем обработчик сообщения WM_COMMNOTIFY
                   ProcessCommNotify( hwnd, wParam, LOWORD( lParam ));
                   return 0;
             }

             case WM_SETFOCUS:
             {
                   // Приложение получило фокус ввода
                   SetFocusTTY(hwnd);
                   return 0;
             }

             case WM_KILLFOCUS:
             {
                   // Приложение потеряло фокус ввода
                   KillFocusTTY(hwnd);
                   return 0;
             }

             case WM_CHAR:
             {
                   // Пользователь нажал на клавишу
                   UserChat(hwnd, message, wParam, lParam);
                   return 0;
             }

             case WM_COMMAND:
             {
                   switch ( wParam )
                   {
                         case CM_EXIT:
                         {
                                PostMessage( hwnd, WM_CLOSE, 0, 0L );
                                break;
                         }

                         case CM_ABOUT:
                         {
                                About(hwnd);
                                break;
                         }
                   }
                   return 0;
             }

             case WM_CLOSE:
             {
                   // Закрываем COM-порт и завершаем приложение
                   CloseCommPort(idOpenCommPort);

                   DestroyWindow( hwnd );
                   PostQuitMessage( 0 );
                   return 0;
             }

             default:
                   return( DefWindowProc( hwnd, message, wParam, lParam ) );
      }
}

// ============================================================
// Функция About
// ============================================================
void About(HWND hwnd)
{
      BWCCMessageBox(hwnd,
                   "Телекоммуникационная программа\n\n"
                   "(C) Фролов Г.В., 1994",
                   "Информация",
                   MB_OK | MB_ICONINFORMATION);

      return;
}

В файле CONNECT.CPP (см. листинг 7.17) содержится определение функции UserChat. Эта функция предназначен для обработки сообщения WM_CHAR, поступающего функции окна в ответ на ввод с клавиатуры.

Функция UserChat проверяет, нажал ли пользователь клавишу <Enter> (<Return>). Если нажал, то в рабочий массив szTemp записываются три символа '\r', '\n' и '\0'. Таким образом, в szTemp записывается строка "\r\n". В противном случае в szTemp записывается только код нажатой клавиши, содержащийся в параметре wParam и символ '\0'.

Строка, подготовленная в массиве szTemp, передается в COM-порт и далее модему.

WriteComm( idOpenCommPort,(LPSTR)szTemp,
                  lstrlen((LPCSTR)szTemp))

После вызова функции WriteComm приложение вызывает функцию GetCommError, которая сбрасывает код ошибки, если таковая случилась при передаче. В нашем приложении полученный код ошибки не обрабатывается.

Переданная модему строка не отображается автоматически в окне приложения. Для этого строка, записанная в szTemp, передается функции WriteTTY, определенной в файле TTY.CPP.

WriteTTY( szTemp, hwnd, DARK_BLUE );

Первый параметр функции WriteTTY должен содержать строку, которую надо отобразить на экране, второй параметр - идентификатор окна и третий - цвет отображаемой строки. Мы указали в качестве третьего параметра константу DARK_BLUE, соответствующую синему цвету. Константы DARK_BLUE и DARK_GREEN, используемые в нашем приложении определены в файле TELETYPE.H.

Листинг 7.17. Файл CONNECT.CPP

#include <windows.h>
#include "tele.h"

// ============================================================
// Функция UserChat
// ============================================================
#pragma argsused

LRESULT
UserChat(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
      char     szTemp[3];      // Рабочий массив

      switch (wParam)
      {
             // Пользователь нажал клавишу <Return> (<Enter>)
             case VK_RETURN:

                   szTemp[0] = '\r';
                   szTemp[1] = '\n';
                   szTemp[2] = '\0';

                   break;

             // Пользователь нажал другую клавишу (не <Return>)
             default:
                   szTemp[0] = (char)wParam;
                   szTemp[1] = '\0';

                   break;
      }

      // Передаем код нажатой клавиши в COM-порт
      WriteComm( idOpenCommPort,(LPSTR)szTemp,
                                      lstrlen((LPCSTR) szTemp));

      // Получаем и сбрасываем код ошибки
      GetCommError( idOpenCommPort,NULL );

      // Отображаем символ нажатой клавиши на экране
      WriteTTY( szTemp, hwnd, DARK_BLUE );

      return 0;
}

В файле COMMPORT.CPP (см. листинг 7.18) определены основные функции, непосредственно взаимодействующие с COM-портом - InitComPort, CloseComPort и ReadCommPort.

Функция InitComPort выполняет все действия по инициализации COM-порта. Рассмотрим ее более подробно.

Сначала InitComPort определяет режим работы COM-порта, для этого она считывает строку Mode из раздела Port файла TELETYPE.INI. Затем функция OpenComm открывает соответствующий COM-порт.

Потом функция BuildCommDCB заполняет структуру DCB, которая передается функции SetCommState. Функция SetCommState устанавливает новый режим работы порта. Сразу после открытия COM-порта в его буферах могут остаться данные. Чтобы их удалить, мы использовали функцию FlushComm.

В приложении TELETYPE мы обрабатываем сообщения WM_COMMNOTIFY, вырабатываемые драйвером COM-порта. Для того чтобы разрешить генерацию этих сообщений, предназначена функция EnableCommNotification.

if(!EnableCommNotification(idCommPort,hwnd,32,-1))
return -1;

После вызова EnableCommNotification функции окна будут поступать сообщения WM_COMMNOTIFY с кодом извещения CN_RECEIVE, если во входную очередь COM-порта поступило больше 32 символов или истек тайм-аут. Более подробную информацию о функции EnableCommNotification можно получить в разделах "Функция EnableCommNotification" и "Сообщение WM_COMMNOTIFY".

Затем вызывается функция EscapeCommFunction, которая устанавливает сигнал DTR, сообщая модему, что компьютер готов к обмену данными.

На этом функция InitCommPort заканчивает свою работу и возвращает вызывающей процедуре идентификатор открытого COM-порта.

Самая простая функция нашего приложения, предназначенная для работы с COM-портами, называется CloseComPort. Она сбрасывает сигнал DTR и закрывает COM-порт.

В файле COMMPORT.CPP также определена функция ReadComPort, предназначенная для чтения данных из выходной очереди COM-порта.

Функция имеет три параметра. Первый параметр idComDev определяет идентификатор COM-порта, из которого будут прочитаны данные. Второй параметр szDest должен содержать адрес буфера, в который будет записана поступающая информация, а последний параметр nLength указывает размер этого буфера.

Функция ReadCommPort содержит внутри себя цикл, в котором происходит чтение данных из выходной очереди COM-порта. Мы выполняем чтение из входной очереди в цикле, так как при больших скоростях передачи информации (больше 9600 бит/с) за время чтения данных из очереди, в нее могут поступить новые данные.

В цикле сначала вызывается функция GetCommError. Она заполняет структуру ComStat типа COMSTAT. Нас интересует только поле cbInQue этой структуры. В нем записано, сколько байт находится во входной очереди COM-порта. Если в очереди есть данные, считываем их, вызывая функцию ReadComm. В противном случае выходим из цикла чтения и возвращаем вызывающей процедуре количество прочитанных байт.

Листинг 7.18. Файл COMMPORT.CPP

#include <windows.h>
#include "tele.h"

// ============================================================
// Функция InitComPort
// ============================================================
int InitCommPort(HWND hwnd)
{
      DCB    dcb;                // структура DCB
      int  idCommPort;           // идентификатор COM-порта
      char     szPort[6];        // имя порта
      char     szInitMode[40];   // режим работы
      int  nResult;              // временная переменная

      // Определяем режим работы COM-порта, для этого считываем
      // строку Mode из раздела Port файла phone.ini
      GetPrivateProfileString("Port", "Mode", "COM2:2400,n,8,1",
                               szInitMode, sizeof(szInitMode), "teletype.ini");

      // Открываем COM-порт, заданный в строке szInitMode
      wsprintf( szPort, "COM%c", szInitMode[3] );

      idCommPort = OpenComm(szPort, INQUEUE, OUTQUEUE);
      if (idCommPort < 0)
             return idCommPort;

      // Заполняем структуру DCB в соответствии с szInitMode
      nResult = BuildCommDCB(szInitMode,&dcb);
      if (nResult < 0)
             return nResult;

      // Устанавливаем новый режим работы COM-порта
      nResult = SetCommState(&dcb);
      if (nResult < 0)
             return nResult;

      // Удаляем данные из входной и выходной очередей COM-порта
      FlushComm(idCommPort, 1);
      FlushComm(idCommPort, 0);

      // Разрешаем генерацию сообщения WM_COMMNOTIFY
      if(!EnableCommNotification(idCommPort,hwnd,32,-1))
             return -1;

      // Подаем сигнал DTR
      EscapeCommFunction(idCommPort, SETDTR);

      return idCommPort;
}

// ============================================================
// Функция CloseComPort
// ============================================================
int CloseCommPort(int idCommPort)
{
      // Сбрасываем сигнал DTR
      EscapeCommFunction(idCommPort, CLRDTR);

      // Закрываем COM-порт
      CloseComm(idCommPort);

      return 0;
}

// ============================================================
// Функция ReadComPort
// ============================================================
int ReadCommPort(int idComDev, LPSTR szDest, int nLength)
{

      COMSTAT ComStat;
      int nTotalRead = 0, nRead = 0;

      // Цикл чтения данных из входной очереди COM-порта
      while(nLength > nTotalRead)
      {
             // Определяем, есть ли данные во входной очереди
             GetCommError(idComDev,&ComStat);

             if (ComStat.cbInQue == 0)
                   break;

             // Считываем данные из входной очереди COM-порта
             nRead = ReadComm(idComDev,&(szDest[nTotalRead]),
                              nLength - nTotalRead);
             // Возникла ошибка
             if(nRead < 0)
             {
                   nTotalRead = -nTotalRead - nRead;
                   break;
             }

             nTotalRead += nRead;
      }

      return nTotalRead;
}

Файл COMMSG.CPP (см. листинг 7.19) содержит определение только одной функции ProcessCommNotify. Эта функция выполняет обработку сообщений WM_COMMNOTIFY, вырабатываемых COM-портом.

При получении сообщения WM_COMMNOTIFY с кодом извещения CN_RECEIVE функция ProcessCommNotify считывает данные из входной очереди COM-порта, вызывая функцию ReadCommPort.

Если ReadCommPort возвратила положительную величину, отображаем на экране прочитанную информацию при помощи функции WriteTTY. Обратите внимание, что данные полученные из COM-порта, отображаются зеленым цветом, в то время как данные передаваемые в COM-порт - синим.

Если функция ReadCommPort возвратила отрицательную величину, значит при чтении из COM-порта возникла ошибка, например, был получен байт с ошибкой по четности. В этом случае мы также отображаем полученные данные на экране, а затем вызываем функцию GetCommError, которая определяет код ошибки и сбрасывает регистр ошибок.

Листинг 7.19. Файл COMMSG.CPP

#include <windows.h>
#include "tele.h"

// ============================================================
// Функция ProcessCommNotify
// ============================================================
int ProcessCommNotify(HWND hwnd, int nComID, int nNotification)
{
      int  nResult;                                          // временная переменная
      char     szData[OUTQUEUE+1];        // временный буфер данных

      // Получено сообщение WM_COMMNOTIFY с кодом извещения
      // CN_RECEIVE
      if ( nNotification & CN_RECEIVE )
      {
             // Считываем данные из входной очереди COM-порта
             nResult = ReadCommPort ( nComID, (LPSTR)szData, OUTQUEUE );
             if (nResult > 0)
             {
                   szData[nResult] = 0;

                   // Отображаем считанные из COM-порта данные в окне
                   WriteTTY(szData, hwnd, DARK_GREEN);
             }

             // Возникла ошибка при чтении из COM-порта
             else if (nResult < 0)
             {
                   szData[-nResult] = 0;

                   // Отображаем считанные из COM-порта данные в окне
                   // и возвращаем код ошибки
                   WriteTTY(szData, hwnd, DARK_GREEN);

                   return nResult;
             }

             // Определяем код ошибки и сбрасываем регистр ошибок
            GetCommError(nComID,NULL);
      }

      return nResult;
}

Предусмотрены четыре функции, предназначенные для работы с главным окном приложения TELETYPE. Их имена - InitTTY, WriteTTY, SetFocusTTY, KillFocusTTY. Эти функции определены в файле TTY.CPP (см. листинг 7.20). Так как указанные функции не содержат ничего, непосредственно относящегося к COM-портам, мы опишем их кратко.

Функция InitTTY определяет размеры окна приложения в символах. Главное окно приложения не может изменять свой размер. Поэтому функцию InitTTY достаточно вызвать в начале работы приложения. Прототип функции представлен ниже:

void InitTTY(HWND hwnd);

Функция InitTTY имеет только один параметр hwnd, который должен содержать идентификатор главного окна приложения. Это значение возвращается функцией CreateWindow.

Функция WriteTTY предназначена для отображения данных в главном окне приложения. Прототип функции:

void WriteTTY(LPSTR lpOutString, HWND hwnd,
                   COLORREF rgbColor);

Первый параметр lpOutString должен содержать указатель на строку символов, закрытую двоичным нулем. Эта строка будет выведена на экран.

Второй параметр hwnd должен содержать идентификатор главного окна приложения.

Последний параметр функции WriteTTY - rgbColor. Он определяет цвет символов, которые будут отображаться на экране. В приложении TELETYPE символы, полученные от модема, отображаются зеленым цветом, а символы набираемые пользователем на клавиатуре и передаваемые модему - синим.

Функции SetFocusTTY и KillFocusTTY управляют текстовым курсором, отображаемым в окне приложения. Функция SetFocusTTY отображает курсор, а функция KillFocusTTY - убирает его из окна приложения. Прототипы функций SetFocusTTY и KillFocusTTY аналогичны:

void SetFocusTTY(HWND hwnd);
void KillFocusTTY(HWND hwnd);

Эти функции имеют единственный параметр hwnd, который должен содержать идентификатор главного окна приложения.

Листинг 7.20. Файл TTY.CPP

#include <windows.h>
#include "tele.h"

// Текущее положение курсора
static int    nyCurrRow, nxCurrCol;

// Размер главного окна приложения в символах
static int    nyRows, nxCols;

// Размер символов
static int    nxCharSize, nyCharSize;

// ============================================================
// Функция InitTTY
// ============================================================
void InitTTY(HWND hwnd)
{
      HDC            hdc;           // индекс контекста устройства
      TEXTMETRIC     tiTextMetric;  // структура для записи метрик
                                    // шрифта
      RECT           rcTTYWindow;   // размер окна

      // Определяем начальное положение курсора
      nyCurrRow = 0;
      nxCurrCol = 0;

      // Получаем контекст отображения
      hdc = GetDC( hwnd );

      // Выбираем в контекст отображения шрифт OEM_FIXED_FONT
      SelectObject(hdc, GetStockObject(OEM_FIXED_FONT));

      // Заполняем структуру tiTextMetric информацией о метрике
      // шрифта, выбранного в контекст отображения
      GetTextMetrics(hdc, &tiTextMetric);

      // Освобождаем контекст
      ReleaseDC( hwnd, hdc );

      // Запоминаем среднее значение ширины символов
      nxCharSize = tiTextMetric.tmAveCharWidth;

      // Запоминаем значение высоты символов с учетом
      // межстрочного интервала
      nyCharSize = tiTextMetric.tmHeight +
                                 tiTextMetric.tmExternalLeading;

      // Определяем текущее размеры окна
      GetClientRect(hwnd, &rcTTYWindow);

      // Определяем количество строк, помещающихся в окне
      nyRows = (rcTTYWindow.bottom - rcTTYWindow.top) / nyCharSize;

      // Определяем количество символов, помещающихся в строке окне
      nxCols = (rcTTYWindow.right - rcTTYWindow.left) / nxCharSize;

      return;
}

// ============================================================
// Функция WriteTTY
// ============================================================
void
WriteTTY( LPSTR lpOutString, HWND hwnd, COLORREF rgbColor )
{
      LPSTR       lpCurrChar;     // рабочий указатель
      HDC         hdc;            // индекс контекста устройства

      // Получаем контекст отображения
      hdc = GetDC( hwnd );

      // Выбираем в контекст отображения шрифт OEM_FIXED_FONT
      SelectObject( hdc, GetStockObject(OEM_FIXED_FONT));

      // Устанавливаем цвет текста
      SetTextColor(hdc, rgbColor);

      // Устанавливаем цвет фона текста соответствующий цвету окна
      SetBkColor(hdc, GetSysColor(COLOR_WINDOW));

      // Устанавливаем режим вывода текста
      SetBkMode(hdc, OPAQUE);

      // Выключаем курсор
      HideCaret( hwnd );

      // Отображаем строку lpOutString в окне
      for ( lpCurrChar = lpOutString; *lpCurrChar; lpCurrChar++ )
      {
             switch ( *lpCurrChar )
             {
                   // Возвращаем курсор на одну позицию назад
                   case ASCII_BACK:
                   {
                         if(nxCurrCol)
                                nxCurrCol--;

                         break;
                   }

                   // Переводим курсор в начало текущей строки
                   case ASCII_CR:
                   {
                         nxCurrCol = 0;
                         break;
                   }

                   // Переводим курсор на новую строку
                   case ASCII_LF:
                   {
                         nyCurrRow++;

                         // При необходимости выполняем вертикальную
                         // свертку окна
                         if ( nyCurrRow == ( nyRows - 1 ) )
                         {
                                ValidateRect( hwnd, NULL );
                                ScrollWindow( hwnd, 0, -nyCharSize, NULL, NULL );

                                // Передаем сообщение WM_PAINT
                                UpdateWindow( hwnd );

                                // Изменяем текущее положение курсора
                                nyCurrRow = nyRows - 2;
                         }
                         break;
                   }

                   // Подаем звуковой сигнал
                   case ASCII_BELL:
                   {
                         MessageBeep(MB_OK);
                         break;
                   }

                   default:
                   {
                         // Отображаем очередной символ из строки
                         // lpOutString в текущей позиции окна

                         TextOut( hdc, nxCurrCol * nxCharSize,
                                  nyCurrRow * nyCharSize, lpCurrChar, 1 );

                         // Смещаем курсор вправо
                         nxCurrCol++;

                         // При необходимости переходим на следующую строку
                         if ( nxCurrCol == ( nxCols - 1 ) )
                         {
                                nxCurrCol = 0;
                                nyCurrRow++;

                                // Если это необходимо, выполняем вертикальную
                                // свертку экрана
                                if ( nyCurrRow == ( nyRows - 1 ) )
                                {
                                      ValidateRect( hwnd, NULL );
                                      ScrollWindow( hwnd, 0, -nyCharSize, NULL, NULL );
                                      UpdateWindow( hwnd );
                                      nyCurrRow = nyRows - 2;
                                }
                         }
                         break;
                   }
             }
      }

      // Перемещаем курсор в новую позиицию
      SetCaretPos( nxCurrCol * nxCharSize,
                   nyCurrRow * nyCharSize );

      // Отображаем курсор
      ShowCaret( hwnd );

      // Освобождаем контекст
      ReleaseDC( hwnd, hdc );

      return;
}

// ============================================================
// Функция SetFocusTTY
// ============================================================
void SetFocusTTY(HWND hwnd)
{
      // Создаем текстовый курсор
      CreateCaret( hwnd, NULL, nxCharSize, nyCharSize );

      // Перемещаем курсор в текущую позиицию
      SetCaretPos( nxCurrCol * nxCharSize,
             nyCurrRow * nyCharSize );

      // Отображаем курсор
      ShowCaret( hwnd );

      return;
}

// ============================================================
// Функция KillFocusTTY
// ============================================================
void KillFocusTTY(HWND hwnd)
{
      // Выключаем курсор
      HideCaret(hwnd);

      // Удаляем курсор
      DestroyCaret();

      return;
}

Включаемый файл TELE.H (см. листинг 7.21) содержит определения констант, идентификаторов и глобальных переменных, а также объявления функций, используемых в приложении.

Листинг 7.21. Файл TELE.H

#define DARK_BLUE    RGB(0,0,127)   // синий цвет
#define DARK_GREEN   RGB(0,127,0)   // зеленый цвет

// Идентификаторы меню приложения
#define CM_EXIT     101
#define CM_ABOUT    102

// Сообщение WM_CONNECT
#define WM_CONNECT  WM_USER

// Размеры входной и выходной очереди
#define INQUEUE  4096
#define OUTQUEUE 4096

// ASCII-коды
#define ASCII_BELL 0x07
#define ASCII_BACK 0x08
#define ASCII_LF   0x0A
#define ASCII_CR   0x0D

// Определяем глобальные переменные только в главном модуле
#ifdef MAIN_MODULE
      HINSTANCE hInst;
      int idOpenCommPort;

#else
// Объявляем глобальные переменные,
// определенные в главном модуле
      extern int idOpenCommPort;
      extern HINSTANCE hInst;

#endif

// Функции, определенные в файле TELETYPE.CPP
void           About(HWND hwnd);
BOOL           InitApp(HINSTANCE hInstance);

LRESULT CALLBACK _export
WndProc (HWND hWnd, UINT message, WPARAM wParam,
                                  LPARAM lParam);

// Функция, определенная в файле COMMSG.CPP

LRESULT
UserChat(HWND hWnd, UINT message,
                    WPARAM wParam, LPARAM lParam);

// Функция, определенная в файле COMMSG.CPP
int
ProcessCommNotify(HWND hWnd, int nComID, int nNotification);

// Функции, определенные в файле COMMPORT.CPP
int InitCommPort( HWND hwnd);
int CloseCommPort( int nComID );
int ReadCommPort( int nComID , LPSTR Data , int nMaxLength );

// Функции, определенные в файле TTY.CPP
void InitTTY(HWND hWnd);
void WriteTTY(LPSTR lpOutString, HWND hwnd, COLORREF rgbColor);
void SetFocusTTY(HWND hwnd);
void KillFocusTTY(HWND hwnd);

В листинге 7.22 представлен исходный текст файла TELETYPE.RC, содержащего описание ресурсов приложения TELETYPE. В нем описаны меню APP_MENU и пиктограмма PHONE.

Листинг 7.22. Файл TELETYPE.RC

#include "tele.h"

PHONE ICON "teletype.ico"

APP_MENU MENU
BEGIN
      MENUITEM "Выход",      CM_EXIT
      MENUITEM "Информация", CM_ABOUT
END

В листинге 7.23 приведено изображение пиктограммы, расположенной в файле TELETYPE.ICO, на который ссылается оператор ICON в файле описания ресурсов TELETYPE.RC.

Листинг 7.23. Файл TELETYPE.ICO

Файл определения модуля для приложения TELETYPE приведен в листинге 7.24.

Листинг 7.24. Файл TELETYPE.DEF

; ==========================================================
; Файл определения модуля
; ==========================================================

NAME TELETYPE
DESCRIPTION 'Приложение TELETYPE, (C) 1994, Frolov G.V.'
EXETYPE windows
STUB 'winstub.exe'
STACKSIZE   16384
HEAPSIZE    16384
CODE preload moveable discardable
DATA preload moveable multiple