联网与数据交换 / 实时曲线发布 内容更新

ESP32实时曲线:用有界WebSocket流观察SHT40

原生ESP-IDF WebSocket页面显示温湿度与连接状态,限制采样、待执行广播、客户端和图表点数;用实际浏览器及本机服务核多页面、断线重连和慢消费者。

读完能做什么
得到不刷新整页的温度曲线与湿度数值;失效留空、重连清图,能解释丢样和内存预算。
开始之前
下载包为完整独立工程。先读列出的前置文章,或具备等价ESP-IDF、GPIO、局域网经验。
所需器材
指定S3板与USB数据线、Adafruit SHT40 PID4885、短线、2.4GHz WPA2私有网络、现代浏览器、可选Python主机夹具环境
版本组合
SDK:ESP-IDF5.5.5 / b774170ff46c参考板:DevKitC-1 v1.1 / WROOM-1-N8R8;8MB DIO80MHz;PSRAM不启用依赖:SDK内置esp_http_server WS;Sensirion C1.1.0
适用范围

官方ESP32-S3-DevKitC-1 v1.1 / ESP32-S3-WROOM-1-N8R8;桥接UART115200、USB单路供电、8MB DIO80MHz、不启用PSRAM;PID4885 SDA8/SCL9、100kHz、0x44;可信LAN HTTP/WS明文只读仪表盘,无认证、不暴露公网。

这篇验证到了哪一步

资料核对已通过
docs/esp32-evidence/network-systems.md#n09;固定源码/器材与具体引用。验证日期:
构建 / 配置已通过
docs/esp32-evidence/network-systems-builds.json;实际构建Exit0和源/日志/产物SHA。对象:ESP32-S3-DevKitC-1 v1.1 / WROOM-1-N8R8版本:ESP-IDF5.5.5验证日期:
仿真不适用
未执行ESP32或外设仿真。同源host与本机服务检查不冒充Wi-Fi、调度、NVS或电气结果。
本站实板待验证
无本站烧录/实体网络/传感器/灯运行记录;observations.csv仅表头,需按README逐项实测。

构建通过只说明指定环境中的代码或配置通过检查;实物功能、功耗和稳定性以对应的实板记录为准。

01 / 实时曲线

实时图需要推送,也需要允许丢中间点

如果只需几分钟读一次配置,普通HTTP请求足够;当页面连续观察传感器变化,WebSocket在一次握手后双向保留连接,可以减少轮询请求。本例只用服务器到页面的只读数据,不增加远程写入入口。

SHT40约每2秒测一次,服务器发送最新可用样本;慢网络时跳过中间条目。这个“实时”指尽量呈现当前状态,不是完整采样历史,更不承诺固定端到端延迟。

本站实际完成默认与配置网络分支构建,同源序列化/年龄检查以及真实Chrome连接本机WebSocket服务。ESP32实体Wi-Fi、多页面承载能力与传感器电气尚未实测,不从本机结果推导实际帧率或内存余量。

参考:[1] 固定HTTPD接口:WebSocket、work与socket限制[2] 固定HTTPD WS实现:同步帧发送、控制帧和长度[3] 固定HTTPD发送路径与socket等待

02 / 实时曲线

沿用指定SHT40接线,不从曲线猜传感器正常

官方v1.1/N8R8板,PID4885 VIN接3V3、GND共地、SDA接GPIO8、SCL接GPIO9,3Vo不接。SHT40固定0x44,HAL使用100kHz与有限I2C等待,C驱动1.1.0保留CRC校验。

默认Wi-Fi占位只构建;在自己的sdkconfig.private配置2.4GHz WPA2后重建/烧录,串口出现ONLINE IP与HTTP :80,再用浏览器打开http://该IP/。DNS不是本例依赖,也不要把电脑localhost填成板地址。

该服务无登录或TLS,只用于你管理的可信局域网。AP隔离/访客网会阻断互访;若需要更大部署范围,先设计认证、授权、TLS和反向代理,再做业务扩展。

  1. 安装或切换ESP-IDF正式v5.5.5,并在新终端运行export.sh或使用EIM对应环境;idf.py --version应为v5.5.5。
  2. 完整解压示例,进入含顶层CMakeLists.txt的目录;先用idf.py -B build build核默认配置。
  3. 使用README的私有sdkconfig路径进入menuconfig;不要把真实Wi-Fi、PoP或AP口令加入版本库/公开日志。
  4. 按指定模块断电接线,确认3.3V与GND,再执行idf.py -B build flash monitor;Flash/串口须对应这块板,缺实板时停在构建。

