Історія одного диплома або як зробити шаробота

Історія цього проекту починається в 2014 році, коли я вчився на 4-му курсі в провідному технічному вузі Росії на кафедрі «Робототехнічні системи». В цей час я вже почав замислюватися над темою диплома і шукав проект, який був би цікавий мені, і при цьому в ньому була присутня певна новизна. І ось одного разу, побачивши відео шаробота Rezero, я захотів спробувати повторити успіх. Кому цікаво, що з цього вийшло — прошу під кат.


Введення
На початку хотілося б поговорити про переваги шаробота. Завдяки єдиній точці контакту з поверхнею, шаробот однаково легко пересувається у всіх напрямках, будучи надзвичайно рухливим і маневреним, порівняно зі звичайними колісними роботами. Маневреність шаробота обмежена тільки його динамікою, на відміну від механічних обмежень, що накладаються колесами (наприклад, неможливість руху боком).
Таке важливе достоїнство — робот може бути високим, і чим вище він буде, тим він стійкіший. Чому стійкіше? Це видно з уравнения динаміки зворотного маятника. Прискорення відхилення від вертикального положення рівноваги обернено-пропорційно відстані до центра мас, тобто більш високий зворотний маятник буде падати повільніше. Це знижує вимоги до швидкості реакції системи управління, але, можливо, збільшує момент, який повинні розвивати приводу.
Ще одна перевага — він може їздити по похилих і рухомим поверхонь, наприклад, палуба корабля або підлогу літака при зльоті. Та й погодьтеся, рухається він набагато красивіше, ніж звичайні колісні роботи.
Один з головних недоліків шаробота — можливість втрати вертикального положення рівноваги. Але особисто я думаю, що це цілком вирішуване інженерна завдання. Інженери роботів Aido і Mobi вирішили її наступним чином: при перевищенні деякого кута відхилення висуваються «ноги», щоб робот не втратив стійкість.
Мій розповідь складається з наступних частин:
— Математична модель
— Розробка алгоритмів управління
— Конструкція
— Апаратно-програмне забезпечення
— Результати

1 Математична модель
Висновок рівнянь руху необхідний для подальшого синтезу управління та моделювання руху. У цьому розділі буде трохи математики і механіки.
На даний момент всі існуючі математичні моделі шаробота складені з урахуванням деяких спрощень за допомогою рівнянь Лагранжа 2-го роду. А так як шаробот є неголономної механічної системою, то застосовувати рівняння Лагранжа 2-го роду до такої систему некоректно. В роботі CMU модель шаробота розглядається як три незалежні плоскі моделі, тим самим не враховується взаємовплив цих моделей. У Rezero розробили тривимірну математичну модель, в якій не враховуються гіроскопічні ефекти, що виникають при обертанні омниколес.
Я поставив мету створити найбільш повну математичну модель робота з мінімальною кількістю припущень. У досягненні цієї мети мені допоміг мій науковий керівник С. Л. Крутіков, за що я висловлюю йому щиру подяку.
1.1 Кінематика
Для кінематичного опису системи я ввів рухливі системи координат, як показано на рисунку.

Інерціальна система координат позначена як xyz. Система координат x_1y_1z_1знаходиться в центрі кулі. Для переходу від інерціальної системи відліку до центру кулі використовується наступна однорідна матриця:
\begin{equation*}
\m{A}_c=\begin{pmatrix} \nonumber
1 & 0 & 0 & x \\
0 & 1 & 0 & y\\
0 & 0 & 1 & r_{ball} \\
0 & 0 & 0 & 1
\end{pmatrix},
\end{equation*}
xyзміщення центра кулі вздовж осі xyвідповідно.
Для опису обертання кулі зручно використовувати кардановы кути або кути Tait–Bryan. Таким чином, система координат '_1y'_1z'_1, пов'язана з кулею, виходить шляхом повороту Cx_1y_1z_1на кути \varphi_{x}\varphi_{y}\varphi_{z}навколо осей x_1, y_1z_1відповідно. Дану послідовність поворотів можна представити у вигляді добутку матриць повороту:
\begin{equation*} \label{eq:m_rotaion_ball}
\m{R}_{\varphi} = \m{R}_x(\varphi_x) \m{R}_y(\varphi_y) R_z(\varphi_z).
\end{equation*}
Аналогічно можна описати обертання тіла шаробота в просторі. Система координат x_2y_2z_22-го ланки, пов'язана з тілом, виходить шляхом повороту x_1y_1z_1на кути \vartheta_{x}\vartheta_{y}\vartheta_{z}навколо осей x_1, y_1z_1відповідно. Дану послідовність поворотів також можна представити у вигляді добутку матриць повороту:
\begin{equation} \label{eq:m_rotaion_body}
\m{R}_{\vartheta} = \m{R}_x(\vartheta_{x}) \m{R}_y(\vartheta_{y}) \m{R}_z(\vartheta_{z}). \nonumber
\end{equation}
3-я, 4-а і 5-а системи координат пов'язані з омниколесами. Щоб перейти від системи координат тіла до системи координат омниколеса потрібно виконати ряд елементарних перетворень(два повороту, перенесення, потім знову поворот), я не буду їх описувати тут.
Послідовність переходів представлена у вигляді наступного кінематичного графа:


