feat: 链路控制、延迟校准与长测工具,补全文档
- 固件:新增 link_control / stream_control,覆盖运行中 Wi-Fi 断线恢复、取帧节拍、 温控与质量/帧率减载;UDP 发送增加有界重试预算与帧期限,PSRAM JPEG 直发减少拷贝。 - 协议:重组器严格校验分片布局,统计未收齐、超时与完全缺失的帧,处理帧号回绕与设备重启。 - 桌面端:视频改 latest-value 通道并前置帧时间标记,拒绝积压和慢解码的过期画面;新增 延迟标记页与 --latency-report 校准,完善录制、服务端与退出流程。 - 文档与脚本:新增硬件、开发、延迟、功耗、实时性、稳定性文档和改动记录,重写根 README; 新增构建、性能、延迟与串口长测脚本,以及长测客户端脚本。 - 构建:固定 esp-1.97.0.0 工具链,分区表随构建复制,更新 sdkconfig 与忽略规则。
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
// Exercise the production StreamClient against native hardware video, while
|
||||
// deliberately stalling only this auxiliary local consumer. Adds no UDP peer.
|
||||
// Bitmap decoding is stubbed here; actual JPEG decoding is checked separately.
|
||||
import {readFileSync,writeFileSync} from 'node:fs';
|
||||
import assert from 'node:assert/strict';
|
||||
import ts from 'typescript';
|
||||
|
||||
const [logPath,outPath]=process.argv.slice(2);
|
||||
if(!logPath||!outPath)throw new Error('Use <private native debug log> <result JSON>');
|
||||
const log=readFileSync(logPath,'utf8');
|
||||
const url=[...log.matchAll(/ws:\/\/127\.0\.0\.1:\d+\/\?token=[A-Za-z0-9_-]+/g)].at(-1)?.[0];
|
||||
if(!url)throw new Error('No local video URL in the private debug log');
|
||||
const source=readFileSync(new URL('../src/lib/stream.ts',import.meta.url),'utf8');
|
||||
const compiled=ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022}}).outputText;
|
||||
const exports={};new Function('require','exports',compiled)(()=>({FRAME_HEADER_LEN:16}),exports);
|
||||
globalThis.window={setTimeout,clearTimeout};
|
||||
globalThis.createImageBitmap=async()=>({close(){}});
|
||||
const rx=new exports.StreamClient({onMessage(){},onConnection(){}});
|
||||
const delay=ms=>new Promise(resolve=>setTimeout(resolve,ms));
|
||||
try {
|
||||
rx.connect(url);
|
||||
const deadline=performance.now()+10_000;
|
||||
while(rx.counters.frames<10&&performance.now()<deadline)await delay(30);
|
||||
assert.ok(rx.counters.frames>=10,'Real native JPEG video did not arrive');
|
||||
const camera=[...rx.frames.keys()][0];
|
||||
const before=rx.latest(camera)?.frameId;
|
||||
const staleBefore=rx.counters.staleFrames;
|
||||
const blockedAt=performance.now();
|
||||
while(performance.now()-blockedAt<350) { /* synthetic consumer stall */ }
|
||||
await delay(700);
|
||||
const latest=rx.latest(camera);
|
||||
assert.ok(rx.counters.staleFrames>staleBefore,'Stale native-timestamped frames were not rejected');
|
||||
assert.ok(latest&&latest.frameId!==before,'The consumer did not recover to new video');
|
||||
assert.ok(performance.now()-latest.receivedAt<150,'Recovered image is still stale locally');
|
||||
const result={passed:true,stall_ms:350,stale_frames_rejected:rx.counters.staleFrames-staleBefore,
|
||||
frame_before:before,frame_after:latest.frameId,frames_received:rx.counters.frames,
|
||||
additional_udp_subscribers:0,jpeg_decoder_stubbed:true,
|
||||
scope:'Production StreamClient over real native WebSocket; tests queued-frame age and recovery, not physical display latency'};
|
||||
writeFileSync(outPath,JSON.stringify(result,null,2)+'\n');
|
||||
console.log(JSON.stringify(result));
|
||||
} finally {rx.close();}
|
||||
Reference in New Issue
Block a user