70歳の挑戦... ESP32-S3-CAMでウェブリモコンカーを制作　以前のモノをバージョンアップしました

【注意喚起　盗撮が簡単にできる時代です。被害にあわないよう気を付けましょう】
 <a href="https://github.com/Freenove/Freenove_ESP32_S3_WROOM_Board">ESP32-S3-CAMボード</a>を購入したので、以前制作した<a href="https://qiita.com/Mobu_Kyoto/items/e26e56612cfaa7912fe3">ロボットカー</a>をバージョンアップしました。HTMLの書き方は相変らず解っておりませんが、見様見真似で書き加えてみました。ポイントは次の４点です。

:::note
１．映像ストリーミングのオンオフ
２．カメラの仰角をサーボモーターで20度ずつ変化させる
３．サーボ角をスライダーで連続変化させる(2024/10/23機能追加)
４．前照灯（LED)を２段階点灯及び消灯
５．クラクション（ホーン）を鳴らす
:::
使用した物品です。

    １．Freenove ESP32-S3 WROOM board. Flash Size 8MB.（ArduinoIDEでは「ESP32 S3 Dev Module）
    ２．サーボでカメラの仰角を変えられるよう「OV2640-75mm」に交換(ケーブル長75mm 画角160°)
    ３．TOWER PRO製「Micro Servo Digital 9g SG90」（ESP32では動かないモノもあります）
    ４．モータードライバー TA7291P（使い慣れてるので）
    ５．前照灯用 白色LED、680Ωの抵抗
    ６．クラクション用 小型スピーカーΦ20mm 8W
    ６．タミヤのミニモーター低速ギアボックス(4速)　ギアレシオ 314.9:1 で使用
    ７．タミヤのユニバーサルプレート(210×160)・スリムタイヤ小など
    ８．どこでもキャスター（6kgまで）
    ９．単三電池×４電池ボックス

![robocar_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3189926/24cfb9e8-c961-3c33-5896-1e6c67a3c767.png)
![robocar_1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3189926/515bac66-7a3d-0bcc-66d7-fb87faa99941.jpeg)
![ESP32_S3_Cam_Tools.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3189926/34551f6b-326c-ffc0-5cbf-4cc77d56a145.png)
　カメラ関係はデフォルトですので、それ以外の配線を示します。
|ESP32 S3 Board|TA7291P 右|TA7291P 右|サーボ|前照灯|ホーン|
|--------------|----------|---------|----|---|---|
|GPIO 19|5|||||
|GPIO 20|6|||||
|GPIO 21||5||||
|GPIO 47||6||||
|GPIO 48|||信号Pin 黄|||
|GPIO 42|||||〇|
|GPIO 2||||+||
|5V|8(Batt 4.8V)|8(Batt 4.8V)|電源 赤|||
|3V3|7|7||||
|GND|1|1|G 濃茶|-(670Ω)ｰ|〇|
||2 Motor+|2 Motor+|||
||10 Motor-|10 Motor-|||

なお、モータードライバーTA7291Pの

    2ピンと10ピン間に「0.1uF」
    4ピンと8ピン間に「3kΩ」
