Простая реализация Модбас-стека на MSP430. Часть третья: улучшаем библиотеку

Продолжаю тему работы простых МК-устройств с Модбасом. В предыдущих частях я кратко описал сам Модбас, а также первый вариант библиотеки. После обсуждения с коллегами я подверг библиотеку некоторому рефакторингу, который, ИМХО, существенно улучшил читабельность и сделал процесс заимствования либы более простым. Какие-то численные параметры библиотеки существенно не изменились. Самым заметным я бы назвал реализацию парсинга в прерывании (об этом ниже)

Для тех, кто не читал предыдущие «модбасные» заметки, сообщаю: речь идет об очень простой, но вполне рабочей библиотеке, написанной на Си. Библиотека реализует работу с Modbus RTU по последовательному протоколу RS-485. Библиотека писалась и проверена только с микроконтроллером MSP430FR57xx семейства.
Программа создана в виде статической библиотеки и состоит из файлов:

MB_header.h — дефайны из driverlib MSP430, дефайны Модбаса, несколько перечислений ENUM и простых макросов. При портировании сюда зайти обязательно, но изменять нужно немного — почти весь платформенно-зависимый текст я вынес во второй хедер-файл. Да, еще одно: в этом файле содержится краткое описание библиотеки, интерфейса для работы с ней, подсказки для портирования и пример вызывающей программы (аппликации). Так что все, недопонятое в моем корявом русском, можно допонять в перфектном аглицком :)
MB_hardware.h — вот сюда я стащил все, что нужно внимательнейшим образом перелопатить при портировании либы на другие МК. В том числе, здесь помещены макросы, заменяющие самые примитивные обращения к ресурсам МК — чтение из приемного регистра УАРТа, запись в передающий регистр, чтение счетчика-таймера и т.п. Таким образом, я постарался сделать тексты С-файлов малозависимыми от примененного МК.
MB_parsing.c — главная функция разбора запроса от Мастера (и подготовки ответа, разумеется), а также несколько ее помощников. По сути, здесь собрано все, минимально зависящее от МК. Здесь и собственно 2 «точки входа» в библиотеку — функции MBinit() и MBserve(), и обе подпрограммы отработки прерываний, и самая «жирная» функция MBparsing(), в которой происходит разбор полученного фрейма и формирование ответа.
MB_crc.c — вынес совершенно отдельно все, связанное с вычислением CRC. Желающие применить какую-нибудь другую реализацию могут сделать это простой заменой файла. У меня по-прежнему осталась табличная функция, ибо не вижу смысла жаться, а быстрота реакции Слейва мне по душе.
MB_hardware.c — здесь функция инициализации 3-х периферийных модулей: таймера, УАРТа и 3 битов порта (RxD, TxD и направление драйвера шины RS-485, сигнал, который я называю RTS). Конечно, это очень платформо-зависимый файл. Он должен быть полностью пересмотрен при портировании.

Помимо несколько иной разбивки на файлы, работа программы подобна предыдущему варианту библиотеки. Поэтому подробно описывать не буду, тем более, что уделил внимание более тщательному комментированию. Коснусь только существенных отличий.
Отличий два. Первое — передача параметров библиотеке происходит через указатель на структуру (пример коллеги reptile меня воодушевил). Второе — я дал возможность пользователю самому выбрать, куда поместить парсинг — вызывать ли его из главной петли аппликации по наличию принятого фрейма или разбирать фрейм прямо в прерывании таймера, когда становится ясно, что произошло событие t3.5 — то есть, пора рассмотреть, что там накапало в бутыль буфер приема.
Хочу дать уважаемым коллегам некоторую «фактурку», чтобы вы могли сделать выбор в пользу того или иного варианта парсинга.
Итак, вот фрагмент обмена, а именно: переход от приема запроса к формированию ответа и началу его передачи. Вариант с «традиционным» парсингом в фоновой программе (в файле MB_header.h следует закоментить строку "#define PARSE_IN_INT").
КАНАЛЫ логического анализатора:
канал 0 — шина RS-485, там видно начало ответа Слейва (байты 0х01 и 0х03).
каналы 1..3 — код MBState, выведенный на порты МК. В пределах данного рисунка мы видим последовательно состояния этой переменной 1 (RCVE), 2 (PARS) и 3 (SEND)
канал 4 — вызов функции MBserve() из главной петли. Короткий вызов — влетаем в либу и видим, что событие t3.5 еще не произошло, долгий вызов (синие 130 мкс) — таки да! произошло, надыть чьой-то делать…

