ESP32 веб-сервер с MPU-6050: акселерометр и гироскоп (3D-представление объекта)

В этом проекте мы создадим веб-сервер на ESP32 для отображения показаний акселерометра и гироскопа MPU-6050. Мы также создадим 3D-представление ориентации датчика в веб-браузере. Показания обновляются автоматически с помощью Server-Sent Events, а 3D-представление реализовано с использованием JavaScript-библиотеки three.js. Плата ESP32 будет программироваться с использованием ядра Arduino.

Смотрите видеоурок

ESP32 веб-сервер MPU-6050 акселерометр гироскоп 3D-представление объекта Arduino

Для создания веб-сервера мы будем использовать библиотеку ESPAsyncWebServer, которая предоставляет простой способ создания асинхронного веб-сервера и обработки Server-Sent Events.

Чтобы узнать больше о Server-Sent Events, прочитайте: ESP32 Web Server using Server-Sent Events (Update Sensor Readings Automatically).

Обзор проекта

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

ESP MPU6050 веб-сервер Arduino обзор
  • Веб-сервер отображает значения гироскопа по осям X, Y и Z;

  • Значения гироскопа обновляются на веб-сервере каждые 10 миллисекунд;

  • Отображаются значения акселерометра (X, Y, Z). Эти значения обновляются каждые 200 миллисекунд;

  • Модуль датчика MPU-6050 также измеряет температуру, поэтому мы также отобразим значение температуры. Температура обновляется каждую секунду (1000 миллисекунд);

  • Все показания обновляются с помощью Server-Sent Events;

  • Есть 3D-представление датчика. Ориентация 3D-объекта изменяется в соответствии с ориентацией датчика. Текущее положение датчика рассчитывается на основе значений гироскопа;

  • 3D-объект создаётся с помощью JavaScript-библиотеки three.js;

  • Есть четыре кнопки для настройки положения 3D-объекта:

    • RESET POSITION: устанавливает угловое положение в ноль по всем осям;

    • X: устанавливает угловое положение X в ноль;

    • Y: устанавливает угловое положение Y в ноль;

    • Z: устанавливает угловое положение Z в ноль;

MPU-6050 акселерометр гироскоп веб-сервер ESP32 как это работает

Файловая система ESP32

Для организации нашего проекта и упрощения его понимания, мы создадим четыре разных файла для построения веб-сервера:

Структура папок проекта MPU6050 веб-сервер SPIFFS
  • Скетч Arduino, который управляет веб-сервером;

  • HTML-файл: для определения содержимого веб-страницы;

  • CSS-файл: для стилизации веб-страницы;

  • JavaScript-файл: для программирования поведения веб-страницы (обработка ответов веб-сервера, событий и создание 3D-объекта).

HTML, CSS и JavaScript файлы будут загружены в файловую систему ESP32 LittleFS. Для загрузки файлов в файловую систему ESP32 мы будем использовать плагин LittleFS Uploader. Убедитесь, что он установлен в вашей Arduino IDE:

Если вы используете PlatformIO + VS Code, прочитайте эту статью, чтобы узнать, как загружать файлы в файловую систему ESP32:

Гироскоп и акселерометр MPU-6050

MPU-6050 — это модуль с 3-осевым акселерометром и 3-осевым гироскопом.

Модуль MPU6050 акселерометр гироскоп датчик температуры

Гироскоп измеряет угловую скорость вращения (рад/с) — это изменение углового положения во времени по осям X, Y и Z (крен, тангаж и рыскание). Это позволяет нам определить ориентацию объекта.

Углы крена, тангажа и рыскания гироскоп

Акселерометр измеряет ускорение (скорость изменения скорости объекта). Он ощущает статические силы, такие как гравитация (9.8 м/с2), или динамические силы, такие как вибрации или движение. MPU-6050 измеряет ускорение по осям X, Y и Z. В идеале, в неподвижном объекте ускорение по оси Z равно гравитационной силе, а по осям X и Y оно должно быть равно нулю.

Используя значения акселерометра, можно рассчитать углы крена (roll) и тангажа (pitch) с помощью тригонометрии, но невозможно рассчитать угол рыскания (yaw).

Мы можем комбинировать информацию от обоих датчиков для получения точной информации об ориентации датчика.

Подробнее о датчике MPU-6050: ESP32 with MPU-6050 Accelerometer, Gyroscope and Temperature Sensor.

Схема подключения — ESP32 с MPU-6050

Для этого проекта вам понадобятся следующие компоненты:

Вы можете использовать приведённые выше ссылки или перейти непосредственно на MakerAdvisor.com/tools для поиска всех деталей для ваших проектов по лучшей цене!

Подключите ESP32 к датчику MPU-6050, как показано на следующей схеме: подключите вывод SCL к GPIO 22, а вывод SDA к GPIO 21.

MPU6050 акселерометр гироскоп схема подключения к ESP32

Подготовка Arduino IDE

Мы будем программировать плату ESP32 с помощью Arduino IDE. Убедитесь, что у вас установлено дополнение ESP32. Следуйте этому руководству:

Если вы предпочитаете использовать VSCode + PlatformIO, следуйте этому руководству:

Установка библиотек

Существуют различные способы получения показаний от датчика. В этом руководстве мы будем использовать библиотеку Adafruit MPU6050. Для использования этой библиотеки вам также необходимо установить библиотеку Adafruit Unified Sensor и библиотеку Adafruit Bus IO.