参考:[5] 官方 ESP32-S3-DevKitC-1 v1.1 用户指南与 N8R8 模组配置[6] Adafruit SHT40 PID4885:VIN/3Vo、3.3V逻辑与0x44[7] Sensirion C driver1.1.0:高精度测量、CRC与milli单位

03 / 实时曲线

逐层给出边界,不把PSRAM当无限队列

固件采样队列长度1,xQueueOverwrite复制最新Sample;HTTPD待执行广播最多1个,由atomic pending门闩保护;producer不为每帧或每客户端malloc任务。采样任务栈4096B,HTTPD配置栈6144B,另有主任务、Wi-Fi/TCP堆和SDK缓冲,不能把这两项相加当总RAM。

HTTPD max_open_sockets=4,最多3个WebSocket,其余槽供页面请求;页面响应后请求关闭连接,已有3个WS时新WS关闭。广播使用栈上256B JSON与4个fd,客户端入站应用帧超过64B或文本/二进制写入会关闭,控制帧由SDK默认处理。

HTTPD发送等待配置1秒是底层send等待参数,不是每个客户端/整轮的严格总deadline;头、payload及多次写可累计,最多3客户端依次发送,慢客户端仍会延迟其他页面。发送失败请求关闭;一个pending和最新样本队列保持应用内存有界,但不提供完全隔离的低延迟服务。

SDK名为httpd_ws_send_frame_async的API在本回调中同步完成发送;它不是返回后继续引用栈payload的异步队列。代码把调用放在HTTPD work上下文,payload在返回前有效;不使用会额外排队/拷贝的每帧异步工作接口。

参考:[1] 固定HTTPD接口:WebSocket、work与socket限制[2] 固定HTTPD WS实现:同步帧发送、控制帧和长度[3] 固定HTTPD发送路径与socket等待[4] 固定HTTPD工作控制路径和连接管理

04 / 实时曲线

数值、年龄和连接是三种状态

Sample有64位单调at和sequence。广播先dequeue,再取now,避免另一核在取时之后入队导致合法新样本被误判未来;序列化只接受valid且0..6000毫秒年龄。失效/超龄发null,不沿用上次温湿度。

湿度显示裁到0..100%,这不是重新校准传感器。浏览器把有效温度画成折线,null留间隔;横轴是接收样本次序,慢网络丢点后不再代表等时间步。当前湿度用文本显示,图表只画温度,避免把两个单位挤进一个不明坐标轴。

浏览器用performance.now单调接收年龄保留最近120点,离线或超过6秒没新帧清除当前读数,保留图中旧点作为已接收轨迹;重连时清空图,防止新旧启动序列混成一段。设备uptime不是UTC历史时间,JSON没有伪造墙上时钟。

代码 / 配置
// ---- main/stream_policy.h ----
/* SPDX-License-Identifier: MIT */
#pragma once
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <inttypes.h>
typedef struct {uint32_t sequence;int64_t at;int32_t temp_milli,rh_milli;bool valid;} Sample;
static inline bool sample_fresh(Sample s,int64_t now){return s.valid&&now>=s.at&&now-s.at<=6000;}
static inline int sample_json(char *p,size_t n,Sample s,int64_t now){
 if(sample_fresh(s,now)){
  int32_t rh=s.rh_milli<0?0:s.rh_milli>100000?100000:s.rh_milli;
  return snprintf(p,n,"{\"source\":\"esp32-sht40\",\"sequence\":%" PRIu32 ",\"uptime_ms\":%" PRIi64 ",\"valid\":true,\"temperature_c\":%.3f,\"humidity_percent\":%.3f}",s.sequence,s.at,s.temp_milli/1000.,rh/1000.);
 }
 return snprintf(p,n,"{\"source\":\"esp32-sht40\",\"sequence\":%" PRIu32 ",\"uptime_ms\":%" PRIi64 ",\"valid\":false,\"temperature_c\":null,\"humidity_percent\":null}",s.sequence,s.at);
}

参考:[1] 固定HTTPD接口:WebSocket、work与socket限制[2] 固定HTTPD WS实现:同步帧发送、控制帧和长度[7] Sensirion C driver1.1.0:高精度测量、CRC与milli单位

05 / 实时曲线

完整main:单一广播工作消费最新副本

