开箱测试报告1. 目的
测试日期:2026-10-24
测试对象:WS8204-EVB ×2
固件:+GMR="WS8204","silent-smart","V0.1.0","V1.4.0"
测试工具:tools\ws8204_perf\ws8204_perf.exe tc01
数据目录:docs\02_测试数据\TC01_report_data
1. 目的
验证两块 WS8204-EVB 开箱后能否正常上电、串口识别、AT 固件通信、主从连接和双向小数据互传。
2. 测试环境
| PC | Windows 10/11 |
| 串口 | COM3、COM4(CH9102) |
| 串口参数 | 115200 8N1,无流控 |
| 测试工具 | C# / .NET Framework 4.8,ws8204_perf.exe |
| A 板 | COM3,MAC 54:ef:80:1a:51:d9 |
| B 板 | COM4,MAC 1a:ac:10:3d:36:e6 |
3. 测试步骤
| 1 | COM3/COM4 打开 115200 8N1 | 端口可打开 |
| 2 | 两块板发送 AT | 返回 +OK |
| 3 | 发送 AT+GMR? | 返回固件版本 |
| 4 | 发送 AT+SMAC? | 返回本机 MAC |
| 5 | A AT+SROLE=1,B AT+SROLE=0 | 角色设置成功 |
| 6 | 两块板 AT+RST | 复位并自动连接 |
| 7 | 等待 +SCONN、查询 AT+SPMAC? | 对端 MAC 正确 |
| 8 | A→B AT+SSEND=0,5 + hello | B 收到 +SRD:0,5 + hello |
| 9 | B→A AT+SSEND=0,5 + world | A 收到 +SRD:0,5 + world |
| 10 | 恢复 SROLE=0、SMUX=8 | 两块板恢复默认 |
自动化命令:
.\tools\ws8204_perf\ws8204_perf.exe tc01 --a COM3 --b COM4 ` --out "docs\02_测试数据\TC01_report_data"
4. 测试数据
4.1 设备信息
| 固件 | V0.1.0 / V1.4.0 | V0.1.0 / V1.4.0 |
| MAC | 54:ef:80:1a:51:d9 | 1a:ac:10:3d:36:e6 |
| 角色(测试时) | Client/Master | Server/Slave |
| 对端 MAC | 1a:ac:10:3d:36:e6 | 54:ef:80:1a:51:d9 |
4.2 自动化执行结果(tc01.log)
A GMR: +GMR="WS8204","silent-smart","V0.1.0","V1.4.0" B GMR: +GMR="WS8204","silent-smart","V0.1.0","V1.4.0" A MAC: 54:ef:80:1a:51:d9 B MAC: 1a:ac:10:3d:36:e6 link=True Apeer=1a:ac:10:3d:36:e6 Bpeer=54:ef:80:1a:51:d9 A->B hello: send=True recv=hello ok=True B->A world: send=True recv=world ok=True
4.3 结果 JSON(tc01_result.json)
{"pass":true,"A":"54:ef:80:1a:51:d9","B":"1a:ac:10:3d:36:e6"}4.4 COM3 解码日志
14:52:31.905 +GMR="WS8204","silent-smart","V0.1.0","V1.4.0" 14:52:32.370 +SMAC="54:ef:80:1a:51:d9" 14:52:32.835 +OK 14:52:33.278 +OK 14:52:33.741 +OK 14:52:34.244 WS8204 Ready 14:52:34.330 +SCONN:0,"1a:ac:10:3d:36:e6" 14:52:35.400 +SPMACNUM=1 14:52:35.403 +SPMAC=0,"1a:ac:10:3d:36:e6" 14:52:35.403 +OK 14:52:39.574 +SDISCONN:0,7 14:52:39.668 +SCONN:0,"1a:ac:10:3d:36:e6" 14:52:42.618 +OK 14:52:42.619 > 14:52:42.643 +SSEND:5 14:52:42.746 +SRD:0,5 14:52:42.771 +OK 14:52:43.214 +OK 14:52:43.657 +OK 14:52:44.172 WS8204 Ready
4.5 COM4 解码日志
14:52:32.137 +GMR="WS8204","silent-smart","V0.1.0","V1.4.0" 14:52:32.601 +SMAC="1a:ac:10:3d:36:e6" 14:52:33.043 +OK 14:52:33.509 +OK 14:52:34.337 +SCONN:0,"54:ef:80:1a:51:d9" 14:52:34.553 +OK 14:52:35.068 WS8204 Ready 14:52:35.677 +SPMACNUM=0 14:52:35.677 +OK 14:52:36.268 +SPMACNUM=0 14:52:36.268 +OK 14:52:36.858 +SPMACNUM=0 14:52:36.858 +OK 14:52:37.449 +SPMACNUM=0 14:52:37.450 +OK 14:52:38.041 +SPMACNUM=0 14:52:38.041 +OK 14:52:38.632 +SPMACNUM=0 14:52:38.634 +OK 14:52:39.223 +SPMACNUM=0 14:52:39.223 +OK 14:52:39.661 +SCONN:0,"54:ef:80:1a:51:d9" 14:52:39.809 +SPMACNUM=1 14:52:39.813 +SPMAC=0,"54:ef:80:1a:51:d9" 14:52:39.813 +OK 14:52:42.681 +SRD:0,5 14:52:42.705 +OK 14:52:42.705 > 14:52:42.734 +SSEND:5 14:52:42.992 +OK 14:52:43.441 +OK 14:52:44.488 +OK 14:52:45.004 WS8204 Ready
4.6 原始数据 HEX
COM3_raw.bin:
Path: D:\WorkSpace\VSCode\WS8204\docs\02_测试数据\TC01_report_data\COM3_raw.bin 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 00000000 2B 47 4D 52 3D 22 57 53 38 32 30 34 22 2C 22 73 +GMR="WS8204","s 00000010 69 6C 65 6E 74 2D 73 6D 61 72 74 22 2C 22 56 30 ilent-smart","V0 00000020 2E 31 2E 30 22 2C 22 56 31 2E 34 2E 30 22 0D 0A .1.0","V1.4.0".. 00000030 2B 53 4D 41 43 3D 22 35 34 3A 65 66 3A 38 30 3A +SMAC="54:ef:80: 00000040 31 61 3A 35 31 3A 64 39 22 0D 0A 2B 4F 4B 0D 0A 1a:51:d9"..+OK.. 00000050 2B 4F 4B 0D 0A 2B 4F 4B 0D 0A 57 53 38 32 30 34 +OK..+OK..WS8204 00000060 20 52 65 61 64 79 0D 0A 2B 53 43 4F 4E 4E 3A 30 Ready..+SCONN:0 00000070 2C 22 31 61 3A 61 63 3A 31 30 3A 33 64 3A 33 36 ,"1a:ac:10:3d:36 00000080 3A 65 36 22 0D 0A 2B 53 50 4D 41 43 4E 55 4D 3D :e6"..+SPMACNUM= 00000090 31 0D 0A 2B 53 50 4D 41 43 3D 30 2C 22 31 61 3A 1..+SPMAC=0,"1a: 000000A0 61 63 3A 31 30 3A 33 64 3A 33 36 3A 65 36 22 0D ac:10:3d:36:e6". 000000B0 0A 2B 4F 4B 0D 0A 2B 53 44 49 53 43 4F 4E 4E 3A .+OK..+SDISCONN: 000000C0 30 2C 37 0D 0A 2B 53 43 4F 4E 4E 3A 30 2C 22 31 0,7..+SCONN:0,"1 000000D0 61 3A 61 63 3A 31 30 3A 33 64 3A 33 36 3A 65 36 a:ac:10:3d:36:e6 000000E0 22 0D 0A 2B 4F 4B 0D 0A 3E 2B 53 53 45 4E 44 3A "..+OK..>+SSEND: 000000F0 35 0D 0A 2B 53 52 44 3A 30 2C 35 0D 0A 77 6F 72 5..+SRD:0,5..wor 00000100 6C 64 2B 4F 4B 0D 0A 2B 4F 4B 0D 0A 2B 4F 4B 0D ld+OK..+OK..+OK. 00000110 0A 57 53 38 32 30 34 20 52 65 61 64 79 0D 0A .WS8204 Ready..
COM4_raw.bin:
Path: D:\WorkSpace\VSCode\WS8204\docs\02_测试数据\TC01_report_data\COM4_raw.bin 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 00000000 2B 47 4D 52 3D 22 57 53 38 32 30 34 22 2C 22 73 +GMR="WS8204","s 00000010 69 6C 65 6E 74 2D 73 6D 61 72 74 22 2C 22 56 30 ilent-smart","V0 00000020 2E 31 2E 30 22 2C 22 56 31 2E 34 2E 30 22 0D 0A .1.0","V1.4.0".. 00000030 2B 53 4D 41 43 3D 22 31 61 3A 61 63 3A 31 30 3A +SMAC="1a:ac:10: 00000040 33 64 3A 33 36 3A 65 36 22 0D 0A 2B 4F 4B 0D 0A 3d:36:e6"..+OK.. 00000050 2B 4F 4B 0D 0A 2B 53 43 4F 4E 4E 3A 30 2C 22 35 +OK..+SCONN:0,"5 00000060 34 3A 65 66 3A 38 30 3A 31 61 3A 35 31 3A 64 39 4:ef:80:1a:51:d9 00000070 22 0D 0A 2B 4F 4B 0D 0A 57 53 38 32 30 34 20 52 "..+OK..WS8204 R 00000080 65 61 64 79 0D 0A 2B 53 50 4D 41 43 4E 55 4D 3D eady..+SPMACNUM= 00000090 30 0D 0A 2B 4F 4B 0D 0A 2B 53 50 4D 41 43 4E 55 0..+OK..+SPMACNU 000000A0 4D 3D 30 0D 0A 2B 4F 4B 0D 0A 2B 53 50 4D 41 43 M=0..+OK..+SPMAC 000000B0 4E 55 4D 3D 30 0D 0A 2B 4F 4B 0D 0A 2B 53 50 4D NUM=0..+OK..+SPM 000000C0 41 43 4E 55 4D 3D 30 0D 0A 2B 4F 4B 0D 0A 2B 53 ACNUM=0..+OK..+S 000000D0 50 4D 41 43 4E 55 4D 3D 30 0D 0A 2B 4F 4B 0D 0A PMACNUM=0..+OK.. 000000E0 2B 53 50 4D 41 43 4E 55 4D 3D 30 0D 0A 2B 4F 4B +SPMACNUM=0..+OK 000000F0 0D 0A 2B 53 50 4D 41 43 4E 55 4D 3D 30 0D 0A 2B ..+SPMACNUM=0..+ 00000100 4F 4B 0D 0A 2B 53 43 4F 4E 4E 3A 30 2C 22 35 34 OK..+SCONN:0,"54 00000110 3A 65 66 3A 38 30 3A 31 61 3A 35 31 3A 64 39 22 :ef:80:1a:51:d9" 00000120 0D 0A 2B 53 50 4D 41 43 4E 55 4D 3D 31 0D 0A 2B ..+SPMACNUM=1..+ 00000130 53 50 4D 41 43 3D 30 2C 22 35 34 3A 65 66 3A 38 SPMAC=0,"54:ef:8 00000140 30 3A 31 61 3A 35 31 3A 64 39 22 0D 0A 2B 4F 4B 0:1a:51:d9"..+OK 00000150 0D 0A 2B 53 52 44 3A 30 2C 35 0D 0A 68 65 6C 6C ..+SRD:0,5..hell 00000160 6F 2B 4F 4B 0D 0A 3E 2B 53 53 45 4E 44 3A 35 0D o+OK..>+SSEND:5. 00000170 0A 2B 4F 4B 0D 0A 2B 4F 4B 0D 0A 2B 4F 4B 0D 0A .+OK..+OK..+OK.. 00000180 57 53 38 32 30 34 20 52 65 61 64 79 0D 0A WS8204 Ready..
4.7 数据文件与 SHA-256
| tc01.log | 308 | 88b72fbead7081f95a60054860d94101f783b94a112ee634bd905c4088f3fcfb |
| tc01_result.json | 64 | 5567431dfd335745b16da9a121a8be3a2d7eb71110a64f608ab942924c0e3ceb |
| COM3_decoded.log | 547 | bff3c3054bf34a6a1edab1d3988157096687c42b89bc0b9714b478175198e660 |
| COM4_decoded.log | 827 | 57246e9afeaf79b10f7ce648b7a16645d90794c9070edf6fb9a10cae094a6e75 |
| COM3_raw.bin | 287 | 024ac82a0f763ecd442b203c728d798542d23ce22b5061333f6a4c1bac5c4531 |
| COM4_raw.bin | 398 | da47b0551554688c72fc2b226e88838307fc38bfe01d174b0d68a1267b92424e |
| console.txt | 784 | 707070e0f4e5f49cecef104d8690246c27ebdf4b0f2e59a039341bc014cdecae |
5. 结果判定
| COM3 AT | +OK | +OK | PASS |
| COM4 AT | +OK | +OK | PASS |
| 固件版本 | V1.4.0 | V1.4.0 | PASS |
| A 角色 | Client/Master | SROLE=1 | PASS |
| B 角色 | Server/Slave | SROLE=0 | PASS |
| 自动连接 | +SCONN + 对端 MAC | 约 1 s 内连接 | PASS |
| A→B hello | 5 B 一致 | +SRD:0,5 + hello | PASS |
| B→A world | 5 B 一致 | +SRD:0,5 + world | PASS |
| 恢复默认 | SROLE=0、SMUX=8 | 已恢复 | PASS |
6. 结论
TC-01 PASS。 两块板开箱可用,串口、固件、主从连接和 AT 双向小数据均正常。
7. 测试脚本源代码
以下为本次 TC-01 使用的测试工具完整源代码与编译脚本,保存在 tools\ws8204_perf。报告生成脚本为 tools\build_tc01_report.ps1。
点击展开:build.ps1$ErrorActionPreference = "Stop"
$root = Split-Path -Parent $MyInvocation.MyCommand.Path
$csc = Join-Path $env:WINDIR "Microsoft.NET\Framework64\v4.0.30319\csc.exe"
if (-not (Test-Path $csc)) { throw "csc.exe not found: $csc" }
$out = Join-Path $root "ws8204_perf.exe"
$src = @(
(Join-Path $root "FrameCodec.cs"),
(Join-Path $root "AtPort.cs"),
(Join-Path $root "Program.cs")
)
& $csc /nologo /optimize+ /target:exe /out:$out /r:System.dll /r:System.Core.dll $src
if ($LASTEXITCODE -ne 0) { throw "compile failed: $LASTEXITCODE" }
Write-Output ("built: " + $out)
Get-FileHash $out -Algorithm SHA256 | Format-List点击展开:FrameCodec.csusing System;
using System.Collections.Generic;
namespace Ws8204Perf
{
public class Frame
{
public byte Type;
public byte Flags;
public uint Seq;
public ulong Timestamp;
public byte[] Payload;
public bool CrcOk;
public int WireLength;
}
public static class FrameCodec
{
public const int HeaderLen = 26;
public const byte TypeData = 1;
public const byte TypePing = 2;
public const byte TypePong = 3;
public const byte TypeFin = 4;
private static readonly byte[] Magic = new byte[] { 0x57, 0x53, 0x38, 0x32 }; // WS82
public static byte[] Build(byte type, uint seq, ulong timestamp, byte[] payload)
{
if (payload == null) payload = new byte[0];
if (payload.Length > 60000) throw new ArgumentOutOfRangeException("payload");
byte[] buf = new byte[HeaderLen + payload.Length];
Buffer.BlockCopy(Magic, 0, buf, 0, 4);
buf[4] = 1; // version
buf[5] = type;
buf[6] = 0; // flags
buf[7] = (byte)HeaderLen;
WriteU16(buf, 8, (ushort)payload.Length);
WriteU32(buf, 10, seq);
WriteU64(buf, 14, timestamp);
WriteU32(buf, 22, 0); // crc placeholder
Buffer.BlockCopy(payload, 0, buf, HeaderLen, payload.Length);
uint crc = Crc32(buf, 0, HeaderLen - 4, buf, HeaderLen, payload.Length);
WriteU32(buf, 22, crc);
return buf;
}
public static bool TryParse(byte[] buf, int offset, int count, out Frame frame, out int consumed)
{
frame = null;
consumed = 0;
if (buf == null || count < HeaderLen) return false;
if (buf[offset] != Magic[0] || buf[offset + 1] != Magic[1] ||
buf[offset + 2] != Magic[2] || buf[offset + 3] != Magic[3]) return false;
int payloadLen = ReadU16(buf, offset + 8);
int total = HeaderLen + payloadLen;
if (count < total) return false;
uint expected = ReadU32(buf, offset + 22);
uint actual = Crc32(buf, offset, HeaderLen - 4, buf, offset + HeaderLen, payloadLen);
Frame f = new Frame();
f.Type = buf[offset + 5];
f.Flags = buf[offset + 6];
f.Seq = ReadU32(buf, offset + 10);
f.Timestamp = ReadU64(buf, offset + 14);
f.Payload = new byte[payloadLen];
if (payloadLen > 0) Buffer.BlockCopy(buf, offset + HeaderLen, f.Payload, 0, payloadLen);
f.CrcOk = (expected == actual);
f.WireLength = total;
frame = f;
consumed = total;
return true;
}
public static byte[] MakePayload(int length, uint seq, uint seed)
{
byte[] p = new byte[length];
uint x = seed ^ (seq * 2654435761u) ^ 0x9E3779B9u;
if (x == 0) x = 0x12345678u;
for (int i = 0; i < length; i++)
{
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
p[i] = (byte)(x & 0xFF);
}
return p;
}
public static uint Crc32(byte[] a, int ao, int alen, byte[] b, int bo, int blen)
{
uint crc = 0xFFFFFFFFu;
for (int i = 0; i < alen; i++) crc = Crc32Update(crc, a[ao + i]);
for (int i = 0; i < blen; i++) crc = Crc32Update(crc, b[bo + i]);
return ~crc;
}
private static uint Crc32Update(uint crc, byte data)
{
crc ^= data;
for (int k = 0; k < 8; k++)
crc = ((crc & 1) != 0) ? (crc >> 1) ^ 0xEDB88320u : (crc >> 1);
return crc;
}
public static void WriteU16(byte[] b, int o, ushort v)
{
b[o] = (byte)(v >> 8); b[o + 1] = (byte)v;
}
public static void WriteU32(byte[] b, int o, uint v)
{
b[o] = (byte)(v >> 24); b[o + 1] = (byte)(v >> 16);
b[o + 2] = (byte)(v >> 8); b[o + 3] = (byte)v;
}
public static void WriteU64(byte[] b, int o, ulong v)
{
WriteU32(b, o, (uint)(v >> 32));
WriteU32(b, o + 4, (uint)v);
}
public static ushort ReadU16(byte[] b, int o)
{
return (ushort)((b[o] << 8) | b[o + 1]);
}
public static uint ReadU32(byte[] b, int o)
{
return ((uint)b[o] << 24) | ((uint)b[o + 1] << 16) | ((uint)b[o + 2] << 8) | b[o + 3];
}
public static ulong ReadU64(byte[] b, int o)
{
return ((ulong)ReadU32(b, o) << 32) | ReadU32(b, o + 4);
}
}
public class RawFrameParser
{
private readonly List<byte> _buf = new List<byte>();
public long GarbageBytes;
public long BadCrc;
public long Frames;
public long PayloadBytes;
public List<Frame> Feed(byte[] data)
{
List<Frame> result = new List<Frame>();
if (data != null && data.Length > 0) _buf.AddRange(data);
int search = 0;
while (true)
{
int idx = FindMagic(search);
if (idx < 0)
{
int keep = _buf.Count >= 3 ? 3 : _buf.Count;
GarbageBytes += _buf.Count - keep;
if (_buf.Count > keep) _buf.RemoveRange(0, _buf.Count - keep);
break;
}
if (idx > 0)
{
GarbageBytes += idx;
_buf.RemoveRange(0, idx);
search = 0;
}
if (_buf.Count < FrameCodec.HeaderLen) break;
int payloadLen = FrameCodec.ReadU16(_buf.ToArray(), 8);
int total = FrameCodec.HeaderLen + payloadLen;
if (payloadLen < 0 || payloadLen > 60000)
{
GarbageBytes++;
_buf.RemoveAt(0);
search = 0;
continue;
}
if (_buf.Count < total) break;
byte[] arr = _buf.ToArray();
Frame f;
int consumed;
if (FrameCodec.TryParse(arr, 0, arr.Length, out f, out consumed))
{
result.Add(f);
Frames++;
if (f.CrcOk) PayloadBytes += f.Payload.Length; else BadCrc++;
_buf.RemoveRange(0, consumed);
search = 0;
}
else
{
GarbageBytes++;
_buf.RemoveAt(0);
search = 0;
}
}
return result;
}
private int FindMagic(int start)
{
for (int i = start; i + 3 < _buf.Count; i++)
{
if (_buf[i] == 0x57 && _buf[i + 1] == 0x53 && _buf[i + 2] == 0x38 && _buf[i + 3] == 0x32)
return i;
}
return -1;
}
}
}点击展开:AtPort.csusing System;
using System.Collections.Generic;
using System.IO;
using System.IO.Ports;
using System.Text;
using System.Threading;
using System.Diagnostics;
namespace Ws8204Perf
{
public class AtPort : IDisposable
{
public string PortName;
public int Baud;
public string LocalMac = "";
public string PeerMac = "";
public int ConnId = -1;
public bool Connected = false;
public bool HasError = false;
public bool LogRaw = true;
public long TxBytes;
public long RxBytes;
public long TxFrames;
public long RxFrames;
private SerialPort _sp;
private Thread _reader;
private volatile bool _running;
private readonly object _writeLock = new object();
private readonly object _respLock = new object();
private readonly List<string> _resp = new List<string>();
private AutoResetEvent _lineEvent = new AutoResetEvent(false);
private ManualResetEvent _promptEvent = new ManualResetEvent(false);
private ManualResetEvent _sendDoneEvent = new ManualResetEvent(false);
private ManualResetEvent _errorEvent = new ManualResetEvent(false);
private ManualResetEvent _readyEvent = new ManualResetEvent(false);
private readonly Queue<byte[]> _rxQ = new Queue<byte[]>();
private readonly object _rxLock = new object();
private ManualResetEvent _rxEvent = new ManualResetEvent(false);
private readonly StringBuilder _line = new StringBuilder();
private int _payloadRemaining;
private int _payloadConnId;
private readonly List<byte> _payload = new List<byte>();
private volatile bool _transparent;
private FileStream _raw;
private StreamWriter _log;
private readonly object _logLock = new object();
private int _logWrites;
public AtPort(string portName, int baud, string logDir, bool logRaw)
{
PortName = portName;
Baud = baud;
LogRaw = logRaw;
_sp = new SerialPort(portName, baud, Parity.None, 8, StopBits.One);
_sp.ReadTimeout = 50;
_sp.WriteTimeout = 2000;
_sp.DtrEnable = false;
_sp.RtsEnable = false;
_sp.Open();
if (logDir != null)
{
Directory.CreateDirectory(logDir);
if (LogRaw)
_raw = new FileStream(Path.Combine(logDir, portName + "_raw.bin"), FileMode.Create, FileAccess.Write, FileShare.Read);
_log = new StreamWriter(Path.Combine(logDir, portName + "_decoded.log"), false, Encoding.UTF8);
_log.AutoFlush = true;
}
_running = true;
_reader = new Thread(ReaderLoop);
_reader.IsBackground = true;
_reader.Start();
}
public SerialPort Serial { get { return _sp; } }
public void WriteLine(string cmd)
{
byte[] b = Encoding.ASCII.GetBytes(cmd + "\r\n");
_sp.Write(b, 0, b.Length);
}
public bool WriteRaw(byte[] data)
{
lock (_writeLock)
{
try
{
_sp.Write(data, 0, data.Length);
TxBytes += data.Length;
return true;
}
catch (Exception ex) { LogLine("WRITE_RAW_ERROR " + ex.Message); return false; }
}
}
public string[] SendCommand(string cmd, int timeoutMs, int settleMs)
{
lock (_writeLock)
{
try
{
lock (_respLock) { _resp.Clear(); }
_lineEvent.Reset();
_errorEvent.Reset();
WriteLine(cmd);
_lineEvent.WaitOne(timeoutMs, false);
Thread.Sleep(settleMs);
lock (_respLock) { return _resp.ToArray(); }
}
catch (Exception ex)
{
LogLine("CMD_ERROR " + cmd + " " + ex.Message);
return new string[0];
}
}
}
public string[] Cmd(string cmd)
{
return SendCommand(cmd, 1000, 200);
}
public bool SendData(int connId, byte[] payload, int timeoutMs)
{
lock (_writeLock)
{
try
{
_promptEvent.Reset();
_sendDoneEvent.Reset();
_errorEvent.Reset();
WriteLine(string.Format("AT+SSEND={0},{1}", connId, payload.Length));
int idx = WaitHandle.WaitAny(new WaitHandle[] { _promptEvent, _errorEvent }, timeoutMs, false);
if (idx != 0) return false;
try
{
_sp.Write(payload, 0, payload.Length);
}
catch (Exception ex)
{
LogLine("SEND_PAYLOAD_ERROR " + ex.Message);
TryCompletePending(payload.Length);
return false;
}
TxBytes += payload.Length;
TxFrames++;
idx = WaitHandle.WaitAny(new WaitHandle[] { _sendDoneEvent, _errorEvent }, timeoutMs, false);
if (idx != 0)
{
TryCompletePending(0);
return false;
}
return true;
}
catch (Exception ex)
{
LogLine("SEND_DATA_ERROR " + ex.Message);
return false;
}
}
}
private void TryCompletePending(int alreadySent)
{
try
{
int len = 256 - alreadySent;
if (len > 0) _sp.Write(new byte[len], 0, len);
Thread.Sleep(200);
SendCommand("AT+RST", 1000, 100);
}
catch { }
}
public bool EnterTransparent(int timeoutMs)
{
_transparent = false;
string[] r = SendCommand("AT+SSEND", timeoutMs, 200);
if (!HasOk(r)) return false;
Thread.Sleep(100);
_transparent = true;
return true;
}
public bool ExitTransparent()
{
if (!_transparent) return true;
for (int attempt = 0; attempt < 3; attempt++)
{
try
{
Thread.Sleep(120);
lock (_writeLock) { _sp.Write("+++"); }
Thread.Sleep(350);
_transparent = false;
string[] r = SendCommand("AT", 800, 100);
if (HasOk(r)) return true;
}
catch { }
try
{
Reopen(Baud);
_transparent = false;
string[] r = SendCommand("AT", 800, 100);
if (HasOk(r)) return true;
}
catch { }
}
_transparent = false;
return false;
}
public void ResetAndWait(int timeoutMs)
{
Connected = false;
PeerMac = "";
ConnId = -1;
_readyEvent.Reset();
SendCommand("AT+RST", 1200, 100);
Thread.Sleep(300);
_readyEvent.WaitOne(timeoutMs, false);
Thread.Sleep(300);
}
public bool WaitConnected(int timeoutMs)
{
Stopwatch sw = Stopwatch.StartNew();
while (sw.ElapsedMilliseconds < timeoutMs)
{
string[] r = SendCommand("AT+SPMAC?", 600, 250);
if (Contains(r, "+SPMACNUM=1"))
{
ParsePeerFromLines(r);
return true;
}
Thread.Sleep(300);
}
return false;
}
public string Info(string cmd)
{
string[] r = SendCommand(cmd, 1000, 250);
return string.Join(" | ", r);
}
public bool ChangeBaud(int newBaud)
{
if (newBaud == Baud) return true;
string[] r = SendCommand("AT+UART=" + newBaud.ToString(), 1200, 250);
if (!HasOk(r)) return false;
SendCommand("AT+RST", 1200, 100);
Thread.Sleep(600);
Reopen(newBaud);
Thread.Sleep(400);
string[] t = SendCommand("AT", 800, 100);
if (HasOk(t)) return true;
// Recovery: scan common rates, then set back to 115200.
int[] rates = new int[] { 115200, 460800, 921600, 1000000, 1500000, 2000000, newBaud };
foreach (int rate in rates)
{
try
{
Reopen(rate);
Thread.Sleep(200);
string[] x = SendCommand("AT", 500, 100);
if (HasOk(x))
{
SendCommand("AT+UART=115200", 1000, 200);
SendCommand("AT+RST", 1000, 100);
Thread.Sleep(500);
Reopen(115200);
return false;
}
}
catch { }
}
return false;
}
public void Reopen(int baud)
{
StopReader();
try { if (_sp.IsOpen) _sp.Close(); } catch { }
Thread.Sleep(300);
_sp = new SerialPort(PortName, baud, Parity.None, 8, StopBits.One);
_sp.ReadTimeout = 50;
_sp.WriteTimeout = 2000;
_sp.DtrEnable = false;
_sp.RtsEnable = false;
Exception last = null;
for (int i = 0; i < 6; i++)
{
try { _sp.Open(); last = null; break; }
catch (Exception ex) { last = ex; Thread.Sleep(300); }
}
if (last != null) throw last;
Baud = baud;
_running = true;
_reader = new Thread(ReaderLoop);
_reader.IsBackground = true;
_reader.Start();
}
public byte[] TakeRx(int timeoutMs)
{
if (!_rxEvent.WaitOne(timeoutMs, false)) return null;
lock (_rxLock)
{
if (_rxQ.Count == 0) return new byte[0];
byte[] b = _rxQ.Dequeue();
if (_rxQ.Count == 0) _rxEvent.Reset();
return b;
}
}
public int PendingRx
{
get { lock (_rxLock) { return _rxQ.Count; } }
}
private void EnqueueRx(byte[] data)
{
if (data == null || data.Length == 0) return;
lock (_rxLock)
{
_rxQ.Enqueue(data);
_rxEvent.Set();
}
RxBytes += data.Length;
}
private void ReaderLoop()
{
byte[] buf = new byte[8192];
while (_running)
{
int n = 0;
try { n = _sp.Read(buf, 0, buf.Length); }
catch (TimeoutException) { continue; }
catch (Exception) { break; }
if (n <= 0) continue;
lock (_logLock)
{
if (_raw != null)
{
try { _raw.Write(buf, 0, n); _raw.Flush(); } catch { }
}
}
if (_transparent)
{
byte[] chunk = new byte[n];
Buffer.BlockCopy(buf, 0, chunk, 0, n);
EnqueueRx(chunk);
continue;
}
for (int i = 0; i < n; i++)
{
try { ProcessByte(buf[i]); }
catch (Exception ex) { LogLine("PARSER_ERROR " + ex.GetType().Name + " " + ex.Message); }
}
}
}
private void ProcessByte(byte b)
{
if (_payloadRemaining > 0)
{
_payload.Add(b);
_payloadRemaining--;
if (_payloadRemaining == 0)
{
EnqueueRx(_payload.ToArray());
_payload.Clear();
}
return;
}
if (b == (byte)'>')
{
_line.Clear();
_promptEvent.Set();
LogLine(">");
return;
}
if (b == (byte)'\n')
{
string line = _line.ToString();
_line.Length = 0;
if (line.EndsWith("\r")) line = line.Substring(0, line.Length - 1);
HandleLine(line);
return;
}
_line.Append((char)b);
}
private void HandleLine(string line)
{
LogLine(line);
if (line.StartsWith("+SRD:"))
{
int comma = line.IndexOf(',');
if (comma > 0)
{
int cid;
int len;
string a = line.Substring(5, comma - 5);
string b = line.Substring(comma + 1);
if (int.TryParse(a, out cid) && int.TryParse(b, out len) && len >= 0 && len <= 8192)
{
_payloadConnId = cid;
_payloadRemaining = len;
_payload.Clear();
if (len == 0) EnqueueRx(new byte[0]);
return;
}
}
return;
}
if (line.StartsWith("+SSEND:"))
{
_sendDoneEvent.Set();
return;
}
if (line.StartsWith("+SCONN:"))
{
int firstComma = line.IndexOf(',');
int q1 = line.IndexOf('"');
int q2 = line.LastIndexOf('"');
if (firstComma > 0 && q1 >= 0 && q2 > q1)
{
ConnId = int.Parse(line.Substring(7, firstComma - 7));
PeerMac = line.Substring(q1 + 1, q2 - q1 - 1);
Connected = true;
_readyEvent.Set();
}
return;
}
if (line.StartsWith("+SDISCONN"))
{
Connected = false;
return;
}
if (line.IndexOf("WS8204 Ready", StringComparison.OrdinalIgnoreCase) >= 0)
{
_readyEvent.Set();
return;
}
if (line.StartsWith("+ERROR"))
{
HasError = true;
_errorEvent.Set();
}
lock (_respLock) { _resp.Add(line); }
_lineEvent.Set();
}
private void ParsePeerFromLines(string[] lines)
{
foreach (string line in lines)
{
if (line.StartsWith("+SPMAC="))
{
int q1 = line.IndexOf('"');
int q2 = line.LastIndexOf('"');
if (q1 >= 0 && q2 > q1)
{
PeerMac = line.Substring(q1 + 1, q2 - q1 - 1);
Connected = true;
}
}
}
}
public static bool HasOk(string[] lines)
{
return Contains(lines, "+OK");
}
public static bool Contains(string[] lines, string token)
{
if (lines == null) return false;
foreach (string l in lines)
if (l != null && l.IndexOf(token, StringComparison.OrdinalIgnoreCase) >= 0) return true;
return false;
}
public static string GetValue(string[] lines, string prefix)
{
if (lines == null) return "";
foreach (string l in lines)
if (l != null && l.StartsWith(prefix)) return l;
return "";
}
private void LogLine(string line)
{
lock (_logLock)
{
if (_log == null) return;
try
{
_log.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff") + " " + line);
if (++_logWrites % 100 == 0) _log.Flush();
}
catch { }
}
}
private void StopReader()
{
_running = false;
try { if (_sp != null && _sp.IsOpen) _sp.Close(); } catch { }
try { if (_reader != null && _reader.IsAlive) _reader.Join(1000); } catch { }
Thread.Sleep(200);
}
public void Dispose()
{
StopReader();
try { if (_sp != null) _sp.Dispose(); } catch { }
lock (_logLock)
{
try { if (_raw != null) { _raw.Flush(); _raw.Close(); } } catch { }
try { if (_log != null) { _log.Flush(); _log.Close(); } } catch { }
}
}
}
}点击展开:Program.csusing System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
namespace Ws8204Perf
{
internal class Args
{
private readonly Dictionary<string, string> _d = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
public Args(string[] args)
{
for (int i = 1; i < args.Length; i++)
{
string a = args[i];
if (a.StartsWith("--"))
{
string k = a.Substring(2);
string v = "1";
if (i + 1 < args.Length && !args[i + 1].StartsWith("--")) { v = args[++i]; }
_d[k] = v;
}
}
}
public string Get(string k, string def) { string v; return _d.TryGetValue(k, out v) ? v : def; }
public int GetInt(string k, int def) { string v; int x; return _d.TryGetValue(k, out v) && int.TryParse(v, out x) ? x : def; }
public bool Has(string k) { return _d.ContainsKey(k); }
}
internal class RxStat
{
public long Frames;
public long Bytes;
public long CrcErrors;
public long Garbage;
public long SeqGaps;
public long Duplicates;
public uint LastSeq;
public bool HasLast;
public void Add(Frame f)
{
if (!f.CrcOk) { CrcErrors++; return; }
Frames++;
Bytes += f.Payload == null ? 0 : f.Payload.Length;
if (HasLast)
{
uint expected = LastSeq + 1;
if (f.Seq == LastSeq) Duplicates++;
else if (f.Seq != expected)
{
if (f.Seq > expected) SeqGaps += (long)(f.Seq - expected);
else SeqGaps += 1;
}
}
LastSeq = f.Seq;
HasLast = true;
}
}
internal class PerSecond
{
public int Second;
public long ATx, ARx, BTx, BRx;
public long AFrames, BFrames;
public long ACrc, BCrc;
}
internal static class Program
{
private static string Root = @"D:\WorkSpace\VSCode\WS8204";
private static string OutDir = "";
private static string RunStamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
private static int Main(string[] args)
{
AppDomain.CurrentDomain.UnhandledException += delegate(object sender, UnhandledExceptionEventArgs e)
{
try
{
string dir = Path.Combine(Root, "docs", "02_测试数据");
Directory.CreateDirectory(dir);
File.AppendAllText(Path.Combine(dir, "crash.log"),
DateTime.Now.ToString("s") + " " + (e.ExceptionObject == null ? "null" : e.ExceptionObject.ToString()) + "\r\n");
}
catch { }
};
if (args.Length == 0)
{
Console.WriteLine("ws8204_perf.exe <info|tc01|config|perf|latency|file|soak|restore|selftest> [options]");
return 1;
}
string cmd = args[0].ToLowerInvariant();
Args a = new Args(args);
try
{
switch (cmd)
{
case "selftest": return SelfTest();
case "info": return CmdInfo(a);
case "tc01": return CmdTc01(a);
case "config": return CmdConfig(a);
case "perf": return CmdPerf(a);
case "soak": return CmdSoak(a);
case "latency": return CmdLatency(a);
case "file": return CmdFile(a);
case "restore": return CmdRestore(a);
default: Console.WriteLine("unknown command: " + cmd); return 1;
}
}
catch (Exception ex)
{
Console.WriteLine("ERROR: " + ex.GetType().Name + ": " + ex.Message);
Console.WriteLine(ex.StackTrace);
return 2;
}
}
private static string MakeOut(Args a, string tag)
{
string d = a.Get("out", null);
if (string.IsNullOrEmpty(d))
d = Path.Combine(Root, "docs", "02_测试数据", tag + "_" + RunStamp);
Directory.CreateDirectory(d);
OutDir = d;
return d;
}
private static int CmdInfo(Args a)
{
string d = MakeOut(a, "INFO");
AtPort aa = Open(a.Get("a", "COM3"), 115200, d, true);
AtPort bb = Open(a.Get("b", "COM4"), 115200, d, true);
try
{
string[] cmds = new string[] {
"AT", "AT+GMR?", "AT+SROLE?", "AT+SNAME?", "AT+SMAC?",
"AT+UART?", "AT+SMUX?", "AT+SMTU?", "AT+SCONPARA?",
"AT+SPHY?", "AT+ECHO?", "AT+SRFPWR?", "AT+SPMAC?"
};
StringBuilder sb = new StringBuilder();
foreach (string c in cmds)
{
string ra = string.Join(" | ", aa.Cmd(c));
string rb = string.Join(" | ", bb.Cmd(c));
sb.AppendLine("## " + c);
sb.AppendLine("A(" + aa.PortName + "): " + ra);
sb.AppendLine("B(" + bb.PortName + "): " + rb);
Console.WriteLine(c + " A=" + ra + " B=" + rb);
}
File.WriteAllText(Path.Combine(d, "info.md"), sb.ToString(), Encoding.UTF8);
}
finally { aa.Dispose(); bb.Dispose(); }
return 0;
}
private static int CmdRestore(Args a)
{
string d = MakeOut(a, "RESTORE");
AtPort aa = Open(a.Get("a", "COM3"), 115200, d, true);
AtPort bb = Open(a.Get("b", "COM4"), 115200, d, true);
try
{
Console.WriteLine("restore A: " + string.Join(" | ", aa.Cmd("AT+RESTORE")));
Console.WriteLine("restore B: " + string.Join(" | ", bb.Cmd("AT+RESTORE")));
Thread.Sleep(1500);
aa.Reopen(115200);
bb.Reopen(115200);
Thread.Sleep(1000);
Console.WriteLine("A role=" + string.Join(" | ", aa.Cmd("AT+SROLE?")) + " smux=" + string.Join(" | ", aa.Cmd("AT+SMUX?")));
Console.WriteLine("B role=" + string.Join(" | ", bb.Cmd("AT+SROLE?")) + " smux=" + string.Join(" | ", bb.Cmd("AT+SMUX?")));
}
finally { aa.Dispose(); bb.Dispose(); }
return 0;
}
private static AtPort Open(string port, int baud, string dir, bool raw)
{
return new AtPort(port, baud, dir, raw);
}
private static string MacOf(AtPort p)
{
string[] r = p.Cmd("AT+SMAC?");
foreach (string l in r)
{
int q1 = l.IndexOf('"');
int q2 = l.LastIndexOf('"');
if (q1 >= 0 && q2 > q1) return l.Substring(q1 + 1, q2 - q1 - 1);
}
return "";
}
private static bool PrepareLink(AtPort a, AtPort b, int baud, int roleA, int roleB,
int mtu, int interval, int phy, int format, int smux, int txPower, bool reset)
{
if (a.Baud != baud) if (!a.ChangeBaud(baud)) Console.WriteLine("WARN: A baud change failed");
if (b.Baud != baud) if (!b.ChangeBaud(baud)) Console.WriteLine("WARN: B baud change failed");
a.Cmd("AT+ECHO=0"); b.Cmd("AT+ECHO=0");
a.Cmd("AT+SROLE=" + roleA); b.Cmd("AT+SROLE=" + roleB);
a.Cmd("AT+SMUX=" + smux); b.Cmd("AT+SMUX=" + smux);
a.Cmd("AT+SCONPARA=" + interval + ",0,500");
b.Cmd("AT+SCONPARA=" + interval + ",0,500");
a.Cmd("AT+SPHY=" + phy + "," + format);
b.Cmd("AT+SPHY=" + phy + "," + format);
a.Cmd("AT+SRFPWR=" + txPower); b.Cmd("AT+SRFPWR=" + txPower);
if (reset)
{
a.ResetAndWait(6000); b.ResetAndWait(6000);
bool ca = a.WaitConnected(10000);
bool cb = b.WaitConnected(10000);
Thread.Sleep(3000);
Console.WriteLine("connect: A=" + ca + " peer=" + a.PeerMac + " B=" + cb + " peer=" + b.PeerMac);
return ca && cb;
}
return true;
}
private static bool EnsureDefaultLink(AtPort a, AtPort b, bool transparent)
{
int smux = transparent ? 1 : 8;
return PrepareLink(a, b, 115200, 1, 0, 248, 160, 0, 1, smux, 6, true);
}
private static bool PrepareBasicLink(AtPort a, AtPort b)
{
a.Cmd("AT+ECHO=0"); b.Cmd("AT+ECHO=0");
a.Cmd("AT+SROLE=1"); b.Cmd("AT+SROLE=0");
a.ResetAndWait(6000); b.ResetAndWait(6000);
bool ca = a.WaitConnected(10000);
bool cb = b.WaitConnected(10000);
Thread.Sleep(2500);
Console.WriteLine("basic link: A=" + ca + " peer=" + a.PeerMac + " B=" + cb + " peer=" + b.PeerMac);
return ca && cb;
}
private static int SelfTest()
{
Console.WriteLine("selftest: frame codec + raw parser");
List<byte> stream = new List<byte>();
for (uint i = 0; i < 100; i++)
{
byte[] f = FrameCodec.Build(FrameCodec.TypeData, i, (ulong)i * 1000, FrameCodec.MakePayload(200, i, 12345));
stream.AddRange(f);
if (i == 50) { stream.Add(0x00); stream.Add(0x11); stream.Add(0x57); }
}
RawFrameParser p = new RawFrameParser();
List<Frame> all = new List<Frame>();
byte[] arr = stream.ToArray();
int pos = 0;
Random rnd = new Random(1);
while (pos < arr.Length)
{
int n = Math.Min(arr.Length - pos, rnd.Next(1, 700));
byte[] part = new byte[n];
Buffer.BlockCopy(arr, pos, part, 0, n);
all.AddRange(p.Feed(part));
pos += n;
}
Console.WriteLine("frames=" + all.Count + " badcrc=" + p.BadCrc + " garbage=" + p.GarbageBytes);
bool ok = all.Count == 100 && p.BadCrc == 0;
foreach (Frame f in all) if (!f.CrcOk) ok = false;
Console.WriteLine(ok ? "SELFTEST PASS" : "SELFTEST FAIL");
return ok ? 0 : 3;
}
private static int CmdTc01(Args a)
{
string d = MakeOut(a, "TC01");
AtPort aa = Open(a.Get("a", "COM3"), 115200, d, true);
AtPort bb = Open(a.Get("b", "COM4"), 115200, d, true);
bool pass = false;
StringBuilder log = new StringBuilder();
try
{
log.AppendLine("A GMR: " + string.Join(" | ", aa.Cmd("AT+GMR?")));
log.AppendLine("B GMR: " + string.Join(" | ", bb.Cmd("AT+GMR?")));
aa.LocalMac = MacOf(aa);
bb.LocalMac = MacOf(bb);
log.AppendLine("A MAC: " + aa.LocalMac);
log.AppendLine("B MAC: " + bb.LocalMac);
bool link = PrepareBasicLink(aa, bb);
log.AppendLine("link=" + link + " Apeer=" + aa.PeerMac + " Bpeer=" + bb.PeerMac);
if (link)
{
int ca = aa.ConnId >= 0 ? aa.ConnId : 0;
int cb = bb.ConnId >= 0 ? bb.ConnId : 0;
bool s1 = aa.SendData(ca, Encoding.ASCII.GetBytes("hello"), 3000);
byte[] r1 = bb.TakeRx(3000);
bool ok1 = s1 && r1 != null && Encoding.ASCII.GetString(r1) == "hello";
if (!ok1)
{
Thread.Sleep(1000);
s1 = aa.SendData(ca, Encoding.ASCII.GetBytes("hello"), 3000);
r1 = bb.TakeRx(3000);
ok1 = s1 && r1 != null && Encoding.ASCII.GetString(r1) == "hello";
}
log.AppendLine("A->B hello: send=" + s1 + " recv=" + (r1 == null ? "<null>" : Encoding.ASCII.GetString(r1)) + " ok=" + ok1);
bool s2 = bb.SendData(cb, Encoding.ASCII.GetBytes("world"), 3000);
byte[] r2 = aa.TakeRx(3000);
bool ok2 = s2 && r2 != null && Encoding.ASCII.GetString(r2) == "world";
if (!ok2)
{
Thread.Sleep(1000);
s2 = bb.SendData(cb, Encoding.ASCII.GetBytes("world"), 3000);
r2 = aa.TakeRx(3000);
ok2 = s2 && r2 != null && Encoding.ASCII.GetString(r2) == "world";
}
log.AppendLine("B->A world: send=" + s2 + " recv=" + (r2 == null ? "<null>" : Encoding.ASCII.GetString(r2)) + " ok=" + ok2);
pass = ok1 && ok2;
}
File.WriteAllText(Path.Combine(d, "tc01.log"), log.ToString(), Encoding.UTF8);
File.WriteAllText(Path.Combine(d, "tc01_result.json"),
"{\"pass\":" + (pass ? "true" : "false") + ",\"A\":\"" + aa.LocalMac + "\",\"B\":\"" + bb.LocalMac + "\"}",
Encoding.UTF8);
Console.WriteLine(log.ToString());
Console.WriteLine(pass ? "TC01 PASS" : "TC01 FAIL");
}
finally
{
try { aa.Cmd("AT+SROLE=0"); bb.Cmd("AT+SROLE=0"); aa.Cmd("AT+SMUX=8"); bb.Cmd("AT+SMUX=8"); aa.ResetAndWait(5000); bb.ResetAndWait(5000); } catch { }
aa.Dispose(); bb.Dispose();
}
return pass ? 0 : 4;
}
private static int CmdConfig(Args a)
{
string d = MakeOut(a, "CONFIG");
AtPort aa = Open(a.Get("a", "COM3"), 115200, d, true);
AtPort bb = Open(a.Get("b", "COM4"), 115200, d, true);
try
{
int baud = a.GetInt("baud", 115200);
int mtu = a.GetInt("mtu", 248);
int interval = a.GetInt("interval", 160);
int phy = a.GetInt("phy", 0);
int format = a.GetInt("format", 1);
int smux = a.GetInt("smux", 8);
bool ok = PrepareLink(aa, bb, baud, 1, 0, mtu, interval, phy, format, smux, 6, true);
string s = "ok=" + ok + "\nA=" + aa.PortName + " baud=" + aa.Baud + " peer=" + aa.PeerMac +
"\nB=" + bb.PortName + " baud=" + bb.Baud + " peer=" + bb.PeerMac + "\n";
File.WriteAllText(Path.Combine(d, "config_result.txt"), s, Encoding.UTF8);
Console.WriteLine(s);
return ok ? 0 : 5;
}
finally { aa.Dispose(); bb.Dispose(); }
}
private static int CmdPerf(Args a)
{
string d = MakeOut(a, "PERF");
bool transparent = a.Get("mode", "at").ToLowerInvariant() == "transparent";
int baud = a.GetInt("baud", 115200);
int payload = a.GetInt("payload", transparent ? 480 : 200);
int duration = a.GetInt("duration", 5);
int rate = a.GetInt("rate", 0);
int phy = a.GetInt("phy", 0);
int format = a.GetInt("format", 1);
int interval = a.GetInt("interval", 160);
int smux = transparent ? 1 : a.GetInt("smux", 8);
string direction = a.Get("direction", "a2b").ToLowerInvariant();
bool logRaw = a.Get("raw", "yes").ToLowerInvariant() != "no";
AtPort aa = Open(a.Get("a", "COM3"), 115200, d, logRaw);
AtPort bb = Open(a.Get("b", "COM4"), 115200, d, logRaw);
try
{
bool link;
if (a.Has("no-reset"))
{
if (a.Has("phy") || a.Has("format") || a.Has("interval"))
{
aa.Cmd("AT+SPHY=" + phy + "," + format);
bb.Cmd("AT+SPHY=" + phy + "," + format);
aa.Cmd("AT+SCONPARA=" + interval + ",0,500");
bb.Cmd("AT+SCONPARA=" + interval + ",0,500");
Thread.Sleep(1500);
}
link = aa.WaitConnected(12000) && bb.WaitConnected(12000);
Console.WriteLine("assume link: A=" + link + " peer=" + aa.PeerMac + " B=" + bb.PeerMac);
}
else
{
link = PrepareLink(aa, bb, baud, 1, 0, 248, interval, phy, format, smux, 6, true);
}
if (!link) { Console.WriteLine("PERF FAIL: no link"); return 6; }
PerSecond[] samples;
long txA, txB, rxAB, rxBA;
RxStat sa = new RxStat(), sb = new RxStat();
RawFrameParser pa = new RawFrameParser(), pb = new RawFrameParser();
bool[] stop = new bool[1];
Thread ra = null, rb = null;
if (transparent)
{
if (!aa.EnterTransparent(1500) || !bb.EnterTransparent(1500))
{
Console.WriteLine("PERF FAIL: enter transparent");
return 7;
}
}
// Receiver threads
if (direction == "a2b" || direction == "both")
rb = StartReceiver(bb, pb, sb, stop);
if (direction == "b2a" || direction == "both")
ra = StartReceiver(aa, pa, sa, stop);
// Stats sampler
List<PerSecond> secs = new List<PerSecond>();
Thread statThread = StartStatSampler(aa, bb, secs, stop);
long sentA = 0, sentB = 0, bytesA = 0, bytesB = 0;
Thread ta = null, tb = null;
if (direction == "a2b" || direction == "both")
ta = StartSender(aa, 0, payload, duration, rate, 0x1111, transparent, delegate(long f, long by) { sentA = f; bytesA = by; }, stop);
if (direction == "b2a" || direction == "both")
tb = StartSender(bb, 0, payload, duration, rate, 0x2222, transparent, delegate(long f, long by) { sentB = f; bytesB = by; }, stop);
Thread.Sleep(duration * 1000 + 300);
if (ta != null) ta.Join(5000);
if (tb != null) tb.Join(5000);
// Keep receivers alive while the module drains its TX queue.
Thread.Sleep(3000);
stop[0] = true;
if (ra != null) ra.Join(3000);
if (rb != null) rb.Join(3000);
if (statThread != null) statThread.Join(2000);
if (transparent)
{
aa.ExitTransparent(); bb.ExitTransparent();
aa.Cmd("AT+SMUX=8"); bb.Cmd("AT+SMUX=8");
aa.ResetAndWait(5000); bb.ResetAndWait(5000);
}
double sec = Math.Max(0.001, duration);
double rxBytes = (direction == "a2b" ? sb.Bytes : direction == "b2a" ? sa.Bytes : sa.Bytes + sb.Bytes);
double txBytes = (direction == "a2b" ? bytesA : direction == "b2a" ? bytesB : bytesA + bytesB);
double goodput = rxBytes * 8.0 / sec;
StringBuilder js = new StringBuilder();
js.Append("{\n");
js.Append(" \"test\":\"perf\",\n");
js.Append(" \"mode\":\"" + (transparent ? "transparent" : "at") + "\",\n");
js.Append(" \"direction\":\"" + direction + "\",\n");
js.Append(" \"baud\":" + baud + ",\n");
js.Append(" \"payload\":" + payload + ",\n");
js.Append(" \"duration_s\":" + duration + ",\n");
js.Append(" \"phy\":" + phy + ",\n \"format\":" + format + ",\n \"interval\":" + interval + ",\n");
js.Append(" \"tx_frames\":" + (sentA + sentB) + ",\n");
js.Append(" \"tx_payload_bytes\":" + (long)txBytes + ",\n");
js.Append(" \"rx_frames\":" + (sa.Frames + sb.Frames) + ",\n");
js.Append(" \"rx_payload_bytes\":" + (long)rxBytes + ",\n");
js.Append(" \"goodput_bps\":" + goodput.ToString("F2", CultureInfo.InvariantCulture) + ",\n");
js.Append(" \"goodput_KBs\":" + (goodput / 8192.0).ToString("F3", CultureInfo.InvariantCulture) + ",\n");
js.Append(" \"crc_errors\":" + (sa.CrcErrors + sb.CrcErrors) + ",\n");
js.Append(" \"seq_gaps\":" + (sa.SeqGaps + sb.SeqGaps) + ",\n");
js.Append(" \"garbage_bytes\":" + (pa.GarbageBytes + pb.GarbageBytes) + ",\n");
js.Append(" \"peerA\":\"" + aa.PeerMac + "\",\"peerB\":\"" + bb.PeerMac + "\"\n");
js.Append("}\n");
File.WriteAllText(Path.Combine(d, "summary.json"), js.ToString(), Encoding.UTF8);
WritePerSecond(Path.Combine(d, "per_second.csv"), secs);
Console.WriteLine(js.ToString());
return 0;
}
finally { aa.Dispose(); bb.Dispose(); }
}
private static Thread StartReceiver(AtPort p, RawFrameParser parser, RxStat stat, bool[] stop)
{
Thread t = new Thread(delegate()
{
while (!stop[0])
{
byte[] chunk = p.TakeRx(100);
if (chunk == null || chunk.Length == 0) continue;
List<Frame> fs = parser.Feed(chunk);
foreach (Frame f in fs) stat.Add(f);
}
// drain
for (int i = 0; i < 20; i++)
{
byte[] chunk = p.TakeRx(50);
if (chunk == null || chunk.Length == 0) continue;
foreach (Frame f in parser.Feed(chunk)) stat.Add(f);
}
});
t.IsBackground = true; t.Start(); return t;
}
private static Thread StartSender(AtPort p, int connId, int payload, int durationSec, int rateBps,
uint seed, bool transparent, Action<long, long> done, bool[] stop)
{
Thread t = new Thread(delegate()
{
Stopwatch sw = Stopwatch.StartNew();
uint seq = 0; long bytes = 0;
int frameLen = FrameCodec.HeaderLen + payload;
double frameIntervalMs = rateBps > 0 ? (frameLen * 8.0 * 1000.0 / rateBps) : 0;
Stopwatch pace = Stopwatch.StartNew();
while (!stop[0] && sw.Elapsed.TotalSeconds < durationSec)
{
byte[] payloadBytes = FrameCodec.MakePayload(payload, seq, seed);
byte[] frame = FrameCodec.Build(FrameCodec.TypeData, seq, (ulong)sw.ElapsedTicks, payloadBytes);
if (transparent)
{
if (!p.WriteRaw(frame)) break;
}
else
{
if (!p.SendData(connId, frame, 3000)) break;
}
bytes += payload; seq++;
if (frameIntervalMs > 0)
{
double target = seq * frameIntervalMs;
double actual = pace.Elapsed.TotalMilliseconds;
if (target > actual) Thread.Sleep((int)Math.Min(1000, target - actual));
}
}
done(seq, bytes);
});
t.IsBackground = true; t.Start(); return t;
}
private static Thread StartStatSampler(AtPort a, AtPort b, List<PerSecond> secs, bool[] stop)
{
Thread t = new Thread(delegate()
{
Stopwatch sw = Stopwatch.StartNew();
int last = 0;
long atx = 0, arx = 0, btx = 0, brx = 0;
while (!stop[0])
{
Thread.Sleep(200);
int now = (int)sw.Elapsed.TotalSeconds;
if (now > last)
{
PerSecond p = new PerSecond();
p.Second = now;
p.ATx = a.TxBytes; p.ARx = a.RxBytes;
p.BTx = b.TxBytes; p.BRx = b.RxBytes;
p.AFrames = a.TxFrames; p.BFrames = b.TxFrames;
secs.Add(p);
last = now;
}
}
});
t.IsBackground = true; t.Start(); return t;
}
private static void WritePerSecond(string path, List<PerSecond> secs)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("second,ATxBytes,ARxBytes,BTxBytes,BRxBytes,ATxFrames,BTxFrames");
foreach (PerSecond p in secs)
sb.AppendLine(p.Second + "," + p.ATx + "," + p.ARx + "," + p.BTx + "," + p.BRx + "," + p.AFrames + "," + p.BFrames);
File.WriteAllText(path, sb.ToString(), Encoding.UTF8);
}
private static int CmdSoak(Args a)
{
// Reuse perf with a long duration and no raw logs.
a = a; // no-op
return CmdPerf(a);
}
private static int CmdLatency(Args a)
{
string d = MakeOut(a, "LATENCY");
int count = a.GetInt("count", 1000);
int payload = a.GetInt("payload", 32);
int baud = a.GetInt("baud", 115200);
AtPort aa = Open(a.Get("a", "COM3"), 115200, d, true);
AtPort bb = Open(a.Get("b", "COM4"), 115200, d, true);
try
{
bool link;
if (a.Has("no-reset")) link = aa.WaitConnected(3000) && bb.WaitConnected(3000);
else link = PrepareLink(aa, bb, baud, 1, 0, 248, 160, 0, 1, 8, 6, true);
if (!link) { Console.WriteLine("LATENCY FAIL: no link"); return 6; }
bool[] stop = new bool[1];
Thread responder = new Thread(delegate()
{
RawFrameParser parser = new RawFrameParser();
while (!stop[0])
{
byte[] chunk = bb.TakeRx(50);
if (chunk == null || chunk.Length == 0) continue;
foreach (Frame f in parser.Feed(chunk))
{
if (f.Type == FrameCodec.TypePing && f.CrcOk)
{
byte[] pong = FrameCodec.Build(FrameCodec.TypePong, f.Seq, f.Timestamp, f.Payload);
bb.SendData(0, pong, 2000);
}
}
}
});
responder.IsBackground = true; responder.Start();
RawFrameParser pa = new RawFrameParser();
List<double> rtts = new List<double>();
int timeouts = 0;
for (uint i = 0; i < count; i++)
{
byte[] payloadBytes = FrameCodec.MakePayload(payload, i, 0x3333);
byte[] ping = FrameCodec.Build(FrameCodec.TypePing, i, 0, payloadBytes);
Stopwatch sw = Stopwatch.StartNew();
bool sent = aa.SendData(0, ping, 2500);
if (!sent) { timeouts++; continue; }
bool got = false;
Stopwatch wait = Stopwatch.StartNew();
while (wait.ElapsedMilliseconds < 2500)
{
byte[] chunk = aa.TakeRx(50);
if (chunk == null || chunk.Length == 0) continue;
foreach (Frame f in pa.Feed(chunk))
{
if (f.Type == FrameCodec.TypePong && f.Seq == i)
{
rtts.Add(sw.Elapsed.TotalMilliseconds);
got = true; break;
}
}
if (got) break;
}
if (!got) timeouts++;
}
stop[0] = true;
responder.Join(1000);
rtts.Sort();
double mean = 0; foreach (double x in rtts) mean += x; if (rtts.Count > 0) mean /= rtts.Count;
double p50 = Pct(rtts, 0.50), p95 = Pct(rtts, 0.95), p99 = Pct(rtts, 0.99);
double sd = 0; foreach (double x in rtts) sd += (x - mean) * (x - mean);
if (rtts.Count > 1) sd = Math.Sqrt(sd / (rtts.Count - 1));
StringBuilder js = new StringBuilder();
js.Append("{\n \"test\":\"latency\",\n");
js.Append(" \"count\":" + count + ",\n \"payload\":" + payload + ",\n \"baud\":" + baud + ",\n");
js.Append(" \"ok\":" + rtts.Count + ",\n \"timeouts\":" + timeouts + ",\n");
js.Append(" \"mean_ms\":" + mean.ToString("F3", CultureInfo.InvariantCulture) + ",\n");
js.Append(" \"p50_ms\":" + p50.ToString("F3", CultureInfo.InvariantCulture) + ",\n");
js.Append(" \"p95_ms\":" + p95.ToString("F3", CultureInfo.InvariantCulture) + ",\n");
js.Append(" \"p99_ms\":" + p99.ToString("F3", CultureInfo.InvariantCulture) + ",\n");
js.Append(" \"stddev_ms\":" + sd.ToString("F3", CultureInfo.InvariantCulture) + ",\n");
js.Append(" \"min_ms\":" + (rtts.Count > 0 ? rtts[0].ToString("F3", CultureInfo.InvariantCulture) : "0") + ",\n");
js.Append(" \"max_ms\":" + (rtts.Count > 0 ? rtts[rtts.Count - 1].ToString("F3", CultureInfo.InvariantCulture) : "0") + "\n}\n");
File.WriteAllText(Path.Combine(d, "summary.json"), js.ToString(), Encoding.UTF8);
StringBuilder csv = new StringBuilder();
csv.AppendLine("seq,rtt_ms");
for (int i = 0; i < rtts.Count; i++) csv.AppendLine(i + "," + rtts[i].ToString("F3", CultureInfo.InvariantCulture));
File.WriteAllText(Path.Combine(d, "rtt.csv"), csv.ToString(), Encoding.UTF8);
Console.WriteLine(js.ToString());
return 0;
}
finally { aa.Dispose(); bb.Dispose(); }
}
private static double Pct(List<double> sorted, double p)
{
if (sorted.Count == 0) return 0;
int idx = (int)Math.Round((sorted.Count - 1) * p);
if (idx < 0) idx = 0; if (idx >= sorted.Count) idx = sorted.Count - 1;
return sorted[idx];
}
private static int CmdFile(Args a)
{
string d = MakeOut(a, "FILE");
string file = a.Get("file", null);
if (string.IsNullOrEmpty(file) || !File.Exists(file)) { Console.WriteLine("file not found"); return 8; }
string direction = a.Get("direction", "a2b").ToLowerInvariant();
int chunk = a.GetInt("chunk", 200);
int baud = a.GetInt("baud", 115200);
int delayMs = a.GetInt("delay-ms", 20);
int burst = a.GetInt("burst", 10);
int pauseMs = a.GetInt("pause-ms", 1000);
AtPort aa = Open(a.Get("a", "COM3"), 115200, d, true);
AtPort bb = Open(a.Get("b", "COM4"), 115200, d, true);
try
{
bool link;
if (a.Has("no-reset")) link = aa.WaitConnected(3000) && bb.WaitConnected(3000);
else link = PrepareLink(aa, bb, baud, 1, 0, 248, 160, 0, 1, 8, 6, true);
if (!link) { Console.WriteLine("FILE FAIL: no link"); return 6; }
bool ok = true;
if (direction == "a2b" || direction == "both")
ok &= SendFileOneWay(aa, bb, file, Path.Combine(d, "received_a2b.bin"), chunk, delayMs, burst, pauseMs, d + "\\a2b");
if (direction == "b2a" || direction == "both")
ok &= SendFileOneWay(bb, aa, file, Path.Combine(d, "received_b2a.bin"), chunk, delayMs, burst, pauseMs, d + "\\b2a");
Console.WriteLine(ok ? "FILE PASS" : "FILE FAIL");
return ok ? 0 : 9;
}
finally { aa.Dispose(); bb.Dispose(); }
}
private static bool SendFileOneWay(AtPort tx, AtPort rx, string path, string outPath, int chunk, int delayMs, int burst, int pauseMs, string tag)
{
byte[] data = File.ReadAllBytes(path);
string srcHash = Sha256(data);
bool[] stop = new bool[1];
long written = 0;
bool rxDone = false;
string rxHash = "";
RawFrameParser parser = new RawFrameParser();
FileStream fs = new FileStream(outPath, FileMode.Create, FileAccess.Write, FileShare.Read);
Thread receiver = new Thread(delegate()
{
while (!stop[0])
{
byte[] c = rx.TakeRx(100);
if (c == null || c.Length == 0) continue;
foreach (Frame f in parser.Feed(c))
{
if (!f.CrcOk) continue;
if (f.Type == FrameCodec.TypeData)
{
fs.Write(f.Payload, 0, f.Payload.Length);
written += f.Payload.Length;
}
else if (f.Type == FrameCodec.TypeFin)
{
rxHash = BitConverter.ToString(f.Payload).Replace("-", "").ToLowerInvariant();
rxDone = true;
stop[0] = true;
break;
}
}
}
});
receiver.IsBackground = true; receiver.Start();
Stopwatch sw = Stopwatch.StartNew();
uint seq = 0;
for (int off = 0; off < data.Length; off += chunk)
{
int n = Math.Min(chunk, data.Length - off);
byte[] p = new byte[n];
Buffer.BlockCopy(data, off, p, 0, n);
byte[] frame = FrameCodec.Build(FrameCodec.TypeData, seq, (ulong)sw.ElapsedTicks, p);
if (!tx.SendData(0, frame, 5000)) { Console.WriteLine("send failed at seq " + seq); break; }
seq++;
if (burst > 0 && seq % burst == 0 && pauseMs > 0) Thread.Sleep(pauseMs);
else if (delayMs > 0) Thread.Sleep(delayMs);
}
byte[] fin = FrameCodec.Build(FrameCodec.TypeFin, seq, (ulong)sw.ElapsedTicks, HexToBytes(srcHash));
tx.SendData(0, fin, 3000);
Stopwatch wait = Stopwatch.StartNew();
while (!rxDone && wait.ElapsedMilliseconds < 5000) Thread.Sleep(20);
stop[0] = true;
receiver.Join(2000);
fs.Flush(); fs.Close();
byte[] recv = File.ReadAllBytes(outPath);
string gotHash = Sha256(recv);
bool ok = (written == data.Length) && (gotHash == srcHash) && (rxHash == srcHash);
Console.WriteLine(tag + ": src=" + data.Length + " written=" + written + " srcHash=" + srcHash + " gotHash=" + gotHash + " ok=" + ok);
File.WriteAllText(tag + "_result.txt",
"source=" + path + "\nsize=" + data.Length + "\nwritten=" + written +
"\nsrc_sha256=" + srcHash + "\nrecv_sha256=" + gotHash + "\nmatch=" + ok + "\nseconds=" + sw.Elapsed.TotalSeconds.ToString("F3") + "\n",
Encoding.UTF8);
return ok;
}
private static byte[] HexToBytes(string hex)
{
byte[] b = new byte[hex.Length / 2];
for (int i = 0; i < b.Length; i++) b[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
return b;
}
private static string Sha256(byte[] data)
{
using (SHA256 sha = SHA256.Create())
{
byte[] h = sha.ComputeHash(data);
StringBuilder sb = new StringBuilder();
foreach (byte b in h) sb.Append(b.ToString("x2"));
return sb.ToString();
}
}
}
}8. 复现步骤
cd D:\WorkSpace\VSCode\WS8204 .\tools\ws8204_perf\build.ps1 .\tools\ws8204_perf\ws8204_perf.exe tc01 --a COM3 --b COM4 ` --out "docs\02_测试数据\TC01_repeat"
环境依赖:Windows 10/11、.NET Framework 4.8、PowerShell 5.1、CH9102 驱动、两块 WS8204-EVB。
我要赚赏金
