网页 HTML ↔ ESP32 完整通信体系详解

一、整体分层架构(从上到下)

  1. 应用层:网页 JS / ESP32 业务逻辑 网页:生成航点 JSON 文本,调用 WebSocket 发送 API; ESP32:接收 JSON、解析坐标、串口打印 / 转发飞控。
  2. 传输层:WebSocket 协议(基于 TCP) 底层依托 TCP 长连接,是两者直接通信的核心协议。
  3. 网络层:WiFi 局域网 IP 通信(IPv4) 电脑和 ESP32 接入同一个路由器 WiFi ,处于同一局域网,靠 IP 地址寻址(你的 ESP32:192.168.1.74)。
  4. 物理层:2.4G WiFi 无线射频 ESP32 自带 WiFi 模组,电脑靠无线网卡收发路由器信号。

二、核心通信协议:WebSocket(重点)

1. 是什么

WebSocket 是基于 TCP 的全双工长连接协议,专门用于浏览器与设备实时双向通信,弥补普通 HTTP 短连接的缺陷。

  • 普通 HTTP:一问一答,发完就断开,每次传数据都要重新握手;
  • WebSocket:一次握手建立持久连接,网页和 ESP32 随时互相发数据,延迟极低,适合实时下发航点。

2. 完整通信流程(分 2 大阶段)

阶段 1:HTTP 握手升级连接(仅连接时执行 1 次)
  1. 浏览器发起 HTTP 请求,请求头携带 Upgrade: websocket,告知服务器(ESP32)要升级协议;
  2. ESP32 校验握手密钥,返回 HTTP/1.1 101 Switching Protocols
  3. 握手完成,TCP 通道永久保留,切换为 WebSocket 二进制 / 文本帧传输,不再走 HTTP。
阶段 2:WebSocket 帧双向数据传输(下发航点就是这个阶段)

你下发航点时,网页把 JSON 字符串封装成WebSocket 文本帧发给 ESP32:

  1. JS 调用 ws.send(JSON字符串)
  2. 浏览器自动包装 WebSocket 帧头、掩码;
  3. WiFi 通过 TCP 数据包发到 ESP32;
  4. ESP32 原生 socket 代码解析帧、去除掩码,还原原始 JSON 文本;
  5. ESP32 解析 JSON 得到航点坐标,串口打印。

3. 你的场景传输格式

  • 传输载体:WebSocket 文本帧(Text Frame)
  • 载荷内容:UTF-8 编码 JSON 字符串(结构化航点数据)
  • 端口:固定 81,浏览器通过 ws://192.168.1.74:81 定位 ESP32 设备

4. 全双工特性(双向收发)

  • 正向:网页 → ESP32:下发航点 JSON;
  • 反向:ESP32 → 网页:可回传状态(例如 "航点接收成功"、GPS 状态),你当前代码未启用,但协议支持。

三、底层承载:TCP 协议(WebSocket 的地基)

  1. WebSocket不能脱离 TCP 单独运行,所有 WebSocket 帧都会被封装成 TCP 数据包;
  2. TCP 是可靠传输:自带重传、校验、分包重组;
    • 优势:航点 JSON 不会丢包、不会乱序,坐标完整送达 ESP32;
    • 对比 UDP:UDP 无校验,容易丢失航点,不适合航线下发。
  3. 端口作用:一台设备可同时开多个服务(80 网页、81WebSocket),靠端口区分。

四、网络环境:局域网内网通信

1. 通信前提硬性要求

电脑(打开 HTML 的设备)和 ESP32 必须连接同一个路由器的 2.4G WiFi

  • ESP32 只支持 2.4G WiFi,不支持 5G;
  • 两者网关、子网掩码一致,路由器允许内网设备互相访问。

2. 寻址逻辑

  • ESP32 上电连接 WiFi 后,路由器分配内网静态 / 动态 IP:192.168.1.74
  • 浏览器通过这个 IP + 端口 81,在内网找到 ESP32 的 WebSocket 服务;
  • 无法跨外网直接访问:如果电脑连手机热点、ESP 连家里 WiFi,不在同一局域网,连接会直接失败。

五、两端数据交互格式详解

1. 网页发送侧(输出)

  1. JS 处理高德地图坐标,GCJ02 转 WGS84;
  2. 组装 JS 对象,{cmd:"upload_mission",waypoints:[坐标数组]}
  3. JSON.stringify() 转为纯文本字符串;
  4. WebSocket 文本帧封装,TCP 通过 WiFi 发给 ESP32。