Для опису стану системи використовується наступний вектор узагальнених координат:
<img src=«tex.s2cms.ru/svg/%0A%5Cbegin%7Bequation%7D%0A%20%20%20%20%5Cve%7Bq%7D%3D(x%2C%20y%2C%20%5Cvarphi_x%2C%20%5Cvarphi_y%2C%20%5Cvarphi_z%2C%20%5Cvartheta_x%2C%20%5Cvartheta_y%2C%20%5Cvartheta_z%2C%20%5Cpsi_1%2C%20%5Cpsi_2%2C%20%5Cpsi_3)%5ET%2C%20%5Cnonumber%0A%5Cend%7Bequation%7D%5C%5C%0A» alt="\begin{equation}
\ve{q}=(x, y, \varphi_x, \varphi_y, \varphi_z, \vartheta_x, \vartheta_y, \vartheta_z, \psi_1, \psi_2, \psi_3)^T, \nonumber
\end{equation}\\"/>
x, y— координати центра кулі, \varphi_x, \varphi_y, \varphi_z, \vartheta_x, \vartheta_y, \vartheta_z— кардановы кути, описують обертання кулі і тіла відповідно, \psi_1, \psi_2, \psi_3— кути повороту омниколеса навколо осі двигуна.
1.2 Динаміка
Виходячи з неголономности системи було прийнято рішення використовувати рівняння Аппеля для складання диференціальних рівнянь руху шаробота.
На систему накладено 6 неголономних зв'язків: три зв'язку кочення омниколес по кулі, дві зв'язку для швидкості центру кулі і одна зв'язок відсутності вертіння кулі. Таким чином, число узагальнених координат m=11, число зв'язків s=6, а число ступенів свободи n=5. Будемо використовувати наступний вектор псевдошвидкостей:
\begin{equation*}
\bm{\dot{\pi}} = (\dot{x}, \dot{y}, \dot{\vartheta_x}, \dot{\vartheta_y}, \dot{\vartheta_z})^T.
\end{equation*}
Для складання рівнянь руху необхідно обчислити енергію прискорень для кожної ланки:
\begin{equation*}
S= \sum\limits_{i=1}^N \frac{1}{2} \cdot tr( \ddot{\m{T}}_{i} \m{H}_{i} \ddot{\m{T}}^\m{T}_{i}}),
\end{equation*}
N— кількість ланок, \m{T}_{i}— матриця переходу від інерціальної системи координат до системи координат i-го ланки, \m{H}_{i}— матриця інерції i-го ланки, а trслід матриці. У загальному випадку Sє функцією від \ddot{q}_1,...,\ddot{q}_m, t. За допомогою рівнянь зв'язку енергія прискорень зводиться до функції, що залежить тільки від \ddot{\pi}_1,...,\ddot{\pi}_n, t.
Масо-інерційні параметриМатриця інерції кулі має діагональний вигляд, т. к. осі системи координат Ox_1y_1z_1є головними центральними осями інерції:
\begin{equation*}
\m{H}_{1}=\begin{pmatrix} %\nonumber
I_{1,xx} &amp;amp; 0 &amp;amp; 0 &amp;amp; 0\\
0 &amp;amp; I_{1,yy} &amp;amp; 0 &amp;amp; 0\\
0 &amp;amp; 0 &amp;amp; I_{1,zz} &amp;amp; 0\\ 
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; m_{1} 
\end{pmatrix}.
\end{equation*}
оскільки площині Cx_2z_2Cy_2z_2є площинами симетрії тіла, відцентрові моменти інерції I_{xy}, I_{xz}I_{yz}дорівнюють нулю. Матриця інерції тіла буде мати наступний вигляд:
\begin{equation*}
\m{H}_{2}=\begin{pmatrix} %\nonumber
I_{2,xx} &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \\
0 &amp;amp; I_{2,yy} &amp;amp; 0 &amp;amp; 0 \\
0 &amp;amp; 0 &amp;amp; I_{2,zz} &amp;amp; m_{2} \cdot l \\ 
0 &amp;amp; 0 &amp;amp; m_{2} \cdot l &amp;amp; m_{2} 
\end{pmatrix},
\end{equation*}
l– відстань від центру кулі до центру мас тіла вздовж осі Cz_2.
Матриця інерції омниколес має діагональний вигляд, т. к. осі системи координат омниколеса утворюють площини симетрії:
\begin{equation*}
\m{H}_{3,4,5}=\begin{pmatrix} %\nonumber
I_{w, xx} &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \\
0 &amp;amp; I_{w, yy} &amp;amp; 0 &amp;amp; 0\\
0 &amp;amp; 0 &amp;amp; I_{w, zz} &amp;amp; 0 \\
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; m_{wheel}
\end{pmatrix}.
\end{equation*}
У цьому моменті інерції I_{w,zz}необхідно також врахувати момент інерції ротора двигуна:
I_{w,zz} =I&#39;_{w,zz} + i^2 \cdot I_{\text{рот}},
i– передавальне число редуктора.
Диференціальне рівняння руху Аппеля в псевдокоординатах:
\begin{equation} \label{eq:appel}
\frac{\partial{S}}{\partial{ \boldsymbol{\ddot{\pi}} }} = \m{Q}_1 + \m{Q}_2,
\end{equation}
\m{Q}_1– узагальнена сила від моментів приводів \m{Q}_2– узагальнена сила від сил тяжіння.
Записуємо рівняння Аппеля в матричній формі та вирішуємо щодо \ddot{\pi}:
\begin{equation*} 
\boldsymbol{\ddot{\pi}} = \m{A}^{-1}(\boldsymbol{\ve{q}}) (\m{Q}_{1} - \boldsymbol{b(\ve{q}, \dot{\pi})} - \boldsymbol{c(\ve{q})})
\end{equation*}
Трохи докладнішеРівняння руху можна записати в матричній формі
\begin{equation*}
\boldsymbol{f}(\boldsymbol{q}, \boldsymbol{\dot{\pi}}, \boldsymbol{\ddot{\pi}}, \boldsymbol{\tau}, t) = \m{A}(\boldsymbol{q}) \boldsymbol{\ddot{\pi}} + \boldsymbol{b}(\boldsymbol{q}, \boldsymbol{\dot{\pi}}) + \boldsymbol{c}(\boldsymbol{q}) - \m{Q}_1 = 0,
\end{equation}
де
\begin{equation*}
\m{A}(\boldsymbol{q}) = \frac{\partial{\boldsymbol{f}}}{ \partial{\boldsymbol{\ddot{\pi}}} }, \\
\boldsymbol{b}(\boldsymbol{q}, \boldsymbol{\dot{\pi}}) = \boldsymbol{f} - \m{A} \cdot \boldsymbol{\ddot{\pi}}, \\
&amp;amp;\boldsymbol{c}(\boldsymbol{q}) = - \m{Q}_{2}. 
\end{equation*}
Всі обчислення виконувалися символьно з допомогою Maple. Потім отримані рівняння були перенесені з Maple в Matlab для моделювання.

