这次使用ESP32构建一个Web服务器,以显示MPU-6050加速度计和陀螺仪传感器的读数。我们还将在网页中创建传感器方向的3D模型展示。使用服务器发送的事件会自动更新读数,并使用名为three.js的JavaScript库来处理3D表示形式。这里的ESP32依然是使用Arduino架构核心进行编程。
本项目可以用ArduinoIDE,但是我这里使用的IDE是platformIO(以下称PIO).鉴于此前有些项目做的都是用这个IDE,也没怎么说明过,群里有些新人不太懂,所以这次讲得细化写的篇幅会比较长,因为我想顺便把PIO和ESP32入门尽量简要的一次过说说,毕竟平常没这个时间,以后估计很少玩Arduino了,树莓派感觉杀鸡用牛刀,没啥让我感兴趣的内容,让我感兴趣的也用不着它,所以现在只当做服务器用,以后想搞搞MicroPython.
事前准备
- 软件环境
- PlatformIO 配置好Arduino架构的ESP32板库
PlatformIO是开源的物联网开发生态系统。提供跨平台的代码构建器、集成开发环境(IDE),兼容 Arduino,ESP8266和mbed等。由于PIO相对Arduino来说支持的架构平台更多,即使是ESP系列他也可以支持官方的espidf核心架构,但是使用也比较复杂.在国内,比较少人在Arduino上研究,相反的是国外比较多,可能是国外喜欢创客类玩意的人比较多吧,毕竟西方国家的人动手能力和主动性都比较强。国内还是那些51 STM系列什么的才是主流,创客类在最近几年才慢慢流行即使只用Arduino架构,它的功能也比Arduino的官方IDE更强大和方便,只是对于刚上手的新人来说不太友好.
下面是PO官方的说明Wwiki,PIO可以在atom和vscode里安装使用,选自己喜欢的,电脑配置不太好的建议安装vscode.在项目动手前请耐心看完:
PIO的安装:https://docs.platformio.org/
PIO支持的平台架构:https://docs.platformio.org/en/latest/frameworks/arduino.html
PIO下关于ESP32板库的配置:https://docs.platformio.org/en/latest/platforms/espressif32.html#platform-espressif32
PIO下上传文件到ESP32的SPIFFS方法:https://docs.platformio.org/en/latest/platforms/espressif32.html#uploading-files-to-file-system-spiffs
- PlatformIO 配置好Arduino架构的ESP32板库
以上内容新手必须详细看清楚,实在不行你可以选择用ArduinoIDE,上传文件到ESP32的SPIFFS具体方法可以自行百度,这里就不另外说明了.另外,在你上传文件到ESP的SPIFFS之前,必须关闭串口监视器,否则ESP的串口会被占用而上传不上
-
程序库
可以先写在PIO项目目录里的platformio.ini
,在文件里加入:lib_deps = ome-no-dev/AsyncTCP @ 1.1.1 me-no-dev/ESP Async WebServer @ 1.2.3 adafruit/Adafruit Unified Sensor @ 1.1.4 adafruit/Adafruit MPU6050 @ 2.0.3 arduino-libraries/Arduino_JSON @ 0.1.0
加入以上内容后,在编译的时候PIO就会自动帮你下载相关库了,关于ini的配置说明请看PIO的官方wiki。要构建网络服务器,我们将使用ESPAsyncWebServer库,该库需要AsyncTCP库才能正常工作。
-
硬件准备 项目名称 数量 单位 备注 ESP32开发板 1 块 市面上版本太多,会有点引脚差异,这里用的是 DOIT ESP32 DEVKIT V1
MPU 6050 1 块 I2C接口,市场上版本也是很多,这里建议选广运电子的GY版本 杜邦线 若干 条 面包板 1 块 -
硬件接线
比较简单,也就不画图了,见下表:
ESP32 | GPIO 21 | GPIO 22 | 3.3 | GND |
---|---|---|---|---|
MPU 6050 | SDA | SCL | VCC | GND |
项目简述
- 项目是以Web服务器显示X,Y和Z轴的陀螺仪值;
- 陀螺仪值每10毫秒在Web服务器上更新一次;
- 显示加速度计值(X,Y,Z)每200毫秒更新一次;
- MPU-6050传感器模块还可以测量温度,因此我们还将显示温度值,温度每秒更新一次(1000毫秒),服务器使用发送的事件更新所有读数;
- 所有数据的更新都使用
Server-Sent Events
- 有传感器动态使用3D模型表示,模型的方向会相应于传感器方向发生变化而使用陀螺仪值计算传感器的当前位置;
- 3D模型对象是使用名为
three.js
的JavaScript库创建的。 - 有四个按钮可调整3D对象的位置
- 复位位置:将所有轴上的角度位置设置为零
- X:将X角位置设置为零;
- Y:将Y角位置设置为零;
- Z:将Z角位置设置为零;
什么是引入服务器发送事件(Server-Sent Events,SSE)
Server-Sent Events(一下称作SSE)即服务器发送事件(SSE),Server-Sent事件指的是网页自动获取来自服务器的更新,event指的是事件。简单说就是,允许客户端通过HTTP连接从服务器接收自动更新,一般的流程图如下:
同理,应用到本项目中即如下图:
客户端启动SSE连接,服务器使用事件源协议将更新发送到客户端。客户端将接收来自服务器的更新,但在初始握手后无法将任何数据发送到服务器。
这对于将更新的传感器读数发送到浏览器很有用。只要有新的读数,ESP32就会将其发送给客户端,并且可以自动更新网页,而无需其他请求。除了发送传感器读数外,你还可以发送对项目可能有用的任何数据,例如GPIO状态,检测到运动时发出的通知等。例如我做的这个项目:例如我做的这个项目:https://chrisxs.com/esp-weather-station.php,如果你不再浏览器内手动点击刷新的话,是没办法看见新的数据的,所以这里要使用SSE来自动更新.
ESP32文件系统
为了使我们的项目井井有条并易于理解,我们将创建四个不同的文件来构建网络服务器,目录结构如下:
- 处理Web服务器的Arduino代码;
- HTML文件:定义网页的内容;
- CSS文件:用于设置网页样式;
- JavaScript文件:对网页的行为进行编程(处理Web服务器的响应,事件和创建3D对象)。
本项目中,HTML,CSS和JavaScript文件需要被上传到ESP32 SPIFFS(文件系统)。而要让文件上传到ESP32文件系统,需要使用本文开头中的SPIFFS上传功能,详情见上文,PIO的官方文档有详细的说明,而ArduinoIDE则可以百度一下。
MPU-6050陀螺仪和加速度计
MPU-6050是带有3轴加速度计和3轴陀螺仪的模块,陀螺仪测量旋转速度(rad / s)–这是沿X,Y和Z轴(滚动,俯仰和偏航,即roll, pitch and yaw)的角位置随时间的变化,透过这样才能够确定对象的方向。
加速度计测量加速度(物体速度的变化率)。它可以感应静态焦点,例如重力(9.8m / s2)或动态力,例如振动或运动。 MPU-6050在X,Y和Z轴上测量加速度。 理想情况下,在静态物体中,Z轴上的加速度等于重力,并且X和Y轴上的加速度应为零。通过使用来自加速度计的值,可以使用三角函数来计算侧倾角和俯仰角,但无法计算偏航角。继而,合并来自两个传感器的信息,以获得有关传感器方向的准确信息。
网页端的准备工作
index.html
创建名为index.html
的文件,代码如下:
<!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
标签使您的网页具有响应能力。响应式网页设计将自动针对不同的屏幕尺寸和视口进行调整。
<meta name="viewport" content="width=device-width, initial-scale=1">
我们使用以下元标记,因为我们不会在此项目中为我们的网页提供网站图标。
<link rel="icon" href="data:,">
用于设置网页样式的样式位于名为style.css
文件的单独文件中。因此,我们必须按如下所示在HTML文件上引用CSS文件。
<link rel="stylesheet" type="text/css" href="style.css">
包括Font Awesome网站样式,以在网页中包含图标,如陀螺仪图标。
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
最后,我们需要包括three.js
库来创建传感器的3D演示模型。
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/107/three.min.js"></script>
Body
<body>
和</body>
标记标记主体的开始和结束,这些标记内的所有内容都是可见的页面内容。
Top Bar
网页顶部有一个标题栏。它是标题1,放置在类名称为topnav
的<div>
标记内。将HTML元素放在<div>
标签之间,以使用CSS设置样式。
`
MPU6050
`
内容网格
所有其他内容都放在名为content
的<div>
标记内:<div class="content">
我们使用CSS网格布局在不同的对齐框(cards
)上显示读数。 每个框对应一个网格单元。 网格单元需要位于网格容器内,因此框需要置于另一个<div>
标签内。 此新标签具有类名cards
。
<div class="cards">
这是陀螺仪读数的card:
<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>
该card的标题带有card的名称:
<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>
每个段落中都有一个带有唯一ID的<span>
标记。这是必需的,以便我们稍后可以使用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
表示加速度计的读数;
最后,以下几行显示温度和重置按钮:
<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>
温度读数的唯一标识为temp
,然后有四个不同的按钮,当单击它们时,稍后将调用resetPosition()
JavaScript函数。 该功能负责向ESP32发送请求,告知我们要重置位置,无论是在所有轴上还是在单个轴上。每个按钮都有一个唯一的ID,以便我们知道单击了哪个按钮:
reset
:复位所有轴的位置;resetX
:重置X轴上的位置;resetY
:重置Y轴上的位置;resetZ
:重置Z轴上的位置;
3D制图
创建一个部分来显示3D模型:
<div class="cube-content">
<div id="3Dcube"></div>
</div>
3D对象将使用3Dcube
这个id在<div>
上呈现。
引用JavaScript文件
<script src="script.js"></script>
创建CSS文件
创建名为style.css
的文件,内容如下
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
的文件,内容如下:
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);
const geometry = new THREE.BoxGeometry(5, 1, 4);
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);
}
function onWindowResize(){
camera.aspect = parentWidth(document.getElementById("3Dcube")) / parentHeight(document.getElementById("3Dcube"));
camera.updateProjectionMatrix();
renderer.setSize(parentWidth(document.getElementById("3Dcube")), parentHeight(document.getElementById("3Dcube")));
}
window.addEventListener('resize', onWindowResize, false);
init3D();
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) {
var obj = JSON.parse(e.data);
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);
}, 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
。在框几何中,可以设置对象的尺寸,我们以正确的比例创建了跟MPU-6050差不多形状的对象:
const geometry = new THREE.BoxGeometry(5, 1, 4);
除了几何图形外,我们还需要一种材质(材料)为对象着色,有多种方法可以为对象着色,这里为面孔选择了三种不同的颜色。
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
教程,以更好地理解它的工作原理:Three.js入门–创建场景。 在Web浏览器窗口更改大小时调整对象的大小,我们需要在当resize
被触发时,调用onWindowResize()
函数
function onWindowResize(){
camera.aspect = parentWidth(document.getElementById("3Dcube")) / parentHeight(document.getElementById("3Dcube"));
camera.updateProjectionMatrix();
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格式的传感器读数,然后,我们可以按以下方式访问读数:
- gyroscope X 读数:
obj.gyroX;
- gyroscope Y 读数:
obj.gyroY;
- gyroscope Z 读数:
obj.gyroZ;
以下几行将接收到的数据放入网页上相应的HTML元素中:
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();
}
此函数只是根据所按下的按钮(element.id)
,通过不同的URL向服务器发送HTTP请求。
xhr.open("GET", "/"+element.id, true);
- 位置复位 按钮 –> 请求:
/reset
- X 按钮 –> 请求:
/resetX
- Y 按钮 –> 请求:
/resetY
- Z 按钮 –> 请求:
/resetZ
Arduino 代码
#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 "SPIFFS.h"
// 填入你的WiFi信息
const char* ssid = "你的WiFi SSID";
const char* password = "你的WiFi密码";
// 设置 AsyncWebServer 的端口为 :80
AsyncWebServer server(80);
// 新建一个资源路径: /events
AsyncEventSource events("/events");
// JSON变量负责存储读数
JSONVar readings;
// 计时器变量
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
Adafruit_MPU6050 mpu;
sensors_event_t a, g, temp;
float gyroX, gyroY, gyroZ;
float accX, accY, accZ;
float temperature;
//陀螺仪传感器偏差
float gyroXerror = 0.07;
float gyroYerror = 0.03;
float gyroZerror = 0.01;
// 初始化 MPU6050
void initMPU(){
if (!mpu.begin()) {
Serial.println("Failed to find MPU6050 chip");
while (1) {
delay(10);
}
}
Serial.println("MPU6050 Found!");
}
void initSPIFFS() {
if (!SPIFFS.begin()) {
Serial.println("An error has occurred while mounting SPIFFS");
}
Serial.println("SPIFFS mounted successfully");
}
// 初始化 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);
// 读取当前加速度计的值
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();
initSPIFFS();
initMPU();
// 处理 Web Server
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(SPIFFS, "/index.html", "text/html");
});
server.serveStatic("/", SPIFFS, "/");
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");
});
// 处理 Web Server Events
events.onConnect([](AsyncEventSourceClient *client){
if(client->lastId()){
Serial.printf("Client reconnected! Last message ID that it got is: %un", client->lastId());
}
// 消息为“ hello!”的结束事件,id为当前毫秒
// 并将重新连接延迟设置为1秒
client->send("hello!", NULL, millis(), 10000);
});
server.addHandler(&events);
server.begin();
}
void loop() {
if ((millis() - lastTime) > gyroDelay) {
// 通过传感器读数将事件发送到Web服务器
events.send(getGyroReadings().c_str(),"gyro_readings",millis());
lastTime = millis();
}
if ((millis() - lastTimeAcc) > accelerometerDelay) {
// 通过传感器读数将事件发送到Web服务器
events.send(getAccReadings().c_str(),"accelerometer_readings",millis());
lastTimeAcc = millis();
}
if ((millis() - lastTimeTemperature) > temperatureDelay) {
// 通过传感器读数将事件发送到Web服务器
events.send(getTemperature().c_str(),"temperature_reading",millis());
lastTimeTemperature = millis();
}
}
代码解释
讲解一下代码
库
首先,导入该项目所需的所有库:
#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 "SPIFFS.h"
将网络凭据插入以下变量:
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
AsyncWebServer和AsyncEventSource
在端口80上创建一个AsyncWebServer对象:AsyncWebServer server(80);
在/ events上创建一个新的事件源(路径)。
AsyncEventSource events("/events");
声明变量
readings
变量是一个JSON变量,用于以JSON格式保存传感器读数:JSONVar readings;
在此项目中,每10毫秒发送一次陀螺仪读数,每200毫秒发送一次加速度计读数,以及每秒发送一次温度读数。因此,需要为每个读数创建辅助计时器变量.当然你也可以根据需要更改延迟时间。
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
创建一个名为mpu
的Adafruit_MPU6050
对象,为传感器读数创建事件,并创建变量以保存读数。
Adafruit_MPU6050 mpu;
sensors_event_t a, g, temp;
float gyroX, gyroY, gyroZ;
float accX, accY, accZ;
float temperature;
陀螺仪偏移
修正陀螺仪传感器在所有轴上的偏移:
float gyroXerror = 0.07;
float gyroYerror = 0.03;
float gyroZerror = 0.01;
要获取传感器偏移,可以在IDE中Adafruit MPU6050
库自带的示例:basic_readings
.在传感器处于静止位置的情况下,检查陀螺仪的X,Y和Z值。然后,将这些值添加到gyroXerror
,gyroYerror
和gyroZerror
变量中。
初始化MPU-6050
void initMPU(){
if (!mpu.begin()) {
Serial.println("Failed to find MPU6050 chip");
while (1) {
delay(10);
}
}
Serial.println("MPU6050 Found!");
}
初始化SPIFFS
initSPIFFS()`函数会初始化ESP32文件系统,以便我们能够访问保存在SPIFFS上的文件(`index.html`,`style.css`和`script.js`)。
`void initSPIFFS() {
if (!SPIFFS.begin()) {
Serial.println("An error has occurred while mounting SPIFFS");
}
Serial.println("SPIFFS mounted successfully");
}
初始化WiFi
initWiFi()
函数将ESP32连接到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()
函数获取新的陀螺仪读数,并以JSON字符串的形式返回X 、Y和Z轴上的当前角度方向.陀螺仪返回当前角速度,角速度以弧度/秒为单位,为了确定对象的当前位置,我们需要将角速度乘以经过的时间(10毫秒)并将其添加到先前的位置。
current angle (rad) = last angle (rad) + angular velocity (rad/s) * time(s)
gyroX_temp
变量临时保存当前的陀螺仪X值:
float gyroX_temp = g.gyro.x;
为了防止传感器产生小振荡(请参见陀螺仪偏移),我们首先检查传感器的值是否大于偏移: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变量(读数)中,并返回一个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);
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);
}
处理请求
当ESP32收到根URL的请求时,我们想发送一个响应,其中包含存储在SPIFFS中的HTML文件(index.html
)内:
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(SPIFFS, "/index.html", "text/html");
});
send()
函数的第一个参数是保存文件的文件系统,在这种情况下,该文件系统保存在SPIFFS中。第二个参数是文件所在的路径。第三个参数引用内容类型(HTML文本)。
在HTML文件中,引用了style.css
和script.js
文件。因此,当HTML文件加载到您的浏览器中时,它将请求这些CSS
和JavaScript
文件。这些是保存在同一目录(SPIFFS
)中的静态文件。我们可以简单地添加以下行,以在根URL请求时在目录中提供静态文件。它会自动提供CSS
和JavaScript
文件:
server.serveStatic("/", SPIFFS, "/");
我们还需要处理按下重置按钮时发生的情况。按下RESET POSITION按钮时,ESP32在/reset
路径上收到一个请求。发生这种情况时,我们只需将gyroX
,gyroY
和gyroZ
变量设置为0即可恢复传感器的初始位置:
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");
});
以下几行在服务器上设置事件源(路径):
events.onConnect([](AsyncEventSourceClient *client){
if(client->lastId()){
Serial.printf("Client reconnected! Last message ID that it got is: %un", 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()
中,我们会将事件和新的传感器读数发送给客户端.以下各行每隔10毫秒(gyroDelay
)发送有关gyro_readings
事件的陀螺仪读数:
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()
方法,并将要发送的内容和事件的名称作为参数传递。在这种情况下,我们要发送getGyroReadings()
函数返回的JSON字符串。 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();
}
上传代码
在代码上传后,开机连接WiFi,串口会输出显示ESP32所获取到的IP地址,在浏览器直接输入改地址即可看见页面.
程序效果
电脑端效果
手机端效果
人机联动效果
补充
(待续)