Одной из самых увлекательных сторон работы с микроконтроллерами, лично для меня, является то, что вы можете создать свой собственный аппарат, управляемый по радиоканалу. Есть большое количество разных возможностей для удалённого управления устройствами. В этой же статье мы поговорим о том, как организовать такое управление с помощью микроконтроллера 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.
Как можно видеть, код содержит отдельные блоки, в частности, каскадную таблицу стилей, в которой определяются все шрифты, кнопки, на которые будет нажимать пользователь:
Вот такой кусок
Там же отдельными классами прописываются кнопки:
<source lang="java"> <style>
html {
font-family: Arial, Helvetica, sans-serif;
text-align: center;
}
h1 {
font-size: 1.8rem;
color: white;
}
h2{
font-size: 1.5rem;
font-weight: bold;
color: #143642;
}
.topnav {
overflow: hidden;
background-color: #143642;
}
body {
margin: 0;
}
.content {
padding: 5 px; //30px;
display: flex;
justify-content: center;
// margin: 0 auto;
text-align: center;
// margin-left: auto;
// margin-right: auto;
}
.content2 {
padding: 5 px; //30px;
display: flex;
justify-content: space-between;
// // padding: px; //30px;
// max-width: 400px;
// margin: 0 auto;
}
.content3 {
padding: 40 px; //30px;
display: flex;
justify-content: center;
}
.button1 {
padding: 15px 50px;
font-size: 24px;
text-align: center;
outline: none;
color: #fff;
background-color: #0f8b8d;
border: none;
border-radius: 5px;
width: 670px;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-tap-highlight-color: rgba(0,0,0,0);
}
.button2 {
padding: 15px 5px;
font-size: 24px;
text-align: center;
outline: none;
color: #fff;
background-color: #0f8b8d;
border: none;
border-radius: 5px;
width: 140px;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-tap-highlight-color: rgba(0,0,0,0);
}
/*.button:hover {background-color: #0f8b8d}*/
.button:active {
background-color: #0f8b8d;
box-shadow: 2 2px #CDCDCD;
transform: translateY(2px);
}
Там же отдельными классами прописываются кнопки:
</style> <div class="content">
<p><button id="button" class="button1">forward</button></p>
</div>
<div class="content2">
<p><button id="button2" class="button2"><<< left</button></p>
<p><button id="button3" class="button2">right >>></button></p>
</div>
<div class="content3">
<p><button id="button4" class="button1">reverse</button></p>
</div>
Для обработки нажатий используется 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:
Разбор websocket-сообщения
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 протестирован и работает. Остаётся только прописать свою реализацию, для нужного типа двигателя/лей.
А код можно скачать вот здесь.
Комментарии (12)
roboter
01.12.2021 14:07+1«нажали на кнопку управления — машинка поехала, отпустили кнопку управления — машинка остановилась» - Я так и сделал на Bluetooth, машинка выехала за пределы досягаемости и помчалась прочь. :)
DAN_SEA Автор
01.12.2021 15:21+1Кстати сказать, если ничего не путаю - то по спецификации, протокол bluetooth 5 - до 300 метров пробивает. По идее, можно по одному синему зубу управлять, без nrf-ок. Только вот не факт, что esp32 поддерживает его (не интересовался просто).
roboter
01.12.2021 15:46+2Я это к тому что нужно учитывать что сигнал может пропасть, телефон зависнуть, и т.д.
Алгоритм едем пока не придёт команда стоп подвержен такой ошибке, как я описал.
dmitryrf
02.12.2021 11:39На такой дальности работает только режим Long Range/Coded Phy, который есть не везде.
serafims
Какие получились задержки управления?
Библиотека сервера Websocket своя или откуда-то взявшаяся?
Получалось ли сделать пропорциональное управление?
Заблюренные адреса в локальной сети - зачем...
DAN_SEA Автор
Задержки не измерял - но, по ощущениям "мгновенно".
Библиотеку можно качнуть тут.
Пропорциональное управление делать не пробовал.
Адреса - не знаю зачем. Реверанс в сторону моей паранойи :-)))
nochkin
На WS задержки не ощущаются, отзывчивость для такого медленного устройства вполне на уровне.
Сделать пропорциональное управление не должно быть проблемой. Всё просто упирается в само управление на телефоне.
k0teyka
Задержки будут в зависимости от качества сигнала, я создал достаточное количество вариаций машинок на радиоуправлении по web-socket (esp8266/esp32) включая разные способы проброса видео через онного. Так вот, это не юзабельно скажем в квартире, заехал в соседнюю комнату в угол под диван и т.д. появляются провалы. Далее возникают проблемы с кешем сообщений на уровне смартфона, можно делать ответ-вопрос с uuid, но теряем скорость. Поиграться да, но не надежно к сожалению.
nochkin
Конечно, качество сигнала будет влиять. Я о том, что это уже не проблема WS. С плохим сигналом любой подход будет плохо работать или с задержками.
А если сигнала вообще нет, то и вообще не будет работать вне зависимости от выбора протокола.
У меня вроде не было особых проблем езды "в углу под диваном", но я через роутер делаю, а не p2p. Первый раз слышу, что бы WS что-то сам кешировал на своё усмотрение. Или речь о чём-то другом?