如何基于持续传入的UDP数据实时更新Canvas绘图?
ESP32主从架构实时Canvas数据展示方案
我是Web开发新手,首次接触该领域,如有不当之处请见谅。目前正在搭建一个ESP32主从架构系统:
- 从设备通过UDP向主设备传输8路传感器数据
- 主设备托管轻量化网页,需要在Canvas上以类柱状图形式实时展示数据,要求接近实时且不能通过页面刷新实现
当前已完成数据传输和网页托管,但还需要将接收的数据整理为变量并驱动Canvas实时重绘。我认为GET/POST无法实现跨用户页面更新,fetch不适用于实时修改,不确定WebSocket能否解决重绘问题,寻求可行的技术方案。
现有代码
HTML页面
<!DOCTYPE html> <html> <body> <center> <h2>Planter Air Balancer</h2> <script> setInterval(myTimer, 1000); function myTimer() { const date = new Date(); document.getElementById("time").innerHTML = date.toLocaleTimeString(); } </script> <canvas id="myCanvas" width="800" height="400" style="border:2px solid #000000;"> Sorry, your browser does not support canvas. </canvas> <p id="Graph"></p> </body> <script> setInterval(myCanvas, 100); function myCanvas(){ let numRows = 47; // Create a Canvas: const canvas = document.getElementById("myCanvas"); const ctx = canvas.getContext("2d"); // Set font size and style ctx.font = "10px Arial"; // Clear drawing field ctx.clearRect(0,0,canvas.width, canvas.height); for(let i = 1; i <= numRows; i++){ ctx.fillText (i, 16.5*i, canvas.height-10); // Define a new path ctx.beginPath(); // Set a start-point ctx.moveTo(2+16.5*i, canvas.height-20); // Set an end-point ctx.lineTo(2+16.5*i, canvas.height/2-(Math.random()*10)); //This Math.random() piece is just to give an idea of what I want to have the end product resemble. I'll set up some arrays or a structure to feed data into later on. // Stroke it (Do the Drawing) ctx.stroke(); } } </script> <p id = "time"></p> </center> </html>
主设备(ESP32)代码
// Load Wi-Fi library #include <WiFi.h> #include <esp_littlefs.h> #include <WiFiUdp.h> // Replace with your network credentials const char* ssid = "ESP32-Access-Point"; const char* password = "123456789"; // Set web server port number to 80 WiFiServer server(80); WiFiUDP udp; const int udpPort = 80; // Variable to store the HTTP request String header; void setup() { Serial.begin(115200); //udp.begin(WiFi.localIP(), udpPort); // Connect to Wi-Fi network with SSID and password Serial.print("Setting AP (Access Point)…"); // Remove the password parameter, if you want the AP (Access Point) to be open WiFi.softAP(ssid, password); IPAddress IP = WiFi.softAPIP(); Serial.print("AP IP address: "); Serial.println(IP); server.begin(); udp.begin(udpPort); Serial.println(udpPort); } void loop(){ static bool clientConnected = false; int randomNumber = random(100); int serialAvailability = Serial.available(); if(Serial.available()){ char text[100] = {0}; Serial.readBytesUntil('\n', text, 100); Serial.println(text); if(!clientConnected) return; } int packetSize = udp.parsePacket(); if(packetSize){ Serial.print("Received packet of size "); Serial.println(packetSize); Serial.print("From: "); Serial.print(udp.remoteIP()); //Serial.print(" , Port: "); //Serial.print(udp.remotePort()); Serial.print(". "); clientConnected = true; char packetBuffer[150] = {0}; udp.read(packetBuffer, packetSize); Serial.println(packetBuffer); } WiFiClient client = server.available(); // Listen for incoming clients if (client) { // If a new client connects, //Serial.println("New Client."); // print a message out in the serial port String currentLine = ""; // make a String to hold incoming data from the client while (client.connected()) { // loop while the client's connected if (client.available()) { // if there's bytes to read from the client, char c = client.read(); // read a byte, then Serial.write(c); // print it out the serial monitor header += c; if (c == '\n') { // if the byte is a newline character // if the current line is blank, you got two newline characters in a row. // that's the end of the client HTTP request, so send a response: if (currentLine.length() == 0) { // HTTP headers always start with a response code (e.g. HTTP/1.1 200 OK) // and a content-type so the client knows what's coming, then a blank line: client.println("HTTP/1.1 200 OK"); client.println("Content-type:text/html"); //client.println("Connection: close"); client.println(); // Display the HTML web page client.println("<!DOCTYPE html><html>"); client.println("<body><center><h2>Planter Air Balancer</h2>"); client.println("<script>"); client.println("setInterval(myTimer,1000);"); client.println("function myTimer(){"); client.println("const date = new Date();"); client.println("document.getElementById(\"time\").innerHTML = date.toLocaleTimeString();}"); client.println("</script>"); client.println("<canvas id=\"myCanvas\" width = \"800\" height = \"400\" style = \"border:2px solid #000000;\">"); client.println("Sorry, your browser does not support canvas."); client.println("</canvas><p id=\"Graph\"></p></body>"); client.println("<script>"); client.println("setInterval(myCanvas, 1000);"); client.println("function myCanvas(){"); client.println("let numRows = 47;"); client.println("const canvas = document.getElementById(\"myCanvas\");"); client.println("const ctx = canvas.getContext(\"2d\");"); client.println("ctx.font = \"10px Arial\";"); client.println("ctx.clearRect(0,0, canvas.width, canvas.height)"); client.println("for(let i = 1; i <=numRows; i++){"); client.println("ctx.fillText (i, 16.5*i, canvas.height-10);"); client.println("ctx.beginPath();"); client.println("ctx.moveTo(2+16.5*i, canvas.height-20);"); client.println("ctx.lineTo(2+16.5*i, canvas.height/2-Math.random()*10);"); client.println("ctx.stroke();"); client.println("}"); client.println("}"); client.println("</script>"); client.println("<p id = \"time\"></p>"); client.println("</center>"); client.println("</html>"); // The HTTP response ends with another blank line client.println(); // Break out of the while loop break; } else { // if you got a newline, then clear currentLine currentLine = ""; } } else if (c != '\r') { // if you got anything else but a carriage return character, currentLine += c; // add it to the end of the currentLine } } } // Clear the header variable header = ""; // Close the connection client.stop(); //Serial.println("Client disconnected."); //Serial.println(""); } }
从设备(ESP32)代码
#include <arduino.h> #include <Wire.h> #include <WiFi.h> #include <WiFiUdp.h> const int udpPort = 80; boolean connected = false; WiFiUDP udp; #define S1 36 #define S2 39 #define S3 34 #define S4 35 #define S5 32 #define S6 33 #define S7 32 #define S8 33 int randLOW = -1; int randHIGH = 1; unsigned long sensorOne = 0; unsigned long sensorTwo = 0; unsigned long sensorThree = 0; unsigned long sensor[48]; struct{ int Sensor1; int Sensor2; int Sensor3; int Sensor4; int Sensor5; int Sensor6; int Sensor7; int Sensor8; } pSensor; unsigned long timeSinceLast; unsigned long timeSetAvg; int16_t sensor1Init; int16_t sensor2Init; int16_t sensor3Init; int16_t sensor4Init; int16_t sensor5Init; int16_t sensor6Init; int16_t sensor7Init; int16_t sensor8Init; const char* ssid = "ESP32-Access-Point"; const char* password = "123456789"; const char* host = "192.168.4.1"; const char * networkName = "ESP32-Access-Point"; const char * networkPswd = "123456789"; const char * udpAddress = "192.168.4.1"; void setup() { pinMode(S1, INPUT); pinMode(S2, INPUT); pinMode(S3, INPUT); pinMode(S4, INPUT); pinMode(S5, INPUT); pinMode(S6, INPUT); pinMode(S7, INPUT); pinMode(S8, INPUT_PULLDOWN); Serial.begin(115200); Serial.println(); Serial.print("Connecting to "); Serial.println(ssid); connectToWiFi(networkName, networkPswd); udp.begin(udpPort); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } } void loop() { delay(100); pSensor.Sensor1 = analogRead(S1)-sensor1Init; pSensor.Sensor2 = analogRead(S2)-sensor2Init; pSensor.Sensor3 = analogRead(S3)-sensor3Init; pSensor.Sensor4 = analogRead(S4)-sensor4Init; pSensor.Sensor5 = analogRead(S5)-sensor5Init; pSensor.Sensor6 = analogRead(S6)-sensor6Init; pSensor.Sensor7 = analogRead(S7)-sensor7Init; pSensor.Sensor8 = analogRead(S8)-sensor8Init; /////////////////////////WIFI STUFF BEGINS/////////////////////// if(connected){ //Send a packet //int a = a++; udp.beginPacket(udpAddress,udpPort); //udp.printf("Seconds since boot: %lu", millis()/1000); udp.printf("%i", pSensor.Sensor1); udp.printf(", "); udp.printf("%i", pSensor.Sensor2); udp.printf(", "); udp.printf("%i", pSensor.Sensor3); udp.printf(", "); udp.printf("%i", pSensor.Sensor4); udp.printf(", "); udp.printf("%i", pSensor.Sensor5); udp.printf(", "); udp.printf("%i", pSensor.Sensor6); udp.printf(", "); udp.printf("%i", pSensor.Sensor7); udp.printf(", "); udp.printf("%i", pSensor.Sensor8); udp.endPacket(); Serial.print(pSensor.Sensor1); Serial.print(", "); Serial.print(pSensor.Sensor2); Serial.print(", "); Serial.print(pSensor.Sensor3); Serial.print(", "); Serial.print(pSensor.Sensor4); Serial.print(", "); Serial.print(pSensor.Sensor5); Serial.print(", "); Serial.print(pSensor.Sensor6); Serial.print(", "); Serial.print(pSensor.Sensor7); Serial.print(", "); Serial.println(pSensor.Sensor8); //Serial.println("Sent Data!"); //Serial.println(udp.remotePort()); //Serial.println(udp.remoteIP()); } } void connectToWiFi(const char * ssid, const char * pwd){ Serial.println("Connecting to WiFi network: " + String(ssid)); // delete old config WiFi.disconnect(true); //register event handler WiFi.onEvent(WiFiEvent); //Initiate connection WiFi.begin(ssid, pwd); Serial.println("Waiting for WIFI connection..."); } //wifi event handler void WiFiEvent(WiFiEvent_t event){ switch(event) { case ARDUINO_EVENT_WIFI_STA_GOT_IP: //When connected set Serial.print("WiFi connected! IP address: "); Serial.println(WiFi.localIP()); //initializes the UDP state //This initializes the transfer buffer udp.begin(WiFi.localIP(),udpPort); connected = true; break; case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: Serial.println("WiFi lost connection"); connected = false; break; default: break; } }
技术解决方案
核心方案:WebSocket双向通信
WebSocket是解决实时数据推送的最优选择,主设备收到UDP传感器数据后,可主动推送给所有连接的网页客户端,实现接近实时的Canvas重绘,无需页面刷新或轮询。
步骤1:主设备(ESP32)改造
使用ESPAsyncWebServer和AsyncWebSocket库替换原有WiFiServer,实现HTTP网页托管+WebSocket推送功能:
#include <WiFi.h> #include <ESPAsyncWebServer.h> #include <AsyncWebSocket.h> #include <WiFiUdp.h> const char* ssid = "ESP32-Access-Point"; const char* password = "123456789"; AsyncWebServer server(80); AsyncWebSocket ws("/ws"); WiFiUDP udp; const int udpPort = 80; String latestSensorData = ""; // WebSocket事件处理 void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) { switch(type) { case WS_EVT_CONNECT: Serial.printf("WebSocket客户端 #%u 连接来自 %s\n", client->id(), client->remoteIP().toString().c_str()); // 新连接立即发送最新数据 client->text(latestSensorData); break; case WS_EVT_DISCONNECT: Serial.printf("WebSocket客户端 #%u 断开连接\n", client->id()); break; default: break; } } void setup() { Serial.begin(115200); WiFi.softAP(ssid, password); IPAddress IP = WiFi.softAPIP(); Serial.print("AP IP地址: "); Serial.println(IP); // 初始化WebSocket ws.onEvent(onWsEvent); server.addHandler(&ws); // 托管网页(直接嵌入HTML,也可存到LittleFS) server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ request->send_P(200, "text/html", R"EOF( <!DOCTYPE html> <html> <body> <center> <h2>Planter Air Balancer</h2> <script> setInterval(myTimer, 1000); function myTimer() { const date = new Date(); document.getElementById("time").innerHTML = date.toLocaleTimeString(); } </script> <canvas id="myCanvas" width="800" height="400" style="border:2px solid #000000;"> Sorry, your browser does not
相关产品推荐
相关产品推荐