Для создания веб-сервера мы будем использовать библиотеки ESPAsyncWebServer и AsyncTCP. В этом примере мы будем отправлять показания датчика в браузер в формате JSON. Для упрощения работы с JSON-переменными мы будем использовать библиотеку Arduino_JSON от Arduino.

Вот список библиотек, которые нужно установить:

Откройте вашу Arduino IDE и перейдите в Sketch > Include Library > Manage Libraries. Должен открыться Library Manager. Найдите названия библиотек и установите их.

Плагин загрузки файловой системы

Для выполнения этого руководства у вас должен быть установлен плагин ESP32 Filesystem Uploader в вашей Arduino IDE. Если нет, следуйте этому руководству для его установки:

Если вы используете VS Code + PlatformIO, следуйте этому руководству, чтобы узнать, как загружать файлы в файловую систему ESP32:

Организация файлов

Для создания веб-сервера вам нужны четыре разных файла. Скетч Arduino, HTML-файл, CSS-файл и JavaScript-файл. HTML, CSS и JavaScript файлы должны быть сохранены в папке data внутри папки скетча Arduino, как показано ниже:

Организация файлов проекта ESP HTML CSS JavaScript Arduino

Вы можете скачать все файлы проекта:

Создание HTML-файла

Создайте файл index.html со следующим содержимым или скачайте все файлы проекта.

<!--
  Rui Santos
  Complete project details at https://RandomNerdTutorials.com/esp32-mpu-6050-web-server/

  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-->

<!DOCTYPE HTML><html>
<head>
  <title>ESP Web Server</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" href="data:,">
  <link rel="stylesheet" type="text/css" href="style.css">
  <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/107/three.min.js"></script>
</head>
<body>
  <div class="topnav">
    <h1><i class="far fa-compass"></i> MPU6050 <i class="far fa-compass"></i></h1>
  </div>
  <div class="content">
    <div class="cards">
      <div class="card">
        <p class="card-title">GYROSCOPE</p>
        <p><span class="reading">X: <span id="gyroX"></span> rad</span></p>
        <p><span class="reading">Y: <span id="gyroY"></span> rad</span></p>
        <p><span class="reading">Z: <span id="gyroZ"></span> rad</span></p>
      </div>
      <div class="card">
        <p class="card-title">ACCELEROMETER</p>
        <p><span class="reading">X: <span id="accX"></span> ms<sup>2</sup></span></p>
        <p><span class="reading">Y: <span id="accY"></span> ms<sup>2</sup></span></p>
        <p><span class="reading">Z: <span id="accZ"></span> ms<sup>2</sup></span></p>
      </div>
      <div class="card">
        <p class="card-title">TEMPERATURE</p>
        <p><span class="reading"><span id="temp"></span> &deg;C</span></p>
        <p class="card-title">3D ANIMATION</p>
        <button id="reset" onclick="resetPosition(this)">RESET POSITION</button>
        <button id="resetX" onclick="resetPosition(this)">X</button>
        <button id="resetY" onclick="resetPosition(this)">Y</button>
        <button id="resetZ" onclick="resetPosition(this)">Z</button>
      </div>
    </div>
    <div class="cube-content">
      <div id="3Dcube"></div>
    </div>
  </div>
<script src="script.js"></script>
</body>
</html>

Посмотреть исходный код

Body

Теги <body> и </body> отмечают начало и конец тела. Всё, что находится внутри этих тегов – это видимое содержимое страницы.

На веб-странице есть верхняя панель с заголовком. Это заголовок первого уровня, и он размещён внутри тега <div> с именем класса topnav. Размещение ваших HTML-элементов между тегами <div> упрощает их стилизацию с помощью CSS.

<div class="topnav">
  <h1><i class="far fa-compass"></i> MPU6050 <i class="far fa-compass"></i></h1>
</div>

Весь остальной контент размещён внутри тега <div> с именем content.

<div class="content">

Мы используем CSS grid layout для отображения показаний в различных выровненных блоках (card). Каждый блок соответствует ячейке сетки. Ячейки сетки должны находиться внутри контейнера сетки, поэтому блоки нужно разместить внутри другого тега <div>. Этот новый тег имеет имя класса cards.

<div class="cards">

Чтобы узнать больше о CSS grid layout, мы рекомендуем эту статью: A Complete Guide to Grid. Вот карточка для показаний гироскопа:

<div class="card">
  <p class="card-title">GYROSCOPE</p>
  <p><span class="reading">X: <span id="gyroX"></span> rad/s</span></p>
  <p><span class="reading">Y: <span id="gyroY"></span> rad/s</span></p>
  <p><span class="reading">Z: <span id="gyroZ"></span> rad/s</span></p>
</div>

Карточка имеет заголовок с именем карточки:

<p class="card-title">GYROSCOPE</p>

И три абзаца для отображения значений гироскопа по осям X, Y и Z.

<p><span class="reading">X: <span id="gyroX"></span> rad/s</span></p>
<p><span class="reading">Y: <span id="gyroY"></span> rad/s</span></p>
<p><span class="reading">Z: <span id="gyroZ"></span> rad/s</span></p>

В каждом абзаце есть тег <span> с уникальным id. Это нужно для того, чтобы позже с помощью JavaScript вставить показания в нужное место. Вот используемые id:

  • gyroX для показания гироскопа по оси X;

  • gyroY для показания гироскопа по оси Y;

  • gyroZ для показания гироскопа по оси Z.

