Здравствуйте! Перебирая вещи, я наткнулась на сломанную радиоуправляемую машинку и задумалась: почему бы не переделать её и не дать ей второй шанс с новыми функциями? У меня есть интересные платы, которые в последнее время лежат без дела, и я решила, что это отличная возможность их использовать.
Конечно, проект не идеален и требует доработок, но это всего лишь первая проба. В скором времени я планирую превратить этот набросок в полноценный рабочий вариант.
![Проект "МАШИНА" Проект "МАШИНА"](https://habrastorage.org/getpro/habr/upload_files/1ba/b22/f97/1bab22f97fa32814dd85aa4e66a6ec0d.jpg)
Подключение:
HC-SR04:
Trig (триггер) подключите к пину D3 (пин 3) Elbear ACE-UNO.
Echo (эхо) подключите к пину D2 (пин 2) Elbear ACE-UNO.
VCC (питание) подключите к 5V.
GND (земля) подключите к GND.
Светодиод:
подключите к пину D12 (пин 12) Elbear ACE-UNO.
Сервопривод:
Провод управления (обычно желтый или оранжевый) подключите к пину D9 (пин 9) Elbear ACE-UNO.
Провод питания (обычно красный) подключите к 5V.
Провод земли (обычно черный или коричневый) подключите к GND.
ESP8266:
TX (передача) ESP8266 подключите к RX (прием) Elbear ACE-UNO (например, D0).
RX (прием) ESP8266 подключите к TX (передача) Elbear ACE-UNO (например, D1).
GND ESP8266 подключите к GND Elbear ACE-UNO.
VCC ESP8266 подключите к 3.3V (обратите внимание, что ESP8266 работает на 3.3V).
Описание проекта
Проект "МАШИНА" представляет собой систему, которая может выполнять команды через последовательный порт и веб-интерфейс. Он включает в себя:
Сервопривод для управления движением.
Светодиоды для визуальной индикации состояния.
Ультразвуковой датчик HC-SR04 для измерения расстояния.
ESP8266 для создания веб-сервера, позволяющего управлять устройством через браузер.
Компоненты
Elbear ACE-UNO (Российская Arduino-совместимая плата на отечественном микроконтроллере MIK32 АМУР (Микрон)): Основной контроллер, который управляет сервоприводом и светодиодами.
ESP8266: Модуль Wi-Fi, который позволяет подключать устройство к сети и управлять им удаленно.
HC-SR04: Датчик расстояния, который измеряет расстояние до объекта.
Сервоприводы и светодиоды: Для управления движением и индикации.
Код 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:
Измерение расстояния: Когда команда MEASURE_DISTANCE получена, устройство отправляет команду на выполнение и ждет ответа. После получения расстояния оно выводит результат в последовательный порт и сохраняет команду.
Установка скорости мотора: Если команда начинается с SET_SPEED_, функция извлекает значение скорости и проверяет, находится ли оно в допустимых пределах (0-50 и кратно 10). Если да, то устанавливает скорость мотора и сохраняет команду.
Движение мотора: Команды FORWARD, BACKWARD и STOP обрабатываются отдельно, где выполняется соответствующее действие и выводится подтверждение в последовательный порт.
Обработка неизвестных команд: Если команда не распознана, выводится сообщение об ошибке.
Объяснение функций в handleRoot:
Создание HTML-страницы: Функция формирует HTML-код, который будет отправлен клиенту. Включает заголовок, стили и скрипты для взаимодействия с сервером.
Подключение jQuery: Библиотека jQuery используется для упрощения AJAX-запросов.
JavaScript функции:
sendCommand(): Получает текст из поля ввода, отправляет его на сервер и отображает ответ на странице.
measureDistance(): Отправляет запрос на сервер для измерения расстояния и отображает результат.
Структура страницы: HTML включает заголовок, поле ввода для команд, кнопки для выполнения команд и блоки для отображения вывода и результатов измерения.
Отправка HTML: В конце функции HTML-код отправляется клиенту с кодом состояния 200 (OK).
Объяснение handleSendCommand: Эта функция отвечает за обработку команды, которую пользователь отправляет через веб. Если команда есть, она убирает лишние пробелы, показывает, какую команду мы получили, и выполняет ее. Если команды нет, она отправляет сообщение об ошибке.
Объяснение handleSendSerialInput: Эта функция похожа на первую, но она обрабатывает ввод для серийного порта. Она также убирает лишние пробелы, показывает ввод в консоли и выполняет его. Если ввода нет, она отправляет сообщение об ошибке.
Объяснение handleSendMeasureDistance: Эта функция отправляет команду для измерения расстояния. После выполнения команды она возвращает пользователю информацию о том, какое расстояние было измерено.
Объяснение handleConnectedDevices: Эта функция собирает информацию о всех устройствах, которые подключены к нашему Wi-Fi. Она создает список и показывает, сколько устройств подключено, а также их MAC-адреса. Затем она отправляет этот список пользователю в виде веб-страницы.
Объяснение storeCommand: Эта функция сохраняет последние команды, которые были отправлены. Она добавляет новую команду в специальный список и следит за тем, чтобы не сохранять больше пяти команд. Если команд больше пяти, она начинает заменять самые старые.
Объяснение macToString: Эта функция преобразует MAC-адрес (уникальный адрес устройства в сети) в строку, чтобы его было легче читать. Она разбивает адрес на части и добавляет двоеточия между ними.
Итог
Если интересно, подписывайтесь на мой ТГ