Жизненный RFID
Четверг, 26. Июль 2012
Раздел: C/C++, Windows, автор: Kaimi
Последний месяц выдался не слишком продуктивным, так как приходилось много ездить по разнообразным медицинским учреждениям. На вторую неделю поездок выяснилось, что большинство учреждений используют 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)
непонятно зачем.
Результирующий перечень прототипов:
int WINAPI rf_init_com(int port,int baud_rate);int WINAPI rf_ClosePort();int WINAPI * rf_beep(unsignedshort hz,unsignedchar ms);int WINAPI * Read_Em4001(void* dst);int __cdecl * Reset_Command();int WINAPI * Standard_Write(char a1,char a2,constvoid* src,char a4); |
Аргументы первых пяти функций не вызвали затруднений, однако с последней сначала возникло некоторое недопонимание. Через некоторое время выяснилось, что для записи десятизначного идентификатора карты (10 символов в hex или 5 байт данных) эта функция вызывается три раза примерно следующим образом:
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. Подробнее о протоколе можно почитать тут (отсюда и была взята картинка для наглядности).
Теперь перейдем к коду. Первоочередной задачей является написание функции, которая будет преобразовывать записываемый идентификатор вышеописанным образом.
//Заинлайним повторяющийся небольшой кусок кодаinlinevoid append_bits(vector<unsignedchar>& vec, unsignedint value, unsignedchar& vector_pos, unsignedchar& 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<unsignedchar> id_transform(const string & hex){//Заранее выделенный вектор байтов vector<unsignedchar> ret(8, 0);//Первые 9 битов всегда такие ret[0]=0xFF; ret[1]=0x80; //Преобразуем hex-строку в вектор байтов vector<unsignedchar> bin = hex2bin(hex);//Контроль четностиunsignedint col_parity =0; //Временная величинаunsignedint temp;//Количество занятых битов в байте вектора ret//Сначала равно единице (см. начало функции, 9 битов занято)//(т.е. 1 байт и 1 бит в следующем байте)unsignedchar used_bits =1;//Текущая позиция, то бишь количество полностью занятых байтов//Сейчас это 1, как описано вышеunsignedchar curr_vector_pos =1; //Перебираем входные байтыfor(vector<unsignedchar>::const_iterator it = bin.begin(); it != bin.end();++it){unsignedchar 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;} /* Вообще тут была довольно интуитивная реализация в южноазиатском стиле, но потом пришли умные люди и сказали, что так не кошерно */ |
Костяк взаимодействия готов, реализуем небольшой GUI с маджонгом и гейшами WinAPI и говнокодом. Результат будет выглядеть примерно так:
Начнем, как обычно, с заголовочного файла и вспомогательных функций:
#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") usingnamespace std; staticconst string ini_file ="cards.ini"; |
/* Функция добавления потомка к корневому элементу 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:
int MainDlgProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam){ TV_ITEM item; HTREEITEM parent;staticbool conn_state =false;unsignedint 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<unsignedchar> 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:return0;} return1;} 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); return0;} |
INI-файл, как видно из кода выше, хранит в себе сохраненные идентификаторы карт и дополнительные пользовательские данные, которые можно указать в интерфейсе программы. То есть можно довольно быстро найти нужную карту, записать на пустую болванку и воспользоваться.
Опишем формат хранения данных в INI-файле, класс для работы с которыми (CIniFile) использовался в коде выше:
;Имя карты в Tree View[card1];Расположение, а также имя корневого разделаbuilding=Hospital1;Данные с картыdata=DEADBEEF;Примечаниеnote=cool[card3]building=Hospital3data=12345678note= |
И, наконец, файл ресурсов (за вычетом студийной генеренки), который можно было и не приводить, но пусть будет.
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 и скомпилированный экзешник: скачать