MEFF 0 12 февраля, 2010 Опубликовано 12 февраля, 2010 (изменено) · Жалоба Есть некое устройство и программа, которая общается с ним. Исходников и документации нет. Общается по 3-м проводам (GND, RX, TX). Хочется написать свою программку. Сниффинг обмена вроде бы прояснил ситуацию, протокол вроде бы понятен, но с общением есть проблемы :crying: При работе через терминальные проги железяка или молчит в ответ на запросы, или отвечает один раз и больше не реагирует. :07: Вот пример лога общения родной программы (использовал Eltima Serial Port Monitor 4, но пробовал и другие): [12/02/2010 17:19:21] 9245 IRP_MJ_CREATE - Opens a COM port (COM1) [12/02/2010 17:19:21] 9247 IOCTL_SERIAL_SET_QUEUE_SIZE - Request sets the size of the internal receive buffer InSize - 1024 OutSize - 300 [12/02/2010 17:19:21] 9249 IOCTL_SERIAL_GET_BAUD_RATE BaudRate - 9600 [12/02/2010 17:19:21] 9251 IOCTL_SERIAL_GET_LINE_CONTROL StopBits - 0 Parity - 0 WordLength - 8 [12/02/2010 17:19:21] 9253 IOCTL_SERIAL_GET_CHARS EofChar - 0 ErrorChar - 0 BreakChar - 0 EventChar - 0 XonChar - 17 XoffChar - 19 [12/02/2010 17:19:21] 9255 IOCTL_SERIAL_GET_HANDFLOW ControlHandShake - 0x1 (SERIAL_DTR_CONTROL) FlowReplace - 0x40 (SERIAL_RTS_CONTROL) XonLimit - 2048 XoffLimit - 512 Зачем-то еще раз GET_BAUDRATE, GET_LINE_CONTROL, GET_CHARS, GET_HANDFLOW [12/02/2010 17:19:21] 9265 IOCTL_SERIAL_SET_BAUD_RATE BaudRate - 9600 [12/02/2010 17:19:21] 9267 IOCTL_SERIAL_SET_RTS [12/02/2010 17:19:21] 9269 IOCTL_SERIAL_SET_DTR [12/02/2010 17:19:21] 9271 IOCTL_SERIAL_SET_LINE_CONTROL StopBits - 0 Parity - 0 WordLength - 8 [12/02/2010 17:19:21] 9273 IOCTL_SERIAL_SET_CHARS EofChar - 0 ErrorChar - 0 BreakChar - 0 EventChar - 0 XonChar - 17 XoffChar - 19 [12/02/2010 17:19:21] 9275 IOCTL_SERIAL_SET_HANDFLOW - Request sets the configuration of handshake flow control ControlHandShake - 0x1 (SERIAL_DTR_CONTROL) FlowReplace - 0x40 (SERIAL_RTS_CONTROL) XonLimit - 2048 XoffLimit - 512 Несколько десятков вот таких команд: [12/02/2010 17:19:21] 9277 IRP_MJ_DEVICE_CONTROL - Request operates a serial port STATUS_SUCCESS IOCTL_SERIAL_GET_COMMSTATUS - Request returns information about the communication status of a COM port Errors - 0 HoldReasons - 0 AmountInInQueue - 0 AmountInOutQueue - 0 EofReceived - 0 WaitForImmediate - 0 Запрос железке: [12/02/2010 17:19:23] 9347 IRP_MJ_DEVICE_CONTROL - Request operates a serial port STATUS_SUCCESS IOCTL_SERIAL_SET_BREAK_ON - Sets the line control break signal active 9349 IRP_MJ_DEVICE_CONTROL - Request operates a serial port STATUS_SUCCESS IOCTL_SERIAL_SET_BREAK_OFF - Request sets the line control break signal inactive [12/02/2010 17:19:23] 9351 IRP_MJ_WRITE - Request transfers data from a client to a COM port STATUS_SUCCESS 00 ff ff 01 f0 a6 10 47 .??.?│.G Вот тут непонятно, почему Errors = 1 - глюк программы-сниффера? [12/02/2010 17:19:23] 9353 IRP_MJ_DEVICE_CONTROL - Request operates a serial port STATUS_SUCCESS IOCTL_SERIAL_GET_COMMSTATUS - Request returns information about the communication status of a COM port Errors - 1 HoldReasons - 0 AmountInInQueue - 0 AmountInOutQueue - 0 EofReceived - 0 WaitForImmediate - 0 [12/02/2010 17:19:23] 9355 IRP_MJ_DEVICE_CONTROL - Request operates a serial port STATUS_SUCCESS IOCTL_SERIAL_GET_COMMSTATUS - Request returns information about the communication status of a COM port Errors - 0 HoldReasons - 0 AmountInInQueue - 25 AmountInOutQueue - 0 EofReceived - 0 WaitForImmediate - 0 Ну и сам ответ железки: [12/02/2010 17:19:23] 9357 IRP_MJ_READ - Transfers data from a COM port to a client STATUS_SUCCESS 00 00 00 00 01 f0 a6 10 29 99 04 50 00 00 00 00 .....?│.)T.P.... 00 00 00 00 00 00 00 00 a3 99 04 50 00 00 00 00 ........?T.P.... Дальше пара IOCTL_SERIAL_GET_COMMSTATUS IOCTL_SERIAL_SET_BREAK_ON IOCTL_SERIAL_SET_BREAK_OFF IRP_MJ_WRITE 00 ff ff 01 03 00 04 06 - запрос железке IOCTL_SERIAL_GET_COMMSTATUS IRP_MJ_READ - ответ железки ну и т.д. В winapi я не силен, попробовал накропать небольшую программенку, на основе каких-то примеров. #include <windows.h> #include <stdio.h> HANDLE hCom; char *pcCommPort = "COM1"; void mem_read (char *buffer, WORD addr, unsigned char count) { unsigned char command_mem_request [8] = { 0, 0xFF, 0xFF, 1, 0xF0, 0xA6, 0x10, 0x47 }; unsigned char buffer2 [30]; // Зачем это - не знаю. // В логе фирменной программы была установка/снятие BREAK SetCommBreak (hCom); ClearCommBreak (hCom); // Запрос железке WriteFile (hCom, @command_mem_request, 8, &dwBytes, NULL); if (dwBytes != 8) printf ("Error!!!\n"); // Хотим ответ // По факту ничего не получаем while (1) { ReadFile (hCom, &buffer2, 0x1, &dwBytes, NULL); printf ("%u bytes read!\n", dwBytes); } } int main(int argc, char *argv[]) { unsigned char buffer, buffer_prev = 0xF0; COMMTIMEOUTS CommTimeouts; DCB dcb; hCom = CreateFile( pcCommPort, GENERIC_READ | GENERIC_WRITE, 0, // must be opened with exclusive-access NULL, // no security attributes OPEN_EXISTING, // must use OPEN_EXISTING 0, // not overlapped I/O NULL // hTemplate must be NULL for comm devices ); if (hCom == INVALID_HANDLE_VALUE) { printf ("CreateFile failed with error %d.\n", GetLastError()); return (1); } if (!SetupComm (hCom, 1024, 300); ) { printf ("SetupComm with error %d.\n", GetLastError()); return (1); } if (!GetCommState (hCom, &dcb)) { printf ("GetCommState failed with error %d.\n", GetLastError()); return (1); } dcb.BaudRate = CBR_9600; dcb.ByteSize = 8; dcb.Parity = NOPARITY; dcb.StopBits = ONESTOPBIT; if (!SetCommState(hCom, &dcb)) { printf ("SetCommState failed with error %d.\n", GetLastError()); return (1); } if (hCom != INVALID_HANDLE_VALUE && hCom != NULL) printf ("COM1 cofigured!\n"); mem_read (NULL, 0xA6F0, 0x10); return (0); } Лог сниффера: IRP_MJ_CREATE IOCTL_SERIAL_SET_QUEUE_SIZE InSize - 1024 OutSize - 300 IOCTL_SERIAL_GET_BAUD_RATE BaudRate - 9600 IOCTL_SERIAL_GET_LINE_CONTROL StopBits - 0 Parity - 0 WordLength - 8 IOCTL_SERIAL_GET_CHARS EofChar - 0 ErrorChar - 0 BreakChar - 0 EventChar - 0 XonChar - 17 XoffChar - 19 IOCTL_SERIAL_GET_HANDFLOW ControlHandShake - 0x1 (SERIAL_DTR_CONTROL) FlowReplace - 0x40 (SERIAL_RTS_CONTROL) XonLimit - 2048 XoffLimit - 512 Зачем-то еще раз GET_BAUDRATE, GET_LINE_CONTROL, GET_CHARS, GET_HANDFLOW IOCTL_SERIAL_SET_BAUD_RATE BaudRate - 9600 IOCTL_SERIAL_SET_RTS IOCTL_SERIAL_SET_DTR IOCTL_SERIAL_SET_LINE_CONTROL StopBits - 0 Parity - 0 WordLength - 8 IOCTL_SERIAL_SET_CHARS EofChar - 0 ErrorChar - 0 BreakChar - 0 EventChar - 0 XonChar - 17 XoffChar - 19 IOCTL_SERIAL_SET_HANDFLOW ControlHandShake - 0x1 (SERIAL_DTR_CONTROL) FlowReplace - 0x40 (SERIAL_RTS_CONTROL) XonLimit - 2048 XoffLimit - 512 Даем запрос устройству. IOCTL_SERIAL_SET_BREAK_ON IOCTL_SERIAL_SET_BREAK_OFF IRP_MJ_WRITE 00 ff ff 01 f0 a6 10 47 И вместо ответа вот такая ерунда в цикле: 14101 IRP_MJ_READ - STATUS_TIMEOUT Есть у кого-нибудь мысли, что я делаю не так? Какая функция winapi вызывает IOCTL_SERIAL_GET_COMMSTATUS? P.S. Прошу прощения за большой объем сообщения :laughing: Изменено 12 февраля, 2010 пользователем rezident Уменьшение видимого размера цитаты исходника. Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
Xenia 45 12 февраля, 2010 Опубликовано 12 февраля, 2010 · Жалоба Какая функция winapi вызывает IOCTL_SERIAL_GET_COMMSTATUS? BOOL GetCommState( HANDLE hFile, // handle of communications device LPDCB lpDCB // address of device-control block structure ); Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
MEFF 0 12 февраля, 2010 Опубликовано 12 февраля, 2010 (изменено) · Жалоба Нет =( на GetCommState сниффер выдает IOCTL_SERIAL_GET_BAUD_RATE IOCTL_SERIAL_GET_LINE_ IOCTL_SERIAL_GET_CHARS IOCTL_SERIAL_GET_HANDFLOW GetCommModemStatus тоже не подходит =( Изменено 12 февраля, 2010 пользователем MEFF Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
Xenia 45 12 февраля, 2010 Опубликовано 12 февраля, 2010 · Жалоба Тогда не знаю... Но если вы так быстро это проверяете, то проверьте для меня вот это: BOOL TransmitCommChar( HANDLE hFile, // handle of communications device char cChar // character to transmit ); Дает ли она какой-нибудь IOCTL, а если да, то какой? Это самая загадочная команда из всех, т.к. про нее прочти ничего не написано. А очень хотелось бы знать, в каком виде она поступает на USB-устройство. Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
UniSoft 15 12 февраля, 2010 Опубликовано 12 февраля, 2010 (изменено) · Жалоба Какая функция winapi вызывает IOCTL_SERIAL_GET_COMMSTATUS? Скорее всего эта BOOL ClearCommError( HANDLE hFile, // handle to communications device LPDWORD lpErrors, // pointer to variable to receive error codes LPCOMSTAT lpStat // pointer to buffer for communications status ); The ClearCommError function retrieves information about a communications error and reports the current status of a communications device. The function is called when a communications error occurs, and it clears the device's error flag to enable additional input and output (I/O) operations. Эта функция заполняет структуру COMSTAT. И часто используется для того, чтобы определить количество данных в буфере приемника Изменено 12 февраля, 2010 пользователем UniSoft Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
MEFF 0 12 февраля, 2010 Опубликовано 12 февраля, 2010 (изменено) · Жалоба Тогда не знаю... Но если вы так быстро это проверяете, то проверьте для меня вот это: BOOL TransmitCommChar( HANDLE hFile, // handle of communications device char cChar // character to transmit ); Дает ли она какой-нибудь IOCTL, а если да, то какой? Это самая загадочная команда из всех, т.к. про нее прочти ничего не написано. А очень хотелось бы знать, в каком виде она поступает на USB-устройство. [12/02/2010 22:23:32] 41636 IRP_MJ_DEVICE_CONTROL - Request operates a serial port STATUS_SUCCESS IOCTL_SERIAL_IMMEDIATE_CHAR - Request causes a specified character to be transmitted as soon as possible Попробовал с помощью нее побайтно запрос отправить - все равно устройство не отвечает. Serial Port Monitor ее как-то странно обрабатывает. Что команда есть - рисует, а в dump view (дамп принимаемых/отсылаемых байт) ничего нет. Наверное не поддерживает ее полностью =) Скорее всего эта BOOL ClearCommError( Спасибо! Это действительно она. Правда, после посылки запроса и ClearCommError, железка не отвечает AmountInInQueue = 0 =( IOCTL_SERIAL_GET_COMMSTATUS - Request returns information about the communication status of a COM port Errors - 0 HoldReasons - 0 AmountInInQueue - 0 AmountInOutQueue - 0 EofReceived - 0 WaitForImmediate - 0 То ли порт как-то хитро инициализируется, то ли что.... =( Изменено 12 февраля, 2010 пользователем MEFF Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
UniSoft 15 12 февраля, 2010 Опубликовано 12 февраля, 2010 · Жалоба [12/02/2010 22:23:32] То ли порт как-то хитро инициализируется, то ли что.... =( А что за прога если не секрет? Если не большая, и написана не на каком-нибудь визуал басике или жаве, то я могу глянуть что там происходит. Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
MEFF 0 12 февраля, 2010 Опубликовано 12 февраля, 2010 (изменено) · Жалоба А что за прога если не секрет? Если не большая, и написана не на каком-нибудь визуал басике или жаве, то я могу глянуть что там происходит. Прога - специфическая, для мониторинга оборудования. Она защищена HardLock донглом. Написана ногами, и наверняка на васике =) Как распаковать exe-шник от донгловского упаковщика не знаю =( Изменено 12 февраля, 2010 пользователем MEFF Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
UniSoft 15 12 февраля, 2010 Опубликовано 12 февраля, 2010 (изменено) · Жалоба вот так должно работать #include <windows.h> #include <stdio.h> HANDLE hCom; char *pcCommPort = "\\\\.\\COM1"; int comport_read(HANDLE hPort,char *buffer,int cnt,int timeout) { DWORD dwErr; OVERLAPPED o; COMSTAT cstat; int BytesReaded = 0; int tmo = GetTickCount(); while ( (GetTickCount() - tmo) < timeout ) { ClearCommError(hPort, &dwErr, &cstat); if (cstat.cbInQue >= cnt) break; Sleep(1); // это нужно чтобы дать поработать другим процессам системы, // а не занимать весь временной слот ожидая данные и тем самым не грузить систему } BytesReaded = cstat.cbInQue; if (BytesReaded > 0) { if (BytesReaded > cnt) BytesReaded = cnt; o.Internal = 0; o.InternalHigh = 0; o.loffset = 0; o.OffsetHigh = 0; o.hEvent = 0; // можно и так // memclr(&o,0,sizeof(OVERLAPPED)); if (!ReadFile(hPort,buffer,BytesReaded,&BytesReaded,&o)) { BytesReaded = 0; } } return (BytesReaded); } void mem_read (char *buffer, WORD addr, unsigned char count) { unsigned char command_mem_request [8] = { 0, 0xFF, 0xFF, 1, 0xF0, 0xA6, 0x10, 0x47 }; unsigned char buffer2 [300]; OVERLAPPED o; PurgeComm(hCom, PURGE_TXCLEAR | PURGE_RXCLEAR); // Зачем это - не знаю. // В логе фирменной программы была установка/снятие BREAK // SetCommBreak (hCom); // ClearCommBreak (hCom); // Запрос железке // обнулим структуру OVERLAPPED o.Internal = 0; o.InternalHigh = 0; o.loffset = 0; o.OffsetHigh = 0; o.hEvent = 0; // можно и так // memclr(&o,0,sizeof(OVERLAPPED)); WriteFile (hCom, @command_mem_request, 8, &dwBytes, &o); if (dwBytes != 8) printf ("Error!!!\n"); // Хотим ответ // По факту возможно получаем dwBytes = comport_read(hCom,&buffer2,32,5000); // << ждем ответ 5 секунд printf ("%u bytes read!\n", dwBytes); } int main(int argc, char *argv[]) { unsigned char buffer, buffer_prev = 0xF0; COMMTIMEOUTS CommTimeouts; DCB dcb; hCom = CreateFile( pcCommPort, GENERIC_READ | GENERIC_WRITE, NULL, // must be opened with exclusive-access NULL, // no security attributes OPEN_EXISTING, // must use OPEN_EXISTING 0, // not overlapped I/O FILE_FLAG_OVERLAPPED // hTemplate must be NULL for comm devices ); if (hCom == INVALID_HANDLE_VALUE) { printf ("CreateFile failed with error %d.\n", GetLastError()); return (1); } if (!SetupComm (hCom, 1024, 300); ) { printf ("SetupComm with error %d.\n", GetLastError()); return (1); } if (!GetCommState (hCom, &dcb)) { printf ("GetCommState failed with error %d.\n", GetLastError()); return (1); } dcb.BaudRate = CBR_9600; dcb.ByteSize = 8; dcb.Parity = NOPARITY; dcb.StopBits = ONESTOPBIT; if (!SetCommState(hCom, &dcb)) { printf ("SetCommState failed with error %d.\n", GetLastError()); return (1); } PurgeComm(hCom,PURGE_TXABORT | PURGE_RXABORT | PURGE_TXCLEAR | PURGE_RXCLEAR); if (hCom != INVALID_HANDLE_VALUE && hCom != NULL) printf ("COM1 cofigured!\n"); mem_read (NULL, 0xA6F0, 0x10); return (0); } Изменено 12 февраля, 2010 пользователем rezident Уменьшение видимого размера цитаты исходника. Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
MEFF 0 12 февраля, 2010 Опубликовано 12 февраля, 2010 · Жалоба вот так должно работать #include <windows.h> #include <stdio.h> if (!WriteFile (hCom, &command_mem_request, 8, &dwBytes, &o)) { printf ("WriteFile failed with error %d.\n", GetLastError()); return; } WriteFile failed with error 997. Сейчас попробую сам поразбираться... Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
Xenia 45 12 февраля, 2010 Опубликовано 12 февраля, 2010 · Жалоба А мой вариант вам не годится? http://caxapa.ru/138649.html Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
UniSoft 15 12 февраля, 2010 Опубликовано 12 февраля, 2010 · Жалоба if (!WriteFile (hCom, &command_mem_request, 8, &dwBytes, &o)) { printf ("WriteFile failed with error %d.\n", GetLastError()); return; } WriteFile failed with error 997. Сейчас попробую сам поразбираться... упс, конечно-же, я не туда прописал константу hCom = CreateFile( pcCommPort, GENERIC_READ | GENERIC_WRITE, NULL, // must be opened with exclusive-access NULL, // no security attributes OPEN_EXISTING, // must use OPEN_EXISTING 0, // not overlapped I/O FILE_FLAG_OVERLAPPED // [color="#FF0000"][b]<<<< Вот тут ошибся[/b][/color] ); надо так hCom = CreateFile( pcCommPort, GENERIC_READ | GENERIC_WRITE, NULL, // must be opened with exclusive-access NULL, // no security attributes OPEN_EXISTING, // must use OPEN_EXISTING FILE_FLAG_OVERLAPPED , // not overlapped I/O NULL // ); Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
MEFF 0 13 февраля, 2010 Опубликовано 13 февраля, 2010 · Жалоба упс, конечно-же, я не туда прописал константу надо так hCom = CreateFile( pcCommPort, GENERIC_READ | GENERIC_WRITE, NULL, // must be opened with exclusive-access NULL, // no security attributes OPEN_EXISTING, // must use OPEN_EXISTING FILE_FLAG_OVERLAPPED , // not overlapped I/O NULL // ); Почти =) hCom = CreateFile( pcCommPort, GENERIC_READ | GENERIC_WRITE, 0, // must be opened with exclusive-access NULL, // no security attributes OPEN_EXISTING, // must use OPEN_EXISTING FILE_FLAG_OVERLAPPED, // hTemplate must be NULL for comm devices NULL ); Это все я понял и так... Тут выяснился вопрос. Если устройство выключить/включить, то работает и ваш вариант, и мой. Но только на один раз. Например, нужно послать команды (и получить ответ после каждой) 00 FF FF 01 F0 A6 10 47 00 FF FF 01 03 00 04 06 00 FF FF 01 F0 F0 0A 0B На первую команду устройство отвечает, а на остальные - тишина. Устройство больше не реагирует на команды от нашей проги. Нужно его выключить/включить - и оно опять работает на одну команду. Самое интересное, что фирменная прога отлично пашет даже когда железка якобы зависла (не отвечала на самописную программку). Т.е. как по мне, дело не в железке... Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
UniSoft 15 13 февраля, 2010 Опубликовано 13 февраля, 2010 (изменено) · Жалоба Тут выяснился вопрос. Если устройство выключить/включить, то работает и ваш вариант, и мой. Но только на один раз. Например, нужно послать команды (и получить ответ после каждой) 00 FF FF 01 F0 A6 10 47 00 FF FF 01 03 00 04 06 00 FF FF 01 F0 F0 0A 0B На первую команду устройство отвечает, а на остальные - тишина. Устройство больше не реагирует на команды от нашей проги. Нужно его выключить/включить - и оно опять работает на одну команду. Самое интересное, что фирменная прога отлично пашет даже когда железка якобы зависла (не отвечала на самописную программку). Т.е. как по мне, дело не в железке... Ну тут уже нужно более детально анализировать протокол обмена... А там точно не используются дополнительные сигналы порта, как DTR и/или RTS??? либо программный контроль через BREAK??? не зря они там есть... А не отвечает, значит чего-то ждет, может нужно дрыгнуть какой ногой порта, либо может то самое дрыгание BREAK'ом... Самое интересное, что фирменная прога отлично пашет даже когда железка якобы зависла Это еще раз подтверждает, что устройство как-то перезапускается... и скорее всего это один из сигналов... 00 FF FF 01 F0 A6 10 47 а последний байт в командах это CRC (XOR всех байт) ;) Для более подробной информации нужно глубже копать само устройство Изменено 13 февраля, 2010 пользователем UniSoft Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться
MEFF 0 13 февраля, 2010 Опубликовано 13 февраля, 2010 · Жалоба Ну тут уже нужно более детально анализировать протокол обмена... А там точно не используются дополнительные сигналы порта, как DTR и/или RTS??? DTR и RTS не используются. Я отпаял в кабеле все провода кроме TX, RX и GND =) либо программный контроль через BREAK??? не зря они там есть... А не отвечает, значит чего-то ждет, может нужно дрыгнуть какой ногой порта, либо может то самое дрыгание BREAK'ом... SetCommBreak (hCom); ClearCommBreak (hCom); Это не дрыганье? Или нужно что-то сложнее? Завтра буду искать, чтобы почитать на эту тему. Это еще раз подтверждает, что устройство как-то перезапускается... и скорее всего это один из сигналов... а последний байт в командах это CRC (XOR всех байт) ;) Ага =) Для более подробной информации нужно глубже копать само устройство Не знаю уже куда копать, кроме логов сниффера =( А там вроде ничего такого. Только BREAK ON-OFF перед WRITE Цитата Поделиться сообщением Ссылка на сообщение Поделиться на другие сайты Поделиться