Жизненный RFID Последний месяц выдался не слишком продуктивным, так как приходилось много ездить по разнообразным медицинским учреждениям. На вторую неделю поездок выяснилось, что большинство учреждений используют RFID-карты для разграничения доступа во внутренних помещениях. Таким образом, под конец месяца у меня набралось великое множество таких карт, что было дико неудобно: приехал в очередное место, открыл рюкзак, достал кипу карт и ищешь где именно та, которую тебе дали на прошлой неделе местные сотрудники. Ещё одним негативным моментом этих поездок было время, которое приходилось проводить в транспорте. В итоге я решил избавиться от одного из неудобств, а именно купить программатор и написать программу-менеджер, которая избавит от необходимости таскать с собой кучу карт (все равно ношу в рюкзаке нетбук, а программатор много веса не добавит) и позволит вести базу, по которой можно будет быстро найти карту от нужного помещения для заданного учреждения, записать идентификатор на болванку и воспользоваться им по назначению. Сказано - сделано, вчера, во время очередной серии поездок, написал соответствующую программку, которую далее и рассмотрю подробнее. Также обозначим формат карт, который, как оказалось, является доминирующим по неведомой мне причине - это EM-4100. Мимоходом, в магазине со всякой электроникой, был куплен программатор китайского производства, к которому прилагались драйвера для USB-UART моста модели CP210x производства Silicon Laboratories и стремный софт с китайским интерфейсом (имеющий в своем арсенале только функции чтения и записи идентификатора карты), который все же пригодился в дальнейшем. Первым делом встал вопрос, чем пользоваться для взаимодействия с устройством. Беглый обзор доступных библиотек не особо меня вдохновил, поэтому я решил взять дизассемблер и посмотреть как устроен прилагающийся продукт. Софт оказался написан на Visual Basic, о чем намекнула секция импорта, состоящая из одной MSVBVM60.DLL. VB Decompiler показал, что используются следующие функции из сторонних библиотек: Таким образом, софт оказался завязан на некую MasterRD.dll, которая, в свою очередь, использовала MasterCom.dll. С помощью отладчика я выяснил, что для подключения к устройству использовалась функция rf_init_com(int port, int baud_rate), для отключения rf_ClosePort(void), для чтения идентификатора карты Read_Em4001(void * dst) и для записи Standard_Write(char a1, char a2, const void * src, char a4). Также обнаружилась полезная функция rf_beep(unsigned short hz, unsigned char ms) и обращение к Reset_Command(void) непонятно зачем. Результирующий перечень прототипов: PHP: int WINAPI rf_init_com(int port, int baud_rate); int WINAPI rf_ClosePort(); int WINAPI * rf_beep(unsigned short hz, unsigned char ms); int WINAPI * Read_Em4001(void * dst); int __cdecl * Reset_Command(); int WINAPI * Standard_Write(char a1, char a2, const void * src, char a4); Аргументы первых пяти функций не вызвали затруднений, однако с последней сначала возникло некоторое недопонимание. Через некоторое время выяснилось, что для записи десятизначного идентификатора карты (10 символов в hex или 5 байт данных) эта функция вызывается три раза примерно следующим образом: PHP: Standard_Write(2, 0, ptr, 1); Standard_Write(2, 0, ptr, 2); Standard_Write(2, 0, ptr, 0); Поэксперементировав с записью, обнаружил, что в третьем вызове в ptr всегда хранится следующая последовательность байт: 0x00, 0x14, 0x80, 0x40 Закономерность для первых двух вызовов оставалась загадкой. На помощь пришел гугл, который помог найти формат кодирования данных. Все оказалось довольно просто: Допустим, мы хотим записать последовательность 123456789A на карту. По сути это 5 байтов или 40 бит данных (D00..D39), которые дополняются статичным заголовком (9 единичных битов), битом четности для каждых четырех байт (P0..P9) и стоп-битом в конце (S0). Плюс считается бит четности для каждого из столбцов бит (PC0..PC3). Далее все это складывается в здоровенное 64 битное число, которое и передается в два захода (по 4 байта) первыми двумя вызовами Standard_Write. Подробнее о протоколе можно почитать тут (отсюда и была взята картинка для наглядности). Теперь перейдем к коду. Первоочередной задачей является написание функции, которая будет преобразовывать записываемый идентификатор вышеописанным образом. PHP: //Заинлайним повторяющийся небольшой кусок кода inline void append_bits(vector<unsigned char>& vec, unsigned int value, unsigned char& vector_pos, unsigned char& used_bits) { //Нам нужны только младшие 5 битов value &= 0x1F; //Дополняем биты в вектор по 5 штук. Такое преобразование - следствие //того, что 5 битов и байт (8 битов) никак не кореллируют vec[vector_pos] |= used_bits <= 3 ? value << (3 - used_bits) : value >> (used_bits - 3); used_bits += 5; //Если при дополнении нам не хватило места if(used_bits >= 8) { used_bits -= 8; //Запишем оставшиеся биты в следующий по счету байт vec[++vector_pos] |= value << (8 - used_bits); } } vector<unsigned char> id_transform(const string & hex) { //Заранее выделенный вектор байтов vector<unsigned char> ret(8, 0); //Первые 9 битов всегда такие ret[0] = 0xFF; ret[1] = 0x80; //Преобразуем hex-строку в вектор байтов vector<unsigned char> bin = hex2bin(hex); //Контроль четности unsigned int col_parity = 0; //Временная величина unsigned int temp; //Количество занятых битов в байте вектора ret //Сначала равно единице (см. начало функции, 9 битов занято) //(т.е. 1 байт и 1 бит в следующем байте) unsigned char used_bits = 1; //Текущая позиция, то бишь количество полностью занятых байтов //Сейчас это 1, как описано выше unsigned char curr_vector_pos = 1; //Перебираем входные байты for(vector<unsigned char>::const_iterator it = bin.begin(); it != bin.end(); ++it) { unsigned char c = (*it); //Берем старшие 4 бита temp = (c >> 4) << 1; //Сдвигаем их на 1 влево, а в освободившийся бит пишем четность от этих четырех temp |= (char)compute_parity( c >> 4 ); //Считаем четность col_parity ^= temp; //Добавляем полученные биты в вектор append_bits(ret, temp, curr_vector_pos, used_bits); //Теперь то же самое - с младшими битами temp = (c & 0x0F) << 1; temp |= (char)compute_parity( c & 0x0F ); append_bits(ret, temp, curr_vector_pos, used_bits); col_parity ^= temp; } //Обнуляем все, кроме 1 - 5 битов четности col_parity &= 0x1E; //Ее тоже дописываем в вектор, получив в итоге 8 полных байтов на выходе ret[curr_vector_pos] |= used_bits <= 3 ? col_parity << (3 - used_bits) : col_parity >> (used_bits - 3); //Возвращаем результат return ret; } /* [Вообще тут была довольно интуитивная реализация в южноазиатском стиле, но потом пришли умные люди и сказали, что так не кошернo */ Костяк взаимодействия готов, реализуем небольшой GUI с маджонгом и гейшами WinAPI и говнокодом. Результат будет выглядеть примерно так: Начнем, как обычно, с заголовочного файла и вспомогательных функций: PHP: #include <Windows.h> #include <TlHelp32.h> #include <Commctrl.h> #include <algorithm> #include <bitset> #include <iostream> #include <sstream> #include <fstream> #include <map> #include <set> #include "IniFile.h" #include "str_util.h" #include "resource.h" #pragma comment(lib, "comctl32") using namespace std; static const string ini_file = "cards.ini"; PHP: /* Функция добавления потомка к корневому элементу Tree View */ HTREEITEM insert_child(HWND tree, const wstring & title, map<HTREEITEM, pair<wstring, wstring>> & cards, HTREEITEM parent) { TVINSERTSTRUCT insert; wchar_t temp[256]; ZeroMemory(&insert, sizeof(TVINSERTSTRUCT)); wcscpy_s(temp, _countof(temp), title.c_str()); insert.hParent = parent; insert.hInsertAfter = TVI_LAST; insert.item.mask = TVIF_TEXT; insert.item.pszText = temp; insert.item.cchTextMax = _countof(temp); return TreeView_InsertItem(tree, &insert); } /* Функция добавления корневого элемента */ HTREEITEM insert_root(HWND tree, const wstring & title, map<wstring, HTREEITEM> * buildings) { HTREEITEM root; TVINSERTSTRUCT insert; wchar_t temp[256]; ZeroMemory(&insert, sizeof(TVINSERTSTRUCT)); wcscpy_s(temp, _countof(temp), title.c_str()); insert.hParent = NULL; insert.hInsertAfter = TVI_ROOT; insert.item.mask = TVIF_TEXT | TVIF_CHILDREN; insert.item.cChildren = 1; insert.item.pszText = temp; insert.item.cchTextMax = _countof(temp); root = TreeView_InsertItem(tree, &insert); if(buildings != 0) buildings->insert(make_pair(title, root)); return root; } /* Функция заполнения Tree View элементами из ini-файла */ map<HTREEITEM, pair<wstring, wstring>> fill_tree_view(CIniFile & ini, HWND tree) { HTREEITEM root; wstring v, d, n; map<wstring, HTREEITEM> buildings; map<HTREEITEM, pair<wstring, wstring>> cards; map<wstring, HTREEITEM>::const_iterator b_it; vector<CIniFile::Record> r; /* Получаем список секций из INI-файла */ vector<string> s = ini.GetSectionNames(ini_file); for(vector<string>::const_iterator it = s.begin(); it != s.end(); ++it) { v = str2wstr(ini.GetValue("building", (*it), ini_file)); /* Пропускаем карту без поля building */ if(v.empty()) continue; if(buildings.find(v) == buildings.end()) { /* Добавляем корневой элемент */ root = insert_root(tree, v, &buildings); } else { /* Ищем хендл существующего корневого элемента */ b_it = buildings.find(v); if(b_it != buildings.end()) root = (*b_it).second; } r = ini.GetSection((*it), ini_file); /* Добавляем дочерний элемент */ root = insert_child(tree, str2wstr(r[0].Section), cards, root); d = str2wstr(ini.GetValue("data", (*it), ini_file)); n = str2wstr(ini.GetValue("note", (*it), ini_file)); cards.insert(make_pair(root, make_pair(d, n))); } return cards; } Теперь WinMain и DlgProc: PHP: int MainDlgProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { TV_ITEM item; HTREEITEM parent; static bool conn_state = false; unsigned int i; wchar_t name[256], data[256], note[256], building[256]; static CIniFile ini; static HMODULE dll; static HWND tree; static map<HTREEITEM, pair<wstring, wstring>> cards; map<HTREEITEM, pair<wstring, wstring>>::iterator it; vector<unsigned char> exch_data; switch(uMsg) { case WM_INITDIALOG: /* Подгружаем DLL'ку и заполняем указатели на функции */ dll = LoadLibrary(L"MasterRD.dll"); if(dll == NULL) { MessageBox(hWnd, L"Failed to load MasterRD.dll", L"Error", MB_OK | MB_ICONERROR); EndDialog(hWnd, 0); break; } (FARPROC &)rf_init_com = GetProcAddress(dll, "rf_init_com"); (FARPROC &)rf_ClosePort = GetProcAddress(dll, "rf_ClosePort"); (FARPROC &)rf_beep = GetProcAddress(dll, "rf_beep"); (FARPROC &)Read_Em4001 = GetProcAddress(dll, "Read_Em4001"); (FARPROC &)Standard_Write = GetProcAddress(dll, "Standard_Write"); (FARPROC &)Reset_Command = GetProcAddress(dll, "Reset_Command"); if ( rf_init_com == NULL || rf_ClosePort == NULL || rf_beep == NULL || Read_Em4001 == NULL || Standard_Write == NULL || Reset_Command == NULL ) { MessageBox(hWnd, L"Failed to obtain procedure address from MasterRD.dll", L"Error", MB_OK | MB_ICONERROR); EndDialog(hWnd, 0); break; } /* Получаем хендл на Tree View и заполняем его данными */ tree = GetDlgItem(hWnd, IDC_TREE); cards = fill_tree_view(ini, tree); /* Добавляем перечень COM-портов в ComboBox */ SendDlgItemMessage(hWnd, IDC_COM, CB_ADDSTRING, 0, reinterpret_cast<LPARAM>(L"COM1")); SendDlgItemMessage(hWnd, IDC_COM, CB_ADDSTRING, 0, reinterpret_cast<LPARAM>(L"COM2")); SendDlgItemMessage(hWnd, IDC_COM, CB_ADDSTRING, 0, reinterpret_cast<LPARAM>(L"COM3")); SendDlgItemMessage(hWnd, IDC_COM, CB_ADDSTRING, 0, reinterpret_cast<LPARAM>(L"COM4")); /* Ограничиваем размер полей ввода */ SendDlgItemMessage(hWnd, IDC_DATA, EM_LIMITTEXT, 10, 0); SendDlgItemMessage(hWnd, IDC_NAME, EM_LIMITTEXT, 256, 0); SendDlgItemMessage(hWnd, IDC_NOTE, EM_LIMITTEXT, 256, 0); break; case WM_NOTIFY: switch((reinterpret_cast<LPNMHDR>(lParam))->code) { /* При изменении выбранного элемента в Tree View */ case TVN_SELCHANGED: /* Ищем хендл элемента в мэпе */ it = cards.find(reinterpret_cast<LPNMTREEVIEW>(lParam)->itemNew.hItem); if(it != cards.end()) { /* Получаем имя элемента */ ZeroMemory(&item, sizeof(TV_ITEM)); item.mask = TVIF_HANDLE | TVIF_TEXT; item.hItem = (*it).first; item.pszText = data; item.cchTextMax = _countof(data); TreeView_GetItem(tree, &item); /* Отображаем полученные данные в соответствующих контролах */ SetDlgItemText(hWnd, IDC_NAME, data); SetDlgItemText(hWnd, IDC_DATA, (*it).second.first.c_str()); SetDlgItemText(hWnd, IDC_NOTE, (*it).second.second.c_str()); /* Получаем имя корневого элемента */ parent = TreeView_GetParent(tree, (*it).first); ZeroMemory(&item, sizeof(TV_ITEM)); item.mask = TVIF_HANDLE | TVIF_TEXT; item.hItem = parent; item.pszText = data; item.cchTextMax = _countof(data); TreeView_GetItem(tree, &item); /* Выводим имя корневого элемента в контрол */ SetDlgItemText(hWnd, IDC_BLD, data); /* А можно было не заморачиваться и все брать из ini-файла... */ } break; } break; case WM_COMMAND: switch(LOWORD(wParam)) { /* Обработчик кнопки записи */ case IDC_WRITE: i = GetDlgItemText(hWnd, IDC_DATA, data, sizeof(data)); /* ID-карты должен состоять из 10 символов */ if(i != 10) { SetDlgItemText(hWnd, IDC_STATUS, L"Erroneous ID size"); break; } /* Преобразовываем данные в формат, используемый библиотекой */ exch_data = id_transform(wstr2str(data)); reverse(exch_data.begin(), exch_data.end()); /* Пишем данные на карту */ if ( Standard_Write(2, 0, &exch_data[0], 1) == 0 && Standard_Write(2, 0, &exch_data[4], 2) == 0 && Standard_Write(2, 0, "\x00\x14\x80\x40", 0) == 0 ) { /* Оповещаем пользователя об успешной записи */ SetDlgItemText(hWnd, IDC_STATUS, L"Data was successfully written"); rf_beep(0, 6); } else { SetDlgItemText(hWnd, IDC_STATUS, L"Can't write data"); } Reset_Command(); break; /* Обработчик кнопки чтения */ case IDC_READ: exch_data.resize(5); /* Читаем данные с карты и выводим в HEX в соответствующий контрол */ if(Read_Em4001(&exch_data[0])) { SetDlgItemText(hWnd, IDC_STATUS, L"Can't read card data"); } else { SetDlgItemText(hWnd, IDC_DATA, str2wstr(bin2hex(exch_data)).c_str()); rf_beep(0, 6); } break; /* Обработчик кнопки подключения к COM-порту */ case IDC_CONNECT: /* Если мы уже подключены */ if(conn_state) { /* Закрываем порт, меняем состояние контролов */ conn_state = false; rf_ClosePort(); SetDlgItemText(hWnd, IDC_CONNECT, L"Connect"); SetDlgItemText(hWnd, IDC_STATUS, L"Disconnected"); EnableWindow(GetDlgItem(hWnd, IDC_READ), FALSE); EnableWindow(GetDlgItem(hWnd, IDC_WRITE), FALSE); } else { EnableWindow(GetDlgItem(hWnd, IDC_CONNECT), FALSE); i = SendDlgItemMessage(hWnd, IDC_COM, CB_GETCURSEL, 0, 0); /* CB_GETCURSEL возвращает индекс элемента начиная с 0, а нам необходимо с 1 */ i++; /* Инициализируем подключение на скорости 9600 бод */ if(rf_init_com(i, 9600)) { SetDlgItemText(hWnd, IDC_STATUS, L"Can't connect to device"); break; } /* Оповещаем пользователя, меняем надпись на кнопке и включаем нужные контролы */ conn_state = true; SetDlgItemText(hWnd, IDC_CONNECT, L"Disconnect"); SetDlgItemText(hWnd, IDC_STATUS, L"Connected"); EnableWindow(GetDlgItem(hWnd, IDC_CONNECT), TRUE); EnableWindow(GetDlgItem(hWnd, IDC_READ), TRUE); EnableWindow(GetDlgItem(hWnd, IDC_WRITE), TRUE); } break; /* Обработчик кнопки удаления карты из списка */ case IDC_DELETE: /* Ищем карту в мэпе */ it = cards.find(TreeView_GetSelection(tree)); if(it == cards.end()) break; else /* Удаляем карту из ini-файла */ ini.DeleteSection(wstr2str(name), ini_file); /* break опущен умышленно */ /* Обработчик кнопки обновления Tree View */ case IDC_REFRESH: /* Очищаем Tree View и повторно заполняем из данных файла */ TreeView_DeleteAllItems(tree); cards = fill_tree_view(ini, tree); break; /* Обработчик клавиши save */ case IDC_SAVE: /* Считываем данные из контролов */ GetDlgItemText(hWnd, IDC_NAME, name, _countof(name)); GetDlgItemText(hWnd, IDC_DATA, data, _countof(data)); GetDlgItemText(hWnd, IDC_NOTE, note, _countof(note)); GetDlgItemText(hWnd, IDC_BLD, building, _countof(building)); /* Проверяем наличие карты с таким именем в файле, если нет такой, то создаем соответствующую секцию */ if(ini.GetSection(wstr2str(name), ini_file).empty()) ini.AddSection(wstr2str(name), ini_file); /* Обновляем данные */ ini.SetValue("data", wstr2str(data), wstr2str(name), ini_file); ini.SetValue("note", wstr2str(note), wstr2str(name), ini_file); ini.SetValue("building", wstr2str(building), wstr2str(name), ini_file); /* Очищаем Tree View и повторно заполняем из данных файла */ TreeView_DeleteAllItems(tree); cards = fill_tree_view(ini, tree); break; } break; case WM_CLOSE: rf_ClosePort(); EndDialog(hWnd, 0); break; default: return 0; } return 1; } int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { /* Инициализируем контролы для работы с Tree View и Status Bar */ INITCOMMONCONTROLSEX ccl = { sizeof(INITCOMMONCONTROLSEX), ICC_BAR_CLASSES | ICC_TREEVIEW_CLASSES }; InitCommonControlsEx(&ccl); DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_MAIN), 0, (DLGPROC) MainDlgProc, 0); return 0; } INI-файл, как видно из кода выше, хранит в себе сохраненные идентификаторы карт и дополнительные пользовательские данные, которые можно указать в интерфейсе программы. То есть можно довольно быстро найти нужную карту, записать на пустую болванку и воспользоваться. Опишем формат хранения данных в INI-файле, класс для работы с которыми (CIniFile) использовался в коде выше: PHP: ;Имя карты в Tree View [card1] ;Расположение, а также имя корневого раздела building=Hospital1 ;Данные с карты data=DEADBEEF ;Примечание note=cool [card3] building=Hospital3 data=12345678 note= И, наконец, файл ресурсов (за вычетом студийной генеренки), который можно было и не приводить, но пусть будет. PHP: IDD_MAIN DIALOGEX 0, 0, 342, 169 STYLE DS_SETFONT | DS_MODALFRAME | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION "RFID Manager" FONT 10, "Verdana", 400, 0, 0xCC BEGIN CONTROL "",IDC_TREE,"SysTreeView32",TVS_HASLINES | WS_BORDER | WS_HSCROLL | WS_TABSTOP,1,4,96,128 EDITTEXT IDC_DATA,156,12,180,13,ES_AUTOHSCROLL GROUPBOX "Card info",IDC_STATIC,102,0,240,102 LTEXT "Data",IDC_STATIC,114,12,17,8 LTEXT "Name",IDC_STATIC,114,30,19,8 EDITTEXT IDC_NAME,156,30,180,14,ES_AUTOHSCROLL LTEXT "Note",IDC_STATIC,114,48,17,8 EDITTEXT IDC_NOTE,156,48,180,14,ES_AUTOHSCROLL PUSHBUTTON "Save",IDC_SAVE,287,84,50,12 COMBOBOX IDC_COM,156,114,48,30,CBS_DROPDOWN | CBS_SORT | WS_VSCROLL | WS_TABSTOP LTEXT "COM Port",IDC_STATIC,114,114,32,8 GROUPBOX "Controls",IDC_STATIC,102,102,240,48 PUSHBUTTON "Connect",IDC_CONNECT,210,114,42,12 PUSHBUTTON "Read",IDC_READ,287,114,50,12,WS_DISABLED PUSHBUTTON "Write",IDC_WRITE,287,132,50,12,WS_DISABLED CONTROL "Ready",IDC_STATUS,"msctls_statusbar32",0x3,0,156,342,12 PUSHBUTTON "Refresh",IDC_REFRESH,1,138,96,12 PUSHBUTTON "Delete",IDC_DELETE,234,84,50,12 EDITTEXT IDC_BLD,156,66,180,14,ES_AUTOHSCROLL LTEXT "Building",IDC_STATIC,114,66,26,8 END Не были рассмотрены реализация класса для работы с INI-файлами, так как она была найдена в интернете, и небольшой вспомогательный файл для работы со строками. Итак, поставленная цель достигнута всего-то за день поездок, что не может не радовать. Проект для MSVC 2010 и скомпилированный экзешник: скачать автор: Kaimi http://kaimi.ru/2012/07/rfid/