Проект «МАШИНА»: Управление с помощью Elbear ace-uno и ESP8266

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Здравствуйте! Перебирая вещи, я наткнулась на сломанную радиоуправляемую машинку и задумалась: почему бы не переделать её и не дать ей второй шанс с новыми функциями? У меня есть интересные платы, которые в последнее время лежат без дела, и я решила, что это отличная возможность их использовать.

Конечно, проект не идеален и требует доработок, но это всего лишь первая проба. В скором времени я планирую превратить этот набросок в полноценный рабочий вариант.

Проект "МАШИНА"
Проект "МАШИНА"

Подключение:

HC-SR04:

  1. Trig (триггер) подключите к пину D3 (пин 3) Elbear ACE-UNO.

  2. Echo (эхо) подключите к пину D2 (пин 2) Elbear ACE-UNO.

  3. VCC (питание) подключите к 5V.

  4. GND (земля) подключите к GND.

Светодиод:

подключите к пину D12 (пин 12) Elbear ACE-UNO.

Сервопривод:

  1. Провод управления (обычно желтый или оранжевый) подключите к пину D9 (пин 9) Elbear ACE-UNO.

  2. Провод питания (обычно красный) подключите к 5V.

  3. Провод земли (обычно черный или коричневый) подключите к GND.

ESP8266:

  1. TX (передача) ESP8266 подключите к RX (прием) Elbear ACE-UNO (например, D0).

  2. RX (прием) ESP8266 подключите к TX (передача) Elbear ACE-UNO (например, D1).

  3. GND ESP8266 подключите к GND Elbear ACE-UNO.

  4. VCC ESP8266 подключите к 3.3V (обратите внимание, что ESP8266 работает на 3.3V).

Описание проекта

Проект "МАШИНА" представляет собой систему, которая может выполнять команды через последовательный порт и веб-интерфейс. Он включает в себя:

  1. Сервопривод для управления движением.

  2. Светодиоды для визуальной индикации состояния.

  3. Ультразвуковой датчик HC-SR04 для измерения расстояния.

  4. ESP8266 для создания веб-сервера, позволяющего управлять устройством через браузер.

Компоненты

  1. Elbear ACE-UNO (Российская Arduino-совместимая плата на отечественном микроконтроллере MIK32 АМУР (Микрон)): Основной контроллер, который управляет сервоприводом и светодиодами.

  2. ESP8266: Модуль Wi-Fi, который позволяет подключать устройство к сети и управлять им удаленно.

  3. HC-SR04: Датчик расстояния, который измеряет расстояние до объекта.

  4. Сервоприводы и светодиоды: Для управления движением и индикации.

Код Elbear ace-uno

#include <Servo.h> // Подключаем библиотеку, которая поможет управлять сервоприводом

// Указываем, к каким пинам подключены наши детали
const int trigPin = 3; // Триггер (для отправки сигнала) подключен к пину D3
const int echoPin = 2; // Эхо (для получения сигнала) подключено к пину D2
const int ledPinD12 = 12; // Внешний светодиод подключен к пину D12
const int servoPin = 9; // Пин, через который будем управлять сервоприводом

Servo myServo; // Создаем "умную" машинку для управления сервоприводом

void setup() {
  pinMode(LED_BUILTIN, OUTPUT); // Настраиваем встроенный светодиод как выход (можем включать/выключать)
  pinMode(ledPinD12, OUTPUT); // Настраиваем D12 как выход для внешнего светодиода
  Serial.begin(9600); // Начинаем общение с компьютером
  Serial.println("Ready to receive commands..."); // Сообщаем, что мы готовы получать команды

  myServo.attach(servoPin); // Подключаем наш сервопривод к указанному пину

  // Настраиваем пины для ультразвукового датчика HC-SR04
  pinMode(trigPin, OUTPUT); // Триггер будет отправлять сигнал
  pinMode(echoPin, INPUT); // Эхо будет получать сигнал

  // Устанавливаем сервопривод в положение 90 градусов (это значит, что он "стоит на месте")
  myServo.write(90);
  delay(1000); // Ждем 1 секунду, чтобы все стабилизировалось
}

