这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 活动中心 » 板卡试用 » 星闪模组开发板-WS8204开箱测试

共1条 1/1 1 跳转至页

星闪模组开发板-WS8204开箱测试

菜鸟
2026-10-25 15:03:52     打赏
开箱测试报告
  • 测试日期: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. 目的image.png

验证两块 WS8204-EVB 开箱后能否正常上电、串口识别、AT 固件通信、主从连接和双向小数据互传。

2. 测试环境

项目内容
PCWindows 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. 测试步骤

#步骤预期
1COM3/COM4 打开 115200 8N1端口可打开
2两块板发送 AT返回 +OK
3发送 AT+GMR?返回固件版本
4发送 AT+SMAC?返回本机 MAC
5A AT+SROLE=1,B AT+SROLE=0角色设置成功
6两块板 AT+RST复位并自动连接
7等待 +SCONN、查询 AT+SPMAC?对端 MAC 正确
8A→B AT+SSEND=0,5 + helloB 收到 +SRD:0,5 + hello
9B→A AT+SSEND=0,5 + worldA 收到 +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 设备信息

项目A 板(COM3)B 板(COM4)
固件V0.1.0 / V1.4.0V0.1.0 / V1.4.0
MAC54:ef:80:1a:51:d91a:ac:10:3d:36:e6
角色(测试时)Client/MasterServer/Slave
对端 MAC1a:ac:10:3d:36:e654: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

文件字节数SHA-256
tc01.log30888b72fbead7081f95a60054860d94101f783b94a112ee634bd905c4088f3fcfb
tc01_result.json645567431dfd335745b16da9a121a8be3a2d7eb71110a64f608ab942924c0e3ceb
COM3_decoded.log547bff3c3054bf34a6a1edab1d3988157096687c42b89bc0b9714b478175198e660
COM4_decoded.log82757246e9afeaf79b10f7ce648b7a16645d90794c9070edf6fb9a10cae094a6e75
COM3_raw.bin287024ac82a0f763ecd442b203c728d798542d23ce22b5061333f6a4c1bac5c4531
COM4_raw.bin398da47b0551554688c72fc2b226e88838307fc38bfe01d174b0d68a1267b92424e
console.txt784707070e0f4e5f49cecef104d8690246c27ebdf4b0f2e59a039341bc014cdecae

5. 结果判定

检查项预期实际结果
COM3 AT+OK+OKPASS
COM4 AT+OK+OKPASS
固件版本V1.4.0V1.4.0PASS
A 角色Client/MasterSROLE=1PASS
B 角色Server/SlaveSROLE=0PASS
自动连接+SCONN + 对端 MAC约 1 s 内连接PASS
A→B hello5 B 一致+SRD:0,5 + helloPASS
B→A world5 B 一致+SRD:0,5 + worldPASS
恢复默认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.cs
using 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.cs
using 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.cs
using 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。





关键词: WS8204     开箱     开发     模组     星闪     测试    

共1条 1/1 1 跳转至页

回复

匿名不能发帖!请先 [ 登陆 注册 ]