From a98930aa7153a01e2922536e2dbf941041cc86ba Mon Sep 17 00:00:00 2001 From: tonycao Date: Fri, 11 Sep 2026 01:08:47 +0800 Subject: [PATCH] docs+scripts: enet_protocol.md, verification plan, PS verify suite, session analysis scripts, DLL tools, README --- README.md | 69 + docs/enet_protocol.md | 710 ++++++++ docs/rust_verification_plan.md | 85 + scripts/Verify-EnetProtocol.ps1 | 1902 +++++++++++++++++++++ scripts/Verify-Rdm.ps1 | 38 + scripts/enet_lib.ps1 | 47 + scripts/session/analyze_ack.ps1 | 55 + scripts/session/analyze_aech.ps1 | 48 + scripts/session/analyze_branch.ps1 | 74 + scripts/session/analyze_branch2.ps1 | 74 + scripts/session/analyze_ck.ps1 | 81 + scripts/session/analyze_claim_seq.ps1 | 56 + scripts/session/analyze_cold.ps1 | 240 +++ scripts/session/analyze_cold2.ps1 | 142 ++ scripts/session/analyze_coldstart_seq.ps1 | 31 + scripts/session/analyze_dmx1.ps1 | 70 + scripts/session/analyze_dmx2.ps1 | 82 + scripts/session/analyze_dmx3.ps1 | 96 ++ scripts/session/analyze_fix2.ps1 | 81 + scripts/session/analyze_mute.ps1 | 69 + scripts/session/analyze_set.ps1 | 57 + scripts/session/analyze_setb.ps1 | 57 + scripts/session/analyze_ss.ps1 | 157 ++ scripts/session/analyze_ss2.ps1 | 81 + scripts/session/analyze_startup.ps1 | 14 + scripts/session/cap_hostdemo.ps1 | 11 + scripts/session/cat_runbutler.ps1 | 9 + scripts/session/check_desc.ps1 | 25 + scripts/session/check_fix1_aech.ps1 | 12 + scripts/session/check_log.ps1 | 9 + scripts/session/check_log2.ps1 | 9 + scripts/session/check_log3.ps1 | 9 + scripts/session/check_log4.ps1 | 9 + scripts/session/check_log5.ps1 | 9 + scripts/session/check_proc.ps1 | 11 + scripts/session/check_proc2.ps1 | 9 + scripts/session/check_procs.ps1 | 9 + scripts/session/check_set.ps1 | 9 + scripts/session/check_status.ps1 | 11 + scripts/session/check_ubuntu_git.ps1 | 10 + scripts/session/cold2_order.ps1 | 24 + scripts/session/deploy_g.ps1 | 38 + scripts/session/deploy_set.ps1 | 39 + scripts/session/deploy_set2.ps1 | 31 + scripts/session/deploy_tests.ps1 | 29 + scripts/session/diff_ann2.ps1 | 49 + scripts/session/diff_ann3.ps1 | 24 + scripts/session/diff_announce.ps1 | 31 + scripts/session/enum_preclaim.ps1 | 20 + scripts/session/extract_symclaim.ps1 | 13 + scripts/session/find_claim2.ps1 | 8 + scripts/session/find_sub07.ps1 | 9 + scripts/session/find_sub07b.ps1 | 7 + scripts/session/grep_aech.ps1 | 11 + scripts/session/listen_113.ps1 | 18 + scripts/session/mystery60.ps1 | 11 + scripts/session/post_claim.ps1 | 11 + scripts/session/probe_loop.ps1 | 20 + scripts/session/probe_loop2.ps1 | 20 + scripts/session/push_try.ps1 | 8 + scripts/session/rdm_tail.ps1 | 11 + scripts/session/rdm_tail2.ps1 | 11 + scripts/session/read_symclose.ps1 | 16 + scripts/session/read_symclose2.ps1 | 16 + scripts/session/restore_butler.ps1 | 9 + scripts/session/run_hd3.ps1 | 11 + scripts/session/run_hd4.ps1 | 4 + scripts/session/start_butler.ps1 | 13 + scripts/session/start_local.ps1 | 23 + scripts/session/sync_docs.ps1 | 52 + scripts/session/sync_push.ps1 | 42 + scripts/session/sync_push2.ps1 | 36 + scripts/session/test_g.ps1 | 47 + scripts/session/test_newmac.ps1 | 14 + scripts/session/test_norelay.ps1 | 14 + scripts/session/test_setack.ps1 | 52 + scripts/session/test_setget.ps1 | 60 + scripts/session/verify_dmxsend.ps1 | 11 + scripts/session/verify_frame.ps1 | 27 + scripts/session/watch_claim.ps1 | 11 + scripts/session/watch_sym_claim.ps1 | 16 + scripts/session/webui_probe.ps1 | 11 + tools/GetTable.cs | 28 + tools/IlDump.cs | 71 + tools/Scan.cs | 30 + 85 files changed, 5634 insertions(+) create mode 100644 README.md create mode 100644 docs/enet_protocol.md create mode 100644 docs/rust_verification_plan.md create mode 100644 scripts/Verify-EnetProtocol.ps1 create mode 100644 scripts/Verify-Rdm.ps1 create mode 100644 scripts/enet_lib.ps1 create mode 100644 scripts/session/analyze_ack.ps1 create mode 100644 scripts/session/analyze_aech.ps1 create mode 100644 scripts/session/analyze_branch.ps1 create mode 100644 scripts/session/analyze_branch2.ps1 create mode 100644 scripts/session/analyze_ck.ps1 create mode 100644 scripts/session/analyze_claim_seq.ps1 create mode 100644 scripts/session/analyze_cold.ps1 create mode 100644 scripts/session/analyze_cold2.ps1 create mode 100644 scripts/session/analyze_coldstart_seq.ps1 create mode 100644 scripts/session/analyze_dmx1.ps1 create mode 100644 scripts/session/analyze_dmx2.ps1 create mode 100644 scripts/session/analyze_dmx3.ps1 create mode 100644 scripts/session/analyze_fix2.ps1 create mode 100644 scripts/session/analyze_mute.ps1 create mode 100644 scripts/session/analyze_set.ps1 create mode 100644 scripts/session/analyze_setb.ps1 create mode 100644 scripts/session/analyze_ss.ps1 create mode 100644 scripts/session/analyze_ss2.ps1 create mode 100644 scripts/session/analyze_startup.ps1 create mode 100644 scripts/session/cap_hostdemo.ps1 create mode 100644 scripts/session/cat_runbutler.ps1 create mode 100644 scripts/session/check_desc.ps1 create mode 100644 scripts/session/check_fix1_aech.ps1 create mode 100644 scripts/session/check_log.ps1 create mode 100644 scripts/session/check_log2.ps1 create mode 100644 scripts/session/check_log3.ps1 create mode 100644 scripts/session/check_log4.ps1 create mode 100644 scripts/session/check_log5.ps1 create mode 100644 scripts/session/check_proc.ps1 create mode 100644 scripts/session/check_proc2.ps1 create mode 100644 scripts/session/check_procs.ps1 create mode 100644 scripts/session/check_set.ps1 create mode 100644 scripts/session/check_status.ps1 create mode 100644 scripts/session/check_ubuntu_git.ps1 create mode 100644 scripts/session/cold2_order.ps1 create mode 100644 scripts/session/deploy_g.ps1 create mode 100644 scripts/session/deploy_set.ps1 create mode 100644 scripts/session/deploy_set2.ps1 create mode 100644 scripts/session/deploy_tests.ps1 create mode 100644 scripts/session/diff_ann2.ps1 create mode 100644 scripts/session/diff_ann3.ps1 create mode 100644 scripts/session/diff_announce.ps1 create mode 100644 scripts/session/enum_preclaim.ps1 create mode 100644 scripts/session/extract_symclaim.ps1 create mode 100644 scripts/session/find_claim2.ps1 create mode 100644 scripts/session/find_sub07.ps1 create mode 100644 scripts/session/find_sub07b.ps1 create mode 100644 scripts/session/grep_aech.ps1 create mode 100644 scripts/session/listen_113.ps1 create mode 100644 scripts/session/mystery60.ps1 create mode 100644 scripts/session/post_claim.ps1 create mode 100644 scripts/session/probe_loop.ps1 create mode 100644 scripts/session/probe_loop2.ps1 create mode 100644 scripts/session/push_try.ps1 create mode 100644 scripts/session/rdm_tail.ps1 create mode 100644 scripts/session/rdm_tail2.ps1 create mode 100644 scripts/session/read_symclose.ps1 create mode 100644 scripts/session/read_symclose2.ps1 create mode 100644 scripts/session/restore_butler.ps1 create mode 100644 scripts/session/run_hd3.ps1 create mode 100644 scripts/session/run_hd4.ps1 create mode 100644 scripts/session/start_butler.ps1 create mode 100644 scripts/session/start_local.ps1 create mode 100644 scripts/session/sync_docs.ps1 create mode 100644 scripts/session/sync_push.ps1 create mode 100644 scripts/session/sync_push2.ps1 create mode 100644 scripts/session/test_g.ps1 create mode 100644 scripts/session/test_newmac.ps1 create mode 100644 scripts/session/test_norelay.ps1 create mode 100644 scripts/session/test_setack.ps1 create mode 100644 scripts/session/test_setget.ps1 create mode 100644 scripts/session/verify_dmxsend.ps1 create mode 100644 scripts/session/verify_frame.ps1 create mode 100644 scripts/session/watch_claim.ps1 create mode 100644 scripts/session/watch_sym_claim.ps1 create mode 100644 scripts/session/webui_probe.ps1 create mode 100644 tools/GetTable.cs create mode 100644 tools/IlDump.cs create mode 100644 tools/Scan.cs diff --git a/README.md b/README.md new file mode 100644 index 0000000..621a3a2 --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# enet-validator — e:cue SymphoLight ↔ Butler e:net 协议逆向与复现 + +对 e:cue SymphoLight 5.5 与 Butler S2 之间 UDP/31772 私有协议(e:net)的完整逆向: +**AECH 认领算法破解、DMX 帧混淆破解、RDM over e:net 解码**,并分别实现了 +**设备端仿真**(虚拟 Butler)与**主机端仿真**(hostdemo 直接接管真机)。 + +## 目录 + +``` +├── src/ Rust 源码(std only,无外部依赖) +│ ├── main.rs 子命令入口 +│ ├── enet.rs e:net 帧模板 / SO_REUSEADDR 绑定 +│ ├── tests.rs 虚拟 Butler 设备端(AECH 本地应答 + RDM 灯具仿真 + 解密日志) +│ ├── aech.rs AgiosElias.G 认领算法、DMX zPprv0TXRRW 混淆加解密、dmxdec/dmxsend +│ ├── hostdemo.rs 主机侧等效示例(真机接管:认领→RDM 发现→加密 DMX 控制) +│ └── dmxhash.rs 早期帧标签分析 +├── samples/ 37 个真实抓包帧样本(hex) +├── docs/ +│ ├── enet_protocol.md ★协议逆向文档(帧布局/算法/实证,最核心) +│ └── rust_verification_plan.md 验证计划 +├── scripts/ +│ ├── Verify-EnetProtocol.ps1 协议验证套件 +│ ├── Verify-Rdm.ps1 RDM 验证 +│ ├── enet_lib.ps1 PS 公共库 +│ └── session/ 逆向过程中的会话脚本(抓包/分析/部署/推送) +└── tools/ DLL 反编译辅助工具(C#) + ├── IlDump.cs / GetTable.cs / Scan.cs +``` + +## 构建 + +``` +cargo build --release # std only, 无外部依赖 +``` + +## 子命令(设备端仿真,跑在任意机器,绑 58 网段示例) + +``` +enet-validator device # 虚拟 Butler 主循环(192.168.58.7) +enet-validator discover|announce|heartbeat|euce|session|all +``` + +## 子命令(主机侧 / 离线分析) + +``` +enet-validator hostdemo <目标IP> [秒] # 主机侧等效:认领→RDM 发现→加密 DMX 控制 + # 环境变量 HOSTDEMO_MODE=quick 最小认领+统一会话环 + # =claimonly 最小认领探针 + # HOSTDEMO_PRE=<秒> 前奏时长(默认45) +enet-validator dmxsend <目标IP> [秒] # 主机侧加密 DMX 播放(自证闭环) +enet-validator dmxdec # DMX 帧解密验证(离线) +enet-validator dmxroll # 滚动值分析(离线) +enet-validator aechprobe # AECH 在线探测 +``` + +## 已破解要点(详见 docs/enet_protocol.md) + +- **AECH 认领**:`G(x) = x ^ TABLE[(x%32 + 1 + (x*13)%30) % 32]`(无密钥,表内嵌 DLL) +- **DMX 混淆**:`r=Random.Next()`;`out[i]=src[(i+rot)%512]^cnt[i]`,`rot=(B12^r)&511` +- **RDM**:标准 E1.20 内嵌 type=04 帧(0x3C 起,免 0xCC 起始字节,checksum 含隐含 0xCC) +- **1947 端口**:40B ASCII 许可令牌广播(`BlZRQphA…`,每 ~34s 一轮) +- 真机固件过滤:AECH 仅应答源 IP=192.168.123.113 + +## Ubuntu 虚拟 Butler 部署 + +``` +scp -r enet-validator/ user@192.168.58.7:~/ +ssh user@192.168.58.7 "cd ~/enet-validator && cargo build --release && ~/run_butler_local.sh" +``` diff --git a/docs/enet_protocol.md b/docs/enet_protocol.md new file mode 100644 index 0000000..19d865f --- /dev/null +++ b/docs/enet_protocol.md @@ -0,0 +1,710 @@ +# e:net 协议复原文档(e:cue SymphoLight 5.5 ↔ Butler S2) + +> 基于实测抓包复原(2026-09-08 初版,Wireshark/tshark 抓取于"以太网 3")。 +> 抓包环境:SymphoLight 5.5 (192.168.123.113) ↔ Butler S2 固件 2.0.3322 (192.168.123.1)。 +> 样本:discovery.pcap(启动/发现阶段 56 帧)、dmx40s.pcap(工程驱动阶段 ~5900 帧)。 +> +> **2026-09-09 重大更新**:基于真机冷启动全程抓包 `cold2.pcap`(168.7s / 19471 帧, +> SymphoLight 冷启动 → 发现 → 注册 → AECH 认领 → RDM 全程)与虚拟 Butler 验证抓包 +> `fix1.pcap`。补全设备上线协议链(§3.6/§3.7),RDM 层完全解码(§5 重写), +> 新增虚拟 Butler 实现与验证(§11)。 +> +> **2026-09-10 更新**:稳态双网段对比(ss123/ss58)+ P1 修复验证(fix2/fix3)。 +> DMX 帧完全解码(§4.1 重写:seq/保护字节 `(CH-1)^K`/节点序号/端口布局); +> 42B 帧重定性为事件计数遥测(§4.3,推翻"逐帧 ACK");发现收敛与稳态轮询 +> 对齐(§11.3);新增 §11.5 DMX 解码里程碑、§12 缺口清单与攻关路线图。 +> +> **尺寸口径约定**:本文所有帧长如无特别说明均为 **UDP 负载字节数**。 +> 以太网整帧长 = UDP 负载 + 42(Eth14 + IP20 + UDP8)。历史记录中 +> "89B 注册帧"=47B 负载、"594B AECH"=552B 负载、"259B 发现应答"=217B 负载、 +> "101B 脉冲"=59B 负载、"84B ACK"=42B 负载(§4.3 遥测帧),均为同一帧的两种口径。 + +--- + +## 1. 总览 + +| 项目 | 值 | +|------|-----| +| 传输层 | UDP(无 TCP) | +| 主数据端口 | **31772** (0x7C1C),双向,主机与设备均使用该端口收发 | +| 密钥/许可端口 | **1947**,主机→广播,周期性,静态负载 | +| 帧签名 | 负载前 4 字节 = ASCII `"ENet"`(`45 4E 65 74`) | +| 字节序 | 混合:长度/计数器字段为 **小端 (LE)**,端口字段为 **大端 (BE)** | +| 加密 | **DMX 通道数据经 zPprv0TXRRW 逐帧混淆**(§4.1,已完全破解可双向复现):rot=(B12^r)&511,B12=AECH 实例随机(会话),r=Random.Next()(每帧每端口,帧内 0x22/0x26)。发现帧含部分随机材料 | +| Wireshark 识别 | 无 dissector,全部显示为 `data` | + +--- + +## 2. e:net 通用帧头(所有 31772 报文) + +UDP 负载布局(偏移为负载内字节偏移): + +``` +偏移 长度 示例(主机DMX帧) 含义 +0x00 4 45 4E 65 74 魔数 "ENet"(另见 §3.4 的 "EUCE" 第二魔数) +0x04 4 04 00 00 00 消息类型 (LE uint32):01=发现/宣告, 02=主机标识心跳, 04=会话数据 +0x08 4 00 00 1C 7C 端口 31772 (BE uint16 前置 0000) +0x0C 6 00 00 00 00 00 00 保留 (实测恒 0) +0x12 2 01 00 协议版本/标志 (实测恒 0x0001) +0x14 4 3C 04 00 00 负载总长 (LE uint32) = UDP负载字节数 +0x18 2 01 00 固定字段 (实测所有 ENet 会话帧恒 0x0001) +0x1A 2 97 01 DMX 帧序号 (LE uint16,仅 DMX 数据帧;0x197→0x198→…) +0x1C ... 类型专属字段 +``` + +已实测的长度字段对应关系: +| 帧总长(以太网) | UDP负载长 | 0x14 处 LE 值 | +|---------------|-----------|---------------| +| 222 / 259 / 540 / 686 / 1126 | 180/217/498/644/1084 | 0xB4/0xD9/0x1F2/0x284/0x43C ✓ | +| 84 / 171(Butler→主机) | 42/129 | 0x2A/0x81 ✓ | + +--- + +## 3. 消息类型 01 —— 设备发现 / 宣告 + +### 3.1 主机发现请求(222 B,主机→255.255.255.255 广播) +- 负载 180 B。0x18 处 `B4 00 00 00`(=180)。 +- 负载中含:主机 IP (LE `71 7B A8 C0`=192.168.123.113)、主机 MAC、 + 机器名+GUID ASCII(如 `tonycaoDESKTOP-PED2LVD-f7f4a043-1a`)、尾部 `09 00`。 + +### 3.2 Butler 发现应答(259 B,Butler→主机单播) +- 负载 217 B。含设备名 ASCII `"Butler S2"`、Butler IP (LE `7B A8 C0 00` 区域)、 + Butler MAC (`00 16 1C F1 2E 39`)、固件信息区 + (`23 01 00 00 00 00 24 00 00 00 24 00 00 00 1C 00 ...` 版本/参数块)、 + DMX 时序参数区(`F0 0A`=BRK 100µs? `F0 0A`、MAB 等)。 + +### 3.3 主机宣告帧(540 B / 686 B,主机→广播,周期 ~30s) +- type=01,686 B 版负载 644 B(0x284 ✓)。含: + - 主机 IP、广播地址、主机 MAC + - 16 B 随机/密钥材料:`43 A0 F4 F7 EC 17 56 41 81 2B 90 9B DC 99 F2 FF` + - **UTF-16LE** 主机名+GUID:`tonycaoDESKTOP-PED2LVD-f7f4a043-17ec-4156-812b-909bdc99f2ff` + - `D0 07` = 2000(LE,疑似辅助端口) + - 时间戳 `EA 07 09 07 17 14 12 F4` = 2026-09-07 17:14:20.244(年/月/日/时/分/秒/毫秒) + - 尾部 `5A 00 FF FF` + +### 3.4 主机标识心跳(36 B 负载 / 78 B 帧,type=02 sub=03,主机→广播,~6-30s 周期) + +``` +0x14 24000000 负载长 36 +0x18 0300 子类型 3 +0x1A 717BA8C0 主机 IP 192.168.123.113 (LE) +0x1E 300ED559BB36 主机 MAC +``` + +★2026-09-09 实测(cold2.pcap):认领后每 ~6s 一发,**设备从不应答**。 +(旧虚拟 Butler 实现误回 42B 状态帧,已列入修正项。) + +### 3.5 "EUCE" 帧(582 B,非 ENet 魔数,主机→广播,~30s 周期) + +- 魔数 `45 55 43 45` = ASCII **"EUCE"**(与 "ENet" 并存的第二种 e:cue 载荷签名)。 +- `EUCE 0000 0000 0000 0000 0500 0000 …`(**0x0C 处 LE `05`**=类型/版本 5),其余大量零。 +- 40s 窗口观测 6 帧(帧号 668/1576/2530/3421/4309/5222,间隔 ~908 帧 ≈ 30s), + 与 type=02 心跳交替出现,属主机的周期性身份/发现通告族。 + +### 3.6 设备注册(type=02 sub=01 → sub=02)—— 认领前的关键步骤 ★2026-09-09 补全 + +冷启动实测(cold2.pcap,t=38.087): + +``` +主机 → Butler: 102B type=02 sub=01 主机身份宣告 + 0x1A-0x1D 主机 IP (LE) + 0x1E-0x23 主机 MAC +Butler → 主机: 47B type=02 sub=02 注册应答 (10ms 内) + 0x1A-0x1D 设备 IP (LE) 例: 017ba8c0 = 192.168.123.1 + 0x1E-0x23 设备 MAC 例: 00161cf12e39 + 0x24 00 + 0x25-0x28 主机 IP (LE) 例: 717ba8c0 = 192.168.123.113 + 0x29-0x2E 主机 MAC 例: 300ed559bb36 +``` + +47B 注册应答完整样本: +`454e6574 02000000 00001c7c 00000000 0000 0100 2f000000 0200 017ba8c0 00161cf12e39 00717ba8c0 300ed559bb36` + +广播的 sub=01 宣告(255.255.255.255)同样触发设备回 sub=02 注册帧。 + +### 3.7 AECH 认领(type=02 sub=05 → sub=06)—— 加密会话建立 ★★2026-09-10 完全破解 + +注册完成后主机立即发起认领(cold2.pcap,t=38.105,**连续 3 轮**,设备 **1ms** 内应答): + +``` +主机 → Butler: 552B type=02 sub=05 AECH 质询 (含 "AECH"=41454348 @0x28) + 0x1A-0x1D 主机 IP (LE) + 0x1E-0x23 主机 MAC + 0x28-0x2B "AECH" + 0x2C-0x87 92B 纯随机填充 (设备完全忽略!) + 0x88-0x8B ★ 4B 质询 nonce (LE) — 唯一影响应答的输入! + 0x8C-0x8F ★ 4B 主机实例随机 B12 (LE, 会话内不变, 会话间不同) + — 兼任 DMX 混淆基值: rot=(B12^r)&511 (§4.1) + 0x90-0x123 148B 纯随机填充 (设备完全忽略!) +Butler → 主机: 552B type=02 sub=06 AERE 应答 (含 "AERE"=41455245 @0x28) + 0x1A-0x1D / 0x41-0x44 设备 IP (LE, 两处) + 0x1E-0x23 / 0x49-0x4E 设备 MAC (两处) + 0x25-0x27 主机 IP 前3字节 (LE) + 0x2C-0x2E 主机 MAC 后3字节 + 0xC0-0xC3 ★ 应答 = G(nonce) — 算法见下 + 其余 设备身份串 + RDM 灯具摘要 + 259 式信息块 (全部静态) +``` + +**★应答算法(源自 SymphoLight `Ecue.Devices.dll` 反编译,类 `Ecue.Devices.ENet.Protocol.AgiosElias`)**: + +``` +G(x) = x ^ TABLE[(x%32 + 1 + (x*13)%30) % 32] (u32 环绕乘法) +TABLE = 32×u32 常量表, 内嵌于 DLL 静态数组 (0A8F2B10..., 128B): + 1008de00 e3b77ff6 31183e1c 90fb53c8 ff10ba5c 030bc4b6 e22935c8 10e8ede1 + 55f409a0 3eb0b9a3 f7333308 edf07810 8abf4078 60ec0545 1f4645c8 54094148 + 04c0f500 fabe50bb f3f3e54c 6c2a02cb a2546e60 4f6ae10e 51dfe8d8 2eedb0ec + 74006928 48fade70 e5346e9c 1c3fe0bc f6700700 6b7f1ba0 a1143cf4 3be8426c +主机验证 (AgiosElias.i4GrvIBQ6xF): AERE blob[152] (= 帧 0xC0) == G(主机 nonce) +``` + +**无密钥、无 dongle 参与**——纯混淆级算法。黑盒探测(aechprobe/aechprobe2 子命令)实证: +仅 0x88-0x8B 影响应答(全雪崩),确定性;离线 G 对 cold2+fix1 共 10/10 样本命中; +虚拟 Butler 已实现本地计算(`aech::agios_g`),随机 nonce 测试 5/5 PASS。 +**终极实测(2026-09-10 14:29)**:SymphoLight 重启后 .7 在无桥/无中继条件下完成 +3 轮 AECH 本地应答 → 89B 注册 → DMX 流恢复 → RDM 灯具重新注册,全流程独立。 +**认领不再依赖真机中继**(中继/桥代码保留作对比,桥已退役)。 + +**实测过滤行为**:真机 .1 只应答**源 IP=192.168.123.113** 的 AECH(任意源端口均可), +其他源 IP 的质询直接丢弃(这是真机固件行为;虚拟设备无需此过滤)。 + +认领通过后:主机开始 DMX 会话(§4),心跳 sub=03(36B,每 ~6s,**设备不应答**)。 + +--- + +## 4. 消息类型 04 —— 会话数据(连接后的主通道) + +type=04 的帧在 0x20 处有 2 字节**子类型/方向标记**: +- `01 01` = 主机→设备 **DMX 通道数据**(形态 A) +- `02 00` = 事件/状态帧(形态 B,双向:主机发事件,Butler 周期回 42B 计数遥测 §4.3) + +### 4.1 形态 A:DMX 通道数据帧(1126 B,主机→Butler,~30 Hz)★2026-09-10 完全解码(含混淆算法) + +``` +负载偏移 长度 示例 含义 +0x00 4 454E6574 "ENet" +0x04 4 04000000 type=4 +0x08 4 00001C7C 端口 31772 +0x0C 6 000000000000 保留 +0x12 2 0100 版本/标志 +0x14 4 3C040000 负载长 1084 +0x18 2 0100 wnor8bDK5nr u16 = 1 (DMX 数据消息) +0x1A 4 4CB20000 ★ 帧序号 aOKr8qAKOAa LE u32,逐帧 +1(高 2B 恒 0) +0x1E 2 0000 OcpmOpr u16 = 0 +0x20 2 0101 cbY0uIrZR2S3QxGbNbNC × 2 (每端口 1B, 均=1) +0x22 4 6E100C2D ★ r1 = Random.Next() LE u32 (每帧每端口随机, MSB=0) +0x26 4 80681200 ★ r2 = Random.Next() LE u32 (端口2) +0x2A 2 0000 端口状态字节 × 2 (universe 状态表查得, 通常 0) +0x2C 2 0400 端口1 ID = 4 (LE, 两设备同值) +0x2E 2 0200/0300 ★ 端口2 ID = 节点序号 LE: 真机 .1=2, 虚拟 .7=3 +0x30 2 0000 端口索引0 (LE) +0x32 2 0100 端口索引1 (LE) +0x34 2 0002 端口1 通道数 = 512 (LE! 00 02, 旧文档"BE"系误读) +0x36 2 0002 端口2 通道数 = 512 (LE) +0x38 2 0100 固定 (01 00) +0x3A 2 0000 固定 +0x3C 512 ★ 端口1 DMX 通道 1-512 — zPprv0TXRRW 混淆 (见下, 非明文!) +0x23C 512 ★ 端口2 DMX 通道 513-1024 — 同式混淆 (用 r2) +``` + +**★通道数据混淆(2026-09-10 反编译+实证完全破解;旧文档"明文/保护字节/滚动值"系误判)**: + +构建链(反编译 `iey7k4rDmrsMD1TvRywm::ScheduleUpdate` → `ENet2MessageCreator::QJiBkgLcunE` +→ `vpFbA8r8ibe5ncT1Lum7::CopyToBuffer`,60B 头结构逐字段与线值吻合): + +``` +r = Random.Next() # 每帧每端口独立, u31 (MSB 恒 0), 写入 0x22/0x26 +rot = (B12 ^ r) & 511 # B12 = AECH 质询 0x8C-0x8F 实例随机 (AgiosElias.B12rvMuR8pT, 会话不变) +out[i] = src[(i+rot) % 512] ^ cnt[i] # src = 真实通道值 + cnt[i] = (512-rot-i) & 255 (i < 512-rot) + = (rot-(i-(512-rot))) & 255 (i >= 512-rot) +``` + +接收方解密:`src[(i+rot)%512] = wire[i] ^ cnt[i]`(rot 由帧内 r 与会话 B12 重建)。 + +**实证**:cold2(B12=0x1e67079f)3893 帧 + fix1(B12=0x70959ecf)1781 帧全部解密为 +跨帧稳定的全零场景(测试期间主机发的是 blackout),加解密回环 5674/5674 逐字节一致 +(`aech::agios_dmx_encrypt`/`agios_dmx_decrypt`,`dmxdec` 子命令)。 +**"保护字节 (CH-1)^K" 系涌现巧合**:src=0 时 wire[0x3C]=(512-rot)&255,代数化简得 +K = ~B12 & 0xFF(cold2: ~0x9f=0x60 ✓、fix1: ~0xcf=0x30 ✓、ss123: K=0x70 → B12l=0x8f); +0.5% 失配 = rot%256==0 的边界帧。宿主侧另有 `kWarv47o79k`(新算法, +`out[i]=src[i]^((512-i)&255)^key[(512-i)&3]`, key=r^B12,CP7rDoD7VL4() 标志选择)—— +Butler S2 走旧算法。 + +**端口映射**:工程 universe 号(U6/U2、U1/U5)**不上线**,主机按端口序拼包,Butler 解密后按 +port1/port2 各 512B 直接映射到 RS485 输出。实测帧率 29.8/s(中位间隔 33ms),稳态恒定。 + +### 4.2 形态 B:事件/状态帧(主机→Butler,1126 B)★2026-09-10 完全解码 + +> 速率随主机活动模式变化:纯 DMX 播放时 A:B ≈ 1:2(dmx40s: 1207A+2336B/40s); +> RDM 激活稳态时 B ≈ 24.7/s、A 恒定 29.8/s(ss123)。旧版"A·B·B 固定节奏" +> 仅为其中一种模式。 +> +> **帧构成 = [事件头 0x00-0x3B] + [标准 E1.20 RDM 消息] + [通道图案填充至 1084B]**。 +> ss123 实证:RDM 激活稳态下 2963/2963 事件帧 100% 为 RDM 查询 +> (cmd24×1886 + cmd18×645 + cmd19×352 + flag=01 内嵌副本×80),无非 RDM 事件帧。 + +``` +负载偏移 长度 示例 含义 +0x00-0x17 24 … 通用头 ("ENet"/type=4/端口/负载长 1084) +0x18 4 01000000 固定 +0x1C 4 00000000 固定 +0x20 2 0200 子类型 = 事件 +0x22 4 65890000 ★ 事件序号 u32 LE,严格 +1(2962/2962 无例外) +0x26 4 00000000 固定 +0x2A 4 CC000000 常量 0xCC +0x2E 2 0100 事件类标记(DMX-A 帧此位为节点序号 2/3) +0x30 1 00/01 ★ flag:00=独立查询帧(设备应答); + 01=DMX 内嵌副本(设备不应答),此时 0x32=01 +0x31 3 000000 固定 +0x34 4 25000000 ★ RDM 消息长度 LE u32 = E1.20 msgLen+1(含 SubSC) +0x38 4 01000000 疑似 RDM 总线/端口号(LE,本样本恒 1) +0x3C … ★ 标准 E1.20 消息(免 0xCC 起始字节): + 3C:SubSC=01 3D:msgLen 3E-43:destUID 44-49:srcUID + 4A:TN 4B:port 4C:msgCount 4D-4E:subDevice 4F:CC + 50-51:PID(BE) 52:PDL 53+:PD 末尾:checksum(BE) +… … ★ 通道图案填充:从 0x3C+RDM长 到帧尾,约 1011-1031B +``` + +**填充区性质(2026-09-10 实证)**:外观为 DMX 通道图案(渐变波形),但 24B +签名在本网段与跨网段 DMX-A 流中均只命中**周期性效果的自相似片段** +(时间差中位 -50s、偏移散乱)→ 与任何实时 universe 数据无系统对应, +判定为**主机缓冲复用/填充,无协议语义**。Butler 只需解析 RDM 消息即可 +(虚拟 Butler 实现即如此)。 + +**E1.20 字段实证对齐**(逐偏移标注): +- 0x4F = Command Class:cmd24/18 → 0x10(DISCOVERY)、cmd19 → 0x20(GET)、cmd=1a → 0x30(SET) +- cmd24 → PID=0001 UNIQUE_BRANCH(PD=分支 lo/hi 12B);cmd18 → PID=0002 UN_MUTE + (PDL=0);cmd19 → PID=0201 SENSOR_VALUE(PD=传感器序号) +- RDM 长度字段实证:cmd24→37、cmd18→25、cmd19→26(= 24 头 + PDL + 1) +- flag=01 内嵌副本:srcUID 末字节 fe(独立帧为 ff)、0x4B(port)=01 + +### 4.3 Butler 42B 事件计数遥测帧(Butler→主机,~每 115ms 一帧) + +``` +0x14 2A000000 负载长 42 +0x18 0200 子类型 +0x1A 4275746C 设备名前缀 "Butl" +0x1E 0100 +0x20 4 xxxxxxxx ★ Butler 本地"已收事件B帧"累计计数(LE, 严格递增) +0x24 6 010000000000 +``` + +★2026-09-10 更正(ss123 实证):0x20-0x23 **不是** "被确认事件帧的序号", +而是 Butler 自己累计收到的事件B帧数(120s 内 +2959 ≈ 实收事件 2963; +相邻帧差 +1~+10,1039/1039 严格递增)。该帧以 ~8.7/s(中位间隔 35ms, +有事件才发)周期上报,与具体查询**无逐帧配对关系**——旧结论"ACK seq = +事件 seq - 1"是"计数器≈主机事件序号"的自然巧合,叠加分析脚本 +PowerShell 运算符优先级 bug(`$a.seq -eq $q.seq - 1` 解析为 +`($a.seq -eq $q.seq) - 1`)导致的假相关。收到非 RDM 事件B帧**不应**即时 +回 42B。 + +### 4.4 Butler 状态帧(171 B 等,Butler→主机,周期性) + +与 42B 遥测同构:`ENet` + type=04 + `0200` + `"Butl"` + `0100` + 事件计数器(0x317…) + +`0000` + 状态块(首字节 `57`=87…,尾部补零至 129 B 负载)。 +观测到的状态帧长度分布:171/177-247 B(约 60 余种长度,随状态内容伸缩)。 + +★2026-09-09 补注(cold2.pcap):上述"状态帧"实际多为 RDM 应答帧(69-129B 负载); +认领后的稳态会话中,真机**几乎不发送任何自发状态帧**。旧虚拟 Butler 的 +"每 3.5s 突发 10 帧 84B"为错误指纹。 + +★2026-09-10 再补(ss123):稳态下真机**确有** 42B 帧,但性质是 §4.3 的 +事件计数遥测(~8.7/s,有事件才发),既非状态帧也非逐帧 ACK。cold2 的 +168s 仅 1 帧是因为冷启动窗口内事件量小。 + +### 4.5 流量节奏(实测 40 s 窗口) + +| 流量 | 包数 | 说明 | +|------|------|------| +| 主机→Butler 1126 B | 3543 | 形态A 1207 + 形态B 2336(该窗口 A:B≈1:2;稳态 RDM 窗口 A 29.8/s 恒定、B ~24.7/s) | +| Butler→主机 84 B(负载42B) | 1250 | 事件计数遥测(§4.3,非逐帧 ACK) | +| Butler→主机 状态帧 | ~700 | 各长度状态/心跳 | +| 主机→广播 1947 | 42 | 密钥报文,~1 Hz 成对 | +| 主机→广播 686 B | 13 | type=01 宣告,~3 s 间隔 | +| 主机→广播 78 B | 7 | type=02 标识心跳,~30 s 间隔 | +| 主机→广播 582 B | 6 | "EUCE" 帧,~30 s 间隔 | + +## 5. RDM over e:net ★2026-09-09 完全解码(推翻旧版"私有变体"结论) + +> 触发条件:SymphoLight RDM 窗口 → Butler 端口 Enable RDM → Discover; +> 认领通过后主机持续进行后台 RDM 发现/轮询(无需人工触发)。 +> 旧版"e:cue 私有变体、字段序与 E1.20 不一致"结论系解析偏移错误—— +> **e:net 封装的 RDM 负载就是标准 E1.20 报文**(含标准 checksum,真机 1396/1396 验证通过)。 + +RDM 流量全部复用 type=04 会话帧,无独立消息类型。 +主机→设备的 RDM 查询帧完整布局见 **§4.2**(事件头 + 免 0xCC 起始的 E1.20 消息 +@0x3C + 通道图案填充)。 + +### 5.1 e:net 封装(RDM 应答,Butler → 主机) + +``` +0x00 4 "ENet" +0x04 4 04000000 type=4 +0x08 4 00001C7C 端口 +0x0C 6 00…00 保留 +0x12 2 0100 +0x14 4 负载总长 (LE) +0x18 2 0200 子类型 +0x1A 4 "Butl" (4275746C) +0x1E 2 0000 / 0100 标志 +0x20 4 序号 (LE):参数应答 = 回显查询 0x22-0x23;DISC 脉冲 = 设备本地自增 +0x24 4 00000000 +0x28 2 ★ RDM 数据区长度 (LE) = msgLen + 2 +0x2A ... ★ RDM 数据区 + - 标准 E1.20 报文: CC 01 ... + - DISC 脉冲: AA <9-bit 编码 16B> (无 CC01, 见 §5.4) +``` + +### 5.2 标准 E1.20 字段布局(0x2A 起) + +``` +0x2A CC Start Code (SC_RDM) +0x2B 01 Sub Start Code +0x2C msgLen Message Length = CC 起到最后一个 PD 字节的字节数 (不含 checksum) +0x2D 6B Dest UID = 主机 RDM 控制器 UID (实测 45:3a:7f:ff:ff:ff) +0x33 6B Src UID = 灯具 UID (应答时) +0x39 TN Transaction Number — 应答回显查询的 TN +0x3A Response Type (00=ACK) +0x3B Message Count +0x3C 2B Sub-Device (0000) +0x3E CC Command Class (0x11=DISC_RESP, 0x21=GET_RESP, 0x31=SET_RESP) +0x3F 2B PID (BE) +0x41 PDL +0x42 PD (PDL 字节) +末尾 2B Checksum = 从 CC(0x2A) 起 msgLen 字节的 16-bit 和, 大端 +``` + +**checksum 规则(已验证 1396/1396)**:`sum16(CC..最后一个PD字节) == checksum(BE)`。 +任何模板 patch(改 UID/TN/bridge-id)后**必须重算**,否则主机按 E1.20 规范丢弃。 +(注意 PowerShell 陷阱:`-shl` 保持类型宽度,`[byte] -shl 8` 恒得 0,验证脚本须先转 int。) + +### 5.3 主机 → Butler 的 RDM 请求(type=04,"cmd" @0x3D 实为 E1.20 msgLen) + +请求帧两种形态:独立短帧(~112B 负载)与 1084B DMX 内嵌帧(0x30 flag=01)。 +**实测真机只应答 flag=00 的帧**,flag=01 为 DMX 流内嵌副本,不答。 + +★2026-09-10 更正(SET 抓包实证):**0x3D 不是 e:net 命令字节,是 E1.20 msgLen**。 +旧称 cmd=24/18/19/1a 恰为各类请求的 msgLen:0x24=36(UNIQUE_BRANCH, PDL=12)、 +0x18=24(PDL=0 的 DISC/GET)、0x19=25(GET SENSOR_VALUE, PDL=1)、 +0x1A=26(SET DMX_START_ADDRESS, PDL=2)。命令语义由 CC@0x4F 决定 +(0x10 DISCOVERY / 0x20 GET / 0x30 SET)。下文沿用"cmd=xx"仅作 msgLen 类别简写。 + +``` +0x20 2 子类型 0200 +0x22 2 查询序号 (LE) — 应答在 0x20-0x21 回显 +0x30 1 flag: 00=独立帧(应答), 01=DMX内嵌副本(不答) +0x3C 1 SubSC = 01 +0x3D 1 E1.20 msgLen(非命令字节!) +0x3E 6 目标灯具 UID (发现时为分支下界, 全范围=ffffffffffff) +0x44 6 主机 RDM 控制器 UID (bridge-id, 453a7fffffff) +0x4A 1 TN (Transaction Number) +0x4F 1 Command Class: 0x10=DISCOVERY / 0x20=GET / 0x30=SET +0x50 2 ★ PID (BE) +0x52 1 PDL;0x53+ PD;末尾 checksum (BE) +``` + +### 5.4 DISC 脉冲(DISC_UNIQUE_BRANCH 响应)—— 编码已破解 + +脉冲不是标准 RDM 包,是 E1.20 发现响应的 9-bit 线路编码(17B): + +``` +AA 分隔符前缀 +<16B: 8×2B 编码 UID> 每字节 b → byte1=0xAA|(b&0x55), byte2=0x55|(b&0xAA) + (GF(2) 仿射求解, 13 样本全过; 解码: b=(b1&0x55)|(b2&0xAA)) +<4B: 2×2B 编码 checksum> checksum=16 位编码后 UID 字节的和, 先高字节后低字节 +``` + +- 脉冲帧 e:net 序号(0x20)= **设备本地自增序号**(非回显查询!),每次 +1..3。 +- 实测脉冲时延 2-48ms(中位 29ms);应答率 ~40%(967/2419,含 mute 效果)。 + +### 5.5 发现/验证语义(实测裁决) + +| 主机查询 | 真机应答 | 说明 | +|---|---|---| +| cmd=24 全范围 UNIQUE_BRANCH (UID=ffffffffffff) | DISC 脉冲(59B 负载) | 未静音灯具应答 | +| cmd=18 PID=**0002** (DISC_UN_MUTE) | **70B 参数 ACK**: `CC=11 PID=0002 PDL=2 PD=0000` + 有效 checksum,延迟 17-29ms | ⚠️ **不是脉冲!** 虚拟 Butler 曾错答脉冲导致主机死循环重试(947 次),灯具永不注册 | +| cmd=18 PID=**0003** 广播 MUTE (UID=ffff…) | **无应答**(0/10) | 广播静音不应答;780 条 flag=01 0003 为 DMX 内嵌广播 MUTE | +| cmd=18 PID=0060 (DEVICE_INFO) | 87B,每灯具每轮 ~10 次/3min | 注册后主机每 ~1min 重读 | +| cmd=18 PID=00f0 (DMX_START_ADDRESS) | 70B | 同上节奏 | +| cmd=18 PID=0050 (SUPPORTED_PARAMETERS) | 78/178/182B | — | +| cmd=18 PID=80xx(厂商-specific 40 余个) | 69-72B,100% 应答 | 延迟 12-35ms(中位 29ms) | + +**主机 RDM 流程**:后台持续全范围发现(~14-24/s)→ 对新 UID 发 UN_MUTE(0002) 验证 +→ 拿到合法 ACK 后读 DEVICE_INFO(0060)/标签/参数注册进拓扑 → 每 ~1min 轮询。 +**若 0002 应答非法,主机永远停在 0002 重试,不会进入参数读取阶段。** + +### 5.6 SET 写路径(★2026-09-10 set123b.pcap 实证,GUI 改地址触发) + +**抓取方法**:SymphoLight RDM 窗口改真机灯具 DMX 地址(NFM680S `010734880900` +111→114→111→112),双网段同抓。 + +**SET 请求**(主机→Butler,1084B 事件帧,flag=00): +- CC=**0x30**@0x4F、PID=00f0@0x50 (BE)、PDL=2@0x52、PD@0x53 = **DMX 地址 BE u16** +- 0x3D msgLen=26 (=24+2),0x34 RDM 长=27 (=msgLen+1) +- 完整布局与 GET 相同(§4.2/§5.3),仅 CC/PDL/PD 不同 + +**SET 应答**(Butler→主机,68B 负载): +- CC=**0x31**@0x3E (SET_COMMAND_RESPONSE)、RT=**00 (ACK)**@0x3A、**PDL=0**(无参数数据) +- TN 回显(查询 0x4A → 应答 0x39)、checksum 有效、延迟 18-25ms(RS485 指纹) + +**主机行为**:SET 后 **+33ms 立即 GET 同一 PID 验证**(TN 连续 +1),应答中 +新地址已生效;此后稳态轮询按新地址继续。三次 SET (114/111/112) 模式完全一致。 + +**虚拟 Butler 实现(2026-09-10 上线,GUI 改地址实测通过)**:SET (CC=0x30) 一律回 +68B ACK(`rdm_setack_68.hex` 模板,TN/UID/seq/PID 回显 + checksum 重算,延迟 15-25ms), +并按 `(UID, PID) → PD` 落存 `set_store`;后续 GET 同 PID 时以存储值覆盖模板 PD 再重算 +checksum。**只 ACK 不落存会被主机的 SET→GET 验证识破**(GUI 显示改地址无效)。 +该机制对所有 PID 通用(地址/标签等均可写)。 + +**残余**:厂商 PID 0x0200 族的 SET 写路径未触发(GUI 未暴露对应操作); +IDENTIFY_DEVICE 等其他 SET PID 未专项。 + +--- + +## 6. 端口 1947 —— 密钥/许可报文 + +- 主机→`192.168.123.255` 与 `255.255.255.255` 成对广播,源端口 53685/53687。 +- 负载恒定 40 B,ASCII Base64: + `BlZRQphAGxUbi7at8KEfmdZ7TdSB903b+qoC1RIA` +- Base64 解码 = 30 B 高熵二进制(`06 56 51 42 98 40 1B 15 1B 8B B6 AD F0 A1 1F 99 D6 7B 4D D4 81 F7 4D DB FA AA 02 D5 12 00`)。 +- 长时间观察内容**不变** → 静态许可/身份令牌,非会话密钥。 +- 与 OLA 社区所述 "key message" 一致。DMX 数据并不依赖其加密(见 §4.1 明文结论)。 + +--- + +## 7. 会话建立时序(★2026-09-09 以 cold2.pcap 冷启动实测更新) + +``` +t=0~23s 主机 Butler S2 + │ 01 发现请求 180B 广播(~每3-5s) ─▶│ + │◀────── 01 发现应答 217B (设备名/MAC/IP/ConnectedTo) │ +t=38.087 │ 02 宣告 sub=01 102B ─────────▶│ + │◀────── 02 注册 sub=02 47B (10ms) │ ★ 认领前关键步骤 +t=38.105 │ 02 AECH sub=05 552B ─────────▶│ + │◀────── 02 AERE sub=06 552B (1ms) │ ×3 轮 (38.105/38.111/38.140) +t=40+ │ 04 DMX帧 A(seq) ─────────────▶│ + │ 04 事件帧 B ──────────────────▶│ + │◀─── 04 计数遥测 42B (~115ms) ────│ … A 29.8/s 恒定 + B 随模式 … + │ 04 RDM 查询(cmd24/18) ────────▶│ 后台持续发现/轮询 + │◀─────── 04 RDM 脉冲/参数应答 ────│ 延迟 12-48ms +t=44+ │ 02 心跳 sub=03 36B (每~6s) ───▶│ ★ 设备不应答 +``` + +Butler 若 ~数秒收不到主机数据则自动回落 standalone 模式(播 SD 卡 show)。 + +--- + +## 8. ESP32 模拟可行性结论 + +| 需求 | 结论 | +|------|------| +| 接收 DMX 数据 | ✅ **可行**:明文。监听 UDP 31772,过滤 `454E6574 04000000` + 0x20 处 `0101`,从负载 0x3C 取 1024 B 通道数据 | +| 维持连接 | ⚠️ 需模拟:周期回复 ACK(0x2A 负载、递增序号)+ 状态帧,防止主机判定离线;同时需应答 type=01 发现(259B 应答帧格式见 §3.2) | +| 阻止 standalone 抢发 | 停止收 DMX 后设备(真 Butler)会播 SD show;ESP32 自身无此问题 | +| 1947 端口 | 无需实现(仅主机广播,设备不监听) | +| 加密 | 无需解密——通道数据明文 | + +ESP32 最小实现 = **被动接收**:绑 31772 收形态A 帧、取 0x3C 起 1024 B 即得到全通道值; +要长期稳定在线再补 ACK/状态/发现应答三类模拟帧(结构均已在本文档给出)。 + +--- + +## 9. 抓包样本索引 + +| 文件 | 内容 | +|------|------| +| `C:\Users\tonycao\AppData\Local\Temp\opencode\discovery.pcap` | 启动/发现阶段 56 帧 | +| `C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s.pcap` | 驱动阶段 40s,5903 帧 | +| `dmx_one_hex.txt` | 1126 B DMX 帧完整 hex ×2 | +| `rdm3.pcap` | RDM 会话(Enable RDM + Discover,DUT 停发后抓取) | +| `f686.txt` | 686 B 宣告帧完整 hex | +| `cold2.pcap` ★ | **真机 .1 冷启动全程**(168.7s/19471 帧:发现→注册→AECH→RDM),2026-09-09 | +| `fix1.pcap` ★ | **虚拟 Butler .7 验证**(58 网段,P0 修复后主机读 DEVICE_INFO 全程),2026-09-09 | +| `ss123.pcap` / `ss58.pcap` ★ | **稳态双网段对比**(120s:.1 真机 vs .7 虚拟),2026-09-10 | +| `fix2.pcap` / `fix3.pcap` ★ | **P1 修复后 .7 验证**(cmd24 收敛/稳态轮询/42B 遥测),2026-09-10 | + +## 10. 待确认项(低置信度字段) + +- ~~0x22 处 8 B 字段~~ → **已破解**(§4.1,2026-09-10):0x22/0x26 = 两端口首通道 + 保护字节 `(CH-1) XOR K`(K 会话相关,三会话验证 2000/2000);残余开放: + 0x23-0x25/0x27-0x29 共 6B 为会话密钥派生滚动值(标准哈希/计数器/时间戳 + 全部排除),破解需先还原 AECH 密钥协商;K 与 AECH 凭证字节的对应关系未考 +- ~~形态 B 事件帧内部布局~~ → **已完全解码**(§4.2,2026-09-10):事件头 + + 标准 E1.20 消息 @0x3C + 通道图案填充(缓冲复用,无协议语义); + 事件序号 u32@0x22 严格 +1(2962/2962,旧"偶发交换/重号"为抓包顺序现象)。 + 残余细节:0x38 疑似总线/端口号(样本恒 1,多端口语境未验证);flag=01 + 内嵌副本的生成规则(srcUID 末字节 fe、port=01) +- Butler 应答帧(217B 负载)内的版本/时序参数块逐字段含义 +- "EUCE" 帧内部布局(仅确认魔数与 05 版本字节) +- ~~RDM 数据区内层字段序~~ → 已完全解码,标准 E1.20(§5.2,2026-09-09) +- cmd=19/1a(SET 类,厂商 PID 0x0200 族)的请求/应答布局(§5.6) +- RDM / show 上传 / 配置写回报文:RDM 已覆盖(§5),show 上传与配置写回未触发 + +--- + +## 11. 虚拟 Butler 实现与验证(★2026-09-09) + +### 11.1 已实现能力(Rust,enet-validator) + +- 发现应答(217B,Connected To 动态取请求方主机 IP/MAC,网段自适应) +- 注册应答(sub=02,47B) +- **AECH 认领本地计算(2026-09-10 起)**:虚拟设备收 AECH → 取 nonce@0x88-0x8B(LE) → + 本地算 `G(nonce)`(§3.7,无密钥)写入 AERE 0xC0-0xC3 → 直接应答,**无需真机中继/桥**。 + (旧中继路径保留:经源 113 socket 转发真机 .1 算 AERE;非 123 网段曾需 Windows 桥。) +- RDM 生成式应答:全新 UID 灯具的 DISC 脉冲(§5.4 编码器)+ pid_map 模板参数应答 +- 高速主循环:socket 读超时 2ms(原 80/100ms 时吞吐仅 ~5/s,无法支撑发现收敛) +- DMX 接收:形态 A 帧按 §4.1 布局;0x22/0x26 = 每端口随机 r,通道数据经 + zPprv0TXRRW 混淆 → **已实现实时解密**(AECH 认领时取 B12=质询 0x8C-0x8F, + `agios_dmx_decrypt` 还原真实通道值后打日志;旧日志里的 "CH1-16" 递减序列 + 实为 blackout 全零场景的密文形态) + +### 11.2 灯具注册根因与修复(P0) + +**症状**:虚拟 Butler 被认领、DMX 流入、主机能发现全新 UID,但 RDM 拓扑里 +.7 的总线始终 "no devices";主机只对 .7 反复发 PID=0002(947 次)从不读 DEVICE_INFO。 + +**根因(cold2.pcap 实证)**: +1. 虚拟 Butler 把 PID=0002(DISC_UN_MUTE)错答成 DISC 脉冲; + 真机应答为 70B 参数 ACK(§5.5)。主机拿不到合法 UN_MUTE ACK → 死循环重试。 +2. 所有模板应答 patch UID 后未重算 E1.20 checksum(§5.2)→ 主机按规范丢弃。 + +**修复**:0002 走 pid_map 70B 模板 + TN 回显(查询 0x4A → 应答 0x39)+ +全部 RDM 应答 patch 后重算 checksum。 + +**验证结果(fix1.pcap + GUI)**:主机对 .7 的查询变为 `0002×772 + 0060×771` +(3 个新 UID 各 257 次 DEVICE_INFO,0002:0060 = 1:1 与真机模式一致), +.7 应答 checksum 777/777 有效;SymphoLight RDM 拓扑显示 +**Butler S2(.7) → Port 1 (U1) — 3 devices**,灯具注册成功。 + +### 11.3 稳态对齐修复(P1,2026-09-10 全部落地) + +**根因(ss123/ss58 双网段对比实证)**:cmd=24 是标准 DISC_UNIQUE_BRANCH +(分支范围 lo@0x53-0x58 / hi@0x59-0x5E),真机灯具只答 UID 在范围内的查询 +(二分收敛);虚拟 Butler 无视范围全答 → 主机二分永不排空 → 发现洪水 +51/s(真机仅 16/s)→ 稳态轮询对 .7 永不启动。 + +**已修复项**: +1. cmd24 分支范围过滤,仅范围内灯具各答一个脉冲(延时入队); +2. RDM 应答延迟队列:脉冲 2-40ms / 参数 15-40ms(LCG),对齐真机 RS485 + 延迟指纹(中位 29ms); +3. 心跳 sub=03 改静默(真机从不应答);删除 84B 突发遥测; +4. 参数应答按镜像灯具取帧池:FIXTURE_MIRROR 130→125 / 131→80900 / + 132→26c9,cmd=19/1a 再按 PD[0](传感器序号)匹配; +5. 42B 改为周期遥测(§4.3):event_ctr 累计事件B帧,~115ms 上报一次, + 不再即时 ACK。 + +**验证(fix2/fix3.pcap)**:cmd24 率 51/s → **8.5/s**(真机 15.7/s,洪水消除); +主机对 .7 启动稳态轮询:0002/0060 每灯具每周期 + **0201 SENSOR_VALUE +23/17/4 次,与镜像灯具传感器数完全一致**;脉冲 4.9/s(真机 6.2/s); +42B 遥测 6.5/s 计数严格递增(真机 8.7/s);RDM 应答 checksum 全有效。 + +**残余开放项(不影响注册与轮询)**:42B 精确节流规则未明(真机差值 +1~+10 +不平滑);mute 状态机语义未明(真机灯具在广播 0003 后反而恢复应答,未实现 +mute 亦无碍);cmd=19/1a 中 SET 类厂商 PID(0x0200 族)写路径未专项。 + +### 11.4 运维要点 + +- 模板全部 `include_str!` 编译期内嵌,改 samples/*.hex 必须重新编译部署 +- 部署:分块 base64 + Invoke-SSHCommand(Set-SFTPItem 不可用);.7 用 `~/run_butler.sh` 启动 +- 进程窗口:用 86400s(24h)避免反复到期退出 +- ~~桥若退出,.7 认领即卡死~~ → 2026-09-10 起本地计算 G(§3.7),桥/中继不再是必需 +- .200(Windows 次 IP)的 Butler 必须与 .7 错开 MAC,避免冲突 +- 离线分析:`enet-validator dmxdec ` — DMX 帧 zPprv0TXRRW 解密 + 加解密回环验证 + (需 TSV 含 AECH 质询以取 B12);`dmxroll ` — 滚动值/保护字节历史分析; + tshark 导出 TSV 经 PowerShell 重定向会落为 UTF-16LE(带 BOM) + +### 11.5 DMX 帧解码里程碑(2026-09-10,§4.1)★混淆算法完全破解 + +- 60B 帧头逐字段反编译对账(`vpFbA8r8ibe5ncT1Lum7` 结构体,Pack=2): + 帧序号 u32@0x1A、cbY0u×2@0x20、r1/r2@0x22/0x26、端口ID@0x2C/0x2E、 + 端口索引@0x30/0x32、通道数 512 LE@0x34/0x36; +- **通道数据非明文**——zPprv0TXRRW 逐帧混淆:rot=(B12^r)&511,r=Random.Next() 每帧每端口, + B12=AECH 实例随机;cold2+fix1 共 5674 帧全部解密为稳定 blackout 场景, + 加解密回环 5674/5674 逐字节一致; +- 旧结论 "(CH-1)^K 保护字节/6B 滚动值/会话密钥 K" 全部证伪——K=~B12&0xFF 为 + 计数器结构的涌现巧合(0.5% 失配=rot%256==0 边界);滚动值=Random.Next() 本身; +- 主机侧复现要素齐备:B12 主机自选、r 随机、加解密双向实现(`dmxdec` 验证); +- 帧率 29.8/s 恒定,A:B 比例随模式变化(推翻"A·B·B 固定节奏 ~40Hz")。 + +### 11.6 主机侧等效播放实测(2026-09-10,档 4 首个里程碑)★ + +`dmxsend` 子命令实现**完整主机侧流程**(不依赖 SymphoLight): +1. sub=01 宣告 → 收 89B 注册应答 ✓ +2. AECH 认领(nonce/B12 均自选随机)→ .7 本地 G 应答 `AERE[0xC0]==G(nonce)` ✓ +3. 30fps 发送 zPprv0TXRRW 加密 DMX(583 帧,高识别度图案 ch1=AA/ch2=55/ch3-10=1..128/ + ch101=EF/ch513=77/ch1024=33)→ **.7 实时解密日志精确还原** `CH1-16: aa550102040810204080…` ✓ + +全链路(认领→注册→加密→解密)闭环自证。对真机 .1 仿冒的唯一额外约束: +AECH 质询须源自 192.168.123.113(真机固件过滤,§3.7)。 + +### 11.7 hostdemo:真机 .1 主机侧仿真(2026-09-10 晚)★进行中 + +`hostdemo [目标IP] [秒]` 子命令(src/hostdemo.rs):完整主机等效流程——发现 → +冷启动前奏(discovery/announce 广播 ×10s + EUCE)→ 102B 身份帧 → AECH×3 → +DISC_UN_MUTE 广播 → UNIQUE_BRANCH 二分遍历(含 DISC 脉冲解码,§5.4 逆运算) +→ 逐 UID DISC_MUTE → GET DEVICE_INFO/DMX_START_ADDRESS → 30fps 加密 DMX 呼吸控制。 + +**实测进展**: +- ✅ 发现应答(217B)每次都正常——设备在任何状态下都答发现 +- ✅ **AECH 认领真机 3 次完整通过**(3 轮 G(nonce) 验证 PASS,nonce/B12 自选随机) +- ⏳ RDM 发现/控制段:尚未跑到(受下方认领门控阻断) +- cold2 时序佐证:首个 RDM 请求在认领后 54ms 发出;首个 RDM 应答与首个 42B + 遥测同步出现于认领后 1.37s(此前主机已连发 ~14 次 UNIQUE_BRANCH—— + 冷启动应答延迟大属正常,hostdemo 每分支按 4 次 ×250ms 重试) + +**★新发现:真机认领门控/锁死行为(§3.7 补遗)** +连续 3 次成功认领但**不发任何会话流量**(无 DMX/心跳)后,.1 进入锁定态: +- 对 ident102 不回 47B 注册应答、对 AECH 质询完全不答—— + **连 cold2 原始 ident/质询的逐字节重放也一并忽略**(设备端状态,非帧构造问题) +- 发现请求(type=1)仍正常应答(225B) +- SymphoLight 于锁定约 55-70 分钟后认领成功(19:2x,期间它先广播了 >90s 才发起) +- SymphoLight 关闭(20:04,无释放帧、无 AECH,DMX 流直接消失)后 15 分钟仍锁定 +- 暂定模型:**会话保持 ≈ 55-60 分钟**(自最后一次会话流量起算),期间拒答一切新认领 + +--- + +## 12. 缺口清单与攻关路线图(2026-09-10) + +### 档 1 — 核心缺口(阻塞完整复现) + +| 缺口 | 位置 | 影响 | +|---|---|---| +| ~~AECH 密钥协商/凭证算法~~ | §3.7 | **已破解(2026-09-10)**:反编译 SymphoLight Ecue.Devices.dll 得 AgiosElias.G —— 无密钥公开算法,本地计算应答,10/10 样本 + 在线 5/5 验证。**同日 SymphoLight 重启无中继终极实测通过**(14:29:35,3 轮本地 G 应答 + 89B 注册 + DMX 恢复 + RDM 灯具重新注册,Windows 桥已退役)| +| ~~会话 K 派生关系~~ | §4.1 | **证伪并消解(2026-09-10)**:K 不是密钥——是 zPprv0TXRRW 计数器结构的涌现巧合,K=~B12&0xFF(cold2/fix1 精确吻合)| +| ~~DMX 帧 6B 滚动值/通道加密~~(0x22-0x29 + 0x3C/0x23C 数据区) | §4.1 | **已破解(2026-09-10)**:r=Random.Next() 每帧每端口;数据 zPprv0TXRRW 混淆,rot=(B12^r)&511;5674 帧解密+回环 100% | +| ~~形态 B 事件帧内部布局~~ | §4.2 | **已完全解码(2026-09-10)**:事件头 + 标准 E1.20 @0x3C + 通道图案填充(无协议语义);残余细节见 §10 | + +### 档 2 — 写路径/配置类(从未触发过捕获) + +- ~~RDM SET 写路径(DMX_START_ADDRESS)~~ → **已解码(§5.6,2026-09-10)**: + CC=0x30 请求 / CC=0x31 ACK PDL=0 应答 / SET 后 +33ms GET 验证; + 残余:厂商 PID 0x0200 族 SET、IDENTIFY_DEVICE 等其他 SET PID +- **show 上传 / SD 卡 / standalone 配置写回**:协议存在性已知,报文从未捕获 +- **节点序号(0x2E)分配机制**:仅从 2 个样本推断(.1=2/.7=3),注册顺序还是工程配置未验证 + +### 档 3 — 辅助帧细节 + +- **EUCE 帧**(§3.5):仅确认魔数 + 版本字节,内部布局空白 +- **217B 发现应答**(§3.2):版本/时序参数块逐字段含义不全 +- **1947 许可报文**(§6):静态 token 已记录,Butler 是否/如何校验未知 +- 行为指纹级小问题(不影响功能):42B 遥测精确节流规则(差值 +1~+10)、形态 B 序号偶发交换/重号、RDM mute 状态机语义 + +### 档 4 — 主机侧仿真(协议要素齐备,首个里程碑已实测 ★§11.6) + +文档主要覆盖**设备侧**(Butler 视角收/答)。主机侧全部协议要素已解码且 +`dmxsend` 已实测闭环(§11.6:认领→注册→加密 DMX→对端精确解密); +剩余工作为产品化(工程解析、场景调度、RDM 主轮询、ESP32 移植)。 +§8 的 ESP32 章节也只写了"消费 DMX"的被动模式。 + +### 建议攻关顺序 + +1. ~~形态 B 事件帧布局~~ —— **已完成(2026-09-10,§4.2)**; +2. ~~AECH 密钥协商~~ —— **已完成(2026-09-10,§3.7:反编译 AgiosElias,无密钥 G 算法)**; +3. ~~RDM SET 写路径~~ —— **已完成(2026-09-10,§5.6)**; +4. ~~K 派生 + 6B 滚动值 + DMX 加密~~ —— **已完成(2026-09-10,§4.1)**:反编译 + ScheduleUpdate 构建链,zPprv0TXRRW 混淆完全破解(rot=(B12^r)&511), + "K" 证实为 ~B12&0xFF 涌现巧合;5674 帧解密+回环 100%,加解密双向实现; +5. 顺带修正:0x3D "cmd 字节"实为 E1.20 msgLen(§5.3 更正)。 + +**至此档 1 全部关闭。** 剩余开放项均为非阻塞细节(档 2/3):show 上传/SD 写回 +(未捕获)、节点序号分配机制、EUCE 内部布局、42B 节流细则、mute 状态机。 diff --git a/docs/rust_verification_plan.md b/docs/rust_verification_plan.md new file mode 100644 index 0000000..4a5197e --- /dev/null +++ b/docs/rust_verification_plan.md @@ -0,0 +1,85 @@ +# e:net 协议 Rust 独立验证程序 —— 实施计划 + +> 目标:关闭 SymphoLight 5.5,用 Rust 程序独立扮演 e:cue 主机,按《enet_protocol.md》 +> 对每一条协议做**实测验证**(连接真机 Butler S2 192.168.123.1,以其真实应答为判据)。 + +--- + +## 0. 前置条件 + +| 项 | 动作 | 状态 | +|----|------|------| +| P1 | 关闭 SymphoLight 5.5(PID 检查 + Stop-Process) | 待执行 | +| P2 | 确认 Butler 在线(ping 192.168.123.1) | 已通 | +| P3 | 网口"以太网 3" = 192.168.123.113/24(Rust 程序绑定此地址) | 已确认 | +| P4 | 抓包样本齐备(discovery/dmx40s/rdm3 classic pcap)用于报文参数提取 | 已有 | +| P5 | 注意:SymphoLight 关闭后 Butler 回落 standalone 播 SD show(DMX 输出仍活动),Rust 程序需完成"发现→认领"接管 | 已知 | + +--- + +## 1. 技术选型与工程结构 + +``` +enet-validator/ +├── Cargo.toml # clap(CLI) anyhow hex rand;(无 async,std::net::UdpSocket 足够) +├── src/ +│ ├── main.rs # 子命令入口:discover / dmux / session / rdm / all +│ ├── enet.rs # ENet 帧构造/解析(§2 通用头:magic+type+port+len) +│ ├── discovery.rs # §3:type=01 发现请求/应答解析、宣告帧、type=02 心跳、EUCE 帧 +│ ├── dmx.rs # §4.1:形态A DMX 帧(60B头+1024B通道,序号0x1A、时间戳0x22) +│ ├── event.rs # §4.2-4.4:形态B 事件帧、ACK 配对、状态帧解析 +│ ├── rdm.rs # §5:RDM 事件帧封装(0x4A cc01)、应答解析(0x28长度+cc01)、UID/TN +│ └── report.rs # 验证报告输出(JSON + 控制台 PASS/FAIL) +└── samples/ # 从 pcap 提取的基准报文(hex 常量,参数化模板) +``` + +报文构造策略:**以实测样本为模板**(非凭空拼字段)——从 pcap 提取 222B 发现请求、 +686B 宣告、1126B DMX 帧、1126B 事件帧的原始字节,替换可变字段(序号/时间戳/通道值)。 + +--- + +## 2. 验证项分解(对应协议文档章节,判据=Butler 实际行为) + +| ID | 协议条目(文档章节) | 程序动作 | PASS 判据 | +|----|---------------------|----------|-----------| +| T1 | 发现请求 222B 广播(§3.1→§3.2) | 发 type=01 请求 → 监听 31772 | 收到 259B 应答;含 ASCII "Butler S2"、MAC `00:16:1c:f1:2e:39`、负载长字段 0xD9 ✓ | +| T2 | 主机宣告 686B(§3.3) | 发 type=01 宣告(主机名GUID/时间/密钥材料按模板) | 不破坏会话;后续 Butler 状态流正常 | +| T3 | type=02 心跳 78B(§3.4) | 发主机 IP+MAC 标识帧 | 同上,无异常 | +| T4 | EUCE 帧 582B(§3.5) | 发 EUCE 魔数帧(版本05@0x0C) | 同上(观察性验证) | +| T5 | DMX 数据帧(§4.1) | 按 A·B·B 节奏发形态A:序号 0x1A 递增、0x22 时间戳变化、0x3C 起 1024B 通道值(先全0→渐变图案) | Butler 持续 ACK/状态应答(不回落 standalone);7 段显示离开 standalone | +| T6 | 事件帧 + ACK 配对(§4.2/4.3) | 每帧 DMX 配 2 事件帧,记录事件序号 0x22 与 ACK 序号 0x20 | ACK 配对率 ≥95%;序号规律与文档一致 | +| T7 | 状态帧解析(§4.4) | 被动接收 171-247B 状态帧 | 长度字段 0x14 自洽率 100%;"Butl"前缀 100% | +| T8 | 会话保持(§6) | 持续会话 ≥60s | 全程无中断;通道值变化(全0↔全255↔渐变)期间 ACK 持续 | +| T9 | 回落行为(§6 注) | 停发 10s 后观察 | Butler 回落 standalone(状态帧模式变化)→ 再恢复会话成功 | +| T10 | RDM 应答解析(§5.1-5.2) | 主动监听(若 Butler 已 Enable RDM)或发送 RDM 请求事件帧(0x4A cc01 模板) | 收到 cc01 起始帧;0x28 长度自洽;提取灯具 UID | +| T11 | 写地址探索(§5.4) | 在 RDM 请求模板中尝试标准 PID 0x0330 SET 与私有 PID 0x2100 族 | 观察 Butler 是否下发 RDM 应答(记录 PID 行为,产出逆向数据) | + +判据统一由程序自动记录(报文 hex + 时间戳 + PASS/FAIL),输出 `validation_report.json`。 + +--- + +## 3. 执行阶段 + +- **阶段0 准备**(P1-P5):关 SymphoLight、(可选)抓 10s standalone 对照流量存档。 +- **阶段1 骨架**:Rust 工程 + pcap 样本提取器(PowerShell 导出基准 hex 常量)。 +- **阶段2 单向验证**:T1→T2/T3/T4(每项独立子命令,互不依赖)。 +- **阶段3 会话验证**:T5→T6→T7→T8(核心:接管 Butler,验证 DMX/ACK/状态全链路)。 +- **阶段4 行为验证**:T9 回落与恢复。 +- **阶段5 RDM 探索**:T10/T11(可选,依赖 Butler RDM 端口已启用状态)。 +- **阶段6 报告**:validation_report.json + 更新 enet_protocol.md(修正/补充实测发现)。 + +## 4. 风险与对策 + +| 风险 | 对策 | +|------|------| +| Butler 在 standalone 播 show 干扰观察 | T5 认领成功即接管 DMX;必要时临时拔 SD 卡(用户决定) | +| DUT(ESP32-P4) 仍在总线上 | 验证前确认 DUT 状态:保持暂停(download mode)或已恢复;勿冲突 | +| 报文字段拼错导致 Butler 无响应 | 全部以 pcap 样本为模板参数化;T1 失败时 diff 实发与样本 | +| 端口 31772 被占(SymphoLight 残留) | P1 确认进程退出;程序绑定失败即报错 | +| RDM 未启用时 T10/T11 无效 | 作为可选阶段,跳过不影响主结论 | + +## 5. 交付物 + +1. Rust 工程(可执行 `enet-validator.exe`,子命令式) +2. `validation_report.json` + 控制台报告 +3. `enet_protocol.md` 增补(以 Rust 实测修正/确认各字段) diff --git a/scripts/Verify-EnetProtocol.ps1 b/scripts/Verify-EnetProtocol.ps1 new file mode 100644 index 0000000..9e0197f --- /dev/null +++ b/scripts/Verify-EnetProtocol.ps1 @@ -0,0 +1,1902 @@ +# Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2) +param( + [string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap", + [string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap" +) +$ErrorActionPreference = 'Stop' +$script:pass = 0; $script:fail = 0 +function Check($name, $cond, $detail) { + if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) } + else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) } +} +function Read-Pcap($path) { + $b = [IO.File]::ReadAllBytes($path) + function U32([byte[]]$x, [int]$o, [bool]$sw) { + if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] } + return [BitConverter]::ToUInt32($x, $o) + } + $magic = U32 $b 0 $false + $swap = ($magic -eq 0xa1b2c3d4) + $pkts = New-Object System.Collections.Generic.List[object] + $o = 24 + while ($o + 16 -le $b.Length) { + $incl = U32 $b ($o+8) $swap + if ($o + 16 + $incl -gt $b.Length) { break } + $data = New-Object byte[] $incl + [Array]::Copy($b, $o+16, $data, 0, $incl) + $pkts.Add([pscustomobject]@{ Len = $incl; Data = $data }) + $o += 16 + $incl + } + return $pkts +} +function UdpPayload($pkt) { + $d = $pkt.Data + if ($d.Length -lt 42) { return $null } + $ethType = ([uint32]$d[12] -shl 8) -bor $d[13] + $off = 14 + if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 } + if ($ethType -ne 0x0800) { return $null } + $ihl = ($d[$off] -band 0x0F) * 4 + if ($d[$off + 9] -ne 17) { return $null } + $ipStart = $off; $udp = $off + $ihl + $src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15] + $dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19] + $sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1] + $dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3] + $pl = New-Object byte[] ($d.Length - $udp - 8) + [Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length) + return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl } +} +function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) } +function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) } +function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' } + +Write-Host "=== 载入 $Pcap ===" +$pkts = Read-Pcap $Pcap +Write-Host ("总包数: {0}" -f $pkts.Count) +$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } } + +# ---- A1: 31772 帧魔数只允许 ENet / EUCE ---- +$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 }) +$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 } +$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr" + +# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ---- +$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) +$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count) + +# ---- A3: ENet 帧类型仅 1/2/4 ---- +$types = $enet | Group-Object { LE32 $_.Payload 4 } +$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr" + +# ---- A4: 形态A DMX 帧 ---- +$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 }) +Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count) +Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024" +$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) }) +$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } } +Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count) +$tsSame = 0 +for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } } +Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0" + +# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ---- +$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' }) +$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' }) +Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count) +$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) }) +$hbMono = $true; $swaps = 0 +for ($i=1; $i -lt $hostSeq.Count; $i++) { + if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序) + elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败 +} +Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps) +$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) }) +$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count +Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched) +$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' }) +Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count) + +# ---- A6: 1947 密钥报文 ---- +$k = @($msgs | Where-Object { $_.Dport -eq 1947 }) +if ($k.Count -gt 0) { + $h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload) + $allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0 + Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0) + Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌" +} else { Check "A6 1947负载恒定" $false "| 无样本" } + +# ---- A7: A·B·B 节奏 ---- +$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21) +$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join '' +Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat" + +# ---- C1: type=02 主机标识心跳 ---- +$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 }) +$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' }) +Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count) + +# ---- C2: EUCE 帧 ---- +$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' }) +$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 }) +Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count) + +# ---- 发现阶段 ---- +Write-Host "`n=== 载入 $PcapDiscovery ===" +$pkts2 = Read-Pcap $PcapDiscovery +$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } } +$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 }) +$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 }) +Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count) +$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 }) +$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' }) +Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count) +$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致" +$hasHost = $false +foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } } +Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在" + + +# ---- R1-R3: RDM 帧 (rdm3.pcap) ---- +Write-Host "`n=== 载入 RDM 抓包 ===" +$rdmPcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\rdm3.pcap" +if (Test-Path $rdmPcap) { + $pkts3 = Read-Pcap $rdmPcap + $msgs3 = @(); foreach ($p in $pkts3) { $u = UdpPayload $p; if ($u) { $msgs3 += $u } } + $rdm = @($msgs3 | Where-Object { # Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2) +param( + [string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap", + [string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap" +) +$ErrorActionPreference = 'Stop' +$script:pass = 0; $script:fail = 0 +function Check($name, $cond, $detail) { + if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) } + else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) } +} +function Read-Pcap($path) { + $b = [IO.File]::ReadAllBytes($path) + function U32([byte[]]$x, [int]$o, [bool]$sw) { + if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] } + return [BitConverter]::ToUInt32($x, $o) + } + $magic = U32 $b 0 $false + $swap = ($magic -eq 0xa1b2c3d4) + $pkts = New-Object System.Collections.Generic.List[object] + $o = 24 + while ($o + 16 -le $b.Length) { + $incl = U32 $b ($o+8) $swap + if ($o + 16 + $incl -gt $b.Length) { break } + $data = New-Object byte[] $incl + [Array]::Copy($b, $o+16, $data, 0, $incl) + $pkts.Add([pscustomobject]@{ Len = $incl; Data = $data }) + $o += 16 + $incl + } + return $pkts +} +function UdpPayload($pkt) { + $d = $pkt.Data + if ($d.Length -lt 42) { return $null } + $ethType = ([uint32]$d[12] -shl 8) -bor $d[13] + $off = 14 + if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 } + if ($ethType -ne 0x0800) { return $null } + $ihl = ($d[$off] -band 0x0F) * 4 + if ($d[$off + 9] -ne 17) { return $null } + $ipStart = $off; $udp = $off + $ihl + $src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15] + $dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19] + $sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1] + $dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3] + $pl = New-Object byte[] ($d.Length - $udp - 8) + [Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length) + return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl } +} +function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) } +function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) } +function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' } + +Write-Host "=== 载入 $Pcap ===" +$pkts = Read-Pcap $Pcap +Write-Host ("总包数: {0}" -f $pkts.Count) +$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } } + +# ---- A1: 31772 帧魔数只允许 ENet / EUCE ---- +$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 }) +$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 } +$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr" + +# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ---- +$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) +$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count) + +# ---- A3: ENet 帧类型仅 1/2/4 ---- +$types = $enet | Group-Object { LE32 $_.Payload 4 } +$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr" + +# ---- A4: 形态A DMX 帧 ---- +$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 }) +Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count) +Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024" +$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) }) +$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } } +Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count) +$tsSame = 0 +for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } } +Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0" + +# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ---- +$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' }) +$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' }) +Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count) +$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) }) +$hbMono = $true; $swaps = 0 +for ($i=1; $i -lt $hostSeq.Count; $i++) { + if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序) + elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败 +} +Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps) +$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) }) +$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count +Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched) +$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' }) +Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count) + +# ---- A6: 1947 密钥报文 ---- +$k = @($msgs | Where-Object { $_.Dport -eq 1947 }) +if ($k.Count -gt 0) { + $h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload) + $allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0 + Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0) + Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌" +} else { Check "A6 1947负载恒定" $false "| 无样本" } + +# ---- A7: A·B·B 节奏 ---- +$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21) +$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join '' +Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat" + +# ---- C1: type=02 主机标识心跳 ---- +$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 }) +$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' }) +Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count) + +# ---- C2: EUCE 帧 ---- +$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' }) +$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 }) +Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count) + +# ---- 发现阶段 ---- +Write-Host "`n=== 载入 $PcapDiscovery ===" +$pkts2 = Read-Pcap $PcapDiscovery +$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } } +$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 }) +$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 }) +Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count) +$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 }) +$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' }) +Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count) +$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致" +$hasHost = $false +foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } } +Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在" + +Write-Host "`n========================================" +Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail) +if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" } +.Src -eq '192.168.123.1' -and (LE32 # Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2) +param( + [string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap", + [string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap" +) +$ErrorActionPreference = 'Stop' +$script:pass = 0; $script:fail = 0 +function Check($name, $cond, $detail) { + if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) } + else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) } +} +function Read-Pcap($path) { + $b = [IO.File]::ReadAllBytes($path) + function U32([byte[]]$x, [int]$o, [bool]$sw) { + if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] } + return [BitConverter]::ToUInt32($x, $o) + } + $magic = U32 $b 0 $false + $swap = ($magic -eq 0xa1b2c3d4) + $pkts = New-Object System.Collections.Generic.List[object] + $o = 24 + while ($o + 16 -le $b.Length) { + $incl = U32 $b ($o+8) $swap + if ($o + 16 + $incl -gt $b.Length) { break } + $data = New-Object byte[] $incl + [Array]::Copy($b, $o+16, $data, 0, $incl) + $pkts.Add([pscustomobject]@{ Len = $incl; Data = $data }) + $o += 16 + $incl + } + return $pkts +} +function UdpPayload($pkt) { + $d = $pkt.Data + if ($d.Length -lt 42) { return $null } + $ethType = ([uint32]$d[12] -shl 8) -bor $d[13] + $off = 14 + if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 } + if ($ethType -ne 0x0800) { return $null } + $ihl = ($d[$off] -band 0x0F) * 4 + if ($d[$off + 9] -ne 17) { return $null } + $ipStart = $off; $udp = $off + $ihl + $src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15] + $dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19] + $sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1] + $dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3] + $pl = New-Object byte[] ($d.Length - $udp - 8) + [Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length) + return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl } +} +function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) } +function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) } +function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' } + +Write-Host "=== 载入 $Pcap ===" +$pkts = Read-Pcap $Pcap +Write-Host ("总包数: {0}" -f $pkts.Count) +$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } } + +# ---- A1: 31772 帧魔数只允许 ENet / EUCE ---- +$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 }) +$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 } +$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr" + +# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ---- +$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) +$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count) + +# ---- A3: ENet 帧类型仅 1/2/4 ---- +$types = $enet | Group-Object { LE32 $_.Payload 4 } +$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr" + +# ---- A4: 形态A DMX 帧 ---- +$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 }) +Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count) +Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024" +$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) }) +$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } } +Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count) +$tsSame = 0 +for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } } +Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0" + +# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ---- +$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' }) +$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' }) +Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count) +$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) }) +$hbMono = $true; $swaps = 0 +for ($i=1; $i -lt $hostSeq.Count; $i++) { + if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序) + elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败 +} +Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps) +$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) }) +$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count +Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched) +$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' }) +Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count) + +# ---- A6: 1947 密钥报文 ---- +$k = @($msgs | Where-Object { $_.Dport -eq 1947 }) +if ($k.Count -gt 0) { + $h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload) + $allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0 + Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0) + Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌" +} else { Check "A6 1947负载恒定" $false "| 无样本" } + +# ---- A7: A·B·B 节奏 ---- +$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21) +$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join '' +Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat" + +# ---- C1: type=02 主机标识心跳 ---- +$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 }) +$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' }) +Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count) + +# ---- C2: EUCE 帧 ---- +$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' }) +$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 }) +Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count) + +# ---- 发现阶段 ---- +Write-Host "`n=== 载入 $PcapDiscovery ===" +$pkts2 = Read-Pcap $PcapDiscovery +$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } } +$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 }) +$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 }) +Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count) +$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 }) +$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' }) +Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count) +$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致" +$hasHost = $false +foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } } +Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在" + +Write-Host "`n========================================" +Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail) +if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" } +.Payload 4) -eq 4 -and (Hex # Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2) +param( + [string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap", + [string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap" +) +$ErrorActionPreference = 'Stop' +$script:pass = 0; $script:fail = 0 +function Check($name, $cond, $detail) { + if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) } + else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) } +} +function Read-Pcap($path) { + $b = [IO.File]::ReadAllBytes($path) + function U32([byte[]]$x, [int]$o, [bool]$sw) { + if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] } + return [BitConverter]::ToUInt32($x, $o) + } + $magic = U32 $b 0 $false + $swap = ($magic -eq 0xa1b2c3d4) + $pkts = New-Object System.Collections.Generic.List[object] + $o = 24 + while ($o + 16 -le $b.Length) { + $incl = U32 $b ($o+8) $swap + if ($o + 16 + $incl -gt $b.Length) { break } + $data = New-Object byte[] $incl + [Array]::Copy($b, $o+16, $data, 0, $incl) + $pkts.Add([pscustomobject]@{ Len = $incl; Data = $data }) + $o += 16 + $incl + } + return $pkts +} +function UdpPayload($pkt) { + $d = $pkt.Data + if ($d.Length -lt 42) { return $null } + $ethType = ([uint32]$d[12] -shl 8) -bor $d[13] + $off = 14 + if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 } + if ($ethType -ne 0x0800) { return $null } + $ihl = ($d[$off] -band 0x0F) * 4 + if ($d[$off + 9] -ne 17) { return $null } + $ipStart = $off; $udp = $off + $ihl + $src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15] + $dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19] + $sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1] + $dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3] + $pl = New-Object byte[] ($d.Length - $udp - 8) + [Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length) + return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl } +} +function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) } +function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) } +function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' } + +Write-Host "=== 载入 $Pcap ===" +$pkts = Read-Pcap $Pcap +Write-Host ("总包数: {0}" -f $pkts.Count) +$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } } + +# ---- A1: 31772 帧魔数只允许 ENet / EUCE ---- +$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 }) +$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 } +$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr" + +# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ---- +$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) +$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count) + +# ---- A3: ENet 帧类型仅 1/2/4 ---- +$types = $enet | Group-Object { LE32 $_.Payload 4 } +$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr" + +# ---- A4: 形态A DMX 帧 ---- +$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 }) +Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count) +Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024" +$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) }) +$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } } +Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count) +$tsSame = 0 +for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } } +Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0" + +# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ---- +$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' }) +$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' }) +Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count) +$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) }) +$hbMono = $true; $swaps = 0 +for ($i=1; $i -lt $hostSeq.Count; $i++) { + if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序) + elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败 +} +Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps) +$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) }) +$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count +Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched) +$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' }) +Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count) + +# ---- A6: 1947 密钥报文 ---- +$k = @($msgs | Where-Object { $_.Dport -eq 1947 }) +if ($k.Count -gt 0) { + $h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload) + $allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0 + Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0) + Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌" +} else { Check "A6 1947负载恒定" $false "| 无样本" } + +# ---- A7: A·B·B 节奏 ---- +$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21) +$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join '' +Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat" + +# ---- C1: type=02 主机标识心跳 ---- +$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 }) +$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' }) +Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count) + +# ---- C2: EUCE 帧 ---- +$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' }) +$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 }) +Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count) + +# ---- 发现阶段 ---- +Write-Host "`n=== 载入 $PcapDiscovery ===" +$pkts2 = Read-Pcap $PcapDiscovery +$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } } +$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 }) +$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 }) +Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count) +$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 }) +$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' }) +Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count) +$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致" +$hasHost = $false +foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } } +Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在" + +Write-Host "`n========================================" +Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail) +if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" } +.Payload 0x1A 4) -eq '4275746c' -and (Hex # Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2) +param( + [string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap", + [string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap" +) +$ErrorActionPreference = 'Stop' +$script:pass = 0; $script:fail = 0 +function Check($name, $cond, $detail) { + if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) } + else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) } +} +function Read-Pcap($path) { + $b = [IO.File]::ReadAllBytes($path) + function U32([byte[]]$x, [int]$o, [bool]$sw) { + if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] } + return [BitConverter]::ToUInt32($x, $o) + } + $magic = U32 $b 0 $false + $swap = ($magic -eq 0xa1b2c3d4) + $pkts = New-Object System.Collections.Generic.List[object] + $o = 24 + while ($o + 16 -le $b.Length) { + $incl = U32 $b ($o+8) $swap + if ($o + 16 + $incl -gt $b.Length) { break } + $data = New-Object byte[] $incl + [Array]::Copy($b, $o+16, $data, 0, $incl) + $pkts.Add([pscustomobject]@{ Len = $incl; Data = $data }) + $o += 16 + $incl + } + return $pkts +} +function UdpPayload($pkt) { + $d = $pkt.Data + if ($d.Length -lt 42) { return $null } + $ethType = ([uint32]$d[12] -shl 8) -bor $d[13] + $off = 14 + if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 } + if ($ethType -ne 0x0800) { return $null } + $ihl = ($d[$off] -band 0x0F) * 4 + if ($d[$off + 9] -ne 17) { return $null } + $ipStart = $off; $udp = $off + $ihl + $src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15] + $dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19] + $sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1] + $dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3] + $pl = New-Object byte[] ($d.Length - $udp - 8) + [Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length) + return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl } +} +function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) } +function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) } +function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' } + +Write-Host "=== 载入 $Pcap ===" +$pkts = Read-Pcap $Pcap +Write-Host ("总包数: {0}" -f $pkts.Count) +$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } } + +# ---- A1: 31772 帧魔数只允许 ENet / EUCE ---- +$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 }) +$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 } +$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr" + +# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ---- +$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) +$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count) + +# ---- A3: ENet 帧类型仅 1/2/4 ---- +$types = $enet | Group-Object { LE32 $_.Payload 4 } +$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr" + +# ---- A4: 形态A DMX 帧 ---- +$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 }) +Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count) +Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024" +$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) }) +$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } } +Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count) +$tsSame = 0 +for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } } +Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0" + +# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ---- +$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' }) +$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' }) +Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count) +$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) }) +$hbMono = $true; $swaps = 0 +for ($i=1; $i -lt $hostSeq.Count; $i++) { + if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序) + elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败 +} +Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps) +$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) }) +$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count +Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched) +$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' }) +Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count) + +# ---- A6: 1947 密钥报文 ---- +$k = @($msgs | Where-Object { $_.Dport -eq 1947 }) +if ($k.Count -gt 0) { + $h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload) + $allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0 + Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0) + Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌" +} else { Check "A6 1947负载恒定" $false "| 无样本" } + +# ---- A7: A·B·B 节奏 ---- +$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21) +$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join '' +Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat" + +# ---- C1: type=02 主机标识心跳 ---- +$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 }) +$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' }) +Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count) + +# ---- C2: EUCE 帧 ---- +$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' }) +$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 }) +Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count) + +# ---- 发现阶段 ---- +Write-Host "`n=== 载入 $PcapDiscovery ===" +$pkts2 = Read-Pcap $PcapDiscovery +$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } } +$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 }) +$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 }) +Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count) +$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 }) +$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' }) +Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count) +$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致" +$hasHost = $false +foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } } +Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在" + +Write-Host "`n========================================" +Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail) +if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" } +.Payload 0x2A 2) -eq 'cc01' }) + Check "R1 RDM帧=Butler→主机+cc01" ($rdm.Count -gt 0) ("| n={0} 长度分布={1}" -f $rdm.Count, (($rdm | Group-Object { # Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2) +param( + [string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap", + [string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap" +) +$ErrorActionPreference = 'Stop' +$script:pass = 0; $script:fail = 0 +function Check($name, $cond, $detail) { + if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) } + else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) } +} +function Read-Pcap($path) { + $b = [IO.File]::ReadAllBytes($path) + function U32([byte[]]$x, [int]$o, [bool]$sw) { + if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] } + return [BitConverter]::ToUInt32($x, $o) + } + $magic = U32 $b 0 $false + $swap = ($magic -eq 0xa1b2c3d4) + $pkts = New-Object System.Collections.Generic.List[object] + $o = 24 + while ($o + 16 -le $b.Length) { + $incl = U32 $b ($o+8) $swap + if ($o + 16 + $incl -gt $b.Length) { break } + $data = New-Object byte[] $incl + [Array]::Copy($b, $o+16, $data, 0, $incl) + $pkts.Add([pscustomobject]@{ Len = $incl; Data = $data }) + $o += 16 + $incl + } + return $pkts +} +function UdpPayload($pkt) { + $d = $pkt.Data + if ($d.Length -lt 42) { return $null } + $ethType = ([uint32]$d[12] -shl 8) -bor $d[13] + $off = 14 + if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 } + if ($ethType -ne 0x0800) { return $null } + $ihl = ($d[$off] -band 0x0F) * 4 + if ($d[$off + 9] -ne 17) { return $null } + $ipStart = $off; $udp = $off + $ihl + $src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15] + $dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19] + $sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1] + $dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3] + $pl = New-Object byte[] ($d.Length - $udp - 8) + [Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length) + return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl } +} +function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) } +function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) } +function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' } + +Write-Host "=== 载入 $Pcap ===" +$pkts = Read-Pcap $Pcap +Write-Host ("总包数: {0}" -f $pkts.Count) +$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } } + +# ---- A1: 31772 帧魔数只允许 ENet / EUCE ---- +$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 }) +$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 } +$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr" + +# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ---- +$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) +$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count) + +# ---- A3: ENet 帧类型仅 1/2/4 ---- +$types = $enet | Group-Object { LE32 $_.Payload 4 } +$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr" + +# ---- A4: 形态A DMX 帧 ---- +$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 }) +Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count) +Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024" +$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) }) +$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } } +Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count) +$tsSame = 0 +for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } } +Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0" + +# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ---- +$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' }) +$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' }) +Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count) +$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) }) +$hbMono = $true; $swaps = 0 +for ($i=1; $i -lt $hostSeq.Count; $i++) { + if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序) + elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败 +} +Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps) +$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) }) +$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count +Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched) +$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' }) +Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count) + +# ---- A6: 1947 密钥报文 ---- +$k = @($msgs | Where-Object { $_.Dport -eq 1947 }) +if ($k.Count -gt 0) { + $h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload) + $allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0 + Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0) + Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌" +} else { Check "A6 1947负载恒定" $false "| 无样本" } + +# ---- A7: A·B·B 节奏 ---- +$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21) +$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join '' +Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat" + +# ---- C1: type=02 主机标识心跳 ---- +$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 }) +$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' }) +Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count) + +# ---- C2: EUCE 帧 ---- +$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' }) +$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 }) +Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count) + +# ---- 发现阶段 ---- +Write-Host "`n=== 载入 $PcapDiscovery ===" +$pkts2 = Read-Pcap $PcapDiscovery +$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } } +$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 }) +$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 }) +Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count) +$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 }) +$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' }) +Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count) +$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致" +$hasHost = $false +foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } } +Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在" + +Write-Host "`n========================================" +Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail) +if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" } +.Payload.Length } | ForEach-Object { "$(# Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2) +param( + [string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap", + [string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap" +) +$ErrorActionPreference = 'Stop' +$script:pass = 0; $script:fail = 0 +function Check($name, $cond, $detail) { + if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) } + else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) } +} +function Read-Pcap($path) { + $b = [IO.File]::ReadAllBytes($path) + function U32([byte[]]$x, [int]$o, [bool]$sw) { + if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] } + return [BitConverter]::ToUInt32($x, $o) + } + $magic = U32 $b 0 $false + $swap = ($magic -eq 0xa1b2c3d4) + $pkts = New-Object System.Collections.Generic.List[object] + $o = 24 + while ($o + 16 -le $b.Length) { + $incl = U32 $b ($o+8) $swap + if ($o + 16 + $incl -gt $b.Length) { break } + $data = New-Object byte[] $incl + [Array]::Copy($b, $o+16, $data, 0, $incl) + $pkts.Add([pscustomobject]@{ Len = $incl; Data = $data }) + $o += 16 + $incl + } + return $pkts +} +function UdpPayload($pkt) { + $d = $pkt.Data + if ($d.Length -lt 42) { return $null } + $ethType = ([uint32]$d[12] -shl 8) -bor $d[13] + $off = 14 + if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 } + if ($ethType -ne 0x0800) { return $null } + $ihl = ($d[$off] -band 0x0F) * 4 + if ($d[$off + 9] -ne 17) { return $null } + $ipStart = $off; $udp = $off + $ihl + $src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15] + $dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19] + $sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1] + $dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3] + $pl = New-Object byte[] ($d.Length - $udp - 8) + [Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length) + return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl } +} +function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) } +function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) } +function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' } + +Write-Host "=== 载入 $Pcap ===" +$pkts = Read-Pcap $Pcap +Write-Host ("总包数: {0}" -f $pkts.Count) +$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } } + +# ---- A1: 31772 帧魔数只允许 ENet / EUCE ---- +$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 }) +$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 } +$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr" + +# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ---- +$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) +$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count) + +# ---- A3: ENet 帧类型仅 1/2/4 ---- +$types = $enet | Group-Object { LE32 $_.Payload 4 } +$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr" + +# ---- A4: 形态A DMX 帧 ---- +$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 }) +Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count) +Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024" +$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) }) +$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } } +Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count) +$tsSame = 0 +for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } } +Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0" + +# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ---- +$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' }) +$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' }) +Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count) +$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) }) +$hbMono = $true; $swaps = 0 +for ($i=1; $i -lt $hostSeq.Count; $i++) { + if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序) + elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败 +} +Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps) +$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) }) +$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count +Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched) +$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' }) +Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count) + +# ---- A6: 1947 密钥报文 ---- +$k = @($msgs | Where-Object { $_.Dport -eq 1947 }) +if ($k.Count -gt 0) { + $h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload) + $allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0 + Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0) + Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌" +} else { Check "A6 1947负载恒定" $false "| 无样本" } + +# ---- A7: A·B·B 节奏 ---- +$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21) +$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join '' +Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat" + +# ---- C1: type=02 主机标识心跳 ---- +$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 }) +$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' }) +Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count) + +# ---- C2: EUCE 帧 ---- +$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' }) +$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 }) +Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count) + +# ---- 发现阶段 ---- +Write-Host "`n=== 载入 $PcapDiscovery ===" +$pkts2 = Read-Pcap $PcapDiscovery +$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } } +$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 }) +$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 }) +Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count) +$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 }) +$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' }) +Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count) +$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致" +$hasHost = $false +foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } } +Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在" + +Write-Host "`n========================================" +Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail) +if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" } +.Name)x$(# Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2) +param( + [string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap", + [string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap" +) +$ErrorActionPreference = 'Stop' +$script:pass = 0; $script:fail = 0 +function Check($name, $cond, $detail) { + if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) } + else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) } +} +function Read-Pcap($path) { + $b = [IO.File]::ReadAllBytes($path) + function U32([byte[]]$x, [int]$o, [bool]$sw) { + if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] } + return [BitConverter]::ToUInt32($x, $o) + } + $magic = U32 $b 0 $false + $swap = ($magic -eq 0xa1b2c3d4) + $pkts = New-Object System.Collections.Generic.List[object] + $o = 24 + while ($o + 16 -le $b.Length) { + $incl = U32 $b ($o+8) $swap + if ($o + 16 + $incl -gt $b.Length) { break } + $data = New-Object byte[] $incl + [Array]::Copy($b, $o+16, $data, 0, $incl) + $pkts.Add([pscustomobject]@{ Len = $incl; Data = $data }) + $o += 16 + $incl + } + return $pkts +} +function UdpPayload($pkt) { + $d = $pkt.Data + if ($d.Length -lt 42) { return $null } + $ethType = ([uint32]$d[12] -shl 8) -bor $d[13] + $off = 14 + if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 } + if ($ethType -ne 0x0800) { return $null } + $ihl = ($d[$off] -band 0x0F) * 4 + if ($d[$off + 9] -ne 17) { return $null } + $ipStart = $off; $udp = $off + $ihl + $src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15] + $dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19] + $sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1] + $dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3] + $pl = New-Object byte[] ($d.Length - $udp - 8) + [Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length) + return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl } +} +function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) } +function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) } +function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' } + +Write-Host "=== 载入 $Pcap ===" +$pkts = Read-Pcap $Pcap +Write-Host ("总包数: {0}" -f $pkts.Count) +$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } } + +# ---- A1: 31772 帧魔数只允许 ENet / EUCE ---- +$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 }) +$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 } +$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr" + +# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ---- +$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) +$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count) + +# ---- A3: ENet 帧类型仅 1/2/4 ---- +$types = $enet | Group-Object { LE32 $_.Payload 4 } +$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr" + +# ---- A4: 形态A DMX 帧 ---- +$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 }) +Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count) +Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024" +$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) }) +$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } } +Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count) +$tsSame = 0 +for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } } +Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0" + +# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ---- +$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' }) +$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' }) +Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count) +$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) }) +$hbMono = $true; $swaps = 0 +for ($i=1; $i -lt $hostSeq.Count; $i++) { + if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序) + elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败 +} +Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps) +$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) }) +$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count +Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched) +$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' }) +Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count) + +# ---- A6: 1947 密钥报文 ---- +$k = @($msgs | Where-Object { $_.Dport -eq 1947 }) +if ($k.Count -gt 0) { + $h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload) + $allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0 + Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0) + Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌" +} else { Check "A6 1947负载恒定" $false "| 无样本" } + +# ---- A7: A·B·B 节奏 ---- +$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21) +$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join '' +Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat" + +# ---- C1: type=02 主机标识心跳 ---- +$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 }) +$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' }) +Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count) + +# ---- C2: EUCE 帧 ---- +$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' }) +$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 }) +Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count) + +# ---- 发现阶段 ---- +Write-Host "`n=== 载入 $PcapDiscovery ===" +$pkts2 = Read-Pcap $PcapDiscovery +$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } } +$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 }) +$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 }) +Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count) +$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 }) +$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' }) +Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count) +$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致" +$hasHost = $false +foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } } +Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在" + +Write-Host "`n========================================" +Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail) +if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" } +.Count)" }) -join ',')) + $badRdmLen = @($rdm | Where-Object { [int](LE16 # Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2) +param( + [string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap", + [string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap" +) +$ErrorActionPreference = 'Stop' +$script:pass = 0; $script:fail = 0 +function Check($name, $cond, $detail) { + if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) } + else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) } +} +function Read-Pcap($path) { + $b = [IO.File]::ReadAllBytes($path) + function U32([byte[]]$x, [int]$o, [bool]$sw) { + if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] } + return [BitConverter]::ToUInt32($x, $o) + } + $magic = U32 $b 0 $false + $swap = ($magic -eq 0xa1b2c3d4) + $pkts = New-Object System.Collections.Generic.List[object] + $o = 24 + while ($o + 16 -le $b.Length) { + $incl = U32 $b ($o+8) $swap + if ($o + 16 + $incl -gt $b.Length) { break } + $data = New-Object byte[] $incl + [Array]::Copy($b, $o+16, $data, 0, $incl) + $pkts.Add([pscustomobject]@{ Len = $incl; Data = $data }) + $o += 16 + $incl + } + return $pkts +} +function UdpPayload($pkt) { + $d = $pkt.Data + if ($d.Length -lt 42) { return $null } + $ethType = ([uint32]$d[12] -shl 8) -bor $d[13] + $off = 14 + if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 } + if ($ethType -ne 0x0800) { return $null } + $ihl = ($d[$off] -band 0x0F) * 4 + if ($d[$off + 9] -ne 17) { return $null } + $ipStart = $off; $udp = $off + $ihl + $src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15] + $dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19] + $sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1] + $dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3] + $pl = New-Object byte[] ($d.Length - $udp - 8) + [Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length) + return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl } +} +function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) } +function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) } +function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' } + +Write-Host "=== 载入 $Pcap ===" +$pkts = Read-Pcap $Pcap +Write-Host ("总包数: {0}" -f $pkts.Count) +$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } } + +# ---- A1: 31772 帧魔数只允许 ENet / EUCE ---- +$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 }) +$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 } +$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr" + +# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ---- +$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) +$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count) + +# ---- A3: ENet 帧类型仅 1/2/4 ---- +$types = $enet | Group-Object { LE32 $_.Payload 4 } +$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr" + +# ---- A4: 形态A DMX 帧 ---- +$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 }) +Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count) +Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024" +$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) }) +$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } } +Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count) +$tsSame = 0 +for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } } +Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0" + +# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ---- +$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' }) +$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' }) +Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count) +$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) }) +$hbMono = $true; $swaps = 0 +for ($i=1; $i -lt $hostSeq.Count; $i++) { + if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序) + elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败 +} +Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps) +$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) }) +$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count +Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched) +$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' }) +Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count) + +# ---- A6: 1947 密钥报文 ---- +$k = @($msgs | Where-Object { $_.Dport -eq 1947 }) +if ($k.Count -gt 0) { + $h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload) + $allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0 + Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0) + Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌" +} else { Check "A6 1947负载恒定" $false "| 无样本" } + +# ---- A7: A·B·B 节奏 ---- +$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21) +$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join '' +Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat" + +# ---- C1: type=02 主机标识心跳 ---- +$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 }) +$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' }) +Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count) + +# ---- C2: EUCE 帧 ---- +$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' }) +$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 }) +Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count) + +# ---- 发现阶段 ---- +Write-Host "`n=== 载入 $PcapDiscovery ===" +$pkts2 = Read-Pcap $PcapDiscovery +$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } } +$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 }) +$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 }) +Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count) +$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 }) +$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' }) +Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count) +$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致" +$hasHost = $false +foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } } +Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在" + +Write-Host "`n========================================" +Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail) +if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" } +.Payload 0x28) -ne (# Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2) +param( + [string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap", + [string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap" +) +$ErrorActionPreference = 'Stop' +$script:pass = 0; $script:fail = 0 +function Check($name, $cond, $detail) { + if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) } + else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) } +} +function Read-Pcap($path) { + $b = [IO.File]::ReadAllBytes($path) + function U32([byte[]]$x, [int]$o, [bool]$sw) { + if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] } + return [BitConverter]::ToUInt32($x, $o) + } + $magic = U32 $b 0 $false + $swap = ($magic -eq 0xa1b2c3d4) + $pkts = New-Object System.Collections.Generic.List[object] + $o = 24 + while ($o + 16 -le $b.Length) { + $incl = U32 $b ($o+8) $swap + if ($o + 16 + $incl -gt $b.Length) { break } + $data = New-Object byte[] $incl + [Array]::Copy($b, $o+16, $data, 0, $incl) + $pkts.Add([pscustomobject]@{ Len = $incl; Data = $data }) + $o += 16 + $incl + } + return $pkts +} +function UdpPayload($pkt) { + $d = $pkt.Data + if ($d.Length -lt 42) { return $null } + $ethType = ([uint32]$d[12] -shl 8) -bor $d[13] + $off = 14 + if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 } + if ($ethType -ne 0x0800) { return $null } + $ihl = ($d[$off] -band 0x0F) * 4 + if ($d[$off + 9] -ne 17) { return $null } + $ipStart = $off; $udp = $off + $ihl + $src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15] + $dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19] + $sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1] + $dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3] + $pl = New-Object byte[] ($d.Length - $udp - 8) + [Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length) + return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl } +} +function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) } +function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) } +function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' } + +Write-Host "=== 载入 $Pcap ===" +$pkts = Read-Pcap $Pcap +Write-Host ("总包数: {0}" -f $pkts.Count) +$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } } + +# ---- A1: 31772 帧魔数只允许 ENet / EUCE ---- +$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 }) +$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 } +$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr" + +# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ---- +$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) +$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count) + +# ---- A3: ENet 帧类型仅 1/2/4 ---- +$types = $enet | Group-Object { LE32 $_.Payload 4 } +$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr" + +# ---- A4: 形态A DMX 帧 ---- +$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 }) +Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count) +Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024" +$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) }) +$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } } +Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count) +$tsSame = 0 +for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } } +Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0" + +# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ---- +$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' }) +$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' }) +Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count) +$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) }) +$hbMono = $true; $swaps = 0 +for ($i=1; $i -lt $hostSeq.Count; $i++) { + if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序) + elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败 +} +Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps) +$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) }) +$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count +Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched) +$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' }) +Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count) + +# ---- A6: 1947 密钥报文 ---- +$k = @($msgs | Where-Object { $_.Dport -eq 1947 }) +if ($k.Count -gt 0) { + $h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload) + $allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0 + Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0) + Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌" +} else { Check "A6 1947负载恒定" $false "| 无样本" } + +# ---- A7: A·B·B 节奏 ---- +$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21) +$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join '' +Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat" + +# ---- C1: type=02 主机标识心跳 ---- +$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 }) +$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' }) +Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count) + +# ---- C2: EUCE 帧 ---- +$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' }) +$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 }) +Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count) + +# ---- 发现阶段 ---- +Write-Host "`n=== 载入 $PcapDiscovery ===" +$pkts2 = Read-Pcap $PcapDiscovery +$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } } +$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 }) +$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 }) +Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count) +$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 }) +$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' }) +Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count) +$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致" +$hasHost = $false +foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } } +Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在" + +Write-Host "`n========================================" +Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail) +if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" } +.Payload.Length - 0x2A) }) + Check "R2 RDM长度字段0x28" ($badRdmLen.Count -eq 0) ("| {0}/{1} 一致" -f ($rdm.Count - $badRdmLen.Count), $rdm.Count) + $uids = @($rdm | ForEach-Object { (Hex # Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2) +param( + [string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap", + [string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap" +) +$ErrorActionPreference = 'Stop' +$script:pass = 0; $script:fail = 0 +function Check($name, $cond, $detail) { + if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) } + else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) } +} +function Read-Pcap($path) { + $b = [IO.File]::ReadAllBytes($path) + function U32([byte[]]$x, [int]$o, [bool]$sw) { + if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] } + return [BitConverter]::ToUInt32($x, $o) + } + $magic = U32 $b 0 $false + $swap = ($magic -eq 0xa1b2c3d4) + $pkts = New-Object System.Collections.Generic.List[object] + $o = 24 + while ($o + 16 -le $b.Length) { + $incl = U32 $b ($o+8) $swap + if ($o + 16 + $incl -gt $b.Length) { break } + $data = New-Object byte[] $incl + [Array]::Copy($b, $o+16, $data, 0, $incl) + $pkts.Add([pscustomobject]@{ Len = $incl; Data = $data }) + $o += 16 + $incl + } + return $pkts +} +function UdpPayload($pkt) { + $d = $pkt.Data + if ($d.Length -lt 42) { return $null } + $ethType = ([uint32]$d[12] -shl 8) -bor $d[13] + $off = 14 + if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 } + if ($ethType -ne 0x0800) { return $null } + $ihl = ($d[$off] -band 0x0F) * 4 + if ($d[$off + 9] -ne 17) { return $null } + $ipStart = $off; $udp = $off + $ihl + $src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15] + $dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19] + $sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1] + $dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3] + $pl = New-Object byte[] ($d.Length - $udp - 8) + [Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length) + return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl } +} +function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) } +function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) } +function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' } + +Write-Host "=== 载入 $Pcap ===" +$pkts = Read-Pcap $Pcap +Write-Host ("总包数: {0}" -f $pkts.Count) +$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } } + +# ---- A1: 31772 帧魔数只允许 ENet / EUCE ---- +$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 }) +$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 } +$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr" + +# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ---- +$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) +$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count) + +# ---- A3: ENet 帧类型仅 1/2/4 ---- +$types = $enet | Group-Object { LE32 $_.Payload 4 } +$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr" + +# ---- A4: 形态A DMX 帧 ---- +$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 }) +Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count) +Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024" +$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) }) +$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } } +Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count) +$tsSame = 0 +for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } } +Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0" + +# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ---- +$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' }) +$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' }) +Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count) +$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) }) +$hbMono = $true; $swaps = 0 +for ($i=1; $i -lt $hostSeq.Count; $i++) { + if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序) + elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败 +} +Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps) +$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) }) +$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count +Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched) +$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' }) +Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count) + +# ---- A6: 1947 密钥报文 ---- +$k = @($msgs | Where-Object { $_.Dport -eq 1947 }) +if ($k.Count -gt 0) { + $h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload) + $allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0 + Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0) + Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌" +} else { Check "A6 1947负载恒定" $false "| 无样本" } + +# ---- A7: A·B·B 节奏 ---- +$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21) +$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join '' +Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat" + +# ---- C1: type=02 主机标识心跳 ---- +$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 }) +$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' }) +Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count) + +# ---- C2: EUCE 帧 ---- +$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' }) +$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 }) +Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count) + +# ---- 发现阶段 ---- +Write-Host "`n=== 载入 $PcapDiscovery ===" +$pkts2 = Read-Pcap $PcapDiscovery +$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } } +$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 }) +$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 }) +Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count) +$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 }) +$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' }) +Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count) +$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致" +$hasHost = $false +foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } } +Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在" + +Write-Host "`n========================================" +Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail) +if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" } +.Payload 0x32 6) } | Sort-Object -Unique) + Check "R3 灯具UID(Src@0x32)" ($uids.Count -ge 1) ("| UID: {0}" -f ($uids -join ' ')) + $disc = @($msgs3 | Where-Object { # Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2) +param( + [string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap", + [string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap" +) +$ErrorActionPreference = 'Stop' +$script:pass = 0; $script:fail = 0 +function Check($name, $cond, $detail) { + if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) } + else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) } +} +function Read-Pcap($path) { + $b = [IO.File]::ReadAllBytes($path) + function U32([byte[]]$x, [int]$o, [bool]$sw) { + if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] } + return [BitConverter]::ToUInt32($x, $o) + } + $magic = U32 $b 0 $false + $swap = ($magic -eq 0xa1b2c3d4) + $pkts = New-Object System.Collections.Generic.List[object] + $o = 24 + while ($o + 16 -le $b.Length) { + $incl = U32 $b ($o+8) $swap + if ($o + 16 + $incl -gt $b.Length) { break } + $data = New-Object byte[] $incl + [Array]::Copy($b, $o+16, $data, 0, $incl) + $pkts.Add([pscustomobject]@{ Len = $incl; Data = $data }) + $o += 16 + $incl + } + return $pkts +} +function UdpPayload($pkt) { + $d = $pkt.Data + if ($d.Length -lt 42) { return $null } + $ethType = ([uint32]$d[12] -shl 8) -bor $d[13] + $off = 14 + if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 } + if ($ethType -ne 0x0800) { return $null } + $ihl = ($d[$off] -band 0x0F) * 4 + if ($d[$off + 9] -ne 17) { return $null } + $ipStart = $off; $udp = $off + $ihl + $src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15] + $dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19] + $sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1] + $dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3] + $pl = New-Object byte[] ($d.Length - $udp - 8) + [Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length) + return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl } +} +function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) } +function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) } +function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' } + +Write-Host "=== 载入 $Pcap ===" +$pkts = Read-Pcap $Pcap +Write-Host ("总包数: {0}" -f $pkts.Count) +$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } } + +# ---- A1: 31772 帧魔数只允许 ENet / EUCE ---- +$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 }) +$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 } +$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr" + +# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ---- +$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) +$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count) + +# ---- A3: ENet 帧类型仅 1/2/4 ---- +$types = $enet | Group-Object { LE32 $_.Payload 4 } +$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr" + +# ---- A4: 形态A DMX 帧 ---- +$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 }) +Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count) +Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024" +$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) }) +$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } } +Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count) +$tsSame = 0 +for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } } +Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0" + +# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ---- +$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' }) +$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' }) +Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count) +$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) }) +$hbMono = $true; $swaps = 0 +for ($i=1; $i -lt $hostSeq.Count; $i++) { + if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序) + elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败 +} +Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps) +$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) }) +$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count +Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched) +$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' }) +Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count) + +# ---- A6: 1947 密钥报文 ---- +$k = @($msgs | Where-Object { $_.Dport -eq 1947 }) +if ($k.Count -gt 0) { + $h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload) + $allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0 + Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0) + Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌" +} else { Check "A6 1947负载恒定" $false "| 无样本" } + +# ---- A7: A·B·B 节奏 ---- +$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21) +$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join '' +Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat" + +# ---- C1: type=02 主机标识心跳 ---- +$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 }) +$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' }) +Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count) + +# ---- C2: EUCE 帧 ---- +$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' }) +$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 }) +Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count) + +# ---- 发现阶段 ---- +Write-Host "`n=== 载入 $PcapDiscovery ===" +$pkts2 = Read-Pcap $PcapDiscovery +$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } } +$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 }) +$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 }) +Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count) +$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 }) +$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' }) +Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count) +$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致" +$hasHost = $false +foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } } +Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在" + +Write-Host "`n========================================" +Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail) +if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" } +.Src -eq '192.168.123.1' -and (Hex # Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2) +param( + [string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap", + [string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap" +) +$ErrorActionPreference = 'Stop' +$script:pass = 0; $script:fail = 0 +function Check($name, $cond, $detail) { + if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) } + else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) } +} +function Read-Pcap($path) { + $b = [IO.File]::ReadAllBytes($path) + function U32([byte[]]$x, [int]$o, [bool]$sw) { + if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] } + return [BitConverter]::ToUInt32($x, $o) + } + $magic = U32 $b 0 $false + $swap = ($magic -eq 0xa1b2c3d4) + $pkts = New-Object System.Collections.Generic.List[object] + $o = 24 + while ($o + 16 -le $b.Length) { + $incl = U32 $b ($o+8) $swap + if ($o + 16 + $incl -gt $b.Length) { break } + $data = New-Object byte[] $incl + [Array]::Copy($b, $o+16, $data, 0, $incl) + $pkts.Add([pscustomobject]@{ Len = $incl; Data = $data }) + $o += 16 + $incl + } + return $pkts +} +function UdpPayload($pkt) { + $d = $pkt.Data + if ($d.Length -lt 42) { return $null } + $ethType = ([uint32]$d[12] -shl 8) -bor $d[13] + $off = 14 + if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 } + if ($ethType -ne 0x0800) { return $null } + $ihl = ($d[$off] -band 0x0F) * 4 + if ($d[$off + 9] -ne 17) { return $null } + $ipStart = $off; $udp = $off + $ihl + $src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15] + $dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19] + $sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1] + $dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3] + $pl = New-Object byte[] ($d.Length - $udp - 8) + [Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length) + return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl } +} +function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) } +function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) } +function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' } + +Write-Host "=== 载入 $Pcap ===" +$pkts = Read-Pcap $Pcap +Write-Host ("总包数: {0}" -f $pkts.Count) +$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } } + +# ---- A1: 31772 帧魔数只允许 ENet / EUCE ---- +$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 }) +$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 } +$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr" + +# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ---- +$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) +$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count) + +# ---- A3: ENet 帧类型仅 1/2/4 ---- +$types = $enet | Group-Object { LE32 $_.Payload 4 } +$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr" + +# ---- A4: 形态A DMX 帧 ---- +$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 }) +Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count) +Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024" +$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) }) +$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } } +Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count) +$tsSame = 0 +for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } } +Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0" + +# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ---- +$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' }) +$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' }) +Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count) +$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) }) +$hbMono = $true; $swaps = 0 +for ($i=1; $i -lt $hostSeq.Count; $i++) { + if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序) + elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败 +} +Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps) +$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) }) +$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count +Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched) +$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' }) +Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count) + +# ---- A6: 1947 密钥报文 ---- +$k = @($msgs | Where-Object { $_.Dport -eq 1947 }) +if ($k.Count -gt 0) { + $h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload) + $allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0 + Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0) + Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌" +} else { Check "A6 1947负载恒定" $false "| 无样本" } + +# ---- A7: A·B·B 节奏 ---- +$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21) +$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join '' +Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat" + +# ---- C1: type=02 主机标识心跳 ---- +$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 }) +$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' }) +Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count) + +# ---- C2: EUCE 帧 ---- +$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' }) +$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 }) +Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count) + +# ---- 发现阶段 ---- +Write-Host "`n=== 载入 $PcapDiscovery ===" +$pkts2 = Read-Pcap $PcapDiscovery +$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } } +$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 }) +$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 }) +Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count) +$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 }) +$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' }) +Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count) +$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致" +$hasHost = $false +foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } } +Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在" + +Write-Host "`n========================================" +Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail) +if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" } +.Payload 0x2A 4) -eq 'aaaa5dbb' }) + Check "R4 DISC发现响应脉冲" ($disc.Count -gt 0) ("| n={0}" -f $disc.Count) +} else { Check "R1 RDM帧" $false "| rdm3.pcap 不存在" } +Write-Host " +========================================" +Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail) +if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" } diff --git a/scripts/Verify-Rdm.ps1 b/scripts/Verify-Rdm.ps1 new file mode 100644 index 0000000..6dc50ee --- /dev/null +++ b/scripts/Verify-Rdm.ps1 @@ -0,0 +1,38 @@ +# Verify-Rdm.ps1 — 验证 enet_protocol.md §5 RDM 断言 +$ErrorActionPreference = 'Stop' +$script:pass = 0; $script:fail = 0 +function Check($name, $cond, $detail) { + if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) } + else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) } +} +. "C:\Users\tonycao\work\light\enet_lib.ps1" + +$pkts = Read-Pcap "C:\Users\tonycao\AppData\Local\Temp\opencode\rdm3_classic.pcap" +$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } } +Write-Host ("RDM 抓包总包数: {0}" -f $msgs.Count) + +# R1: RDM 应答帧 = Butler→主机 + "Butl" + cc01 起始 +$rdm = @($msgs | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Payload.Length -gt 0x2C -and (LE32 $_.Payload 4) -eq 4 -and (Hex $_.Payload 0x1A 4) -eq '4275746c' -and (Hex $_.Payload 0x2A 2) -eq 'cc01' }) +$dist = ($rdm | Group-Object { $_.Payload.Length } | ForEach-Object { "$($_.Name)x$($_.Count)" }) -join ',' +Check "R1 RDM帧=Butler→主机+cc01" ($rdm.Count -gt 0) ("| n={0} ({1})" -f $rdm.Count, $dist) + +# R2: 0x28 长度字段 == 实际 RDM 区长 (负载长 - 0x2A) +$bad = @($rdm | Where-Object { [int](LE16 $_.Payload 0x28) -ne ($_.Payload.Length - 0x2A) }) +Check "R2 RDM长度字段0x28" ($bad.Count -eq 0) ("| {0}/{1} 一致" -f ($rdm.Count - $bad.Count), $rdm.Count) + +# R3: 灯具 UID (Src @ 0x32, 6字节) — 至少 1 台, 收集全部 +$uids = @($rdm | ForEach-Object { (Hex $_.Payload 0x32 6) } | Sort-Object -Unique) +$uidFmt = ($uids | ForEach-Object { ($_.ToCharArray() | ForEach-Object { if ($_ -eq '*') { $_ } else { $_ } }) -join '' }) -join ':' +Check "R3 灯具UID@0x32" ($uids.Count -ge 1) ("| {0} 台: {1}" -f $uids.Count, ($uids -join ' ')) + +# R4: DISC 发现响应原始脉冲 +$disc = @($msgs | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Payload.Length -gt 0x2E -and (Hex $_.Payload 0x2A 4) -eq 'aaaa5dbb' }) +Check "R4 DISC发现响应脉冲" ($disc.Count -gt 0) ("| n={0} 负载={1}" -f $disc.Count, $(if($disc.Count){ (Hex $disc[0].Payload 0x2A 17) } else { '-' })) + +# R5: 主机请求封装在 1126 事件帧 (0x4A 处 cc01) +$req = @($msgs | Where-Object { $_.Src -eq '192.168.123.113' -and $_.Payload.Length -ge 0x4C -and (Hex $_.Payload 0x4A 2) -eq 'cc01' }) +Check "R5 主机请求封装于事件帧@0x4A" ($req.Count -gt 0) ("| n={0}" -f $req.Count) + +Write-Host "`n========================================" +Write-Host ("RDM 验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail) +if ($script:fail -eq 0) { Write-Host ">>> RDM 协议断言全部验证通过 <<<" } diff --git a/scripts/enet_lib.ps1 b/scripts/enet_lib.ps1 new file mode 100644 index 0000000..0b91407 --- /dev/null +++ b/scripts/enet_lib.ps1 @@ -0,0 +1,47 @@ +# enet_lib.ps1 — e:net 协议验证公共函数库 +$script:pass = 0; $script:fail = 0 +function Check($name, $cond, $detail) { + if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) } + else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) } +} +function Read-Pcap([string]$path) { + $b = [IO.File]::ReadAllBytes($path) + function U32([byte[]]$x, [int]$o, [bool]$sw) { + if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] } + return [BitConverter]::ToUInt32($x, $o) + } + $magic = U32 $b 0 $false + $swap = ($magic -eq 0xa1b2c3d4) + $pkts = New-Object System.Collections.Generic.List[object] + $o = 24 + while ($o + 16 -le $b.Length) { + $incl = U32 $b ($o+8) $swap + if ($o + 16 + $incl -gt $b.Length) { break } + $data = New-Object byte[] $incl + [Array]::Copy($b, $o+16, $data, 0, $incl) + $pkts.Add([pscustomobject]@{ Len = $incl; Data = $data }) + $o += 16 + $incl + } + return $pkts +} +function UdpPayload($pkt) { + $d = $pkt.Data + if ($d.Length -lt 42) { return $null } + $ethType = ([uint32]$d[12] -shl 8) -bor $d[13] + $off = 14 + if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 } + if ($ethType -ne 0x0800) { return $null } + $ihl = ($d[$off] -band 0x0F) * 4 + if ($d[$off + 9] -ne 17) { return $null } + $ipStart = $off; $udp = $off + $ihl + $src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15] + $dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19] + $sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1] + $dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3] + $pl = New-Object byte[] ($d.Length - $udp - 8) + [Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length) + return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl } +} +function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) } +function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) } +function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' } diff --git a/scripts/session/analyze_ack.ps1 b/scripts/session/analyze_ack.ps1 new file mode 100644 index 0000000..6c40ba1 --- /dev/null +++ b/scripts/session/analyze_ack.ps1 @@ -0,0 +1,55 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +function H2B($hex) { + if (-not $hex) { return $null } + $n = $hex.Length / 2 + $b = New-Object byte[] $n + for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring(2*$i, 2), 16) } + return $b +} +$lines = [IO.File]::ReadAllLines("$env:TEMP\opencode\ss123.tsv") +$acks = New-Object System.Collections.ArrayList +$qs = New-Object System.Collections.ArrayList +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + $b = H2B $p[4]; if ($b.Length -lt 0x2A) { continue } + $t = [double]$p[0] + if ($p[1] -eq "192.168.123.1" -and $p[2] -eq "192.168.123.113" -and $b.Length -eq 42 -and [int]$b[4] -eq 4) { + $v = [int]$b[0x20] + [int]$b[0x21]*256 + [int]$b[0x22]*65536 + [int]$b[0x23]*16777216 + [void]$acks.Add(@{ t=$t; v=$v; hex=(($b[0x1A..0x29] | ForEach-Object { $_.ToString("x2") }) -join "") }) + } + if ($p[1] -eq "192.168.123.113" -and $p[2] -eq "192.168.123.1" -and $b.Length -gt 0x52 -and $b.Length -lt 2000 -and [int]$b[4] -eq 4 -and $b[0x20] -eq 2 -and $b[0x21] -eq 0 -and $b[0x30] -eq 0 -and $b[0x3C] -eq 1) { + [void]$qs.Add(@{ t=$t; cmd=$b[0x3D] }) + } +} +Write-Host ("42B 帧数={0}" -f $acks.Count) +# 0x20-0x23 值序列: 单调性 + 间隔 +$mono = 0; $notMono = 0; $gaps = @{} +for ($i = 1; $i -lt $acks.Count; $i++) { + $d = $acks[$i].v - $acks[$i-1].v + if ($d -gt 0) { $mono++ } else { $notMono++ } + $gk = "$d" + if (-not $gaps.ContainsKey($gk)) { $gaps[$gk] = 0 }; $gaps[$gk]++ +} +Write-Host ("0x20-0x23 值: 递增={0} 非递增={1}" -f $mono, $notMono) +Write-Host "--- 相邻差值分布 ---" +$gaps.GetEnumerator() | Sort-Object { [int]$_.Key } | Select-Object -First 10 | ForEach-Object { Write-Host (" 差={0} : {1}" -f $_.Key, $_.Value) } +Write-Host "--- 前 8 个 42B 帧 (时间/值/hex) ---" +foreach ($a in ($acks | Select-Object -First 8)) { Write-Host (" t={0,8:F3} v={1} hex={2}" -f $a.t, $a.v, $a.hex) } +Write-Host "--- 后 4 个 ---" +foreach ($a in ($acks | Select-Object -Last 4)) { Write-Host (" t={0,8:F3} v={1} hex={2}" -f $a.t, $a.v, $a.hex) } +# 42B 帧与 cmd 查询的时间相关性: 每个 42B 前 50ms 内最近的查询 cmd +$byCmd = @{} +foreach ($a in $acks) { + $best = $null + for ($i = $qs.Count - 1; $i -ge 0; $i--) { if ($qs[$i].t -le $a.t) { $best = $qs[$i]; break } } + $k = if ($best -and (($a.t - $best.t) -lt 0.05)) { "cmd={0:x2}" -f $best.cmd } else { "无临近查询" } + if (-not $byCmd.ContainsKey($k)) { $byCmd[$k] = 0 }; $byCmd[$k]++ +} +Write-Host "--- 42B 帧前 50ms 内最近查询 ---" +$byCmd.GetEnumerator() | Sort-Object { [int]$_.Value } -Descending | ForEach-Object { Write-Host (" {0} : {1}" -f $_.Key, $_.Value) } +# 42B 帧间时间隔 +$tlast = $null; $tg = New-Object System.Collections.ArrayList +foreach ($a in $acks) { if ($null -ne $tlast) { [void]$tg.Add([int](($a.t - $tlast) * 1000)) }; $tlast = $a.t } +$tgs = $tg | Sort-Object +Write-Host ("--- 42B 帧时间隔ms: min={0} 中位={1} max={2}" -f $tgs[0], $tgs[[int]($tgs.Count/2)], $tgs[-1]) diff --git a/scripts/session/analyze_aech.ps1 b/scripts/session/analyze_aech.ps1 new file mode 100644 index 0000000..74c3218 --- /dev/null +++ b/scripts/session/analyze_aech.ps1 @@ -0,0 +1,48 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +# Extract AECH (sub=05) / AERE (sub=06) frames from cold2.tsv, diff across rounds +$raw = [IO.File]::ReadAllBytes("$env:TEMP\opencode\cold2.tsv") +$text = [Text.Encoding]::UTF8.GetString($raw) +if ($raw[0] -eq 0xFF -and $raw[1] -eq 0xFE) { $text = [Text.Encoding]::Unicode.GetString($raw) } +$lines = $text -split "`r?`n" + +function H2B($hex) { $h = $hex -replace " ",""; $n = $h.Length/2; $b = New-Object byte[] $n; for ($i=0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($h.Substring(2*$i,2),16) }; return $b } + +$chals = @(); $resps = @() +foreach ($l in $lines) { + $p = $l -split "`t" + if ($p.Count -lt 5 -or -not $p[4]) { continue } + $b = H2B $p[4] + if ($b.Length -lt 0x30) { continue } + if ($b[0] -ne 0x45 -or $b[1] -ne 0x4e -or $b[2] -ne 0x65 -or $b[3] -ne 0x74) { continue } # ENet + $tag = [Text.Encoding]::ASCII.GetString($b[0x28..0x2B]) + if ($tag -eq "AECH") { $chals += ,@{ t=[double]$p[0]; src=$p[1]; b=$b } } + if ($tag -eq "AERE") { $resps += ,@{ t=[double]$p[0]; src=$p[1]; b=$b } } +} +Write-Host "AECH challenges: $($chals.Count), AERE responses: $($resps.Count)" +foreach ($c in $chals) { Write-Host (" CHAL t={0,8:F3} src={1} len={2}" -f $c.t, $c.src, $c.b.Length) } +foreach ($r in $resps) { Write-Host (" RESP t={0,8:F3} src={1} len={2}" -f $r.t, $r.src, $r.b.Length) } + +# diff rounds: varying offset runs +function Diff-Frames($frames, $label) { + if ($frames.Count -lt 2) { return } + $len = $frames[0].b.Length + $vary = @() + for ($i = 0; $i -lt $len; $i++) { + $v = $frames[0].b[$i]; $diff = $false + foreach ($f in $frames) { if ($f.b[$i] -ne $v) { $diff = $true; break } } + if ($diff) { $vary += $i } + } + Write-Host "`n=== $label varying offsets ($($vary.Count) bytes) ===" + # group into runs + $runs = @(); $start = -1; $prev = -2 + foreach ($o in $vary) { if ($o -ne $prev + 1) { if ($start -ge 0) { $runs += ,"$start-$prev" }; $start = $o }; $prev = $o } + if ($start -ge 0) { $runs += ,"$start-$prev" } + foreach ($r in $runs) { + $a, $bb = $r -split "-"; $a = [int]$a; $bb = [int]$bb + $parts = @() + foreach ($f in $frames) { $parts += (($f.b[$a..$bb] | ForEach-Object { $_.ToString("x2") }) -join "") } + Write-Host (" 0x{0:x2}-0x{1:x2} ({2}B): {3}" -f $a, $bb, ($bb-$a+1), ($parts -join " | ")) + } +} +Diff-Frames $chals "CHALLENGES" +Diff-Frames $resps "RESPONSES" diff --git a/scripts/session/analyze_branch.ps1 b/scripts/session/analyze_branch.ps1 new file mode 100644 index 0000000..779967c --- /dev/null +++ b/scripts/session/analyze_branch.ps1 @@ -0,0 +1,74 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +function H2B($hex) { + if (-not $hex) { return $null } + $n = $hex.Length / 2 + $b = New-Object byte[] $n + for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring(2*$i, 2), 16) } + return $b +} +$lines = [IO.File]::ReadAllLines("$env:TEMP\opencode\ss123.tsv") + +# 收集 cmd=24 查询 (0x20==0200, flag==0) 与 其后 100ms 内的脉冲 UID +$queries = New-Object System.Collections.ArrayList +$pulses = New-Object System.Collections.ArrayList +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + $b = H2B $p[4]; if ($b.Length -lt 0x30) { continue } + $t = [double]$p[0] + if ($p[1] -eq "192.168.123.113" -and $p[2] -eq "192.168.123.1" -and [int]$b[4] -eq 4 -and $b.Length -gt 0x52 -and $b.Length -lt 2000 -and $b[0x20] -eq 2 -and $b[0x21] -eq 0 -and $b[0x30] -eq 0 -and $b[0x3C] -eq 1 -and $b[0x3D] -eq 0x24) { + [void]$queries.Add(@{ t=$t; b=$b }) + } + if ($p[1] -eq "192.168.123.1" -and $p[2] -eq "192.168.123.113" -and $b.Length -lt 200 -and $b[0x2A] -eq 0xAA) { + $uid = "" + for ($i = 0; $i -lt 6; $i++) { + $b1 = $b[0x2B + 2*$i]; $b2 = $b[0x2C + 2*$i] + $byte = ([int]$b1 -band 0x55) -bor ([int]$b2 -band 0xAA) + $uid += $byte.ToString("x2") + } + [void]$pulses.Add(@{ t=$t; uid=$uid }) + } +} +Write-Host ("cmd=24 查询={0} 脉冲={1}" -f $queries.Count, $pulses.Count) + +# cmd=24 查询布局: 打印 0x3C..0x60 区域的不同取值分布 +Write-Host "`n--- cmd=24 查询 0x3E..0x55 字段分布 (前 8 条不同样本) ---" +$seen = @{} +$shown = 0 +foreach ($q in $queries) { + $key = ($q.b[0x3E..0x55] | ForEach-Object { $_.ToString("x2") }) -join "" + if ($seen.ContainsKey($key)) { continue } + $seen[$key] = 1 + $b = $q.b + Write-Host (" t={0,7:F2} 0x3E={1} 0x44={2} 0x4A={3} 0x50={4}" -f $q.t, + (($b[0x3E..0x43] | ForEach-Object { $_.ToString("x2") }) -join ""), + (($b[0x44..0x49] | ForEach-Object { $_.ToString("x2") }) -join ""), + (($b[0x4A..0x4F] | ForEach-Object { $_.ToString("x2") }) -join ""), + (($b[0x50..0x55] | ForEach-Object { $_.ToString("x2") }) -join "")) + $shown++; if ($shown -ge 12) { break } +} + +# 关键验证: 每个查询的分支范围 [lo,hi] 与 其后 100ms 内应答脉冲 UID 的关系 +# 假设: 0x3E-0x43=下界, 0x44-0x49=上界 (大端 UID 序) +function UidNum($b, $o) { + $v = [double]0 + for ($i = 0; $i -lt 6; $i++) { $v = $v * 256 + [int]$b[$o + $i] } + return $v +} +Write-Host "`n--- 分支范围 vs 应答脉冲 (前 25 条有应答/无应答对比) ---" +$fx = @("010734670125", "010734880900", "0899500026c9") +$fxNum = @{} +foreach ($f in $fx) { $n = [double]0; for ($i = 0; $i -lt 6; $i++) { $n = $n * 256 + [Convert]::ToInt32($f.Substring(2*$i, 2), 16) }; $fxNum[$f] = $n } +$n2 = 0 +foreach ($q in $queries) { + $lo = UidNum $q.b 0x3E; $hi = UidNum $q.b 0x44 + $ans = @($pulses | Where-Object { $_.t -ge $q.t -and $_.t -lt $q.t + 0.1 }) + $ansStr = ($ans | ForEach-Object { $_.uid.Substring(8) }) -join "," + $inRange = @() + foreach ($f in $fx) { if ($fxNum[$f] -ge $lo -and $fxNum[$f] -le $hi) { $inRange += $f.Substring(8) } } + Write-Host (" t={0,7:F2} lo={1} hi={2} 范围内灯具=[{3}] 应答=[{4}]" -f $q.t, + (($q.b[0x3E..0x43] | ForEach-Object { $_.ToString("x2") }) -join "").Substring(4), + (($q.b[0x44..0x49] | ForEach-Object { $_.ToString("x2") }) -join "").Substring(4), + ($inRange -join ","), $ansStr) + $n2++; if ($n2 -ge 25) { break } +} diff --git a/scripts/session/analyze_branch2.ps1 b/scripts/session/analyze_branch2.ps1 new file mode 100644 index 0000000..8c467d5 --- /dev/null +++ b/scripts/session/analyze_branch2.ps1 @@ -0,0 +1,74 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +function H2B($hex) { + if (-not $hex) { return $null } + $n = $hex.Length / 2 + $b = New-Object byte[] $n + for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring(2*$i, 2), 16) } + return $b +} +function UidNumArr($arr) { $v = [double]0; foreach ($x in $arr) { $v = $v * 256 + [int]$x }; return $v } +$lines = [IO.File]::ReadAllLines("$env:TEMP\opencode\ss123.tsv") + +$queries = New-Object System.Collections.ArrayList +$pulses = New-Object System.Collections.ArrayList +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + $b = H2B $p[4]; if ($b.Length -lt 0x30) { continue } + $t = [double]$p[0] + if ($p[1] -eq "192.168.123.113" -and $p[2] -eq "192.168.123.1" -and [int]$b[4] -eq 4 -and $b.Length -gt 0x5E -and $b.Length -lt 2000 -and $b[0x20] -eq 2 -and $b[0x21] -eq 0 -and $b[0x30] -eq 0 -and $b[0x3C] -eq 1 -and $b[0x3D] -eq 0x24) { + [void]$queries.Add(@{ t=$t; lo=(UidNumArr $b[0x53..0x58]); hi=(UidNumArr $b[0x59..0x5E]); loS=(($b[0x53..0x58] | ForEach-Object { $_.ToString("x2") }) -join ""); hiS=(($b[0x59..0x5E] | ForEach-Object { $_.ToString("x2") }) -join "") }) + } + if ($p[1] -eq "192.168.123.1" -and $p[2] -eq "192.168.123.113" -and $b.Length -lt 200 -and $b[0x2A] -eq 0xAA) { + $uidBytes = @() + for ($i = 0; $i -lt 6; $i++) { + $b1 = $b[0x2B + 2*$i]; $b2 = $b[0x2C + 2*$i] + $uidBytes += ([int]$b1 -band 0x55) -bor ([int]$b2 -band 0xAA) + } + [void]$pulses.Add(@{ t=$t; uid=(UidNumArr $uidBytes); uidS=(($uidBytes | ForEach-Object { $_.ToString("x2") }) -join "") }) + } +} +Write-Host ("cmd=24 查询={0} 脉冲={1}" -f $queries.Count, $pulses.Count) + +# 分支范围分布 +Write-Host "`n--- 分支范围 lo-hi 分布 (Top 15) ---" +$ranges = @{} +foreach ($q in $queries) { $k = "$($q.loS)-$($q.hiS)"; if (-not $ranges.ContainsKey($k)) { $ranges[$k] = 0 }; $ranges[$k]++ } +$ranges.GetEnumerator() | Sort-Object { [int]$_.Value } -Descending | Select-Object -First 15 | ForEach-Object { Write-Host (" {0} : {1}" -f $_.Key, $_.Value) } + +# 核心验证: 范围内含灯具的查询 → 应答率; 范围外 → 应答率 +$fx = @{} +foreach ($f in @("010734670125", "010734880900", "0899500026c9")) { + $n = [double]0; for ($i = 0; $i -lt 6; $i++) { $n = $n * 256 + [Convert]::ToInt32($f.Substring(2*$i, 2), 16) } + $fx[$f] = $n +} +$inN = 0; $inAns = 0; $outN = 0; $outAns = 0; $mismatches = 0 +foreach ($q in $queries) { + $ans = @($pulses | Where-Object { $_.t -ge $q.t -and $_.t -lt $q.t + 0.1 }) + $hasFx = $false; foreach ($f in $fx.Values) { if ($f -ge $q.lo -and $f -le $q.hi) { $hasFx = $true; break } } + if ($hasFx) { + $inN++ + if ($ans.Count -gt 0) { + $inAns++ + # 应答的 UID 是否都在范围内? + foreach ($a in $ans) { if ($a.uid -lt $q.lo -or $a.uid -gt $q.hi) { $mismatches++ } } + } + } else { + $outN++ + if ($ans.Count -gt 0) { $outAns++ } + } +} +Write-Host "`n--- 分支语义验证 ---" +Write-Host (" 范围内有灯具的查询: {0}, 有应答: {1} ({2:F0}%)" -f $inN, $inAns, $(if ($inN) {100.0*$inAns/$inN} else {0})) +Write-Host (" 范围内无灯具的查询: {0}, 有应答: {1} ({2:F0}%)" -f $outN, $outAns, $(if ($outN) {100.0*$outAns/$outN} else {0})) +Write-Host (" 应答 UID 超出查询范围的次数: {0}" -f $mismatches) + +# 有应答查询的时间序列样本 +Write-Host "`n--- 样本: 分支范围 vs 应答 UID (前 15 条有应答) ---" +$shown = 0 +foreach ($q in $queries) { + $ans = @($pulses | Where-Object { $_.t -ge $q.t -and $_.t -lt $q.t + 0.1 }) + if ($ans.Count -eq 0) { continue } + Write-Host (" t={0,7:F2} [{1}..{2}] → {3}" -f $q.t, $q.loS, $q.hiS, (($ans | ForEach-Object { $_.uidS }) -join ",")) + $shown++; if ($shown -ge 15) { break } +} diff --git a/scripts/session/analyze_ck.ps1 b/scripts/session/analyze_ck.ps1 new file mode 100644 index 0000000..afa2f91 --- /dev/null +++ b/scripts/session/analyze_ck.ps1 @@ -0,0 +1,81 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +$f = "$env:TEMP\opencode\cold2.tsv" +$lines = [IO.File]::ReadAllLines($f) + +function H2B($hex) { + if (-not $hex) { return $null } + $n = $hex.Length / 2 + $b = New-Object byte[] $n + for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring(2*$i, 2), 16) } + return $b +} +function Hex($b) { return ($b | ForEach-Object { $_.ToString("x2") }) -join "" } +function UidStr($b, $o) { if ($b.Length -lt $o+6) { return "?" }; return ($b[$o..($o+5)] | ForEach-Object { $_.ToString("x2") }) -join "" } + +# 采集 70B RDM 应答 +$resp70 = @() +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + if ($p[1] -ne "192.168.123.1" -or $p[2] -ne "192.168.123.113") { continue } + $b = H2B $p[4] + if ($b.Length -eq 70 -and $b[0x2A] -eq 0xCC) { $resp70 += ,$b } +} +Write-Host ("70B RDM 应答数: {0}" -f $resp70.Count) + +# 全字段偏移扫描: sum(b[o..66])==ck16 且唯一解 +Write-Host "`n--- 唯一偏移扫描: sum(b[o..66]) == ck@68-69 (BE) ---" +foreach ($o in 0x2A..0x34) { + $matchAll = $true + foreach ($b in $resp70) { + $sum = 0; for ($i = $o; $i -le 66; $i++) { $sum += $b[$i] } + $ck = ($b[67] -shl 8) -bor $b[68] + # 注意: 上面偏移占位, 实际 ck 在最后两字节 68-69 + $ck = ($b[68] -shl 8) -bor $b[69] + if (($sum -band 0xFFFF) -ne $ck) { $matchAll = $false; break } + } + Write-Host (" start=0x{0:x2} end=66 ck@68-69 : {1}" -f $o, $(if ($matchAll) {"全部匹配"} else {"否"})) +} + +# 结合 msgLen: 若 start=0x2E, 覆盖字节数 = 66-0x2E+1 = 0x27? 试 sum(b[0x2A..0x2A+msgLen+2]) (msgLen 不含 CC01) +Write-Host "`n--- 假设: msgLen@0x2C 不含 CC01 两字节, 求和范围 = CC01 + msgLen 字节 ---" +$ok = 0; $bad = 0 +foreach ($b in $resp70) { + $msgLen = $b[0x2C] + $sum = 0; for ($i = 0x2A; $i -lt 0x2A + 2 + $msgLen; $i++) { $sum += $b[$i] } + $ck = ($b[68] -shl 8) -bor $b[69] + if (($sum -band 0xFFFF) -eq $ck) { $ok++ } else { $bad++ } +} +Write-Host (" OK={0} BAD={1}" -f $ok, $bad) + +# 展示两个 70B 样本的完整 hex + 各字段 +Write-Host "`n--- 70B 样本字段 ---" +foreach ($b in ($resp70 | Select-Object -First 2)) { + $msgLen = $b[0x2C] + Write-Host (" frame: " + (Hex $b)) + Write-Host (" 0x28 RDMlen={0} 0x2C msgLen={1} dest={2} src={3} TN={4:x2} RT={5:x2} CC={6:x2} PID={7:x2}{8:x2} PD={9} ck={10:x2}{11:x2}" -f ` + ($b[0x28] -bor ($b[0x29] -shl 8)), $msgLen, (UidStr $b 0x2D), (UidStr $b 0x33), $b[0x39], $b[0x3A], $b[0x3E], $b[0x3F], $b[0x40], (Hex $b[0x42..0x43]), $b[0x44], $b[0x45]) + # 试 sum(0x2E..0x2E+msgLen-1) + $s1 = 0; for ($i = 0x2E; $i -lt 0x2E + $msgLen; $i++) { $s1 += $b[$i] } + Write-Host (" sum(0x2E..0x2E+msgLen-1)=0x{0:x4} (end=0x{1:x2})" -f ($s1 -band 0xFFFF), (0x2E + $msgLen - 1)) + $s2 = 0; for ($i = 0x2A; $i -lt 0x2A + 2 + $msgLen; $i++) { $s2 += $b[$i] } + Write-Host (" sum(0x2A..0x2A+msgLen+1)=0x{0:x4} (end=0x{1:x2})" -f ($s2 -band 0xFFFF), (0x2A + 2 + $msgLen - 1)) +} + +# 同样验证一个 87B (DEVICE_INFO) 应答 +Write-Host "`n--- 87B DEVICE_INFO 应答验证 ---" +$cnt = 0 +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + if ($p[1] -ne "192.168.123.1" -or $p[2] -ne "192.168.123.113") { continue } + $b = H2B $p[4] + if ($b.Length -eq 87 -and $b[0x2A] -eq 0xCC) { + $msgLen = $b[0x2C] + $ckOff = $b.Length - 2 + $ck = ($b[$ckOff] -shl 8) -bor $b[$ckOff+1] + $s1 = 0; for ($i = 0x2E; $i -lt 0x2E + $msgLen; $i++) { $s1 += $b[$i] } + $s2 = 0; for ($i = 0x2A; $i -lt 0x2A + 2 + $msgLen; $i++) { $s2 += $b[$i] } + Write-Host (" msgLen={0} ck=0x{1:x4} sum_noCC01=0x{2:x4}({3}) sum_withCC01=0x{4:x4}({5})" -f $msgLen, $ck, ($s1 -band 0xFFFF), $(if (($s1 -band 0xFFFF) -eq $ck) {"OK"} else {"x"}), ($s2 -band 0xFFFF), $(if (($s2 -band 0xFFFF) -eq $ck) {"OK"} else {"x"})) + $cnt++; if ($cnt -ge 5) { break } + } +} diff --git a/scripts/session/analyze_claim_seq.ps1 b/scripts/session/analyze_claim_seq.ps1 new file mode 100644 index 0000000..bcf5c9f --- /dev/null +++ b/scripts/session/analyze_claim_seq.ps1 @@ -0,0 +1,56 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$f = "$env:TEMP\opencode\cold2.tsv" +$lines = [IO.File]::ReadAllLines($f) +function H2B($hex) { $n = $hex.Length / 2; $b = New-Object byte[] $n; for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring(2*$i, 2), 16) }; return $b } +function Hex($b) { return ($b | ForEach-Object { $_.ToString("x2") }) -join "" } + +# (a) 38.087 的 102B type=2 帧 +Write-Host "=== (a) 首个 type=2 102B 帧 (claim 前) ===" +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5) { continue } + if ($p[1] -ne "192.168.123.113" -or $p[2] -ne "192.168.123.1") { continue } + $b = H2B $p[4] + if ($b.Length -eq 102 -and $b[4] -eq 2) { + Write-Host (" t={0} len={1}" -f $p[0], $b.Length) + Write-Host (" hex=" + (Hex $b)) + break + } +} + +# (b) 首个 644B announce: 找主机IP(717ba8c0)偏移 + 其它可变字段 +Write-Host "`n=== (b) 首个 644B type=1 announce ===" +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5) { continue } + if ($p[1] -ne "192.168.123.113") { continue } + $b = H2B $p[4] + if ($b.Length -eq 644) { + Write-Host (" t={0} dst={1}" -f $p[0], $p[2]) + $ip = [byte[]]@(0x71,0x7b,0xa8,0xc0) # 113.123.168.192 = LE of 192.168.123.113 + for ($i = 0; $i -lt $b.Length - 4; $i++) { + if ($b[$i] -eq 0x71 -and $b[$i+1] -eq 0x7b -and $b[$i+2] -eq 0xa8 -and $b[$i+3] -eq 0xc0) { + Write-Host (" 主机IP@0x{0:x}" -f $i) + } + } + Write-Host (" 0x00-0x40: " + (Hex $b[0..0x3F])) + break + } +} + +# (c) rdm_disc_req 模板 0x18-0x19 / 0x2A-0x3B +Write-Host "`n=== (c) rdm_disc_req 模板关键字段 ===" +$h = (Get-Content "C:\Users\tonycao\work\light\enet-validator\samples\rdm_disc_req_1126.hex" -Raw).Trim() -replace '\s','' +$b = H2B $h +Write-Host (" len={0} 0x18-0x19={1} 0x20-0x23={2} 0x2A-0x3B={3}" -f $b.Length, (Hex $b[0x18..0x19]), (Hex $b[0x20..0x23]), (Hex $b[0x2A..0x3B])) + +# (d) 首个 .1→113 的 RDM 应答帧: 0x18 sub + 0x2A 起内容 +Write-Host "`n=== (d) 首个 .1→113 RDM 应答 (sub@0x18, 0x2A 起 32B) ===" +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5) { continue } + if ($p[1] -ne "192.168.123.1") { continue } + $b = H2B $p[4] + if ($b.Length -ge 0x30 -and $b[4] -eq 4 -and ($b[0x2A] -eq 0xCC -or $b[0x2A] -eq 0xAA)) { + Write-Host (" t={0} len={1} 0x18-0x19={2} 0x20-0x21={3} 0x28-0x29={4}" -f $p[0], $b.Length, (Hex $b[0x18..0x19]), (Hex $b[0x20..0x21]), (Hex $b[0x28..0x29])) + Write-Host (" 0x2A+: " + (Hex $b[0x2A..([Math]::Min(0x4A, $b.Length-1))])) + break + } +} diff --git a/scripts/session/analyze_cold.ps1 b/scripts/session/analyze_cold.ps1 new file mode 100644 index 0000000..c2e97c1 --- /dev/null +++ b/scripts/session/analyze_cold.ps1 @@ -0,0 +1,240 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +$f = "$env:TEMP\opencode\cold2.tsv" +$lines = [IO.File]::ReadAllLines($f) + +function H2B($hex) { + if (-not $hex) { return $null } + $n = $hex.Length / 2 + $b = New-Object byte[] $n + for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring(2*$i, 2), 16) } + return $b +} +function G16($b, $o) { if ($b.Length -lt $o+2) { return -1 }; return $b[$o] -bor ($b[$o+1] -shl 8) } +function G32($b, $o) { if ($b.Length -lt $o+4) { return -1 }; return $b[$o] -bor ($b[$o+1] -shl 8) -bor ($b[$o+2] -shl 16) -bor ($b[$o+3] -shl 24) } +function UidStr($b, $o) { if ($b.Length -lt $o+6) { return "?" }; return ($b[$o..($o+5)] | ForEach-Object { $_.ToString("x2") }) -join "" } + +# ============ 1) 冷启动时间线: 前 30 秒关键帧 ============ +Write-Host "`n=========== [1] 冷启动前 30s 关键帧时间线 ===========" +$shown = 0 +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + $t = [double]$p[0]; if ($t -gt 30) { break } + $b = H2B $p[4]; if ($b.Length -lt 0x1A) { continue } + if ($b[0] -ne 0x45 -or $b[1] -ne 0x4E) { continue } # "EN" + $type = G32 $b 4; $dir = "$($p[1])->$($p[2])"; $len = $b.Length + $desc = $null + if ($type -eq 1) { $desc = "发现请求" } + elseif ($type -eq 2) { $sub = G16 $b 0x18 + if ($sub -eq 1) { $desc = "宣告sub=01" } + elseif ($sub -eq 2) { $desc = "注册sub=02(89B)" } + elseif ($sub -eq 3) { $desc = "心跳sub=03" } + elseif ($sub -eq 5) { $desc = "AECH质询sub=05" } + elseif ($sub -eq 6) { $desc = "AERE应答sub=06" } + else { $desc = "type2 sub=$sub" } } + elseif ($type -eq 4 -and $len -eq 259) { $desc = "259发现应答" } + elseif ($type -eq 4 -and $len -eq 84) { $desc = "84B状态" } + elseif ($type -eq 4 -and $len -gt 1000) { $desc = "DMX/RDM长帧" } + elseif ($type -eq 4) { $desc = "type4短帧" } + if ($desc -and $desc -ne "84B状态" -and $desc -ne "心跳sub=03" -and $shown -lt 60) { + Write-Host (" t={0,8:F3} {1,-26} len={2,-5} {3}" -f $t, $dir, $len, $desc); $shown++ + } +} + +# ============ 2) RDM 查询直方图 (主机->.1, type=4) ============ +Write-Host "`n=========== [2] 主机 RDM 查询 cmd 直方图 (type=4, 0x3D) ===========" +$cmdHist = @{}; $pidHist = @{}; $queries = New-Object System.Collections.ArrayList +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + if ($p[1] -ne "192.168.123.113" -or $p[2] -ne "192.168.123.1") { continue } + $b = H2B $p[4]; if ($b.Length -lt 0x52) { continue } + if ((G32 $b 4) -ne 4) { continue } + if ($b.Length -gt 2000) { continue } # 纯DMX 1126B? 实际 1084=DMX + $t = [double]$p[0] + $len = $b.Length + if ($len -eq 1084 -or $len -eq 1126) { # DMX 帧或内嵌 RDM 副本 + if ($b[0x3C] -ne 0x01) { continue } + } + $cmd = $b[0x3D] + if ($cmd -ne 0x24 -and $cmd -ne 0x18 -and $cmd -ne 0x19 -and $cmd -ne 0x1a) { continue } + $flag = $b[0x30] + $key = ("cmd=0x{0:x2} flag={1:x2}" -f $cmd, $flag) + if (-not $cmdHist.ContainsKey($key)) { $cmdHist[$key] = 0 }; $cmdHist[$key]++ + $uid = UidStr $b 0x3E + $pid2 = "" + if ($cmd -eq 0x18 -and $b.Length -gt 0x51) { $pid2 = "{0:x2}{1:x2}" -f $b[0x50], $b[0x51] + $pk = "$pid2 flag=$("{0:x2}" -f $flag)"; if (-not $pidHist.ContainsKey($pk)) { $pidHist[$pk] = 0 }; $pidHist[$pk]++ } + [void]$queries.Add(@{ t=$t; cmd=$cmd; flag=$flag; uid=$uid; pid=$pid2; seq=((G16 $b 0x22)); seq16lo=(G16 $b 0x22) }) +} +Write-Host "--- cmd 分布 ---" +$cmdHist.GetEnumerator() | Sort-Object Value -Descending | ForEach-Object { Write-Host (" {0} : {1}" -f $_.Key, $_.Value) } +Write-Host "--- cmd=18 的 PID 分布 ---" +$pidHist.GetEnumerator() | Sort-Object Value -Descending | ForEach-Object { Write-Host (" PID={0} : {1}" -f $_.Key, $_.Value) } + +# ============ 3) .1 应答帧: 长度/内容直方图 + checksum 验证 ============ +Write-Host "`n=========== [3] .1 应答帧分析 (.1->113, type=4 短帧) ===========" +$answers = New-Object System.Collections.ArrayList +$ansLenHist = @{} +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + if ($p[1] -ne "192.168.123.1" -or $p[2] -ne "192.168.123.113") { continue } + $b = H2B $p[4]; if ($b.Length -lt 0x2E) { continue } + if ((G32 $b 4) -ne 4) { continue } + if ($b.Length -gt 200) { continue } # 只看 RDM 短应答 (59/69/77/87/101/111/119B) + if ($b[0x2A] -ne 0xCC -and $b[0x2A] -ne 0xAA) { continue } + $t = [double]$p[0] + $len = $b.Length + if (-not $ansLenHist.ContainsKey($len)) { $ansLenHist[$len] = 0 }; $ansLenHist[$len]++ + [void]$answers.Add(@{ t=$t; b=$b; len=$len }) +} +Write-Host "--- 应答长度分布 ---" +$ansLenHist.GetEnumerator() | Sort-Object Name | ForEach-Object { Write-Host (" {0}B : {1}" -f $_.Key, $_.Value) } + +# checksum 验证 (标准 RDM 帧: 0x2A=0xCC, msgLen@0x2C, checksum=前 msgLen 字节和, BE) +Write-Host "--- 标准 RDM 应答 checksum 验证 (抽样全部) ---" +$okC = 0; $badC = 0; $badSamples = New-Object System.Collections.ArrayList +foreach ($a in $answers) { + $b = $a.b + if ($b[0x2A] -ne 0xCC) { continue } + $msgLen = $b[0x2C] + if ($b.Length -lt 0x2A + $msgLen + 2) { continue } + $sum = 0; for ($i = 0x2A; $i -lt 0x2A + $msgLen; $i++) { $sum += $b[$i] } + $ck = ($b[0x2A + $msgLen] -shl 8) -bor $b[0x2A + $msgLen + 1] + if (($sum -band 0xFFFF) -eq $ck) { $okC++ } else { $badC++; if ($badSamples.Count -lt 3) { [void]$badSamples.Add(@{t=$a.t; len=$a.len; sum=($sum -band 0xFFFF); ck=$ck}) } } +} +Write-Host (" checksum OK={0} BAD={1}" -f $okC, $badC) +foreach ($s in $badSamples) { Write-Host (" BAD样本: t={0:F3} len={1} sum=0x{2:x4} ck=0x{3:x4}" -f $s.t, $s.len, $s.sum, $s.ck) } + +# 脉冲帧 (0x2A=0xAA) 内容: 解码 UID +Write-Host "--- DISC 脉冲帧 (0x2A=0xAA) UID 解码 ---" +$pulseUids = @{} +foreach ($a in $answers) { + $b = $a.b + if ($b[0x2A] -ne 0xAA) { continue } + # 定位 0xAA 分隔符后的 16 字节编码 UID + $uid = "" + for ($i = 0; $i -lt 6; $i++) { + $b1 = $b[0x2B + 2*$i]; $b2 = $b[0x2C + 2*$i] + $byte = ($b1 -band 0x55) -bor ($b2 -band 0xAA) + $uid += $byte.ToString("x2") + } + if (-not $pulseUids.ContainsKey($uid)) { $pulseUids[$uid] = 0 }; $pulseUids[$uid]++ +} +$pulseUids.GetEnumerator() | Sort-Object Value -Descending | ForEach-Object { Write-Host (" UID={0} : {1} 脉冲" -f $_.Key, $_.Value) } + +# ============ 4) 查询-应答配对: cmd=18 按 PID, 时延统计 ============ +Write-Host "`n=========== [4] cmd=18 查询→应答配对 (seq 0x22-0x23 ↔ 0x20-0x21) ===========" +$ansBySeq = @{} +foreach ($a in $answers) { + $b = $a.b + if ($b.Length -lt 0x22) { continue } + $s = G16 $b 0x20 + if (-not $ansBySeq.ContainsKey($s)) { $ansBySeq[$s] = New-Object System.Collections.ArrayList } + [void]$ansBySeq[$s].Add($a) +} +$pairStats = @{}; $latByPid = @{} +foreach ($q in $queries) { + if ($q.cmd -ne 0x18 -or $q.flag -ne 0) { continue } + $pk = if ($q.pid) { $q.pid } else { "????" } + if (-not $pairStats.ContainsKey($pk)) { $pairStats[$pk] = @{ n=0; ans=0; lens=@{} } } + $pairStats[$pk].n++ + # 找该查询之后 200ms 内 seq 回显的应答 + $best = $null + foreach ($a in $answers) { + if ($a.t -lt $q.t -or $a.t -gt $q.t + 0.2) { continue } + $s = G16 $a.b 0x20 + if ($s -eq $q.seq) { $best = $a; break } + } + if ($best) { + $pairStats[$pk].ans++ + $al = $best.len + if (-not $pairStats[$pk].lens.ContainsKey($al)) { $pairStats[$pk].lens[$al] = 0 } + $pairStats[$pk].lens[$al]++ + if (-not $latByPid.ContainsKey($pk)) { $latByPid[$pk] = New-Object System.Collections.ArrayList } + [void]$latByPid[$pk].Add([int](($best.t - $q.t) * 1000)) + } +} +foreach ($k in ($pairStats.Keys | Sort-Object)) { + $st = $pairStats[$k] + $lensStr = ($st.lens.GetEnumerator() | ForEach-Object { "$($_.Key)B×$($_.Value)" }) -join " " + $latStr = "" + if ($latByPid.ContainsKey($k) -and $latByPid[$k].Count -gt 0) { + $la = $latByPid[$k] | Sort-Object + $latStr = (" 时延ms: min={0} 中位={1} max={2}" -f $la[0], $la[[int]($la.Count/2)], $la[-1]) + } + Write-Host (" PID={0} 查询={1} 配对应答={2} 长度[{3}]{4}" -f $k, $st.n, $st.ans, $lensStr, $latStr) +} + +# ============ 5) 0002/0003 应答逐字节样本 ============ +Write-Host "`n=========== [5] PID=0002 / 0003 应答样本 (前2个) ===========" +foreach ($wantPid in @("0002", "0003")) { + $cnt = 0 + foreach ($q in $queries) { + if ($q.cmd -ne 0x18 -or $q.flag -ne 0 -or $q.pid -ne $wantPid) { continue } + foreach ($a in $answers) { + if ($a.t -lt $q.t -or $a.t -gt $q.t + 0.2) { continue } + if ((G16 $a.b 0x20) -eq $q.seq) { + Write-Host (" PID={0} UID={1} t={2:F3} 查询len -> 应答{3}B: {4}" -f $wantPid, $q.uid, $q.t, $a.len, (($a.b | ForEach-Object { $_.ToString("x2") }) -join "")) + $cnt++; break + } + } + if ($cnt -ge 2) { break } + } + if ($cnt -eq 0) { Write-Host " PID=$wantPid 无配对样本" } +} + +# ============ 6) 发现收敛: 全范围查询(24) 与脉冲随时间 ============ +Write-Host "`n=========== [6] 发现收敛时间线 (10s 桶: 全范围查询数 / 脉冲应答数 / MUTE数) ===========" +$buckets = @{} +foreach ($q in $queries) { + if ($q.flag -ne 0) { continue } + $bk = [int]($q.t / 10) * 10 + if (-not $buckets.ContainsKey($bk)) { $buckets[$bk] = @{ disc=0; get=0; pulse=0; mute=0; unmute=0 } } + if ($q.cmd -eq 0x24) { $buckets[$bk].disc++ } + if ($q.cmd -eq 0x18 -and $q.pid -eq "0003") { $buckets[$bk].mute++ } + if ($q.cmd -eq 0x18 -and $q.pid -eq "0002") { $buckets[$bk].unmute++ } + if ($q.cmd -eq 0x18) { $buckets[$bk].get++ } +} +# 脉冲数按桶 +foreach ($a in $answers) { + if ($a.b[0x2A] -ne 0xAA) { continue } + $bk = [int]($a.t / 10) * 10 + if (-not $buckets.ContainsKey($bk)) { $buckets[$bk] = @{ disc=0; get=0; pulse=0; mute=0; unmute=0 } } + $buckets[$bk].pulse++ +} +foreach ($bk in ($buckets.Keys | Sort-Object)) { + $v = $buckets[$bk] + Write-Host (" t={0,4}-{1,-4}s 全范围查询={2,-5} GET查询={3,-5} 脉冲={4,-5} MUTE={5,-4} UNMUTE={6}" -f $bk, ($bk+10), $v.disc, $v.get, $v.pulse, $v.mute, $v.unmute) +} + +# ============ 7) 84B 状态帧节奏 ============ +Write-Host "`n=========== [7] 84B 状态帧节奏 (.1->113) ===========" +$statTimes = New-Object System.Collections.ArrayList +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + if ($p[1] -ne "192.168.123.1" -or $p[2] -ne "192.168.123.113") { continue } + $b = H2B $p[4] + if ($b.Length -eq 84 -and (G32 $b 4) -eq 4) { [void]$statTimes.Add([double]$p[0]) } +} +Write-Host (" 84B 总数={0}" -f $statTimes.Count) +if ($statTimes.Count -gt 2) { + $gaps = New-Object System.Collections.ArrayList + for ($i = 1; $i -lt $statTimes.Count; $i++) { [void]$gaps.Add([int](($statTimes[$i] - $statTimes[$i-1]) * 1000)) } + $gs = $gaps | Sort-Object + Write-Host (" 帧间隔ms: min={0} 中位={1} max={2}" -f $gs[0], $gs[[int]($gs.Count/2)], $gs[-1]) + # 突发检测: 间隔>1000ms 为新突发 + $bursts = 1; foreach ($g in $gaps) { if ($g -gt 1000) { $bursts++ } } + Write-Host (" 突发数={0} (平均每突发 {1:F1} 帧)" -f $bursts, ($statTimes.Count / $bursts)) +} + +# ============ 8) DEVICE_INFO(0060) 读取对象 UID ============ +Write-Host "`n=========== [8] DEVICE_INFO(0060)/参数读取的 UID 分布 ===========" +$diUids = @{} +foreach ($q in $queries) { + if ($q.cmd -ne 0x18 -or $q.flag -ne 0) { continue } + if ($q.pid -eq "0060" -or $q.pid -eq "0080" -or $q.pid -eq "00f0" -or $q.pid -eq "0003") { + $k = "$($q.pid)->$($q.uid)"; if (-not $diUids.ContainsKey($k)) { $diUids[$k] = 0 }; $diUids[$k]++ + } +} +$diUids.GetEnumerator() | Sort-Object Name | ForEach-Object { Write-Host (" {0} : {1}" -f $_.Key, $_.Value) } +Write-Host "`n分析完成。" diff --git a/scripts/session/analyze_cold2.ps1 b/scripts/session/analyze_cold2.ps1 new file mode 100644 index 0000000..2b4e313 --- /dev/null +++ b/scripts/session/analyze_cold2.ps1 @@ -0,0 +1,142 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +$f = "$env:TEMP\opencode\cold2.tsv" +$lines = [IO.File]::ReadAllLines($f) + +function H2B($hex) { + if (-not $hex) { return $null } + $n = $hex.Length / 2 + $b = New-Object byte[] $n + for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring(2*$i, 2), 16) } + return $b +} +function G16le($b, $o) { if ($b.Length -lt $o+2) { return -1 }; return $b[$o] -bor ($b[$o+1] -shl 8) } +function G32le($b, $o) { if ($b.Length -lt $o+4) { return -1 }; return $b[$o] -bor ($b[$o+1] -shl 8) -bor ($b[$o+2] -shl 16) -bor ($b[$o+3] -shl 24) } +function Hex($b) { return ($b | ForEach-Object { $_.ToString("x2") }) -join "" } +function UidStr($b, $o) { if ($b.Length -lt $o+6) { return "?" }; return ($b[$o..($o+5)] | ForEach-Object { $_.ToString("x2") }) -join "" } + +# checksum 测试: 先用已知 0002 样本验证公式 +Write-Host "=========== [0] checksum 公式自验 (0002 样本) ===========" +$sample = H2B "454e65740400000000001c7c00000000000001004600000002004275746c0000a5000000000000001c00cc011a453a7fffffff0899500026c952000000001100020200000729" +$msgLen = $sample[0x2C] +$sum = 0; for ($i = 0x2A; $i -lt 0x2A + $msgLen; $i++) { $sum += $sample[$i] } +$ckOff = 0x2A + $msgLen +Write-Host (" msgLen={0} sum=0x{1:x4} field=0x{2:x4} -> {3}" -f $msgLen, ($sum -band 0xFFFF), (($sample[$ckOff] -shl 8) -bor $sample[$ckOff+1]), $(if (($sum -band 0xFFFF) -eq (($sample[$ckOff] -shl 8) -bor $sample[$ckOff+1])) {"OK"} else {"BAD"})) + +# ============ A) .1->113 全帧型直方图 (type x len) ============ +Write-Host "`n=========== [A] .1→113 全帧型 (type×len) 直方图 ===========" +$h = @{} +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + if ($p[1] -ne "192.168.123.1") { continue } + $b = H2B $p[4]; if ($b.Length -lt 0x1A) { continue } + $t = G32le $b 4 + $k = "type=$t len=$($b.Length)" + if (-not $h.ContainsKey($k)) { $h[$k] = 0 }; $h[$k]++ +} +$h.GetEnumerator() | Sort-Object { [int]($_.Value) } -Descending | Select-Object -First 25 | ForEach-Object { Write-Host (" {0} : {1}" -f $_.Key, $_.Value) } + +# ============ B) type=2 帧时间线 (注册/AECH/AERE/心跳) ============ +Write-Host "`n=========== [B] type=2 帧时间线 (全程, 前 40 条) ===========" +$shown = 0 +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + $b = H2B $p[4]; if ($b.Length -lt 0x1A) { continue } + if ((G32le $b 4) -ne 2) { continue } + $sub = G16le $b 0x18 + $t = [double]$p[0] + Write-Host (" t={0,8:F3} {1,-26} len={2,-4} sub={3}" -f $t, "$($p[1])->$($p[2])", $b.Length, $sub) + $shown++; if ($shown -ge 40) { break } +} + +# ============ C) 发现请求/应答: 180B 请求 vs 217B 应答 全文 ============ +Write-Host "`n=========== [C] 180B 发现请求 与 217B 应答 样本 ===========" +$req180 = $null; $resp217 = $null +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + $b = H2B $p[4] + if (-not $req180 -and $p[1] -eq "192.168.123.113" -and $b.Length -eq 180) { $req180 = $b } + if (-not $resp217 -and $p[1] -eq "192.168.123.1" -and $b.Length -eq 217) { $resp217 = $b } + if ($req180 -and $resp217) { break } +} +if ($req180) { Write-Host (" 请求180B: " + (Hex $req180)) } +if ($resp217) { Write-Host (" 应答217B: " + (Hex $resp217)) } + +# ============ D) cmd=19 / cmd=1a 查询与应答样本 ============ +Write-Host "`n=========== [D] cmd=19 / cmd=1a 样本 (各3条查询+应答) ===========" +foreach ($wantCmd in @(0x19, 0x1a)) { + $cnt = 0 + foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + if ($p[1] -ne "192.168.123.113" -or $p[2] -ne "192.168.123.1") { continue } + $b = H2B $p[4]; if ($b.Length -lt 0x52 -or $b.Length -gt 2000) { continue } + if ((G32le $b 4) -ne 4 -or $b[0x3D] -ne $wantCmd -or $b[0x30] -ne 0) { continue } + $t = [double]$p[0] + Write-Host (" cmd={0:x2} t={1:F3} UID={2} 查询({3}B): {4}" -f $wantCmd, $t, (UidStr $b 0x3E), $b.Length, (Hex ($b[0x20..0x2F] + $b[0x3C..([Math]::Min($b.Length-1, 0x58))]))) + $cnt++; if ($cnt -ge 3) { break } + } +} + +# ============ E) flag=01 PID=0003 帧样本 (780条是什么) ============ +Write-Host "`n=========== [E] flag=01 PID=0003 帧样本 (前3条, 含UID) ===========" +$cnt = 0 +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + if ($p[1] -ne "192.168.123.113" -or $p[2] -ne "192.168.123.1") { continue } + $b = H2B $p[4]; if ($b.Length -lt 0x52 -or $b.Length -gt 2000) { continue } + if ((G32le $b 4) -ne 4 -or $b[0x3D] -ne 0x18 -or $b[0x30] -ne 1) { continue } + if ($b[0x50] -ne 0 -or $b[0x51] -ne 3) { continue } + $t = [double]$p[0] + Write-Host (" t={0:F3} len={1} flag01 UID={2} hdr: {3}" -f $t, $b.Length, (UidStr $b 0x3E), (Hex $b[0x20..0x33])) + $cnt++; if ($cnt -ge 3) { break } +} + +# ============ F) 脉冲时延: cmd=24 查询 → 脉冲 ============ +Write-Host "`n=========== [F] cmd=24(flag=00) 查询→脉冲 时延分布 ===========" +$discQ = New-Object System.Collections.ArrayList +$pulses = New-Object System.Collections.ArrayList +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + $b = H2B $p[4]; if ($b.Length -lt 0x2E) { continue } + $t = [double]$p[0] + if ($p[1] -eq "192.168.123.113" -and $p[2] -eq "192.168.123.1" -and $b.Length -gt 0x44 -and $b.Length -lt 2000 -and (G32le $b 4) -eq 4 -and $b[0x3D] -eq 0x24 -and $b[0x30] -eq 0) { + [void]$discQ.Add(@{ t=$t; lo=($b[0x3E..0x43] | ForEach-Object { $_.ToString("x2") }) -join "" }) + } + if ($p[1] -eq "192.168.123.1" -and $b.Length -lt 200 -and $b[0x2A] -eq 0xAA) { + [void]$pulses.Add(@{ t=$t }) + } +} +Write-Host (" cmd24查询={0} 脉冲={1}" -f $discQ.Count, $pulses.Count) +$lats = New-Object System.Collections.ArrayList +foreach ($pu in $pulses) { + # 找之前最近的查询 + $best = $null + for ($i = $discQ.Count - 1; $i -ge 0; $i--) { + if ($discQ[$i].t -le $pu.t) { $best = $discQ[$i]; break } + } + if ($best -and ($pu.t - $best.t) -lt 0.5) { [void]$lats.Add([int](($pu.t - $best.t) * 1000)) } +} +if ($lats.Count -gt 0) { + $ls = $lats | Sort-Object + Write-Host (" 脉冲时延ms: n={0} min={1} 中位={2} max={3}" -f $ls.Count, $ls[0], $ls[[int]($ls.Count/2)], $ls[-1]) +} + +# ============ G) cmd=24 查询负载: 分支范围 vs 全范围 ============ +Write-Host "`n=========== [G] cmd=24 查询的搜索范围分布 (UID 下界@0x3E) ===========" +$ranges = @{} +foreach ($q in $discQ) { + if (-not $ranges.ContainsKey($q.lo)) { $ranges[$q.lo] = 0 }; $ranges[$q.lo]++ +} +$ranges.GetEnumerator() | Sort-Object Value -Descending | Select-Object -First 10 | ForEach-Object { Write-Host (" 范围下界={0} : {1}" -f $_.Key, $_.Value) } + +# ============ H) AECH/AERE 帧 (594B) 抓取 ============ +Write-Host "`n=========== [H] AECH/AERE (594B type=2) 时间点 ===========" +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + $b = H2B $p[4]; if ($b.Length -lt 500) { continue } + if ((G32le $b 4) -ne 2) { continue } + $sub = G16le $b 0x18 + $t = [double]$p[0] + Write-Host (" t={0:F3} {1} len={2} sub={3} hasAECH={4} hasAERE={5}" -f $t, "$($p[1])->$($p[2])", $b.Length, $sub, ((Hex $b) -match "41454348"), ((Hex $b) -match "41455245")) +} +Write-Host "`n分析完成。" diff --git a/scripts/session/analyze_coldstart_seq.ps1 b/scripts/session/analyze_coldstart_seq.ps1 new file mode 100644 index 0000000..12a30a5 --- /dev/null +++ b/scripts/session/analyze_coldstart_seq.ps1 @@ -0,0 +1,31 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$f = "$env:TEMP\opencode\cold2.tsv" +$lines = [IO.File]::ReadAllLines($f) +function H2B($hex) { $n = $hex.Length / 2; $b = New-Object byte[] $n; for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring(2*$i, 2), 16) }; return $b } +function G32le($b, $o) { return $b[$o] -bor ($b[$o+1] -shl 8) -bor ($b[$o+2] -shl 16) -bor ($b[$o+3] -shl 24) } + +Write-Host "=== cold2 前 120 帧 113→.1 (认领建立期) ===" +$count = 0 +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5) { continue } + if ($p[1] -ne "192.168.123.113") { continue } + $b = H2B $p[4] + $t = [double]$p[0] + $len = $b.Length + $magic = [Text.Encoding]::ASCII.GetString($b[0..3]) + $type = G32le $b 4 + $desc = "" + if ($magic -eq "ENet" -and $type -eq 4 -and $len -ge 0x50) { + $sub = "{0:x2}{1:x2}" -f $b[0x19], $b[0x18] + $flag = $b[0x30] + if ($b[0x2A] -eq 0xCC -or $b[0x3C] -eq 0x01) { + $msgLen = $b[0x3D]; $cc = $b[0x4F]; $pid = ($b[0x50] -shl 8) -bor $b[0x51] + $desc = "RDM请求 sub=$sub flag=$('{0:x2}' -f $flag) msgLen=$msgLen CC=$('{0:x2}' -f $cc) PID=$('{0:x4}' -f $pid) TN=$('{0:x2}' -f $b[0x4A])" + } else { $desc = "type4 sub=$sub flag=$('{0:x2}' -f $flag) 0x2A=$('{0:x2}' -f $b[0x2A])" } + } elseif ($magic -eq "ENet" -and $type -eq 4) { + $desc = "type4 短帧" + } + Write-Host ("{0,9:N3} len={1,-5} magic={2} type={3} {4}" -f $t, $len, $magic, $type, $desc) + $count++ + if ($count -ge 120) { break } +} diff --git a/scripts/session/analyze_dmx1.ps1 b/scripts/session/analyze_dmx1.ps1 new file mode 100644 index 0000000..31e55ba --- /dev/null +++ b/scripts/session/analyze_dmx1.ps1 @@ -0,0 +1,70 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +function H2B($hex) { + if (-not $hex) { return $null } + $n = $hex.Length / 2 + $b = New-Object byte[] $n + for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring(2*$i, 2), 16) } + return $b +} +$lines = [IO.File]::ReadAllLines("$env:TEMP\opencode\ss123.tsv") +$dmx = New-Object System.Collections.ArrayList +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + if ($p[1] -ne "192.168.123.113" -or $p[2] -ne "192.168.123.1") { continue } + $b = H2B $p[4]; if ($b.Length -ne 1084) { continue } + if ([int]$b[4] -ne 4) { continue } + if (-not ($b[0x20] -eq 1 -and $b[0x21] -eq 1)) { continue } + [void]$dmx.Add(@{ t=[double]$p[0]; b=$b }) +} +Write-Host ("DMX-A 帧数: {0}" -f $dmx.Count) +if ($dmx.Count -eq 0) { exit } + +# --- 1. 头部逐字节变化分析 (0x00-0x3B): 常量 or 变量 --- +Write-Host "`n=== 头部 0x00-0x3B 逐字节分析 ===" +$first = $dmx[0].b +for ($off = 0; $off -lt 0x3C; $off++) { + $min = 255; $max = 0; $diff = 0 + foreach ($f in $dmx) { $v = [int]$f.b[$off]; if ($v -lt $min) { $min = $v }; if ($v -gt $max) { $max = $v }; if ($v -ne [int]$first[$off]) { $diff++ } } + if ($diff -gt 0) { + Write-Host (" 0x{0:x2}: 变量 首值={1:x2} min={2:x2} max={3:x2} 变化帧数={4}" -f $off, $first[$off], $min, $max, $diff) + } +} +Write-Host " (未列出 = 全部帧常量)" +Write-Host "`n=== 常量头 hex (首帧 0x00-0x3B) ===" +Write-Host (" " + (($first[0..0x3B] | ForEach-Object { $_.ToString("x2") }) -join " ")) + +# --- 2. 变化字段的语义: 找 16/32 位递增计数器 --- +Write-Host "`n=== 疑似序号字段 (按 LE u16/u32 检验单调性) ===" +foreach ($off in @(0x22, 0x24, 0x26, 0x28, 0x2A, 0x2C, 0x2E, 0x30, 0x32, 0x34, 0x36, 0x38, 0x3A)) { + $vals = $dmx | ForEach-Object { [int]$_.b[$off] + [int]$_.b[$off+1]*256 } + $inc = 0; $dec = 0 + for ($i = 1; $i -lt $vals.Count; $i++) { if ($vals[$i] -gt $vals[$i-1]) { $inc++ } elseif ($vals[$i] -lt $vals[$i-1]) { $dec++ } } + if ($inc -gt $vals.Count * 0.8 -or $dec -gt $vals.Count * 0.8) { + Write-Host (" u16@0x{0:x2}: 首={1} 尾={2} 增={3} 减={4}" -f $off, $vals[0], $vals[-1], $inc, $dec) + } +} + +# --- 3. 通道数据区 0x3C-0x43B (1024B): 非零分布 --- +Write-Host "`n=== 通道数据区非零统计 (0x3C 起 1024B) ===" +$nz = @{} +foreach ($f in $dmx) { + for ($i = 0x3C; $i -lt 0x43C; $i++) { if ($f.b[$i] -ne 0) { $rel = $i - 0x3C; if (-not $nz.ContainsKey($rel)) { $nz[$rel] = 0 }; $nz[$rel]++ } } +} +$nzKeys = $nz.Keys | Sort-Object +Write-Host (" 非零字节位置数: {0}" -f $nzKeys.Count) +if ($nzKeys.Count -gt 0) { + Write-Host (" 最小相对偏移: {0} (CH{1}) 最大相对偏移: {2} (CH{3})" -f $nzKeys[0], ($nzKeys[0]+1), $nzKeys[-1], ($nzKeys[-1]+1)) + $ranges = @(); $rs = $nzKeys[0]; $rp = $nzKeys[0] + foreach ($k in $nzKeys) { if ($k -gt $rp + 1) { $ranges += "$rs-$rp"; $rs = $k }; $rp = $k } + $ranges += "$rs-$rp" + Write-Host (" 连续非零区间 (相对0x3C): " + ($ranges -join ", ")) +} +# 首帧前 32 通道值 +Write-Host (" 首帧 CH1-32: " + (($first[0x3C..0x5B] | ForEach-Object { $_.ToString("x2") }) -join " ")) + +# --- 4. 速率与时间隔 --- +$tg = New-Object System.Collections.ArrayList +for ($i = 1; $i -lt $dmx.Count; $i++) { [void]$tg.Add([int](($dmx[$i].t - $dmx[$i-1].t) * 1000)) } +$tgs = $tg | Sort-Object +Write-Host ("`n=== 帧间隔ms: min={0} p50={1} p90={2} max={3} 率={4:N1}/s ===" -f $tgs[0], $tgs[[int]($tgs.Count/2)], $tgs[[int]($tgs.Count*0.9)], $tgs[-1], ($dmx.Count / ($dmx[-1].t - $dmx[0].t))) diff --git a/scripts/session/analyze_dmx2.ps1 b/scripts/session/analyze_dmx2.ps1 new file mode 100644 index 0000000..30693c8 --- /dev/null +++ b/scripts/session/analyze_dmx2.ps1 @@ -0,0 +1,82 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +function H2B($hex) { + if (-not $hex) { return $null } + $n = $hex.Length / 2 + $b = New-Object byte[] $n + for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring(2*$i, 2), 16) } + return $b +} +$lines = [IO.File]::ReadAllLines("$env:TEMP\opencode\ss123.tsv") +$dmx = New-Object System.Collections.ArrayList +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + if ($p[1] -ne "192.168.123.113" -or $p[2] -ne "192.168.123.1") { continue } + $b = H2B $p[4]; if ($b.Length -ne 1084) { continue } + if ([int]$b[4] -ne 4) { continue } + if (-not ($b[0x20] -eq 1 -and $b[0x21] -eq 1)) { continue } + [void]$dmx.Add(@{ t=[double]$p[0]; b=$b }) +} +Write-Host ("DMX-A 帧数: {0}" -f $dmx.Count) + +# --- 0x22-0x29 作为 LE u64 检验 --- +function GU64($b, $off) { + $v = [uint64]0 + for ($i = 7; $i -ge 0; $i--) { $v = $v * 256 + [uint64]$b[$off+$i] } + return $v +} +$inc64 = 0; $dec64 = 0; $eq = 0 +$deltas = New-Object System.Collections.ArrayList +for ($i = 1; $i -lt $dmx.Count; $i++) { + $v0 = GU64 $dmx[$i-1].b 0x22; $v1 = GU64 $dmx[$i].b 0x22 + if ($v1 -gt $v0) { $inc64++; [void]$deltas.Add($v1 - $v0) } elseif ($v1 -lt $v0) { $dec64++ } else { $eq++ } +} +Write-Host ("u64@0x22: 增={0} 减={1} 等={2}" -f $inc64, $dec64, $eq) +if ($deltas.Count -gt 0) { + $ds = $deltas | Sort-Object + Write-Host (" 增量: min={0} p50={1} p90={2} max={3}" -f $ds[0], $ds[[int]($ds.Count/2)], $ds[[int]($ds.Count*0.9)], $ds[-1]) + $top = $deltas | Group-Object | Sort-Object { [int]$_.Name } | Select-Object -First 8 + Write-Host (" 常见增量: " + (($top | ForEach-Object { "{0}×{1}" -f $_.Name, $_.Count }) -join ", ")) +} +Write-Host (" 首帧 u64={0} (0x{0:x}) 尾帧 u64={1} (0x{1:x})" -f (GU64 $dmx[0].b 0x22), (GU64 $dmx[-1].b 0x22)) + +# --- u64 增量 vs 实际时间差: 是否微秒时间戳 --- +Write-Host "`n=== u64 增量 vs 时间差相关性 (前 12 帧) ===" +for ($i = 1; $i -lt 13; $i++) { + $dt = ($dmx[$i].t - $dmx[$i-1].t) * 1000000 # us + $dv = [int64]((GU64 $dmx[$i].b 0x22) - (GU64 $dmx[$i-1].b 0x22)) + Write-Host (" dt={0,8:N0}us du64={1,8}" -f $dt, $dv) +} + +# --- 0x1A-0x1B: 与通道数据的校验和关系? --- +Write-Host "`n=== 0x1A-0x1B 检验 (是否通道数据校验) ===" +$matchSum = 0; $matchCrc = 0 +foreach ($f in ($dmx | Select-Object -First 200)) { + $stored = [int]$f.b[0x1A] + [int]$f.b[0x1B]*256 + # 简单和 (0x3C 起 1024B) + $sum = 0; foreach ($v in $f.b[0x3C..0x43B]) { $sum += $v } + if (($sum -band 0xFFFF) -eq $stored) { $matchSum++ } + # CRC16-IBM + $crc = 0 + foreach ($v in $f.b[0x3C..0x43B]) { + $crc = $crc -bxor $v + for ($j = 0; $j -lt 8; $j++) { if ($crc -band 1) { $crc = ($crc -shr 1) -bxor 0xA001 } else { $crc = $crc -shr 1 } } + } + if ($crc -eq $stored) { $matchCrc++ } +} +Write-Host (" 简单和匹配: {0}/200 CRC16-IBM匹配: {1}/200" -f $matchSum, $matchCrc) +# 打印几个 stored 值与帧序关系 +Write-Host (" 0x1A-1B 前 10 帧: " + ((($dmx | Select-Object -First 10) | ForEach-Object { "{0:x2}{1:x2}" -f $_.b[0x1B], $_.b[0x1A] }) -join " ")) + +# --- 0x22-0x29 是否某种位打包 seq? 试 u32@0x22 / u32@0x26 --- +foreach ($off in @(0x22, 0x24, 0x26)) { + $v0 = (GU64 $dmx[0].b $off) -band 0xFFFFFFFF + $vN = (GU64 $dmx[-1].b $off) -band 0xFFFFFFFF + $inc = 0; $dec = 0 + for ($i = 1; $i -lt $dmx.Count; $i++) { + $a = (GU64 $dmx[$i-1].b $off) -band 0xFFFFFFFF + $b2 = (GU64 $dmx[$i].b $off) -band 0xFFFFFFFF + if ($b2 -gt $a) { $inc++ } elseif ($b2 -lt $a) { $dec++ } + } + Write-Host ("u32@0x{0:x2}: 首={1} 尾={2} 增={3} 减={4}" -f $off, $v0, $vN, $inc, $dec) +} diff --git a/scripts/session/analyze_dmx3.ps1 b/scripts/session/analyze_dmx3.ps1 new file mode 100644 index 0000000..a28bb5f --- /dev/null +++ b/scripts/session/analyze_dmx3.ps1 @@ -0,0 +1,96 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +function H2B($hex) { + if (-not $hex) { return $null } + $n = $hex.Length / 2 + $b = New-Object byte[] $n + for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring(2*$i, 2), 16) } + return $b +} +function HexR($b, $o, $l) { return (($b[$o..($o+$l-1)] | ForEach-Object { $_.ToString("x2") }) -join "") } +$lines = [IO.File]::ReadAllLines("$env:TEMP\opencode\ss123.tsv") +$dmx = New-Object System.Collections.ArrayList +$keys = New-Object System.Collections.ArrayList +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + $b = H2B $p[4] + if ($p[1] -eq "192.168.123.113" -and $p[2] -eq "192.168.123.1" -and $b.Length -eq 1084 -and [int]$b[4] -eq 4 -and $b[0x20] -eq 1 -and $b[0x21] -eq 1) { + [void]$dmx.Add(@{ t=[double]$p[0]; b=$b }) + } + # 密钥报文: 主机→广播 1947B 帧 + if ($p[1] -eq "192.168.123.113" -and $p[2] -match "255" -and $b.Length -ge 1900) { + [void]$keys.Add(@{ t=[double]$p[0]; b=$b }) + } +} +Write-Host ("DMX-A={0} 密钥广播={1}" -f $dmx.Count, $keys.Count) + +# === 1. 相同通道数据的帧, 可变字节是否相同? === +Write-Host "`n=== 相同通道数据帧的可变字节对比 ===" +$byData = @{} +foreach ($f in $dmx) { + $dh = HexR $f.b 0x3C 64 # 前64通道作指纹 + if (-not $byData.ContainsKey($dh)) { $byData[$dh] = New-Object System.Collections.ArrayList } + [void]$byData[$dh].Add($f) +} +$groups = $byData.GetEnumerator() | Where-Object { $_.Value.Count -ge 2 } | Select-Object -First 3 +$gcount = 0 +foreach ($g in $groups) { + $gcount++ + Write-Host (" 组{0} ({1}帧):" -f $gcount, $g.Value.Count) + foreach ($f in ($g.Value | Select-Object -First 4)) { + Write-Host (" t={0,8:F3} 0x1A={1} 0x22={2}" -f $f.t, (HexR $f.b 0x1A 2), (HexR $f.b 0x22 8)) + } +} +if ($gcount -eq 0) { Write-Host " (120s 内通道数据无重复 — 效果持续变化)" } + +# === 2. 可变字节是否随帧序平滑变化? 连续 8 帧的 0x22-0x29 === +Write-Host "`n=== 连续 8 帧 0x1A/0x22 原始值 ===" +foreach ($f in ($dmx | Select-Object -First 8)) { + Write-Host (" t={0,8:F3} 0x1A={1} 0x22-0x29={2}" -f $f.t, (HexR $f.b 0x1A 2), (HexR $f.b 0x22 8)) +} + +# === 3. 密钥广播结构 === +if ($keys.Count -gt 0) { + Write-Host "`n=== 密钥广播帧 (前2个) ===" + foreach ($k in ($keys | Select-Object -First 2)) { + Write-Host (" t={0,8:F3} len={1}" -f $k.t, $k.b.Length) + Write-Host (" 0x00-0x2F: " + (HexR $k.b 0 0x30)) + Write-Host (" 0x30-0x5F: " + (HexR $k.b 0x30 0x30)) + } + # 密钥帧之间是否相同 + if ($keys.Count -ge 2) { + $same = 0; $diff = 0 + for ($i = 0; $i -lt [Math]::Min(0x100, $keys[0].b.Length); $i++) { + if ($keys[0].b[$i] -eq $keys[1].b[$i]) { $same++ } else { $diff++ } + } + Write-Host (" 帧1vs帧2 前256B: 同={0} 异={1}" -f $same, $diff) + } + # 密钥帧间隔 + $kg = New-Object System.Collections.ArrayList + for ($i = 1; $i -lt $keys.Count; $i++) { [void]$kg.Add([int](($keys[$i].t - $keys[$i-1].t) * 1000)) } + Write-Host (" 间隔ms: " + (($kg | Select-Object -First 12) -join ", ")) +} + +# === 4. 0x22-0x29 是否 = f(通道数据)? 试常见简单哈希 === +Write-Host "`n=== 0x22-0x29 vs 通道数据简单函数 ===" +$m_add=0; $m_xor=0; $m_crc32=0 +foreach ($f in ($dmx | Select-Object -First 100)) { + $d = $f.b[0x3C..0x43B] + # CRC32 (IEEE) + $crc = [uint32]0xFFFFFFFF + foreach ($v in $d) { + $crc = $crc -bxor $v + for ($j=0; $j -lt 8; $j++) { if ($crc -band 1) { $crc = ($crc -shr 1) -bxor 0xEDB88320 } else { $crc = $crc -shr 1 } } + } + $crc = $crc -bxor 0xFFFFFFFF + $s0 = [uint32]$f.b[0x22] + [uint32]$f.b[0x23]*256 + [uint32]$f.b[0x24]*65536 + [uint32]$f.b[0x25]*16777216 + $s1 = [uint32]$f.b[0x26] + [uint32]$f.b[0x27]*256 + [uint32]$f.b[0x28]*65536 + [uint32]$f.b[0x29]*16777216 + if ($crc -eq $s0 -or $crc -eq $s1) { $m_crc32++ } +} +Write-Host (" CRC32(通道数据) 匹配 0x22或0x26: {0}/100" -f $m_crc32) + +# === 5. 常量字段 0x2C-0x3B 的稳定性 (是否随 universe 变化) === +Write-Host "`n=== 0x2C-0x3B 常量确认 ===" +$var34 = @{} +foreach ($f in $dmx) { $h = HexR $f.b 0x2C 16; if (-not $var34.ContainsKey($h)) { $var34[$h] = 0 }; $var34[$h]++ } +foreach ($kv in $var34.GetEnumerator()) { Write-Host (" {0} : {1}帧" -f $kv.Key, $kv.Value) } diff --git a/scripts/session/analyze_fix2.ps1 b/scripts/session/analyze_fix2.ps1 new file mode 100644 index 0000000..7445eec --- /dev/null +++ b/scripts/session/analyze_fix2.ps1 @@ -0,0 +1,81 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" + +function H2B($hex) { + if (-not $hex) { return $null } + $n = $hex.Length / 2 + $b = New-Object byte[] $n + for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring(2*$i, 2), 16) } + return $b +} +function UidStr($b, $o) { if ($b.Length -lt $o+6) { return "?" }; return ($b[$o..($o+5)] | ForEach-Object { $_.ToString("x2") }) -join "" } + +function Analyze2($name, $tsv, $devIp, $hostIp) { + $lines = [IO.File]::ReadAllLines($tsv) + Write-Host "`n############ $name ($devIp) ############" + # 真实 RDM 查询: 0x20-0x21 == 0200 (事件B帧), flag 0x30==0 + $rdmCmd = @{}; $rdmPid = @{}; $rdmPidUid = @{} + $falsePos = 0 + $bFrames = New-Object System.Collections.ArrayList # 所有事件B帧 (时间, 0x22序号, 0x3C, 0x3D) + foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + if ($p[1] -ne $hostIp -or $p[2] -ne $devIp) { continue } + $b = H2B $p[4]; if ($b.Length -lt 0x52 -or $b.Length -gt 2000) { continue } + if ([int]$b[4] -ne 4) { continue } + $t = [double]$p[0] + $isB = ($b[0x20] -eq 2 -and $b[0x21] -eq 0) + $isA = ($b[0x20] -eq 1 -and $b[0x21] -eq 1) + if ($isB -and $b.Length -gt 0x44) { + [void]$bFrames.Add(@{ t=$t; seq=([int]$b[0x22]+[int]$b[0x23]*256); c3c=$b[0x3C]; c3d=$b[0x3D]; flag=$b[0x30] }) + } + # RDM 判定: 必须 0x3C==01 且 cmd 匹配 + if ($b.Length -gt 0x44 -and $b[0x3C] -eq 1 -and ($b[0x3D] -eq 0x24 -or $b[0x3D] -eq 0x18 -or $b[0x3D] -eq 0x19 -or $b[0x3D] -eq 0x1a)) { + if ($b[0x30] -ne 0) { continue } + if ($isA) { $falsePos++; continue } # DMX-A 帧通道数据巧合 = 假阳性 + $cmd = $b[0x3D] + $ck = "cmd={0:x2}" -f $cmd + if (-not $rdmCmd.ContainsKey($ck)) { $rdmCmd[$ck] = 0 }; $rdmCmd[$ck]++ + if ($cmd -ne 0x24 -and $b.Length -gt 0x51) { + $pidKey = "{0:x2}{1:x2}" -f $b[0x50], $b[0x51] + $uid = UidStr $b 0x3E + if (-not $rdmPid.ContainsKey($pidKey)) { $rdmPid[$pidKey] = 0 }; $rdmPid[$pidKey]++ + $k2 = "$pidKey->$uid"; if (-not $rdmPidUid.ContainsKey($k2)) { $rdmPidUid[$k2] = 0 }; $rdmPidUid[$k2]++ + } + } + } + Write-Host ("--- 真实 RDM 查询 (排除 DMX-A 假阳性 {0} 条) ---" -f $falsePos) + $rdmCmd.GetEnumerator() | Sort-Object { [int]$_.Value } -Descending | ForEach-Object { Write-Host (" {0} : {1}" -f $_.Key, $_.Value) } + Write-Host "--- PID 分布 ---" + $rdmPid.GetEnumerator() | Sort-Object { [int]$_.Value } -Descending | Select-Object -First 12 | ForEach-Object { Write-Host (" PID={0} : {1}" -f $_.Key, $_.Value) } + Write-Host "--- PID->UID 细分 (0002/0003/0060/00f0/0201) ---" + $rdmPidUid.GetEnumerator() | Where-Object { $_.Name -match "^(0002|0003|0060|00f0|0201)" } | Sort-Object Name | ForEach-Object { Write-Host (" {0} : {1}" -f $_.Name, $_.Value) } + + # 42B ACK 相关性: 每个 42B 帧之前 200ms 内最近的 B 帧特征 + $acks = New-Object System.Collections.ArrayList + foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + if ($p[1] -ne $devIp -or $p[2] -ne $hostIp) { continue } + $b = H2B $p[4]; if ($b.Length -ne 42 -or [int]$b[4] -ne 4) { continue } + [void]$acks.Add(@{ t=([double]$p[0]); seq=([int]$b[0x20]+[int]$b[0x21]*256) }) + } + $ackByB = @{} + $ackNoB = 0 + foreach ($a in $acks) { + # 找 seq == ack.seq+1 的 B 帧 (§4.3: ACK seq = 事件 seq - 1) + $found = $null + foreach ($bf in $bFrames) { + if ($bf.seq -eq $a.seq + 1) { $found = $bf; break } + } + if ($found) { + $k = "0x3C={0:x2} 0x3D={1:x2} flag={2:x2}" -f $found.c3c, $found.c3d, $found.flag + if (-not $ackByB.ContainsKey($k)) { $ackByB[$k] = 0 }; $ackByB[$k]++ + } else { $ackNoB++ } + } + Write-Host ("--- 42B ACK ({0}个) 对应的 B 帧特征 ---" -f $acks.Count) + $ackByB.GetEnumerator() | Sort-Object { [int]$_.Value } -Descending | Select-Object -First 8 | ForEach-Object { Write-Host (" {0} : {1}" -f $_.Key, $_.Value) } + Write-Host (" (无配对 B 帧: {0})" -f $ackNoB) +} + +Analyze2 "真机 .1" "$env:TEMP\opencode\ss123.tsv" "192.168.123.1" "192.168.123.113" +Analyze2 "虚拟 .7 新代码" "$env:TEMP\opencode\fix2.tsv" "192.168.58.7" "192.168.58.107" +Write-Host "`n分析完成。" diff --git a/scripts/session/analyze_mute.ps1 b/scripts/session/analyze_mute.ps1 new file mode 100644 index 0000000..46a82d4 --- /dev/null +++ b/scripts/session/analyze_mute.ps1 @@ -0,0 +1,69 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +function H2B($hex) { + if (-not $hex) { return $null } + $n = $hex.Length / 2 + $b = New-Object byte[] $n + for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring(2*$i, 2), 16) } + return $b +} +$lines = [IO.File]::ReadAllLines("$env:TEMP\opencode\ss123.tsv") + +# 事件提取: 0003广播MUTE / 0002定向UNMUTE / 脉冲 +$events = New-Object System.Collections.ArrayList +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + $b = H2B $p[4]; if ($b.Length -lt 0x30) { continue } + $t = [double]$p[0] + if ($p[1] -eq "192.168.123.113" -and $p[2] -eq "192.168.123.1" -and [int]$b[4] -eq 4 -and $b.Length -gt 0x51 -and $b.Length -lt 2000 -and $b[0x20] -eq 2 -and $b[0x21] -eq 0 -and $b[0x30] -eq 0 -and $b[0x3C] -eq 1 -and $b[0x3D] -eq 0x18) { + $pid2 = "{0:x2}{1:x2}" -f $b[0x50], $b[0x51] + $uid = ($b[0x3E..0x43] | ForEach-Object { $_.ToString("x2") }) -join "" + if ($pid2 -eq "0003") { [void]$events.Add(@{ t=$t; kind="MUTE"; uid=$uid }) } + if ($pid2 -eq "0002") { [void]$events.Add(@{ t=$t; kind="UNMUTE"; uid=$uid }) } + } + if ($p[1] -eq "192.168.123.1" -and $p[2] -eq "192.168.123.113" -and $b.Length -lt 200 -and $b[0x2A] -eq 0xAA) { + # 解码脉冲 UID + $uid = "" + for ($i = 0; $i -lt 6; $i++) { + $b1 = $b[0x2B + 2*$i]; $b2 = $b[0x2C + 2*$i] + $byte = ([int]$b1 -band 0x55) -bor ([int]$b2 -band 0xAA) + $uid += $byte.ToString("x2") + } + [void]$events.Add(@{ t=$t; kind="PULSE"; uid=$uid }) + } +} +$mutes = @($events | Where-Object { $_.kind -eq "MUTE" }) +$unmutes = @($events | Where-Object { $_.kind -eq "UNMUTE" }) +$pulses = @($events | Where-Object { $_.kind -eq "PULSE" }) +Write-Host ("MUTE={0} UNMUTE={1} 脉冲={2}" -f $mutes.Count, $unmutes.Count, $pulses.Count) + +# MUTE 时间点与脉冲静默窗口: 每个 MUTE 后 3s 内脉冲数 vs 3-15s 脉冲数 +Write-Host "`n--- 广播 MUTE(0003) 时间点 vs 脉冲密度 ---" +foreach ($m in $mutes) { + $before = @($pulses | Where-Object { $_.t -ge $m.t - 3 -and $_.t -lt $m.t }).Count + $after3 = @($pulses | Where-Object { $_.t -ge $m.t -and $_.t -lt $m.t + 3 }).Count + $after12 = @($pulses | Where-Object { $_.t -ge $m.t + 3 -and $_.t -lt $m.t + 15 }).Count + Write-Host (" MUTE t={0,7:F2} 前3s脉冲={1,-3} 后0-3s={2,-3} 后3-15s={3}" -f $m.t, $before, $after3, $after12) +} + +# UNMUTE 时间点 vs 其后脉冲 (同 UID) +Write-Host "`n--- 定向 UNMUTE(0002) vs 其后 1s 内同 UID 脉冲 ---" +foreach ($u in ($unmutes | Select-Object -First 10)) { + $same = @($pulses | Where-Object { $_.t -ge $u.t -and $_.t -lt $u.t + 1 -and $_.uid -eq $u.uid }).Count + $any = @($pulses | Where-Object { $_.t -ge $u.t -and $_.t -lt $u.t + 1 }).Count + Write-Host (" UNMUTE t={0,7:F2} UID={1} 后1s同UID脉冲={2} 任意={3}" -f $u.t, $u.uid, $same, $any) +} + +# 脉冲的 UID 分布随时间 (每 15s 桶) +Write-Host "`n--- 脉冲 UID×15s 桶 ---" +$buckets = @{} +foreach ($pu in $pulses) { + $bk = [int]($pu.t / 15) + if (-not $buckets.ContainsKey($bk)) { $buckets[$bk] = @{} } + if (-not $buckets[$bk].ContainsKey($pu.uid)) { $buckets[$bk][$pu.uid] = 0 } + $buckets[$bk][$pu.uid]++ +} +foreach ($bk in ($buckets.Keys | Sort-Object)) { + $s = ($buckets[$bk].GetEnumerator() | ForEach-Object { "$($_.Key.Substring(8))×$($_.Value)" }) -join " " + Write-Host (" t={0,3}-{1,-3}s: {2}" -f ($bk*15), ($bk*15+15), $s) +} diff --git a/scripts/session/analyze_set.ps1 b/scripts/session/analyze_set.ps1 new file mode 100644 index 0000000..62e4b89 --- /dev/null +++ b/scripts/session/analyze_set.ps1 @@ -0,0 +1,57 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +function H2B($hex) { + if (-not $hex) { return $null } + $n = $hex.Length / 2 + $b = New-Object byte[] $n + for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring(2*$i, 2), 16) } + return $b +} +function HexR($b, $o, $l) { if ($o + $l -gt $b.Length) { $l = $b.Length - $o }; return (($b[$o..($o+$l-1)] | ForEach-Object { $_.ToString("x2") }) -join " ") } +$lines = [IO.File]::ReadAllLines("$env:TEMP\opencode\set123.tsv") +$sets = New-Object System.Collections.ArrayList +$resps = New-Object System.Collections.ArrayList +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + $b = H2B $p[4]; if ($b.Length -lt 0x40) { continue } + $t = [double]$p[0] + # 主机→设备 事件帧, CC=0x30 (SET) @0x4F + if ($p[1] -eq "192.168.123.113" -and $b.Length -eq 1084 -and [int]$b[4] -eq 4 -and $b[0x20] -eq 2 -and $b[0x21] -eq 0 -and $b.Length -gt 0x52 -and $b[0x4F] -eq 0x30) { + [void]$sets.Add(@{ t=$t; b=$b }) + } + # 设备→主机 应答, CC=0x31 (SET_RESPONSE) @0x2A... 应答帧 RDM 区在 0x2A 起, CC@0x2A, msgLen@0x2B, ... CC字段在 0x2A + if ($p[1] -eq "192.168.123.1" -and $b.Length -lt 300 -and $b.Length -gt 0x30 -and $b[0x2A] -eq 0xCC -and $b[0x3A] -eq 0x31) { + [void]$resps.Add(@{ t=$t; b=$b }) + } +} +Write-Host ("SET 请求 (CC=0x30): {0} SET 应答 (CC=0x31): {1}" -f $sets.Count, $resps.Count) +Write-Host "`n=== SET 请求逐帧解析 ===" +foreach ($s in $sets) { + $b = $s.b + $flag = $b[0x30] + $rdmlen = [int]$b[0x34] + [int]$b[0x35]*256 + $uid = (HexR $b 0x3E 6) -replace " ","" + $tn = $b[0x4A] + $cc = $b[0x4F] + $pidH = "{0:x2}{1:x2}" -f $b[0x50], $b[0x51] + $pdl = [int]$b[0x52] + $pd = HexR $b 0x53 ([Math]::Min($pdl, 24)) + Write-Host ("t={0,8:F3} flag={1:x2} rdmlen={2} UID={3} TN={4:x2} CC={5:x2} PID={6} PDL={7} PD=[{8}]" -f $s.t, $flag, $rdmlen, $uid, $tn, $cc, $pidH, $pdl, $pd) +} +Write-Host "`n=== SET 应答逐帧解析 (CC=0x31) ===" +foreach ($r in $resps) { + $b = $r.b + $ml = [int]$b[0x2B] + $uid = (HexR $b 0x33 6) -replace " ","" + $tn = $b[0x39] + $rt = $b[0x3A] + $pidH = "{0:x2}{1:x2}" -f $b[0x3F], $b[0x40] + $pdl = [int]$b[0x41] + $pd = "" + if ($pdl -gt 0 -and $b.Length -gt 0x42) { $pd = HexR $b 0x42 ([Math]::Min($pdl, 16)) } + # checksum 校验 + $sum = 0; for ($i = 0x2A; $i -lt 0x2A + $ml; $i++) { $sum += $b[$i] } + $cs = ([int]$b[0x2A+$ml] -shl 8) -bor [int]$b[0x2A+$ml+1] + $ok = if (($sum -band 0xFFFF) -eq $cs) { "OK" } else { "BAD" } + Write-Host ("t={0,8:F3} len={1} UID={2} TN={3:x2} RT={4:x2} PID={5} PDL={6} PD=[{7}] cs={8}" -f $r.t, $b.Length, $uid, $tn, $rt, $pidH, $pdl, $pd, $ok) +} diff --git a/scripts/session/analyze_setb.ps1 b/scripts/session/analyze_setb.ps1 new file mode 100644 index 0000000..c6cdd0e --- /dev/null +++ b/scripts/session/analyze_setb.ps1 @@ -0,0 +1,57 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +function H2B($hex) { + if (-not $hex) { return $null } + $n = $hex.Length / 2 + $b = New-Object byte[] $n + for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring(2*$i, 2), 16) } + return $b +} +function HexR($b, $o, $l) { if ($o + $l -gt $b.Length) { $l = $b.Length - $o }; return (($b[$o..($o+$l-1)] | ForEach-Object { $_.ToString("x2") }) -join " ") } +$lines = [IO.File]::ReadAllLines("$env:TEMP\opencode\set123b.tsv") +$sets = New-Object System.Collections.ArrayList +$resps = New-Object System.Collections.ArrayList +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + $b = H2B $p[4]; if ($b.Length -lt 0x40) { continue } + $t = [double]$p[0] + # 主机→设备 事件帧, CC=0x30 (SET) @0x4F + if ($p[1] -eq "192.168.123.113" -and $b.Length -eq 1084 -and [int]$b[4] -eq 4 -and $b[0x20] -eq 2 -and $b[0x21] -eq 0 -and $b.Length -gt 0x52 -and $b[0x4F] -eq 0x30) { + [void]$sets.Add(@{ t=$t; b=$b }) + } + # 设备→主机 应答, CC=0x31 (SET_RESPONSE) @0x2A... 应答帧 RDM 区在 0x2A 起, CC@0x2A, msgLen@0x2B, ... CC字段在 0x2A + if ($p[1] -eq "192.168.123.1" -and $b.Length -lt 300 -and $b.Length -gt 0x30 -and $b[0x2A] -eq 0xCC -and $b[0x3A] -eq 0x31) { + [void]$resps.Add(@{ t=$t; b=$b }) + } +} +Write-Host ("SET 请求 (CC=0x30): {0} SET 应答 (CC=0x31): {1}" -f $sets.Count, $resps.Count) +Write-Host "`n=== SET 请求逐帧解析 ===" +foreach ($s in $sets) { + $b = $s.b + $flag = $b[0x30] + $rdmlen = [int]$b[0x34] + [int]$b[0x35]*256 + $uid = (HexR $b 0x3E 6) -replace " ","" + $tn = $b[0x4A] + $cc = $b[0x4F] + $pidH = "{0:x2}{1:x2}" -f $b[0x50], $b[0x51] + $pdl = [int]$b[0x52] + $pd = HexR $b 0x53 ([Math]::Min($pdl, 24)) + Write-Host ("t={0,8:F3} flag={1:x2} rdmlen={2} UID={3} TN={4:x2} CC={5:x2} PID={6} PDL={7} PD=[{8}]" -f $s.t, $flag, $rdmlen, $uid, $tn, $cc, $pidH, $pdl, $pd) +} +Write-Host "`n=== SET 应答逐帧解析 (CC=0x31) ===" +foreach ($r in $resps) { + $b = $r.b + $ml = [int]$b[0x2B] + $uid = (HexR $b 0x33 6) -replace " ","" + $tn = $b[0x39] + $rt = $b[0x3A] + $pidH = "{0:x2}{1:x2}" -f $b[0x3F], $b[0x40] + $pdl = [int]$b[0x41] + $pd = "" + if ($pdl -gt 0 -and $b.Length -gt 0x42) { $pd = HexR $b 0x42 ([Math]::Min($pdl, 16)) } + # checksum 校验 + $sum = 0; for ($i = 0x2A; $i -lt 0x2A + $ml; $i++) { $sum += $b[$i] } + $cs = ([int]$b[0x2A+$ml] -shl 8) -bor [int]$b[0x2A+$ml+1] + $ok = if (($sum -band 0xFFFF) -eq $cs) { "OK" } else { "BAD" } + Write-Host ("t={0,8:F3} len={1} UID={2} TN={3:x2} RT={4:x2} PID={5} PDL={6} PD=[{7}] cs={8}" -f $r.t, $b.Length, $uid, $tn, $rt, $pidH, $pdl, $pd, $ok) +} diff --git a/scripts/session/analyze_ss.ps1 b/scripts/session/analyze_ss.ps1 new file mode 100644 index 0000000..4b262fd --- /dev/null +++ b/scripts/session/analyze_ss.ps1 @@ -0,0 +1,157 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" + +function H2B($hex) { + if (-not $hex) { return $null } + $n = $hex.Length / 2 + $b = New-Object byte[] $n + for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring(2*$i, 2), 16) } + return $b +} +function UidStr($b, $o) { if ($b.Length -lt $o+6) { return "?" }; return ($b[$o..($o+5)] | ForEach-Object { $_.ToString("x2") }) -join "" } + +function Analyze($name, $tsv, $devIp, $hostIp) { + $lines = [IO.File]::ReadAllLines($tsv) + $t0 = 1e9; $t1 = 0 + foreach ($l in $lines) { $p = $l -split "`t"; if ($p.Count -lt 1 -or -not $p[0]) { continue }; $t = [double]$p[0]; if ($t -lt $t0) { $t0 = $t }; if ($t -gt $t1) { $t1 = $t } } + $dur = $t1 - $t0 + + $h2d = @{} # host->dev: 帧型 + $d2h = @{} # dev->host: 帧型 + $rdmCmd = @{} # host->dev RDM cmd 直方图 (flag=00) + $rdmPid = @{} + $queries = New-Object System.Collections.ArrayList # RDM 查询 (flag=00, cmd 18/19/1a/24) + $answers = New-Object System.Collections.ArrayList # dev->host RDM 短帧 + $dmxA = 0; $evtB = 0; $ack42 = 0 + $devUnsolicited = New-Object System.Collections.ArrayList # dev->host 42B 帧时间 + + foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + $b = H2B $p[4]; if ($b.Length -lt 0x1A) { continue } + $t = [double]$p[0] + $typ = [int]$b[4] + $isH2D = ($p[1] -eq $hostIp -and $p[2] -eq $devIp) + $isD2H = ($p[1] -eq $devIp -and $p[2] -eq $hostIp) + if (-not $isH2D -and -not $isD2H) { continue } + $len = $b.Length + if ($isH2D) { + $k = "t$typ|$len" + if (-not $h2d.ContainsKey($k)) { $h2d[$k] = 0 }; $h2d[$k]++ + if ($typ -eq 4 -and $len -gt 1000) { + if ($b[0x20] -eq 1 -and $b[0x21] -eq 1) { $dmxA++ } else { $evtB++ } + } + if ($typ -eq 4 -and $len -lt 2000 -and $len -gt 0x44 -and $b[0x30] -eq 0) { + $cmd = $b[0x3D] + if ($cmd -eq 0x24 -or $cmd -eq 0x18 -or $cmd -eq 0x19 -or $cmd -eq 0x1a) { + $ck = "cmd={0:x2}" -f $cmd + if (-not $rdmCmd.ContainsKey($ck)) { $rdmCmd[$ck] = 0 }; $rdmCmd[$ck]++ + $pidKey = "" + if ($cmd -ne 0x24 -and $b.Length -gt 0x51) { $pidKey = "{0:x2}{1:x2}" -f $b[0x50], $b[0x51] + if (-not $rdmPid.ContainsKey($pidKey)) { $rdmPid[$pidKey] = 0 }; $rdmPid[$pidKey]++ } + [void]$queries.Add(@{ t=$t; cmd=$cmd; pid=$pidKey; uid=(UidStr $b 0x3E); seq=([int]$b[0x22] + [int]$b[0x23]*256); tn=$b[0x4A]; len=$len }) + } + } + } else { + $k = "t$typ|$len" + if (-not $d2h.ContainsKey($k)) { $d2h[$k] = 0 }; $d2h[$k]++ + if ($typ -eq 4 -and $len -eq 42) { $ack42++; [void]$devUnsolicited.Add($t) } + if ($typ -eq 4 -and $len -lt 200 -and $len -gt 0x2C -and ($b[0x2A] -eq 0xCC -or $b[0x2A] -eq 0xAA)) { + [void]$answers.Add(@{ t=$t; b=$b; len=$len; seq=([int]$b[0x20] + [int]$b[0x21]*256); pulse=($b[0x2A] -eq 0xAA) }) + } + } + } + + Write-Host "`n############ $name ($devIp) 时长 $([int]$dur)s ############" + Write-Host "--- host→dev 帧型 (Top12) ---" + $h2d.GetEnumerator() | Sort-Object { [int]$_.Value } -Descending | Select-Object -First 12 | ForEach-Object { Write-Host (" {0} : {1} ({2:F1}/s)" -f $_.Key, $_.Value, ($_.Value / $dur)) } + Write-Host "--- dev→host 帧型 (Top12) ---" + $d2h.GetEnumerator() | Sort-Object { [int]$_.Value } -Descending | Select-Object -First 12 | ForEach-Object { Write-Host (" {0} : {1} ({2:F1}/s)" -f $_.Key, $_.Value, ($_.Value / $dur)) } + Write-Host ("--- DMX-A={0} ({1:F1}/s) 事件B={2} ({3:F1}/s) 42B回帧={4} ({5:F1}/s) ACK/事件B比={6:F2}" -f $dmxA, ($dmxA/$dur), $evtB, ($evtB/$dur), $ack42, ($ack42/$dur), $(if ($evtB -gt 0) { $ack42/$evtB } else { 0 })) + Write-Host "--- RDM 查询 cmd (flag=00) ---" + $rdmCmd.GetEnumerator() | Sort-Object { [int]$_.Value } -Descending | ForEach-Object { Write-Host (" {0} : {1}" -f $_.Key, $_.Value) } + Write-Host "--- RDM 查询 PID (cmd!=24) ---" + $rdmPid.GetEnumerator() | Sort-Object { [int]$_.Value } -Descending | Select-Object -First 15 | ForEach-Object { Write-Host (" PID={0} : {1}" -f $_.Key, $_.Value) } + + # 应答时延: cmd=18/19/1a 查询 → 200ms 内 seq 回显应答 + $lat = New-Object System.Collections.ArrayList + $paired = 0; $unpaired = 0 + foreach ($q in $queries) { + if ($q.cmd -eq 0x24) { continue } + $best = $null + foreach ($a in $answers) { + if ($a.pulse) { continue } + if ($a.t -lt $q.t -or $a.t -gt $q.t + 0.2) { continue } + if ($a.seq -eq $q.seq) { $best = $a; break } + } + if ($best) { $paired++; [void]$lat.Add([int](($best.t - $q.t) * 1000)) } else { $unpaired++ } + } + if ($lat.Count -gt 0) { + $ls = $lat | Sort-Object + Write-Host ("--- 参数应答时延: 配对={0} 未配对={1} min={2}ms 中位={3}ms max={4}ms" -f $paired, $unpaired, $ls[0], $ls[[int]($ls.Count/2)], $ls[-1]) + } else { + Write-Host ("--- 参数应答时延: 配对={0} 未配对={1} (无样本)" -f $paired, $unpaired) + } + # 脉冲时延 + $plat = New-Object System.Collections.ArrayList + $discQs = @($queries | Where-Object { $_.cmd -eq 0x24 }) + $pulses = @($answers | Where-Object { $_.pulse }) + foreach ($pu in $pulses) { + $best = $null + for ($i = $discQs.Count - 1; $i -ge 0; $i--) { if ($discQs[$i].t -le $pu.t) { $best = $discQs[$i]; break } } + if ($best -and ($pu.t - $best.t) -lt 0.5) { [void]$plat.Add([int](($pu.t - $best.t) * 1000)) } + } + if ($plat.Count -gt 0) { + $ps = $plat | Sort-Object + Write-Host ("--- 脉冲: {0} 个 (应答率 {1:F0}% of cmd24) 时延 min={2}ms 中位={3}ms max={4}ms" -f $pulses.Count, $(if ($discQs.Count -gt 0) { 100.0*$pulses.Count/$discQs.Count } else { 0 }), $ps[0], $ps[[int]($ps.Count/2)], $ps[-1]) + } + return @{ queries=$queries; answers=$answers } +} + +$r1 = Analyze "真机 .1" "$env:TEMP\opencode\ss123.tsv" "192.168.123.1" "192.168.123.113" +$r7 = Analyze "虚拟 .7" "$env:TEMP\opencode\ss58.tsv" "192.168.58.7" "192.168.58.107" + +# ============ cmd=19/1a 深挖: 布局解析 (用 .1 的样本) ============ +Write-Host "`n############ cmd=19/1a 布局深挖 (.1 样本) ############" +$cnt19 = 0; $cnt1a = 0 +foreach ($q in $r1.queries) { + if ($q.cmd -eq 0x19 -and $cnt19 -lt 2) { + Write-Host (" cmd=19 t={0:F3} UID={1} PID={2} TN={3:x2} len={4}" -f $q.t, $q.uid, $q.pid, $q.tn, $q.len) + $cnt19++ + } + if ($q.cmd -eq 0x1a -and $cnt1a -lt 2) { + Write-Host (" cmd=1a t={0:F3} UID={1} PID={2} TN={3:x2} len={4}" -f $q.t, $q.uid, $q.pid, $q.tn, $q.len) + $cnt1a++ + } +} +# cmd=19/1a 的 PID 分布 +$p19 = @{}; $p1a = @{} +foreach ($q in $r1.queries) { + if ($q.cmd -eq 0x19) { if (-not $p19.ContainsKey($q.pid)) { $p19[$q.pid] = 0 }; $p19[$q.pid]++ } + if ($q.cmd -eq 0x1a) { if (-not $p1a.ContainsKey($q.pid)) { $p1a[$q.pid] = 0 }; $p1a[$q.pid]++ } +} +Write-Host "--- cmd=19 PID 分布 (.1) ---" +$p19.GetEnumerator() | Sort-Object { [int]$_.Value } -Descending | Select-Object -First 10 | ForEach-Object { Write-Host (" PID={0} : {1}" -f $_.Key, $_.Value) } +Write-Host "--- cmd=1a PID 分布 (.1) ---" +$p1a.GetEnumerator() | Sort-Object { [int]$_.Value } -Descending | Select-Object -First 10 | ForEach-Object { Write-Host (" PID={0} : {1}" -f $_.Key, $_.Value) } + +# cmd=19 查询的完整 hex 样本 (从原始 tsv 找一条) +Write-Host "--- cmd=19 查询完整 hex (1 条) ---" +$lines = [IO.File]::ReadAllLines("$env:TEMP\opencode\ss123.tsv") +$done = 0 +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + if ($p[1] -ne "192.168.123.113" -or $p[2] -ne "192.168.123.1") { continue } + $b = H2B $p[4]; if ($b.Length -lt 0x60 -or $b.Length -gt 2000) { continue } + if ($b[0x30] -ne 0 -or $b[0x3D] -ne 0x19) { continue } + Write-Host (" len={0} hex[0x20..0x60]={1}" -f $b.Length, (($b[0x20..([Math]::Min($b.Length-1, 0x60))] | ForEach-Object { $_.ToString("x2") }) -join "")) + # 找其应答 + $seq = [int]$b[0x22] + [int]$b[0x23]*256 + foreach ($a in $r1.answers) { + if (-not $a.pulse -and $a.seq -eq $seq -and $a.t -ge [double]$p[0] -and $a.t -le [double]$p[0] + 0.2) { + Write-Host (" 应答 len={0}: {1}" -f $a.len, (($a.b | ForEach-Object { $_.ToString("x2") }) -join "")) + break + } + } + $done++; if ($done -ge 2) { break } +} +Write-Host "`n分析完成。" diff --git a/scripts/session/analyze_ss2.ps1 b/scripts/session/analyze_ss2.ps1 new file mode 100644 index 0000000..87bc5fe --- /dev/null +++ b/scripts/session/analyze_ss2.ps1 @@ -0,0 +1,81 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" + +function H2B($hex) { + if (-not $hex) { return $null } + $n = $hex.Length / 2 + $b = New-Object byte[] $n + for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring(2*$i, 2), 16) } + return $b +} +function UidStr($b, $o) { if ($b.Length -lt $o+6) { return "?" }; return ($b[$o..($o+5)] | ForEach-Object { $_.ToString("x2") }) -join "" } + +function Analyze2($name, $tsv, $devIp, $hostIp) { + $lines = [IO.File]::ReadAllLines($tsv) + Write-Host "`n############ $name ($devIp) ############" + # 真实 RDM 查询: 0x20-0x21 == 0200 (事件B帧), flag 0x30==0 + $rdmCmd = @{}; $rdmPid = @{}; $rdmPidUid = @{} + $falsePos = 0 + $bFrames = New-Object System.Collections.ArrayList # 所有事件B帧 (时间, 0x22序号, 0x3C, 0x3D) + foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + if ($p[1] -ne $hostIp -or $p[2] -ne $devIp) { continue } + $b = H2B $p[4]; if ($b.Length -lt 0x52 -or $b.Length -gt 2000) { continue } + if ([int]$b[4] -ne 4) { continue } + $t = [double]$p[0] + $isB = ($b[0x20] -eq 2 -and $b[0x21] -eq 0) + $isA = ($b[0x20] -eq 1 -and $b[0x21] -eq 1) + if ($isB -and $b.Length -gt 0x44) { + [void]$bFrames.Add(@{ t=$t; seq=([int]$b[0x22]+[int]$b[0x23]*256); c3c=$b[0x3C]; c3d=$b[0x3D]; flag=$b[0x30] }) + } + # RDM 判定: 必须 0x3C==01 且 cmd 匹配 + if ($b.Length -gt 0x44 -and $b[0x3C] -eq 1 -and ($b[0x3D] -eq 0x24 -or $b[0x3D] -eq 0x18 -or $b[0x3D] -eq 0x19 -or $b[0x3D] -eq 0x1a)) { + if ($b[0x30] -ne 0) { continue } + if ($isA) { $falsePos++; continue } # DMX-A 帧通道数据巧合 = 假阳性 + $cmd = $b[0x3D] + $ck = "cmd={0:x2}" -f $cmd + if (-not $rdmCmd.ContainsKey($ck)) { $rdmCmd[$ck] = 0 }; $rdmCmd[$ck]++ + if ($cmd -ne 0x24 -and $b.Length -gt 0x51) { + $pidKey = "{0:x2}{1:x2}" -f $b[0x50], $b[0x51] + $uid = UidStr $b 0x3E + if (-not $rdmPid.ContainsKey($pidKey)) { $rdmPid[$pidKey] = 0 }; $rdmPid[$pidKey]++ + $k2 = "$pidKey->$uid"; if (-not $rdmPidUid.ContainsKey($k2)) { $rdmPidUid[$k2] = 0 }; $rdmPidUid[$k2]++ + } + } + } + Write-Host ("--- 真实 RDM 查询 (排除 DMX-A 假阳性 {0} 条) ---" -f $falsePos) + $rdmCmd.GetEnumerator() | Sort-Object { [int]$_.Value } -Descending | ForEach-Object { Write-Host (" {0} : {1}" -f $_.Key, $_.Value) } + Write-Host "--- PID 分布 ---" + $rdmPid.GetEnumerator() | Sort-Object { [int]$_.Value } -Descending | Select-Object -First 12 | ForEach-Object { Write-Host (" PID={0} : {1}" -f $_.Key, $_.Value) } + Write-Host "--- PID->UID 细分 (0002/0003/0060/00f0/0201) ---" + $rdmPidUid.GetEnumerator() | Where-Object { $_.Name -match "^(0002|0003|0060|00f0|0201)" } | Sort-Object Name | ForEach-Object { Write-Host (" {0} : {1}" -f $_.Name, $_.Value) } + + # 42B ACK 相关性: 每个 42B 帧之前 200ms 内最近的 B 帧特征 + $acks = New-Object System.Collections.ArrayList + foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + if ($p[1] -ne $devIp -or $p[2] -ne $hostIp) { continue } + $b = H2B $p[4]; if ($b.Length -ne 42 -or [int]$b[4] -ne 4) { continue } + [void]$acks.Add(@{ t=([double]$p[0]); seq=([int]$b[0x20]+[int]$b[0x21]*256) }) + } + $ackByB = @{} + $ackNoB = 0 + foreach ($a in $acks) { + # 找 seq == ack.seq+1 的 B 帧 (§4.3: ACK seq = 事件 seq - 1) + $found = $null + foreach ($bf in $bFrames) { + if ($bf.seq -eq $a.seq + 1) { $found = $bf; break } + } + if ($found) { + $k = "0x3C={0:x2} 0x3D={1:x2} flag={2:x2}" -f $found.c3c, $found.c3d, $found.flag + if (-not $ackByB.ContainsKey($k)) { $ackByB[$k] = 0 }; $ackByB[$k]++ + } else { $ackNoB++ } + } + Write-Host ("--- 42B ACK ({0}个) 对应的 B 帧特征 ---" -f $acks.Count) + $ackByB.GetEnumerator() | Sort-Object { [int]$_.Value } -Descending | Select-Object -First 8 | ForEach-Object { Write-Host (" {0} : {1}" -f $_.Key, $_.Value) } + Write-Host (" (无配对 B 帧: {0})" -f $ackNoB) +} + +Analyze2 "真机 .1" "$env:TEMP\opencode\ss123.tsv" "192.168.123.1" "192.168.123.113" +Analyze2 "虚拟 .7" "$env:TEMP\opencode\ss58.tsv" "192.168.58.7" "192.168.58.107" +Write-Host "`n分析完成。" diff --git a/scripts/session/analyze_startup.ps1 b/scripts/session/analyze_startup.ps1 new file mode 100644 index 0000000..2ec6606 --- /dev/null +++ b/scripts/session/analyze_startup.ps1 @@ -0,0 +1,14 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$cap = "$env:TEMP\opencode\sym_startup.pcap" +$ts = "C:\Program Files\Wireshark\tshark.exe" +Write-Host "=== claim frames (ident 110 / 47B 55 / AECH 560) ===" +& $ts -r $cap -Y "udp.length==110 or udp.length==55 or udp.length==560" -T fields -e frame.time_relative -e ip.src -e ip.dst -e udp.length 2>$null | Select-Object -First 12 +Write-Host "" +Write-Host "=== host-sent frame types (all, until +30s after claim window end) ===" +$all = @(& $ts -r $cap -Y "ip.src==192.168.123.113" -T fields -e frame.time_relative -e ip.dst -e udp.length 2>$null) +$g = @{} +foreach ($l in $all) { $p = $l -split "`t"; $k = $p[1] + " len=" + $p[2]; if (-not $g.ContainsKey($k)) { $g[$k] = New-Object System.Collections.ArrayList }; [void]$g[$k].Add([double]$p[0]) } +foreach ($k in ($g.Keys | Sort-Object)) { + $arr = $g[$k] + Write-Host ("{0,-30} n={1,-6} first={2:N1}s last={3:N1}s" -f $k, $arr.Count, $arr[0], $arr[$arr.Count-1]) +} diff --git a/scripts/session/cap_hostdemo.ps1 b/scripts/session/cap_hostdemo.ps1 new file mode 100644 index 0000000..04f1984 --- /dev/null +++ b/scripts/session/cap_hostdemo.ps1 @@ -0,0 +1,11 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$cap = "$env:TEMP\opencode\hd1.pcap" +Remove-Item $cap -ErrorAction SilentlyContinue +$ts = "C:\Program Files\Wireshark\tshark.exe" +$proc = Start-Process -FilePath $ts -ArgumentList "-i `"\Device\NPF_{E7165B1C-BAF6-49CF-9A3D-EF12DBC17329}`" -f `"udp port 31772`" -a duration:40 -w `"$cap`"" -PassThru -WindowStyle Hidden -RedirectStandardError "$env:TEMP\opencode\tshark_err.txt" +Start-Sleep -Seconds 3 +Set-Location "C:\Users\tonycao\work\light\enet-validator" +& .\target\release\enet-validator.exe hostdemo 192.168.123.1 0 +Start-Sleep -Seconds 30 +Write-Host "`n=== 抓包全部帧 (time src dst udplen) ===" +& $ts -r $cap -T fields -e frame.time_relative -e ip.src -e ip.dst -e udp.length 2>$null | Select-Object -First 60 diff --git a/scripts/session/cat_runbutler.ps1 b/scripts/session/cat_runbutler.ps1 new file mode 100644 index 0000000..bb08756 --- /dev/null +++ b/scripts/session/cat_runbutler.ps1 @@ -0,0 +1,9 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +$r = Invoke-SSHCommand -SSHSession $s -Command "cat ~/run_butler.sh" +Write-Host ($r.Output -join "`n") +Remove-SSHSession $s | Out-Null diff --git a/scripts/session/check_desc.ps1 b/scripts/session/check_desc.ps1 new file mode 100644 index 0000000..dcf46ef --- /dev/null +++ b/scripts/session/check_desc.ps1 @@ -0,0 +1,25 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$lines = [IO.File]::ReadAllLines("$env:TEMP\opencode\ss123.tsv") +$checked = 0; $allDesc = 0 +foreach ($ln in $lines) { + $p = $ln -split "`t" + if ($p.Count -lt 5 -or $p[4].Length -lt 100) { continue } + $hex = $p[4] -replace '[^0-9a-fA-F]','' + if ($hex.Length -ne 2168) { continue } + $b = New-Object byte[] 1084 + for ($i = 0; $i -lt 1084; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring($i*2,2),16) } + if ($b[0x20] -ne 1 -or $b[0x21] -ne 1) { continue } + $checked++ + if ($checked -gt 200) { break } + $ok = $true + foreach ($base in @(0x3C, 0x23C)) { + for ($i = 0; $i -lt 511; $i++) { + $d = ($b[$base+$i] - $b[$base+$i+1]) -band 0xFF + if ($d -ne 1) { $ok = $false; break } + } + if (-not $ok) { break } + } + if ($ok) { $allDesc++ } +} +echo "检查帧: $checked, 全 512B 严格递减(mod256): $allDesc" +# 若成立 → 数据=加密后的全零, rot 可从首字节推出 diff --git a/scripts/session/check_fix1_aech.ps1 b/scripts/session/check_fix1_aech.ps1 new file mode 100644 index 0000000..0477ab7 --- /dev/null +++ b/scripts/session/check_fix1_aech.ps1 @@ -0,0 +1,12 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$raw = [IO.File]::ReadAllBytes("$env:TEMP\opencode\fix1.tsv") +$text = if ($raw[0] -eq 0xFF -and $raw[1] -eq 0xFE) { [Text.Encoding]::Unicode.GetString($raw) } else { [Text.Encoding]::UTF8.GetString($raw) } +$lines = $text -split "`r?`n" +foreach ($l in $lines) { + $p = $l -split "`t" + if ($p.Count -lt 5 -or -not $p[4]) { continue } + $h = $p[4] -replace " ", "" + if ($h.Length -lt 100) { continue } + if ($h.Substring(0x28 * 2, 8) -eq "41454348") { Write-Host ("AECH t={0,9} src={1} len={2}" -f $p[0], $p[1], ($h.Length/2)) } + if ($h.Substring(0x28 * 2, 8) -eq "41455245") { Write-Host ("AERE t={0,9} src={1} len={2} c0={3}" -f $p[0], $p[1], ($h.Length/2), $h.Substring(0xC0*2,8)) } +} diff --git a/scripts/session/check_log.ps1 b/scripts/session/check_log.ps1 new file mode 100644 index 0000000..9bc9558 --- /dev/null +++ b/scripts/session/check_log.ps1 @@ -0,0 +1,9 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +$r = Invoke-SSHCommand -SSHSession $s -Command "tail -15 ~/butler.log" +Write-Host ($r.Output -join "`n") +Remove-SSHSession $s | Out-Null diff --git a/scripts/session/check_log2.ps1 b/scripts/session/check_log2.ps1 new file mode 100644 index 0000000..4c2faab --- /dev/null +++ b/scripts/session/check_log2.ps1 @@ -0,0 +1,9 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +$r = Invoke-SSHCommand -SSHSession $s -Command "grep -E 'AECH|注册|RDM|心跳' ~/butler.log | tail -20" +Write-Host ($r.Output -join "`n") +Remove-SSHSession $s | Out-Null diff --git a/scripts/session/check_log3.ps1 b/scripts/session/check_log3.ps1 new file mode 100644 index 0000000..0137fde --- /dev/null +++ b/scripts/session/check_log3.ps1 @@ -0,0 +1,9 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +$r = Invoke-SSHCommand -SSHSession $s -Command "pgrep -af 'enet-validator device' ; echo '---'; tail -8 ~/butler.log ; echo '---'; grep -c 'RDM-REQ' ~/butler.log" +Write-Host ($r.Output -join "`n") +Remove-SSHSession $s | Out-Null diff --git a/scripts/session/check_log4.ps1 b/scripts/session/check_log4.ps1 new file mode 100644 index 0000000..d712ddb --- /dev/null +++ b/scripts/session/check_log4.ps1 @@ -0,0 +1,9 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +$r = Invoke-SSHCommand -SSHSession $s -Command "wc -l ~/butler.log; echo '---'; grep -vE '发现请求 type1' ~/butler.log | tail -30" +Write-Host ($r.Output -join "`n") +Remove-SSHSession $s | Out-Null diff --git a/scripts/session/check_log5.ps1 b/scripts/session/check_log5.ps1 new file mode 100644 index 0000000..a825c24 --- /dev/null +++ b/scripts/session/check_log5.ps1 @@ -0,0 +1,9 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +$r = Invoke-SSHCommand -SSHSession $s -Command "wc -l ~/butler.log; echo '---'; grep -vE 'DMX #' ~/butler.log | tail -25" +Write-Host ($r.Output -join "`n") +Remove-SSHSession $s | Out-Null diff --git a/scripts/session/check_proc.ps1 b/scripts/session/check_proc.ps1 new file mode 100644 index 0000000..571dcd5 --- /dev/null +++ b/scripts/session/check_proc.ps1 @@ -0,0 +1,11 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +if (-not $s) { throw "SSH connect failed" } +$r = Invoke-SSHCommand -SSHSession $s -Command "pgrep -af enet; echo ---; ls -la ~/butler.log; echo ---; tail -n 3 ~/butler.log; echo ---; ss -lunp 2>/dev/null | grep 31772" +$r.Output +Remove-SSHSession -SSHSession $s | Out-Null diff --git a/scripts/session/check_proc2.ps1 b/scripts/session/check_proc2.ps1 new file mode 100644 index 0000000..e5e6b10 --- /dev/null +++ b/scripts/session/check_proc2.ps1 @@ -0,0 +1,9 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +$r = Invoke-SSHCommand -SSHSession $s -Command "pgrep -af 'enet-validator device'; echo '---'; head -5 ~/butler.log" +Write-Host ($r.Output -join "`n") +Remove-SSHSession $s | Out-Null diff --git a/scripts/session/check_procs.ps1 b/scripts/session/check_procs.ps1 new file mode 100644 index 0000000..5c3020f --- /dev/null +++ b/scripts/session/check_procs.ps1 @@ -0,0 +1,9 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +$r = Invoke-SSHCommand -SSHSession $s -Command "pgrep -af enet-validator; echo '---'; ss -unp | grep 31772" +Write-Host ($r.Output -join "`n") +Remove-SSHSession $s | Out-Null diff --git a/scripts/session/check_set.ps1 b/scripts/session/check_set.ps1 new file mode 100644 index 0000000..9faf0e3 --- /dev/null +++ b/scripts/session/check_set.ps1 @@ -0,0 +1,9 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +$r = Invoke-SSHCommand -SSHSession $s -Command "grep -E '\[RDM\] SET' ~/butler.log | tail -10; echo '---'; grep -E 'PID=00f0' ~/butler.log | tail -5" +Write-Host ($r.Output -join "`n") +Remove-SSHSession $s | Out-Null diff --git a/scripts/session/check_status.ps1 b/scripts/session/check_status.ps1 new file mode 100644 index 0000000..1a070b3 --- /dev/null +++ b/scripts/session/check_status.ps1 @@ -0,0 +1,11 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +if (-not $s) { throw "SSH connect failed" } +$r = Invoke-SSHCommand -SSHSession $s -Command "pgrep -af enet-validator; echo ---; tail -n 8 ~/butler.log" +$r.Output +Remove-SSHSession -SSHSession $s | Out-Null diff --git a/scripts/session/check_ubuntu_git.ps1 b/scripts/session/check_ubuntu_git.ps1 new file mode 100644 index 0000000..3ba6809 --- /dev/null +++ b/scripts/session/check_ubuntu_git.ps1 @@ -0,0 +1,10 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +if (-not $s) { throw "SSH connect failed" } +$r = Invoke-SSHCommand -SessionId $s.SessionId -Command "which git && git --version && ls ~/enet-validator 2>/dev/null | head -5" +$r.Output +Remove-SSHSession -SessionId $s.SessionId | Out-Null diff --git a/scripts/session/cold2_order.ps1 b/scripts/session/cold2_order.ps1 new file mode 100644 index 0000000..960c4dc --- /dev/null +++ b/scripts/session/cold2_order.ps1 @@ -0,0 +1,24 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$f = "$env:TEMP\opencode\cold2.tsv" +$lines = [IO.File]::ReadAllLines($f) +function H2B($hex) { $n = $hex.Length / 2; $b = New-Object byte[] $n; for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring(2*$i, 2), 16) }; return $b } + +$firstAECH = $null; $firstDMX = $null; $first42 = $null; $firstRDMreq = $null; $firstRDMresp = $null; $firstTele = $null +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5) { continue } + $b = H2B $p[4]; $t = [double]$p[0] + $from113 = $p[1] -eq "192.168.123.113" + $from1 = $p[1] -eq "192.168.123.1" + if ($from113 -and $b.Length -eq 552 -and -not $firstAECH) { $firstAECH = $t; Write-Host ("first AECH challenge t=$t") } + if ($from113 -and $b.Length -eq 1084) { + $isRDM = ($b[0x3C] -eq 1 -and $b[0x30] -eq 0) + $isDMX = ($b[0x30] -eq 0 -and $b[0x2A] -eq 0) # 0x2A=00 => DMX data frame? from earlier: type4 flag=00 0x2A=00 + if ($isRDM -and -not $firstRDMreq) { $firstRDMreq = $t; Write-Host ("first RDM req(flag00) t=$t") } + if (-not $isRDM -and -not $firstDMX) { $firstDMX = $t; Write-Host ("first DMX frame t=$t (0x2A=$($b[0x2A].ToString('x2')) flag=$($b[0x30].ToString('x2')))") } + } + if ($from1 -and $b.Length -eq 42 -and -not $first42) { $first42 = $t; Write-Host ("first 42B telemetry t=$t") } + if ($from1 -and $b.Length -ge 0x2E -and $b.Length -lt 200 -and ($b[0x2A] -eq 0xCC -or $b[0x2A] -eq 0xAA) -and -not $firstRDMresp) { $firstRDMresp = $t; Write-Host ("first RDM response t=$t len=$($b.Length)") } + if ($firstAECH -and $firstDMX -and $first42 -and $firstRDMreq -and $firstRDMresp) { break } +} +Write-Host "" +Write-Host ("order: AECH=$firstAECH RDMreq=$firstRDMreq DMX=$firstDMX 42B=$first42 RDMresp=$firstRDMresp") diff --git a/scripts/session/deploy_g.ps1 b/scripts/session/deploy_g.ps1 new file mode 100644 index 0000000..955a9b1 --- /dev/null +++ b/scripts/session/deploy_g.ps1 @@ -0,0 +1,38 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +if (-not $s) { throw "SSH connect failed" } + +$files = @( + @("C:\Users\tonycao\work\light\enet-validator\src\tests.rs", "~/enet-validator/src/tests.rs"), + @("C:\Users\tonycao\work\light\enet-validator\src\aech.rs", "~/enet-validator/src/aech.rs"), + @("C:\Users\tonycao\work\light\enet-validator\src\main.rs", "~/enet-validator/src/main.rs"), + @("C:\Users\tonycao\work\light\enet-validator\src\dmxhash.rs", "~/enet-validator/src/dmxhash.rs") +) +foreach ($f in $files) { + $src = $f[0]; $dst = $f[1] + $b64 = [Convert]::ToBase64String([IO.File]::ReadAllBytes($src)) + $chunkSize = 6000 + $chunks = [Math]::Ceiling($b64.Length / $chunkSize) + $r = Invoke-SSHCommand -SSHSession $s -Command "rm -f /tmp/up.b64 && touch /tmp/up.b64" + for ($i = 0; $i -lt $chunks; $i++) { + $part = $b64.Substring($i * $chunkSize, [Math]::Min($chunkSize, $b64.Length - $i * $chunkSize)) + $r = Invoke-SSHCommand -SSHSession $s -Command "echo '$part' >> /tmp/up.b64" + } + $r = Invoke-SSHCommand -SSHSession $s -Command "base64 -d /tmp/up.b64 > $dst && md5sum $dst" + $localMd5 = (Get-FileHash $src -Algorithm MD5).Hash.ToLower() + $remoteMd5 = ($r.Output -join "").Split(" ")[0] + Write-Host ("{0}: {1}" -f (Split-Path $src -Leaf), $(if ($localMd5 -eq $remoteMd5) { "OK" } else { "MISMATCH!" })) +} + +Write-Host "--- build on .7 ---" +$r = Invoke-SSHCommand -SSHSession $s -Command "cd ~/enet-validator && ~/.cargo/bin/cargo build --release 2>&1 | tail -2" -TimeOut 300 +Write-Host ($r.Output -join "`n") +Write-Host "--- restart butler (NO relay - local G) ---" +$r = Invoke-SSHCommand -SSHSession $s -Command "pkill -f 'enet-validator device'; sleep 1; cd ~/enet-validator && nohup ./target/release/enet-validator device 192.168.58.7 00161cf12e88 86400 > ~/butler.log 2>&1 & sleep 2; pgrep -af 'enet-validator device' | head -1" +Write-Host ($r.Output -join "`n") +Remove-SSHSession $s | Out-Null diff --git a/scripts/session/deploy_set.ps1 b/scripts/session/deploy_set.ps1 new file mode 100644 index 0000000..d1fe96b --- /dev/null +++ b/scripts/session/deploy_set.ps1 @@ -0,0 +1,39 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +if (-not $s) { throw "SSH 连接失败" } + +$files = @( + @("C:\Users\tonycao\work\light\enet-validator\src\tests.rs", "~/enet-validator/src/tests.rs"), + @("C:\Users\tonycao\work\light\enet-validator\src\enet.rs", "~/enet-validator/src/enet.rs"), + @("C:\Users\tonycao\work\light\enet-validator\samples\rdm_setack_68.hex", "~/enet-validator/samples/rdm_setack_68.hex") +) +foreach ($f in $files) { + $src = $f[0]; $dst = $f[1] + $b64 = [Convert]::ToBase64String([IO.File]::ReadAllBytes($src)) + $chunkSize = 6000 + $chunks = [Math]::Ceiling($b64.Length / $chunkSize) + $r = Invoke-SSHCommand -SSHSession $s -Command "rm -f /tmp/up.b64 && touch /tmp/up.b64" + for ($i = 0; $i -lt $chunks; $i++) { + $part = $b64.Substring($i * $chunkSize, [Math]::Min($chunkSize, $b64.Length - $i * $chunkSize)) + $r = Invoke-SSHCommand -SSHSession $s -Command "echo '$part' >> /tmp/up.b64" + } + $r = Invoke-SSHCommand -SSHSession $s -Command "base64 -d /tmp/up.b64 > $dst && md5sum $dst" + $localMd5 = (Get-FileHash $src -Algorithm MD5).Hash.ToLower() + $remoteMd5 = ($r.Output -join "").Split(" ")[0] + $ok = if ($localMd5 -eq $remoteMd5) { "OK" } else { "MISMATCH!" } + Write-Host ("{0} -> {1}: {2} ({3})" -f (Split-Path $src -Leaf), $dst, $ok, $localMd5) +} + +Write-Host "--- .7 编译 ---" +$r = Invoke-SSHCommand -SSHSession $s -Command "cd ~/enet-validator && ~/.cargo/bin/cargo build --release 2>&1 | tail -3" -TimeOut 300 +Write-Host ($r.Output -join "`n") + +Write-Host "--- 重启 butler ---" +$r = Invoke-SSHCommand -SSHSession $s -Command "bash ~/run_butler.sh device 2>&1 | tail -5; sleep 2; pgrep -af enet-validator | head -3" +Write-Host ($r.Output -join "`n") +Remove-SSHSession $s | Out-Null diff --git a/scripts/session/deploy_set2.ps1 b/scripts/session/deploy_set2.ps1 new file mode 100644 index 0000000..b29f1a4 --- /dev/null +++ b/scripts/session/deploy_set2.ps1 @@ -0,0 +1,31 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +if (-not $s) { throw "SSH connect failed" } + +$src = "C:\Users\tonycao\work\light\enet-validator\src\tests.rs" +$b64 = [Convert]::ToBase64String([IO.File]::ReadAllBytes($src)) +$chunkSize = 6000 +$chunks = [Math]::Ceiling($b64.Length / $chunkSize) +$r = Invoke-SSHCommand -SSHSession $s -Command "rm -f /tmp/up.b64 && touch /tmp/up.b64" +for ($i = 0; $i -lt $chunks; $i++) { + $part = $b64.Substring($i * $chunkSize, [Math]::Min($chunkSize, $b64.Length - $i * $chunkSize)) + $r = Invoke-SSHCommand -SSHSession $s -Command "echo '$part' >> /tmp/up.b64" +} +$r = Invoke-SSHCommand -SSHSession $s -Command "base64 -d /tmp/up.b64 > ~/enet-validator/src/tests.rs && md5sum ~/enet-validator/src/tests.rs" +$localMd5 = (Get-FileHash $src -Algorithm MD5).Hash.ToLower() +$remoteMd5 = ($r.Output -join "").Split(" ")[0] +Write-Host ("tests.rs sync: {0} ({1})" -f $(if ($localMd5 -eq $remoteMd5) { "OK" } else { "MISMATCH!" }), $localMd5) + +Write-Host "--- build on .7 ---" +$r = Invoke-SSHCommand -SSHSession $s -Command "cd ~/enet-validator && ~/.cargo/bin/cargo build --release 2>&1 | tail -2" -TimeOut 300 +Write-Host ($r.Output -join "`n") + +Write-Host "--- restart butler ---" +$r = Invoke-SSHCommand -SSHSession $s -Command "bash ~/run_butler.sh device 2>&1 | tail -3; sleep 2; pgrep -af 'enet-validator device' | head -2" +Write-Host ($r.Output -join "`n") +Remove-SSHSession $s | Out-Null diff --git a/scripts/session/deploy_tests.ps1 b/scripts/session/deploy_tests.ps1 new file mode 100644 index 0000000..8f2ae9c --- /dev/null +++ b/scripts/session/deploy_tests.ps1 @@ -0,0 +1,29 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +if (-not $s) { throw "SSH 连接失败" } + +$src = "C:\Users\tonycao\work\light\enet-validator\src\tests.rs" +$b64 = [Convert]::ToBase64String([IO.File]::ReadAllBytes($src)) +Write-Host ("本地文件 {0} 字节, base64 {1} 字符" -f (Get-Item $src).Length, $b64.Length) + +# 分块传输 +$chunkSize = 6000 +$chunks = [Math]::Ceiling($b64.Length / $chunkSize) +$r = Invoke-SSHCommand -SSHSession $s -Command "rm -f /tmp/tests.b64 && touch /tmp/tests.b64 && echo OK" +for ($i = 0; $i -lt $chunks; $i++) { + $start = $i * $chunkSize + $len = [Math]::Min($chunkSize, $b64.Length - $start) + $part = $b64.Substring($start, $len) + $r = Invoke-SSHCommand -SSHSession $s -Command "echo '$part' >> /tmp/tests.b64 && wc -c < /tmp/tests.b64" + if (($i % 3) -eq 0) { Write-Host (" chunk {0}/{1}: {2}" -f ($i+1), $chunks, ($r.Output -join "")) } +} +$r = Invoke-SSHCommand -SSHSession $s -Command "base64 -d /tmp/tests.b64 > ~/enet-validator/src/tests.rs && wc -c < ~/enet-validator/src/tests.rs && md5sum ~/enet-validator/src/tests.rs" +Write-Host ("解码: " + ($r.Output -join " | ")) +$localMd5 = (Get-FileHash $src -Algorithm MD5).Hash.ToLower() +Write-Host ("本地MD5: $localMd5") +Remove-SSHSession $s | Out-Null diff --git a/scripts/session/diff_ann2.ps1 b/scripts/session/diff_ann2.ps1 new file mode 100644 index 0000000..f18bf3a --- /dev/null +++ b/scripts/session/diff_ann2.ps1 @@ -0,0 +1,49 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$cap = "$env:TEMP\opencode\sym_startup.pcap" +$ts = "C:\Program Files\Wireshark\tshark.exe" +$tpl = (Get-Content "C:\Users\tonycao\work\light\enet-validator\samples\announce_686.hex" -Raw).Trim() -replace '\s','' +# 取两帧 live announce (不同时刻) +$anns = @(& $ts -r $cap -Y "udp.length==652" -T fields -e frame.time_relative -e udp.payload 2>$null | Select-Object -First 2) +$h0 = ($anns[0] -split "`t")[1]; $t0 = ($anns[0] -split "`t")[0] +$h1 = ($anns[1] -split "`t")[1]; $t1 = ($anns[1] -split "`t")[0] +Write-Host ("template len=" + $tpl.Length/2 + " live len=" + $h0.Length/2) +Write-Host "" +Write-Host "=== template vs liveA(t=$t0) 差异区域 ===" +$n = [Math]::Min($tpl.Length, $h0.Length) +$regions = @() +for ($i = 0; $i -lt $n; $i += 2) { + if ($tpl.Substring($i,2) -ne $h0.Substring($i,2)) { + $off = $i/2 + if ($regions.Count -eq 0 -or ($off - $regions[$regions.Count-1][1]) -gt 4) { + [void]$regions.Add(@($off, $off)) + } else { $regions[$regions.Count-1][1] = $off } + } +} +foreach ($r in $regions) { + $s = [Math]::Max(0, $r[0]-3); $e = [Math]::Min($tpl.Length/2-1, $r[1]+3) + $st=''; $sl=''; $sr='' + for ($i=$s; $i -le $e; $i++) { + $st += $tpl.Substring(2*$i,2) + $sl += $h0.Substring(2*$i,2) + if ($i -le $h1.Length/2-1) { $sr += $h1.Substring(2*$i,2) } else { $sr += '??' } + } + Write-Host ("0x{0:x4}-0x{1:x4}: tpl={2} liveA={3} liveB={4}" -f $s, $e, $st, $sl, $sr) +} +# liveA vs liveB (两帧实时宣告间的差异 = 每次宣告都变的字段) +Write-Host "" +Write-Host "=== liveA(t=$t0) vs liveB(t=$t1) 差异 (每次宣告变化的字段) ===" +$d2 = @() +for ($i = 0; $i -lt $n; $i += 2) { + if ($h0.Substring($i,2) -ne $h1.Substring($i,2)) { + $off = $i/2 + if ($d2.Count -eq 0 -or ($off - $d2[$d2.Count-1][1]) -gt 4) { [void]$d2.Add(@($off,$off)) } else { $d2[$d2.Count-1][1] = $off } + } +} +foreach ($r in $d2) { + $s = [Math]::Max(0, $r[0]-3); $e = [Math]::Min($h0.Length/2-1, $r[1]+3) + $sl=''; $sr='' + for ($i=$s; $i -le $e; $i++) { $sl += $h0.Substring(2*$i,2); $sr += $h1.Substring(2*$i,2) } + Write-Host ("0x{0:x4}-0x{1:x4}: A={2} B={3}" -f $s, $e, $sl, $sr) +} +Set-Content -Encoding ascii "$env:TEMP\opencode\announce_live.hex" $h0 +Write-Host "saved: announce_live.hex" diff --git a/scripts/session/diff_ann3.ps1 b/scripts/session/diff_ann3.ps1 new file mode 100644 index 0000000..d46f09a --- /dev/null +++ b/scripts/session/diff_ann3.ps1 @@ -0,0 +1,24 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$tpl = (Get-Content "C:\Users\tonycao\work\light\enet-validator\samples\announce_686.hex" -Raw).Trim() -replace '\s','' +$h0 = (Get-Content "$env:TEMP\opencode\announce_live.hex" -Raw).Trim() -replace '\s','' +Write-Host ("template len=" + $tpl.Length/2 + " live len=" + $h0.Length/2) +$diffs = New-Object 'System.Collections.Generic.List[int]' +$n = [Math]::Min($tpl.Length, $h0.Length) +for ($i = 0; $i -lt $n; $i += 2) { + if ($tpl.Substring($i,2) -ne $h0.Substring($i,2)) { $diffs.Add([int]($i/2)) } +} +Write-Host ("differing bytes: " + $diffs.Count) +# 分组连续区域 +$groups = New-Object 'System.Collections.Generic.List[object]' +foreach ($d in $diffs) { + if ($groups.Count -gt 0 -and ($d - $groups[$groups.Count-1][1]) -le 6) { $groups[$groups.Count-1][1] = $d } + else { $null = $groups.Add([pscustomobject]@{S=$d; E=$d}) } +} +foreach ($g in $groups) { + $s = [Math]::Max(0, $g.S-4); $e = [Math]::Min($n/2-1, $g.E+4) + $st=''; $sl='' + for ($i=$s; $i -le $e; $i++) { $st += $tpl.Substring(2*$i,2); $sl += $h0.Substring(2*$i,2) } + Write-Host ("0x{0:x4}-0x{1:x4}:" -f $s, $e) + Write-Host (" tpl = " + $st) + Write-Host (" live= " + $sl) +} diff --git a/scripts/session/diff_announce.ps1 b/scripts/session/diff_announce.ps1 new file mode 100644 index 0000000..48f2c1f --- /dev/null +++ b/scripts/session/diff_announce.ps1 @@ -0,0 +1,31 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$cap = "$env:TEMP\opencode\sym_claim2.pcap" +$ts = "C:\Program Files\Wireshark\tshark.exe" +# 提取两个不同时刻的 announce (644B payload = 652 udplen) hex +$anns = @(& $ts -r $cap -Y "udp.length==652" -T fields -e frame.time_relative -e udp.payload 2>$null | Select-Object -First 2) +Write-Host ("announce count sample: " + $anns.Count) +if ($anns.Count -ge 2) { + $t0 = ($anns[0] -split "`t")[0]; $h0 = ($anns[0] -split "`t")[1] + $t1 = ($anns[1] -split "`t")[0]; $h1 = ($anns[1] -split "`t")[1] + Write-Host ("A t=" + $t0 + " len=" + ($h0.Length/2)) + Write-Host ("B t=" + $t1 + " len=" + ($h1.Length/2)) + # 找差异字节 + $diffs = @() + $n = [Math]::Min($h0.Length, $h1.Length) + for ($i = 0; $i -lt $n; $i += 2) { + if ($h0.Substring($i,2) -ne $h1.Substring($i,2)) { $diffs += ($i/2) } + } + Write-Host ("differing byte offsets: " + (($diffs | ForEach-Object { '0x{0:x}' -f $_ }) -join ' ')) + # 打印模板与实时帧在这些区域的内容 + $tpl = (Get-Content "C:\Users\tonycao\work\light\enet-validator\samples\announce_686.hex" -Raw).Trim() -replace '\s','' + Write-Host "" + foreach ($d in $diffs) { + $s = [Math]::Max(0, $d - 4); $e = [Math]::Min($h0.Length/2 - 1, $d + 7) + $seg0 = ''; $seg1 = ''; $segt = '' + for ($i = $s; $i -le $e; $i++) { $seg0 += $h0.Substring(2*$i,2); $seg1 += $h1.Substring(2*$i,2); if (2*$i+2 -le $tpl.Length) { $segt += $tpl.Substring(2*$i,2) } else { $segt += '??????' } } + Write-Host ('0x{0:x}: liveA={1} liveB={2} tpl={3}' -f $d, $seg0, $seg1, $segt) + } + # 存下完整实时 announce 供修补用 + Set-Content -Encoding ascii "$env:TEMP\opencode\announce_live.hex" $h0 + Write-Host "saved live announce to announce_live.hex" +} diff --git a/scripts/session/enum_preclaim.ps1 b/scripts/session/enum_preclaim.ps1 new file mode 100644 index 0000000..1bbc9f0 --- /dev/null +++ b/scripts/session/enum_preclaim.ps1 @@ -0,0 +1,20 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$cap = "$env:TEMP\opencode\sym_claim2.pcap" +$ts = "C:\Program Files\Wireshark\tshark.exe" +Write-Host "=== ALL host-sent frames before claim (t<354.8): dst x udp.len x count + first/last t ===" +$all = @(& $ts -r $cap -Y "ip.src==192.168.123.113 and frame.time_relative<354.5" -T fields -e frame.time_relative -e ip.dst -e udp.length 2>$null) +$groups = @{} +foreach ($l in $all) { $p = $l -split "`t"; $k = $p[1] + " len=" + $p[2]; if (-not $groups.ContainsKey($k)) { $groups[$k] = New-Object System.Collections.ArrayList }; [void]$groups[$k].Add([double]$p[0]) } +foreach ($k in ($groups.Keys | Sort-Object)) { + $arr = $groups[$k] + Write-Host ("{0,-30} n={1,-5} first={2:N1}s last={3:N1}s" -f $k, $arr.Count, $arr[0], $arr[$arr.Count-1]) +} +Write-Host "" +Write-Host "=== ALL device-sent frames before claim (t<354.8) ===" +$dev = @(& $ts -r $cap -Y "ip.src==192.168.123.1 and frame.time_relative<354.5" -T fields -e frame.time_relative -e ip.dst -e udp.length 2>$null) +$g2 = @{} +foreach ($l in $dev) { $p = $l -split "`t"; $k = $p[1] + " len=" + $p[2]; if (-not $g2.ContainsKey($k)) { $g2[$k] = 0 }; $g2[$k]++ } +foreach ($k in ($g2.Keys | Sort-Object)) { Write-Host ("{0,-30} n={1}" -f $k, $g2[$k]) } +Write-Host "" +Write-Host "=== EUCE (548) timing before claim ===" +& $ts -r $cap -Y "udp.length==548" -T fields -e frame.time_relative 2>$null | Select-Object -First 20 diff --git a/scripts/session/extract_symclaim.ps1 b/scripts/session/extract_symclaim.ps1 new file mode 100644 index 0000000..224d100 --- /dev/null +++ b/scripts/session/extract_symclaim.ps1 @@ -0,0 +1,13 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$cap = "$env:TEMP\opencode\sym_claim2.pcap" +$ts = "C:\Program Files\Wireshark\tshark.exe" +# 抓 ident 和第一个 AECH 质询的完整 hex (frame.time_epoch 精确定位) +Write-Host "=== ident102 (t=354.810) full hex ===" +& $ts -r $cap -Y "udp.length==110 and frame.time_epoch>=1789049091.467 and frame.time_epoch<=1789049091.468" -T fields -e udp.payload 2>$null +Write-Host "" +Write-Host "=== AECH challenge #1 (t=354.824) full hex ===" +& $ts -r $cap -Y "udp.length==560 and frame.time_epoch>=1789049091.481 and frame.time_epoch<=1789049091.482" -T fields -e udp.payload 2>$null +Write-Host "" +Write-Host "=== AERE #1 (t=354.826) 0xC0 前 96B ===" +$h = & $ts -r $cap -Y "udp.length==560 and frame.time_epoch>=1789049091.483 and frame.time_epoch<=1789049091.484" -T fields -e udp.payload 2>$null +if ($h) { $h.Substring(0, [Math]::Min(300, $h.Length)) } diff --git a/scripts/session/find_claim2.ps1 b/scripts/session/find_claim2.ps1 new file mode 100644 index 0000000..d649e8f --- /dev/null +++ b/scripts/session/find_claim2.ps1 @@ -0,0 +1,8 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$cap = "$env:TEMP\opencode\sym_claim2.pcap" +$ts = "C:\Program Files\Wireshark\tshark.exe" +Write-Host "=== ident(110) / 47B(55) / AECH-AERE(560) frames ===" +& $ts -r $cap -Y "udp.length==110 or udp.length==55 or udp.length==560" -T fields -e frame.time_relative -e frame.time_epoch -e ip.src -e ip.dst -e udp.length 2>$null +Write-Host "" +Write-Host "=== last discovery(188) + announce(652) before claim (restart boundary) ===" +& $ts -r $cap -Y "udp.length==188 or udp.length==652" -T fields -e frame.time_relative -e ip.src -e ip.dst -e udp.length 2>$null | Select-Object -Last 8 diff --git a/scripts/session/find_sub07.ps1 b/scripts/session/find_sub07.ps1 new file mode 100644 index 0000000..bd705fb --- /dev/null +++ b/scripts/session/find_sub07.ps1 @@ -0,0 +1,9 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ts = "C:\Program Files\Wireshark\tshark.exe" +# sub=07 特征: type=02 帧 0x18-0x19 = 07 00。用 payload 前缀过滤: ENet + 02 + ...+ 0700 +# udp.payload hex 的 0x18-0x19 字节 = 第 48-51 字符 +foreach ($cap in @("$env:TEMP\opencode\sym_claim2.pcap", "$env:TEMP\opencode\cold2.pcap", "$env:TEMP\opencode\sym_close.pcap")) { + Write-Host ("=== " + (Split-Path $cap -Leaf) + " : type=02 sub=07 frames ===") + & $ts -r $cap -Y "udp[16:2]==0x0700 and ip.src==192.168.123.113 and udp.length<200" -T fields -e frame.time_relative -e udp.length -e udp.payload 2>$null | ForEach-Object { $p = $_ -split "`t"; "{0,9:N1}s len={1} {2}" -f [double]$p[0], $p[1], $p[2].Substring(0, [Math]::Min(104, $p[2].Length)) } | Select-Object -First 12 + Write-Host "" +} diff --git a/scripts/session/find_sub07b.ps1 b/scripts/session/find_sub07b.ps1 new file mode 100644 index 0000000..2d75aaa --- /dev/null +++ b/scripts/session/find_sub07b.ps1 @@ -0,0 +1,7 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ts = "C:\Program Files\Wireshark\tshark.exe" +foreach ($cap in @("$env:TEMP\opencode\sym_claim2.pcap", "$env:TEMP\opencode\cold2.pcap", "$env:TEMP\opencode\sym_close.pcap")) { + Write-Host ("=== " + (Split-Path $cap -Leaf) + " : type=02 sub=07 (udp[32:2]==0x0700) host-sent ===") + & $ts -r $cap -Y "udp[32:2]==0x0700 and ip.src==192.168.123.113 and udp.length<300" -T fields -e frame.time_relative -e udp.length -e udp.payload 2>$null | ForEach-Object { $p = $_ -split "`t"; "{0,9:N1}s len={1} {2}" -f [double]$p[0], $p[1], $p[2].Substring(0, [Math]::Min(104, $p[2].Length)) } | Select-Object -First 10 + Write-Host "" +} diff --git a/scripts/session/grep_aech.ps1 b/scripts/session/grep_aech.ps1 new file mode 100644 index 0000000..ffea6e6 --- /dev/null +++ b/scripts/session/grep_aech.ps1 @@ -0,0 +1,11 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +if (-not $s) { throw "SSH connect failed" } +$r = Invoke-SSHCommand -SSHSession $s -Command "grep -nE 'AECH|质询|应答|claim' ~/butler.log | tail -n 15; echo ---; wc -l ~/butler.log" +$r.Output +Remove-SSHSession -SSHSession $s | Out-Null diff --git a/scripts/session/listen_113.ps1 b/scripts/session/listen_113.ps1 new file mode 100644 index 0000000..a626c70 --- /dev/null +++ b/scripts/session/listen_113.ps1 @@ -0,0 +1,18 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$u = New-Object Net.Sockets.UdpClient +$u.Client.SetSocketOption([Net.Sockets.SocketOptionLevel]::Socket, [Net.Sockets.SocketOptionName]::ReuseAddress, $true) +$u.Client.Bind((New-Object Net.IPEndPoint([Net.IPAddress]::Parse("192.168.123.113"), 31772))) +$u.Client.ReceiveTimeout = 1000 +$sw = [Diagnostics.Stopwatch]::StartNew() +$remote = New-Object Net.IPEndPoint([Net.IPAddress]::Any, 0) +$last = Get-Date +while ($sw.Elapsed.TotalSeconds -lt 120) { + try { + $d = $u.Receive([ref]$remote) + $tag = "" + if ($d.Length -ge 4) { $tag = [Text.Encoding]::ASCII.GetString($d[0..3]) } + Write-Host ("{0,7:N1}s len={1,-5} from={2} magic={3} 0x18-0x19={4:x2}{5:x2}" -f $sw.Elapsed.TotalSeconds, $d.Length, $remote.Address, $tag, $(if($d.Length -gt 0x19){$d[0x19]}else{0}), $(if($d.Length -gt 0x18){$d[0x18]}else{0})) + $last = Get-Date + } catch {} +} +Write-Host "监听结束 (120s)" diff --git a/scripts/session/mystery60.ps1 b/scripts/session/mystery60.ps1 new file mode 100644 index 0000000..8f1419b --- /dev/null +++ b/scripts/session/mystery60.ps1 @@ -0,0 +1,11 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$cap = "$env:TEMP\opencode\sym_claim2.pcap" +$ts = "C:\Program Files\Wireshark\tshark.exe" +Write-Host "=== len=60 payload hex (mystery pre-claim frames) ===" +& $ts -r $cap -Y "udp.length==60 and ip.src==192.168.123.113" -T fields -e frame.time_relative -e udp.srcport -e udp.payload 2>$null +Write-Host "" +Write-Host "=== source ports of host frames (pre-claim) ===" +& $ts -r $cap -Y "ip.src==192.168.123.113 and frame.time_relative<354.8" -T fields -e udp.srcport 2>$null | Group-Object | Select-Object Count, Name +Write-Host "" +Write-Host "=== source port of ident + AECH ===" +& $ts -r $cap -Y "(udp.length==110 or udp.length==560) and frame.time_relative>=354" -T fields -e frame.time_relative -e udp.srcport 2>$null | Select-Object -First 5 diff --git a/scripts/session/post_claim.ps1 b/scripts/session/post_claim.ps1 new file mode 100644 index 0000000..1b40fcb --- /dev/null +++ b/scripts/session/post_claim.ps1 @@ -0,0 +1,11 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +if (-not $s) { throw "SSH connect failed" } +$r = Invoke-SSHCommand -SSHSession $s -Command "awk '/5767.0s/,0' ~/butler.log | grep -vE 'DMX #|发现请求' | head -n 30" +$r.Output +Remove-SSHSession -SSHSession $s | Out-Null diff --git a/scripts/session/probe_loop.ps1 b/scripts/session/probe_loop.ps1 new file mode 100644 index 0000000..de4bc9f --- /dev/null +++ b/scripts/session/probe_loop.ps1 @@ -0,0 +1,20 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$exe = "C:\Users\tonycao\work\light\enet-validator\target\release\enet-validator.exe" +$log = "$env:TEMP\opencode\probe_loop.log" +Remove-Item $log -ErrorAction SilentlyContinue +for ($i = 0; $i -lt 14; $i++) { + $ts = Get-Date -Format "HH:mm:ss" + $env:HOSTDEMO_MODE = "claimonly" + $out = & $exe hostdemo 192.168.123.1 0 2>&1 | Out-String + Add-Content -Encoding utf8 $log ("=== " + $ts + " attempt " + $i + " ===`n" + $out) + if ($out -match "CLAIM_PROBE.*PASS") { + Add-Content -Encoding utf8 $log ((Get-Date -Format "HH:mm:ss") + " CLAIM UNLOCKED - full hostdemo starting") + Remove-Item Env:\HOSTDEMO_MODE -ErrorAction SilentlyContinue + $out2 = & $exe hostdemo 192.168.123.1 30 2>&1 | Out-String + Add-Content -Encoding utf8 $log $out2 + Add-Content -Encoding utf8 $log "=== full hostdemo done ===" + break + } + Start-Sleep -Seconds 300 +} +Add-Content -Encoding utf8 $log "probe loop exit" diff --git a/scripts/session/probe_loop2.ps1 b/scripts/session/probe_loop2.ps1 new file mode 100644 index 0000000..3110b70 --- /dev/null +++ b/scripts/session/probe_loop2.ps1 @@ -0,0 +1,20 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$exe = "C:\Users\tonycao\work\light\enet-validator\target\release\enet-validator.exe" +$log = "$env:TEMP\opencode\probe_loop2.log" +Remove-Item $log -ErrorAction SilentlyContinue +for ($i = 0; $i -lt 45; $i++) { + $ts = Get-Date -Format "HH:mm:ss" + $env:HOSTDEMO_MODE = "claimonly" + $out = & $exe hostdemo 192.168.123.1 0 2>&1 | Out-String + Add-Content -Encoding utf8 $log ("=== " + $ts + " attempt " + $i + " ===`n" + $out) + if ($out -match "CLAIM_PROBE.*PASS") { + Add-Content -Encoding utf8 $log ((Get-Date -Format "HH:mm:ss") + " CLAIM UNLOCKED - full hostdemo starting (RDM discovery + 60s DMX)") + Remove-Item Env:\HOSTDEMO_MODE -ErrorAction SilentlyContinue + $out2 = & $exe hostdemo 192.168.123.1 60 2>&1 | Out-String + Add-Content -Encoding utf8 $log $out2 + Add-Content -Encoding utf8 $log "=== full hostdemo done ===" + break + } + Start-Sleep -Seconds 240 +} +Add-Content -Encoding utf8 $log "probe loop2 exit" diff --git a/scripts/session/push_try.ps1 b/scripts/session/push_try.ps1 new file mode 100644 index 0000000..484bf34 --- /dev/null +++ b/scripts/session/push_try.ps1 @@ -0,0 +1,8 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +$r = Invoke-SSHCommand -SessionId $s.SessionId -Command "cd ~/enet-validator && GIT_TERMINAL_PROMPT=0 git push -u origin master 2>&1 | head -10" -TimeOut 120 +$r.Output +Remove-SSHSession -SessionId $s.SessionId | Out-Null diff --git a/scripts/session/rdm_tail.ps1 b/scripts/session/rdm_tail.ps1 new file mode 100644 index 0000000..ba72a03 --- /dev/null +++ b/scripts/session/rdm_tail.ps1 @@ -0,0 +1,11 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +if (-not $s) { throw "SSH connect failed" } +$r = Invoke-SSHCommand -SSHSession $s -Command "grep -vE 'DMX #|发现请求' ~/butler.log | tail -n 15" +$r.Output +Remove-SSHSession -SSHSession $s | Out-Null diff --git a/scripts/session/rdm_tail2.ps1 b/scripts/session/rdm_tail2.ps1 new file mode 100644 index 0000000..20246b8 --- /dev/null +++ b/scripts/session/rdm_tail2.ps1 @@ -0,0 +1,11 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +if (-not $s) { throw "SSH connect failed" } +$r = Invoke-SSHCommand -SSHSession $s -Command "grep -c 'RDM' ~/butler.log; grep 'RDM' ~/butler.log | tail -n 12" +$r.Output +Remove-SSHSession -SSHSession $s | Out-Null diff --git a/scripts/session/read_symclose.ps1 b/scripts/session/read_symclose.ps1 new file mode 100644 index 0000000..e29998e --- /dev/null +++ b/scripts/session/read_symclose.ps1 @@ -0,0 +1,16 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$cap = "$env:TEMP\opencode\sym_close.pcap" +$ts = "C:\Program Files\Wireshark\tshark.exe" +Write-Host "=== stats by type (src dst udplen count) ===" +& $ts -r $cap -T fields -e ip.src -e ip.dst -e udp.length 2>$null | Group-Object { $p = $_ -split "`t"; "$($p[0]) -> $($p[1]) len=$($p[2])" } | Sort-Object Count -Descending | Select-Object -First 12 Count, Name | Format-Table -AutoSize +Write-Host "=== DMX stream (udplen=1092) per-5s buckets ===" +$dmx = @(& $ts -r $cap -Y "udp.length==1092" -T fields -e frame.time_relative 2>$null) +Write-Host ("total DMX frames: " + $dmx.Count) +$buckets = @{} +foreach ($t in $dmx) { $b = [int]([double]$t / 5); $buckets[$b] = 1 + [int]($buckets[$b] | ForEach-Object { $_ }) } +foreach ($k in ($buckets.Keys | Sort-Object)) { Write-Host (" t={0,4}-{1,-4}s : {2} frames" -f ($k*5), ($k*5+5), $buckets[$k]) } +if ($dmx.Count -gt 0) { Write-Host (" last DMX at t=" + $dmx[$dmx.Count-1]) } +Write-Host "=== AECH/AERE (udplen=560) ===" +& $ts -r $cap -Y "udp.length==560" -T fields -e frame.time_relative -e ip.src -e ip.dst 2>$null +Write-Host "=== last 15 frames overall ===" +& $ts -r $cap -T fields -e frame.time_relative -e ip.src -e ip.dst -e udp.length 2>$null | Select-Object -Last 15 diff --git a/scripts/session/read_symclose2.ps1 b/scripts/session/read_symclose2.ps1 new file mode 100644 index 0000000..877c6a8 --- /dev/null +++ b/scripts/session/read_symclose2.ps1 @@ -0,0 +1,16 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$cap = "$env:TEMP\opencode\sym_close.pcap" +$ts = "C:\Program Files\Wireshark\tshark.exe" +Write-Host "=== DMX stream (1092) per-10s buckets ===" +$dmx = @(& $ts -r $cap -Y "udp.length==1092" -T fields -e frame.time_relative 2>$null) +Write-Host ("total: " + $dmx.Count) +if ($dmx.Count -gt 0) { + $buckets = @{} + foreach ($t in $dmx) { $b = [int]([double]$t / 10); if ($buckets.ContainsKey($b)) { $buckets[$b]++ } else { $buckets[$b] = 1 } } + foreach ($k in ($buckets.Keys | Sort-Object)) { Write-Host (" t={0,4}-{1,-4}s : {2}" -f ($k*10), ($k*10+10), $buckets[$k]) } + Write-Host (" last DMX t=" + $dmx[$dmx.Count-1]) +} +Write-Host "=== last 25 frames overall ===" +& $ts -r $cap -T fields -e frame.time_relative -e ip.src -e ip.dst -e udp.length 2>$null | Select-Object -Last 25 +Write-Host "=== any AECH/AERE (560) ===" +& $ts -r $cap -Y "udp.length==560" -T fields -e frame.time_relative -e ip.src 2>$null diff --git a/scripts/session/restore_butler.ps1 b/scripts/session/restore_butler.ps1 new file mode 100644 index 0000000..78b5a4b --- /dev/null +++ b/scripts/session/restore_butler.ps1 @@ -0,0 +1,9 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +$r = Invoke-SSHCommand -SSHSession $s -Command "bash ~/run_butler.sh device 2>&1 | tail -2; sleep 2; pgrep -af 'enet-validator device' | head -1" +Write-Host ($r.Output -join "`n") +Remove-SSHSession $s | Out-Null diff --git a/scripts/session/run_hd3.ps1 b/scripts/session/run_hd3.ps1 new file mode 100644 index 0000000..426fb45 --- /dev/null +++ b/scripts/session/run_hd3.ps1 @@ -0,0 +1,11 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$cap = "$env:TEMP\opencode\hd_claim3.pcap" +Remove-Item $cap -ErrorAction SilentlyContinue +$ts = "C:\Program Files\Wireshark\tshark.exe" +$proc = Start-Process -FilePath $ts -ArgumentList "-i `"\Device\NPF_{E7165B1C-BAF6-49CF-9A3D-EF12DBC17329}`" -f `"udp port 31772`" -a duration:420 -w `"$cap`"" -PassThru -WindowStyle Hidden -RedirectStandardError "$env:TEMP\opencode\tshark_err5.txt" +Start-Sleep -Seconds 2 +Set-Location "C:\Users\tonycao\work\light\enet-validator" +& .\target\release\enet-validator.exe hostdemo 192.168.123.1 30 +Start-Sleep -Seconds 5 +Write-Host "`n=== claim window in capture ===" +& $ts -r $cap -Y "udp.length==110 or udp.length==55 or udp.length==560" -T fields -e frame.time_relative -e ip.src -e ip.dst -e udp.length 2>$null | Select-Object -First 12 diff --git a/scripts/session/run_hd4.ps1 b/scripts/session/run_hd4.ps1 new file mode 100644 index 0000000..af6f37f --- /dev/null +++ b/scripts/session/run_hd4.ps1 @@ -0,0 +1,4 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +Set-Location "C:\Users\tonycao\work\light\enet-validator" +& .\target\release\enet-validator.exe hostdemo 192.168.123.1 30 2>&1 | Out-File -Encoding utf8 "$env:TEMP\opencode\hd3_run.log" +Write-Host "DONE" diff --git a/scripts/session/start_butler.ps1 b/scripts/session/start_butler.ps1 new file mode 100644 index 0000000..367c38a --- /dev/null +++ b/scripts/session/start_butler.ps1 @@ -0,0 +1,13 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +if (-not $s) { throw "SSH connect failed" } +$r = Invoke-SSHCommand -SSHSession $s -Command "cat ~/run_butler_local.sh" +$r.Output +$r = Invoke-SSHCommand -SSHSession $s -Command "nohup ~/run_butler_local.sh > /dev/null 2>&1 & sleep 3; pgrep -af enet; echo ---; tail -n 4 ~/butler.log" +$r.Output +Remove-SSHSession -SSHSession $s | Out-Null diff --git a/scripts/session/start_local.ps1 b/scripts/session/start_local.ps1 new file mode 100644 index 0000000..3689181 --- /dev/null +++ b/scripts/session/start_local.ps1 @@ -0,0 +1,23 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +$cmd = @' +cat > ~/run_butler_local.sh << 'EOF' +#!/bin/bash +pkill -f "release/enet-validator" 2>/dev/null +sleep 1 +cd /home/tonycao/enet-validator +nohup ./target/release/enet-validator device 192.168.58.7 00161cf12e88 86400 > /home/tonycao/butler.log 2>&1 & +disown +sleep 4 +echo "PID=$(pgrep -f 'release/enet-validator' | head -1)" +EOF +chmod +x ~/run_butler_local.sh +bash ~/run_butler_local.sh +'@ +$r = Invoke-SSHCommand -SSHSession $s -Command $cmd +Write-Host ($r.Output -join "`n") +Remove-SSHSession $s | Out-Null diff --git a/scripts/session/sync_docs.ps1 b/scripts/session/sync_docs.ps1 new file mode 100644 index 0000000..20acbc8 --- /dev/null +++ b/scripts/session/sync_docs.ps1 @@ -0,0 +1,52 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) + +function Copy-Up($local, $destDir) { + Set-SCPItem -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -Path $local -Destination $destDir +} + +# 1) docs +Invoke-Command -ScriptBlock { } | Out-Null +$s0 = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +Invoke-SSHCommand -SessionId $s0.SessionId -Command "mkdir -p ~/enet-validator/docs ~/enet-validator/scripts/session ~/enet-validator/tools" | Out-Null +Remove-SSHSession -SessionId $s0.SessionId | Out-Null + +Copy-Up "C:\Users\tonycao\work\light\enet_protocol.md" "~/enet-validator/docs/" +Copy-Up "C:\Users\tonycao\work\light\rust_verification_plan.md" "~/enet-validator/docs/" +Write-Host "sync docs" + +# 2) scripts (root) +foreach ($f in @("enet_lib.ps1","Verify-EnetProtocol.ps1","Verify-Rdm.ps1")) { + Copy-Up "C:\Users\tonycao\work\light\$f" "~/enet-validator/scripts/" +} +Write-Host "sync root scripts" + +# 3) scripts/session (全部 75 个会话脚本) +$sess = Get-ChildItem "$env:TEMP\opencode\*.ps1" +foreach ($f in $sess) { + Copy-Up $f.FullName "~/enet-validator/scripts/session/" +} +Write-Host ("sync session scripts: " + $sess.Count) + +# 4) tools (.cs) +foreach ($f in @("IlDump.cs","GetTable.cs","Scan.cs")) { + Copy-Up "$env:TEMP\opencode\$f" "~/enet-validator/tools/" +} +Write-Host "sync tools" + +# 5) README +Copy-Up "C:\Users\tonycao\work\light\enet-validator\README.md" "~/enet-validator/" +Write-Host "sync README" + +# 6) commit + push +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +$cmds = @' +cd ~/enet-validator && git add -A && git commit -m "docs+scripts: enet_protocol.md, verification plan, PS verify suite, session analysis scripts, DLL tools, README" | tail -2; git push origin master 2>&1 | tail -3; echo ---; git log --oneline | head -3 +'@ +$r = Invoke-SSHCommand -SessionId $s.SessionId -Command $cmds -TimeOut 180 +$r.Output +if ($r.Error) { Write-Host "STDERR:"; $r.Error | Select-Object -First 6 } +Remove-SSHSession -SessionId $s.SessionId | Out-Null diff --git a/scripts/session/sync_push.ps1 b/scripts/session/sync_push.ps1 new file mode 100644 index 0000000..9b6f5a1 --- /dev/null +++ b/scripts/session/sync_push.ps1 @@ -0,0 +1,42 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +if (-not $s) { throw "SSH connect failed" } +$sid = $s.SessionId + +# 1) 同步源码 +$srcFiles = @("main.rs","enet.rs","tests.rs","aech.rs","hostdemo.rs","dmxhash.rs") +foreach ($f in $srcFiles) { + Set-SCPItem -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -Path "C:\Users\tonycao\work\light\enet-validator\src\$f" -Destination "~/enet-validator/src/" +} +Set-SCPItem -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -Path "C:\Users\tonycao\work\light\enet-validator\Cargo.toml" -Destination "~/enet-validator/" +Set-SCPItem -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -Path "C:\Users\tonycao\work\light\enet-validator\Cargo.lock" -Destination "~/enet-validator/" +Set-SCPItem -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -Path "C:\Users\tonycao\work\light\enet-validator\.gitignore" -Destination "~/enet-validator/" + +# 2) 同步 samples +Invoke-SSHCommand -SessionId $sid -Command "mkdir -p ~/enet-validator/samples" | Out-Null +Get-ChildItem "C:\Users\tonycao\work\light\enet-validator\samples\*.hex" | ForEach-Object { + Set-SCPItem -SessionId $sid -Path $_.FullName -Destination "~/enet-validator/samples/" +} +Write-Host "sync done" + +# 3) git init + commit + push +$cmds = @" +cd ~/enet-validator +git init -b master 2>/dev/null || true +git config user.email "tonycao@local" || true +git config user.name "tonycao" || true +git add -A +git commit -m "enet-validator: e:net protocol reverse + host-side emulation (AECH/DMX/RDM, hostdemo)" || true +git remote remove origin 2>/dev/null || true +git remote add origin http://www.kanghexin.work:3000/tonycao/enet-validator.git +git log --oneline | head -3 +git remote -v +"@ +$r = Invoke-SSHCommand -SessionId $sid -Command $cmds -TimeOut 60 +$r.Output +if ($r.Error) { $r.Error | Select-Object -First 5 } +Remove-SSHSession -SessionId $sid | Out-Null diff --git a/scripts/session/sync_push2.ps1 b/scripts/session/sync_push2.ps1 new file mode 100644 index 0000000..defe53d --- /dev/null +++ b/scripts/session/sync_push2.ps1 @@ -0,0 +1,36 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) + +function Copy-Up($local, $destDir) { + Set-SCPItem -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -Path $local -Destination $destDir +} + +# 1) 源码 +foreach ($f in @("main.rs","enet.rs","tests.rs","aech.rs","hostdemo.rs","dmxhash.rs")) { + Copy-Up "C:\Users\tonycao\work\light\enet-validator\src\$f" "~/enet-validator/src/" + Write-Host "sync src/$f" +} +Copy-Up "C:\Users\tonycao\work\light\enet-validator\Cargo.toml" "~/enet-validator/" +Copy-Up "C:\Users\tonycao\work\light\enet-validator\Cargo.lock" "~/enet-validator/" +Copy-Up "C:\Users\tonycao\work\light\enet-validator\.gitignore" "~/enet-validator/" +Write-Host "sync root done" + +# 2) samples +$hexes = Get-ChildItem "C:\Users\tonycao\work\light\enet-validator\samples\*.hex" +foreach ($h in $hexes) { + Copy-Up $h.FullName "~/enet-validator/samples/" +} +Write-Host ("sync samples done: " + $hexes.Count + " files") + +# 3) git init + commit +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +$cmds = @' +cd ~/enet-validator && git init -b master 2>/dev/null; git config user.email "tonycao@local"; git config user.name "tonycao"; git add -A; git commit -m "enet-validator: e:net protocol reverse + host-side emulation (AECH claim / encrypted DMX / RDM discovery, hostdemo)" | tail -2; git remote remove origin 2>/dev/null; git remote add origin http://www.kanghexin.work:3000/tonycao/enet-validator.git; echo ---; git log --oneline 2>/dev/null | head -3; git remote -v +'@ +$r = Invoke-SSHCommand -SessionId $s.SessionId -Command $cmds -TimeOut 120 +$r.Output +if ($r.Error) { Write-Host "STDERR:"; $r.Error | Select-Object -First 6 } +Remove-SSHSession -SessionId $s.SessionId | Out-Null diff --git a/scripts/session/test_g.ps1 b/scripts/session/test_g.ps1 new file mode 100644 index 0000000..4191550 --- /dev/null +++ b/scripts/session/test_g.ps1 @@ -0,0 +1,47 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +# Craft AECH with fresh random nonce -> .7 (local G) -> verify resp32 == G(nonce) +$TABLE = @(0x1008de00,0xe3b77ff6,0x31183e1c,0x90fb53c8,0xff10ba5c,0x030bc4b6,0xe22935c8,0x10e8ede1, + 0x55f409a0,0x3eb0b9a3,0xf7333308,0xedf07810,0x8abf4078,0x60ec0545,0x1f4645c8,0x54094148, + 0x04c0f500,0xfabe50bb,0xf3f3e54c,0x6c2a02cb,0xa2546e60,0x4f6ae10e,0x51dfe8d8,0x2eedb0ec, + 0x74006928,0x48fade70,0xe5346e9c,0x1c3fe0bc,0xf6700700,0x6b7f1ba0,0xa1143cf4,0x3be8426c) +function G([uint32]$x) { + $big = [int64]$x * 13 + $m = [uint32]($big % 4294967296) + $idx = [int]((($x % 32) + 1 + ($m % 30)) % 32) + return $x -bxor $TABLE[$idx] +} +$raw = [IO.File]::ReadAllBytes("$env:TEMP\opencode\cold2.tsv") +$text = if ($raw[0] -eq 0xFF -and $raw[1] -eq 0xFE) { [Text.Encoding]::Unicode.GetString($raw) } else { [Text.Encoding]::UTF8.GetString($raw) } +$hex = $null +foreach ($l in ($text -split "`r?`n")) { + $p = $l -split "`t" + if ($p.Count -lt 5 -or -not $p[4]) { continue } + $h = $p[4] -replace " ", "" + if ($h.Length -eq 1104 -and $h.Substring(0x28 * 2, 8) -eq "41454348") { $hex = $h; break } +} +if (-not $hex) { throw "no AECH template" } +$n = $hex.Length / 2 +$req = New-Object byte[] $n +for ($i = 0; $i -lt $n; $i++) { $req[$i] = [Convert]::ToByte($hex.Substring(2 * $i, 2), 16) } + +$rnd = New-Object Random +$udp = New-Object System.Net.Sockets.UdpClient +$udp.Client.ReceiveTimeout = 3000 +$ep = New-Object System.Net.IPEndPoint([System.Net.IPAddress]::Parse("192.168.58.7"), 31772) +$pass = 0 +for ($t = 0; $t -lt 5; $t++) { + $nonce = [uint32]($rnd.NextDouble() * 4294967295) + $nb = [BitConverter]::GetBytes($nonce) + [Array]::Copy($nb, 0, $req, 0x88, 4) + $expect = G $nonce + [void]$udp.Send($req, $req.Length, $ep) + try { + $rep = $udp.Receive([ref]$ep) + $got = [BitConverter]::ToUInt32($rep, 0xC0) + $ok = $got -eq $expect + if ($ok) { $pass++ } + Write-Host ("#{0} nonce={1:x8} expect={2:x8} got={3:x8} {4} len={5}" -f ($t+1), $nonce, $expect, $got, $(if ($ok) {"PASS"} else {"FAIL"}), $rep.Length) + } catch { Write-Host ("#{0} no reply" -f ($t+1)) } +} +Write-Host "== $pass/5 PASS ==" +$udp.Close() diff --git a/scripts/session/test_newmac.ps1 b/scripts/session/test_newmac.ps1 new file mode 100644 index 0000000..dda0177 --- /dev/null +++ b/scripts/session/test_newmac.ps1 @@ -0,0 +1,14 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +# restart butler with NEW MAC (new device identity), NO relay -> host must re-claim, local AERE +$r = Invoke-SSHCommand -SSHSession $s -Command "pkill -f 'enet-validator device'; sleep 1; cd ~/enet-validator && nohup ./target/release/enet-validator device 192.168.58.7 00161cf12e89 300 > ~/butler.log 2>&1 & sleep 2; pgrep -af 'enet-validator device' | head -1" +Write-Host ($r.Output -join "`n") +Write-Host "--- waiting 90s for host auto-claim of new device ---" +Start-Sleep -Seconds 90 +$r = Invoke-SSHCommand -SSHSession $s -Command "echo AECH次数:; grep -c 'AECH' ~/butler.log; echo 注册次数:; grep -c 'sub=01' ~/butler.log; echo DMX行数:; grep -c 'DMX #' ~/butler.log; echo '---'; grep -E 'AECH|注册|DMX #1 ' ~/butler.log | head -10" +Write-Host ($r.Output -join "`n") +Remove-SSHSession $s | Out-Null diff --git a/scripts/session/test_norelay.ps1 b/scripts/session/test_norelay.ps1 new file mode 100644 index 0000000..41edfd2 --- /dev/null +++ b/scripts/session/test_norelay.ps1 @@ -0,0 +1,14 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +# restart butler WITHOUT relay arg (local AERE answers) +$r = Invoke-SSHCommand -SSHSession $s -Command "pkill -f 'enet-validator device'; sleep 1; cd ~/enet-validator && nohup ./target/release/enet-validator device 192.168.58.7 00161cf12e88 300 > ~/butler.log 2>&1 & sleep 2; pgrep -af 'enet-validator device'" +Write-Host ($r.Output -join "`n") +Write-Host "--- waiting 75s for host re-claim ---" +Start-Sleep -Seconds 75 +$r = Invoke-SSHCommand -SSHSession $s -Command "grep -cE 'AECH' ~/butler.log; echo '---'; grep -E 'AECH|DMX #' ~/butler.log | head -12" +Write-Host ($r.Output -join "`n") +Remove-SSHSession $s | Out-Null diff --git a/scripts/session/test_setack.ps1 b/scripts/session/test_setack.ps1 new file mode 100644 index 0000000..89fe69a --- /dev/null +++ b/scripts/session/test_setack.ps1 @@ -0,0 +1,52 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$lines = [IO.File]::ReadAllLines("$env:TEMP\opencode\set123b.tsv") +$hex = $null +foreach ($l in $lines) { + $p = $l -split "`t" + if ($p.Count -lt 5 -or -not $p[4]) { continue } + if ($p[1] -ne "192.168.123.113") { continue } + $h = $p[4] -replace " ", "" + if ($h.Length -ne 2168) { continue } + if ($h.Substring(0x20 * 2, 4) -ne "0200") { continue } # subtype event + if ($h.Substring(0x3C * 2, 2) -ne "01") { continue } # SubSC=01 + if ($h.Substring(0x3D * 2, 2) -ne "1a") { continue } # msgLen=26 + if ($h.Substring(0x4F * 2, 2) -ne "30") { continue } + if ($h.Substring(0x30 * 2, 2) -ne "00") { continue } + $hex = $h; break +} +if (-not $hex) { throw "no SET request frame found" } +$n = $hex.Length / 2 +$req = New-Object byte[] $n +for ($i = 0; $i -lt $n; $i++) { $req[$i] = [Convert]::ToByte($hex.Substring(2 * $i, 2), 16) } +$uidS = ($req[0x3E..0x43] | ForEach-Object { $_.ToString("x2") }) -join "" +$pidS = $req[0x50].ToString("x2") + $req[0x51].ToString("x2") +Write-Host "SET req: ${n}B UID=$uidS PID=$pidS" + +$udp = New-Object System.Net.Sockets.UdpClient +$udp.Client.ReceiveTimeout = 3000 +$ep = New-Object System.Net.IPEndPoint([System.Net.IPAddress]::Parse("192.168.58.7"), 31772) +$sw = [Diagnostics.Stopwatch]::StartNew() +$sent = $udp.Send($req, $req.Length, $ep) +Write-Host "sent $sent bytes from $($udp.Client.LocalEndPoint)" +while ($true) { + try { + $rep = $udp.Receive([ref]$ep) + if ($rep.Length -lt 0x44) { + Write-Host ("* reply {0}B latency {1}ms (42B telemetry)" -f $rep.Length, $sw.ElapsedMilliseconds) + continue + } + $srcS = ($rep[0x33..0x38] | ForEach-Object { $_.ToString("x2") }) -join "" + Write-Host ("* reply {0}B latency {1}ms srcUID={2} TN={3:x2} RT={4:x2} CC={5:x2} PID={6:x2}{7:x2} PDL={8}" -f ` + $rep.Length, $sw.ElapsedMilliseconds, $srcS, $rep[0x39], $rep[0x3A], $rep[0x3E], $rep[0x3F], $rep[0x40], $rep[0x41]) + $mlen = [int]$rep[0x2C]; $sum = 0 + for ($i = 0x2A; $i -lt 0x2A + $mlen; $i++) { $sum += $rep[$i] } + $ckv = ([int]$rep[0x2A + $mlen] -shl 8) -bor $rep[0x2A + $mlen + 1] + $v = if (($sum -band 0xFFFF) -eq $ckv) { "OK" } else { "FAIL" } + Write-Host (" checksum calc={0:x4} frame={1:x4} {2}" -f ($sum -band 0xFFFF), $ckv, $v) + break + } catch { + Write-Host "no reply within 3s" + break + } +} +$udp.Close() diff --git a/scripts/session/test_setget.ps1 b/scripts/session/test_setget.ps1 new file mode 100644 index 0000000..12fdae5 --- /dev/null +++ b/scripts/session/test_setget.ps1 @@ -0,0 +1,60 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +# E2E: replay SET frame patched to virtual fixture 130 (addr 200), then GET verify +$lines = [IO.File]::ReadAllLines("$env:TEMP\opencode\set123b.tsv") +$hex = $null +foreach ($l in $lines) { + $p = $l -split "`t" + if ($p.Count -lt 5 -or -not $p[4]) { continue } + if ($p[1] -ne "192.168.123.113") { continue } + $h = $p[4] -replace " ", "" + if ($h.Length -ne 2168) { continue } + if ($h.Substring(0x20 * 2, 4) -ne "0200") { continue } + if ($h.Substring(0x3C * 2, 2) -ne "01") { continue } + if ($h.Substring(0x3D * 2, 2) -ne "1a") { continue } + if ($h.Substring(0x4F * 2, 2) -ne "30") { continue } + if ($h.Substring(0x30 * 2, 2) -ne "00") { continue } + $hex = $h; break +} +if (-not $hex) { throw "no SET frame" } +$n = $hex.Length / 2 +$req = New-Object byte[] $n +for ($i = 0; $i -lt $n; $i++) { $req[$i] = [Convert]::ToByte($hex.Substring(2 * $i, 2), 16) } +# patch: dest UID @0x3E = 010734670130, PD @0x53 = 00 C8 (addr 200) +$vuid = [byte[]](0x01, 0x07, 0x34, 0x67, 0x01, 0x30) +[Array]::Copy($vuid, 0, $req, 0x3E, 6) +$req[0x53] = 0x00; $req[0x54] = 0xC8 + +$udp = New-Object System.Net.Sockets.UdpClient +$udp.Client.ReceiveTimeout = 3000 +$ep = New-Object System.Net.IPEndPoint([System.Net.IPAddress]::Parse("192.168.58.7"), 31772) + +function Send-Recv($frame, $label) { + $sw = [Diagnostics.Stopwatch]::StartNew() + [void]$udp.Send($frame, $frame.Length, $ep) + while ($true) { + try { + $rep = $udp.Receive([ref]$ep) + if ($rep.Length -lt 0x44) { continue } # skip 42B telemetry + $srcS = ($rep[0x33..0x38] | ForEach-Object { $_.ToString("x2") }) -join "" + $pdl = [int]$rep[0x41] + $pdS = "" + if ($pdl -gt 0 -and $rep.Length -ge 0x42 + $pdl) { $pdS = ($rep[0x42..(0x41 + $pdl)] | ForEach-Object { $_.ToString("x2") }) -join "" } + $mlen = [int]$rep[0x2C]; $sum = 0 + for ($i = 0x2A; $i -lt 0x2A + $mlen; $i++) { $sum += $rep[$i] } + $ckv = ([int]$rep[0x2A + $mlen] -shl 8) -bor $rep[0x2A + $mlen + 1] + $v = if (($sum -band 0xFFFF) -eq $ckv) { "OK" } else { "FAIL" } + Write-Host ("{0}: {1}B {2}ms srcUID={3} CC={4:x2} PID={5:x2}{6:x2} PDL={7} PD={8} ck={9}" -f ` + $label, $rep.Length, $sw.ElapsedMilliseconds, $srcS, $rep[0x3E], $rep[0x3F], $rep[0x40], $pdl, $pdS, $v) + return + } catch { Write-Host "${label}: no reply"; return } + } +} + +Send-Recv $req "SET 130 addr=200" +Start-Sleep -Milliseconds 100 +# GET: same frame, msgLen=0x18, CC=0x20, PDL=0 +$get = [byte[]]$req.Clone() +$get[0x3D] = 0x18; $get[0x4F] = 0x20; $get[0x52] = 0x00 +$get[0x22] = $get[0x22] + 1 # bump query seq for clarity +Send-Recv $get "GET 130 addr " +$udp.Close() diff --git a/scripts/session/verify_dmxsend.ps1 b/scripts/session/verify_dmxsend.ps1 new file mode 100644 index 0000000..8d31c15 --- /dev/null +++ b/scripts/session/verify_dmxsend.ps1 @@ -0,0 +1,11 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +if (-not $s) { throw "SSH connect failed" } +$r = Invoke-SSHCommand -SSHSession $s -Command "grep 'aa5501' ~/butler.log | tail -n 5; echo ---; grep -c 'DMX #' ~/butler.log; tail -n 4 ~/butler.log" +$r.Output +Remove-SSHSession -SSHSession $s | Out-Null diff --git a/scripts/session/verify_frame.ps1 b/scripts/session/verify_frame.ps1 new file mode 100644 index 0000000..cb2c250 --- /dev/null +++ b/scripts/session/verify_frame.ps1 @@ -0,0 +1,27 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$lines = [IO.File]::ReadAllLines("$env:TEMP\opencode\ss123.tsv") +$n = 0; $guardHit = 0; $msbZero = 0; $msbZero2 = 0; $first = $null +foreach ($ln in $lines) { + $p = $ln -split "`t" + if ($p.Count -lt 5 -or $p[4].Length -lt 100) { continue } + $hex = $p[4] -replace '[^0-9a-fA-F]','' + if ($hex.Length -ne 2168) { continue } # 1084B = 2168 hex chars + $b = New-Object byte[] 1084 + for ($i = 0; $i -lt 1084; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring($i*2,2),16) } + if ($b[0x20] -ne 1 -or $b[0x21] -ne 1) { continue } # DMX subtype + $n++ + if ($null -eq $first) { $first = $b } + $ch1 = $b[0x3C]; $ch513 = $b[0x23C] + if ($b[0x22] -eq (($ch1 - 1) -bxor 0x70)) { $guardHit++ } + if ($b[0x25] -lt 0x80) { $msbZero++ } + if ($b[0x29] -lt 0x80) { $msbZero2++ } +} +echo "DMX frames: $n" +echo "g1==(CH1-1)^0x70: $guardHit/$n" +echo "byte0x25<0x80 (MSB=0, Random.Next 特征): $msbZero/$n" +echo "byte0x29<0x80: $msbZero2/$n" +if ($first) { + $hdr = ($first[0x18..0x3F] | ForEach-Object { $_.ToString("x2") }) -join ' ' + echo "首帧 0x18-0x3F: $hdr" + echo "首帧 CH1-8: $(($first[0x3C..0x43] | ForEach-Object { $_.ToString('x2') }) -join ' ')" +} diff --git a/scripts/session/watch_claim.ps1 b/scripts/session/watch_claim.ps1 new file mode 100644 index 0000000..3aea1b8 --- /dev/null +++ b/scripts/session/watch_claim.ps1 @@ -0,0 +1,11 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +if (-not $s) { throw "SSH connect failed" } +$r = Invoke-SSHCommand -SSHSession $s -Command "tail -n 400 ~/butler.log | grep -nE 'AECH|认领|注册|claim|register|89B' | tail -n 20; echo ---TAIL---; tail -n 5 ~/butler.log" +$r.Output +Remove-SSHSession -SSHSession $s | Out-Null diff --git a/scripts/session/watch_sym_claim.ps1 b/scripts/session/watch_sym_claim.ps1 new file mode 100644 index 0000000..0bbfdf1 --- /dev/null +++ b/scripts/session/watch_sym_claim.ps1 @@ -0,0 +1,16 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$cap = "$env:TEMP\opencode\sym_claim.pcap" +Remove-Item $cap -ErrorAction SilentlyContinue +$ts = "C:\Program Files\Wireshark\tshark.exe" +$proc = Start-Process -FilePath $ts -ArgumentList "-i `"\Device\NPF_{E7165B1C-BAF6-49CF-9A3D-EF12DBC17329}`" -f `"udp port 31772`" -a duration:90 -w `"$cap`"" -PassThru -WindowStyle Hidden -RedirectStandardError "$env:TEMP\opencode\tshark_err2.txt" +Start-Sleep -Seconds 2 +Start-Process -FilePath "C:\Program Files\ecue\Sympholight 5.5\Sympholight.exe" +Write-Host "SymphoLight started, capturing 90s..." +Start-Sleep -Seconds 88 +Write-Host "=== frame type stats (src dst udplen count) ===" +& $ts -r $cap -T fields -e ip.src -e ip.dst -e udp.length 2>$null | Group-Object { $p = $_ -split "`t"; "$($p[0]) -> $($p[1]) len=$($p[2])" } | Sort-Object Count -Descending | Select-Object Count, Name | Format-Table -AutoSize +Write-Host "=== AERE replies (udplen=560, .1->113) times ===" +& $ts -r $cap -Y "ip.src==192.168.123.1 && udp.length==560" -T fields -e frame.time_relative 2>$null +Write-Host "=== DMX stream (udplen=1092, 113->.1) first/last ===" +$dmx = @(& $ts -r $cap -Y "ip.dst==192.168.123.1 && udp.length==1092" -T fields -e frame.time_relative 2>$null) +if ($dmx.Count -gt 0) { Write-Host (" first t=" + $dmx[0] + " last t=" + $dmx[$dmx.Count-1] + " total " + $dmx.Count) } else { Write-Host " no DMX stream" } diff --git a/scripts/session/webui_probe.ps1 b/scripts/session/webui_probe.ps1 new file mode 100644 index 0000000..f10db5a --- /dev/null +++ b/scripts/session/webui_probe.ps1 @@ -0,0 +1,11 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +foreach ($u in @("setup.htm","config.htm","reboot.htm","reset.htm","status.htm")) { + try { + $r = Invoke-WebRequest -Uri "http://192.168.123.1/$u" -TimeoutSec 4 -UseBasicParsing + Write-Host ("== " + $u + " (" + $r.StatusCode + ") ==") + [regex]::Matches($r.Content, '<(form|input|select|a)[^>]*>') | ForEach-Object { $_.Value } | Select-Object -First 20 + Write-Host "" + } catch { + Write-Host ("== " + $u + " : " + $_.Exception.Message) + } +} diff --git a/tools/GetTable.cs b/tools/GetTable.cs new file mode 100644 index 0000000..5603a4d --- /dev/null +++ b/tools/GetTable.cs @@ -0,0 +1,28 @@ +using System; +using System.Linq; +using Mono.Cecil; + +class GetTable { + static void Main(string[] args) { + string dll = args[0]; + string fname = args[1]; // e.g. 0A8F2B10... + var asm = AssemblyDefinition.ReadAssembly(dll); + foreach (var mod in asm.Modules) { + foreach (var t in mod.Types) { + if (!t.FullName.Contains("PrivateImplementationDetails")) continue; + foreach (var f in t.Fields) { + if (f.Name != fname) continue; + byte[] iv = f.InitialValue; + Console.WriteLine("field " + f.Name + " len=" + iv.Length); + for (int i = 0; i + 4 <= iv.Length; i += 4) { + uint v = BitConverter.ToUInt32(iv, i); + Console.WriteLine(" [{0,2}] = 0x{1:x8}", i / 4, v); + } + Console.WriteLine("hex: " + BitConverter.ToString(iv).Replace("-", "")); + return; + } + } + } + Console.WriteLine("NOT FOUND"); + } +} diff --git a/tools/IlDump.cs b/tools/IlDump.cs new file mode 100644 index 0000000..41d1fe1 --- /dev/null +++ b/tools/IlDump.cs @@ -0,0 +1,71 @@ +using System; +using System.IO; +using System.Linq; +using Mono.Cecil; +using Mono.Cecil.Cil; + +class IlDump { + static void Main(string[] args) { + string dll = args[0]; + string mode = args.Length > 1 ? args[1] : "types"; + string pat = args.Length > 2 ? args[2].ToLower() : ""; + var rp = new ReaderParameters { ReadSymbols = false }; + var asm = AssemblyDefinition.ReadAssembly(dll, rp); + foreach (var mod in asm.Modules) { + foreach (var t in mod.Types) { + DumpType(t, mode, pat, 0); + } + } + } + static void DumpType(TypeDefinition t, string mode, string pat, int depth) { + bool tmatch = pat.Length == 0 || t.FullName.ToLower().Contains(pat); + if (mode == "types") { + bool anyMethod = t.Methods.Any(m => m.Name.ToLower().Contains(pat)); + if (tmatch || anyMethod) { + Console.WriteLine("TYPE " + t.FullName); + foreach (var m in t.Methods) { + if (tmatch || m.Name.ToLower().Contains(pat)) + Console.WriteLine(" M " + m.FullName); + } + foreach (var f in t.Fields) { + if (tmatch) Console.WriteLine(" F " + f.FieldType.Name + " " + f.Name); + } + } + } else if (mode == "il") { + foreach (var m in t.Methods) { + if (!(t.FullName.ToLower().Contains(pat) || m.Name.ToLower().Contains(pat))) continue; + if (!m.HasBody) continue; + Console.WriteLine("=== " + m.FullName + " ==="); + foreach (var v in m.Body.Variables) + Console.WriteLine(" .local " + v.VariableType.Name + " V_" + v.Index); + foreach (var ins in m.Body.Instructions) { + string op = ins.Operand == null ? "" : " " + OpText(ins.Operand); + Console.WriteLine(string.Format(" IL_{0:x4}: {1}{2}", ins.Offset, ins.OpCode.Name, op)); + } + } + } else if (mode == "xref") { + // 找所有引用了匹配名称的方法/字段的方法体 + foreach (var m in t.Methods) { + if (!m.HasBody) continue; + bool hit = false; + foreach (var ins in m.Body.Instructions) { + string s = ins.Operand == null ? "" : ins.Operand.ToString(); + if (s.ToLower().Contains(pat)) { hit = true; break; } + } + if (hit) Console.WriteLine("XREF " + m.FullName); + } + } + foreach (var nt in t.NestedTypes) DumpType(nt, mode, pat, depth + 1); + } + static string OpText(object o) { + var mr = o as MethodReference; if (mr != null) return mr.FullName; + var fr = o as FieldReference; if (fr != null) return fr.FullName; + var s = o as string; if (s != null) return "\"" + s + "\""; + var ins = o as Instruction; if (ins != null) return "IL_" + ins.Offset.ToString("x4"); + var arr = o as Instruction[]; if (arr != null) return string.Join(",", arr.Select(i => "IL_" + i.Offset.ToString("x4")).ToArray()); + var p = o as ParameterReference; if (p != null) return "arg" + p.Index; + var vr = o as VariableReference; if (vr != null) return "V_" + vr.Index; + var tr = o as TypeReference; if (tr != null) return tr.FullName; + return o == null ? "" : o.ToString(); + } +} diff --git a/tools/Scan.cs b/tools/Scan.cs new file mode 100644 index 0000000..b22c470 --- /dev/null +++ b/tools/Scan.cs @@ -0,0 +1,30 @@ +using System; +using System.IO; + +class Scan { + static int Count(byte[] hay, byte[] needle) { + int n = 0; + for (int i = 0; i + needle.Length <= hay.Length; i++) { + bool m = true; + for (int j = 0; j < needle.Length; j++) if (hay[i + j] != needle[j]) { m = false; break; } + if (m) n++; + } + return n; + } + static void Main(string[] args) { + string dir = args[0]; + string[] pats = { "AECH", "AERE", "AEREP", "Claim", "challenge" }; + foreach (string f in Directory.GetFiles(dir, "*.dll")) { + byte[] b = File.ReadAllBytes(f); + string hits = ""; + foreach (string p in pats) { + byte[] a = System.Text.Encoding.ASCII.GetBytes(p); + byte[] u = System.Text.Encoding.Unicode.GetBytes(p); + int ca = Count(b, a), cu = Count(b, u); + if (ca > 0 || cu > 0) hits += string.Format(" {0}(ascii={1},utf16={2})", p, ca, cu); + } + if (hits.Length > 0) Console.WriteLine(Path.GetFileName(f) + ":" + hits); + } + Console.WriteLine("scan done"); + } +}