2 Розробка алгоритмів управління
Ще трохи теорії управління та матчастину на цьому закінчиться. В теорії управління добре розвинені методи аналізу лінійних систем. Для цілком наблюдаемых систем найбільш часто застосовується оптимальне керування з квадратичним функціоналом(лінійно-квадратичний регулятор, LQR), яке гарантує стабілізацію системи у разі, якщо система цілком керована. Шаробот є цілком спостерігається системою, т. к. вектор стану може бути повністю виміряний. Обчисливши ранг матриці управляемостия також переконався в керованості системи.
Для отримання лінійно-квадратичного регулятора на початку необхідно лінеаризувати систему в околиці вертикального положення нестійкої рівноваги \boldsymbol{q} = 0, \boldsymbol{\dot{\pi}} = 0, \boldsymbol{\ddot{\pi}} = 0
\begin{equation*} 
\boldsymbol{\dot{x}} &amp;amp;= \m{A} \cdot \boldsymbol{x} + \m{B} \cdot \boldsymbol{u}, \nonumber \\
\boldsymbol{y} &amp;amp;= \m{C} \cdot \boldsymbol{x} + \m{D} \cdot \boldsymbol{u}, \nonumber \\
\boldsymbol{x} &amp;amp;= (x, y, \vartheta_x, \vartheta_y, \vartheta_z, \dot{x}, \dot{y}, \dot{\vartheta_x}, \dot{\vartheta_y}, \dot{\vartheta_z})^T, \nonumber \\
\m{C} &amp;amp;= \m{E}_{10}, \quad
\m{D} = 0. \nonumber
\end{equation*}
Вектор \boldsymbol{x}— вектор стану або фазовий вектор, \boldsymbol{y}=\boldsymbol{x}— вектор виміру і \boldsymbol{u}=(\tau_1, \tau_2, \tau_3)^T— управління(моменти приводів).
LQR регулятор має наступний критерій оптимальності:
\begin{equation*} \label{eq:cost}
\m{J} = \int\limits_{0}^{\infty}(\boldsymbol{x}^T \m{Q} \boldsymbol{x} + \boldsymbol{u}^T \m{R} \boldsymbol{u})dt,
\end{equation*}
\m{Q}\m{R}— позитивно визначена матриця. Задача мінімізації даного функціоналу зводиться до розв'язання матричного алгебраїчного рівняння Риккати:
\begin{equation*} \label{eq:ricatti}
\Phi \cdot \m{B} \cdot \m{R}^{-1} \cdot \m{B}^T \cdot \Phi - \Phi \cdot \m{A} - \m{A}^T \cdot \Phi - \m{Q} = 0.
\end{equation*}
Для лінійно-квадратичного регулятора управління записується у вигляді \boldsymbol{u}=-\m{K}\boldsymbol{x}, де \m{K}=\m{R}^{-1}\m{B}\Phi\Phi– рішення рівняння Риккати. У підсумку ми має матрицю коефіцієнтів зворотного зв'язку:
\begin{equation*}
\m{K} = \begin{pmatrix}
0 &amp;amp; -1.0 &amp;amp; 9.63 &amp;amp; 0 &amp;amp; -0.7 &amp;amp; 0 &amp;amp; -1.34 &amp;amp; 1.88 &amp;amp; 0 &amp;amp; -0.18 \\
0.86 &amp;amp; 0.5 &amp;amp; -4.81 &amp;amp; 8.34 &amp;amp; -0.7 &amp;amp; 1.16 &amp;amp; 0.67 &amp;amp; -0.9 &amp;amp; 1.63 &amp;amp; -0.18 \\
-0.86 &amp;amp; 0.5 &amp;amp; -4.81 &amp;amp; -8.34 &amp;amp; -0.7 &amp;amp; -1.16 &amp;amp; 0.67 &amp;amp; -0.9 &amp;amp; -1.63 &amp;amp; -0.18 \\
\end{pmatrix}. \nonumber
\end{equation*}
При множенні даної матриці на вектор стану \boldsymbol{x}ми отримаємо три моменти, які необхідно подати на двигуни для стабілізації системи. LQR є PD регулятором, у разі якщо вектор стану складається з координат і їх похідних. Як ви розумієте, використовувати звичайний PID регулятор і підбирати коефіцієнти вручну в такій системі майже неможливо.
Модель в Matlab simulink c тривимірною візуалізацієюДам невеликі пояснення до кожного блоку. У блоці «Ballbot 3D model» реалізовані рівняння Аппеля. На вхід подається управління \boldsymbol{u}(три моменти), на виході виходить вектор стану \boldsymbol{x}(див. п. 2).
Блок «VRML transform» перетворює вектор стану в координати для візуалізації. «VR Sink» містить модель у форматі vrml і саме цей блок малює графіку.
На вхід блоку «controller» подається вектор стану \boldsymbol{x}і бажаний вектор стану \boldsymbol{x}_d, за допомогою якого можна задавати траєкторію руху і швидкість. Блок виробляє елементарне уможножение матриць для одержання трьох моментів \boldsymbol{u}=-\m{K}(\boldsymbol{x}-\boldsymbol{x}_d).
Так як момент, який розвивають приводу, обмежений, я додав ланка насиченості(saturation).



