Одной из самых увлекательных сторон работы с микроконтроллерами, лично для меня, является то, что вы можете создать свой собственный аппарат, управляемый по радиоканалу. Есть большое количество разных возможностей для удалённого управления устройствами. В этой же статье мы поговорим о том, как организовать такое управление с помощью микроконтроллера 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)


  1. serafims
    30.11.2021 18:15
    +3

    Какие получились задержки управления?
    Библиотека сервера Websocket своя или откуда-то взявшаяся?
    Получалось ли сделать пропорциональное управление?

    Заблюренные адреса в локальной сети - зачем...


    1. DAN_SEA Автор
      30.11.2021 18:47

      Задержки не измерял - но, по ощущениям "мгновенно".

      Библиотеку можно качнуть тут.

      Пропорциональное управление делать не пробовал.

      Адреса - не знаю зачем. Реверанс в сторону моей паранойи :-)))


    1. nochkin
      30.11.2021 20:01
      +1

      На WS задержки не ощущаются, отзывчивость для такого медленного устройства вполне на уровне.

      Сделать пропорциональное управление не должно быть проблемой. Всё просто упирается в само управление на телефоне.


      1. k0teyka
        30.11.2021 21:23

        Задержки будут в зависимости от качества сигнала, я создал достаточное количество вариаций машинок на радиоуправлении по web-socket (esp8266/esp32) включая разные способы проброса видео через онного. Так вот, это не юзабельно скажем в квартире, заехал в соседнюю комнату в угол под диван и т.д. появляются провалы. Далее возникают проблемы с кешем сообщений на уровне смартфона, можно делать ответ-вопрос с uuid, но теряем скорость. Поиграться да, но не надежно к сожалению.


        1. nochkin
          01.12.2021 03:13
          +1

          Конечно, качество сигнала будет влиять. Я о том, что это уже не проблема WS. С плохим сигналом любой подход будет плохо работать или с задержками.

          А если сигнала вообще нет, то и вообще не будет работать вне зависимости от выбора протокола.

          У меня вроде не было особых проблем езды "в углу под диваном", но я через роутер делаю, а не p2p. Первый раз слышу, что бы WS что-то сам кешировал на своё усмотрение. Или речь о чём-то другом?


  1. iswitch
    01.12.2021 13:49
    +3

    Смешно


    1. DAN_SEA Автор
      01.12.2021 13:56

      Да. Не судите слишком строго. "желание мнимой безопасности" требует своих жертв :-)


      1. Delsian
        01.12.2021 18:37
        +1

        "Я тебя по айпи вычислю!" (с)


  1. roboter
    01.12.2021 14:07
    +1

    «нажали на кнопку управления — машинка поехала, отпустили кнопку управления — машинка остановилась» - Я так и сделал на Bluetooth, машинка выехала за пределы досягаемости и помчалась прочь. :)


    1. DAN_SEA Автор
      01.12.2021 15:21
      +1

      Кстати сказать, если ничего не путаю - то по спецификации, протокол bluetooth 5 - до 300 метров пробивает. По идее, можно по одному синему зубу управлять, без nrf-ок. Только вот не факт, что esp32 поддерживает его (не интересовался просто).


      1. roboter
        01.12.2021 15:46
        +2

        Я это к тому что нужно учитывать что сигнал может пропасть, телефон зависнуть, и т.д.
        Алгоритм едем пока не придёт команда стоп подвержен такой ошибке, как я описал.


      1. dmitryrf
        02.12.2021 11:39

        На такой дальности работает только режим Long Range/Coded Phy, который есть не везде.