2. ESP32 接收侧(输入)

  1. Socket 捕获 TCP 数据流,解析 WebSocket 帧结构,去除掩码;
  2. 得到纯净 JSON 字符串;
  3. json.loads() 把文本还原为字典对象,提取 lat/lon/alt 航点;
  4. 业务处理:串口打印 / 本地存储 / 封装 MAVLink 转发飞控。

3. 中间载体 JSON 的定位

JSON 只是应用层数据封装格式,不属于网络通信协议:

  • 网络传输只认二进制数据流(TCP 包、WebSocket 帧);
  • JSON 是人为约定的文本规范,方便人阅读、两端快速解析航点,换二进制协议也能通信(如直接发 MAVLink 二进制)。

六、对比其他通信方式,为什么选 WebSocket?

表格

通信方式 适配你的场景优缺点
WebSocket(当前方案) 浏览器原生支持,无需插件;长连接实时下发;结构化 JSON 易调试;内网 WiFi 稳定,最优解
HTTP 轮询 短连接频繁握手,下发航点延迟高,不推荐
UDP Socket 无可靠校验,航点容易丢包,不适合航线下发
蓝牙串口 传输距离仅 10 米,地图规划距离受限
串口有线 电脑必须插 USB 连 ESP32,无无线便捷性

七、完整通信链路总结(一句话串联)

电脑浏览器 JS 生成航点 JSON → 封装为WebSocket 文本帧 → 底层 TCP 协议打包 → 2.4G WiFi 路由器局域网传输 → ESP32 的 WiFi 模组接收 TCP 流 → 解析 WebSocket 帧还原 JSON 字符串 → MicroPython 解析 JSON 提取 WGS84 航点坐标 → 串口打印输出。

网页端代码:

<!doctype html>

<html>

<head>

<meta charset="utf-8">

<meta http-equiv="X-UA-Compatible" content="IE=edge">

<link rel="stylesheet" href="https://a.amap.com/jsapi_demos/static/demo-center/css/demo-center.css"/>

<title>JS直接WiFi发送航点至ESP32</title>

<style>

html,body,#container {margin:0;padding:0;width:100%;height:100%;}

.input-card {z-index:999;display:flex;gap:10px;align-items:center;flex-wrap:wrap;padding:8px;}

.item {display:flex;flex-direction:column;gap:4px;}

input {width:340px;padding:4px;}

button {padding:6px 12px;border:none;border-radius:3px;cursor:pointer;}

#clearAllBtn {background:#f44336;color:#fff;}

#sendEsp32Btn {background:#228B22;color:#fff;}

.wp-dot {width:12px;height:12px;background:#ff3300;border-radius:50%;border:2px solid #fff;cursor:pointer;}

</style>

</head>

<body>

<div id="container"></div>

<div class="input-card">

<div class="item"><span>GCJ02高德坐标</span><input readonly id="gcjInput"></div>

<div class="item"><span>WGS84飞控坐标</span><input readonly id="wgsInput"></div>

<button id="clearAllBtn">一键清空全部航点</button>

<button id="sendEsp32Btn">WiFi下发航点到ESP32</button>

</div>

<script src="https://webapi.amap.com/maps?v=2.0\&key=43er461ioff81e37682a97d7fb12"></script>

<script>

const markersArray = \[\];

const pointList = \[\];

let polyline = null;

const DEFAULT_ALT = 10;

// ========== 配置ESP32 WebSocket地址(已改成 192.168.1.74)==========

const ESP32_WS_URL = "ws://192.168.1.74:81";

let ws = null;

const PI = 3.14159265358979324;

const A = 6378245.0;

const EE = 0.006693421622965923;

// GCJ02转WGS84

function gcjToWgs(gcjLng, gcjLat) {

function transLat(x, y) {

let ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x));

ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0;

ret += (20.0 * Math.sin(y * PI) + 40.0 * Math.sin(y / 3.0 * PI)) * 2.0 / 3.0;

ret += (160.0 * Math.sin(y / 12.0 * PI) + 320 * Math.sin(y / 30.0 * PI)) * 2.0 / 3.0;

return ret;

}

function transLng(x, y) {

let ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x));

ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0;

ret += (20.0 * Math.sin(x * PI) + 40.0 * Math.sin(x / 3.0 * PI)) * 2.0 / 3.0;

ret += (150.0 * Math.sin(x / 12.0 * PI) + 300.0 * Math.sin(x / 30.0 * PI)) * 2.0 / 3.0;

return ret;

}

let dLat = transLat(gcjLng - 105.0, gcjLat - 35.0);

