Среди проектов небольших управляемых машинок, оснащенных камерой, особое место занимают те, которые позволяют быстро, с минимумом деталей собрать нечто управляемое по wi-fi. Но, как правило, сложности здесь возникают даже на этапе подборки компонентов, определения их совместимости. В данном проекте мы попробуем в бою esp32-cam и драйвер двигателя — tb6612fng.

Машинка будет управляться со смартфона либо стационарного ПК и, разумеется, будет максимально бюджетная. Помимо прочего в статье предпринята попытка уйти от arduin, уменьшить размеры платформы.

Проект рассчитан на начинающих, а также немного продолжающих.

Предисловие


Почему именно esp32-cam?

Данная плата выбрана в силу своей доступности по цене и относительной простоте разработки.
Из ее преимуществ также необходимо упомянуть — возможность монтирования камеры, наличие встроенного wi-fi, а также низкое энергопотребление самой платы.

Как правило, в схожих проектах используются всем известные arduino. Они берут на себя заботу по управлению двигателями. Нет ничего в этом зазорного. Но можно ли обойтись без них, используя только esp32? Оказывается, можно.

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

Выбранный базовый проект (ссылка), на наш взгляд наиболее удачен среди большинства прочих (например следующего либо этого).
*Хотя приведенные проекты по-своему хороши и тут уже дело вкуса.

Железо


Мы используем одну колесную пару и одно «псевдо-колесо» с шаром внутри. Машинка с двумя колесами будет иметь большую управляемость в отличие от четырехколесной.

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

В целом комплект сборки будет выглядеть следующим образом.

Состав комплектующих:

колесная пара — 35 руб.



двигатели — 185 руб. (12V 1500rpm)



пластиковые крепления для двигателей — 220 руб.



esp32-cam c камерой ночного вида — 550 руб.



-* антенна для esp32-cam — 65 руб.



tb6612fng — драйвер двигателей — 100 руб.



понижающий преобразователь с 12V до 5V — 38 руб.



опора шаровая с железным шаром -60 руб.



— провода — 200 руб.
— ftdi-преобразователь (для прошивки esp-32) — 100 руб.



— 18650 аккумуляторы + батарейный отсек — 300 руб.





Итого: ~1800 руб.

Пояснения по драйверу двигателей

Tb6612fng как драйвер управления двигателями выбран не столько из-за того, чтобы далеко не отходить от базового проекта, сколько по иным причинам.

Характеристики данного драйвера достаточно привлекательны, как и его цена. Кроме того, размеры данного драйвера также впечатляют, если смотреть на «золотой стандарт» в машинках подобного рода — L298N.

Вот сводная таблица по схожим драйверам(включая «золотой стандарт»):



ссылка на
оригинал
.

Как видно, tb6612fng не самый плохой вариант.



Пояснения по камерам esp-32 cam



Камеры для esp-32 cam представлены целым спектром представителей. Но практика показала, что в момент, когда за окном смеркается большинство представителей также «сворачивают свою трансляцию» — на изображении ничего не видно. Поэтому собрав пару проектов с обычной камерой для esp32-cam было принято решение остановиться на камере ночного зрения.

Разумеется речь идет о псевдо ночном зрении, так как в полной темноте через камеру ничего не рассмотреть, даже используя встроенный светодиод-вспышку на esp-32 cam.

Есть проекты, в которых умельцы дополнительно навешивали более мощное освещение — например здесь. Но это уже дело вкуса. В данном проекте доп. освещение-подсветка использоваться не будет.

*Стоит отдельно упомянуть, что из обычной камеры, если уж совсем надо, возможно сделать «ночную», сняв окуляр и отклеив с матрицы фильтр. Но делать это не рекомендую, так как, по практике, редко получается сделать это аккуратно и можно «отклеить» заодно кусок матрицы, испортив камеру.

Софт


Перед тем как начать все собирать, необходимо сперва залить прошивку в esp32-cam и проверить ее работоспособность.

Для этого можно использовать стандартную среду разработки для arduino — arduino ide.

По умолчанию, после установки данной среды, esp32-cam библиотек там нет. Поэтому потребуется их установка в arduino ide.

Открыв Файл-настройки, необходимо добавить ссылку на esp32

https://dl.espressif.com/dl/package_esp32_index.json  



Далее Скетч-Подключить библиотеку-Управлять библиотеками:



в строке поиска найти esp32 и установить.

Перейдем к скетчу


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

esp32cam-robot.ino
/*
  ESP32CAM Robot Car
  esp32cam-robot.ino (requires app_httpd.cpp)
  Based upon Espressif ESP32CAM Examples
  Uses TBA6612FNG H-Bridge Controller
  
  DroneBot Workshop 2021
  https://dronebotworkshop.com
*/

#include "esp_wifi.h"
#include "esp_camera.h"
#include <WiFi.h>
#include "soc/soc.h"
#include "soc/rtc_cntl_reg.h"

// Setup Access Point Credentials
const char* ssid1 = "ESP32-CAM Robot";
const char* password1 = "1234567890";

extern volatile unsigned int  motor_speed;
extern void robot_stop();
extern void robot_setup();
extern uint8_t robo;
extern volatile unsigned long previous_time;        
extern volatile unsigned long move_interval; 

#define CAMERA_MODEL_AI_THINKER
#define PWDN_GPIO_NUM     32
#define RESET_GPIO_NUM    -1
#define XCLK_GPIO_NUM      0
#define SIOD_GPIO_NUM     26
#define SIOC_GPIO_NUM     27

#define Y9_GPIO_NUM       35
#define Y8_GPIO_NUM       34
#define Y7_GPIO_NUM       39
#define Y6_GPIO_NUM       36
#define Y5_GPIO_NUM       21
#define Y4_GPIO_NUM       19
#define Y3_GPIO_NUM       18
#define Y2_GPIO_NUM        5
#define VSYNC_GPIO_NUM    25
#define HREF_GPIO_NUM     23
#define PCLK_GPIO_NUM     22

void startCameraServer();

void setup() 
{
  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); // prevent brownouts by silencing them
  
  Serial.begin(115200);
  Serial.setDebugOutput(true);
  Serial.println();



  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM;
  config.pin_vsync = VSYNC_GPIO_NUM;
  config.pin_href = HREF_GPIO_NUM;
  config.pin_sscb_sda = SIOD_GPIO_NUM;
  config.pin_sscb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 20000000;
  config.pixel_format = PIXFORMAT_JPEG;
  //init with high specs to pre-allocate larger buffers
  if(psramFound()){
    config.frame_size = FRAMESIZE_QVGA;
    config.jpeg_quality = 10;
    config.fb_count = 2;
  } else {
    config.frame_size = FRAMESIZE_QVGA;
    config.jpeg_quality = 12;
    config.fb_count = 1;
  }

  // camera init
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x", err);
    return;
  }

  //drop down frame size for higher initial frame rate
  sensor_t * s = esp_camera_sensor_get();
  s->set_framesize(s, FRAMESIZE_QVGA);
  s->set_vflip(s, 1);
  s->set_hmirror(s, 1);

  WiFi.softAP(ssid1, password1);
  IPAddress myIP = WiFi.softAPIP();
  Serial.print("AP IP address: ");
  Serial.println(myIP);
  
  startCameraServer();

  ledcSetup(7, 5000, 8);
  ledcAttachPin(4, 7);  //pin4 is LED
  robot_setup();
  
  for (int i=0;i<5;i++) 
  {
    ledcWrite(7,10);  // flash led
    delay(50);
    ledcWrite(7,0);
    delay(50);    
  }
      
  previous_time = millis();
}

void loop() {
  if(robo)
  {
    unsigned long currentMillis = millis();
    if (currentMillis - previous_time >= move_interval) {
      previous_time = currentMillis;
      robot_stop();
      char rsp[32];
      sprintf(rsp,"SPPED: %d",motor_speed);
      Serial.println("Stop");
      robo=0;
    }
  }
  delay(1);
  yield();
}


