基于ESP32的浏览器数据采集系统实现
1. 系统概述
本系统借助ESP32微控制器,通过浏览器利用WebSockets技术对数字和模拟信号进行控制与测量。其功能包括控制数字输出引脚DO18,设置其值或按可选择的重复率自动切换;设置引脚25上的DAC为指定值,或在最小和最大电压之间反复斜坡变化以产生可选择速率的简单锯齿波信号;以高达每秒200个样本的可选择速率读取输入引脚DI21以及引脚32和33上的两个ADC通道的电压。该系统具备逻辑分析仪和示波器的功能,尽管速度较慢。
2. 用户界面
用户界面在浏览器窗口中运行,具体布局如下:
| 界面元素 | 功能 |
| ---- | ---- |
| 顶部按钮 | 用于启动和停止数据采集、选择采样时间间隔以及清除显示 |
| 文本字段颜色 | 显示输入引脚DI21的状态,红色表示关闭,绿色表示开启 |
| 复选框 | 用于打开或关闭输出引脚DO18 |
| 选择菜单(右侧) | 以可选择的速率切换DO18 |
| 滑块(标记为DAC25) | 设置DAC的输出电压 |
| 选择菜单(DAC25右侧) | 选择不同速率的锯齿波信号 |
| 条形图(标记为ADC32和ADC33) | 显示两个ADC的输入电压 |
| 图形窗口 | 显示相应的轨迹,ADC32为红色,ADC33为蓝色 |
| 状态行 | 第一行显示浏览器生成的信息,第二行显示从ESP32接收的信息 |
3. 代码实现
3.1 初始化部分
// ESP32 DAQ, V. Ziemann, 221018
const char* ssid = "messnetz";
const char* password = "zxcvZXCV";
#include <WiFi.h>
#include <WebServer.h>
#include <WebSocketsServer.h>
#include <ArduinoJson.h>
#include <SPIFFS.h>
WebServer server2(80); // port 80
WebSocketsServer webSocket = WebSocketsServer(81); // port 81
#include <Ticker.h>
Ticker SampleSlow,Ramp25Ticker,Pulse18Ticker;
volatile uint8_t websock_num=0,info_available=0,output_ready=0;
int sample_period=100,samples[3];
uint8_t dac25val=0,do18=0;
char info_buffer[80];
char out[300]; DynamicJsonDocument doc(300);
此部分代码主要完成以下工作:
- 定义WiFi连接的SSID和密码。
- 包含必要的库,如WiFi、WebServer、WebSocketsServer、ArduinoJson和SPIFFS。
- 创建WebServer和WebSocketsServer实例,分别监听端口80和81。
- 定义Ticker对象,用于生成重复动作。
- 定义一些变量,用于存储采样周期、采样值、DAC值等。
3.2 采样和控制函数
void sampleslow_action() {
samples[0]=analogRead(32)/8;
samples[1]=analogRead(33)/8;
samples[2]=digitalRead(21);
output_ready=1;
}
void ramp25_action() {
dac25val++;
dacWrite(25,dac25val);
}
void pulse18_action() {
do18=!do18;
digitalWrite(18,do18);
}
-
sampleslow_action():读取输入引脚的值,并将其存储在samples数组中,同时设置output_ready标志以表示有新数据可用。 -
ramp25_action():递增8位变量dac25val,并将其写入DAC25。 -
pulse18_action():切换DO18的状态。
3.3 通信处理函数
void handle_notfound() {
server2.send(404,"text/plain","not found, use http://ip-address/");
}
void sendMSG(char *nam, const char *msg) {
(void) sprintf(info_buffer,"{\"%s\":\"%s\"}",nam,msg);
info_available=1;
}
void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) {
Serial.printf("webSocketEvent(%d, %d, ...)\r\n", num, type);
websock_num=num;
switch(type) {
case WStype_DISCONNECTED:
Serial.printf("[%u] Disconnected!\r\n", num);
break;
case WStype_CONNECTED:
{
IPAddress ip = webSocket.remoteIP(num);
Serial.printf("[%u] Connected from %d.%d.%d.%d url: %s\r\n", num, ip[0], ip[1], ip[2], ip[3], payload);
}
sendMSG("INFO","ESP32: Successfully connected");
break;
case WStype_TEXT:
{
Serial.printf("[%u] get Text: %s\r\n", num, payload);
DynamicJsonDocument root(300);
deserializeJson(root,payload);
const char *cmd = root["cmd"];
const long val = root["val"];
if (strstr(cmd,"START")) {
sendMSG("INFO","ESP32: Received Start command");
sample_period=val;
SampleSlow.attach_ms(sample_period,sampleslow_action);
Serial.print("sample_period = "); Serial.println(sample_period);
} else if (strstr(cmd,"STOP")) {
sendMSG("INFO","ESP32: Received Stop command");
SampleSlow.detach();
} else if (strstr(cmd,"DAC25")) {
dacWrite(25,val);
sendMSG("INFO","ESP32: set DAC25 to requested value");
} else if (strstr(cmd,"DO18")) {
digitalWrite(18,val);
} else if (strstr(cmd,"RAMP25")) {
if (val > 0) {
Ramp25Ticker.attach_ms(val,ramp25_action);
} else {
Ramp25Ticker.detach();
}
} else if (strstr(cmd,"PULSE18")) {
if (val > 0) {
Pulse18Ticker.attach_ms(val,pulse18_action);
} else {
Pulse18Ticker.detach();
}
} else {
Serial.println("Unknown command");
sendMSG("INFO","ESP32: Unknown command received");
}
}
}
}
-
handle_notfound():当请求的页面不存在时,返回提示信息。 -
sendMSG():将名称和值格式化为JSON包,并存储在info_buffer中,设置info_available标志。 -
webSocketEvent():处理WebSocket相关事件,如客户端连接、断开连接以及接收JSON格式的消息。根据消息中的cmd和val执行相应的操作,如启动或停止数据采集、设置输出引脚和DAC的值等。
3.4 设置函数
void setup() {
pinMode(21,INPUT_PULLUP);
pinMode(18,OUTPUT);
dacWrite(25,0); // initialize DAC
Serial.begin(115200); delay(1000);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.print("\nConnected to ");
Serial.print(ssid);
Serial.print(" with IP address: ");
Serial.println(WiFi.localIP());
webSocket.begin();
webSocket.onEvent(webSocketEvent);
if (!SPIFFS.begin(true)) {
Serial.println("ERROR: no SPIFFS filesystem found");
return;
} else {
server2.begin();
server2.serveStatic("/", SPIFFS, "/esp32-daq.html");
server2.onNotFound(handle_notfound);
Serial.println("SPIFFS file system found and server started");
}
}
在
setup()
函数中,完成以下操作:
- 配置数字引脚和DAC。
- 初始化串口通信。
- 连接到WiFi网络。
- 启动WebSocket并注册回调函数。
- 检查SPIFFS文件系统是否存在,如果存在则启动Web服务器并提供网页服务。
3.5 循环函数
void loop() {
server2.handleClient();
webSocket.loop();
if (info_available==1) {
info_available=0;
webSocket.sendTXT(websock_num,info_buffer,strlen(info_buffer));
}
if (output_ready==1) {
output_ready=0;
doc.to<JsonObject>();
for (int k=0;k<3;k++) {
doc["ADC"][k]=samples[k];
}
serializeJson(doc,out);
webSocket.sendTXT(websock_num,out,strlen(out));
}
yield();
}
在
loop()
函数中:
- 处理Web服务器和WebSocket的事件。
- 如果
info_available
标志被设置,则将JSON格式的消息发送到浏览器。
- 如果
output_ready
标志被设置,则将测量值组装成JSON消息并发送到浏览器。
4. 网页和JavaScript代码
网页和JavaScript代码构成了WebSocket通信通道的另一端,以下是主要部分:
<!DOCTYPE HTML>
<HTML lang="en">
<HEAD>
<TITLE>ESP-DAQ</TITLE>
<META charset="UTF-8">
<STYLE>
#displayarea { border: 1px solid black; }
#adc32 { border: 1px solid black; }
#adc33 { border: 1px solid black; }
#trace0 { fill: none; stroke: red; stroke-width: 1px;}
#trace1 { fill: none; stroke: blue; stroke-width: 1px;}
#ip {float: right;}
</STYLE>
</HEAD>
<BODY>
<P> ESP-DAQ:
<button id="start" type="button" onclick="start();">Start</button>
<button id="stop" type="button" onclick="stop();">Stop</button>
<SELECT onchange="setSamplePeriod(this.value);">
<OPTGROUP label="Roll mode">
<OPTION value="5">5 ms</OPTION>
<OPTION value="10">10 ms</OPTION>
<OPTION value="20">20 ms</OPTION>
<OPTION value="50">50 ms</OPTION>
<OPTION selected="selected" value="100">100 ms</OPTION>
<OPTION value="200">200 ms</OPTION>
<OPTION value="500">500 ms</OPTION>
<OPTION value="1000">1 s</OPTION>
<OPTION value="2000">2 s</OPTION>
<OPTION value="5000">5 s</OPTION>
</OPTGROUP>
</SELECT>
<button id="clear" type="button" onclick="cleardisplay();">Clear</button>
<A id=’ip’>IP address</A>
</P>
<P><DETAILS open> <SUMMARY>Input output</SUMMARY>
<CANVAS id="Din" width="100" height="20"> </CANVAS>
DO18:<INPUT type="checkbox" id="DO18" onchange="setDO(18,this.checked)"/>
<SELECT onchange="pulsepin18(this.value);">
<OPTION value="0">Pulse D18 OFF</OPTION>
<OPTION value="1">Pulse D18 with 1 ms steps</OPTION>
<OPTION value="10">Pulse D18 with 10 ms steps</OPTION>
<OPTION value="100">Pulse D18 with 100 ms steps</OPTION>
<OPTION value="1000">Pulse D18 with 1 s steps</OPTION>
<OPTION value="2000">Pulse D18 with 2 s steps</OPTION>
<OPTION value="5000">Pulse D18 with 5 s steps</OPTION>
</SELECT> </P>
<P>DAC25: <INPUT type="range" min="0" max="255" step="1" value="0" onchange="updatedac25(this.value)" />
<SELECT onchange="rampdac25(this.value);">
<OPTION value="0">Ramp DAC25 OFF</OPTION>
<OPTION value="10">Ramp DAC25 with 10 ms steps</OPTION>
<OPTION value="50">Ramp DAC25 with 50 ms steps</OPTION>
</SELECT></P>
<P><DIV id="adc32val">ADC32: unknown</DIV>
<CANVAS id="adc32" width="512" height="10"> </CANVAS>
<DIV id="adc33val">ADC33: unknown</DIV>
<CANVAS id="adc33" width="512" height="10"> </CANVAS>
</DETAILS></P>
<DETAILS open>
<SUMMARY>Logging display</SUMMARY>
<SVG id="displayarea" width="1024px" height="512px">
<PATH id="trace0" d="M0 256" />
<PATH id="trace1" d="M0 256" />
</SVG>
</DETAILS>
<DIV id="status">Status window</DIV>
<DIV id="reply">Reply from ESP32</DIV>
<SCRIPT>
var sample_period=100, current_position=0;
var ipaddr=location.hostname + ":81";
document.getElementById(’ip’).innerHTML=ipaddr;
show_noconnect();
var websock = new WebSocket(’ws://’ + ipaddr);
websock.onerror = function(evt) { console.log(evt); toStatus(evt) };
websock.onopen = function(evt) { console.log(’websock open’); };
websock.onclose = function(evt) {
console.log(’websock close’);
toStatus(’websock close’);
};
function toStatus(txt){
document.getElementById(’status’).innerHTML=txt;
}
function toReply(txt) {
document.getElementById(’reply’).innerHTML=txt;
}
function start() {
websock.send(JSON.stringify({"cmd":"START", "val":sample_period}));
}
function stop() {
websock.send(JSON.stringify({ "cmd" : "STOP", "val" : "-1" }));
show_noconnect();
}
websock.onmessage=function(event) {
console.log(event);
var stuff=JSON.parse(event.data);
var val=stuff["INFO"];
if ( val != undefined ) {
toReply(val);
}
val=stuff["ADC"];
if ( val != undefined ) {
document.getElementById(’adc32val’).innerHTML=
"ADC32: " + val[0] + " --> " + (val[0]*3.3/511).toFixed(2) + " V";
c=document.getElementById(’adc32’).getContext("2d");
c.clearRect(0,0,512,10);
c.fillStyle = "#FF0000";
c.fillRect(0,0,val[0],10);
document.getElementById(’adc33val’).innerHTML=
"ADC33: " + val[1] + " --> " + (val[1]*3.3/511).toFixed(2) + " V";
c=document.getElementById(’adc33’).getContext("2d");
c.clearRect(0,0,512,10);
c.fillStyle = "#0000FF";
c.fillRect(0,0,val[1],10);
c=document.getElementById(’Din’).getContext("2d");
if (val[2]==1) {
c.fillStyle = "#00FF00";
} else {
c.fillStyle = "#FF0000";
}
c.fillRect(20,0,100,20);
c.font = "20px Arial";
c.fillStyle = "#000000";
c.fillText("DI21",50,17);
dd=document.getElementById(’trace0’).getAttribute(’d’);
dd += ’ L’ + current_position + ’ ’ + (512-val[0]);
document.getElementById(’trace0’).setAttribute(’d’,dd);
dd=document.getElementById(’trace1’).getAttribute(’d’);
dd += ’ L’ + current_position + ’ ’ + (512-val[1]);
document.getElementById(’trace1’).setAttribute(’d’,dd);
current_position += 1;
if (current_position > 1024) {
dd=document.getElementById(’trace0’).getAttribute(’d’).replace(/M[^L]*L/, "M");
document.getElementById(’trace0’).setAttribute(’d’,dd);
document.getElementById(’trace0’).setAttribute(
’transform’,’translate(’ + (1024-current_position) + ’,0)’);
dd=document.getElementById(’trace1’).getAttribute(’d’).replace(/M[^L]*L/, "M");
document.getElementById(’trace1’).setAttribute(’d’,dd);
document.getElementById(’trace1’).setAttribute(
’transform’,’translate(’ + (1024-current_position) + ’,0)’);
}
}
}
function setSamplePeriod(v) {
toStatus("Setting sample period to "+ v + " ms");
sample_period=v;
}
function cleardisplay() {
dd = "M0 512";
document.getElementById(’trace0’).setAttribute(’d’,dd);
document.getElementById(’trace0’).setAttribute(
’transform’,’translate(0)’);
document.getElementById(’trace1’).setAttribute(’d’,dd);
document.getElementById(’trace1’).setAttribute(
’transform’,’translate(0)’);
current_position=0;
}
function show_noconnect() {
c=document.getElementById(’Din’).getContext("2d");
c.fillStyle = "#DDDDDD"
for (i=2;i<3;i++) {
c.fillRect(20+110*(i-2),0,100,20);
}
c.font = "20px Arial";
c.fillStyle = "#000000";
c.fillText("DI21",50,17);
}
function updatedac25(v) {
volts=v*3.3/255;
toStatus("Set DAC25 = " + v + " -> " + volts.toFixed(2) + " V");
websock.send(JSON.stringify({ "cmd" : "DAC25", "val" : v }));
}
function rampdac25(v) {
toStatus("Ramp DAC25 with time step " + v);
websock.send(JSON.stringify({ "cmd" : "RAMP25", "val" : v }));
}
function setDO(v,s) {
vv=s?1:0;
toStatus("Status changed on DO"+ v + " is " + s + " or " + vv);
websock.send(JSON.stringify({ "cmd" : "DO"+v, "val" : + vv }));
}
function pulsepin18(v) {
toStatus("Pulse D18 with time step " + v + " ms");
websock.send(JSON.stringify({ "cmd" : "PULSE18", "val" : v }));
}
</SCRIPT> </BODY> </HTML>
网页和JavaScript代码的主要功能如下:
- 确定ESP32的IP地址,并将其显示在网页上。
- 打开WebSocket连接,并定义错误处理、打开和关闭连接的回调函数。
-
start()
和
stop()
函数用于发送JSON格式的消息,指示ESP32启动或停止数据采集。
-
.onmessage
方法处理从ESP32接收到的消息,根据消息类型更新网页上的显示内容。
- 其他JavaScript函数用于设置采样周期、清除显示、设置DAC值、切换输出引脚状态等。
5. 网页传输到ESP32
将网页传输到ESP32的步骤如下:
1. 在ESP minidaq.ino文件所在的子目录下创建一个名为data的子目录。
2. 将HTML文件复制到data子目录中。
3. 从https://github.com/me-no-dev/arduino-esp32fs-plugin的发布页面下载ESP32FS - 1.0.zip,并将其解压到/Arduino/tools/子目录中。
4. 重启Arduino IDE,在Tools菜单中找到ESP32 Sketch Data Upload选项,将网页上传到ESP32。
5. 如果上传失败,请确保Serial monitor窗口已关闭,然后再次尝试。
6. 系统优势与拓展思路
通过上述步骤,我们建立了微控制器和浏览器之间的双向通信通道。该系统的用户界面完全由运行在浏览器上的HTML和JavaScript代码指定,具有很高的灵活性和可扩展性。以下是一些拓展项目的思路:
1.
产生500Hz矩形信号
:在ESP32的某个输出引脚产生500Hz的矩形信号。
2.
调整锯齿波信号幅度
:使锯齿波信号的最小和最大幅度可通过网页界面上的选择菜单或滑块进行调整。
3.
输出正弦信号
:使用引脚26上的第二个DAC通过直接数字合成输出类似正弦的信号。
4.
输出三角波信号
:实现输出电压线性上升到最大值再线性降为零的三角波信号。
5.
控制AD9850 DDS板
:添加AD9850 DDS板,并通过网页界面进行控制。
6.
连接传感器
:将喜欢的传感器连接到ESP32,并通过网页界面进行控制。
7.
驱动步进电机
:编程四个输出引脚以产生驱动步进电机的开关模式。
8.
添加连接控制按钮
:在网页界面上添加一个按钮用于关闭WebSocket连接,另一个按钮用于在连接中断时重新连接。
9.
保存测量样本
:使用JavaScript在浏览器上的数组中保存测量样本,以便在按下按钮时显示数值。
通过这些拓展项目,可以进一步发挥该系统的潜力,实现更多有趣的功能。
基于ESP32的浏览器数据采集系统实现
7. 技术点分析
7.1 WebSockets技术
WebSockets是本系统实现双向通信的核心技术。在ESP32端,通过
WebSocketsServer
库创建了一个WebSocket服务器,监听端口81。在浏览器端,使用JavaScript的
WebSocket
对象建立与ESP32的连接。这种通信方式允许浏览器和ESP32实时交换数据,无需频繁的HTTP请求,大大提高了通信效率。
例如,在ESP32的代码中:
WebSocketsServer webSocket = WebSocketsServer(81);
webSocket.begin();
webSocket.onEvent(webSocketEvent);
在浏览器的JavaScript代码中:
var ipaddr = location.hostname + ":81";
var websock = new WebSocket('ws://' + ipaddr);
7.2 JSON数据格式
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,在本系统中用于浏览器和ESP32之间的数据传输。ESP32使用
ArduinoJson
库来处理JSON数据,浏览器则使用
JSON.stringify()
和
JSON.parse()
方法进行JSON数据的序列化和反序列化。
例如,在ESP32的
sendMSG()
函数中:
void sendMSG(char *nam, const char *msg) {
(void) sprintf(info_buffer,"{\"%s\":\"%s\"}",nam,msg);
info_available=1;
}
在浏览器的
start()
函数中:
function start() {
websock.send(JSON.stringify({"cmd":"START", "val":sample_period}));
}
7.3 Ticker库的使用
Ticker库用于在ESP32上实现定时任务。在本系统中,使用
Ticker
对象来控制采样、DAC斜坡和引脚切换的时间间隔。
例如:
Ticker SampleSlow, Ramp25Ticker, Pulse18Ticker;
SampleSlow.attach_ms(sample_period, sampleslow_action);
Ramp25Ticker.attach_ms(val, ramp25_action);
Pulse18Ticker.attach_ms(val, pulse18_action);
8. 关键路径解析
8.1 数据采集流程
graph TD;
A[启动数据采集] --> B[设置采样周期];
B --> C[开始定时采样];
C --> D[读取输入引脚值];
D --> E[存储采样值];
E --> F[设置输出就绪标志];
F --> G[发送采样数据到浏览器];
-
用户在浏览器界面点击“Start”按钮,发送
START命令到ESP32。 -
ESP32接收到命令后,根据命令中的采样周期设置
SampleSlow定时器。 -
SampleSlow定时器定时触发sampleslow_action()函数,读取输入引脚的值并存储在samples数组中。 -
设置
output_ready标志,表示有新的采样数据可用。 -
在
loop()函数中,检查output_ready标志,若设置则将采样数据组装成JSON消息发送到浏览器。
8.2 命令处理流程
graph TD;
A[浏览器发送命令] --> B[ESP32接收命令];
B --> C[解析命令类型];
C --> D{命令类型};
D -- START --> E[设置采样周期并启动采样];
D -- STOP --> F[停止采样];
D -- DAC25 --> G[设置DAC值];
D -- DO18 --> H[设置输出引脚状态];
D -- RAMP25 --> I[启动或停止DAC斜坡];
D -- PULSE18 --> J[启动或停止引脚切换];
D -- 其他 --> K[返回未知命令信息];
- 用户在浏览器界面操作按钮、滑块或选择菜单,发送相应的JSON命令到ESP32。
-
ESP32的
webSocketEvent()函数接收到命令后,解析命令中的cmd和val。 -
根据
cmd的值,使用switch-case语句执行相应的操作,如启动或停止数据采集、设置输出引脚和DAC的值等。
9. 常见问题及解决方法
9.1 网页上传失败
- 问题描述 :在使用ESP32 Sketch Data Upload上传网页到ESP32时失败。
- 解决方法 :确保Serial monitor窗口已关闭,因为串口监视器可能会占用ESP32的通信资源,导致上传失败。关闭后再次尝试上传。
9.2 连接不稳定
- 问题描述 :浏览器和ESP32之间的WebSocket连接不稳定,频繁断开。
-
解决方法
:
- 检查WiFi信号强度,确保ESP32和浏览器所在设备处于稳定的WiFi环境中。
- 检查ESP32的电源供应,不稳定的电源可能导致设备工作异常。
- 在浏览器端添加重连机制,当连接断开时自动尝试重新连接。
10. 总结与展望
本系统通过ESP32微控制器和浏览器之间的双向通信,实现了对数字和模拟信号的采集和控制。用户界面由HTML和JavaScript代码实现,具有很高的灵活性和可扩展性。通过对系统的技术点分析和关键路径解析,我们可以更好地理解系统的工作原理,为进一步的拓展和优化提供基础。
未来,可以根据实际需求对系统进行更多的拓展和优化。例如,添加更多的传感器接口,实现更多类型的数据采集;优化用户界面,提高用户体验;增加数据存储和分析功能,对采集到的数据进行更深入的处理。相信随着技术的不断发展,基于ESP32的浏览器数据采集系统将在更多领域得到应用。
超级会员免费看
891

被折叠的 条评论
为什么被折叠?