下方为原样main。传感器任务独占I2C,主任务每2秒尝试投递一个广播work;网络未配置时采样仍可失败/成功,但不会伪造HTTP连接。

HTTPD启动后在断网期间保留实例,TCP旧连接由SDK/客户端关闭;页面按1/2/4/8/16/30秒等待重连,收到有效结构帧后才重置退避。所有ESP32无线重连和浏览器多页性能仍需实板CSV,不把“编译含这些API”当已测吞吐。

代码 / 配置
// ---- main/main.c ----
/* SPDX-License-Identifier: MIT */
#include <stdio.h>
#include <string.h>
#include <stdatomic.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "esp_http_server.h"
#include "esp_timer.h"
#include "esp_log.h"
#include "net.h"
#include "stream_policy.h"
#include "sensirion_hal_idf.h"
#include "sensirion_i2c_hal.h"
#include "sht4x_i2c.h"
static const char *TAG="N09";
static QueueHandle_t latest;
static httpd_handle_t server;
static atomic_bool pending;
extern const unsigned char index_html_start[] asm("_binary_index_html_start");
extern const unsigned char index_html_end[] asm("_binary_index_html_end");
static esp_err_t page(httpd_req_t *req){httpd_resp_set_type(req,"text/html; charset=utf-8");httpd_resp_set_hdr(req,"Cache-Control","no-store");esp_err_t e=httpd_resp_send(req,(const char *)index_html_start,index_html_end-index_html_start-1);httpd_sess_trigger_close(req->handle,httpd_req_to_sockfd(req));return e;}
static esp_err_t websocket(httpd_req_t *req){
 if(req->method==HTTP_GET){size_t n=4;int fds[4];unsigned peers=0;if(httpd_get_client_list(req->handle,&n,fds)!=ESP_OK)return ESP_FAIL;for(size_t i=0;i<n;i++)if(fds[i]!=httpd_req_to_sockfd(req)&&httpd_ws_get_fd_info(req->handle,fds[i])==HTTPD_WS_CLIENT_WEBSOCKET)peers++;if(peers>=3)return ESP_FAIL;ESP_LOGI(TAG,"WS handshake fd=%d",httpd_req_to_sockfd(req));return ESP_OK;}
 httpd_ws_frame_t f={0};esp_err_t e=httpd_ws_recv_frame(req,&f,0);if(e!=ESP_OK)return e;
 /* This dashboard is server-to-client only. Bounded inbound consumption; no application writes. */
 if(f.len>64){httpd_sess_trigger_close(req->handle,httpd_req_to_sockfd(req));return ESP_FAIL;}
 uint8_t payload[65];f.payload=payload;e=httpd_ws_recv_frame(req,&f,64);
 if(e!=ESP_OK)return e;
 if(f.type==HTTPD_WS_TYPE_TEXT||f.type==HTTPD_WS_TYPE_BINARY){httpd_sess_trigger_close(req->handle,httpd_req_to_sockfd(req));return ESP_FAIL;}
 return ESP_OK;
}
static void send_latest(void *arg){
 (void)arg;Sample s;
 if(xQueueReceive(latest,&s,0)==pdTRUE){
  /* Dequeue BEFORE taking time: a producer on the other core can enqueue between statements. */
  int64_t now=esp_timer_get_time()/1000;char payload[256];int n=sample_json(payload,sizeof(payload),s,now);
  size_t count=4;int fd[4];
  if(n>0&&n<sizeof(payload)&&httpd_get_client_list(server,&count,fd)==ESP_OK){
   httpd_ws_frame_t f={.type=HTTPD_WS_TYPE_TEXT,.payload=(uint8_t *)payload,.len=n};
   for(size_t i=0;i<count;i++)if(httpd_ws_get_fd_info(server,fd[i])==HTTPD_WS_CLIENT_WEBSOCKET){
    /* Despite _async in the name this call sends synchronously here, in HTTPD context.
       Stack payload stays alive through return; no heap job/frame per client. */
    esp_err_t e=httpd_ws_send_frame_async(server,fd[i],&f);
    if(e!=ESP_OK){ESP_LOGW(TAG,"send fd=%d error=%s -> close requested",fd[i],esp_err_to_name(e));httpd_sess_trigger_close(server,fd[i]);}
   }
  }
 }
 atomic_store(&pending,false);
}
static void sampler(void *arg){
 (void)arg;sensirion_i2c_hal_init();sht4x_init(0x44);uint32_t sequence=0;
 for(;;){int32_t t=0,rh=0;int16_t e=demo_i2c_ready()?sht4x_measure_high_precision(&t,&rh):-1;
  Sample s={.sequence=++sequence,.at=esp_timer_get_time()/1000,.temp_milli=t,.rh_milli=rh,.valid=e==0};
  xQueueOverwrite(latest,&s);ESP_LOGI(TAG,"sample=%lu sensor=%d latest-only",(unsigned long)sequence,e);vTaskDelay(pdMS_TO_TICKS(2000));}
}
void app_main(void){
 latest=xQueueCreate(1,sizeof(Sample));if(!latest){ESP_LOGE(TAG,"queue allocation failed");return;}
 net_start();if(xTaskCreate(sampler,"sht40",4096,NULL,5,NULL)!=pdPASS){ESP_LOGE(TAG,"sampler allocation failed");return;}
 for(;;){
  if(!server&&net_online()){
   httpd_config_t cfg=HTTPD_DEFAULT_CONFIG();cfg.max_open_sockets=4;cfg.lru_purge_enable=false;cfg.recv_wait_timeout=1;cfg.send_wait_timeout=1;cfg.stack_size=6144;
   esp_err_t e=httpd_start(&server,&cfg);if(e==ESP_OK){
    httpd_uri_t root={.uri="/",.method=HTTP_GET,.handler=page};httpd_uri_t ws={.uri="/ws",.method=HTTP_GET,.handler=websocket,.is_websocket=true};
    ESP_ERROR_CHECK(httpd_register_uri_handler(server,&root));ESP_ERROR_CHECK(httpd_register_uri_handler(server,&ws));
    ESP_LOGI(TAG,"HTTP :80, at most 4 accepted sockets / 3 WS clients, trusted LAN only");
   }else{server=NULL;ESP_LOGW(TAG,"httpd_start=%s",esp_err_to_name(e));}
  }
  if(server&&!atomic_exchange(&pending,true)){
   esp_err_t e=httpd_queue_work(server,send_latest,NULL);
   if(e!=ESP_OK){atomic_store(&pending,false);ESP_LOGW(TAG,"work rejected; newest sample remains bounded");}
  }
  vTaskDelay(pdMS_TO_TICKS(2000));
 }
}