В коде помимо прочего создается wifi точка доступа, к которой можно подключаться. Поэтому стоит обратить внимание на ее параметры, которые описаны в

const char* ssid1 = "ESP32-CAM Robot";
const char* password1 = "1234567890";

Также при компиляции прошивки используется файл app_httpd.cpp, который должен находиться в одной папке с программой.

Листинг app_httpd.cpp —
app_httpd.cpp
/*
  ESP32CAM Robot Car
  app_httpd.cpp (requires esp32cam-robot.ino)
  Based upon Espressif ESP32CAM Examples
  Uses TBA6612FNG H-Bridge Controller
  
  DroneBot Workshop 2021
  https://dronebotworkshop.com
*/

#include "dl_lib_matrix3d.h"
#include <esp32-hal-ledc.h>
#include "esp_http_server.h"
#include "esp_timer.h"
#include "esp_camera.h"
#include "img_converters.h"
#include "Arduino.h"

// TB6612FNG H-Bridge Connections (both PWM inputs driven by GPIO 12)
#define MTR_PWM   12
#define LEFT_M0     15
#define LEFT_M1     14
#define RIGHT_M0    13
#define RIGHT_M1    2

// Define Speed variables
int speed = 255;
int noStop = 0;

//Setting Motor PWM properties
const int freq = 2000;
const int motorPWMChannnel = 8;
const int lresolution = 8;

volatile unsigned int  motor_speed   = 200;
volatile unsigned long previous_time = 0;
volatile unsigned long move_interval = 250;

// Placeholder for functions
void robot_setup();
void robot_stop();
void robot_fwd();
void robot_back();
void robot_left();
void robot_right();
uint8_t robo = 0;


typedef struct {
  httpd_req_t *req;
  size_t len;
} jpg_chunking_t;

#define PART_BOUNDARY "123456789000000000000987654321"
static const char* _STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY;
static const char* _STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n";
static const char* _STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n";

httpd_handle_t stream_httpd = NULL;
httpd_handle_t camera_httpd = NULL;

static size_t jpg_encode_stream(void * arg, size_t index, const void* data, size_t len) {
  jpg_chunking_t *j = (jpg_chunking_t *)arg;
  if (!index) {
    j->len = 0;
  }
  if (httpd_resp_send_chunk(j->req, (const char *)data, len) != ESP_OK) {
    return 0;
  }
  j->len += len;
  return len;
}

static esp_err_t capture_handler(httpd_req_t *req) {
  camera_fb_t * fb = NULL;
  esp_err_t res = ESP_OK;
  int64_t fr_start = esp_timer_get_time();

  fb = esp_camera_fb_get();
  if (!fb) {
    Serial.println("Camera capture failed");
    httpd_resp_send_500(req);
    return ESP_FAIL;
  }

  httpd_resp_set_type(req, "image/jpeg");
  httpd_resp_set_hdr(req, "Content-Disposition", "inline; filename=capture.jpg");

  size_t out_len, out_width, out_height;
  uint8_t * out_buf;
  bool s;
  {
    size_t fb_len = 0;
    if (fb->format == PIXFORMAT_JPEG) {
      fb_len = fb->len;
      res = httpd_resp_send(req, (const char *)fb->buf, fb->len);
    } else {
      jpg_chunking_t jchunk = {req, 0};
      res = frame2jpg_cb(fb, 80, jpg_encode_stream, &jchunk) ? ESP_OK : ESP_FAIL;
      httpd_resp_send_chunk(req, NULL, 0);
      fb_len = jchunk.len;
    }
    esp_camera_fb_return(fb);
    int64_t fr_end = esp_timer_get_time();
    Serial.printf("JPG: %uB %ums\n", (uint32_t)(fb_len), (uint32_t)((fr_end - fr_start) / 1000));
    return res;
  }

  dl_matrix3du_t *image_matrix = dl_matrix3du_alloc(1, fb->width, fb->height, 3);
  if (!image_matrix) {
    esp_camera_fb_return(fb);
    Serial.println("dl_matrix3du_alloc failed");
    httpd_resp_send_500(req);
    return ESP_FAIL;
  }

  out_buf = image_matrix->item;
  out_len = fb->width * fb->height * 3;
  out_width = fb->width;
  out_height = fb->height;

  s = fmt2rgb888(fb->buf, fb->len, fb->format, out_buf);
  esp_camera_fb_return(fb);
  if (!s) {
    dl_matrix3du_free(image_matrix);
    Serial.println("to rgb888 failed");
    httpd_resp_send_500(req);
    return ESP_FAIL;
  }

  jpg_chunking_t jchunk = {req, 0};
  s = fmt2jpg_cb(out_buf, out_len, out_width, out_height, PIXFORMAT_RGB888, 90, jpg_encode_stream, &jchunk);
  dl_matrix3du_free(image_matrix);
  if (!s) {
    Serial.println("JPEG compression failed");
    return ESP_FAIL;
  }

  int64_t fr_end = esp_timer_get_time();
  return res;
}

static esp_err_t stream_handler(httpd_req_t *req) {
  camera_fb_t * fb = NULL;
  esp_err_t res = ESP_OK;
  size_t _jpg_buf_len = 0;
  uint8_t * _jpg_buf = NULL;
  char * part_buf[64];
  dl_matrix3du_t *image_matrix = NULL;

  static int64_t last_frame = 0;
  if (!last_frame) {
    last_frame = esp_timer_get_time();
  }

  res = httpd_resp_set_type(req, _STREAM_CONTENT_TYPE);
  if (res != ESP_OK) {
    return res;
  }

  while (true) {
    fb = esp_camera_fb_get();
    if (!fb) {
      Serial.println("Camera capture failed");
      res = ESP_FAIL;
    } else {
      {
        if (fb->format != PIXFORMAT_JPEG) {
          bool jpeg_converted = frame2jpg(fb, 80, &_jpg_buf, &_jpg_buf_len);
          esp_camera_fb_return(fb);
          fb = NULL;
          if (!jpeg_converted) {
            Serial.println("JPEG compression failed");
            res = ESP_FAIL;
          }
        } else {
          _jpg_buf_len = fb->len;
          _jpg_buf = fb->buf;
        }
      }
    }
    if (res == ESP_OK) {
      size_t hlen = snprintf((char *)part_buf, 64, _STREAM_PART, _jpg_buf_len);
      res = httpd_resp_send_chunk(req, (const char *)part_buf, hlen);
    }
    if (res == ESP_OK) {
      res = httpd_resp_send_chunk(req, (const char *)_jpg_buf, _jpg_buf_len);
    }
    if (res == ESP_OK) {
      res = httpd_resp_send_chunk(req, _STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY));
    }
    if (fb) {
      esp_camera_fb_return(fb);
      fb = NULL;
      _jpg_buf = NULL;
    } else if (_jpg_buf) {
      free(_jpg_buf);
      _jpg_buf = NULL;
    }
    if (res != ESP_OK) {
      break;
    }
    int64_t fr_end = esp_timer_get_time();
    int64_t frame_time = fr_end - last_frame;
    last_frame = fr_end;
    frame_time /= 1000;
    Serial.printf("MJPG: %uB %ums (%.1ffps)\n",
                  (uint32_t)(_jpg_buf_len),
                  (uint32_t)frame_time, 1000.0 / (uint32_t)frame_time
                 );
  }

  last_frame = 0;
  return res;
}

enum state {fwd, rev, stp};
state actstate = stp;