Стрелка RCVE показывает окончание фазы приема. То есть, в прерывании таймера мы узнаём, что произошло t3.5 — пауза на шине длительностью 3,5 символа. Конечный автомат тут же переходит в состояние PARS, которое и есть признак: можно парсить. Но! Но у главной петли свои планы. И поэтому проходит некоторый временнОй лаг, пока не будет вызвана MBserve(). Вот в последней-то и будет вызвана MBparsing(). Как видим, весь вызов библиотечной функции занял 130 мкс, достаточно шустренько. А вот ждать этого вызова придется по-разному. В текстовке к рисунку я привожу цифры — задержка формирования ответа может составлять от 0 до (250+690) мкс. Тоже ничего смертельного, но нужно учитывать.

Теперь рассмотрим вариант, когда либа скомпилирована с дефинированным PARSE_IN_INT.
Распределение каналов — то же.

Здесь сразу бросается в глаза, что вызовы MBserve() (канал 4) — все короткие. Оно и ясно. Там ничего не проверяется и не производится, кроме сброса программного вотчдога в аппликации. Зато в состоянии PARS автомат находится предельно мало: 124 мкс. Это и понятно, ведь в это состояние мы влетаем в самом прерывании таймера, и тут же, не приходя в сознание, вызывает функцию MBparsing(). Она разбирает запрос, формирует ответ (за 124 мкс) и сразу же переводит наш конечный автомат в состояние SEND. Лепота! Никаких временнЫх лагов. Ответ Слейва начинается практически сразу за паузой t3.5, которую, хош-не-хош, отработать нужно.

В прошлой заметке я уже писал о своем интересе к такой реализации парсинга:

Есть еще более быстрый вариант: не выходить за пределы прерываний вообще. В чем суть: впихнуть функцию MBParsing() прямо в прерывание таймера, в котором стало ясно, что пора переходить к парсингу… И все, ребята! Достаточно теперь в главной программе произвести инициализацию Модбас-стека, как он начнет сам следить за линией. принимать, отвечать — и все это асинхронно к работе основной программы. Очень интересный вариант, но я его не использую: во-первых, некуда спешить, во-вторых, нужно побеспокоиться о правильном обращении к регистрам со стороны главной программы и Модбас-стека. Фокусы могут быть при изменении части переменной...

Вот, честно вам признаюсь, второй вопрос как раз и остался пока за кадром. Ясно, что ситуация очень редкая: в главной программе мы начали обращаться к регистру, а тут бац! — в прерывании таймера оказалось, что пришел запрос от Мастера, а в нем суровый такой наказ: изменить значение именно этого регистра! И регистр изменится, прерывание закончится, а мы, серые, завершаем чтение регистра и получаем кашу… Кроме самой редкости у меня есть джокер в рукаве: я-то пишу все входящие изменения в копию регистров ParsIn[], а работаю всегда с массивом ParsWk[]. То есть, у меня вообще никогда не будет проблем. Но! Но обращаю внимание уважаемых коллег, что при отказе от двух копий регистров нужно будет дополнительно исследовать опасности от парсинга в прерывании таймера.
Ну вот и все. Пока выкладываю на суд коллег в блоге «связь железа с компьютером». Да, решил изменить блог на общественный, но толком так и не понял, куда лучше — строго в МСП нет смысла, «Алгоритмы и программы», «PLC»… Решил, что Модбас таки вяжет наше эмбеддерское железо к чему-то большему — то ли к компу, то ли к ПЛК. Есличьо — перетащим.

ДОБАВЛЕНО: Коллеги, нижайшая просьба: маякните здесь, когда заюзаете библиотеку! Что-то мне подсказывает (да сам характер обсуждаемых вопросов, вот что!) — пока еще пусто на этом фронте. А было бы интересно, есть ли хоть какой выхлоп…

ДОБАВЛЕНО: Обнаружена ошибка при работе с функцией 1 (MB_FUNC_READ_COILS). Будьте внимательны, в файлы архива исправления еще не внесены!
  • +5
  • 08 декабря 2013, 15:14
  • drvlas
  • 1