Для тривимірної візуалізації була зроблена проста модель шаробота в SolidWorks. Потім вона була експортована в форматі vrml додано в блок VR Sink.


Результати моделювання при початковому відхиленні в 10 градусів


3 Конструкція
Конструкція робота розроблена в CAD системі Siemens NX. На підставі побудованої моделі були визначені масові і інерційні характеристики ланок робота.

Конструкція складається з двох алюмінієвих підстав. На нижньому підставі закріплені три двигуни, що приводять в рух омниколеса. Верхня основа кріпиться до нижнього з допомогою амортизаторів і направляючих. Завдання амортизаторів полягала у зменшенні вібрацій верхній частині корпусу, де знаходиться вся електроніка, правда, на практиці це не допомогло.
Полиці зроблені з оргскла для полегшення конструкції. На них розташовується вся бортова електроніка: акумулятор, контролери приводів, мікроконтролер, инерциалка та ін
як кулі використовується баскетбольний м'яч діаметром 240 мм Власники кулі притискають його до омниколесам, тим самим збільшують тертя. На жаль, їх не вийшло виготовити, так як на нашій кафедрі немає 3D принтера, а друкувати на замовлення дорого, т. к. вони не маленькі і коефіцієнт заповнення потрібен великий для міцності.
Омниколеса були придбані на Aliexpress по 120$ за штуку. Найдорожча частина робота, після приводів, звичайно.
Всі металеві деталі виготовлені з дюралюмінію на замовлення на одному Московському заводі. На цьому ж заводі нам зробили полиці з оргскла. Замовлення вийшов на суму 30 000 грн, приблизно.
Трохи збірки фотографій з коментарямиДруг допоміг мені розвести плату, на якій знаходяться DC-DC перетворювач, IMU і логічний перетворювач рівнів для I2C. Зверху встромляється ODROID