static esp_err_t cmd_handler(httpd_req_t *req)
{
  char*  buf;
  size_t buf_len;
  char variable[32] = {0,};
  char value[32] = {0,};

  buf_len = httpd_req_get_url_query_len(req) + 1;
  if (buf_len > 1) {
    buf = (char*)malloc(buf_len);
    if (!buf) {
      httpd_resp_send_500(req);
      return ESP_FAIL;
    }
    if (httpd_req_get_url_query_str(req, buf, buf_len) == ESP_OK) {
      if (httpd_query_key_value(buf, "var", variable, sizeof(variable)) == ESP_OK &&
          httpd_query_key_value(buf, "val", value, sizeof(value)) == ESP_OK) {
      } else {
        free(buf);
        httpd_resp_send_404(req);
        return ESP_FAIL;
      }
    } else {
      free(buf);
      httpd_resp_send_404(req);
      return ESP_FAIL;
    }
    free(buf);
  } else {
    httpd_resp_send_404(req);
    return ESP_FAIL;
  }

  int val = atoi(value);
  sensor_t * s = esp_camera_sensor_get();
  int res = 0;

// Look at values within URL to determine function
  if (!strcmp(variable, "framesize"))
  {
    Serial.println("framesize");
    if (s->pixformat == PIXFORMAT_JPEG) res = s->set_framesize(s, (framesize_t)val);
  }
  else if (!strcmp(variable, "quality"))
  {
    Serial.println("quality");
    res = s->set_quality(s, val);
  }
  else if (!strcmp(variable, "flash"))
  {
    ledcWrite(7, val);
  }
  else if (!strcmp(variable, "flashoff"))
  {
    ledcWrite(7, val);
  }
  else if (!strcmp(variable, "speed"))
  {
    if      (val > 255) val = 255;
    else if (val <   0) val = 0;
    speed = val;
    ledcWrite(8, speed);
  }
  else if (!strcmp(variable, "nostop"))
  {
    noStop = val;
  }
  else if (!strcmp(variable, "car")) {
    if (val == 1) {
      Serial.println("Forward");
      robot_fwd();
      robo = 1;
    }
    else if (val == 2) {
      Serial.println("TurnLeft");
      robot_left();
      robo = 1;
    }
    else if (val == 3) {
      Serial.println("Stop");
      robot_stop();
    }
    else if (val == 4) {
      Serial.println("TurnRight");
      robot_right();
      robo = 1;
    }
    else if (val == 5) {
      Serial.println("Backward");
      robot_back();
      robo = 1;
    }
    if (noStop != 1)
    {

    }
  }
  else
  {
    Serial.println("variable");
    res = -1;
  }

  if (res) {
    return httpd_resp_send_500(req);
  }

  httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
  return httpd_resp_send(req, NULL, 0);
}

static esp_err_t status_handler(httpd_req_t *req) {
  static char json_response[1024];

  sensor_t * s = esp_camera_sensor_get();
  char * p = json_response;
  *p++ = '{';

  p += sprintf(p, "\"framesize\":%u,", s->status.framesize);
  p += sprintf(p, "\"quality\":%u,", s->status.quality);
  *p++ = '}';
  *p++ = 0;
  httpd_resp_set_type(req, "application/json");
  httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
  return httpd_resp_send(req, json_response, strlen(json_response));
}