Файлы в топике: MB.zip

Комментарии (33)

RSS свернуть / развернуть
Прошу прощения, а где же код «улучшенной версии»?
0
там, где кнопочка нарисована и цифра «1»
0
Я так и не понял, то ли было это сразу, то ли я сейчас добавил. КАРОЧИ, уже есть :)
0
Спасибо! Теперь появилось «Файлы в топике: MB.zip». Вопрос снят.
0
Ответ Слейва начинается практически сразу за паузой t3.5.
Обязательно ли выдерживать временные интервалы? Я вот работал с одной панелью по MODBUS. Полностью игнорировал этот нюанс, хотя знал об этом. Интересно, что все четко работает. Скажем так сама панель выдает запросы, а я отвечаю. Так вот ответ панели делал сразу без задержек.
0
Ну, дружище, ты спрашиваешь…
Обязательно ли выдерживать временные интервалы?
А почему не выдерживать? Если даже твой опыт организации слейва дал положительный результат («пронесло») — то стоит ли от этого создавать библиотеку, которую я предлагаю коллегам заимствовать, но которая не соответствует стандарту? Не вижу никакого смысла.
ответ панели делал сразу без задержек
А как ты определял, что панель (Мастер же?) закончила передавать запрос? 3,5 символа тишины и предназначены для сигнализации о конце. И легко отслеживаются. Так зачем делать хуже?
0
А как ты определял, что панель (Мастер же?) закончила передавать запрос?
В принципе, внутри запроса достаточно данных, чтобы определить его конец, если разбирать пакет параллельно с приемом. Пауза нужна не столько для определения конца пакета, сколько для поиска его начала.
0
В самом пакете модбаса есть данные сколько количества байт в самом пакете. По этому количеству полученных байт я и узнавал что пакет получен. по идее то что я игнорировал временные интервалы это отступление от стандарта модбас. Но ведь работает и очень даже хорошо. Благодаря пропуску временных задержек, количество передачи объема информации за период времени намного выше чем с временными задержками. Гдето я читал что из-за этих временных задержек 75% времени модбас «молчит». За точность цифры не ручаюсь ибо точно не помню, но можно посчитать.
0
Но ведь работает и очень даже хорошо
Ты серьезно? Давай я по слогам скажу: «Стан-дар-ту нуж-но со-от-вет-ство-вать». Если нет серьезных причин его нарушать :)
Гдето я читал что из-за этих временных задержек 75% времени модбас «молчит»
Прочти еще раз. Легко подсчитать, что эта цифра ошибочна в разы. А практический опыт (в системе, где много диких обезьян Слейвов висят на линии) говорит о том, что вообще ограничение траффика создается самым медленным на реакцию Слейвом — есть девайсы, откликающиеся через десятки миллисекунд. Кстати, я предполагаю, что в этих девайсах как раз кто-то решил «сэкономить» на выполнении требований стандарта и решил ловить не t3.5, а какие-нить «удобные» 10 или 20 мс, обеспечиваемые системными тиками или ходиками на стене :)
Теперь серьезно. В обсуждении 2-й части много пролито слез по поводу того, как можно выебнуться, чтобы не ловить t3.5. Я сделал либу, которая легко и просто следует стандарту. И не собираюсь размышлять, хорошо ли я поступил. Код маленький, понятный даже такому парню от сохи, каковым я являюсь в программировании — значит, его смогут использовать мои уважаемые коллеги. Кто может сделать лучше — милости просим. Пусть расцветают сто цветов, как говаривал товарисЧ Мао.
0
Так я ж не против стандарта. Только за. Просто я описал свой единичный случай, вот и все. Я как бы не призываю нарушать стандарт. Просто интересно что панель с которой я работал, почему то работает с нарушением стандарта и даже не пукает. Естественно если бы она не работала, то я бы сделал задержку. Но так как она работает, то не делал.
0
Ну, слава Богу, что ты не объявляешь крестовый поход против тех «устаревших догм», которые, на самом деле, позволяют во всем мире легко и просто создавать системы :)
Я читал твои заметки, т.к. и Модбас, и Вайнтеки, и все про умный дом меня интерсует. Уже некоторое время ищу проект, в котором бы мне стало тесно с простыми овеновскими панелями ИП320 — но пока их хватает. Кстати, именно эти ИП320, будучи включеннными как Слейв, тормозят нипадецки. Пара проектов, где их вешал в общую RS-485 кучу — и всьо! Хватит. Вернулся к отдельному RS-232 для этой панели, ибо сил нет 50 мс ждать, пока она пукнет ответ.
А трафик посмотреть на RS-485 очень интересно и поучительно. Вообще, я стал много больше понимать в Модбасе, когда посидел над анализом общения на шине. Уже вполне хорошо работающие системы вдруг показывали всякие глупости, когда шаг по шагу рассматриваешь, чьо они там бухтят меж собой. Особенно стартовые последовательности!
0
Посмотррел я на ИП320, что то как то она меня не впечатлила. Только физические кнопки есть на интерфейсе? Это все? Я думаю если разок побалуешься с Weintek, то потом не захочешь с них слазить. А ИП320 положишь в печку. Хотя для каждой задачи своя железка подбирается. Если скажем для проекта достаточно ИП, то нет смысла переплачивать деньги покупая Weintek. На сайте вижу что она 5 тыс стоит. примерно за такую сумму можно купить и weintek. Правда диагональ там очень маленькая. Следующая модель уже 9 тыс стоит. Но диагональ там уже приемлемая для работы. ну и протоколов разных weintek поддерживает штук около сотни.
0
и протоколов разных weintek поддерживает штук около сотни
А нужен-то всего один какой-то :)
Конечно, панелька очень простая. Графический монохромный экран, никакого тачскрина, несколько кнопок. И, представь себе, прекрасно выполняет асболютно все, что мне нужно в весовых дозаторах. А они являются серийной продукцией — лишнего там не нужно, ибо цена серийного изделия. А вот в системах автоматизации — да, тут я, пожалуй, просто иду накатанной дорожкой, выжимая из панели все, но понимая, что можно сделать и красивее, и функциональнее. Кстати, можно вайнтек, а можно и СП270 от вашего же национального производителя :)
Все еще впереди!
0
Не слишком СП270 дорогая. За 11 тыс такая же по разрешению у Weintek имеет 2-rs485, 1-rs232, 2-USB, 1-ETHERNET, слот для CD карты, аудио выход музыку играть и песни плясать. системная пищалка и пр. Скажем так по меньшей цене больший функционал. Ну и софт к ней конечно навороченный будь здоров. поначалу не знал даже на какой кобыле подъехать, но потом разобрался.
0
Как бы на RS-485 физически невозможно сразу начать отвечать.
0
На что отвечать? Тишина = конец запроса
0
На запрос, ёпт, вверху же написано. На RS-485 эту паузу всяко придётся выдержать и только потом переключиться с приёма на передачу.
0
опять в лужу. сразу и не получится — на подготовку ответного пакета тоже нужно время.
-2
через mutex или semaphore
Ну да, ну да. Я ж не спорю. У меня создано 2 копии регистров — это удобно не только для защиты от таких ситуаций. В первую очередь, я задумал массив «входных» регистров для того, чтобы после приема от Мастера новых значений — проверить их на допустимость. Если все чики-пики — переписываю из «входного» массива в «рабочий». Если ошибка значений — восстанавливаю из «рабочего» во «входной», а в регистре статуса кагбэ намекаю Мастеру, чтобы тот извинился перед тетей и сказал, что спорол хуйню. Но, замечу, все эти проверки выполняются очень скоро по получению команды от Мастера. Реально в этот момент повторный запрос от него не придет никогда — поэтому переписывания «входной» — «рабочий» и наоборот надежно защищены от вторжения просто временнЫми рамками процессов в МК.
Вот так у меня. И думаю, так можно организовывать, если кому интересно. А если есть любовь к более кашерным мютескам и семафорам — велкам, кто ж против!
0
Смотрю на реализацию и возникают вопросы. Начнём с простого — если в программе есть критические ко времени реакции участки, например, используется второй USART, то длинные операции в обработчике прерывания таймера типа расчета CRC пакета крайне нежелательны. Расчет CRC можно сделать пошаговым. Далее. Сам подход с буфером для регистров Modbus это частный случай, когда регистры не «виртуальные», который может быть интересен если есть достаточно ОЗУ и при прочих не мешающих условиях. В реальной программе удобно пользоваться структурированными данными(настройками), которые часто хранятся в энергонезависимой памяти и при этом используются механизмы контроля целостности, например, та же CRC. Для таких случаев изменение «виртуальных» регистров тащит за собой процедуры модификации и сохранения структур, т.е. от подсчетов CRC для блоков, что опять же может быть критично по времени. А представьте, что у Вас не набортная FRAM-память данных, а внешняя последовательная FLASH со временем стирания 100 мс, что вполне реально. Приходилось делать устройства с поддержкой разных протоколов и куда Modbus добавлялся после других. Далее, адреса регистров не обязательно размещаются подряд — кроме назначить свои бывает множество обязов, т.е. «виртуализация» (ака по минимуму трансляция) адресов может потребоваться. Это вопросы привязки к реальному использованию и они часто сложнее самой реализации протокола. Теперь о частностях. Вы используете симпатичный камень, который умеет засыпать и достаточно быстро будится USARTом (конечно для скоростей обмена не более). Не использовать LPM3 как-то нелогично. Для этого нужно использовать DCO для тактирования USART, а таймер тактировать от низкочастотного генератора, например, VLO. Ну и после включения передачи не нашел паузы — если у Вас не FailSafe на линии RS485, то, КМК, «висячая» длинная линия может «сожрать» старт-бит.
0
Ух ты ж-ка! Во наговорил, в одном абзаце :) Ща буду разбираться, по ходу и отвечать.
длинные операции в обработчике прерывания таймера… крайне нежелательны
Ну, так есть настройка: раздел 4. Adjustment в файле MB_header.h говорит нам, что можно закомментировать дефайн PARSE_IN_INT — и в прерывании парсинга не будет. Сам дефайн на полсотни строк ниже.
То есть — кому критично, тот обойдется. Библиотека не сильно изменит поведение, как показано на диаграммах в статье, фаза PARS несколько удлинится. И всьо…
Сам подход с буфером для регистров Modbus это частный случай,… В реальной программе удобно пользоваться структурированными данными, которые часто хранятся в энергонезависимой памяти… Для таких случаев изменение «виртуальных» регистров тащит за собой процедуры модификации и сохранения структур,… что опять же может быть критично по времени.
Конечно частный. Но я, например, его использую и рекомендую. Разработчик сам решит, будет ли он:
— использовать буферный массив регистров
— использовать аж 2 таких массива, рабочий и входной
— использовать подряд идущие адреса
Но! Но хочу заметить, уважаемый коллега sensor_ua, что я говорю о разработчике, о человеке, который и принимает решения. Так кто ему мешает именно так организовать свои регистры? Очень удобно, как на мое ИМХО. И вопрос записи в «медленную» память у меня решается вполне прилично: когда с Мастером переговорим, во входной регистр данные примем, то уже спокойно, неспешно, проверим данные на предмет допустимости, запишем хоть во ФРАМ, хоть во ФЛЕШ, хоть в ЕППЗУ…
Можно ли сделать иначе? Конечно можно! Но я сделал так, очень удобно, чем и делюсь. Не навязывая :)
А если
делать устройства с поддержкой разных протоколов и куда Modbus добавлялся после других
— то тут есть 2 предположения. Первое. Тем, кто такое разрабатывает, смотреть на мою примитивную либу и смысла нет. Берут себе freemodbus — и гей, соколы! Или другой путь: в модбасе принимать в свой массив, а с него потом синхронизовать данные, раскиданные «странным образом».
А с вопросом быстрого усыпления «симпатичного камешка» вышла промашка. Я вообще не усыпляю его, красавчега. В моем девайсе это вовсе ни к чему, да и не научился я еще МСП430 усыплять…