Це самий перший варіант конструкції з приводами Dynamixel MX64. Коробки для приводів надруковані.


Амортизатори фірми HSP для радіокерованих моделей з масштабом 1:10.


В такому вигляді я отримав комплект приводів і контролерів.


Щоб надіти омниколесо на вал двигуна довелося виточувати втулку.


Це вже фінальний результат.


4 Апаратно-програмне забезпечення
Майже всі апаратні компоненти шаробота представлені на функціональній схемі. Скажу пару слів про кожен елемент, рухаючись "знизу вгору".


4.1 Приводу і контролери приводів
Всіх складніше було знайти приводу і контролери приводів. На виході LQR регулятора у нас момент, отже треба мати контролер з можливістю управління по струму(тобто моменту). Мабуть, ця задача зустрічається дуже рідко в повсякденному житті, і я знайшов тільки одне доступне за ціною рішення — Dynamixel. Ми купили і спробували приводу Dynamixel MX64, в яких є режим керування по струму. На жаль, їх швидкодії не вистачало для стабілізації робота.
Я вже було втратив надію на створення реального шаробота, але на щастя мені допоміг Ярослав з ННЦ "Робототехніка" і надав на деякий час три приводу Maxon з контролерами, за що я йому дуже вдячний. В результаті у мене виявився безколекторний двигун постійного струму Maxon EC-max 30 40Вт з наступними характеристиками:
  • Номінальна напруга 24В
  • Номінальна швидкість обертання 7220 про/c
  • Номінальний момент 33.8 мНм
  • Утримуючий момент 160 мНм