let dLng = transLng(gcjLng - 105.0, gcjLat - 35.0);

let radLat = gcjLat / 180.0 * PI;

let magic = Math.sin(radLat);

magic = 1 - EE * magic * magic;

let sqrtMagic = Math.sqrt(magic);

dLat = (dLat * 180.0) / ((A * (1 - EE)) / (magic * sqrtMagic) * PI);

dLng = (dLng * 180.0) / (A / sqrtMagic * Math.cos(radLat) * PI);

return {lng: gcjLng - dLng, lat: gcjLat - dLat};

}

// 地图初始化

const map = new AMap.Map('container', {

center: 110.038988,21.455353,

layers: new AMap.TileLayer.Satellite(),

zoom: 18

});

polyline = new AMap.Polyline({path:\[\],strokeColor:"#00dd00",strokeWeight:3,strokeOpacity:0.8,map:map});

// 点击红点删除航点

document.getElementById('container').addEventListener('click', e=>{

const dot = e.target.closest('.wp-dot');

if(!dot) return e.stopPropagation();

const idx = Number(dot.dataset.index);

markersArrayidx.remove();

markersArray.splice(idx,1);

pointList.splice(idx,1);

polyline.setPath(pointList);

refreshDotIndex();

});

// 地图新增航点

map.on('click', e=>{

const gcjLng = e.lnglat.getLng();

const gcjLat = e.lnglat.getLat();

const wgs = gcjToWgs(gcjLng, gcjLat);

document.getElementById('gcjInput').value = `{gcjLng.toFixed(6)},{gcjLat.toFixed(6)}`;

document.getElementById('wgsInput').value = `{wgs.lng.toFixed(6)},{wgs.lat.toFixed(6)}`;

const idx = markersArray.length;

const marker = new AMap.Marker({

position:new AMap.LngLat(gcjLng,gcjLat),map:map,content:`<div class="wp-dot" data-index="${idx}"></div>`,anchor:'center'

});

markersArray.push(marker);

pointList.push(new AMap.LngLat(gcjLng,gcjLat));

polyline.setPath(pointList);

});

// 一键清空

document.getElementById('clearAllBtn').onclick = ()=>{

markersArray.forEach(m=>m.remove());

markersArray.length = 0;

pointList.length = 0;

polyline.setPath(\[\]);

document.getElementById("gcjInput").value = "";

document.getElementById("wgsInput").value = "";

};

function refreshDotIndex(){

markersArray.forEach((m,i)=>m.getContent().querySelector('.wp-dot').dataset.index = i);

}

// ========== 核心:WebSocket连接ESP32,发送JSON航点数据 ==========

document.getElementById('sendEsp32Btn').onclick = async ()=>{

if(pointList.length < 1) return alert("请先添加航点!");

// 构建标准航点JSON

const sendData = {

cmd: "upload_mission",

home_alt: DEFAULT_ALT,

waypoints: \[\]

};

pointList.forEach(gcjPoint=>{

const wgs = gcjToWgs(gcjPoint.getLng(), gcjPoint.getLat());

sendData.waypoints.push({

lat: Number(wgs.lat.toFixed(6)),

lon: Number(wgs.lng.toFixed(6)),

alt: DEFAULT_ALT,

stay: 0 // 停留时间0秒

});

});

// 建立WebSocket连接

if(!ws || ws.readyState !== WebSocket.OPEN){

ws = new WebSocket(ESP32_WS_URL);

ws.onopen = ()=>{

console.log("ESP32连接成功,发送航点");

ws.send(JSON.stringify(sendData));

alert("航点已下发至ESP32!");

};

ws.onclose = ()=>alert("ESP32断开,请检查WiFi地址");

ws.onerror = ()=>alert("连接ESP32失败,确认同一局域网");

ws.onmessage = (msg)=>console.log("ESP32返回:",msg.data);

}else{

// 已连接直接发送

ws.send(JSON.stringify(sendData));

alert("航点重新下发完成!");

}

};

</script>

</body>

</html>

micropython端的代码:

import network

import socket

import json

import time

===================== 配置项(改成你自己的WiFi信息) =====================

WIFI_SSID = "360WiFi-4A36D5"

WIFI_PASSWORD = "a12345678"

WS_PORT = 81 # 和前端JS里 ws://xxx:81 端口对应

======================================================================

连接WiFi

def connect_wifi():

wlan = network.WLAN(network.STA_IF)

wlan.active(True)

if not wlan.isconnected():

print("正在连接WiFi...")

wlan.connect(WIFI_SSID, WIFI_PASSWORD)