Карточка для отображения показаний акселерометра аналогична, но с другими уникальными id для каждого показания:

<div class="card">
  <p class="card-title">ACCELEROMETER</p>
  <p><span class="reading">X: <span id="accX"></span> ms<sup>2</sup></span></p>
  <p><span class="reading">Y: <span id="accY"></span> ms<sup>2</sup></span></p>
  <p><span class="reading">Z: <span id="accZ"></span> ms<sup>2</sup></span></p>
</div>

Вот id для показаний акселерометра:

  • accX для показания акселерометра по оси X;

  • accY для показания акселерометра по оси Y;

  • accZ для показания акселерометра по оси Z.

Наконец, следующие строки отображают карточку для температуры и кнопки сброса.

<div class="card">
  <p class="card-title">TEMPERATURE</p>
  <p><span class="reading"><span id="temp"></span> &deg;C</span></p>
  <p class="card-title">3D ANIMATION</p>
  <button id="reset" onclick="resetPosition(this)">RESET POSITION</button>
  <button id="resetX" onclick="resetPosition(this)">X</button>
  <button id="resetY" onclick="resetPosition(this)">Y</button>
   <button id="resetZ" onclick="resetPosition(this)">Z</button>
 </div>

Уникальный id для показания температуры – temp.

Затем есть четыре различные кнопки, которые при нажатии вызывают JavaScript-функцию resetPosition(). Эта функция будет отвечать за отправку запроса на ESP32, информирующего о том, что мы хотим сбросить положение, будь то по всем осям или по отдельной оси. Каждая кнопка имеет уникальный id, чтобы мы знали, какая кнопка была нажата:

  • reset: для сброса положения по всем осям;

  • resetX: для сброса положения по оси X;

  • resetY: для сброса положения по оси Y;

  • resetZ: для сброса положения по оси Z.

Нам нужно создать раздел для отображения 3D-представления.

<div class="cube-content">
  <div id="3Dcube"></div>
</div>

3D-объект будет отрисован в <div> с id 3Dcube.

Наконец, поскольку мы будем использовать внешний JavaScript-файл со всеми функциями для обработки HTML-элементов и создания 3D-анимации, нам нужно подключить этот файл (script.js) следующим образом:

<script src="script.js"></script>

Создание CSS-файла

Создайте файл style.css со следующим содержимым или скачайте все файлы проекта.

Этот файл отвечает за стилизацию веб-страницы.

