You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何基于持续传入的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
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.01 13:27:52