Ще раз про прямий доступ до апаратури h2>
Сивцов Павло p>
Преамбула h2>
Одного разу мій знайомий попросив написати йому просту
програму - «сторожовий пес». Все, що потрібно робити - це відловити момент розмикання
або замикання зовнішнього контакту і при настанні такої події запустити
іншу програму. Працювати програма повинна під Windows XP. Завдання виглядала
елементарної. Єдине, що не хотілося робити - апаратну частину. Тобто
найкраще було б знайти таке рішення, при якому майже нічого не потрібно було
б паяти. p>
Досить швидко з'ясувалося, що простіше за все для
такої мети використовувати опитування станів LPT-або COM-портів. Тут і починається
найцікавіше. p>
LPT h2>
Для реалізації «сторожового пса» на LPT-порту можна
використовувати періодичний опитування стану деяких його контактів. Можна
просто виявляти стану ліній SELECTED (контакт 13), BUSY (контакт 11) і
PAPER EMPTY (контакт 12). Досить замикати/розмикати вибраний контакт з
«Землею» (контакти 18-25). Я вибрав використання BUSY - замикав контакти 11 і
23. Отже, апаратна частина виходила елементарної, тепер треба було якось
достукатися до вибраного контакту з програмної сторони. Тут-то і зустрілася
перша складність - легальних способів прямого доступу до портів в лінійці
Windows NT немає. Використовувати примочки типу gwio.sys, що дозволяють прямий доступ
до апаратури, дуже не хотілося. Робота з портом як з файлом у даному випадку
не підходить, тому що потрібно не дані читати, а опитувати стану. Тим не менше,
після тривалого вивчення MSDN, легальний доступ до деяких лініях порту було
виявлено! Спосіб цей - доступ до порту через функцію DeviceIoControl (...,
IOCTL_PAR_QUERY_INFORMATION, ...). Тут виявилася друга складність - відсутність
потрібних заголовків файлів для Delphi. Довелося самостійно портувати
ntddpar.h з DDK. Портована файл отримав назву JwaNtDdPar.pas і був
люб'язно доданий Marcel van Brakel в JEDI Windows API Library. p>
Невеликий приклад демонструє загальний код. Delphi 7. P>
Приклад коду p>
uses p>
SysUtils, JwaWinType, JwaWinNT,
JwaWinBase, JwaNtDdPar; p>
($ WARN SYMBOL_PLATFORM OFF) p>
function GetLptStatus: Boolean; p>
var p>
eFileHandle: THandle; p>
eInfo: TParQueryInformation; p>
eBytesReturned: DWORD; p>
begin p>
// відкриємо порт p>
eFileHandle: =
CreateFile ( 'LPT1', GENERIC_READ, 0, nil, OPEN_EXISTING, 0, 0); p>
Win32Check (eFileHandle <>
INVALID_HANDLE_VALUE); p>
try p>
// дізнаємося стан p>
Win32Check (DeviceIoControl (eFileHandle,
IOCTL_PAR_QUERY_INFORMATION, nil, 0, p>
@ eInfo, SizeOf (eInfo),
@ eBytesReturned, nil )); p>
Result: = (Byte (eInfo.Status)
and PARALLEL_BUSY) = 0; p>
finally p>
// не забудьте закрити хендл по завершенню
роботи p>
Win32Check (CloseHandle (eFileHandle )); p>
end; p>
end; p>
Короткий і елегантний код, чи не так? Для вирішення
поставленого завдання досить опитувати стан порту раз-два в секунду.
В принципі, звичайно ж, краще відразу відкрити порт при старті, а закрити по
завершення. p>
ПОПЕРЕДЖЕННЯ p>
На жаль, не вдасться відкрити порт в
режимі FILE_FLAG_OVERLAPPED, щоб потім використовувати переваги
асинхронної роботи. Точніше, порт відкрити вдасться, не вдасться отримати подія
при зміні статусу ліній порту. p>
Зате цей код успішно відпрацював з-під
гостьовий облікового запису під Windows XP. p>
Останній нюанс - брязкіт контактів. «Брязкіт
контактів - це явище багаторазового неконтрольованого замикання та розмикання
контактів в моменти їхнього зіткнення і розбіжності ». Тривають такі перехідні
процеси в кнопках близько 10-15 мілісекунд. Тобто з великою ймовірністю ми
будемо отримувати помилкові спрацьовування нашого коду, якщо інтервал між перевірками
буде коротшим. p>
Сподіваюся, цей приклад роботи з LPT-портом послужить
хорошою демонстрацією того, як у багатьох випадках легко отримати легальний
доступ до апаратури без написання драйверів або обходу Hardware Abstraction
Layer. Не для того цей HAL придумували, щоб його обходити. P>
Доводилося читати про випадки захоплення порту спулера
друку, але на практиці таку ситуацію зустріти не вдалося. Якщо хто-небудь
зможе прояснити це питання, я буду радий. p>
ПРИМІТКА p>
До речі, в середині сторінки http://cooler.irk.ru/cl190902.html
викладено досить цікавий лист, в якому описується робота з портом
в режимі IEEE_COMPATIBILITY. Такий режим дозволяє з мінімумом рухів тіла
забезпечити повноцінний висновок даних на саморобний LPT-пристрій. p>
COM
h2>
При використанні COM-порту завдання виявлення
зовнішнього події може бути вирішена ще простіше. Досить замикати/розмикати
контакти 7 (RTS) та 8 (CTS) у девятіконтактного роз'єму (знову нічого не
доведеться паяти) і перевіряти наявність сигналу CTS. Причому опитування можна робити
через стандартний CommApi. p>
Приклад коду p>
function GetComStatus: Boolean; p>
var p>
eFileHandle: THandle; p>
eStatus: DWORD; p>
begin p>
// відкриємо порт p>
eFileHandle: =
CreateFile ( 'COM1', GENERIC_READ, 0, nil, OPEN_EXISTING, 0, 0); p>
Win32Check (eFileHandle <>
INVALID_HANDLE_VALUE); p>
try p>
// дізнаємося стан p>
Win32Check (GetCommModemStatus (eFileHandle,
eStatus )); p>
Result: = (eStatus and
MS_CTS_ON)> 0; p>
finally p>
// не забудьте закрити хендл по завершенню
роботи p>
Win32Check (CloseHandle (eFileHandle )); p>
end; p>
end; p>
Втім, цей приклад елегантним вже не назвеш, тому що
використання COM-порту дає можливість позбутися від періодичного опитування,
використовуючи асинхронну роботу через WaitCommEvent і WaitForMultipleObjects. p>
Нижче наведений код прикладу. Для пояснення суті
того, що відбувається код рясно прокоментований. Але все-таки зверну увагу на
деякі нюанси: p>
WaitForMultipleObjects чекає нескінченно. Ніяких
періодичних опитувань - значить і ніякого споживання ресурсів. Все реалізовано
на подіях. p>
Немає необхідності в TerminateThread для
примусового припинення виконання потоку. Виконання може бути
«Культурно» закінчено в будь-який момент. Для цього використовується окрема подія.
p>
Проста реалізація неблокірующей затримки для
придушення брязкоту. Оскільки періодичний опитування ми не застосовуємо, то, щоб
позбутися від помилкових спрацьовувань програмним шляхом, потрібно почекати кілька
десятків мілісекунд, і якщо стан за цей час не змінилося, то
замикання/розмикання ланцюга відбулося. Як таймера використовується
WaitableTimer. Зверніть увагу на його теоретичну точність. p>
Ключовий метод p>
procedure
TComWatchdogThread.Execute; p>
var p>
// структура, що використовується для Win32
зберігання внутрішньої інформації при p>
// асинхронної роботі. Нічого крім поля
hEvent нам від неї не потрібно p>
eOverlapped: TOverlapped; p>
// запит очікування асинхронного події
зміни стану порту p>
//............................................. .............................. p>
procedure InitWaitCommEvent; p>
var p>
eEventMask: DWORD; p>
begin p>
// помилки ERROR_IO_PENDING потрібно просто
ігнорувати - їх наявність означає p>
// тільки те, що остання операція з
портом ще не завершена. p>
// Що цікаво, не можна двічі поспіль
викликати WaitCommEvent, тобто p>
// запросив подія - значить, дочекайся його. p>
if not WaitCommEvent (FComHandle, eEventMask,
@ eOverlapped) p>
and (GetLastError <>
ERROR_IO_PENDING) then p>
RaiseLastOSError; p>
end; p>
var p>
// TWOHandleArray - це просто готовий масив з 64 Хендли для p>
// функції WaitForMultipleObjects. Ми використовуємо тільки 3 Хендли, p>
// але для простоти скористаємося готовим
масивом на 64, щоб p>
// не зв'язуватися з ручним розподілом
пам'яті. p>
eHandles: TWOHandleArray; p>
eTime: Int64; p>
eStatus: DWORD; p>
eStubInstalled: Boolean; p>
begin p>
// заповнимо структури для асинхронної роботи p>
FillChar (eOverlapped, SizeOf (eOverlapped),
0); p>
eOverlapped.hEvent: =
FChangeEvent; p>
eHandles [0]: =
eOverlapped.hEvent; p>
eHandles [1]: = FTerminateEvent; p>
eHandles [2]: = FFlutterTimer; p>
// 0.5 секунди для SetWaitableTimer p>
eTime: = -5000000; p>
// Нічого собі точність, чи не так? p>
// попередньо зведений прапор очікування
події, щоб цикл заробив p>
InitWaitCommEvent; p>
while not Terminated do p>
case WaitForMultipleObjects (3,
@ eHandles, False, INFINITE) of p>
// при зміні стану порту p>
WAIT_OBJECT_0: p>
begin p>
// знову зведений прапор очікування події p>
InitWaitCommEvent; p>
// для придушення брязкоту зробимо невелику
затримку. Якщо стан p>
// порту зміниться швидше, ніж закінчиться
час затримки (брязкіт), то p>
// таймер просто буде переведений "на
пізніше ". p>
Win32Check (SetWaitableTimer (FFlutterTimer,
eTime, 0, nil, nil, p>
False )); p>
end; p>
// при запиті примусового завершення
потоку p>
WAIT_OBJECT_0 + 1: p>
// негайний вихід - завершення
виконання потоку p>
Exit; p>
// після затримки для придушення брязкоту p>
WAIT_OBJECT_0 + 2: p>
begin p>
// дізнаємося стан p>
Win32Check (GetCommModemStatus (FComHandle,
eStatus )); p>
eStubInstalled: = (eStatus and
MS_CTS_ON)> 0; p>
// викличемо обробник події p>
if eStubInstalled then p>
DoOnChange; p>
end; p>
// при наглої помилку p>
WAIT_FAILED: p>
// сталося страшне ... p>
RaiseLastOSError; p>
end; p>
end; p>
Повний код прикладу наведено у доданому до статті
архіві. p>
Список b> b> літератури b> p>
JEDI Windows API
Library http://members.chello.nl/m.vanbrakel2/win32api.zip p>
Parallel Port
Central http://www.lvr.com/parport.htm p>
Serial Port Central
http://www.lvr.com/serport.htm p>
Лист в журнал «Cooler» про прямий доступ до LPT http://cooler.irk.ru/cl190902.html
p>
Для підготовки даної роботи були використані
матеріали з сайту http://www.rsdn.ru/
p>