static const char PROGMEM INDEX_HTML[] = R"rawliteral(
<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width,initial-scale=1">
        <title>ESP32 CAM Robot</title>
        <style>
    body{font-family:Arial,Helvetica,sans-serif;background:#181818;color:#efefef;font-size:16px}h2{font-size:18px}section.main{display:flex}#menu,section.main{flex-direction:column}#menu{display:none;flex-wrap:nowrap;min-width:340px;background:#363636;padding:8px;border-radius:4px;margin-top:-10px;margin-right:10px}#content{display:flex;flex-wrap:wrap;align-items:stretch}figure{padding:0;margin:0;-webkit-margin-before:0;margin-block-start:0;-webkit-margin-after:0;margin-block-end:0;-webkit-margin-start:0;margin-inline-start:0;-webkit-margin-end:0;margin-inline-end:0}figure img{display:block;width:100%;height:auto;border-radius:4px;margin-top:8px}@media (min-width:800px) and (orientation:landscape){#content{display:flex;flex-wrap:nowrap;align-items:stretch}figure img{display:block;max-width:100%;max-height:calc(100vh - 40px);width:auto;height:auto}figure{padding:0;margin:0;-webkit-margin-before:0;margin-block-start:0;-webkit-margin-after:0;margin-block-end:0;-webkit-margin-start:0;margin-inline-start:0;-webkit-margin-end:0;margin-inline-end:0}}section #buttons{display:flex;flex-wrap:nowrap;justify-content:space-between}#nav-toggle{cursor:pointer;display:block}#nav-toggle-cb{outline:0;opacity:0;width:0;height:0}#nav-toggle-cb:checked+#menu{display:flex}.input-group{display:flex;flex-wrap:nowrap;line-height:22px;margin:5px 0}.input-group>label{display:inline-block;padding-right:10px;min-width:47%}.input-group input,.input-group select{flex-grow:1}.range-max,.range-min{display:inline-block;padding:0 5px}button{display:block;margin:5px;padding:5px 12px;border:0;line-height:28px;cursor:pointer;color:#fff;background:#035806;border-radius:5px;font-size:16px;outline:0;width:100px}.button2{background-color:#008cba;width:100px}.button3{background-color:#f44336;width:100px}.button4{background-color:#e7e7e7;color:#000;width:120px}.button5{background-color:#555;width:100px}.button6{visibility:hidden;width:100px}button:hover{background:#ff494d}button:active{background:#f21c21}button.disabled{cursor:default;background:#a0a0a0}input[type=range]{-webkit-appearance:none;width:100%;height:22px;background:#363636;cursor:pointer;margin:0}input[type=range]:focus{outline:0}input[type=range]::-webkit-slider-runnable-track{width:100%;height:2px;cursor:pointer;background:#efefef;border-radius:0;border:0 solid #efefef}input[type=range]::-webkit-slider-thumb{border:1px solid rgba(0,0,30,0);height:22px;width:22px;border-radius:50px;background:#ff3034;cursor:pointer;-webkit-appearance:none;margin-top:-11.5px}input[type=range]:focus::-webkit-slider-runnable-track{background:#efefef}input[type=range]::-moz-range-track{width:100%;height:2px;cursor:pointer;background:#efefef;border-radius:0;border:0 solid #efefef}input[type=range]::-moz-range-thumb{border:1px solid rgba(0,0,30,0);height:22px;width:22px;border-radius:50px;background:#ff3034;cursor:pointer}input[type=range]::-ms-track{width:100%;height:2px;cursor:pointer;background:0 0;border-color:transparent;color:transparent}input[type=range]::-ms-fill-lower{background:#efefef;border:0 solid #efefef;border-radius:0}input[type=range]::-ms-fill-upper{background:#efefef;border:0 solid #efefef;border-radius:0}input[type=range]::-ms-thumb{border:1px solid rgba(0,0,30,0);height:22px;width:22px;border-radius:50px;background:#ff3034;cursor:pointer;height:2px}input[type=range]:focus::-ms-fill-lower{background:#efefef}input[type=range]:focus::-ms-fill-upper{background:#363636}.switch{display:block;position:relative;line-height:22px;font-size:16px;height:22px}.switch input{outline:0;opacity:0;width:0;height:0}.slider{width:50px;height:22px;border-radius:22px;cursor:pointer;background-color:grey}.slider,.slider:before{display:inline-block;transition:.4s}.slider:before{position:relative;content:"";border-radius:50%;height:16px;width:16px;left:4px;top:3px;background-color:#fff}input:checked+.slider{background-color:#ff3034}input:checked+.slider:before{-webkit-transform:translateX(26px);transform:translateX(26px)}select{border:1px solid #363636;font-size:14px;height:22px;outline:0;border-radius:5px}.image-container{position:absolute;top:50px;left:50%;margin-right:-50%;transform:translate(-50%,-50%);min-width:160px}.control-container{position:relative;top:400px;left:50%;margin-right:-50%;transform:translate(-50%,-50%)}.slider-container{position:relative;top:750px;right:36%;margin-left:-50%;transform:translate(-50%,-50%)}.close{position:absolute;right:5px;top:5px;background:#ff3034;width:16px;height:16px;border-radius:100px;color:#fff;text-align:center;line-height:18px;cursor:pointer}.hidden{display:none}.rotate90{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-o-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}
</style>

    </head>
    <body>
    
    <br>
    <br>
        <section class="main">
        <figure>
      <div id="stream-container" class="image-container">
        <div class="close" id="close-stream">×</div>
        <img id="stream" src="" class="rotate90">
      </div>
    </figure>
    <br>
    <br>
    
            
            <section id="buttons">

                <div id="controls" class="control-container">
                  <table>
                  <tr><td align="center"><button class="button button6" id="get-still">Image</button></td><td align="center"><button id="toggle-stream">Start</button></td><td></td></tr>
                  <tr><td></td><td align="center"><button class="button button2" id="forward" onclick="fetch(document.location.origin+'/control?var=car&val=1');">FORWARD</button></td><td></td></tr>
                  <tr><td align="center"><button class="button button2" id="turnleft" onclick="fetch(document.location.origin+'/control?var=car&val=2');">LEFT</button></td><td align="center"></td><td align="center"><button class="button button2" id="turnright" onclick="fetch(document.location.origin+'/control?var=car&val=4');">RIGHT</button></td></tr>
                  <tr><td></td><td align="center"><button class="button button2" id="backward" onclick="fetch(document.location.origin+'/control?var=car&val=5');">REVERSE</button></td><td></td></tr>
                  <tr><td></td><td align="center"><button class="button button4" id="flash" onclick="fetch(document.location.origin+'/control?var=flash&val=1');">LIGHT ON</button></td><td></td></tr>
                  <tr><td></td><td align="center"><button class="button button4" id="flashoff" onclick="fetch(document.location.origin+'/control?var=flashoff&val=0');">LIGHT OFF</button></td><td></td></tr>
                  </table>
                </div>
               <br>
                <div id="sliders" class="slider-container">
                  <table>
                  <tr><td>Motor Speed:</td><td align="center" colspan="2"><input type="range" id="speed" min="0" max="255" value="200" onchange="try{fetch(document.location.origin+'/control?var=speed&val='+this.value);}catch(e){}"></td></tr>
                  <tr><td>Vid Quality:</td><td align="center" colspan="2"><input type="range" id="quality" min="10" max="63" value="10" onchange="try{fetch(document.location.origin+'/control?var=quality&val='+this.value);}catch(e){}"></td></tr>
                  <tr><td>Vid Size:</td><td align="center" colspan="2"><input type="range" id="framesize" min="0" max="6" value="5" onchange="try{fetch(document.location.origin+'/control?var=framesize&val='+this.value);}catch(e){}"></td></tr>
                  
                  
                  </table>
                </div>
            </section>         
        </section>
        <script>
          document.addEventListener('DOMContentLoaded',function(){function b(B){let C;switch(B.type){case'checkbox':C=B.checked?1:0;break;case'range':case'select-one':C=B.value;break;case'button':case'submit':C='1';break;default:return;}const D=`${c}/control?var=${B.id}&val=${C}`;fetch(D).then(E=>{console.log(`request to ${D} finished, status: ${E.status}`)})}var c=document.location.origin;const e=B=>{B.classList.add('hidden')},f=B=>{B.classList.remove('hidden')},g=B=>{B.classList.add('disabled'),B.disabled=!0},h=B=>{B.classList.remove('disabled'),B.disabled=!1},i=(B,C,D)=>{D=!(null!=D)||D;let E;'checkbox'===B.type?(E=B.checked,C=!!C,B.checked=C):(E=B.value,B.value=C),D&&E!==C?b(B):!D&&('aec'===B.id?C?e(v):f(v):'agc'===B.id?C?(f(t),e(s)):(e(t),f(s)):'awb_gain'===B.id?C?f(x):e(x):'face_recognize'===B.id&&(C?h(n):g(n)))};document.querySelectorAll('.close').forEach(B=>{B.onclick=()=>{e(B.parentNode)}}),fetch(`${c}/status`).then(function(B){return B.json()}).then(function(B){document.querySelectorAll('.default-action').forEach(C=>{i(C,B[C.id],!1)})});const j=document.getElementById('stream'),k=document.getElementById('stream-container'),l=document.getElementById('get-still'),m=document.getElementById('toggle-stream'),n=document.getElementById('face_enroll'),o=document.getElementById('close-stream'),p=()=>{window.stop(),m.innerHTML='Start'},q=()=>{j.src=`${c+':81'}/stream`,f(k),m.innerHTML='Stop'};l.onclick=()=>{p(),j.src=`${c}/capture?_cb=${Date.now()}`,f(k)},o.onclick=()=>{p(),e(k)},m.onclick=()=>{const B='Stop'===m.innerHTML;B?p():q()},n.onclick=()=>{b(n)},document.querySelectorAll('.default-action').forEach(B=>{B.onchange=()=>b(B)});const r=document.getElementById('agc'),s=document.getElementById('agc_gain-group'),t=document.getElementById('gainceiling-group');r.onchange=()=>{b(r),r.checked?(f(t),e(s)):(e(t),f(s))};const u=document.getElementById('aec'),v=document.getElementById('aec_value-group');u.onchange=()=>{b(u),u.checked?e(v):f(v)};const w=document.getElementById('awb_gain'),x=document.getElementById('wb_mode-group');w.onchange=()=>{b(w),w.checked?f(x):e(x)};const y=document.getElementById('face_detect'),z=document.getElementById('face_recognize'),A=document.getElementById('framesize');A.onchange=()=>{b(A),5<A.value&&(i(y,!1),i(z,!1))},y.onchange=()=>{return 5<A.value?(alert('Please select CIF or lower resolution before enabling this feature!'),void i(y,!1)):void(b(y),!y.checked&&(g(n),i(z,!1)))},z.onchange=()=>{return 5<A.value?(alert('Please select CIF or lower resolution before enabling this feature!'),void i(z,!1)):void(b(z),z.checked?(h(n),i(y,!0)):g(n))}});
        </script>
    </body>
</html>
)rawliteral";

static esp_err_t index_handler(httpd_req_t *req){
    httpd_resp_set_type(req, "text/html");
    return httpd_resp_send(req, (const char *)INDEX_HTML, strlen(INDEX_HTML));
}

void startCameraServer()
{
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();

    httpd_uri_t index_uri = {
        .uri       = "/",
        .method    = HTTP_GET,
        .handler   = index_handler,
        .user_ctx  = NULL
    };

    httpd_uri_t status_uri = {
        .uri       = "/status",
        .method    = HTTP_GET,
        .handler   = status_handler,
        .user_ctx  = NULL
    };

    httpd_uri_t cmd_uri = {
        .uri       = "/control",
        .method    = HTTP_GET,
        .handler   = cmd_handler,
        .user_ctx  = NULL
    };

    httpd_uri_t capture_uri = {
        .uri       = "/capture",
        .method    = HTTP_GET,
        .handler   = capture_handler,
        .user_ctx  = NULL
    };

   httpd_uri_t stream_uri = {
        .uri       = "/stream",
        .method    = HTTP_GET,
        .handler   = stream_handler,
        .user_ctx  = NULL
    };
    
    Serial.printf("Starting web server on port: '%d'\n", config.server_port);
    if (httpd_start(&camera_httpd, &config) == ESP_OK) {
        httpd_register_uri_handler(camera_httpd, &index_uri);
        httpd_register_uri_handler(camera_httpd, &cmd_uri);
        httpd_register_uri_handler(camera_httpd, &status_uri);
        httpd_register_uri_handler(camera_httpd, &capture_uri);
    }

    config.server_port += 1;
    config.ctrl_port += 1;
    Serial.printf("Starting stream server on port: '%d'\n", config.server_port);
    if (httpd_start(&stream_httpd, &config) == ESP_OK) {
        httpd_register_uri_handler(stream_httpd, &stream_uri);
    }
}

unsigned int get_speed(unsigned int sp)
{
  return map(sp, 0, 100, 0, 255);
}