を付けました。
　Arduino IDE 2.3.2 でのスケッチです。
```c:ESP32S3Cam_WebControl_Car.ino
#include "Arduino.h"
#include <WiFi.h>
#include "esp_camera.h"
#include "esp_http_server.h"
#include "esp_timer.h"
#include "img_converters.h"
#include "fb_gfx.h"
#include <ESP32Servo.h>

// Camera Pin Config for "ESP32 Wrover Module"
#define PWDN_GPIO_NUM -1
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 15
#define SIOD_GPIO_NUM 4
#define SIOC_GPIO_NUM 5
#define Y2_GPIO_NUM 11
#define Y3_GPIO_NUM 9
#define Y4_GPIO_NUM 8
#define Y5_GPIO_NUM 10
#define Y6_GPIO_NUM 12
#define Y7_GPIO_NUM 18
#define Y8_GPIO_NUM 17
#define Y9_GPIO_NUM 16
#define VSYNC_GPIO_NUM 6
#define HREF_GPIO_NUM 7
#define PCLK_GPIO_NUM 13

// Motor Pins
#define MOTOR_1_PIN_1 19
#define MOTOR_1_PIN_2 20
#define MOTOR_2_PIN_1 21
#define MOTOR_2_PIN_2 47

// Replace with your network credentials
const char* ssid = "Your_SSID";
const char* password = "Your_Password";

Servo myservo;  // create servo object to control a servo
const int servoPin = 48;
const int pos0 = 0;   // input min in HTML 
const int pose = 90; // input max in HTML
int pos;        // variable to store the servo position

// LED
const int LedPIN = 2;
// setting PWM properties
int bright = 8;
const int freq = 5000;
const int ledChannel = 2;
const int reso = 8;

// Horn
const int HornPIN = 42;
// setting PWM properties
const int loud = 516;
const int Hornfreq = 432;
const int ledChannelH = 3;
const int Hornreso = 10;

#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 camera_httpd = NULL;
httpd_handle_t stream_httpd = NULL;

static const char PROGMEM INDEX_HTML[] =
  R"rawliteral(
<html>
  <head>
    <title>ESP32-S3_CAM Car</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
      body { font-family: Arial; text-align: center; margin:0px auto; padding-top: 30px;}
      table { margin-left: auto; margin-right: auto; }
      td { padding: 8 px; }
      img {  width: auto ;
        max-width: 100% ;
        height: auto ;
      }
      .button1 {
        background-color: #bf94e4;
        border: none;
        color: black;
        padding: 10px 20px;
        text-align: center;
        text-decoration: none;
        display: inline-block;
        font-size: 18px;
        margin: 6px 3px;
        cursor: pointer;
        -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 {
        background-color: #2f4468;
        border: none;
        color: white;
        padding: 10px 20px;
        text-align: center;
        text-decoration: none;
        display: inline-block;
        font-size: 18px;
        margin: 6px 3px;
        cursor: pointer;
        -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);
      }
    </style>
  </head>
  <body>
    <h1>ESP32-S3-CAM Car</h1>
    <table>
      <tr>
        <td colspan="3" align="center">
          <img id="stream" src="" >
        </td>
      </tr>
      <tr>
        <td colspan="3" align="center">
          <button class="button1"
            id="toggle-stream">CAMERA START
          </button>
        </td>
      </tr>
      <tr>
        <td>
          <input type="range" id="camAngle" min="0" max="90" value="0" />
        </td>
        <td>
          <p class="log">Camera Angle = 0</p>
        </td>
      </tr>
      <tr>
        <td align="center">
          <button class="button1"
            onmousedown ="toggleCheckbox('cam-up');"
            ontouchstart="toggleCheckbox('cam-up');">Camera_UP
          </button>
        </td>
        <td align="center">
          <button class="button1"
            onmousedown ="toggleCheckbox('cam-dn');"
            ontouchstart="toggleCheckbox('cam-dn');">Camera_DOWN
          </button>
        </td>
      </tr>

      <tr>
        <td align="center">
          <button class="button2"
            onmousedown ="toggleCheckbox('led-up');"
            ontouchstart="toggleCheckbox('led-up');">Led_LIGHTER
          </button>
        </td>
        <td align="center">
          <button class="button2"
            onmousedown ="toggleCheckbox('led-dn');"
            ontouchstart="toggleCheckbox('led-dn');">Led_DARKER
          </button>
        </td>
        <td align="center">
          <button class="button2"
            onmousedown ="toggleCheckbox('horn');"
            onmouseup   ="toggleCheckbox('hstop');"
            ontouchstart="toggleCheckbox('horn');"
            ontouchend  ="toggleCheckbox('hstop');">Horn
          </button>
        </td>
      </tr>

      <tr>
        <td colspan="3" align="center">
          <button class="button2"
            onmousedown ="toggleCheckbox('forward');"
            onmouseup   ="toggleCheckbox('stop');"
            ontouchstart="toggleCheckbox('forward');"
            ontouchend  ="toggleCheckbox('stop');">Forward
          </button>
        </td>
      </tr>
      <tr>
        <td align="center">
          <button class="button2"
            onmousedown ="toggleCheckbox('left');"
            onmouseup   ="toggleCheckbox('stop');"
            ontouchstart="toggleCheckbox('left');"
            ontouchend  ="toggleCheckbox('stop');">Turn Left
          </button>
        </td>
        <td align="center">
          <button class="button2"
            onmousedown ="toggleCheckbox('stop');"
            ontouchstart="toggleCheckbox('stop');">Stop
          </button>
        </td>
        <td align="center">
          <button class="button2"
            onmousedown ="toggleCheckbox('right');"
            onmouseup   ="toggleCheckbox('stop');"
            ontouchstart="toggleCheckbox('right');"
            ontouchend  ="toggleCheckbox('stop');">Turn Right
          </button>
        </td>
      </tr>
      <tr>
        <td  colspan="3" align="center">
          <button class="button2"
            onmousedown ="toggleCheckbox('backward');"
            onmouseup   ="toggleCheckbox('stop');"
            ontouchstart="toggleCheckbox('backward');"
            ontouchend  ="toggleCheckbox('stop');">Backward
          </button>
        </td>
      </tr>
    </table>
    <script>
      const elementSV=document.querySelector('#camAngle');
      elementSV.addEventListener('input', handleChange);
      function handleChange(event){
        const value=event.target.value;
        document.querySelector('.log').innerHTML=`Camera Angle = ${value}`;

        var xhr = new XMLHttpRequest();
        xhr.open("GET", "/action?go=" + value, true);
        xhr.send();
      }

      function toggleCheckbox(x) {
        var xhr = new XMLHttpRequest();
        xhr.open("GET", "/action?go=" + x, true);
        xhr.send();
      }

      const streamButton = document.getElementById('toggle-stream')
      streamButton.addEventListener("click",
        function(){
          const view = document.getElementById('stream')
          var baseHost = document.location.origin
          var streamUrl = baseHost + ':81'

          const streamEnabled = streamButton.innerHTML === 'CAMERA STOP'
          if(streamEnabled) {
            window.stop();
            streamButton.innerHTML = 'CAMERA START'
          } else {
            view.src = `${streamUrl}/stream`
            streamButton.innerHTML = 'CAMERA STOP'
          }
        }
      );       
      
    </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));
}

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];

  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 = fb->buf;
        _jpg_buf_len = fb->len;
      }
    }
    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;
    }
    Serial.printf("MJPG: %uB\n", (uint32_t)(_jpg_buf_len));
  }
  return res;
}

static esp_err_t cmd_handler(httpd_req_t* req) {
  char* buf;
  size_t buf_len;
  char variable[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, "go", variable, sizeof(variable)) == ESP_OK)
      {
        // go on
      } 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;
  }

  sensor_t* s = esp_camera_sensor_get();
  int res = 0;

  if (!strcmp(variable, "forward")) {
    Serial.println("Forward");
    digitalWrite(MOTOR_1_PIN_1, 1);
    digitalWrite(MOTOR_1_PIN_2, 0);
    digitalWrite(MOTOR_2_PIN_1, 1);
    digitalWrite(MOTOR_2_PIN_2, 0);
  } else if (!strcmp(variable, "left")) {
    Serial.println("Left");
    digitalWrite(MOTOR_1_PIN_1, 1);
    digitalWrite(MOTOR_1_PIN_2, 0);
    digitalWrite(MOTOR_2_PIN_1, 0);
    digitalWrite(MOTOR_2_PIN_2, 1);
  } else if (!strcmp(variable, "right")) {
    Serial.println("Right");
    digitalWrite(MOTOR_1_PIN_1, 0);
    digitalWrite(MOTOR_1_PIN_2, 1);
    digitalWrite(MOTOR_2_PIN_1, 1);
    digitalWrite(MOTOR_2_PIN_2, 0);
  } else if (!strcmp(variable, "backward")) {
    Serial.println("Backward");
    digitalWrite(MOTOR_1_PIN_1, 0);
    digitalWrite(MOTOR_1_PIN_2, 1);
    digitalWrite(MOTOR_2_PIN_1, 0);
    digitalWrite(MOTOR_2_PIN_2, 1);
  } else if (!strcmp(variable, "stop")) {
    Serial.println("Stop");
    digitalWrite(MOTOR_1_PIN_1, 0);
    digitalWrite(MOTOR_1_PIN_2, 0);
    digitalWrite(MOTOR_2_PIN_1, 0);
    digitalWrite(MOTOR_2_PIN_2, 0);
  } else if (!strcmp(variable, "cam-up")) {
    pos = pos + 15;
    if (pos >= pose) pos = pose;
    Serial.printf("CAMERA=%d\n", pos);
    myservo.write(pos);
  } else if (!strcmp(variable, "cam-dn")) {
    pos = pos - 15;
    if (pos <= pos0) pos = pos0;
    Serial.printf("CAMERA=%d\n", pos);
    myservo.write(pos);
  } else if (!strcmp(variable, "led-up")) {
    bright += 128;
    if (bright >= 255) bright = 255;
    Serial.printf("LED=%d\n", bright);
    ledcWrite(LedPIN, bright);
  } else if (!strcmp(variable, "led-dn")) {
    bright -= 128;
    if (bright <= 0) bright = 0;
    Serial.printf("LED=%d\n", bright);
    ledcWrite(LedPIN, bright);
  } else if (!strcmp(variable, "horn")) {
    Serial.printf("HORN=%d\n", loud);
    ledcWrite(HornPIN, loud);
    delay(1000);
  } else if (!strcmp(variable, "hstop")) {
    ledcWrite(HornPIN, 0);
  } else if (atoi(variable)>0) {
    Serial.print("camAngle=");
    Serial.println(atoi(variable));
    myservo.write(atoi(variable));
  } else {
    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);
}

void startCameraServer() {
  httpd_config_t config = HTTPD_DEFAULT_CONFIG();
  config.server_port = 80;
  httpd_uri_t index_uri = {
    .uri = "/",
    .method = HTTP_GET,
    .handler = index_handler,
    .user_ctx = NULL
  };
  httpd_uri_t cmd_uri = {
    .uri = "/action",
    .method = HTTP_GET,
    .handler = cmd_handler,
    .user_ctx = NULL
  };
  httpd_uri_t stream_uri = {
    .uri = "/stream",
    .method = HTTP_GET,
    .handler = stream_handler,
    .user_ctx = NULL
  };

  if (httpd_start(&camera_httpd, &config) == ESP_OK) {
    httpd_register_uri_handler(camera_httpd, &index_uri);
    httpd_register_uri_handler(camera_httpd, &cmd_uri);
  }

  config.server_port += 1;
  config.ctrl_port += 1;
  if (httpd_start(&stream_httpd, &config) == ESP_OK) {
    httpd_register_uri_handler(stream_httpd, &stream_uri);
  }
}

void setup() {
  pinMode(MOTOR_1_PIN_1, OUTPUT);
  pinMode(MOTOR_1_PIN_2, OUTPUT);
  pinMode(MOTOR_2_PIN_1, OUTPUT);
  pinMode(MOTOR_2_PIN_2, OUTPUT);

  Serial.begin(115200);
  // camera init
  camera_init();
  // Wi-Fi connection
  connectWiFi(ssid, password);
  // Start streaming web server
  startCameraServer();
  // Camera servo
  myservo.setPeriodHertz(50);           // standard 50 hz servo
  // 個体に応じて調整
  myservo.attach(servoPin, 550, 2350);  // attaches the servo
  pos = pos0;
  myservo.write(pos);
  // configure LED PWM functionalitites
  ledcAttachChannel(LedPIN, freq, reso, ledChannel);
  ledcWrite(LedPIN, bright);
  // configure Horn PWM functionalitites
  ledcAttachChannel(HornPIN, Hornfreq, Hornreso, ledChannelH);
}

void camera_init() {
  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_sccb_sda = SIOD_GPIO_NUM;
  config.pin_sccb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 40000000;
  config.frame_size = FRAMESIZE_VGA;
  config.pixel_format = PIXFORMAT_JPEG;  // for streaming
  config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
  config.fb_location = CAMERA_FB_IN_DRAM;  // CAMERA_FB_IN_PSRAM;
  config.jpeg_quality = 12;
  config.fb_count = 1;

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

  sensor_t* s = esp_camera_sensor_get();
  // initial sensors are flipped vertically and colors are a bit saturated
  s->set_vflip(s, 1);       // flip it back
  s->set_brightness(s, 1);  // up the brightness just a bit
  s->set_saturation(s, 0);  // lower the saturation
  s->set_hmirror(s, 1);
}

void connectWiFi(const char* ssid, const char* pwd) {
  Serial.println("");
  Serial.println("Connecting to WiFi network: " + String(ssid));
  WiFi.disconnect(true, true);
  delay(1000);
  WiFi.begin(ssid, password);
  Serial.println("Waiting for WIFI connection ");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println(".");
  Serial.println("WiFi connected!");
  Serial.print("Camera Stream Ready! Go to: http://");
  Serial.println(WiFi.localIP());
  delay(1000);
}

void loop() {
}
```
　最後まで見ていただきありがとうございました。