/*
  Rui Santos
  Complete project details at https://RandomNerdTutorials.com/esp32-mpu-6050-web-server/

  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/

html {
  font-family: Arial;
  display: inline-block;
  text-align: center;
}
p {
  font-size: 1.2rem;
}
body {
  margin: 0;
}
.topnav {
  overflow: hidden;
  background-color: #003366;
  color: #FFD43B;
  font-size: 1rem;
}
.content {
  padding: 20px;
}
.card {
  background-color: white;
  box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5);
}
.card-title {
  color:#003366;
  font-weight: bold;
}
.cards {
  max-width: 800px;
  margin: 0 auto;
  display: grid; grid-gap: 2rem;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
.reading {
  font-size: 1.2rem;
}
.cube-content{
  width: 100%;
  background-color: white;
  height: 300px; margin: auto;
  padding-top:2%;
}
#reset{
  border: none;
  color: #FEFCFB;
  background-color: #003366;
  padding: 10px;
  text-align: center;
  display: inline-block;
  font-size: 14px; width: 150px;
  border-radius: 4px;
}
#resetX, #resetY, #resetZ{
  border: none;
  color: #FEFCFB;
  background-color: #003366;
  padding-top: 10px;
  padding-bottom: 10px;
  text-align: center;
  display: inline-block;
  font-size: 14px;
  width: 20px;
  border-radius: 4px;
}

Посмотреть исходный код

Мы не будем объяснять, как работает CSS для этого проекта, поскольку это не имеет отношения к цели данного проекта.

Создание JavaScript-файла

Создайте файл script.js со следующим содержимым или скачайте все файлы проекта.

/*
  Rui Santos
  Complete project details at https://RandomNerdTutorials.com/esp32-mpu-6050-web-server/

  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/

let scene, camera, rendered, cube;

function parentWidth(elem) {
  return elem.parentElement.clientWidth;
}

function parentHeight(elem) {
  return elem.parentElement.clientHeight;
}

function init3D(){
  scene = new THREE.Scene();
  scene.background = new THREE.Color(0xffffff);

  camera = new THREE.PerspectiveCamera(75, parentWidth(document.getElementById("3Dcube")) / parentHeight(document.getElementById("3Dcube")), 0.1, 1000);

  renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setSize(parentWidth(document.getElementById("3Dcube")), parentHeight(document.getElementById("3Dcube")));

  document.getElementById('3Dcube').appendChild(renderer.domElement);

  // Create a geometry
  const geometry = new THREE.BoxGeometry(5, 1, 4);

  // Materials of each face
  var cubeMaterials = [
    new THREE.MeshBasicMaterial({color:0x03045e}),
    new THREE.MeshBasicMaterial({color:0x023e8a}),
    new THREE.MeshBasicMaterial({color:0x0077b6}),
    new THREE.MeshBasicMaterial({color:0x03045e}),
    new THREE.MeshBasicMaterial({color:0x023e8a}),
    new THREE.MeshBasicMaterial({color:0x0077b6}),
  ];

  const material = new THREE.MeshFaceMaterial(cubeMaterials);

  cube = new THREE.Mesh(geometry, material);
  scene.add(cube);
  camera.position.z = 5;
  renderer.render(scene, camera);
}

// Resize the 3D object when the browser window changes size
function onWindowResize(){
  camera.aspect = parentWidth(document.getElementById("3Dcube")) / parentHeight(document.getElementById("3Dcube"));
  //camera.aspect = window.innerWidth /  window.innerHeight;
  camera.updateProjectionMatrix();
  //renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setSize(parentWidth(document.getElementById("3Dcube")), parentHeight(document.getElementById("3Dcube")));

}

window.addEventListener('resize', onWindowResize, false);

// Create the 3D representation
init3D();

// Create events for the sensor readings
if (!!window.EventSource) {
  var source = new EventSource('/events');

  source.addEventListener('open', function(e) {
    console.log("Events Connected");
  }, false);

  source.addEventListener('error', function(e) {
    if (e.target.readyState != EventSource.OPEN) {
      console.log("Events Disconnected");
    }
  }, false);

  source.addEventListener('gyro_readings', function(e) {
    //console.log("gyro_readings", e.data);
    var obj = JSON.parse(e.data);
    document.getElementById("gyroX").innerHTML = obj.gyroX;
    document.getElementById("gyroY").innerHTML = obj.gyroY;
    document.getElementById("gyroZ").innerHTML = obj.gyroZ;

    // Change cube rotation after receiving the readinds
    cube.rotation.x = obj.gyroY;
    cube.rotation.z = obj.gyroX;
    cube.rotation.y = obj.gyroZ;
    renderer.render(scene, camera);
  }, false);

  source.addEventListener('temperature_reading', function(e) {
    console.log("temperature_reading", e.data);
    document.getElementById("temp").innerHTML = e.data;
  }, false);

  source.addEventListener('accelerometer_readings', function(e) {
    console.log("accelerometer_readings", e.data);
    var obj = JSON.parse(e.data);
    document.getElementById("accX").innerHTML = obj.accX;
    document.getElementById("accY").innerHTML = obj.accY;
    document.getElementById("accZ").innerHTML = obj.accZ;
  }, false);
}

function resetPosition(element){
  var xhr = new XMLHttpRequest();
  xhr.open("GET", "/"+element.id, true);
  console.log(element.id);
  xhr.send();
}

Посмотреть исходный код

Создание 3D-объекта

Функция init3D() создаёт 3D-объект. Чтобы действительно иметь возможность что-то отобразить с помощью three.js, нам нужны три вещи: сцена, камера и рендерер, чтобы мы могли отрисовать сцену с помощью камеры.

function init3D(){
  scene = new THREE.Scene();
  scene.background = new THREE.Color(0xffffff);

  camera = new THREE.PerspectiveCamera(75, parentWidth(document.getElementById("3Dcube")) / parentHeight(document.getElementById("3Dcube")), 0.1, 1000);

  renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setSize(parentWidth(document.getElementById("3Dcube")), parentHeight(document.getElementById("3Dcube")));

  document.getElementById('3Dcube').appendChild(renderer.domElement);

Для создания 3D-объекта нам нужен BoxGeometry. В box geometry вы можете задать размеры вашего объекта. Мы создали объект с правильными пропорциями, чтобы он напоминал форму MPU-6050.

const geometry = new THREE.BoxGeometry(5, 1, 4);

Помимо геометрии, нам также нужен материал для раскраски объекта. Существуют различные способы раскраски объекта. Мы выбрали три разных цвета для граней.

// Materials of each face
var cubeMaterials = [
  new THREE.MeshBasicMaterial({color:0x03045e}),
  new THREE.MeshBasicMaterial({color:0x023e8a}),
  new THREE.MeshBasicMaterial({color:0x0077b6}),
  new THREE.MeshBasicMaterial({color:0x03045e}),
  new THREE.MeshBasicMaterial({color:0x023e8a}),
  new THREE.MeshBasicMaterial({color:0x0077b6}),
];

const material = new THREE.MeshFaceMaterial(cubeMaterials);

Наконец, создайте 3D-объект, добавьте его на сцену и настройте камеру.

cube = new THREE.Mesh(geometry, material);
scene.add(cube);
camera.position.z = 5;
renderer.render(scene, camera);

Мы рекомендуем ознакомиться с этим кратким руководством по three.js, чтобы лучше понять, как это работает: Getting Started with three.js – Creating a Scene.

Чтобы иметь возможность изменять размер объекта при изменении размера окна веб-браузера, нам нужно вызвать функцию onWindowResize() при возникновении события resize.

// Resize the 3D object when the browser window changes size
function onWindowResize(){
  camera.aspect = parentWidth(document.getElementById("3Dcube")) / parentHeight(document.getElementById("3Dcube"));
  //camera.aspect = window.innerWidth /  window.innerHeight;
  camera.updateProjectionMatrix();
  //renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setSize(parentWidth(document.getElementById("3Dcube")), parentHeight(document.getElementById("3Dcube")));
}

window.addEventListener('resize', onWindowResize, false);

Вызовите функцию init3D() для фактического создания 3D-представления.

init3D();

События (SSE)

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

В этом примере мы хотим разместить показания в соответствующих HTML-элементах и соответственно изменить ориентацию 3D-объекта.

Создайте новый объект EventSource и укажите URL страницы, отправляющей обновления. В нашем случае это /events.

if (!!window.EventSource) {
  var source = new EventSource('/events');

После создания экземпляра источника событий вы можете начать прослушивание сообщений от сервера с помощью addEventListener().

Это стандартные обработчики событий, как показано в документации AsyncWebServer.

source.addEventListener('open', function(e) {
  console.log("Events Connected");
}, false);

source.addEventListener('error', function(e) {
  if (e.target.readyState != EventSource.OPEN) {
    console.log("Events Disconnected");
  }
}, false);

Когда появляются новые показания гироскопа, ESP32 отправляет событие gyro_readings клиенту. Нам нужно добавить обработчик для этого конкретного события.

source.addEventListener('gyro_readings', function(e) {

Показания гироскопа представляют собой строку в формате JSON. Например:

{
  "gyroX" : "0.09",
  "gyroY" : "0.05",
  "gyroZ": "0.04"
}

В JavaScript есть встроенная функция для преобразования строки, записанной в формате JSON, в нативные JavaScript-объекты: JSON.parse().

var obj = JSON.parse(e.data);

Переменная obj содержит показания датчика в нативном JavaScript-формате. Затем мы можем получить доступ к показаниям следующим образом:

  • показание гироскопа X: obj.gyroX;

  • показание гироскопа Y: obj.gyroY;

  • показание гироскопа Z: obj.gyroZ;

Следующие строки помещают полученные данные в соответствующие HTML-элементы на веб-странице.

document.getElementById("gyroX").innerHTML = obj.gyroX;
document.getElementById("gyroY").innerHTML = obj.gyroY;
document.getElementById("gyroZ").innerHTML = obj.gyroZ;

Наконец, нам нужно изменить вращение куба в соответствии с полученными показаниями, следующим образом:

cube.rotation.x = obj.gyroY;
cube.rotation.z = obj.gyroX;
cube.rotation.y = obj.gyroZ;
renderer.render(scene, camera);

Примечание: в нашем случае оси переключены, как показано ранее (rotation X –> gyroY, rotation Z –> gyroX, rotation Y –> gyroZ). Вам может потребоваться изменить это в зависимости от ориентации вашего датчика.

Для событий accelerometer_readings и temperature мы просто отображаем данные на HTML-странице.

source.addEventListener('temperature_reading', function(e) {
  console.log("temperature_reading", e.data);
  document.getElementById("temp").innerHTML = e.data;
}, false);

source.addEventListener('accelerometer_readings', function(e) {
  console.log("accelerometer_readings", e.data);
  var obj = JSON.parse(e.data);
  document.getElementById("accX").innerHTML = obj.accX;
  document.getElementById("accY").innerHTML = obj.accY;
  document.getElementById("accZ").innerHTML = obj.accZ;
}, false);

Наконец, нам нужно создать функцию resetPosition(). Эта функция будет вызываться кнопками сброса.

function resetPosition(element){
  var xhr = new XMLHttpRequest();
  xhr.open("GET", "/"+element.id, true);
  console.log(element.id);
  xhr.send();
}

Эта функция просто отправляет HTTP-запрос на сервер по различному URL в зависимости от нажатой кнопки (element.id).

xhr.open("GET", "/"+element.id, true);
  • Кнопка RESET POSITION –> запрос: /reset

  • Кнопка X –> запрос: /resetX

  • Кнопка Y –> запрос: /resetY

  • Кнопка Z –> запрос: /resetZ

Скетч Arduino

Наконец, давайте настроим сервер (ESP32). Скопируйте следующий код в Arduino IDE или скачайте все файлы проекта.

/*********
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete project details at https://RandomNerdTutorials.com/esp32-mpu-6050-web-server/
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*********/
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
#include <Arduino_JSON.h>
#include "LittleFS.h"

// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

// Create AsyncWebServer object on port 80
AsyncWebServer server(80);

// Create an Event Source on /events
AsyncEventSource events("/events");

// Json Variable to Hold Sensor Readings
JSONVar readings;

// Timer variables
unsigned long lastTime = 0;
unsigned long lastTimeTemperature = 0;
unsigned long lastTimeAcc = 0;
unsigned long gyroDelay = 10;
unsigned long temperatureDelay = 1000;
unsigned long accelerometerDelay = 200;

// Create a sensor object
Adafruit_MPU6050 mpu;

sensors_event_t a, g, temp;

float gyroX, gyroY, gyroZ;
float accX, accY, accZ;
float temperature;

//Gyroscope sensor deviation
float gyroXerror = 0.07;
float gyroYerror = 0.03;
float gyroZerror = 0.01;

// Init MPU6050
void initMPU(){
  if (!mpu.begin()) {
    Serial.println("Failed to find MPU6050 chip");
    while (1) {
      delay(10);
    }
  }
  Serial.println("MPU6050 Found!");
}

void initLittleFS() {
  if (!LittleFS.begin()) {
    Serial.println("An error has occurred while mounting LittleFS");
  }
  Serial.println("LittleFS mounted successfully");
}

// Initialize WiFi
void initWiFi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.println("");
  Serial.print("Connecting to WiFi...");
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(1000);
  }
  Serial.println("");
  Serial.println(WiFi.localIP());
}