while not wlan.isconnected():

time.sleep(0.5)

ip_info = wlan.ifconfig()

print("WiFi连接成功!ESP32局域网IP:", ip_info0)

return ip_info0

WebSocket简易服务(MicroPython无官方websocket库,用原生socket实现文本传输)

def start_websocket_server(ip):

addr = socket.getaddrinfo(ip, WS_PORT)0-1

s = socket.socket()

s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

s.bind(addr)

s.listen(1)

print(f"WebSocket服务已启动,端口{WS_PORT},等待网页连接...")

while True:

conn, addr = s.accept()

print(f"收到网页客户端连接:{addr}")

try:

握手处理(网页WebSocket标准握手)

data = conn.recv(1024).decode("utf-8")

if "Upgrade: websocket" in data:

计算握手密钥返回

key = data.split("Sec-WebSocket-Key: ")1.split("\r\n")0.strip()

import ubinascii, uhashlib

magic = key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"

sha1 = uhashlib.sha1(magic.encode()).digest()

accept_key = ubinascii.b2a_base64(sha1).decode().strip()

response = (

"HTTP/1.1 101 Switching Protocols\r\n"

"Upgrade: websocket\r\n"

"Connection: Upgrade\r\n"

f"Sec-WebSocket-Accept: {accept_key}\r\n\r\n"

)

conn.send(response.encode())

print("WebSocket握手完成,可以接收航点JSON!")

循环接收网页发来的JSON帧

while True:

frame_header = conn.recv(2)

if not frame_header:

break

byte0, byte1 = frame_header0, frame_header1

opcode = byte0 & 0x0F

payload_len = byte1 & 0x7F

读取载荷长度扩展

if payload_len == 126:

payload_len = int.from_bytes(conn.recv(2), "big")

elif payload_len == 127:

payload_len = int.from_bytes(conn.recv(8), "big")

读取掩码 + 真实文本数据

mask = conn.recv(4)

payload = conn.recv(payload_len)

WebSocket掩码解码

data_raw = bytes(payload\[i ^ maski % 4 for i in range(payload_len)])

json_str = data_raw.decode("utf-8")

print("\n==================== 收到网页JSON原始数据 ====================")

print(json_str)

解析JSON航点

try:

json_data = json.loads(json_str)

cmd = json_data.get("cmd", "")

if cmd == "upload_mission":

home_alt = json_data.get("home_alt", 10)

wp_list = json_data.get("waypoints", \[\])

print("\n===== 解析完成 航点任务详情 =====")

print(f"基准起飞高度:{home_alt} m")

print(f"总航点数量:{len(wp_list)}")

逐条打印每个航点

for idx, wp in enumerate(wp_list):

lat = wp"lat"

lon = wp"lon"

alt = wp"alt"

stay = wp"stay"

print(f"航点{idx+1} | 纬度:{lat:.6f} 经度:{lon:.6f} 高度:{alt}m 停留:{stay}s")

print("================================================\n")

else:

print(f"未知指令:{cmd}")

except Exception as e:

print("JSON解析失败,错误:", e)

except Exception as e:

print("客户端连接断开,错误:", e)

finally:

conn.close()

print("客户端已断开,等待新连接...")

程序入口

if name == "main":

esp_ip = connect_wifi()

start_websocket_server(esp_ip)

相关推荐
禅思院9 小时前
Agent记忆管理,是一场工程上的平衡艺术
前端·架构·ai编程
crary,记忆9 小时前
React Hook 浅学 2.0 - 自定义Hook
前端·react.js·前端框架
触底反弹9 小时前
🔥 React 零基础入门(下):Props + State + useEffect 生命周期深度解析
前端·react.js·面试
咖啡星人k9 小时前
GitHub Copilot 加入 Agent 浏览器:验证 Web 应用进入编码闭环
前端·人工智能·github·copilot·前端开发·ai agent
Shadow(⊙o⊙)9 小时前
高并发内存池:Part-1——定长内存池
java·前端·javascript
GuWenyue9 小时前
90%Node新手踩坑!path+fs两大内置模块完整实战,3种异步写法彻底告别回调地狱
前端
国服第二切图仔9 小时前
17-config命令 - 配置管理系统
java·前端·javascript
GuWenyue9 小时前
放弃云端API!5套技术栈实战WebGPU端侧AI,前端独立跑本地大模型,省成本还保隐私
前端·数据库·人工智能
胡萝卜术18 小时前
当大模型遇上浏览器:用 React + WebGPU 在前端跑通 DeepSeek-R1 的实战笔记
前端·javascript·面试