Делаем из смартфона websocket-пульт управления радиоуправляемой машинкой
Одной из самых увлекательных сторон работы с микроконтроллерами, лично для меня, является то, что вы можете создать свой собственный аппарат, управляемый по радиоканалу. Есть большое количество разных возможностей для удалённого управления устройствами. В этой же статье мы поговорим о том, как организовать такое управление с помощью микроконтроллера esp32.
В свою бытность, я также как и многие, прошёл путь по созданию разных устройств, базирующихся на модулях HC-05, HC-06:
Источник картинки: www.microsin.net
Однако в данный момент, использование таких модулей представляется нецелесообразным, так как они обладают достаточно высокой ценой, позволяют управлять устройством только по Bluetooth и в то же время требуют наличия внешнего микроконтроллера.
Такая связка была бы целесообразной в начале 2010-х годов, но в настоящее время стоит использовать более современный подход. А если более конкретно, то в качестве микроконтроллера мы возьмём esp32, управлять которым будем с экрана своего смартфона.
Есть разные способы отправки управляющих команд, но, в нашем случае, так как мы рассматриваем способ создания управляемого в реальном времени устройства, то лучше всего использовать передачу команд по протоколу websockets.
Для чего это нужно, вкратце: «нажали на кнопку управления — машинка поехала, отпустили кнопку управления — машинка остановилась» (ну и ещё могут быть всякие плюшки, вроде ответов машинки, отправляемых нам).
Это могут быть не обязательно машинки, это может быть управление различными роботизированными устройствами, что может быть не менее увлекательным.
В этом примере мы разберём только способ передачи команд, реализацию же для конкретных исполнительных устройств, вы сможете разобрать самостоятельно, в зависимости от вашего устройства.
С точки зрения соединения, мы будем подключаться к микроконтроллеру по wifi, где точкой доступа будет выступать наш смартфон (то есть и пультом управления — он же, а управлять будем, соответственно, — машинкой). Поэтому изначально необходимо сконфигурировать и запустить точку доступа на смартфоне, после чего ввести в скетч параметры доступа к ней:
// вставляем ниже SSID и пароль для своей WiFi-сети:
const char* ssid = "сюда название сети";
const char* password = "сюда пароль";
Далее запускаем асинхронный веб-сервер на 80 порту и создаём объект, который будет обрабатывать websocket-ы:
AsyncWebServer server(80);
AsyncWebSocket ws("/ws");
Далее нам необходимо создать веб-страницу, которая будет отображаться у пользователя. Эту страницу мы помещаем в массив index_html.
Как можно видеть, код содержит отдельные блоки, в частности, каскадную таблицу стилей, в которой определяются все шрифты, кнопки, на которые будет нажимать пользователь:
Для обработки нажатий используется JavaScript код, в котором происходит настройка websocket-ов, а также прописывается реакция на происходящие события:
var gateway = `ws://${window.location.hostname}/ws`;
var websocket;
window.addEventListener('load', onLoad);
function initWebSocket() {
console.log('Trying to open a WebSocket connection...');
websocket = new WebSocket(gateway);
websocket.onopen = onOpen;
websocket.onclose = onClose;
websocket.onmessage = onMessage;
}
function onOpen(event) {
console.log('Connection opened');
}
function onClose(event) {
console.log('Connection closed');
setTimeout(initWebSocket, 2000);
}
function onMessage(event) {
var state;
if (event.data == "1"){
state = "ON";
}
else{
state = "OFF";
}
document.getElementById('state').innerHTML = state;
}
Кроме того, инициализируется обработчики нажатий кнопок:
function onLoad(event) {
initWebSocket();
initButton();
initButton2();
initButton3();
initButton4();
}
и прописывается конкретный сценарий, который будет происходить при начале касания и его окончании (будет вызвана соответствующая функция):
function initButton() {
document.getElementById('button').addEventListener('touchstart', forward, false);
document.getElementById('button').addEventListener('touchend', stopper, false);
}
function initButton2() {
document.getElementById('button2').addEventListener('touchstart', left, false);
document.getElementById('button2').addEventListener('touchend', stopper, false);
function initButton3() {
document.getElementById('button3').addEventListener('touchstart', right, false);
document.getElementById('button3').addEventListener('touchend', stopper, false);
}
function initButton4() {
document.getElementById('button4').addEventListener('touchstart', reverse, false);
document.getElementById('button4').addEventListener('touchend', stopper, false);
}
Эта функция, соответственно, посылает определённое сообщение, при произошедшем событии:
function forward(){
websocket.send('forward');
}
function left(){
websocket.send('left');
}
function right(){
websocket.send('right');
}
function reverse(){
websocket.send('reverse');
}
function stopper(){
websocket.send('stop');
}
Теперь нам нужно создать обработчик сообщений, который будет вызываться, при каждом получении нового сообщения. Как можно видеть, при каждом из событий происходит вывод дежурного сообщения в Serial и запись состояния строковой переменной state. Она нужна для прописывания в дальнейшем логики происходящего. Например: «если мы едем вперёд, и поступило сообщение на поворот влево, то машинка начинает плавно подруливать влево».
Именно здесь, чтобы понимать, что происходит в данный момент, и требуется считывать состояние переменной state:
void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) {
// обрабатываем получаемые сообщения
AwsFrameInfo *info = (AwsFrameInfo*)arg;
if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
data[len] = 0;
if (strcmp((char*)data, "forward") == 0) {
ledState = !ledState;
Serial.println("Forward"); // "Вперёд"
state = "Forward";
//сюда вписать обработку для движения вперёд;
}
else if (strcmp((char*)data, "left") == 0) {
ledState = !ledState;
Serial.println("Left"); // "Влево"
state = "Left";
//сюда вписать обработку для движения влево;
}
else if (strcmp((char*)data, "right") == 0) {
ledState = !ledState;
Serial.println("Right"); // "Вправо"
state = "Right";
//сюда вписать обработку для движения вправо;
}
else if (strcmp((char*)data, "reverse") == 0) {
ledState = !ledState;
Serial.println("Reverse"); // "Назад"
state = "Reverse";
//сюда вписать обработку для движения назад;
}
else if (strcmp((char*)data, "stop") == 0) {
ledState = !ledState;
Serial.println("Stop"); // "Стоп"
state = "Stop";
//сюда вписать обработку для остановки;
}
}
}
Как можно видеть, я оставил возможность для прописывания логики событий, так как при каждой конкретной реализации могут происходить различные наборы событий.
Также нам потребуется настроить сервер, который будет отвечать за отслеживание событий клиента: вход в систему, выход из системы, получение данных, получение ошибки, ответ на ping:
void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type,
void *arg, uint8_t *data, size_t len) {
switch (type) {
case WS_EVT_CONNECT:
Serial.printf("WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str());
break;
case WS_EVT_DISCONNECT:
Serial.printf("WebSocket client #%u disconnected\n", client->id());
break;
case WS_EVT_DATA:
handleWebSocketMessage(arg, data, len);
break;
case WS_EVT_PONG:
case WS_EVT_ERROR:
break;
}
}
Для инициализации протокола websockets создана специальная функция:
void initWebSocket() {
ws.onEvent(onEvent);
server.addHandler(&ws);
}
Далее, настроим сервер на прослушивание входящих GET-сообщений, и запустим его:
// Route for root / web page
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send_P(200, "text/html", index_html, processor);
});
// Start server
server.begin();
Как мы говорили уже ранее, в рамках нашей системы смартфон выступает в качестве точки доступа, а esp32 — в роли клиента. Запустим монитор COM порта, точку доступа на смартфоне и посмотрим, что из этого получится. Мы видим, что esp32 подключилась к смартфону и получила IP адрес, и отчёт вывелся в COM порт:
Теперь, если мы попробуем обратиться со смартфона по данному адресу, — то мы увидим, как появится сообщение системы, что появился подключившийся websocket client. При каждом нажатии на кнопку, будет появляться соответствующее сообщение, при отпускании кнопки будет появляться сообщение «Stop». При выключении точки доступа появится сообщение об отключении клиента:
В свою очередь, при загрузке, пульт управления будет таким:
Он выглядит несколько необычно, но это сделано умышленно, чтобы было удобно управлять, при удержании смартфона в горизонтальном положении.
Кстати сказать, при таком способе управления, мы будем ограничены дальностью действия wi-fi. Если же вам потребуется управлять устройством с больших расстояний, то нужно будет изменить схему подключения с такой:
На вот такую:
Для этого придётся использовать радиомодули nrf. Использование их совместно с esp32 таит свои трудности, в частности, придётся использовать видоизменённую библиотеку RF24, с целью дать возможность программе использовать программную реализацию SPI, вместо аппаратной. Об этом неплохо рассказано вот здесь.
Ну вот собственно и всё! Код для работы через websockets протестирован и работает. Остаётся только прописать свою реализацию, для нужного типа двигателя/лей.
А код можно скачать вот здесь.