String getGyroReadings(){
  mpu.getEvent(&a, &g, &temp);

  float gyroX_temp = g.gyro.x;
  if(abs(gyroX_temp) > gyroXerror)  {
    gyroX += gyroX_temp/50.00;
  }

  float gyroY_temp = g.gyro.y;
  if(abs(gyroY_temp) > gyroYerror) {
    gyroY += gyroY_temp/70.00;
  }

  float gyroZ_temp = g.gyro.z;
  if(abs(gyroZ_temp) > gyroZerror) {
    gyroZ += gyroZ_temp/90.00;
  }

  readings["gyroX"] = String(gyroX);
  readings["gyroY"] = String(gyroY);
  readings["gyroZ"] = String(gyroZ);

  String jsonString = JSON.stringify(readings);
  return jsonString;
}

String getAccReadings() {
  mpu.getEvent(&a, &g, &temp);
  // Get current acceleration values
  accX = a.acceleration.x;
  accY = a.acceleration.y;
  accZ = a.acceleration.z;
  readings["accX"] = String(accX);
  readings["accY"] = String(accY);
  readings["accZ"] = String(accZ);
  String accString = JSON.stringify (readings);
  return accString;
}

String getTemperature(){
  mpu.getEvent(&a, &g, &temp);
  temperature = temp.temperature;
  return String(temperature);
}

void setup() {
  Serial.begin(115200);
  initWiFi();
  initLittleFS();
  initMPU();

  // Handle Web Server
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(LittleFS, "/index.html", "text/html");
  });

  server.serveStatic("/", LittleFS, "/");

  server.on("/reset", HTTP_GET, [](AsyncWebServerRequest *request){
    gyroX=0;
    gyroY=0;
    gyroZ=0;
    request->send(200, "text/plain", "OK");
  });

  server.on("/resetX", HTTP_GET, [](AsyncWebServerRequest *request){
    gyroX=0;
    request->send(200, "text/plain", "OK");
  });

  server.on("/resetY", HTTP_GET, [](AsyncWebServerRequest *request){
    gyroY=0;
    request->send(200, "text/plain", "OK");
  });

  server.on("/resetZ", HTTP_GET, [](AsyncWebServerRequest *request){
    gyroZ=0;
    request->send(200, "text/plain", "OK");
  });

  // Handle Web Server Events
  events.onConnect([](AsyncEventSourceClient *client){
    if(client->lastId()){
      Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId());
    }
    // send event with message "hello!", id current millis
    // and set reconnect delay to 1 second
    client->send("hello!", NULL, millis(), 10000);
  });
  server.addHandler(&events);

  server.begin();
}

void loop() {
  if ((millis() - lastTime) > gyroDelay) {
    // Send Events to the Web Server with the Sensor Readings
    events.send(getGyroReadings().c_str(),"gyro_readings",millis());
    lastTime = millis();
  }
  if ((millis() - lastTimeAcc) > accelerometerDelay) {
    // Send Events to the Web Server with the Sensor Readings
    events.send(getAccReadings().c_str(),"accelerometer_readings",millis());
    lastTimeAcc = millis();
  }
  if ((millis() - lastTimeTemperature) > temperatureDelay) {
    // Send Events to the Web Server with the Sensor Readings
    events.send(getTemperature().c_str(),"temperature_reading",millis());
    lastTimeTemperature = millis();
  }
}

Посмотреть исходный код

Перед загрузкой кода убедитесь, что вы вставили свои сетевые учётные данные в следующие переменные:

// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

Как работает код

Продолжайте чтение, чтобы узнать, как работает код, или перейдите к следующему разделу.

Библиотеки

Сначала импортируйте все необходимые библиотеки для этого проекта:

#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
#include <Arduino_JSON.h>
#include "LittleFS.h"

Сетевые учётные данные

Вставьте свои сетевые учётные данные в следующие переменные:

// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

AsyncWebServer и AsyncEventSource

Создайте объект AsyncWebServer на порту 80.

AsyncWebServer server(80);

Следующая строка создаёт новый источник событий на /events.

AsyncEventSource events("/events");

Объявление переменных

Переменная readings – это JSON-переменная для хранения показаний датчика в формате JSON.

JSONVar readings;

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

// Timer variables
unsigned long lastTime = 0;
unsigned long lastTimeTemperature = 0;
unsigned long lastTimeAcc = 0;
unsigned long gyroDelay = 10;
unsigned long temperatureDelay = 1000;
unsigned long accelerometerDelay = 200;

MPU-6050

Создайте объект Adafruit_MPU6050 с именем mpu, создайте события для показаний датчика и переменные для хранения показаний.

// Create a sensor object
Adafruit_MPU6050 mpu;

sensors_event_t a, g, temp;

float gyroX, gyroY, gyroZ;
float accX, accY, accZ;
float temperature;

Настройте смещение гироскопа по всем осям.

//Gyroscope sensor deviation
float gyroXerror = 0.07;
float gyroYerror = 0.03;
float gyroZerror = 0.01;

Чтобы получить смещение датчика, перейдите в File > Examples > Adafruit MPU6050 > basic_readings. С датчиком в статическом положении проверьте значения гироскопа X, Y и Z. Затем добавьте эти значения в переменные gyroXerror, gyroYerror и gyroZerror.

Функция initMPU() инициализирует датчик MPU-6050.

