- 固件:新增 link_control / stream_control,覆盖运行中 Wi-Fi 断线恢复、取帧节拍、 温控与质量/帧率减载;UDP 发送增加有界重试预算与帧期限,PSRAM JPEG 直发减少拷贝。 - 协议:重组器严格校验分片布局,统计未收齐、超时与完全缺失的帧,处理帧号回绕与设备重启。 - 桌面端:视频改 latest-value 通道并前置帧时间标记,拒绝积压和慢解码的过期画面;新增 延迟标记页与 --latency-report 校准,完善录制、服务端与退出流程。 - 文档与脚本:新增硬件、开发、延迟、功耗、实时性、稳定性文档和改动记录,重写根 README; 新增构建、性能、延迟与串口长测脚本,以及长测客户端脚本。 - 构建:固定 esp-1.97.0.0 工具链,分区表随构建复制,更新 sdkconfig 与忽略规则。
51 lines
2.6 KiB
JavaScript
51 lines
2.6 KiB
JavaScript
import {readFileSync} from 'node:fs';
|
|
import assert from 'node:assert/strict';
|
|
import {test} from 'node:test';
|
|
import ts from 'typescript';
|
|
const source=readFileSync(new URL('../src/lib/latency-marker.ts',import.meta.url),'utf8');
|
|
const code=ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022}}).outputText;
|
|
const exports={};new Function('exports',code)(exports);
|
|
const {drawMarker,decodeMarker}=exports;
|
|
function image(counter,w=320,h=240) {
|
|
const pixels=new Uint8ClampedArray(w*h*4);
|
|
const ctx={canvas:{width:w,height:h},fillStyle:'',fillRect(x,y,width,height){
|
|
const c=this.fillStyle.startsWith('rgb(')?this.fillStyle.slice(4,-1).split(',').map(Number):
|
|
this.fillStyle==='#fff'?[255,255,255]:this.fillStyle==='#000'?[0,0,0]:[60,60,60];
|
|
for(let yy=Math.max(0,Math.ceil(y));yy<Math.min(h,y+height);yy++)
|
|
for(let xx=Math.max(0,Math.ceil(x));xx<Math.min(w,x+width);xx++)pixels.set([...c,255],(yy*w+xx)*4);
|
|
}};
|
|
drawMarker(ctx,counter);return pixels;
|
|
}
|
|
test('screen IDs decode at Gray-code boundaries and across counter wrap',()=>{
|
|
for(const id of [0,1,2,3,63,127,1023,2047,2048,4094,4095,4096]) {
|
|
assert.equal(decodeMarker(image(id),320,240)?.counter,id&4095);
|
|
}
|
|
});
|
|
test('camera rotation and reflection preserve the observed ID',()=>{
|
|
const original=image(1371),rotated=new Uint8ClampedArray(original.length),flipped=new Uint8ClampedArray(original.length);
|
|
for(let y=0;y<240;y++)for(let x=0;x<320;x++){
|
|
rotated.set(original.subarray((y*320+x)*4,(y*320+x)*4+4),(x*240+239-y)*4);
|
|
flipped.set(original.subarray((y*320+x)*4,(y*320+x)*4+4),(y*320+319-x)*4);
|
|
}
|
|
assert.equal(decodeMarker(rotated,240,320)?.counter,1371);
|
|
assert.equal(decodeMarker(flipped,320,240)?.counter,1371);
|
|
});
|
|
test('missing fiducials and ambiguous rolling transitions do not invent latency samples',()=>{
|
|
assert.equal(decodeMarker(new Uint8ClampedArray(320*240*4),320,240),null);
|
|
const pixels=image(123);
|
|
for(let y=90;y<152;y++)for(let x=44;x<276;x++)pixels.set([128,128,128,255],(y*320+x)*4);
|
|
assert.equal(decodeMarker(pixels,320,240),null);
|
|
});
|
|
|
|
test('washed screen primaries survive without mistaking a blue-tinted background',()=>{
|
|
const pixels=image(1371);
|
|
for(let i=0;i<pixels.length;i+=4){
|
|
const r=pixels[i],g=pixels[i+1],b=pixels[i+2];
|
|
if(r===32&&g===255&&b===32)pixels.set([190,255,208,255],i);
|
|
else if(r===32&&g===96&&b===255)pixels.set([78,205,255,255],i);
|
|
else if(r===255&&g===32&&b===255)pixels.set([255,112,255,255],i);
|
|
else if(r===60&&g===60&&b===60)pixels.set([165,181,225,255],i);
|
|
}
|
|
assert.equal(decodeMarker(pixels,320,240)?.counter,1371);
|
|
});
|