void loop() {
  // Проверяем, есть ли новые команды от компьютера
  if (Serial.available()) {
    String command = Serial.readStringUntil('\n'); // Читаем команду до новой строки
    command.trim(); // Убираем лишние пробелы
    Serial.println("Received command: " + command); // Показываем, какую команду мы получили
    
    // Включаем или выключаем светодиоды в зависимости от команды
    if (command == "LED_ON") {
      digitalWrite(LED_BUILTIN, HIGH); // Включаем встроенный светодиод
      Serial.println("Built-in LED is ON"); // Подтверждаем, что светодиод включен
    } else if (command == "LED_OFF") {
      digitalWrite(LED_BUILTIN, LOW); // Выключаем встроенный светодиод
      Serial.println("Built-in LED is OFF"); // Подтверждаем, что светодиод выключен
    } else if (command == "EXTERNAL_LED_ON") {
      digitalWrite(ledPinD12, HIGH); // Включаем внешний светодиод на D12
      Serial.println("External LED on D12 is ON"); // Подтверждаем, что внешний светодиод включен
    } else if (command == "EXTERNAL_LED_OFF") {
      digitalWrite(ledPinD12, LOW); // Выключаем внешний светодиод на D12
      Serial.println("External LED on D12 is OFF"); // Подтверждаем, что внешний светодиод выключен
    } else if (command == "STOP") {
      myServo.write(110); // Устанавливаем угол 110 (это значит, что сервопривод останавливается)
      Serial.println("Servo angle set to STOP (110)"); // Подтверждаем, что сервопривод остановлен
    } else if (command == "BACKWARD") {
      myServo.write(0); // Устанавливаем угол 0 (это значит, что сервопривод движется назад)
      Serial.println("Servo angle set to BACKWARD (0)"); // Подтверждаем, что сервопривод движется назад
    } else if (command == "FORWARD") {
      myServo.write(180); // Устанавливаем угол 180 (это значит, что сервопривод движется вперед)
      Serial.println("Servo angle set to FORWARD (180)"); // Подтверждаем, что сервопривод движется вперед
    } else if (command == "MEASURE_DISTANCE") {
      long distance = measureDistance(); // Измеряем расстояние
      Serial.println("Distance: " + String(distance) + " cm"); // Выводим расстояние в сантиметрах
    } else {
      Serial.println("Unknown command"); // Если команда не распознана, сообщаем об этом

Код esp8266

#include <ESP8266WiFi.h> // Подключаем библиотеку для работы с Wi-Fi
#include <ESP8266WebServer.h> // Подключаем библиотеку для создания веб-сервера

const char* ssid = "******"; // Имя Wi-Fi сети
const char* password = "********"; // Пароль от Wi-Fi сети

// Настройки статического IP-адреса и сети
IPAddress local_IP(192, 168, 1, 109);  // Устанавливаем желаемый статический IP
IPAddress gateway(192, 168, 1, 1);      // Шлюз (обычно это IP вашего роутера)
IPAddress subnet(255, 255, 255, 0);     // Подсеть

ESP8266WebServer server(80); // Создаем веб-сервер на порту 80
String lastCommands[5]; // Массив для хранения последних 5 команд
int commandIndex = 0;   // Индекс для добавления новых команд
String serialOutput;     // Строка для хранения вывода в последовательный порт
long measuredDistance;   // Переменная для хранения измеренного расстояния
const int motorPin = D1; // Пин для управления мотором

void setup() {
  Serial.begin(9600);   // Настраиваем последовательный порт для общения с устройством
  pinMode(LED_BUILTIN, OUTPUT); // Настраиваем встроенный светодиод как выход
  pinMode(motorPin, OUTPUT); // Настраиваем пин для мотора как выход

  // Настраиваем Wi-Fi в режиме клиента
  WiFi.mode(WIFI_AP_STA); // Устанавливаем режим точки доступа и станции
  WiFi.config(local_IP, gateway, subnet); // Устанавливаем статический IP
  WiFi.begin(ssid, password); // Подключаемся к Wi-Fi сети

  // Проверяем подключение к Wi-Fi
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000); // Ждем 1 секунду, пока не подключимся
  }

  // Настраиваем точку доступа
  WiFi.softAP("**********", "******"); // Создаем точку доступа с именем и паролем

  // Регистрируем маршруты (команды)
  server.on("/", handleRoot); // Главная страница
  server.on("/sendCommand", handleSendCommand); // Маршрут для отправки команды
  server.on("/sendSerialInput", handleSendSerialInput); // Маршрут для отправки ввода в последовательный порт
  server.on("/sendMeasureDistance", handleSendMeasureDistance); // Маршрут для измерения расстояния
  server.on("/connectedDevices", handleConnectedDevices); // Маршрут для получения информации о подключенных устройствах
  server.begin(); // Запускаем сервер
}

void loop() {
  server.handleClient(); // Обрабатываем запросы от клиентов
}

void processCommand(String command) {
  // Проверяем, начинается ли команда с "MEASURE_DISTANCE"
  if (command == "MEASURE_DISTANCE") {
    Serial.println("Команда MEASURE_DISTANCE отправлена."); // Подтверждение отправки команды
    while (!Serial.available()); // Ждем, пока не будет доступен ответ
    int measuredDistance = Serial.parseInt(); // Читаем расстояние из последовательного порта
    Serial.println("Измеренное расстояние: " + String(measuredDistance) + " см"); // Выводим измеренное расстояние
    storeCommand("DISTANCE " + String(measuredDistance)); // Сохраняем команду с измеренным расстоянием
  } 
  // Проверяем, начинается ли команда с "SET_SPEED_"
  else if (command.startsWith("SET_SPEED_")) {
    int speed = command.substring(10).toInt(); // Извлекаем скорость из команды
    if (speed >= 0 && speed <= 50 && (speed % 10 == 0)) { // Проверяем, что скорость в допустимых пределах
      analogWrite(motorPin, speed); // Устанавливаем скорость мотора
      Serial.println("Скорость мотора установлена на: " + String(speed)); // Подтверждение
      storeCommand("MOTOR SPEED " + String(speed)); // Сохраняем команду
    } else {
      Serial.println("Получена неверная команда скорости: " + command); // Сообщение об ошибке
    }
  } 
  // Проверяем команды для движения мотора
  else if (command == "FORWARD") {
    // Код для движения мотора вперед
    Serial.println("Мотор движется ВПЕРЕД"); // Подтверждение
    storeCommand("FORWARD"); // Сохраняем команду
  } 
  else if (command == "BACKWARD") {
    // Код для движения мотора назад
    Serial.println("Мотор движется НАЗАД"); // Подтверждение
    storeCommand("BACKWARD"); // Сохраняем команду
  } 
  else if (command == "STOP") {
    // Код для остановки мотора
    Serial.println("Мотор ОСТАНОВЛЕН"); // Подтверждение
    storeCommand("STOP"); // Сохраняем команду
  } 
  else {
    Serial.println("Получена неизвестная команда: " + command); // Сообщение о неизвестной команде
  }
}

void handleRoot() {
  // Начинаем создание HTML-страницы
  String html = "<html lang='en'><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width, initial-scale=1.0'>";
  html += "<title>Измерение расстояния</title>"; // Заголовок страницы
  html += "<link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css'>"; // Подключаем стиль иконок
  html += "<style>"; // Начинаем стили
  html += "body { font-family: Arial, sans-serif; background-color: #f4f4f4; color: #333; margin: 0; padding: 20px; }"; // Стили для фона и текста
  html += "h1 { text-align: center; color: #007BFF; }"; // Стили для заголовка
  html += ".container { max-width: 600px; margin: auto; padding: 20px; background: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }"; // Стили для контейнера
  html += ".grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; margin-bottom: 30px; }"; // Стили для сетки кнопок
  html += "button { padding: 10px 15px; background: #007BFF; color: white; border: none; border-radius: 4px; cursor: pointer; }"; // Стили для кнопок
  html += "button:hover { background: #0056b3; }"; // Стили для кнопок при наведении
  html += "#output { margin-top: 20px; padding: 10px; background: #e9ecef; border-radius: 4px; }"; // Стили для вывода сообщений
  html += "#measuredDistance { font-size: 1.5em; text-align: center; margin-top: 10px; }"; // Стили для отображения расстояния
  html += "</style>"; // Закрываем стили
  html += "<script src='https://code.jquery.com/jquery-3.6.0.min.js'></script>"; // Подключаем библиотеку jQuery для работы с JavaScript
  html += "<script>"; // Начинаем скрипты
  html += "function sendCommand() {"; // Функция для отправки команды
  html += "  var command = $('#commandInput').val();"; // Получаем текст из поля ввода
  html += "  $.post('/sendCommand', { command: command }, function(response) {"; // Отправляем команду на сервер
  html += "    $('#output').html(response);"; // Отображаем ответ на странице
  html += "    $('#commandInput').val('');"; // Очищаем поле ввода
  html += "  });"; // Закрываем функцию
  html += "}";
  html += "function measureDistance() {"; // Функция для измерения расстояния
  html += "  $.post('/sendMeasureDistance', function(response) {"; // Отправляем запрос на измерение
  html += "    $('#measuredDistance').html(response);"; // Обновляем отображение расстояния
  html += "  });"; // Закрываем функцию
  html += "}";
  html += "</script></head><body>"; // Закрываем заголовок и открываем тело страницы
  
  html += "<div class='container'>"; // Начинаем контейнер для содержимого
  html += "<h1>Интерфейс измерения расстояния</h1>"; // Заголовок интерфейса

  // Кнопки для управления светодиодами
  html += "<h2>Управление светодиодами:</h2>";
  html += "<div class='grid'>"; // Начинаем сетку для кнопок
  html += "<button onclick=\"$.post('/sendCommand', { command: 'LED_ON' }, function(response) { $('#output').html(response); });\">Включить встроенный светодиод</button>";
  html += "<button onclick=\"$.post('/sendCommand', { command: 'LED_OFF' }, function(response) { $('#output').html(response); });\">Выключить встроенный светодиод</button>";
  html += "<button onclick=\"$.post('/sendCommand', { command: 'EXTERNAL_LED_ON' }, function(response) { $('#output').html(response); });\">Включить внешний светодиод</button>";
  html += "<button onclick=\"$.post('/sendCommand', { command: 'EXTERNAL_LED_OFF' }, function(response) { $('#output').html(response); });\">Выключить внешний светодиод</button>";
  html += "</div>"; // Закрываем сетку кнопок для светодиодов

  // Кнопки для управления сервоприводом
  html += "<h2>Управление сервоприводом:</h2>";
  html += "<div class='grid'>"; // Начинаем сетку для кнопок
  html += "<button onclick=\"$.post('/sendCommand', { command: 'FORWARD' }, function(response) { $('#output').html(response); });\">Двигаться вперед</button>";
  html += "<button onclick=\"$.post('/sendCommand', { command: 'BACKWARD' }, function(response) { $('#output').html(response); });\">Двигаться назад</button>";
  html += "<button onclick=\"$.post('/sendCommand', { command: 'STOP' }, function(response) { $('#output').html(response); });\">Остановиться</button>";
  html += "</div>"; // Закрываем сетку кнопок для сервопривода

  // Кнопки и отображение расстояния
  html += "<h2>Измерение расстояния:</h2>";
  html += "<div class='grid'>"; // Начинаем сетку для кнопок измерения
  html += "<button onclick='measureDistance()'>Измерить расстояние</button>"; // Кнопка для измерения расстояния
  html += "<div id='measuredDistance'>" + String(measuredDistance) + " см</div>"; // Отображаем измеренное расстояние
  html += "</div>"; // Закрываем сетку для измерения расстояния

  // Поле ввода для команды и кнопка для отправки
  html += "<h2>Отправить команду:</h2>";
  html += "<div class='grid'>"; // Начинаем сетку для ввода команды
  html += "<input type='text' id='commandInput' placeholder='Введите команду'>"; // Поле ввода для команды
  html += "<button onclick='sendCommand()'>Отправить команду</button>"; // Кнопка для отправки команды
  html += "</div>"; // Закрываем сетку для ввода команды

  // Отображение последних 5 команд
  html += "<h2>Последние команды:</h2><ul id='lastCommands'>"; // Заголовок для списка последних команд
  for (int i = 0; i < 5; i++) { // Цикл для отображения последних 5 команд
    if (lastCommands[i] != "") { // Проверяем, есть ли команда
      html += "<li>" + lastCommands[i] + "</li>"; // Добавляем команду в список
    }
  }
  html += "</ul>"; // Закрываем список последних команд

  // Отображение выходных сообщений
  html += "<h2>Вывод:</h2><div id='output'></div>"; // Блок для отображения сообщений

  // Отображение подключенных устройств
  html += "<h2>Подключенные устройства:</h2><button onclick='refreshConnectedDevices()'>Обновить</button>"; // Кнопка для обновления списка устройств
  html += "<div id='connectedDevices'></div>"; // Блок для отображения подключенных устройств
  html += "</div></body></html>"; // Закрываем контейнер, тело и HTML-документ
  
  server.send(200, "text/html", html); // Отправляем HTML-код клиенту с кодом состояния 200 (успех)
}

void handleSendCommand() {
  // Проверяем, есть ли в запросе команда
  if (server.hasArg("command")) {
    // Получаем команду из запроса
    String command = server.arg("command"); 
    // Убираем лишние пробелы в начале и в конце
    command.trim(); 
    // Показываем в консоли, какую команду мы получили
    Serial.println("Команда с веба: " + command); 
    // Добавляем команду в вывод, чтобы показать пользователю
    serialOutput += "Команда с веба: " + command + "<br>"; 
    // Обрабатываем команду (например, выполняем какое-то действие)
    processCommand(command); 
    // Отправляем обратно сообщение, что команда была отправлена
    server.send(200, "text/plain", "Команда отправлена: " + command);
  } else {
    // Если команда не найдена, отправляем сообщение об ошибке
    server.send(400, "text/plain", "Команда не предоставлена");
  }
}

void handleSendSerialInput() {
  // Проверяем, есть ли в запросе ввод для серийного порта
  if (server.hasArg("serialInput")) {
    // Получаем ввод из запроса
    String serialInput = server.arg("serialInput"); 
    // Убираем лишние пробелы
    serialInput.trim(); 
    // Показываем в консоли, что мы получили
    Serial.println("Серийный ввод с веба: " + serialInput); 
    // Обрабатываем ввод (выполняем какое-то действие)
    processCommand(serialInput); 
    // Отправляем обратно сообщение, что команда была отправлена
    server.send(200, "text/plain", "Серийная команда отправлена: " + serialInput);
  } else {
    // Если ввод не найден, отправляем сообщение об ошибке
    server.send(400, "text/plain", "Серийная команда не предоставлена");
  }
}

void handleSendMeasureDistance() {
  // Отправляем команду для измерения расстояния
  processCommand("MEASURE_DISTANCE");
  // Отправляем обратно сообщение с измеренным расстоянием
  server.send(200, "text/plain", "Измеренное расстояние: " + String(measuredDistance) + " см");
}

void handleConnectedDevices() {
  // Начинаем создавать HTML-список
  String html = "<ul>";
  // Получаем количество подключенных устройств
  int numDevices = WiFi.softAPgetStationNum(); 
  // Добавляем информацию о количестве подключенных устройств в список
  html += "<li>Количество подключенных устройств: " + String(numDevices) + "</li>";
  
  // Получаем информацию о каждом подключенном устройстве
  struct station_info *stationList = wifi_softap_get_station_info();
  while (stationList != NULL) {
    // Добавляем MAC-адрес устройства в список
    html += "<li>MAC-адрес устройства: " + String(macToString(stationList->bssid)) + "</li>";
    // Переходим к следующему устройству в списке
    stationList = STAILQ_NEXT(stationList, next);
  }
  html += "</ul>"; // Закрываем список
  // Отправляем HTML-страницу с информацией о подключенных устройствах
  server.send(200, "text/html", html);
}

void storeCommand(String command) {
  // Сохраняем команду в массив последних команд
  lastCommands[commandIndex] = command; 
  // Увеличиваем индекс, чтобы сохранять следующую команду, и сбрасываем его, если он достиг 5
  commandIndex = (commandIndex + 1) % 5; 
}

// Функция для преобразования MAC-адреса в строку
String macToString(uint8_t *mac) {
  String macStr = ""; // Создаем пустую строку для MAC-адреса
  for (int i = 0; i < 6; i++) {
    // Добавляем каждый байт MAC-адреса в строку в шестнадцатеричном формате
    macStr += String(mac[i], HEX);
    // Добавляем двоеточие между байтами, кроме последнего
    if (i < 5) macStr += ":";
  }
  return macStr; // Возвращаем строку с MAC-адресом
}

Где

Объяснение функций в processCommand:

  1. Измерение расстояния: Когда команда MEASURE_DISTANCE получена, устройство отправляет команду на выполнение и ждет ответа. После получения расстояния оно выводит результат в последовательный порт и сохраняет команду.

  2. Установка скорости мотора: Если команда начинается с SET_SPEED_, функция извлекает значение скорости и проверяет, находится ли оно в допустимых пределах (0-50 и кратно 10). Если да, то устанавливает скорость мотора и сохраняет команду.

  3. Движение мотора: Команды FORWARD, BACKWARD и STOP обрабатываются отдельно, где выполняется соответствующее действие и выводится подтверждение в последовательный порт.

  4. Обработка неизвестных команд: Если команда не распознана, выводится сообщение об ошибке.

Объяснение функций в handleRoot:

  1. Создание HTML-страницы: Функция формирует HTML-код, который будет отправлен клиенту. Включает заголовок, стили и скрипты для взаимодействия с сервером.

  2. Подключение jQuery: Библиотека jQuery используется для упрощения AJAX-запросов.

  3. JavaScript функции:

    • sendCommand(): Получает текст из поля ввода, отправляет его на сервер и отображает ответ на странице.

    • measureDistance(): Отправляет запрос на сервер для измерения расстояния и отображает результат.

  4. Структура страницы: HTML включает заголовок, поле ввода для команд, кнопки для выполнения команд и блоки для отображения вывода и результатов измерения.

  5. Отправка HTML: В конце функции HTML-код отправляется клиенту с кодом состояния 200 (OK).

Объяснение handleSendCommand: Эта функция отвечает за обработку команды, которую пользователь отправляет через веб. Если команда есть, она убирает лишние пробелы, показывает, какую команду мы получили, и выполняет ее. Если команды нет, она отправляет сообщение об ошибке.

Объяснение handleSendSerialInput: Эта функция похожа на первую, но она обрабатывает ввод для серийного порта. Она также убирает лишние пробелы, показывает ввод в консоли и выполняет его. Если ввода нет, она отправляет сообщение об ошибке.

Объяснение handleSendMeasureDistance: Эта функция отправляет команду для измерения расстояния. После выполнения команды она возвращает пользователю информацию о том, какое расстояние было измерено.

Объяснение handleConnectedDevices: Эта функция собирает информацию о всех устройствах, которые подключены к нашему Wi-Fi. Она создает список и показывает, сколько устройств подключено, а также их MAC-адреса. Затем она отправляет этот список пользователю в виде веб-страницы.

Объяснение storeCommand: Эта функция сохраняет последние команды, которые были отправлены. Она добавляет новую команду в специальный список и следит за тем, чтобы не сохранять больше пяти команд. Если команд больше пяти, она начинает заменять самые старые.

Объяснение macToString: Эта функция преобразует MAC-адрес (уникальный адрес устройства в сети) в строку, чтобы его было легче читать. Она разбивает адрес на части и добавляет двоеточия между ними.

Итог

Если интересно, подписывайтесь на мой ТГ

Источник: https://habr.com/ru/articles/880578/


Интересные статьи

Интересные статьи

Когда каждый клиентский отзыв может стать решающим фактором для развития продукта или услуги, анкетирование выступает не просто как инструмент сбора информации, но как стратегическое преимущество для ...
Сегодня мы поговорим о том, как использовать вложенные транзакции в Bitrix, чтобы обеспечить целостность данных. Транзакции - это ваше надежное магическое заклинание, которое гарантирует, что ваши мн...
8 утра. Как и в любой другой день, я быстро просматриваю уведомления GitHub через Octobox. «Проблема» «Не работает» «Сломанный» «Сбой» «Ошибка» «Баг» «Не работает» «Поломка» ...
В 2019 году люди знакомятся с брендом, выбирают и, что самое главное, ПОКУПАЮТ через интернет. Сегодня практически у любого бизнеса есть свой сайт — от личных блогов, зарабатывающих на рекламе, до инт...
Мы публикуем видео с прошедшего мероприятия. Приятного просмотра.