// Init MPU6050
void initMPU(){
  if (!mpu.begin()) {
    Serial.println("Failed to find MPU6050 chip");
    while (1) {
      delay(10);
    }
  }
  Serial.println("MPU6050 Found!");
}

Инициализация LittleFS

Функция initLittleFS() инициализирует файловую систему ESP32, чтобы мы могли получить доступ к файлам, сохранённым в LittleFS (index.html, style.css и script.js).

void initLittleFS() {
  if (!LittleFS.begin()) {
    Serial.println("An error has occurred while mounting LittleFS");
  }
  Serial.println("LittleFSmounted successfully");
}

Инициализация Wi-Fi

Функция initWiFi() подключает ESP32 к вашей локальной сети.

// Initialize WiFi
void initWiFi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.print("Connecting to WiFi ..");
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print('.');
    delay(1000);
  }
  Serial.println(WiFi.localIP());
}

Получение показаний гироскопа

Функция getGyroReadings() получает новые показания гироскопа и возвращает текущую угловую ориентацию по осям X, Y и Z в виде JSON-строки.

Гироскоп возвращает текущую угловую скорость. Угловая скорость измеряется в рад/с. Чтобы определить текущее положение объекта, нам нужно умножить угловую скорость на прошедшее время (10 миллисекунд) и прибавить к предыдущему положению.

текущий угол (рад) = предыдущий угол (рад) + угловая скорость (рад/с) * время(с)

Переменная gyroX_temp временно хранит текущее значение гироскопа по оси X.

float gyroX_temp = g.gyro.x;

Чтобы предотвратить малые колебания датчика (см. Gyroscope Offset), мы сначала проверяем, превышают ли значения от датчика смещение.

if(abs(gyroX_temp) > gyroXerror) {

Если текущее значение больше значения смещения, мы считаем, что получили корректное показание. Таким образом, мы можем применить предыдущую формулу для получения текущего углового положения датчика (gyroX).

gyroX += gyroX_temp / 50.0;

Примечание: теоретически, мы должны умножить текущую угловую скорость на прошедшее время (10 миллисекунд = 0.01 секунды (gyroDelay)) – или разделить на 100. Однако после нескольких экспериментов мы обнаружили, что датчик лучше реагирует, если мы делим на 50.0. Ваш датчик может отличаться, и вам может потребоваться скорректировать значение.

Мы следуем аналогичной процедуре для получения значений Y и Z.

float gyroX_temp = g.gyro.x;
if(abs(gyroX_temp) > gyroXerror) {
  gyroX += gyroX_temp/50.00;
}

float gyroY_temp = g.gyro.y;
if(abs(gyroY_temp) > gyroYerror) {
  gyroY += gyroY_temp/70.00;
}

float gyroZ_temp = g.gyro.z;
if(abs(gyroZ_temp) > gyroZerror) {
  gyroZ += gyroZ_temp/90.00;
}

Наконец, мы объединяем показания в JSON-переменной (readings) и возвращаем JSON-строку (jsonString).

readings["gyroX"] = String(gyroX);
readings["gyroY"] = String(gyroY);
readings["gyroZ"] = String(gyroZ);

String jsonString = JSON.stringify(readings);
return jsonString;

Получение показаний акселерометра

Функция getAccReadings() возвращает показания акселерометра.

String getAccReadings(){
  mpu.getEvent(&a, &g, &temp);
  // Get current acceleration values
  accX = a.acceleration.x;
  accY = a.acceleration.y;
  accZ = a.acceleration.z;
  readings["accX"] = String(accX);
  readings["accY"] = String(accY);
  readings["accZ"] = String(accZ);
  String accString = JSON.stringify (readings);
  return accString;
}

Получение показаний температуры

Функция getTemperature() возвращает текущее показание температуры.

String getTemperature(){
  mpu.getEvent(&a, &g, &temp);
  temperature = temp.temperature;
  return String(temperature);
}

setup()

В setup() инициализируйте Serial Monitor, Wi-Fi, LittleFS и датчик MPU.

void setup() {
  Serial.begin(115200);
  initWiFi();
  initLittleFS();
  initMPU();

Обработка запросов

Когда ESP32 получает запрос по корневому URL, мы хотим отправить ответ с содержимым HTML-файла (index.html), который хранится в LittleFS.

server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
  request->send(LittleFS, "/index.html", "text/html");
});

Первый аргумент функции send() – это файловая система, в которой сохранены файлы, в данном случае LittleFS. Второй аргумент – путь к файлу. Третий аргумент – тип содержимого (HTML-текст).

В вашем HTML-файле вы ссылаетесь на файлы style.css и script.js. Поэтому, когда HTML-файл загружается в вашем браузере, он делает запрос на эти CSS и JavaScript файлы. Это статические файлы, сохранённые в том же каталоге (LittleFS). Поэтому мы можем просто добавить следующую строку для обслуживания статических файлов из каталога при запросе по корневому URL. Она автоматически обслуживает CSS и JavaScript файлы.

server.serveStatic("/", LittleFS, "/");

Нам также нужно обработать то, что происходит при нажатии кнопок сброса. Когда вы нажимаете кнопку RESET POSITION, ESP32 получает запрос по пути /reset. В этом случае мы просто устанавливаем переменные gyroX, gyroY и gyroZ в ноль для восстановления начального положения датчика.