参考:[1] 固定HTTPD接口:WebSocket、work与socket限制[2] 固定HTTPD WS实现:同步帧发送、控制帧和长度[3] 固定HTTPD发送路径与socket等待[4] 固定HTTPD工作控制路径和连接管理

06 / 实时曲线

完整页面:连接状态、120点与缺样

页面自包含HTML/CSS/JS,不需要CDN。温度/湿度/数据状态有文本,连接区role=status,暂停绘图按钮支持键盘和aria-pressed;暂停只停画图,不停止接收和120点裁窗。无JS时明确说明不能显示实时曲线,并给串口后路。

页面限制256字符JSON并检查来源、sequence类型/范围、valid和数值范围;“host-fixture”来源在主机测试时显式显示,不与ESP32传感器混淆。每次建立连接递增generation,旧连接事件不会覆盖新连接UI;只有一个重试定时器,pagehide清重试定时器并置空、清当前读数/旧图、关闭连接并作废事件;pageshow从浏览器后退缓存恢复时重新建立连接。

下载包中的index.html是固件EMBED_TXTFILES的真实文件。下面原样展示,不靠文章片段另维护一个不同网页。画布保留文本替代,实板触摸、视觉与长时间网络恢复仍需读者检查。

代码 / 配置
<!-- ---- main/index.html ---- -->
<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>SHT40 实时曲线</title><style>
:root{font-family:system-ui,sans-serif;color:#17324d;background:#edf4f8}body{max-width:960px;margin:auto;padding:20px}main{background:white;border-radius:16px;padding:20px}h1{font-size:clamp(24px,5vw,36px)}.status{min-height:3em}canvas{width:100%;height:280px;border:1px solid #a9bbcc;border-radius:8px}button{font:inherit;padding:12px 18px;min-height:44px;border:2px solid #1e5e9b;color:#174a77;background:white;border-radius:8px}button:focus-visible,a:focus-visible{outline:3px solid #aa5c00;outline-offset:3px}dl{display:flex;gap:24px;flex-wrap:wrap}dt{font-size:14px}dd{margin:0;font-size:22px}small{display:block;margin-top:20px;line-height:1.7}p{line-height:1.65}
</style><main><h1>SHT40 实时温度曲线</h1><p id="connection" class="status" role="status">等待连接…</p><dl><div><dt>温度</dt><dd id="temp">—</dd></div><div><dt>相对湿度</dt><dd id="rh">—</dd></div><div><dt>数据状态</dt><dd id="valid">未收到</dd></div></dl><canvas id="chart" width="900" height="280" role="img" aria-label="最近120个接收样本的温度曲线;缺失值留空。上方同时显示最新数值。"></canvas><p><button id="pause" type="button" aria-pressed="false">暂停绘图</button></p><small>最多保留120个样本;服务器每2秒采样,网络慢时跳过中间样本。曲线横轴是接收次序,不是等间隔历史记录。连接恢复后清除旧曲线;失效数据不填造数值。此页面没有历史补录。仅供你管理的可信局域网。</small><noscript>实时曲线需要JavaScript。可通过固件串口日志检查传感器与联网;本页没有无脚本伪实时数据。</noscript></main>
<script>
'use strict';
const CAPACITY=120, points=[], chart=document.getElementById('chart'), ctx=chart.getContext('2d');
const statusEl=document.getElementById('connection'), validEl=document.getElementById('valid');
let socket=null,retryTimer=null,retry=1000,paused=false,lastReceived=0,generation=0;
function draw(){
 ctx.clearRect(0,0,chart.width,chart.height);ctx.fillStyle='#17324d';ctx.font='16px system-ui';
 const values=points.filter(p=>p.t!==null).map(p=>p.t);
 if(!values.length){ctx.fillText('等待有效温度,缺失样本留空',32,50);return;}
 const lo=Math.floor(Math.min(...values))-1,hi=Math.ceil(Math.max(...values))+1;
 ctx.fillText(`${lo}–${hi} °C · 最近 ${points.length} 个接收样本`,24,26);
 ctx.strokeStyle='#1e5e9b';ctx.lineWidth=3;ctx.beginPath();let active=false;
 points.forEach((p,i)=>{if(p.t===null){active=false;return;}const x=24+i/(CAPACITY-1)*(chart.width-48),y=chart.height-30-(p.t-lo)/(hi-lo)*(chart.height-80);if(active)ctx.lineTo(x,y);else ctx.moveTo(x,y);active=true;});ctx.stroke();
}
function clearCurrent(label){validEl.textContent=label;document.getElementById('temp').textContent='—';document.getElementById('rh').textContent='—';}
function schedule(){if(retryTimer!==null)return;const delay=retry;retry=Math.min(retry*2,30000);statusEl.textContent=`连接中断;${delay/1000}秒后重试`;retryTimer=setTimeout(()=>{retryTimer=null;connect();},delay);}
function connect(){
 const own=++generation;points.length=0;lastReceived=0;clearCurrent('未收到');draw();
 const ws=new WebSocket(`${location.protocol==='https:'?'wss':'ws'}://${location.host}/ws`);socket=ws;
 ws.onopen=()=>{if(own!==generation)return;statusEl.textContent='WebSocket已连接,等待样本';};
 ws.onmessage=event=>{
  if(own!==generation||typeof event.data!=='string'||event.data.length>256)return;
  let p;try{p=JSON.parse(event.data);}catch{return;}
  if(!p||!['esp32-sht40','host-fixture'].includes(p.source)||!Number.isInteger(p.sequence)||p.sequence<0||p.sequence>4294967295||typeof p.valid!=='boolean')return;
  const good=p.valid===true&&Number.isFinite(p.temperature_c)&&p.temperature_c>=-45&&p.temperature_c<=130&&Number.isFinite(p.humidity_percent)&&p.humidity_percent>=0&&p.humidity_percent<=100;
  if(p.valid&&!good)return;
  lastReceived=performance.now();retry=1000;statusEl.textContent=`${p.source==='host-fixture'?'主机合成夹具':'ESP32 SHT40'} · 收到序号 ${p.sequence}`;
  points.push({t:good?p.temperature_c:null});if(points.length>CAPACITY)points.shift();
  if(good){validEl.textContent='有效接收';document.getElementById('temp').textContent=p.temperature_c.toFixed(2)+' °C';document.getElementById('rh').textContent=p.humidity_percent.toFixed(2)+' %';}else clearCurrent('传感器失效/超龄');
  if(!paused)draw();
 };
 ws.onclose=()=>{if(own!==generation)return;clearCurrent('连接中断');schedule();};
 ws.onerror=()=>{if(own===generation)ws.close();};
}
document.getElementById('pause').onclick=e=>{paused=!paused;e.currentTarget.setAttribute('aria-pressed',String(paused));e.currentTarget.textContent=paused?'继续绘图':'暂停绘图';if(!paused)draw();};
setInterval(()=>{if(lastReceived&&performance.now()-lastReceived>6000)clearCurrent('超过6秒无新样本');},1000);
window.addEventListener('pagehide',()=>{
 generation++;if(retryTimer!==null){clearTimeout(retryTimer);retryTimer=null;}
 const previous=socket;socket=null;previous?.close();lastReceived=0;points.length=0;
 clearCurrent('页面已暂停连接');statusEl.textContent='页面已暂停连接';draw();
});
window.addEventListener('pageshow',event=>{if(event.persisted){retry=1000;connect();}});
connect();draw();
</script></html>

参考:[1] 固定HTTPD接口:WebSocket、work与socket限制[2] 固定HTTPD WS实现:同步帧发送、控制帧和长度

07 / 实时曲线

本机服务与浏览器实际检查什么?

host_check.c直接包含stream_policy.h,8组检查覆盖6秒边界、未来时间、整32位周期不复活、单位换算、湿度裁剪、invalid/null、超龄序列化和输出缓冲不足。

fixture_server.py只监听127.0.0.1,固定websockets15.0.1,提供真实下载页面和RFC6455连接,数据标host-fixture;为缩短主机压力检查故意50Hz生成合成帧,与固件2秒采样不同。Chrome实际打开3页、观察第4连接容量拒绝、120点裁窗、无效点留空、暂停、断线重连清图和375/768/1440宽度布局。

slow=1是主机服务端显式延迟消费者的注入,不是已测ESP32 TCP阻塞;实际WS链路看到序号跳跃,主机mailbox保持1且替换计数增加。该服务端用Python实现,并未运行ESP32 httpd,因此不能证明固件send_wait_timeout时序、RTOS性能或实际无线容量。

做到这里,你应该看到
实板另开1/2/3页,记录序号、断线/重连、当前/最小内部堆和任务高水位;尝试第4连接并核关闭。慢客户端用你能控制的限速代理/暂停读取工具注入,记录而不补造结果。

参考:[1] 固定HTTPD接口:WebSocket、work与socket限制[2] 固定HTTPD WS实现:同步帧发送、控制帧和长度[3] 固定HTTPD发送路径与socket等待[8] WebSocket主机夹具15.0.1发行包信息

08 / 实时曲线

看到空图时先查哪一层?

有页面而无连接先查/ws握手、客户端槽位和Wi-Fi互访;有连接但valid=false先看SHT40错误和CRC/HAL,再断电核VIN/GND/8/9。只有旧轨迹不代表仍在收到新数据,先看连接区和最新数值状态。

容量满时关闭多余页面释放槽位;长期观测记录内部堆、最大连续块、栈高水位和重连次数,不能直接把更大队列当修复。需要持久历史请把数据写到专门存储/服务端,保留设备身份与时间,不继续扩大本页面120点。

单一HTTPD上下文使示例易核对,但慢发送仍影响其他连接;下一步可阅读队列协作和存储选型,决定是否用独立任务/网关隔离网络与记录职责。

参考:[1] 固定HTTPD接口:WebSocket、work与socket限制[2] 固定HTTPD WS实现:同步帧发送、控制帧和长度[3] 固定HTTPD发送路径与socket等待

BACK TO THE SOURCE

资料来源与版本说明

按固定官方源码核对;实际构建、同源主机检查、本机服务和实体硬件分别记录。未把预期实板步骤写成已运行结果。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。

  1. 固定HTTPD接口:WebSocket、work与socket限制
  2. 固定HTTPD WS实现:同步帧发送、控制帧和长度
  3. 固定HTTPD发送路径与socket等待
  4. 固定HTTPD工作控制路径和连接管理
  5. 官方 ESP32-S3-DevKitC-1 v1.1 用户指南与 N8R8 模组配置
  6. Adafruit SHT40 PID4885:VIN/3Vo、3.3V逻辑与0x44
  7. Sensirion C driver1.1.0:高精度测量、CRC与milli单位
  8. WebSocket主机夹具15.0.1发行包信息