void robot_setup()
{
    // Pins for Motor Controller
    pinMode(LEFT_M0,OUTPUT);
    pinMode(LEFT_M1,OUTPUT);
    pinMode(RIGHT_M0,OUTPUT);
    pinMode(RIGHT_M1,OUTPUT);
    
    // Make sure we are stopped
    robot_stop();

    // Motor uses PWM Channel 8
    ledcAttachPin(MTR_PWM, 8);
    ledcSetup(8, 2000, 8);      
    ledcWrite(8, 130);
    
}

// Motor Control Functions

void update_speed()
{  
    ledcWrite(motorPWMChannnel, get_speed(motor_speed));
    
}

void robot_stop()
{
  digitalWrite(LEFT_M0,LOW);
  digitalWrite(LEFT_M1,LOW);
  digitalWrite(RIGHT_M0,LOW);
  digitalWrite(RIGHT_M1,LOW);
}

void robot_fwd()
{
  digitalWrite(LEFT_M0,HIGH);
  digitalWrite(LEFT_M1,LOW);
  digitalWrite(RIGHT_M0,HIGH);
  digitalWrite(RIGHT_M1,LOW);
  move_interval=250;
  previous_time = millis();  
}

void robot_back()
{
  digitalWrite(LEFT_M0,LOW);
  digitalWrite(LEFT_M1,HIGH);
  digitalWrite(RIGHT_M0,LOW);
  digitalWrite(RIGHT_M1,HIGH);
  move_interval=250;
   previous_time = millis();  
}

void robot_right()
{
  digitalWrite(LEFT_M0,LOW);
  digitalWrite(LEFT_M1,HIGH);
  digitalWrite(RIGHT_M0,HIGH);
  digitalWrite(RIGHT_M1,LOW);
  move_interval=100;
   previous_time = millis();
}

void robot_left()
{
  digitalWrite(LEFT_M0,HIGH);
  digitalWrite(LEFT_M1,LOW);
  digitalWrite(RIGHT_M0,LOW);
  digitalWrite(RIGHT_M1,HIGH);
  move_interval=100;
   previous_time = millis();
}



Этот файл отвечает за web-интерфейс, который будет отображаться при работе с машинкой.
Поэтому, если есть желание что-то изменить под себя, править необходимо этот файл.
В нашем случае мы поправим размер выводимой картинки и заменим управляющие сигналы левый — на правый и наоборот (после подключения двигателей они оказались перепутаны местами).
Для этого в esp32cam-robot.ino необходимо заменить строки:
if(psramFound()){
config.frame_size = FRAMESIZE_QVGA;
config.jpeg_quality = 10;
config.fb_count = 2;
} else {
config.frame_size = FRAMESIZE_QVGA;
config.jpeg_quality = 12;
config.fb_count = 1;
}

на
if(psramFound()){
config.frame_size = FRAMESIZE_UXGA;
config.jpeg_quality = 10;
config.fb_count = 2;
} else {
config.frame_size = FRAMESIZE_SVGA;
config.jpeg_quality = 12;
config.fb_count = 1;
}

*не совсем понятно почему в базовом проекте установлено такое низкое разрешение. Картинка почти не различима.
Доступные режимы
FRAMESIZE_UXGA (1600 x 1200)
FRAMESIZE_QVGA (320 x 240)
FRAMESIZE_CIF (352 x 288)
FRAMESIZE_VGA (640 x 480)
FRAMESIZE_SVGA (800 x 600)
FRAMESIZE_XGA (1024 x 768)
FRAMESIZE_SXGA (1280 x 1024)

Замену направлений поворотов влево на вправо и наоборот мы проведем путем простой замены определения pinов в app_httpd.cpp:
#define LEFT_M0     15
#define LEFT_M1     14
#define RIGHT_M0    13
#define RIGHT_M1    2

на
#define LEFT_M0     13
#define LEFT_M1     2
#define RIGHT_M0    15
#define RIGHT_M1    14

Заливаем прошивку


Перед заливкой прошивки подключим esp32-cam через ftdi переходник.

В оригинале предполагается, что прошивка модуля будет проводиться с использованием 3.3V логики. Это означает, что необходимо питать esp32 через ногу 3.3V. Однако в нашем случае это было неприменимо и модуль не «шился».

Поэтому использовалась следующая видоизмененная схема подключения:



Питание 5V подается на pin 5V вместо 3.3V на pin 3.3V!
*Необходимо помнить, что на ftdi переходнике также должно быть выставлено 5,5V, если это двух режимный ftdi-переходник.

Также перед заливкой скетча ставится перемычка (PGM на схеме).

Кроме того в arduino ide необходимо выставить настройки модуля —

Если скетч залился успешно, то в среде arduino ide можно наблюдать примерно следующую картину:



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

Собираем по схеме


Согласно базового проекта схема сборки следующая —

Здесь нечего добавить, кроме того, что необходимо учесть, что в схеме будет еще понижающий dc-dc, который будет идти с аккумуляторов:



Питание на esp32-cam лучше припаивать снаружи, а не на сами ноги платы, так как иначе в дальнейшем будет затруднено подключение для перепрошивки.

Если все собрано правильно, то после подачи питания, esp32 должен несколько раз мигнуть.
Далее модуль создаст точку доступа, к которой, как уже говорилось, можно подключаться, введя ip 192.168.4.1.

Угол и скорость можно изменять регулируя слайдеры прямо на странице управления.
На этом все.

Фото базового проекта.


Фото проекта после правки.


И немного видео.