Планетарний редуктор Maxon GP 32 з передаточним числом n=14 і контролер приводу Maxon EPOS 24/5, який має режим управління по струму.
Як ви можете помітити, приводу не дуже потужні і передаточне число маленьке, тому моменту на виході ледь вистачає для стабілізації шаробота. У Rezero, наприклад, використовуються двигуни 200Вт і редуктор з передаточним числом 51.
4.2 Мікроконтролер, інерціальна модуль, трансивер
В якості мікроконтролера я використовував STM32F4-Discovery, який має необхідні нам інтерфейси: CAN, UART і I2C. Він отримує дані з гіроскопа і акселерометра за I2C і енкодерів по шині CAN. На основі отриманих даних розраховує управління і надсилає завдання на контролери приводів по шині CAN. Щоб не реалізовувати протокол для зв'язку з EPOS контролерами самому, я використовував бібліотеку libepos. Для того, щоб підключити STM'ку до CAN мережі необхідний приймач CAN(трансивер) за 4$.
як інерціального модуля я використовував плату GY-521 за 3$ на основі мікросхеми MPU6050, яка включає в себе 3-х осьовий гіроскоп і 3-х осьовий акселерометр. Для обробки цих показань датчиків я використовував фільтр Маджвика, який останнім часом так полюбили коптероводы.
Для спрощення розробки під STM я використовував STM32Cube HAL(hardware abstraction layer).
Частота на якій працює управління дорівнює приблизно 300 Гц, тобто 300 разів в секунду ми зчитуємо показання всіх датчиків, розраховуємо управління і відправляємо завдання на приводу. Все це відбувається в нескінченному циклі, який можна представити у вигляді наступного псевдокода:
int main()
{
initialize_imu();/* ініціалізація IMU за I2C */
initialize_motors(); /* ініціалізація двигунів за CAN */

while (1) { 
read_imu(); /* читання і фільтрація даних IMU */
get_omniwheels_speed(); /* читання даних енкодерів за CAN */

/* надійшла команда по UART від odroid */
if (uart_rx_flag) {
uart_rx_flag = 0;

struct joystick_data* joystick = (struct joystick_data*)UARTdev_Get_RX_buf();
process_joystick_input(джойстик);
}

calculate_control(); /* обчислення управління */
set_torque(); /* відправка завдання на приводу CAN */
}
}

4.3 Одноплатний комп'ютер, акумулятор, DC-DC перетворювач, джойстик
Бортовий одноплатний комп'ютер ODROID U3 приймає дані від джойстика через Bluetooth адаптер і передає їх на мікроконтролер через UART. На ньому встановлена операційна система lubuntu c ядром Linux 3.8.13.26-rt31 c підтримкою реального часу.

Свинцево-кислотний акумулятор Delta 12045 ємністю C = 4.5 Ач. Його вистачає приблизно на годину роботи.


Напруга живлення одноплатного комп'ютера Odroid-U3 5В, максимальний споживаний струм 2А. Т. к. джерело живлення 12В, необхідний понижуючий LM2596S DC-DC перетворювач за 2$.


Для управління шароботом використовується Bluetooth джойстик Terios. Джойстик передає команди по Bluetooth на ODROID, в якому варто Asus USB-BT400 адаптер. Для читання команд джойстика я використовував наступну бібліотеки. Надійшла команда парс, пакується і відправляється по UART на STM32, де на прийом кожного байта відбувається переривання.

Так само до ODROID можна підключиться по SSH. При запуску одройд створює Ad-hoc мережі через WiFi адаптер.

Результати
Разом у мене вийшло зібрати працюючого шаробота і захистити диплом. На реалізацію цього проекту у мене пішло рівно 2 роки. Скажімо так, у мене не вийшло наблизитися до результатів Rezero, так як у мене не було таких людських і фінансових ресурсів як у студентів з Швейцарії. Думаю, якщо б були більш потужні двигуни і більш просунутий інерціальна модуль, то результати були б на порядок краще.
Вихідний код я не відкриваю з деяких причин. Якщо хочете знати деталі реалізації — напишіть мені, я із задоволенням поділюся з вами кодом індивідуально. Двигуни довелося повернути і на даний момент "залізяка" стоїть без них. Можливо, проект буде далі розвиватися студентами, якщо знайдуться підходящі движки і знайдуться бажаючі. Наприклад, було б цікаво розробити нелінійні алгоритми управління.
Я досить поверхово розповів про процес розробки, так що буду радий відповісти на ваші запитання.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.