В любом случае, за серьезный анализ либы — спасибо!
0
Тут дело ещё в чём — либа она тем хороша, что её, в т.ч. в предкомпилированном виде, можно прикрутить к чему-либо, а не наоборот. Изоляция от приложения и слой сопряжения с приложением приветствуются, да и слой драйверов тоже. У Вас попытка отделения от привязки к железу есть, но, извините, в зачаточном состоянии, а вот слой сопряжения с пользовательским приложением выглядит всего-лишь как буфер регистров — регистры в любом виде и их Modbus-адреса это прерогатива приложения — их нужно назначать из приложения, а не наоборот.
0
их нужно назначать из приложения, а не наоборот
Ну, тогда я в одинаковой жопе с товарищем Christian Walter. Ибо у него регистры Модбаса тоже в виде массива, без всяких фокусов. А мне такое соседство с уважаемым автором freemodbus весьма даже ненапряжно :)
Что касается зачаточного состояния моих попыток… Честно, не вижу смысла для такой простой задачи (привязать МК к модбасу) очень уж кашерно решать все эти вопросы, не очень даже мне и понятные.
Именно freemodbus, которой я пользовался года 3, меня и не удовлетворяла тем, что слишком заумно написана. Что-то в ней подправить, разобраться с нипаняткой, попытаться оптимизировать под свои критерии — блин… трудно. А в моих нескольких десятках некашерных строк разберется кто угодно. Я же позиционировал предлагаемую либу, как «простую». Но, нассаиваю, рабочую, отвечающую стандарту и вполне переносимую на другой МК, даже без высшего образования :)
0
я нескромно считаю, что freemodbus это просто дема для поисков заказов.
0
Ну, тогда скажи еще, что она написана плохо :)
Ладно, в сухом остатке у нас что:
1) моя либа таки рабочая и ее вполне можно применять как с МСП430, так и с другими МК. То есть, для тех новичков, кто будет читать комменты, ее можно брать и юзать. Так?
2) Вопрос красивого разделения на всякие там лейеры — дело даже не будущего, ибо я не собираюсь такого делать (ты тоже, верно?). Вопрос, ИМХО, стоит ли это делать вообще. Я думаю, что не стоит.
0
1) Не знаю, не пробовал. Прибита гвоздями к конкретному MSP, но при желании можно прибить к другому камню. Только нужно учесть различия в передатчиках USARTов.
2) Для Modbus-Slave использую самописный портируемый код, RTU/ASCII, мультихост и разделение по уровням почти по OSI. Оно того стОит. С приложением стыкуюсь с помощью карт в виде таблиц с адресами и callback-ами. Для «файловых» команд только callback-и.
0
использую самописный портируемый код
Я думаю, многие юзают тот или иной вариант. Жаль только, что не находится желающих предоставить свои наработки для всеобщего пользования и продвижения (несомненно полезного) стандарта в эмбеддерские девайсы. То ли день-матушка, то ли авторские права, то ли вы не придаете такого уж значения популяризации Модбаса… Но вот результат: моя либа плохая, а других нет.
0
Свой код не выложу по ряду причин, в т.ч. некоторых из тех, которые Вы перечислили. В доке MODBUS over Serial Line есть описание машин состояний в виде рисунков. У меня они переведены в код(switch-и) и БУКВАЛЬНО так же обозваны состояния, флаги и ошибки, дабы проще было проверять. Т.е. нового ничего нет. Сам протокол Modbus считаю устаревшим и имеющим множество недостатков. Хотя это не мешает ему оставаться популярным. Да и в практических применениях намного лучше использовать открытый документированный Modbus, чем городить поделки.
И программисты настолько ленивы:), что часто предпочитают таскать за собой отлаженные куски кода, а не писать/отлаживать по-новой, поэтому когда приходится использовать зоопарк камней, то волей-неволей код разделяется на аппаратно-зависимый и портируемый без правок или с косметическими.
Вы свой код «выложили на суд». Я не постеснялся потрудиться его проанализировать, хоть и получилось поверхностно, ну и выдать замечания. Если этого не нужно было делать, то сразу просили бы медальку — ответил бы хорошая или плохая есть или нет.
0
Если этого не нужно было делать
Да ну уж… Я поблагодарил и еще раз повторю: рад обсуждению. И про колбеки — спасибо, внесу в код. Там не так уж сложно подпилить.
Так что не нужно усматривать в моих словах обиды или позу. Я просто хотел, чтобы те, кто не очень понимают суть нашей дискуссии, все же не решили: да ну, тут какой-то левый код, спорят хрен знает о чем, не стоит и вникать.
0
Лично я бы вместо массива регистров сделал пару коллбэков read_register(s)/write_register(s), примерно как в V-USB. А там уж программа на МК пусть сама разбирается, что делать, от read_reпister(uint16_t addr){return Regs[addr];} до чего угодно.
0
Я как бы на это и намекал:)
0
на это и намекал
Со мной надо проще :)
0
А это мысль, да… Надо будет подумать, на досуге.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.