server.on("/reset", HTTP_GET, [](AsyncWebServerRequest *request){
  gyroX=0;
  gyroY=0;
  gyroZ=0;
  request->send(200, "text/plain", "OK");
});

Мы отправляем ответ «OK» для указания на успешное выполнение запроса.

Мы следуем аналогичной процедуре для других запросов (кнопки X, Y и Z).

server.on("/resetX", HTTP_GET, [](AsyncWebServerRequest *request){
  gyroX=0;
  request->send(200, "text/plain", "OK");
});

server.on("/resetY", HTTP_GET, [](AsyncWebServerRequest *request){
  gyroY=0;
  request->send(200, "text/plain", "OK");
});

server.on("/resetZ", HTTP_GET, [](AsyncWebServerRequest *request){
  gyroZ=0;
  request->send(200, "text/plain", "OK");
});

Следующие строки настраивают источник событий на сервере.

// Handle Web Server Events
events.onConnect([](AsyncEventSourceClient *client){
  if(client->lastId()){
    Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId());
  }
  // send event with message "hello!", id current millis
  // and set reconnect delay to 1 second
  client->send("hello!", NULL, millis(), 10000);
});
server.addHandler(&events);

Наконец, запустите сервер.

server.begin();

loop() – Отправка событий

В loop() мы отправляем события клиенту с новыми показаниями датчика.

Следующие строки отправляют показания гироскопа в событии gyro_readings каждые 10 миллисекунд (gyroDelay).

if ((millis() - lastTime) > gyroDelay) {
  // Send Events to the Web Server with the Sensor Readings
  events.send(getGyroReadings().c_str(),"gyro_readings",millis());
  lastTime = millis();
}

Используйте метод send() объекта events и передайте в качестве аргумента содержимое, которое вы хотите отправить, и имя события. В данном случае мы хотим отправить JSON-строку, возвращаемую функцией getGyroReadings(). Метод send() принимает переменную типа char, поэтому нам нужно использовать метод c_str() для преобразования переменной. Имя события – gyro_readings.

Мы следуем аналогичной процедуре для показаний акселерометра, но используем другое событие (accelerometer_readings) и другое время задержки (accelerometerDelay):

if ((millis() - lastTimeAcc) > accelerometerDelay) {
  // Send Events to the Web Server with the Sensor Readings
  events.send(getAccReadings().c_str(),"accelerometer_readings",millis());
  lastTimeAcc = millis();
}

И наконец, для показаний температуры:

if ((millis() - lastTimeTemperature) > temperatureDelay) {
  // Send Events to the Web Server with the Sensor Readings
  events.send(getTemperature().c_str(),"temperature_reading",millis());
  lastTimeTemperature = millis();
}

Загрузка кода и файлов

После ввода ваших сетевых учётных данных сохраните код. Перейдите в Sketch > Show Sketch Folder и создайте папку data.

Arduino IDE открыть папку скетча для создания папки data

Внутри этой папки вы должны сохранить HTML, CSS и JavaScript файлы.

Затем загрузите код на вашу плату ESP32. Убедитесь, что выбраны правильная плата и COM-порт. Также убедитесь, что вы добавили свои сетевые учётные данные в код.

После загрузки кода вам нужно загрузить файлы. В Arduino IDE нажмите [Ctrl] + [Shift] + [P] в Windows или [Cmd] + [Shift] + [P] в MacOS, чтобы открыть палитру команд. Найдите команду Upload LittleFS to Pico/ESP8266/ESP32 и нажмите на неё.

Загрузка LittleFS на Pico ESP8266 ESP32 Arduino IDE

Если у вас нет этой опции, значит вы не установили плагин загрузчика файловой системы. Ознакомьтесь с этим руководством.

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

Когда всё успешно загружено, откройте Serial Monitor на скорости 115200 бод. Нажмите кнопку EN/RST на ESP32, и он должен вывести IP-адрес ESP32.

IP-адрес ESP32 выведен в Serial Monitor Arduino IDE

Демонстрация

Откройте ваш браузер и введите IP-адрес ESP32. Вы должны получить доступ к веб-странице, которая показывает показания датчика.

Перемещайте датчик и наблюдайте за изменением показаний, а также за 3D-объектом в браузере.

ESP32 ESP8266 NodeMCU веб-сервер демонстрация

Примечание: датчик немного дрейфует по оси X, несмотря на некоторые корректировки в коде. Многие наши читатели отмечали, что это нормально для такого типа MCU. Для уменьшения дрейфа некоторые читатели предложили использовать комплементарный фильтр или фильтр Калмана.

Заключение

MPU-6050 – это акселерометр, гироскоп и датчик температуры в одном модуле. В этом руководстве вы узнали, как создать веб-сервер на ESP32 для отображения показаний датчика MPU-6050. Мы использовали Server-Sent Events для отправки показаний клиенту.

Используя JavaScript-библиотеку three.js, мы создали 3D-представление датчика для отображения его углового положения на основе показаний гироскопа. Система не идеальна, но она даёт представление об ориентации датчика. Если кто-то более компетентный в этой теме может поделиться советами по калибровке датчика, мы будем очень благодарны.

Надеемся, это руководство было для вас полезным.

Узнайте больше о ESP32 из наших ресурсов:


Источник: :doc:`ESP32 Web Server with MPU-6050 Accelerometer and Gyroscope (3D object representation) <../esp32-mpu-6050-web-server/index>` – Random Nerd Tutorials, Rui Santos & Sara Santos