ESP32 веб-сервер с MPU-6050: акселерометр и гироскоп (3D-представление объекта)
В этом проекте мы создадим веб-сервер на ESP32 для отображения показаний акселерометра и гироскопа MPU-6050. Мы также создадим 3D-представление ориентации датчика в веб-браузере. Показания обновляются автоматически с помощью Server-Sent Events, а 3D-представление реализовано с использованием JavaScript-библиотеки three.js. Плата ESP32 будет программироваться с использованием ядра Arduino.
Смотрите видеоурок
Для создания веб-сервера мы будем использовать библиотеку ESPAsyncWebServer, которая предоставляет простой способ создания асинхронного веб-сервера и обработки Server-Sent Events.
Чтобы узнать больше о Server-Sent Events, прочитайте: ESP32 Web Server using Server-Sent Events (Update Sensor Readings Automatically).
Обзор проекта
Прежде чем перейти непосредственно к проекту, важно обозначить, что будет делать наш веб-сервер, чтобы было проще понять его работу.
Веб-сервер отображает значения гироскопа по осям 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 в ноль;
Файловая система ESP32
Для организации нашего проекта и упрощения его понимания, мы создадим четыре разных файла для построения веб-сервера:
Скетч 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-осевым гироскопом.
Гироскоп измеряет угловую скорость вращения (рад/с) — это изменение углового положения во времени по осям 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
Для этого проекта вам понадобятся следующие компоненты:
ESP32 (читайте Best ESP32 development boards)
Вы можете использовать приведённые выше ссылки или перейти непосредственно на MakerAdvisor.com/tools для поиска всех деталей для ваших проектов по лучшей цене!
Подключите ESP32 к датчику MPU-6050, как показано на следующей схеме: подключите вывод SCL к GPIO 22, а вывод SDA к GPIO 21.
Подготовка Arduino IDE
Мы будем программировать плату ESP32 с помощью Arduino IDE. Убедитесь, что у вас установлено дополнение ESP32. Следуйте этому руководству:
Если вы предпочитаете использовать VSCode + PlatformIO, следуйте этому руководству:
Установка библиотек
Существуют различные способы получения показаний от датчика. В этом руководстве мы будем использовать библиотеку Adafruit MPU6050. Для использования этой библиотеки вам также необходимо установить библиотеку Adafruit Unified Sensor и библиотеку Adafruit Bus IO.
Для создания веб-сервера мы будем использовать библиотеки ESPAsyncWebServer и AsyncTCP. В этом примере мы будем отправлять показания датчика в браузер в формате JSON. Для упрощения работы с JSON-переменными мы будем использовать библиотеку Arduino_JSON от Arduino.
Вот список библиотек, которые нужно установить:
ESPAsyncWebServer от ESP32Async
AsyncTCP от ESP32Async
Arduino_JSON library от 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, как показано ниже:
Вы можете скачать все файлы проекта:
Создание 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> °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>
Head
Теги <head> и </head> отмечают начало и конец заголовка. Заголовок – это место, куда вы вставляете данные о HTML-документе, которые не видны напрямую конечному пользователю, но добавляют функциональность веб-странице – это называется метаданные.
Следующая строка задаёт заголовок веб-страницы. В данном случае он установлен как ESP Web Server. Вы можете изменить его, если хотите. Заголовок – это именно то, что он означает: заголовок вашего документа, который отображается в строке заголовка вашего веб-браузера.
<title>ESP Web Server</title>
Следующий мета-тег делает вашу веб-страницу адаптивной. Адаптивный веб-дизайн автоматически подстраивается под различные размеры экранов и области просмотра.
<meta name="viewport" content="width=device-width, initial-scale=1">
Мы используем следующий мета-тег, потому что не будем обслуживать favicon для нашей веб-страницы в этом проекте.
<link rel="icon" href="data:,">
Стили для оформления веб-страницы находятся в отдельном файле style.css. Поэтому мы должны подключить CSS-файл в HTML-файле следующим образом.
<link rel="stylesheet" type="text/css" href="style.css">
Подключите стили веб-сайта Font Awesome для включения иконок на веб-страницу, таких как иконка гироскопа.
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
Наконец, нам нужно подключить библиотеку three.js для создания 3D-представления датчика.
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/107/three.min.js"></script>
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> °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.
Внутри этой папки вы должны сохранить HTML, CSS и JavaScript файлы.
Затем загрузите код на вашу плату ESP32. Убедитесь, что выбраны правильная плата и COM-порт. Также убедитесь, что вы добавили свои сетевые учётные данные в код.
После загрузки кода вам нужно загрузить файлы. В Arduino IDE нажмите [Ctrl] + [Shift] + [P] в Windows или [Cmd] + [Shift] + [P] в MacOS, чтобы открыть палитру команд. Найдите команду Upload LittleFS to Pico/ESP8266/ESP32 и нажмите на неё.
Если у вас нет этой опции, значит вы не установили плагин загрузчика файловой системы. Ознакомьтесь с этим руководством.
Важно: убедитесь, что Serial Monitor закрыт перед загрузкой в файловую систему. В противном случае загрузка не удастся.
Когда всё успешно загружено, откройте Serial Monitor на скорости 115200 бод. Нажмите кнопку EN/RST на ESP32, и он должен вывести IP-адрес ESP32.
Демонстрация
Откройте ваш браузер и введите IP-адрес ESP32. Вы должны получить доступ к веб-странице, которая показывает показания датчика.
Перемещайте датчик и наблюдайте за изменением показаний, а также за 3D-объектом в браузере.
Примечание: датчик немного дрейфует по оси 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