Комментарии (31)


  1. tmg
    24.08.2021 10:26
    +1

    нужен еще модуль Qi charger с индикатором заряда в UI - чтобы ставить машинку на зарядку, когда сидя дома по даче поездишь и проверишь обстановку.

    и watchdog - чтобы перезапускал зависшую плату.


    1. sav13
      24.08.2021 10:48

      WDT у ESP32 нормальный, в отличие от ESP8266

      Да и не "зависает" если код без ошибок написан. FreeRTOS великая штука!


  1. predator86
    24.08.2021 14:16

    Какую максимальную скорость сетевого соединения может обеспечить ESP32?


  1. DesertFlow
    24.08.2021 14:58

    На ESP32, как и на ESP8266, прекрасно ставятся как Espruino, так и Micropython. Чтобы такие небольшие проекты писать на javascript или python, кому что привычнее. По быстродействию они примерно одинаковы. Espruino удобнее из-за встроенной IDE для заливки. Точное выдерживание PWM сигнала для ESC контроллеров для мощных двигателей или гарантированную обработку прерываний, например от датчиков холла, обе версии ESP не тянут по своим внутренним причинам, независимо от языка. Поэтому писать в ардуино среде на голом С для них нет особого смысла, это не поможет. И так как на этих микроконтроллерах обычно поднимается http или websocket сервер со встроенной html страничкой, то писать скрипт для ESP удобнее сразу в одном стеке, т.е. сразу на javascript (Espruino). Кстати, для радиоуправления даже нет необходимости использовать постоянное websocket соединение, отдельные http запросы обе ESP тянут с частотой в 20-30 мс, что сравнимо с классическими RC пультами. И для управления машинкой в html интерфейсе на телефоне удобно использовать слайдеры, а не кнопки.


    1. dcoder_mm
      24.08.2021 17:33

      Точное выдерживание PWM сигнала для ESC контроллеров для мощных двигателей или гарантированную обработку прерываний, например от датчиков холла, обе версии ESP не тянут по своим внутренним причинам, независимо от языка

      А можете рассказать по-подробнее?


      1. DesertFlow
        25.08.2021 05:54
        +4

        Это из-за того, что ESP32 и ESP8266 работают не так, как обычные микроконтроллеры. В обычных разработчик чипа предоставляет полное аппаратное описание чипа, и пользовательская прошивка - это единственный код, который работает в микроконтроллере. Программа пользователя только записывает и считывает биты в регистры. А дальше микроконтроллер делает всю работу аппаратно. Это позволяет точно знать число тактов, сколько занимает каждая команда. И, например, задавать точные временные интервалы при генерации ШИМ сигнала. А также вовремя реагировать на прерывания. А у ESP часть архитектуры закрыта, а доступ к аппаратным функциям делается через софтверный API. То есть, параллельно с вашей прошивкой, внутри ESP крутится какой-то код от производителя чипа. Он жрет много ресурсов и приводит к непредсказуемым задержкам. Поэтому ESP не могут выдерживать точные временные интервалы. Это приводит к куче проблем с управлением электродвигателями и сервоприводами (даже с использованием внешних аппаратных драйверов). Моторы могут дергаться, врубаться неожиданно на полный газ и все такое. По этой же причине на ESP возникают проблемы у софтверных реализаций протоколов UART/I2C/OneWire и при обработке прерываний.

        В общем, для очень многих точных аппаратных задач esp НЕ МОГУТ заменить ардуино или stm. И это очень жаль, потому что ESP действительно классные и дешевые чипы. Было бы круто все делать чисто на них. Хотя благодаря большой частоте 80/160/240 МГц, эта проблема сглаживается, конечно. Если подключить обычную серву, то она на первый взгляд, даже заработает. Но может непредсказуемо дергаться и глючить. Аналогично с попыткой собрать на базе ESP энкодер на датчиках холла для определения скорости вращения быстрых моторов, с этим совсем беда. Оно то работает, то пропускает прерывания.

        Но для простых задач - вроде обработать нажатия кнопок и обеспечивать web интерфейс, ESP прекрасно подходят. Да и пауза между постоянно пересылаемыми GET запросами там на практике всего около 30 мс, что очень круто. Можно на базе ESP делать радиоуправление в почти реальном времени. Но надо быть готовым к тому, что в любой момент ESP может зависнуть на 300-1500 мс. При этом, если вы своим кодом задержите ESP то ли на 1 мс, то ли на 10 мс, то она молча упадет. Вот такая несправедливость =). Через websocket, который держит соединение постоянно открытым и имеет меньше накладных расходов, пауза между пакетами чуть меньше 20 мс (но сами библиотеки weboscket жрут довольно много памяти, если ее не хватает, проще делать на обычных GET запросах, разница невелика).

        P.S. В ESP32 двухядерный процессор, вроде можно один выделить чисто под свой код, на который внутренний код от производителя не будет влиять, но это сложно и требует нестандартных процедур при программировании.


        1. dcoder_mm
          25.08.2021 17:18

          Спасибо. Звучит очень печально, конечно.


          1. DesertFlow
            26.08.2021 00:49
            +1

            Я сейчас вижу, что как-то слишком многословно и непонятно описал. Если коротко, процессор ESP занят своей основной работой - поддержкой Wi-Fi. То что мы пишем для него программы - это внедряем свой дополнительный код к тому коду, который на нем уже работает. Причем обработка Wi-Fi для чипа остается основным приоритетом. Поэтому нельзя своим кодом задерживать непрерывно процессор дольше, чем на несколько миллисекунд. Иначе он по watchdog упадет. И обработка сами чипом Wi-FI вносит непредсказуемые задержки, так как у него более высокий приоритет. Это основное отличие ESP32 и ESP8266 от обычных микроконтроллеров, вроде чипов Atmel в Arduino.

            У обычных такие аппаратные штуки действительно аппаратные: например, для реализации UART есть буфер, куда ваша программа записывает строку, а дальше микроконтроллер эту строку отсылает аппаратно в течении некоторого времени, не задерживая вашу программу (она продолжает выполняться, как обычно). Но у ESP нет по-настоящему аппаратного Wi-FI. Они обрабатывают его софтверно самим же процессором, на котором крутится и ваша программа. При этом у ESP есть и типичные аппаратные решения, вроде аппаратного UART с его буфером. Таймеры там всякие, регистры и прочие характерные для обычных микроконтроллеров вещи. Что позволяет относиться к ESP и программировать их как обычные микроконтроллеры. Точнее, это и есть обычный микроконтроллер. У которого единственное предназначение - обработка Wi-Fi. Просто из-за избыточной производительности, мы можем внедрять в него свой код и получать доступ к некоторым аппаратным частям, например тому же аппаратному UART, пинам, таймерам, прерываниям и т.д. Но не ко всем. Часть с Wi-Fi разработчиком закрыта и поэтому для ESP невозможно написать полноценную прошивку, превращающую ее в обычный микроконтроллер с полным контролем над его аппаратными ресурсами. Впрочем, для большинства задач это и не нужно.

            Мда, коротко опять не получилось, простите). Я просто большой поклонник этих чипов, жаль только в нескольких нужных мне проектах с точным таймингом столкнулся с проблемами из-за этих ограничений. Но главным их достоинством (помимо дешевизны), имхо является возможность запускать на них javascript (через Espruino) и python (через Micropython). На этих языках программирование ESP превращается в сплошное удовольствие, по сравнению с Си. Так как проекты на ESP из-за поднятого HTTP сервера с html интерфейсом обычно получаются более сложные, чем типичные для ардуинок. Поэтому синтаксический сахар этих языков приходится очень кстати.


            1. dcoder_mm
              26.08.2021 17:13

              Спасибо за подробный ответ.
              А вот про условные сервомоторы и софтовый API: я верно понимаю что в ESP32 почему-то не используется аппаратный таймер с PWM выходом для этого?


              1. DesertFlow
                31.08.2021 13:05
                +1

                Использует. По крайней мере, Arduino Core и Espruino последних версий (про Micropython не знаю). Но это не очень помогало, т.к. у wifi все равно более высокий приоритет. Но вообще, может я слишком категорично заявлял насчет серво и PWM, возможно это была специфика моего проекта или конкретно моих модулей. Я встречал аналогичные проблемы по форумам, когда разбирался в чем дело. Но в интернете также полно статей, где никакие проблемы с серво у ESP не упоминаются. Наверно надо просто пробовать и все.


        1. usa_habro_user
          27.08.2021 02:48

          Он жрет много ресурсов и приводит к непредсказуемым задержкам. Поэтому ESP не могут выдерживать точные временные интервалы. Это приводит к куче проблем с управлением электродвигателями и сервоприводами (даже с использованием внешних аппаратных драйверов). Моторы могут дергаться, врубаться неожиданно на полный газ и все такое.

          Вот тут нельзя ли поподробнее (можно даже с примерами кода)? Я использую ESP32 уже много лет в разных проектах, но никогда с подобным поведением не сталкивался. Нет, не то, чтобы я вам не верил, но, вероятно, у вас какие-то уж больно специфические проекты, требующие микросекундного real time тайминга. Управлял различными сервами и степперами, все работало "искаропки" и "как часы". Собственно, ESP32 меня когда-то и покорил тем, что весьма непростые вещи, типа BLE, RFComm, web server, WiFi direct шли прямо с SDK, и все примеры работали именно так, как и должны, без "бубна".
          Честно говоря, я за много лет никогда еще не сталкивался с тем, чтобы что-то работало "не штатно", чтобы нужно было "мутить" с таймингом и "стучать в бубен".

          P.S. Я отнюдь не пытаюсь утверждать, что ESP32 под Arduino - это RT система, отнюдь нет. Просто спроецировал свой личный опыт на ваш.


          1. DesertFlow
            31.08.2021 13:43

            Возможно, вы правы, и дело было в конкретном проекте или не очень качественных модулях. В коде ничего необычного - два датчика холла в качестве двухстороннего энкодера, авиамодельный ESC от мультикоптера с быстрой реакцией (т.е. почти без внутренних фильтров) и стандартная библиотека Servo.h (хотя пытался написать и свою). Это была электролебедка, которая могла включаться на 200-300 мс с несколькими десятками оборотов двигателя через редуктор за это время. Поэтому нужно было точное управление, так как даже одиночные пропуски прерываний были нежелательны. Нюанс в том, что в цикле использовался человек, поэтому были повышенные требования к надежности, иначе это могло привести к травмам. И отсюда же было тщательное и долгое тестирование. К сожалению, на обоих ESP8266 и ESP32 на нескольких экземплярах, купленных у разных производителей, так и не удалось достичь надежной работы без сбоев. Либо раз в несколько секунд начинались пропуски прерываний от датчиков холла, либо появлялись случайные задержки в тайминге PWM сигнала более 0.1 мс, что в моем случае означало либо блокировку мотора во время работы, либо снятие с тормоза, когда он в режиме торможения. А на голом Arduino Nano ни разу ошибок не было. Все варианты конверторов логического уровня, разных библиотек, пинов и всего что только можно было придумать, разумеется были перепробованы. Поведение было одинаковым на всех трех Arduino Core, Espruino и Micropython, что и послужило последней точкой, что проблема аппаратная, раз не зависит от разных языков. Я находил по форумам аналогичные проблемы у людей, кто пытался работать на ESP с точным таймингом (например, с PWM управлять напрямую мосфетами). В итоге мы сдались и остались на Arduino Nano V3 (для прототипирования, я имею ввиду). Однако на более простых задачах, быстродействия ESP должно хватать. Поэтому ни в коем случае не говорю, что они принципиально не годятся для управления моторами или сервоприводами по радиоуправлению. Но с осторожностью... У нас не получилось.


            1. usa_habro_user
              31.08.2021 19:27

              Спасибо за пояснение, понятно. В подобном проекте, наверное, и я бы не рискнул использовать ESP32 - все-таки это не RT система, ну, и если нужна безопасность, то однозначно это не выбор.
              Вот по быстрому "слепить" домашнюю поделку для smart home - тут конкурентов ESP32 мало (по цене и интегрированности).

              Кстати, если не сложно, то поясните вашу мысль касательно "WiFi-ориентированности" ESP32? А если я не подключаю Wi-Fi библиотеку, неужели проприетарная часть прошивки все равно будет что-то пытаться делать?


              1. DesertFlow
                02.09.2021 07:49

                Вот насчет этого не знаю. Я сам думал, что wifi и tcp стек в них аппаратные, по аналогии как реализованы другие аппаратные протоколы, вроде uart/i2c. То есть, это отдельный аппаратный блок, общение с которым делается записью байтов в некоторый участок памяти или в регистры. Из которых он сам потом читает и делает свою работу независимо от центрального процессора. Но оказалось, что довольно значительные ресурсы основного процессора как раз заняты обработкой wifi. А наш пользовательский код — только пассажир на этом поезде).


                С отключенным wifi я не пробовал. В документации, кстати, пишут что работа в качестве точки доступа более нестабильна и потребляет больше ресурсов, чем в режиме станции (собственно, на esp8266 максимум подключенных клиентов по умолчанию только 4, что тоже говорит об ограниченности ресурсов). Кстати, если кому потребуется именно стабильная работа wifi блока на ESP, то я в итоге пришел к такой схеме: посылать HTTP запросы GET к ESP не по таймеру setInterval(), а последовательно. То есть, обязательно дождаться ответа (какой-нибудь OK со стороны микроконтроллера), а после делать новый GET запрос. Потому что, похоже, накладывающиеся запросы тоже ее сильно нагружают. И дополнительно использовать таймаут в запросах (для fetch() а javascript есть механизм через signal). Иначе пропавшие пакеты или неудачный запрос могут повесить связь на десятки секунд, вплоть до перезагрузки модуля, а со своим таймаутом у вас будет фиксированная задержка не более 500 мс или секунды, сколько поставите. С таким подходом радиоуправление ESP через wifi в реальном времени работает более менее нормально (я его сейчас использую для RC газонокосилки, получается вполне приемлемо, сравнимо с авиамодельными пультами).


                1. usa_habro_user
                  02.09.2021 08:52

                  Вашу мысль я понял; думаю, впрочем, что касательно встроенного обработчика Wi-Fi и т.п. вы, наверное, все-таки ошибаетесь - если не поднимать WiFi (равно, как и BT/BLE), и не пользоваться серверами, то никаких особых задержек в пользовательском коде не будет. Я не хочу с вами спорить (на этом сайте вообще высказывать свое мнение чревато - затыкают рот сходу!), но так следует из моего опыта, да и из чисто логических рассуждений. Можно, конечно, посчитать, сколько тактов уходит на прогон void loop(), и как действует на это WiFi, но есть куда более интересные занятия ;) Вот только что закончил возиться со степпером для автоматических жалюзи - все работает (тьфу-тьфу) и с http сервером, и со чтением IR, и даже с Arduino-вским MQTT клиентом (для "OK Google"), крутит чисто и гладко.

                  я его сейчас использую для RC газонокосилки

                  А вот тут можно поподробнее? Я давно этой темой интересуюсь, и слежу за новостями (но больше по роботокосилкам); к сожалению, очень многие жалуются на качество работы даже самых дорогих моделей - основные претензии к красоте, так как эти роботокосилки "елозят" по газону, как пылесосы Roomba.


                  1. DesertFlow
                    02.09.2021 11:34
                    +1

                    Да, если не поднимать wifi и http/websocket сервер, то наверно почти не будет отличаться от микроконтроллеров arduino (atmega) или stm32. Я так понял, именно wifi и его tcp стек много жрут, что аж аппаратный таймер и прерывания могут перебивать (но не всегда, видимо какое-то стечение обстоятельств, вроде слабого уровня сигнала на пределе дальности, и связанные с этим ошибки. Или большая нагрузка от частых запросов (у меня они были каждые 20-30 мс)). За информацию что нормально работает с шаговыми двигателями спасибо, буду иметь ввиду.

                    По самодельной RC газонокосилке пока особо нечего сказать, так как она не до конца сделана (может позже напишу статью) - обычная бензиновая, к которой приделаны колеса от б/у гироскутера с его родной, но перепрошитой платой . Управление моторами по UART, поэтому ESP32 хорошо вписалась, так как на ней можно параллельно запустить кучу своей логики и сложный HTML интерфейс на телефоне/планшете (что особенно важно на этапе отладки с обратной связью). Я пытаюсь заставить ее определять высоту травы с помощью нескольких ультразвуковых HC-SR04 и лазерных VL53l0X дальномеров и следовать по границе скошенной травы, как делают автоматизированные сельскохозяйственные комбайны. Идея в том, чтобы вручную по радиоуправлению проехать по контуру, а внутри она сама по спирали все скосит сама. Но пока нормально работает только ручное радиоуправление. В будущем хотелось бы прикрутить сегментацию скошенной/не скошенной травы и всех препятствий нейросетью, чтобы сделать полностью автоматической. Я даже обучил пару тестовых нейросеток - на первый взгляд, работает довольно неплохо. Нейросеть успешно различает скошенные и не скошенные участки. Но пока не удалось избавиться от вибраций камеры, это планы на следующий сезон. В этом смысле ESP32 как базовый контроллер очень хорош, потому что к нему одновременно по wifi может подключиться пульт ручного управления (телефон или вторая ESP32 с джойстиком) и внешний компьютер с нейросетью и автопилотом, работающий по камере. От первоначального варианта с Aruco маркерами и определения своих координат на карте относительно них пришлось отказаться, потому что в реальности они камерой телефона определяются всего с 4-5 м, и оказываются часто загорожены листвой/кустами/деревьями.


                    1. jabacrack
                      02.09.2021 14:52

                      А не пробовали посмотреть как логика обхода комнаты устроена у роботов пылесосов с лазерным дальнометром? У сяоми первого поколения прошивка была вскрыта и ее можно посмотреть. Или даже сам пылесос использовать как управляющий модуль, правда придется или поставить стены вокруг участка или проложить магнитную ленту и вынести датчик холла вперед косилки.


                      1. DesertFlow
                        03.09.2021 08:10

                        Да… Идей там можно придумать много) Мне, например, очень нравится идея с визуальной одометрей (ORB SLAM 2), с синхронизацией по редким ARUCO маркерам, стоящим по углам газона. Как это сделано в UcoSLAM. Так газонокосилка сможет ездить между кустами, а маркеры будут синхронизировать с глобальной картой и давать реальный масштаб.


                    1. usa_habro_user
                      02.09.2021 17:47

                      Понятно. Буду ждать статью (подписался на вас), и обязательно "проплюсую".
                      Я подумывал, для создания своей роботокосилки, о покупке самоходной (есть в продаже по сравнительно приемлемым ценам) и автоматизации управления через RPi: подключиться через релюшки, ориентацию на газоне (у меня довольно простая "конфигурация", прямоугольный backyard, соединенный сбоку от дома с front yard-ом (квадратным) прямоугольной полосой земли) проводить через камеру RPi + триангуляцию по хорошо распознаваемым маркерам - по моим расчётам, должно работать намного точнее, чем GPS - углы с хорошего степпера можно снимать с большой точностью. Есть, правда, неясности с поворотом, но и на этот счет есть идеи (прямо по поговорке, "зачем телеге пятое колесо" :D ).
                      Единственно, останавливают пока "жаба" (не $50, и даже не $100) и жена (она любит сама косить, меня не пускает, и даже не соглашается кого-то нанимать, хотя это не так уж и дорого).


                      1. DesertFlow
                        03.09.2021 08:03
                        +1

                        Мне переделка бензиновой обошлась в 2000 руб за б/у гироскутер и ещё два самоцентрирующихся колёсика спреди, кажется по 200 руб за штуку. И управление через ESP8266. Я просто на штатные оси косилки закрепил деревянный брусок 40х40 мм, а к нему прикрутил мотор-колеса родными зажимами, снятыми с корпуса гироскутера. Получилось очень просто и бюджетно. Я наверно напишу статью через пару недель, надо только наделать фотографий и причесать код.


                      1. usa_habro_user
                        04.09.2021 07:37

                        Я наверно напишу статью через пару недель, надо только наделать фотографий и причесать код.

                        Жду с нетерпением, "проплюсовал" "карму" (если-бы было можно, сделал бы несколько раз!), Вы меня реально заинтриговали! У меня есть бензиновая газонокосилка, валяющаяся в подвале - Вы прямо вдохнули в меня энтузиазм! :D
                        P.S. Засел сегодня переделывать валяющуюся "тележку"; буду пытаться оснащать распознаванием координат с камеры, ну, и т.д. и т.п. (не хочу врать, пока не достигнуты реальные результаты - но потом обязательно напишу).


  1. Sanriko
    24.08.2021 15:07

    Это выглядит очень круто, чтобы попробовать сделать самому и познакомиться с робототехникой. Закажу комплектующие и отпишу что из этого в итоге получится. Один нюанс только пока не понял, каким образом выставляются вольты на ftdi-преобразователе?


    1. zoldaten Автор
      24.08.2021 15:09

      Как правило, перемычкой на самой плате. Паять ничего не нужно, просто переставить.


  1. Gudd-Head
    24.08.2021 19:55

    "р", "р." и даже один раз "руб.", хотя уже давно есть "₽". Но всего в трёх местах число отделено от единицы измерения пробелом (


  1. usa_habro_user
    27.08.2021 02:34

    Вопрос "на засыпку" - а почему, собственно, машинка? Глянул - и в предыдущих публикациях у вас то "танчик", то "тележка". Понятно, что все с этого начинают (у самого валяются пара "машинок" и робот-шестиног), но, к сожалению, все поделки такого рода интересны лишь во время разработки, притом, как правило, лишь самому разработчику. Потом, даже детям становится неинтересно играть с "самодвижущейся" (по маркеру) машинкой, или даже с "машинкой", в которую "гениальный" (sic! :D) папа встроил построение карты местности, используя хитроумные алгоритмы обработки данных с камеры и других сенсоров. Ну, и, говоря в общем, просто воспроизведение DIY поделок, один к одному, занятие достаточно малоинтересное (по крайней мере, для меня).

    Я вот нашел лично для себя "нишу": стараюсь, по мере своих скромных возможностей, делать "полезный DIY". Вот сейчас "загорелся" сделать "умные жалюзи"; конечно, за $180-500 можно купить и готовые, но это, во-первых, не интересно, во-вторых, все-таки дорого ("жаба" мне сказала - "Вперед и с песней!", а, в-третьих, будет весьма полезно "по жизни" (меня уже давно раздражает свет, падающий из окна гостиной на телевизор, реально мешает смотреть днем).


    1. zoldaten Автор
      27.08.2021 09:46

      Тут несколько векторов:
      — кружки робототехники, в которых, как правило, либо arduino либо mindstorms. Да и бывают проекты, когда есть красивый корпус, но нет начинки.
      — роботы телеинспекции (топ решения слишком дороги). Был проект поездить по трубе, поискать трещины.
      — собственные нужды — контроль помещения, игрушка ребенку и т.п.
      Так что, полезность присутствует.
      По поводу воспроизведения проектов — это бесценный опыт, учитывая, что многие проекты просто не допилены, а некоторые имеют потенциал, который не сразу заметен.

      Относительно умных жалюзей — дело хорошее, однако в свой majordomo не стал их внедрять.


      1. usa_habro_user
        27.08.2021 18:04

        По ряду упомянутых возможных применений не соглашусь:

        • для "поездить по трубе" машинки не годятся от слова "совсем": для этого используются миниатюрные камеры на достаточно жестком кабеле; более-менее профессиональные версии стоят порядка $400 (на Amazon), но можно купить и "полупрофессиональную" камеру за $50-70, или даже дешевле.

        • "Контроль помещения" - гмм, веб-камеры справляются на "все сто", "игрушка ребенку" - ну, вам просто повезло, моим детям хватало самое больше на пару раз, после чего интерес терялся совершенно

        Касательно кружков ничего не скажу: у нас в Штатах с таким не густо. Те, что я видел (они рекламируют себя на ежегодной ярмарке "киберпанка" у нас в городе), используют разработку в виде "smart blocks", электронных и механических "кубиков", эдаких "черных ящиков", легко соединяющихся между собой. У детей получалось собирать что-то почти сразу, а вот с Arduino, боюсь, так не выйдет.

        По поводу воспроизведения проектов — это бесценный опыт, учитывая, что многие проекты просто не допилены, а некоторые имеют потенциал, который не сразу заметен.

        Вы тут противоречите сами себе: ведь вы-то сами делали все "с нуля", а не воспользовались одним из бесчисленных проектов на create.arduino.cc


        1. zoldaten Автор
          27.08.2021 20:49

          Возможно, вы правы. Но по поводу труб, я имел в виду те трубы, в которые может пролезть человек. Но почему-то не хочет… Хотя сложно было что-то предложить кроме хвоста из проводов, хотелось автономного решения.

          Контроль помещения тоже разный бывает. Был забавный случай, когда упавшие вещи из корзины на камере напоминали лежащего человека. И рассмотреть поближе не было возможности. А так да, камеры справляются.

          В моем понимании «сделать с нуля» — это спроектировать плату, выйти на kickstarter, придумать язык программирования. Шутка.


          1. usa_habro_user
            27.08.2021 21:23

            Касательно тех труб, в которые может пролезть человек: я когда-то работал на Shell, и видел автономный дивайс, используемый для подобного рода инспекций. Поверьте мне на слово, он ну очень далек от "ардуинных" самоделок, а по стоимости приближается к автомобилю премиум класса, и вовсе не от "жадности" производителя. Вообще, это очень непростая инженерная задача (говоря о реальном промышленном исполнении и применении).


  1. Affdey
    10.11.2021 21:06

    Хороший проект! я вижу широкие возможности.

    Скажите, а текущее изображение с камеры как-то сохраняется в ОЗУ модуля? по коду - нет. Это возможно?


    1. zoldaten Автор
      11.11.2021 09:06

      В ОЗУ не сохраняется.
      Однако ничто не мешает «навесить» micro sd