From 9995335d745c7063123f1cc98ca35d90ecf5e364 Mon Sep 17 00:00:00 2001 From: OpenCode Date: Fri, 11 Sep 2026 12:03:43 +0800 Subject: [PATCH] feat: show mode for standalone 1-32 rainbow DMX + RDM status polling - Merge show/hybrid paths: use dmx_a template for DMX payload and replay-extracted SL frame skeleton for RDM. - HOSTDEMO_MODE=show runs indefinitely when secs=0; wave period slowed for smoother breathing. - Update README with show mode usage. --- .gitignore | 4 + Cargo.lock | 7 + Cargo.toml | 10 + README.md | 86 ++ docs/hostdemo_recap_2026-09-10.md | 80 ++ samples/ack_84.hex | 1 + samples/aere_594.hex | 1 + samples/aerep_113.hex | 1 + samples/aerep_115.hex | 1 + samples/aerep_117.hex | 1 + samples/announce_144.hex | 1 + samples/announce_686.hex | 1 + samples/announce_live.hex | 1 + samples/claim_h112.hex | 1 + samples/claim_h114.hex | 1 + samples/claim_h116.hex | 1 + samples/discovery_req_222.hex | 1 + samples/discovery_resp_259.hex | 1 + samples/discovery_resp_259_orig.hex | 1 + samples/discovery_resp_259_rdm.hex | 1 + samples/dmx_a_1126.hex | 1 + samples/euce_582.hex | 1 + samples/event_b_1126.hex | 1 + samples/heartbeat_78.hex | 1 + samples/pid_map.txt | 50 + samples/rdm_coldpulse_0.hex | 1 + samples/rdm_coldpulse_1.hex | 1 + samples/rdm_coldpulse_2.hex | 1 + samples/rdm_coldpulse_3.hex | 1 + samples/rdm_coldpulse_4.hex | 1 + samples/rdm_disc_req_1126.hex | 1 + samples/rdm_info_119.hex | 1 + samples/rdm_pool.txt | 178 +++ samples/rdm_pulse_101.hex | 1 + samples/rdm_pulse_101_0.hex | 1 + samples/rdm_pulse_101_1.hex | 1 + samples/rdm_pulse_101_2.hex | 1 + samples/rdm_pulse_101_3.hex | 1 + samples/rdm_pulse_101_4.hex | 1 + samples/rdm_req_1126.hex | 1 + samples/rdm_resp_111.hex | 1 + samples/rdm_setack_68.hex | 1 + samples/register_89.hex | 1 + samples/status_84.hex | 1 + src/aech.rs | 1226 +++++++++++++++++++ src/dmxhash.rs | 1201 +++++++++++++++++++ src/enet.rs | 239 ++++ src/hostdemo.rs | 1731 +++++++++++++++++++++++++++ src/main.rs | 194 +++ src/tests.rs | 905 ++++++++++++++ validation_report.json | 10 + 51 files changed, 5958 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 docs/hostdemo_recap_2026-09-10.md create mode 100644 samples/ack_84.hex create mode 100644 samples/aere_594.hex create mode 100644 samples/aerep_113.hex create mode 100644 samples/aerep_115.hex create mode 100644 samples/aerep_117.hex create mode 100644 samples/announce_144.hex create mode 100644 samples/announce_686.hex create mode 100644 samples/announce_live.hex create mode 100644 samples/claim_h112.hex create mode 100644 samples/claim_h114.hex create mode 100644 samples/claim_h116.hex create mode 100644 samples/discovery_req_222.hex create mode 100644 samples/discovery_resp_259.hex create mode 100644 samples/discovery_resp_259_orig.hex create mode 100644 samples/discovery_resp_259_rdm.hex create mode 100644 samples/dmx_a_1126.hex create mode 100644 samples/euce_582.hex create mode 100644 samples/event_b_1126.hex create mode 100644 samples/heartbeat_78.hex create mode 100644 samples/pid_map.txt create mode 100644 samples/rdm_coldpulse_0.hex create mode 100644 samples/rdm_coldpulse_1.hex create mode 100644 samples/rdm_coldpulse_2.hex create mode 100644 samples/rdm_coldpulse_3.hex create mode 100644 samples/rdm_coldpulse_4.hex create mode 100644 samples/rdm_disc_req_1126.hex create mode 100644 samples/rdm_info_119.hex create mode 100644 samples/rdm_pool.txt create mode 100644 samples/rdm_pulse_101.hex create mode 100644 samples/rdm_pulse_101_0.hex create mode 100644 samples/rdm_pulse_101_1.hex create mode 100644 samples/rdm_pulse_101_2.hex create mode 100644 samples/rdm_pulse_101_3.hex create mode 100644 samples/rdm_pulse_101_4.hex create mode 100644 samples/rdm_req_1126.hex create mode 100644 samples/rdm_resp_111.hex create mode 100644 samples/rdm_setack_68.hex create mode 100644 samples/register_89.hex create mode 100644 samples/status_84.hex create mode 100644 src/aech.rs create mode 100644 src/dmxhash.rs create mode 100644 src/enet.rs create mode 100644 src/hostdemo.rs create mode 100644 src/main.rs create mode 100644 src/tests.rs create mode 100644 validation_report.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1073d60 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +*.log +*.pcap +*.tsv diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..5879716 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "enet-validator" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a5cca18 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "enet-validator" +version = "0.1.0" +edition = "2021" + +[dependencies] +# 闆跺閮ㄤ緷璧栵細std::net::UdpSocket + std::fs 瓒冲 + +[profile.release] +opt-level = 2 diff --git a/README.md b/README.md new file mode 100644 index 0000000..a8888b5 --- /dev/null +++ b/README.md @@ -0,0 +1,86 @@ +# 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 在线探测 +``` + +## show 模式:地址 1-32 炫彩呼吸 + RDM 状态轮询 + +无需 SymphoLight 运行,独立接管 Butler,持续输出炫彩呼吸 DMX 并轮询设备状态。 + +```powershell +# 永久运行(按 Ctrl+C 停止) +$env:HOSTDEMO_MODE = "show" +.\target\release\enet-validator.exe hostdemo 192.168.123.1 0 + +# 或运行固定秒数 +.\target\release\enet-validator.exe hostdemo 192.168.123.1 600 +``` + +- DMX:地址 1-32 彩虹呼吸渐变(HMKJ 32 通道灯具) +- RDM:每 700ms 轮询 3 台已知 UID,实时打印 `DEVICE_INFO` 与 `DMX_START_ADDRESS` +- 依赖:仅需首次抓包文件 `replay_frames.txt` 作为帧骨架模板(程序自带 `samples/` 中亦有备用模板) + +## 已破解要点(详见 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/hostdemo_recap_2026-09-10.md b/docs/hostdemo_recap_2026-09-10.md new file mode 100644 index 0000000..22cf358 --- /dev/null +++ b/docs/hostdemo_recap_2026-09-10.md @@ -0,0 +1,80 @@ +# hostdemo 攻坚总结 — 真机 Butler .1 主机侧仿真全链路打通 + +**日期**:2026-09-10 晚 ~ 09-11 凌晨 +**结论**:✅ 认领 + RDM 设备发现(3/3)+ DMX 呼吸控制(灯光肉眼确认) +**代码**:`HOSTDEMO_MODE=slfull`(src/hostdemo.rs),commit fcb20a7/3056a69 + +--- + +## 一、最终工作配方 + +``` +1. 发现: type=1 (180B) → 217B 应答 +2. 认领: announce144 (=102B 身份帧) 单播 → 47B 注册应答 + → AECH 质询 (nonce/B12 自选随机) → AERE = G(nonce) ✓ +3. 循环 verbatim 重放 SL 真实会话帧流 (32540 帧 / 126.7s): + - DMX 数据帧: 保留 SL 原帧全部结构, 仅替换 r1/r2 + 密文段 + (呼吸图案用本次认领的 B12 重加密 → Butler 解密上灯) + - RDM 请求: SL 原帧原样 (UN_MUTE / UNIQUE_BRANCH / GET, ff+fe 成对) +4. 被动解码应答: DISC 脉冲 → UID; DEVICE_INFO / START_ADDRESS → DMX 地址 +``` + +**实测**:3/3 灯具发现(NFS1K2D@101/2ch、NFM680S@123、HMKJ@1/32ch, +应答 15.5k 帧)+ 三色异相呼吸(fade-in),**灯光肉眼确认**。 + +运行方式: +``` +set HOSTDEMO_MODE=slfull +set REPLAY_FILE=<重放清单.txt> (tshark 提取: time|dst|dstport|payload) +enet-validator hostdemo 192.168.123.1 480 +``` + +--- + +## 二、假设-证伪链(当晚完整过程) + +| # | 假设 | 实验 | 结果 | +|---|---|---|---| +| 1 | 认领后需长前奏 | 45s discovery/announce 广播 | ❌ 无效 | +| 2 | 需要 sub=07 绑定帧 | 按实测节奏重放 ×4 | ❌ 无效 | +| 3 | 宣告时间戳过期被拒 | live 模板 + 新鲜时间戳 | ❌ 无效 | +| 4 | 会话保持 ~2h 需等待 | 45→86 分钟轮询探针 | ❌ 超时后仍拒 | +| 5 | 1947 密钥/EUCE 6s 节奏门控 | 会话内补发 | ❌ 单独无效 | +| 6 | RDM 帧字段差异 | 模板 vs 实帧 0 差异 | ❌ 排除 | +| 7 | **verbatim 重放二分** | 阶段 A(无认领)/B(先认领) | ✅ **B 全通** | + +**教训**:帧级逐字节比对全通过后仍被拒时,问题在会话状态层—— +用 verbatim 重放做二分是定位此类问题的最快路径。 + +--- + +## 三、深层发现(已写入协议文档 §3.7/§11.7) + +1. **AECH 握手成功 ≠ 会话通道打通**:47B/AERE 全过但自构造 type=4 帧被静默丢弃; + verbatim 的 SL 历史帧立即全通 → Butler 在帧级校验某种会话一致性(规则未破解)。 +2. **RDM 请求帧消息起始位置可变**:UN_MUTE@0x3A vs UNIQUE_BRANCH@0x3C + (前置 len16 字段浮动 2B)→ 固定偏移构造必被拒 → 重放绕过。 +3. **announce144 == ident102**(同一帧两种称呼);真机冷启动序列: + 广播前奏 → 身份帧 → 47B → AECH×3 → DMX+RDM(首应答延迟 ~1.4s 属正常)。 +4. **1947 端口"密钥报文"** = 40B ASCII 许可令牌(安装级常量,双目标 21 帧 @200ms + 每 ~34s 一轮);**EUCE** = 全程每 6s 的许可信标。 +5. **灯具 MUTE 态跨会话持久**(SL 遗留 mute 挡后续 branch 查询)。 +6. **密钥盘点**:nonce/B12 逐会话随机自主可控;G 表固定;1947 令牌安装级常量 + (未做必需性消融)。详见协议文档 §3.7 密钥盘点表。 + +--- + +## 四、遗留事项 + +- [ ] RDM 帧变长结构规则(破解后可纯自构造帧发现,摆脱对 SL 原帧依赖) +- [ ] 1947 令牌必需性消融实验 +- [ ] NFM680S 通道数应答漂移(2ch vs 1ch,疑查询时机差异) +- [ ] 会话级校验的具体算法(当前只知道"verbatim 能过") + +## 五、产物清单 + +- 代码:`src/hostdemo.rs`(slfull/slclaim/sls/hybrid/replay 五模式) +- 重放清单:`replay_merged.txt`(sym_startup 101-520s + rdm3 全程,32540 帧) +- 抓包:`sym_startup.pcap`(认领+发现全程)、`sym_claim2.pcap`(接管+重启)、 + `allports.pcap`(全端口,1947 发现)、`hd_claim3.pcap` 等 +- 文档:`enet_protocol.md` §3.7/§11.7 本夜更新 diff --git a/samples/ack_84.hex b/samples/ack_84.hex new file mode 100644 index 0000000..d79721e --- /dev/null +++ b/samples/ack_84.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001002a00000002004275746c010018030000010000000000 \ No newline at end of file diff --git a/samples/aere_594.hex b/samples/aere_594.hex new file mode 100644 index 0000000..b5e707e --- /dev/null +++ b/samples/aere_594.hex @@ -0,0 +1 @@ +454e65740200000000001c7c0000000000000100280200000600017ba8c000161cf12e3900717ba84145524559bb367fffffff0107346701250015000a00000001017ba8c000ffffff00161cf12e39000000000000000000001a0000000101fa0c000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000023010000000024000000240000001c001c001d001d8361bf1d00f00af00a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/samples/aerep_113.hex b/samples/aerep_113.hex new file mode 100644 index 0000000..c05a30d --- /dev/null +++ b/samples/aerep_113.hex @@ -0,0 +1 @@ +454e65740200000000001c7c0000000000000100280200000600017ba8c000161cf12e3900717ba84145524559bb367ffffffd0107348809000015000a00000001017ba8c000ffffff00161cf12e39000000000000000000001a0000000101fa0c000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000023010000000023000000230000001c001c001d001d5904e1c100f00af00a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/samples/aerep_115.hex b/samples/aerep_115.hex new file mode 100644 index 0000000..8a0d423 --- /dev/null +++ b/samples/aerep_115.hex @@ -0,0 +1 @@ +454e65740200000000001c7c0000000000000100280200000600017ba8c000161cf12e3900717ba84145524559bb367ffffffd0107348809000015000a00000001017ba8c000ffffff00161cf12e39000000000000000000001a0000000101fa0c000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000023010000000023000000230000001c001c001d001d51cf405000f00af00a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/samples/aerep_117.hex b/samples/aerep_117.hex new file mode 100644 index 0000000..36c4f0b --- /dev/null +++ b/samples/aerep_117.hex @@ -0,0 +1 @@ +454e65740200000000001c7c0000000000000100280200000600017ba8c000161cf12e3900717ba84145524559bb367ffffffd0107348809000015000a00000001017ba8c000ffffff00161cf12e39000000000000000000001a0000000101fa0c000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000023010000000023000000230000001c001c001d001dfce6923d00f00af00a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/samples/announce_144.hex b/samples/announce_144.hex new file mode 100644 index 0000000..3d03fa8 --- /dev/null +++ b/samples/announce_144.hex @@ -0,0 +1 @@ +454e65740200000000001c7c0000000000000100660000000100717ba8c0300ed559bb361a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/samples/announce_686.hex b/samples/announce_686.hex new file mode 100644 index 0000000..3066eb9 --- /dev/null +++ b/samples/announce_686.hex @@ -0,0 +1 @@ +454e65740100000000001c7c000000000000010084020000150000000000000000000000000000000000000000000000000000000000000000001a000000000001717ba8c000ffffff300ed559bb3600000000000000000000000000000001ce43050500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000043a0f4f7ec175641812b909bdc99f2ff4000000000010003000000000000000000000000000100030000000000000000000000000074006f006e007900630061006f004400450053004b0054004f0050002d0050004500440032004c00560044002d00660037006600340061003000340033002d0031003700650063002d0034003100350036002d0038003100320062002d0039003000390062006400630039003900660032006600660000000000000000000000911f343cf708bf342e8e519da944bd8a5a4a24433989ea070902001819410343003a005c00550073006500720073005c0074006f006e007900630061006f005c0041007000700044006100740061005c004c006f00630061006c005c0065006300750065005c00530059004d00500048004f004c004900470048005400200035002e0035005c00430075007200720065006e007400530068006f0077000000000000000000000000000000000000000000d00df1e81900000081710b0e000000008ee4f7f6190000001e00000000000000010000005a00ffffea07090717182f16010200000000000000000000000002000000000000000000000000201c0000000000000000000001000000000000000000000001000000000000000100000001000000 \ No newline at end of file diff --git a/samples/announce_live.hex b/samples/announce_live.hex new file mode 100644 index 0000000..a515f9e --- /dev/null +++ b/samples/announce_live.hex @@ -0,0 +1 @@ +454e65740100000000001c7c000000000000010084020000150000000000000000000000000000000000000000000000000000000000000000001a000000000001717ba8c000ffffff300ed559bb3600000000000000000000000000000001ce43050500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000043a0f4f7ec175641812b909bdc99f2ff4000000000010003000000000000000000000000000100030000000000000000000000000074006f006e007900630061006f004400450053004b0054004f0050002d0050004500440032004c00560044002d00660037006600340061003000340033002d0031003700650063002d0034003100350036002d0038003100320062002d0039003000390062006400630039003900660032006600660000000000000000000000911f0000000000000000000000000000000000000000d00701010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e1bf21b20c0000001e00000000000000010000005a00ffffea07090a163432e003020000000000000000000000000200000000000000000000000098080000000000000000000000000000000000000000000000000000000000000000000000000000 diff --git a/samples/claim_h112.hex b/samples/claim_h112.hex new file mode 100644 index 0000000..7a7191e --- /dev/null +++ b/samples/claim_h112.hex @@ -0,0 +1 @@ +454e65740200000000001c7c0000000000000100280200000500717ba8c0300ed559bb36000000004145434841cad262b4c0d9aea3e3c1cc0898bffec74c63aca7b51e5727a7ae330ca7d61e162e57e9fa86044d898e5f16a8249dd5d6ab360eedef3a8cab7a3a0d12a6a1fb0da2b5d940d50340481523195508af0309d95c6071c73027f4ca3e80e2545f3b9bb5327e4cf63be3ec6408056ec49400a7f6c6d7cc2136a3cb37043849418945728ee5769783f351da3f2cf1ffa99445455bb87a01585bd3949ab415f20299abdac393e0de1e69240274fd8cf445a422a2bef3ed880e44193212e1fccede5fd8a681163c16b46626f294788d5bcfdf4ef69272330eac22cb7e79adcc3c6c55231525e5722e670bed58ccfbf496b7be633393063c6c2bc9fa0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/samples/claim_h114.hex b/samples/claim_h114.hex new file mode 100644 index 0000000..6d0fd5d --- /dev/null +++ b/samples/claim_h114.hex @@ -0,0 +1 @@ +454e65740200000000001c7c0000000000000100280200000500717ba8c0300ed559bb3600000000414543484bbd9d079aea853e547835e96c208e8685db41b02e35c7ba4998a96619b7e95beb13e3aa6ee5bde275b5413f897ac67277b5e64f87493016f5e2bd01411e259c8ed9fd593de4189f1d5f3a78f38830cfd236ee63d099d4a2e2cdb48b5f2e2a1f9bb5327e27bca0578fa4b0af7c6d35939c4731280eb8226db1ee2f0a30c2ccf4055a77702f8d7761bee3e94ed7815265015ac68967ac9429aa47d9398d9626cdd8bca921f5c4640fd0d0692aced495309bca09d5fb438c59c64dc6e8b33dd428bc1c751bd7306f3cf6b6b89742695bd6b1a99ec38d51d1952f505ba7fe7b5c92fb416eb25e7a586a5accde458af5165df2f0370a9f119b6b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/samples/claim_h116.hex b/samples/claim_h116.hex new file mode 100644 index 0000000..85e3f14 --- /dev/null +++ b/samples/claim_h116.hex @@ -0,0 +1 @@ +454e65740200000000001c7c0000000000000100280200000500717ba8c0300ed559bb360000000041454348e0eb87d5c73e7a5a269b01faa87737503368c0728c0b3a39fdb50e4452f3eaa602687e8ed195928e64ea645915624a7800ba6ca8daf9bdd5b14dd8122f360833b15af840f9a87e9dd6320ea1a6e1239a6b9644978641cb09aaa8dc834a22993e9bb5327ebd805e2de231a8bb6a0a7e573bced2a8ea1c2ed02a3da799f170ac6f8fee9550cbfae056d3e0bf7539682f6117ec5cb76f231f8e7a0c597fd8c53b7284382c7c742e8b40ee7cd50a5db897c10d45820413f4006bcf07d6bfa0607d0da44cf0003cb3ede8611cc9f34d1430c17598719342365e9037c0ad6c57cc9f8dfec76510c796855a5899a3f72a0d1ebad6144a08eeeaca6d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/samples/discovery_req_222.hex b/samples/discovery_req_222.hex new file mode 100644 index 0000000..364ca7b --- /dev/null +++ b/samples/discovery_req_222.hex @@ -0,0 +1 @@ +454e65740100000000001c7c0000000000000100b40000000100746f6e7963616f4445534b544f502d504544324c56442d66376634613034332d1a000000000002717ba8c000ffffff300ed559bb3600000000000000000000000000000001ce430505000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000900 \ No newline at end of file diff --git a/samples/discovery_resp_259.hex b/samples/discovery_resp_259.hex new file mode 100644 index 0000000..8c6fdad --- /dev/null +++ b/samples/discovery_resp_259.hex @@ -0,0 +1 @@ +454e65740100000000001c7c0000000000000100d90000000f004275746c657220533200000000001b00cc0119453a7ffffff30107346701250015000a00000001017ba8c000ffffff00161cf12e396b3aa8c0300ed559bb351a0000000101fa0c00020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002301000000001e0000001e000000240024002400240020002000720d720d1e0000000a0000000000000000000a00 \ No newline at end of file diff --git a/samples/discovery_resp_259_orig.hex b/samples/discovery_resp_259_orig.hex new file mode 100644 index 0000000..946c00a --- /dev/null +++ b/samples/discovery_resp_259_orig.hex @@ -0,0 +1 @@ +454e65740100000000001c7c0000000000000100d90000000f004275746c657220533200000000000000aaaa5dbbddfa55aa55ae77ebddaf570015000a00000001017ba8c000ffffff00161cf12e39000000000000000000001a0000000101fa0c000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000023010000000024000000240000001c001c001d001d001c001c00f00af00a00000000000000000000000000000000 diff --git a/samples/discovery_resp_259_rdm.hex b/samples/discovery_resp_259_rdm.hex new file mode 100644 index 0000000..946c00a --- /dev/null +++ b/samples/discovery_resp_259_rdm.hex @@ -0,0 +1 @@ +454e65740100000000001c7c0000000000000100d90000000f004275746c657220533200000000000000aaaa5dbbddfa55aa55ae77ebddaf570015000a00000001017ba8c000ffffff00161cf12e39000000000000000000001a0000000101fa0c000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000023010000000024000000240000001c001c001d001d001c001c00f00af00a00000000000000000000000000000000 diff --git a/samples/dmx_a_1126.hex b/samples/dmx_a_1126.hex new file mode 100644 index 0000000..2eb4c1d --- /dev/null +++ b/samples/dmx_a_1126.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001003c040000010097010000000001011a8c191563d67616000000000100000001000002000201000000abaaa9a8a7a6a5a4a3a2a1a09f9e9d9c9b9a999897969594939291908f8e8d8c8b8a898887868584838281807f7e7d7c7b7a797877767574737271706f6e6d6c6b6a696867666564636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0efeeedecebeae9e8e7e6e5e4e3e2e1e0dfdedddcdbdad9d8d7d6d5d4d3d2d1d0cfcecdcccbcac9c8c7c6c5c4c3c2c1c0bfbebdbcbbbab9b8b7b6b5b4b3b2b1b0afaeadacabaaa9a8a7a6a5a4a3a2a1a09f9e9d9c9b9a999897969594939291908f8e8d8c8b8a898887868584838281807f7e7d7c7b7a797877767574737271706f6e6d6c6b6a696867666564636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a0908070605040302015554535251504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a090807060504030201d4d3d2d1d0cfcecdcccbcac9c8c7c6c5c4c3c2c1c0bfbebdbcbbbab9b8b7b6b5b4b3b2b1b0afaeadacabaaa9a8a7a6a5a4a3a2a1a09f9e9d9c9b9a999897969594939291908f8e8d8c8b8a898887868584838281807f7e7d7c7b7a797877767574737271706f6e6d6c6b6a696867666564636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0efeeedecebeae9e8e7e6e5e4e3e2e1e0dfdedddcdbdad9d8d7d6d5d4d3d2d1d0cfcecdcccbcac9c8c7c6c5c4c3c2c1c0bfbebdbcbbbab9b8b7b6b5b4b3b2b1b0afaeadacabaaa9a8a7a6a5a4a3a2a1a09f9e9d9c9b9a999897969594939291908f8e8d8c8b8a898887868584838281807f7e7d7c7b7a797877767574737271706f6e6d6c6b6a696867666564636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a0908070605040302012c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a090807060504030201 \ No newline at end of file diff --git a/samples/euce_582.hex b/samples/euce_582.hex new file mode 100644 index 0000000..30b6d8f --- /dev/null +++ b/samples/euce_582.hex @@ -0,0 +1 @@ +4555434500000000000000000500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000746f6e7963616f4445534b544f502d504544324c56442d66376634613034332d313765632d343135362d383132622d3930396264633939663266660000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300ed559bb36 \ No newline at end of file diff --git a/samples/event_b_1126.hex b/samples/event_b_1126.hex new file mode 100644 index 0000000..794806b --- /dev/null +++ b/samples/event_b_1126.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001003c040000010000000000000002001903000000000000cc00000001000100000025000000010000000124ffffffffffff453a7ffffffe96010000001000010c80007ffffffffffffffffffe158e1b1a191817161514131211100f0e0d0c0b0a09080706050403020100fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0efeeedecebeae9e8e7e6e5e4e3e2e1e0dfdedddcdbdad9d8d7d6d5d4d3d2d1d0cfcecdcccbcac9c8c7c6c5c4c3c2c1c0bfbebdbcbbbab9b8b7b6b5b4b3b2b1b0afaeadacabaaa9a8a7a6a5a4a3a2a1a09f9e9d9c9b9a999897969594939291908f8e8d8c8b8a898887868584838281807f7e7d7c7b7a797877767574737271706f6e6d6c6b6a696867666564636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a090807060504030201c0bfbebdbcbbbab9b8b7b6b5b4b3b2b1b0afaeadacabaaa9a8a7a6a5a4a3a2a1a09f9e9d9c9b9a999897969594939291908f8e8d8c8b8a898887868584838281807f7e7d7c7b7a797877767574737271706f6e6d6c6b6a696867666564636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a0908070605040302018b8a898887868584838281807f7e7d7c7b7a797877767574737271706f6e6d6c6b6a696867666564636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a0908070605040302017574737271706f6e6d6c6b6a696867666564636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0efeeedecebeae9e8e7e6e5e4e3e2e1e0dfdedddcdbdad9d8d7d6d5d4d3d2d1d0cfcecdcccbcac9c8c7c6c5c4c3c2c1c0bfbebdbcbbbab9b8b7b6b5b4b3b2b1b0afaeadacabaaa9a8a7a6a5a4a3a2a1a09f9e9d9c9b9a999897969594939291908f8e8d8c8b8a898887868584838281807f7e7d7c7b7a797877767574737271706f6e6d6c6b6a696867666564636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a090807060504030201 \ No newline at end of file diff --git a/samples/heartbeat_78.hex b/samples/heartbeat_78.hex new file mode 100644 index 0000000..ba0f9bc --- /dev/null +++ b/samples/heartbeat_78.hex @@ -0,0 +1 @@ +454e65740200000000001c7c0000000000000100240000000300717ba8c0300ed559bb36 \ No newline at end of file diff --git a/samples/pid_map.txt b/samples/pid_map.txt new file mode 100644 index 0000000..7ce72fd --- /dev/null +++ b/samples/pid_map.txt @@ -0,0 +1,50 @@ +0080 454e65740400000000001c7c00000000000001005200000002004275746c000098000000000000002800cc0126453a7fffffff01073467012584000000002100800e4e46532d314b32447878784278200b32 +0504 454e65740400000000001c7c00000000000001003b0000000200017ba8c001000d010000000000001100aaab55af57be75aaddab5daa55ae57efd7 +8013 454e65740400000000001c7c00000000000001004500000002004275746c0000a2000000000000001b00cc0119453a7fffffff0107346701258e00000000218013010106ee +8074 454e65740400000000001c7c00000000000001004500000002004275746c0000b9000000000000001b00cc0119453a7fffffff010734670125a50000000021807401000765 +0060 454e65740400000000001c7c00000000000001005700000002004275746c000021010000000000002d00cc012b453a7fffffff0107348809000c00000000210060130100020005090001000100010101006f00001106f6 +8021 454e65740400000000001c7c00000000000001004500000002004275746c0000a4000000000000001b00cc0119453a7fffffff0107346701259000000000218021010106fe +8064 454e65740400000000001c7c00000000000001004600000002004275746c0000b3000000000000001c00cc011a453a7fffffff0107346701259f000000002180640200000751 +00c0 454e65740400000000001c7c00000000000001005700000002004275746c000096000000000000002d00cc012b453a7fffffff01073467012582000000002100c0134e46532d314b324478787842782056312e30310c90 +8051 454e65740400000000001c7c00000000000001004500000002004275746c0000ac000000000000001b00cc0119453a7fffffff010734670125980000000021805101000735 +8072 454e65740400000000001c7c00000000000001004500000002004275746c0000b7000000000000001b00cc0119453a7fffffff010734670125a30000000021807201000761 +8063 454e65740400000000001c7c00000000000001004600000002004275746c0000b2000000000000001c00cc011a453a7fffffff0107346701259e0000000021806302154d07b1 +8084 454e65740400000000001c7c00000000000001004600000002004275746c0000bf000000000000001c00cc011a453a7fffffff010734670125aa0000000021808402005c07d8 +8050 454e65740400000000001c7c00000000000001004500000002004275746c0000ab000000000000001b00cc0119453a7fffffff010734670125970000000021805001010734 +8071 454e65740400000000001c7c00000000000001004500000002004275746c0000b6000000000000001b00cc0119453a7fffffff010734670125a2000000002180710100075f +0082 454e65740400000000001c7c00000000000001005000000002004275746c00009a000000000000002600cc0124453a7fffffff01073467012586000000002100820c4e4653314b324478787842780ae5 +8041 454e65740400000000001c7c00000000000001004500000002004275746c0000a8000000000000001b00cc0119453a7fffffff010734670125940000000021804101fe081f +0003 454e65740400000000001c7c00000000000001005700000002004275746c00005c000000000000002d00cc012b453a7fffffff010734670125510000000021006013010001030509000100010002010100650000170736 +0401 454e65740400000000001c7c00000000000001004800000002004275746c00009c000000000000001e00cc011c453a7fffffff0107346701258800000000210401040000000d066c +8090 454e65740400000000001c7c00000000000001004500000002004275746c0000c2000000000000001b00cc0119453a7fffffff010734670125ad00000000218090013207bb +8053 454e65740400000000001c7c00000000000001004500000002004275746c0000ae000000000000001b00cc0119453a7fffffff0107346701259a0000000021805301000739 +8010 454e65740400000000001c7c00000000000001004500000002004275746c0000a0000000000000001b00cc0119453a7fffffff0107346701258c00000000218010010106e9 +8042 454e65740400000000001c7c00000000000001004500000002004275746c0000a9000000000000001b00cc0119453a7fffffff010734670125950000000021804201ff0822 +8061 454e65740400000000001c7c00000000000001004800000002004275746c0000b0000000000000001e00cc011c453a7fffffff0107346701259c00000000218061040000a3650857 +8081 454e65740400000000001c7c00000000000001004800000002004275746c00007c010000000000001e00cc011c453a7fffffff0107348809005e00000000218081040008a67b085e +0405 454e65740400000000001c7c00000000000001004800000002004275746c00009f000000000000001e00cc011c453a7fffffff0107346701258b00000000210405040000006906cf +00f0 454e65740400000000001c7c0000000000000100460000000200017ba8c000002e020000000000001c00cc011a453a7fffffff0107346701250f000000002100f00200650732 +0002 454e65740400000000001c7c00000000000001004600000002004275746c000020010000000000001c00cc011a453a7fffffff0107348809000b0000000011000202000005cf +0403 454e65740400000000001c7c00000000000001004500000002004275746c00009e000000000000001b00cc0119453a7fffffff0107346701258a00000000210403017f06dc +8060 454e65740400000000001c7c00000000000001004700000002004275746c0000af000000000000001d00cc011b453a7fffffff0107346701259b000000002180600309334707ce +8022 454e65740400000000001c7c00000000000001004500000002004275746c0000a5000000000000001b00cc0119453a7fffffff010734670125910000000021802201010700 +8065 454e65740400000000001c7c00000000000001004800000002004275746c0000b4000000000000001e00cc011c453a7fffffff010734670125a00000000021806504000000000757 +8085 454e65740400000000001c7c00000000000001004800000002004275746c0000c0000000000000001e00cc011c453a7fffffff010734670125ab0000000021808504000000040786 +8011 454e65740400000000001c7c00000000000001004500000002004275746c0000a1000000000000001b00cc0119453a7fffffff0107346701258d00000000218011013c0726 +8070 454e65740400000000001c7c00000000000001004500000002004275746c0000b5000000000000001b00cc0119453a7fffffff010734670125a1000000002180700100075d +8040 454e65740400000000001c7c00000000000001004500000002004275746c0000a7000000000000001b00cc0119453a7fffffff010734670125930000000021804001010720 +8023 454e65740400000000001c7c00000000000001004500000002004275746c0000a6000000000000001b00cc0119453a7fffffff010734670125920000000021802301000701 +8083 454e65740400000000001c7c00000000000001004600000002004275746c0000be000000000000001c00cc011a453a7fffffff010734670125a9000000002180830208f60878 +8052 454e65740400000000001c7c00000000000001004500000002004275746c0000ad000000000000001b00cc0119453a7fffffff010734670125990000000021805201010738 +1000 454e65740400000000001c7c00000000000001004500000002004275746c000097000000000000001b00cc0119453a7fffffff01073467012583000000002110000100065f +0081 454e65740400000000001c7c00000000000001005f00000002004275746c000099000000000000003500cc0133453a7fffffff01073467012585000000002100811b496e76656e74726f6e6963732848616e677a686f75292c496e632e1111 +8043 454e65740400000000001c7c00000000000001004500000002004275746c0000aa000000000000001b00cc0119453a7fffffff010734670125960000000021804301ff0824 +8087 454e65740400000000001c7c00000000000001004800000002004275746c0000c1000000000000001e00cc011c453a7fffffff010734670125ac0000000021808704000000d30858 +0050 454e65740400000000001c7c00000000000001004e0000000200017ba8c00000fd010000000000002400cc0122453a7fffffff0899500026c9de000000002100500a020002010081008204030932 +8073 454e65740400000000001c7c00000000000001004500000002004275746c0000b8000000000000001b00cc0119453a7fffffff010734670125a40000000021807301000763 +8082 454e65740400000000001c7c00000000000001004500000002004275746c0000bd000000000000001b00cc0119453a7fffffff010734670125a80000000021808201210797 +0400 454e65740400000000001c7c00000000000001004800000002004275746c00009b000000000000001e00cc011c453a7fffffff010734670125870000000021040004000000c80725 +8080 454e65740400000000001c7c00000000000001004700000002004275746c0000ba000000000000001d00cc011b453a7fffffff010734670125a6000000002180800300006907df +8062 454e65740400000000001c7c00000000000001004500000002004275746c0000b1000000000000001b00cc0119453a7fffffff0107346701259d00000000218062017f07ca +0402 454e65740400000000001c7c00000000000001004800000002004275746c00009d000000000000001e00cc011c453a7fffffff010734670125890000000021040204000933840721 +8020 454e65740400000000001c7c00000000000001004500000002004275746c0000a3000000000000001b00cc0119453a7fffffff0107346701258f00000000218020010006fb diff --git a/samples/rdm_coldpulse_0.hex b/samples/rdm_coldpulse_0.hex new file mode 100644 index 0000000..fafb893 --- /dev/null +++ b/samples/rdm_coldpulse_0.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001003b00000002004275746c0100bc010000000000001100aaaa5dbbddfa55aa55ae77ebddaf57fadf \ No newline at end of file diff --git a/samples/rdm_coldpulse_1.hex b/samples/rdm_coldpulse_1.hex new file mode 100644 index 0000000..93d2e0b --- /dev/null +++ b/samples/rdm_coldpulse_1.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001003b00000002004275746c010036000000000000001100aaab55af57be75ef77ab55af75ae57ebd7 \ No newline at end of file diff --git a/samples/rdm_coldpulse_2.hex b/samples/rdm_coldpulse_2.hex new file mode 100644 index 0000000..0f51d37 --- /dev/null +++ b/samples/rdm_coldpulse_2.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001003b0000000200017ba8c00100fb000000000000001100aaab55af57be75aaddab5daa55ae57efd7 \ No newline at end of file diff --git a/samples/rdm_coldpulse_3.hex b/samples/rdm_coldpulse_3.hex new file mode 100644 index 0000000..a1123d2 --- /dev/null +++ b/samples/rdm_coldpulse_3.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001003b00000002004275746c01009c050000000000001100fefefeffffffddffbfbfbfbfbfbf57efd7 \ No newline at end of file diff --git a/samples/rdm_coldpulse_4.hex b/samples/rdm_coldpulse_4.hex new file mode 100644 index 0000000..0109267 --- /dev/null +++ b/samples/rdm_coldpulse_4.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001003b0000000200017ba8c001001b000000000000001100fefefefefefffffff7f7bfbfff35ddedd7 \ No newline at end of file diff --git a/samples/rdm_disc_req_1126.hex b/samples/rdm_disc_req_1126.hex new file mode 100644 index 0000000..2966fa9 --- /dev/null +++ b/samples/rdm_disc_req_1126.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001003c04000001000000000000000200090f000000000000cc00000001000000000025000000010000000124ffffffffffff453a7ffffffd86000000001000010c089950001767089950001f660e6c050500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000043a0f4f7ec175641812b909bdc99f2ff4000000000010003000000000000000000000000000100030000000000000000000000000074006f006e007900630061006f004400450053004b0054004f0050002d0050004500440032004c00560044002d00660037006600340061003000340033002d0031003700650063002d0034003100350036002d0038003100320062002d0039003000390062006400630039003900660032006600660000000000000000000000911f343cf708bf342e8e519da944bd8a5a4a24433989ea070902001819410343003a005c00550073006500720073005c0074006f006e007900630061006f005c0041007000700044006100740061005c004c006f00630061006c005c0065006300750065005c00530059004d00500048004f004c004900470048005400200035002e0035005c00430075007200720065006e007400530068006f0077000000000000000000000000000000000000000000d00df1e819000000f6d1c94c090000003737b635230000001e00000000000000010000005a00ffffea0709080a1a2245020200000000000000000000000002000000000000000000000000000000000000000000000000010000000100000000000000010000000000000001000000010000002f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a090807060504030201898887868584838281807f7e7d7c7b7a797877767574737271706f6e6d6c6b6a696867666564636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0efeeedecebeae9e8e7e6e5e4e3e2e1e0dfdedddcdbdad9d8d7d6d5d4d3d2d1d0cfcecdcccbcac9c8c7c6c5c4c3c2c1c0bfbebdbcbbbab9b8b7b6b5b4b3b2b1b0afaeadacabaaa9a8a7a6a5a4a3a2a1a09f9e9d9c9b9a999897969594939291908f8e8d8c8b8a898887868584838281807f7e7d7c7b7a797877767574737271706f6e6d6c6b6a696867666564636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a090807060504030201 \ No newline at end of file diff --git a/samples/rdm_info_119.hex b/samples/rdm_info_119.hex new file mode 100644 index 0000000..9df8925 --- /dev/null +++ b/samples/rdm_info_119.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001004d00000002004275746c00007e0f0000000000002300cc0121453a7ffffffd010734670125c00000000021020109000280000000000000071f \ No newline at end of file diff --git a/samples/rdm_pool.txt b/samples/rdm_pool.txt new file mode 100644 index 0000000..18fc463 --- /dev/null +++ b/samples/rdm_pool.txt @@ -0,0 +1,178 @@ +59 454e65740400000000001c7c00000000000001003b00000002004275746c01000b0f0000000000001100aaaa5dbbddfa55aa55ae77ebddaf57fadf +70 454e65740400000000001c7c00000000000001004600000002004275746c00002f0f0000000000001c00cc011a453a7ffffffd0899500026c99900000000110002020000076e +87 454e65740400000000001c7c00000000000001005700000002004275746c0000310f0000000000002d00cc012b453a7ffffffd0899500026c99a0000000021006013010001000100010009050020000000010000040836 +59 454e65740400000000001c7c00000000000001003b00000002004275746c0100790f0000000000001100aaff7fffffaa5feaf5bad7ab7dab57ab77 +77 454e65740400000000001c7c00000000000001004d00000002004275746c00007e0f0000000000002300cc0121453a7ffffffd010734670125c00000000021020109000280000000000000071f +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000800f0000000000002300cc0121453a7ffffffd010734670125c1000000002102010901001b00000000000006ba +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000830f0000000000002300cc0121453a7ffffffd010734670125c200000000210201090208dd08bc094000000893 +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000850f0000000000002300cc0121453a7ffffffd010734670125c30000000021020109030010000505c900000786 +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000870f0000000000002300cc0121453a7ffffffd010734670125c4000000002102010904003200000000000006d7 +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000890f0000000000002300cc0121453a7ffffffd010734670125c500000000210201090500040004087f00000736 +77 454e65740400000000001c7c00000000000001004d00000002004275746c00008b0f0000000000002300cc0121453a7ffffffd010734670125c6000000002102010906000000000000000006a9 +77 454e65740400000000001c7c00000000000001004d00000002004275746c00008d0f0000000000002300cc0121453a7ffffffd010734670125c70000000021020109070019000b0d20000006fc +77 454e65740400000000001c7c00000000000001004d00000002004275746c00008f0f0000000000002300cc0121453a7ffffffd010734670125c8000000002102010908000000000000000006ad +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000910f0000000000002300cc0121453a7ffffffd010734670125c9000000002102010909000b00000000000006ba +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000930f0000000000002300cc0121453a7ffffffd010734670125ca00000000210201090a000000000000000006b1 +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000950f0000000000002300cc0121453a7ffffffd010734670125cb00000000210201090b002900000000000006dc +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000960f0000000000002300cc0121453a7ffffffd010734670125cc00000000210201090c007f0000000000000734 +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000980f0000000000002300cc0121453a7ffffffd010734670125cd00000000210201090d0503002a163200000731 +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000990f0000000000002300cc0121453a7ffffffd010734670125ce00000000210201090e0000000000c700000780 +77 454e65740400000000001c7c00000000000001004d00000002004275746c00009b0f0000000000002300cc0121453a7ffffffd010734670125cf00000000210201090f00000000037e0000073c +77 454e65740400000000001c7c00000000000001004d00000002004275746c00009d0f0000000000002300cc0121453a7ffffffd010734670125d0000000002102010910000000000000000006bd +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000a00f0000000000002300cc0121453a7ffffffd010734670125d1000000002102010911001200000000000006d1 +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000a20f0000000000002300cc0121453a7ffffffd010734670125d2000000002102010912007f0000000000000740 +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000a30f0000000000002300cc0121453a7ffffffd010734670125d3000000002102010913051c0044163b00000779 +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000a60f0000000000002300cc0121453a7ffffffd010734670125d4000000002102010914000000000015000006da +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000a70f0000000000002300cc0121453a7ffffffd010734670125d500000000210201091500000000004200000709 +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000a90f0000000000002300cc0121453a7ffffffd010734670125d6000000002102010916000000000000000006c9 +70 454e65740400000000001c7c00000000000001004600000002004275746c0000ab0f0000000000001c00cc011a453a7ffffffd0899500026c9d7000000002100f002000108ab +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000ac0f0000000000002300cc0121453a7ffffffd0899500026c9d80000000021020109000ce400000000000008bc +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000ae0f0000000000002300cc0121453a7ffffffd0899500026c9d90000000021020109010945000000000000081c +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000b00f0000000000002300cc0121453a7ffffffd0899500026c9da000000002102010902001800000000000007e8 +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000b20f0000000000002300cc0121453a7ffffffd0899500026c9db00000000210201090320db00000000000008cd +70 454e65740400000000001c7c00000000000001004600000002004275746c0000b50f0000000000001c00cc011a453a7ffffffd010734880900dc000000002100f0020070080c +69 454e65740400000000001c7c00000000000001004500000002004275746c0000b70f0000000000001b00cc0119453a7ffffffd010734880900dd00000000218011013c0778 +69 454e65740400000000001c7c00000000000001004500000002004275746c0000b90f0000000000001b00cc0119453a7ffffffd010734880900de000000002180200100074c +69 454e65740400000000001c7c00000000000001004500000002004275746c0000ba0f0000000000001b00cc0119453a7ffffffd010734880900df000000002180210101074f +69 454e65740400000000001c7c00000000000001004500000002004275746c0000bc0f0000000000001b00cc0119453a7ffffffd010734880900e00000000021802201010751 +69 454e65740400000000001c7c00000000000001004500000002004275746c0000be0f0000000000001b00cc0119453a7ffffffd010734880900e10000000021802301000752 +69 454e65740400000000001c7c00000000000001004500000002004275746c0000bf0f0000000000001b00cc0119453a7ffffffd010734880900e20000000021804001010771 +69 454e65740400000000001c7c00000000000001004500000002004275746c0000c10f0000000000001b00cc0119453a7ffffffd010734880900e30000000021804101fe0870 +69 454e65740400000000001c7c00000000000001004500000002004275746c0000c30f0000000000001b00cc0119453a7ffffffd010734880900e40000000021804201ff0873 +69 454e65740400000000001c7c00000000000001004500000002004275746c0000c50f0000000000001b00cc0119453a7ffffffd010734880900e50000000021804301ff0875 +69 454e65740400000000001c7c00000000000001004500000002004275746c0000c70f0000000000001b00cc0119453a7ffffffd010734880900e60000000021805001010785 +69 454e65740400000000001c7c00000000000001004500000002004275746c0000c90f0000000000001b00cc0119453a7ffffffd010734880900e70000000021805101000786 +69 454e65740400000000001c7c00000000000001004500000002004275746c0000cb0f0000000000001b00cc0119453a7ffffffd010734880900e80000000021805201010789 +69 454e65740400000000001c7c00000000000001004500000002004275746c0000ce0f0000000000001b00cc0119453a7ffffffd010734880900e9000000002180530100078a +71 454e65740400000000001c7c00000000000001004700000002004275746c0000cf0f0000000000001d00cc011b453a7ffffffd010734880900ea00000000218060030006d8087a +72 454e65740400000000001c7c00000000000001004800000002004275746c0000d10f0000000000001e00cc011c453a7ffffffd010734880900eb000000002180610400039c2a0869 +69 454e65740400000000001c7c00000000000001004500000002004275746c0000d30f0000000000001b00cc0119453a7ffffffd010734880900ec0000000021806201d80874 +70 454e65740400000000001c7c00000000000001004600000002004275746c0000d50f0000000000001c00cc011a453a7ffffffd010734880900ed0000000021806302005e07fe +70 454e65740400000000001c7c00000000000001004600000002004275746c0000d60f0000000000001c00cc011a453a7ffffffd010734880900ee0000000021806402000007a2 +72 454e65740400000000001c7c00000000000001004800000002004275746c0000d80f0000000000001e00cc011c453a7ffffffd010734880900ef00000000218065040000000007a8 +69 454e65740400000000001c7c00000000000001004500000002004275746c0000da0f0000000000001b00cc0119453a7ffffffd010734880900f000000000218070010007ae +69 454e65740400000000001c7c00000000000001004500000002004275746c0000db0f0000000000001b00cc0119453a7ffffffd010734880900f100000000218071010007b0 +69 454e65740400000000001c7c00000000000001004500000002004275746c0000de0f0000000000001b00cc0119453a7ffffffd010734880900f200000000218072010007b2 +69 454e65740400000000001c7c00000000000001004500000002004275746c0000e00f0000000000001b00cc0119453a7ffffffd010734880900f300000000218073010007b4 +69 454e65740400000000001c7c00000000000001004500000002004275746c0000e10f0000000000001b00cc0119453a7ffffffd010734880900f400000000218074010007b6 +71 454e65740400000000001c7c00000000000001004700000002004275746c0000e30f0000000000001d00cc011b453a7ffffffd010734880900f50000000021808003000075083c +72 454e65740400000000001c7c00000000000001004800000002004275746c0000e50f0000000000001e00cc011c453a7ffffffd010734880900f6000000002180810400082b820880 +69 454e65740400000000001c7c00000000000001004500000002004275746c0000e70f0000000000001b00cc0119453a7ffffffd010734880900f700000000218082011b07e2 +70 454e65740400000000001c7c00000000000001004600000002004275746c0000e90f0000000000001c00cc011a453a7ffffffd010734880900f8000000002180830208fe08d1 +70 454e65740400000000001c7c00000000000001004600000002004275746c0000eb0f0000000000001c00cc011a453a7ffffffd010734880900f90000000021808402000007cd +72 454e65740400000000001c7c00000000000001004800000002004275746c0000ed0f0000000000001e00cc011c453a7ffffffd010734880900fa00000000218085040000000407d7 +72 454e65740400000000001c7c00000000000001004800000002004275746c0000ee0f0000000000001e00cc011c453a7ffffffd010734880900fb00000000218087040000000007d6 +69 454e65740400000000001c7c00000000000001004500000002004275746c0000f10f0000000000001b00cc0119453a7ffffffd010734880900fc000000002180900134080e +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000f20f0000000000002300cc0121453a7ffffffd010734880900fd000000002102010900021700000000000006f7 +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000f40f0000000000002300cc0121453a7ffffffd010734880900fe000000002102010901001b00000000000006fb +77 454e65740400000000001c7c00000000000001004d00000002004275746c0100f60f0000000000002300cc0121453a7ffffffd010734880900ff00000000210201090208fe08cc096100000926 +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000f80f0000000000002300cc0121453a7ffffffd0107348809000000000000210201090300000000024e00000634 +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000fa0f0000000000002300cc0121453a7ffffffd010734880900010000000021020109040034000000000000061a +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000fc0f0000000000002300cc0121453a7ffffffd01073488090002000000002102010905000400040405000005f9 +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000fe0f0000000000002300cc0121453a7ffffffd01073488090003000000002102010906000000000000000005ea +77 454e65740400000000001c7c00000000000001004d00000002004275746c000000100000000000002300cc0121453a7ffffffd0107348809000400000000210201090700000000054e0000063f +77 454e65740400000000001c7c00000000000001004d00000002004275746c000003100000000000002300cc0121453a7ffffffd01073488090005000000002102010908000000000000000005ee +77 454e65740400000000001c7c00000000000001004d00000002004275746c000004100000000000002300cc0121453a7ffffffd0107348809000600000000210201090900630000000000000653 +77 454e65740400000000001c7c00000000000001004d00000002004275746c000007100000000000002300cc0121453a7ffffffd0107348809000700000000210201090a000000000000000005f2 +77 454e65740400000000001c7c00000000000001004d00000002004275746c000008100000000000002300cc0121453a7ffffffd0107348809000800000000210201090b00ec00000000000006e0 +77 454e65740400000000001c7c00000000000001004d00000002004275746c00000a100000000000002300cc0121453a7ffffffd0107348809000900000000210201090c000000000000000005f6 +77 454e65740400000000001c7c00000000000001004d00000002004275746c00000c100000000000002300cc0121453a7ffffffd0107348809000a00000000210201090d005c00081536000006a7 +77 454e65740400000000001c7c00000000000001004d00000002004275746c00000e100000000000002300cc0121453a7ffffffd0107348809000b00000000210201090e00000000057c0000067b +77 454e65740400000000001c7c00000000000001004d00000002004275746c000010100000000000002300cc0121453a7ffffffd0107348809000c00000000210201090f000000001dbb000006d4 +77 454e65740400000000001c7c00000000000001004d00000002004275746c000012100000000000002300cc0121453a7ffffffd0107348809000d000000002102010910000000000000000005fe +69 454e65740400000000001c7c00000000000001004500000002004275746c000014100000000000001b00cc0119453a7ffffffd0107346701250e00000000210403017f065e +69 454e65740400000000001c7c00000000000001004500000002004275746c000016100000000000001b00cc0119453a7ffffffd0899500026c90f00000000210403010106f8 +69 454e65740400000000001c7c00000000000001004500000002004275746c000019100000000000001b00cc0119453a7ffffffd0107348809001000000000210403017f0664 +65 454e65740400000000001c7c00000000000001004100000002004275746c01001f100000000000001700fefefefefefffffffdf5fdeff7ddf7ddd5f775f5d7ffdf +66 454e65740400000000001c7c00000000000001004200000002004275746c010021100000000000001800fefefefffffffcfdddfd7ffbffb5f5aeae7bffdf7f75ffff +66 454e65740400000000001c7c00000000000001004200000002004275746c010023100000000000001800fefefffffdfffff7ba357fd7ddf75d55bbdff7dfffef9ddf +67 454e65740400000000001c7c00000000000001004300000002004275746c010025100000000000001900feffffffffffffffabd5353dff5fabf7ddf57debd7bd75ffff +57 454e65740400000000001c7c00000000000001003900000002004275746c010028100000000000000f00fefeffffffffffffd5ffffffffdfff +59 454e65740400000000001c7c00000000000001003b00000002004275746c01002c100000000000001100aaab55af57be75efffab5deff5aed7ffff +62 454e65740400000000001c7c00000000000001003e00000002004275746c010030100000000000001400fefefefefefefefffeffabffffffffffae57efd7 +66 454e65740400000000001c7c00000000000001004200000002004275746c010032100000000000001800fefefeffffffffffffffff7d7f757ffff7bdfdeff7b5dfff +54 454e65740400000000001c7c00000000000001003600000002004275746c010034100000000000000c00fefe75fffffffbbbbbafdfff +60 454e65740400000000001c7c00000000000001003c00000002004275746c010036100000000000001200fefefefeffff5fffdff57d7ff7ff3fddedd7 +54 454e65740400000000001c7c00000000000001003600000002004275746c010038100000000000000c00fefefffff5fffbfbfbffffd7 +58 454e65740400000000001c7c00000000000001003a00000002004275746c01003e100000000000001000fefefefffdff5ffff7f7fddf7f57efd7 +66 454e65740400000000001c7c00000000000001004200000002004275746c010042100000000000001800fefefefefffffffffffffffdff75ff57f7fddf5f7357efd7 +56 454e65740400000000001c7c00000000000001003800000002004275746c010044100000000000000e00fff57dfdfffffddf7dfd5fff7fff +61 454e65740400000000001c7c00000000000001003d00000002004275746c010046100000000000001300fefefefffffffdb7fdf375f5fffdfbbf57efd7 +60 454e65740400000000001c7c00000000000001003c00000002004275746c010048100000000000001200fefefefefeffffffeed7ffbfbfffcd777bd7 +53 454e65740400000000001c7c00000000000001003500000002004275746c01004c100000000000000b00fefefeffffffffffb5dfff +64 454e65740400000000001c7c00000000000001004000000002004275746c010051100000000000001600fefefefefefefffffffff777f777f7ff7f55ae57efd7 +70 454e65740400000000001c7c00000000000001004600000002004275746c010052100000000000001c00fefefefefefefeffffffffbeffffbfd7bef7badfabfdeb55ae57efd7 +53 454e65740400000000001c7c00000000000001003500000002004275746c010058100000000000000b00febfffbfbfffffffbff5fd +50 454e65740400000000001c7c00000000000001003200000002004275746c01005c100000000000000800fefdfffffffffffd +59 454e65740400000000001c7c00000000000001003b00000002004275746c01005f100000000000001100aaab55af57be75ef77ab55af75ae57ebd7 +70 454e65740400000000001c7c00000000000001004600000002004275746c0000a7100000000000001c00cc011a453a7ffffffd01073467012557000000001100020200000615 +87 454e65740400000000001c7c00000000000001005700000002004275746c0000aa100000000000002d00cc012b453a7ffffffd01073467012558000000002100601301000103050900010001000201010065000017073b +59 454e65740400000000001c7c00000000000001003b00000002004275746c0100be100000000000001100aaab55af57be75aaddab5daa55ae57efd7 +70 454e65740400000000001c7c00000000000001004600000002004275746c000006110000000000001c00cc011a453a7ffffffd0107348809008800000000110002020000064a +87 454e65740400000000001c7c00000000000001005700000002004275746c000008110000000000002d00cc012b453a7ffffffd010734880900890000000021006013010002000509000100010001010100700000110772 +70 454e65740400000000001c7c00000000000001004600000002004275746c0000b6110000000000001c00cc011a453a7ffffffd0899500026c9df0000000011000202000007b4 +87 454e65740400000000001c7c00000000000001005700000002004275746c0000b7110000000000002d00cc012b453a7ffffffd0899500026c9e0000000002100601301000100010001000905002000000001000004087c +70 454e65740400000000001c7c00000000000001004600000002004275746c000006120000000000001c00cc011a453a7ffffffd01073467012508000000002100f00200650729 +69 454e65740400000000001c7c00000000000001004500000002004275746c000008120000000000001b00cc0119453a7ffffffd010734670125090000000021801001010664 +69 454e65740400000000001c7c00000000000001004500000002004275746c00000b120000000000001b00cc0119453a7ffffffd0107346701250a00000000218011013c06a1 +69 454e65740400000000001c7c00000000000001004500000002004275746c00000d120000000000001b00cc0119453a7fbffffd0107346701250b0000000021801301010669 +69 454e65740400000000001c7c00000000000001004500000002004275746c00000d120000000000001b00cc0119453a7ffffffd0107346701250b0000000021801301010669 +69 454e65740400000000001c7c00000000000001004500000002004275746c000010120000000000001b00cc0119453a7ffffffd0107346701250c0000000021802001000676 +69 454e65740400000000001c7c00000000000001004500000002004275746c000011120000000000001b00cc0119453a7ffffffd0107346701250d0000000021802101010679 +69 454e65740400000000001c7c00000000000001004500000002004275746c000013120000000000001b00cc0119453a7ffffffd0107346701250e000000002180220101067b +69 454e65740400000000001c7c00000000000001004500000002004275746c000015120000000000001b00cc0119453a7ffffffd0107346701250f000000002180230100067c +69 454e65740400000000001c7c00000000000001004500000002004275746c000017120000000000001b00cc0119453a7ffffffd01073467012510000000002180400101069b +69 454e65740400000000001c7c00000000000001004500000002004275746c000019120000000000001b00cc0119453a7ffffffd010734670125110000000021804101fe079a +69 454e65740400000000001c7c00000000000001004500000002004275746c00001b120000000000001b00cc0119453a7ffffffd010734670125120000000021804201ff079d +69 454e65740400000000001c7c00000000000001004500000002004275746c00001d120000000000001b00cc0119453a7ffffffd010734670125130000000021804301ff079f +69 454e65740400000000001c7c00000000000001004500000002004275746c00001f120000000000001b00cc0119453a7ffffffd0107346701251400000000218050010106af +69 454e65740400000000001c7c00000000000001004500000002004275746c000021120000000000001b00cc0119453a7ffffffd0107346701251500000000218051010006b0 +69 454e65740400000000001c7c00000000000001004500000002004275746c000023120000000000001b00cc0119453a7ffffffd0107346701251600000000218052010106b3 +69 454e65740400000000001c7c00000000000001004500000002004275746c000025120000000000001b00cc0119453a7ffffffd0107346701251700000000218053010006b4 +71 454e65740400000000001c7c00000000000001004700000002004275746c000027120000000000001d00cc011b453a7ffffffd0107346701251800000000218060030933470749 +72 454e65740400000000001c7c00000000000001004800000002004275746c000029120000000000001e00cc011c453a7ffffffd0107346701251900000000218061040000a36507d2 +69 454e65740400000000001c7c00000000000001004500000002004275746c00002b120000000000001b00cc0119453a7ffffffd0107346701251a00000000218062017f0745 +70 454e65740400000000001c7c00000000000001004600000002004275746c00002e120000000000001c00cc011a453a7ffffffd0107346701251b0000000021806302050306d2 +70 454e65740400000000001c7c00000000000001004600000002004275746c000030120000000000001c00cc011a453a7ffffffd0107346701251c0000000021806402000006cc +72 454e65740400000000001c7c00000000000001004800000002004275746c000031120000000000001e00cc011c453a7ffffffd0107346701251d00000000218065040000000006d2 +69 454e65740400000000001c7c00000000000001004500000002004275746c000033120000000000001b00cc0119453a7ffffffd0107346701251e00000000218070010006d8 +69 454e65740400000000001c7c00000000000001004500000002004275746c000035120000000000001b00cc0119453a7ffffffd0107346701251f00000000218071010006da +69 454e65740400000000001c7c00000000000001004500000002004275746c000037120000000000001b00cc0119453a7ffffffd0107346701252000000000218072010006dc +69 454e65740400000000001c7c00000000000001004500000002004275746c000039120000000000001b00cc0119453a7ffffffd0107346701252100000000218073010006de +69 454e65740400000000001c7c00000000000001004500000002004275746c00003b120000000000001b00cc0119453a7ffffffd0107346701252200000000218074010006e0 +71 454e65740400000000001c7c00000000000001004700000002004275746c00003d120000000000001d00cc011b453a7ffffffd010734670125230000000021808003000069075a +72 454e65740400000000001c7c00000000000001004800000002004275746c000040120000000000001e00cc011c453a7ffffffd0107346701252400000000218081040009c587084a +69 454e65740400000000001c7c00000000000001004500000002004275746c000041120000000000001b00cc0119453a7ffffffd0107346701252500000000218082011b070c +70 454e65740400000000001c7c00000000000001004600000002004275746c000043120000000000001c00cc011a453a7ffffffd01073467012526000000002180830208e507e2 +70 454e65740400000000001c7c00000000000001004600000002004275746c000045120000000000001c00cc011a453a7ffffffd010734670125270000000021808402000b0702 +72 454e65740400000000001c7c00000000000001004800000002004275746c000047120000000000001e00cc011c453a7ffffffd010734670125280000000021808504000000040701 +72 454e65740400000000001c7c00000000000001004800000002004275746c000049120000000000001e00cc011c453a7ffffffd010734670125290000000021808704000000190719 +69 454e65740400000000001c7c00000000000001004500000002004275746c00004b120000000000001b00cc0119453a7ffffffd0107346701252a0000000021809001320736 +77 454e65740400000000001c7c00000000000001004d00000002004275746c00004d120000000000002300cc0121453a7ffffffd0107346701252b0000000021020109000280000000000000068a +77 454e65740400000000001c7c00000000000001004d00000002004275746c00004f120000000000002300cc0121453a7ffffffd0107346701252c000000002102010901001b0000000000000625 +77 454e65740400000000001c7c00000000000001004d00000002004275746c000051120000000000002300cc0121453a7ffffffd0107346701252d00000000210201090208dd08bc0940000007fe +77 454e65740400000000001c7c00000000000001004d00000002004275746c000053120000000000002300cc0121453a7ffffffd0107346701252e0000000021020109030010000505c9000006f1 +77 454e65740400000000001c7c00000000000001004d00000002004275746c000055120000000000002300cc0121453a7ffffffd0107346701252f00000000210201090400320000000000000642 +77 454e65740400000000001c7c00000000000001004d00000002004275746c000057120000000000002300cc0121453a7ffffffd0107346701253000000000210201090500040004087f000006a1 +77 454e65740400000000001c7c00000000000001004d00000002004275746c000059120000000000002300cc0121453a7ffffffd0107346701253100000000210201090600000000000000000614 +77 454e65740400000000001c7c00000000000001004d00000002004275746c00005b120000000000002300cc0121453a7ffffffd010734670125320000000021020109070019000b0d2000000667 +77 454e65740400000000001c7c00000000000001004d00000002004275746c00005d120000000000002300cc0121453a7ffffffd0107346701253300000000210201090800000000000000000618 +77 454e65740400000000001c7c00000000000001004d00000002004275746c00005e120000000000002300cc0121453a7ffffffd010734670125340000000021020109090010000000000000062a +77 454e65740400000000001c7c00000000000001004d00000002004275746c000060120000000000002300cc0121453a7ffffffd0107346701253500000000210201090a0000000000000000061c +77 454e65740400000000001c7c00000000000001004d00000002004275746c000062120000000000002300cc0121453a7ffffffd0107346701253600000000210201090b00290000000000000647 +77 454e65740400000000001c7c00000000000001004d00000002004275746c000064120000000000002300cc0121453a7ffffffd0107346701253700000000210201090c007f000000000000069f +77 454e65740400000000001c7c00000000000001004d00000002004275746c000066120000000000002300cc0121453a7ffffffd0107346701253800000000210201090d04fa002a163200000792 +77 454e65740400000000001c7c00000000000001004d00000002004275746c000068120000000000002300cc0121453a7ffffffd0107346701253900000000210201090e0000000000c7000006eb +77 454e65740400000000001c7c00000000000001004d00000002004275746c00006a120000000000002300cc0121453a7ffffffd0107346701253a00000000210201090f00000000037e000006a7 +77 454e65740400000000001c7c00000000000001004d00000002004275746c00006c120000000000002300cc0121453a7ffffffd0107346701253b00000000210201091000000000000000000628 +77 454e65740400000000001c7c00000000000001004d00000002004275746c00006e120000000000002300cc0121453a7ffffffd0107346701253c0000000021020109110012000000000000063c +77 454e65740400000000001c7c00000000000001004d00000002004275746c000070120000000000002300cc0121453a7ffffffd0107346701253d000000002102010912007f00000000000006ab +77 454e65740400000000001c7c00000000000001004d00000002004275746c000073120000000000002300cc0121453a7ffffffd0107346701253e000000002102010913051c0044163b000006e4 +77 454e65740400000000001c7c00000000000001004d00000002004275746c000074120000000000002300cc0121453a7ffffffd0107346701253f00000000210201091400000000001500000645 +77 454e65740400000000001c7c00000000000001004d00000002004275746c000076120000000000002300cc0121453a7ffffffd0107346701254000000000210201091500000000004200000674 +77 454e65740400000000001c7c00000000000001004d00000002004275746c000078120000000000002300cc0121453a7ffffffd0107346701254100000000210201091600000000000000000634 +70 454e65740400000000001c7c00000000000001004600000002004275746c00007a120000000000001c00cc011a453a7ffffffd0899500026c942000000002100f00200010816 +77 454e65740400000000001c7c00000000000001004d00000002004275746c00007c120000000000002300cc0121453a7ffffffd0899500026c9430000000021020109000ce40000000000000827 +77 454e65740400000000001c7c00000000000001004d00000002004275746c00007f120000000000002300cc0121453a7ffffffd0899500026c94400000000210201090109450000000000000787 +77 454e65740400000000001c7c00000000000001004d00000002004275746c000080120000000000002300cc0121453a7ffffffd0899500026c94500000000210201090200180000000000000753 +77 454e65740400000000001c7c00000000000001004d00000002004275746c000082120000000000002300cc0121453a7ffffffd0899500026c94600000000210201090320dd000000000000083a diff --git a/samples/rdm_pulse_101.hex b/samples/rdm_pulse_101.hex new file mode 100644 index 0000000..e0a4d08 --- /dev/null +++ b/samples/rdm_pulse_101.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001003b00000002004275746c01000b0f0000000000001100aaaa5dbbddfa55aa55ae77ebddaf57fadf \ No newline at end of file diff --git a/samples/rdm_pulse_101_0.hex b/samples/rdm_pulse_101_0.hex new file mode 100644 index 0000000..e0a4d08 --- /dev/null +++ b/samples/rdm_pulse_101_0.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001003b00000002004275746c01000b0f0000000000001100aaaa5dbbddfa55aa55ae77ebddaf57fadf \ No newline at end of file diff --git a/samples/rdm_pulse_101_1.hex b/samples/rdm_pulse_101_1.hex new file mode 100644 index 0000000..a103f91 --- /dev/null +++ b/samples/rdm_pulse_101_1.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001003b00000002004275746c01005f100000000000001100aaab55af57be75ef77ab55af75ae57ebd7 \ No newline at end of file diff --git a/samples/rdm_pulse_101_2.hex b/samples/rdm_pulse_101_2.hex new file mode 100644 index 0000000..911a5fb --- /dev/null +++ b/samples/rdm_pulse_101_2.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001003b00000002004275746c0100be100000000000001100aaab55af57be75aaddab5daa55ae57efd7 \ No newline at end of file diff --git a/samples/rdm_pulse_101_3.hex b/samples/rdm_pulse_101_3.hex new file mode 100644 index 0000000..e89032c --- /dev/null +++ b/samples/rdm_pulse_101_3.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001003b00000002004275746c0100790f0000000000001100aaff7fffffaa5feaf5bad7ab7dab57ab77 \ No newline at end of file diff --git a/samples/rdm_pulse_101_4.hex b/samples/rdm_pulse_101_4.hex new file mode 100644 index 0000000..f1a1f3d --- /dev/null +++ b/samples/rdm_pulse_101_4.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001003b00000002004275746c01002c100000000000001100aaab55af57be75efffab5deff5aed7ffff \ No newline at end of file diff --git a/samples/rdm_req_1126.hex b/samples/rdm_req_1126.hex new file mode 100644 index 0000000..95dfb42 --- /dev/null +++ b/samples/rdm_req_1126.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001003c040000010000000000000002007f0f000000000000cc00000001000100000025000000010000000124ffffffffffff453a7ffffffccc010000001000010c80007ffffffffffffffffffe15c291908f8e8d8c8b8a898887868584838281807f7e7d7c7b7a797877767574737271706f6e6d6c6b6a696867666564636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0efeeedecebeae9e8e7e6e5e4e3e2e1e0dfdedddcdbdad9d8d7d6d5d4d3d2d1d0cfcecdcccbcac9c8c7c6c5c4c3c2c1c0bfbebdbcbbbab9b8b7b6b5b4b3b2b1b0afaeadacabaaa9a8a7a6a5a4a3a2a1a09f9e9d9c9b9a999897969594939291908f8e8d8c8b8a898887868584838281807f7e7d7c7b7a797877767574737271706f6e6d6c6b6a696867666564636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a0908070605040302014a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a090807060504030201c0bfbebdbcbbbab9b8b7b6b5b4b3b2b1b0afaeadacabaaa9a8a7a6a5a4a3a2a1a09f9e9d9c9b9a999897969594939291908f8e8d8c8b8a898887868584838281807f7e7d7c7b7a797877767574737271706f6e6d6c6b6a696867666564636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0efeeedecebeae9e8e7e6e5e4e3e2e1e0dfdedddcdbdad9d8d7d6d5d4d3d2d1d0cfcecdcccbcac9c8c7c6c5c4c3c2c1c0bfbebdbcbbbab9b8b7b6b5b4b3b2b1b0afaeadacabaaa9a8a7a6a5a4a3a2a1a09f9e9d9c9b9a999897969594939291908f8e8d8c8b8a898887868584838281807f7e7d7c7b7a797877767574737271706f6e6d6c6b6a696867666564636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a090807060504030201403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a090807060504030201 \ No newline at end of file diff --git a/samples/rdm_resp_111.hex b/samples/rdm_resp_111.hex new file mode 100644 index 0000000..d4e6827 --- /dev/null +++ b/samples/rdm_resp_111.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001004500000002004275746c0000b70f0000000000001b00cc0119453a7ffffffd010734880900dd00000000218011013c0778 \ No newline at end of file diff --git a/samples/rdm_setack_68.hex b/samples/rdm_setack_68.hex new file mode 100644 index 0000000..22a9f67 --- /dev/null +++ b/samples/rdm_setack_68.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001004400000002004275746c00006efa0600000000001a00cc0118453a7fffffff010734880900b1000000003100f000077f \ No newline at end of file diff --git a/samples/register_89.hex b/samples/register_89.hex new file mode 100644 index 0000000..cbde05c --- /dev/null +++ b/samples/register_89.hex @@ -0,0 +1 @@ +454e65740200000000001c7c00000000000001002f0000000200017ba8c000161cf12e3900717ba8c0300ed559bb36 \ No newline at end of file diff --git a/samples/status_84.hex b/samples/status_84.hex new file mode 100644 index 0000000..d5f2611 --- /dev/null +++ b/samples/status_84.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001002a00000002004275746c010062000000010000000000 diff --git a/src/aech.rs b/src/aech.rs new file mode 100644 index 0000000..b7a8148 --- /dev/null +++ b/src/aech.rs @@ -0,0 +1,1226 @@ +//! AECH 密钥协商密码学分析 (aech 子命令) +//! +//! 已知结构 (§3.7): 主机发 552B sub=05 "AECH" 质询 (0x2c-0x8b 96B + 0x90-0x123 148B +//! 随机 blob, 共 244B), 真机回 552B sub=06 "AERE", 跨轮唯一变化 = 0xC0-0xC3 (4B)。 +//! 核心问题: resp32 = f(challenge, 设备密钥) 是什么函数? +//! +//! 分析策略 (对全部质询/应答对): +//! A. 无密钥直接命中: CRC32(任意切片) / MD5/SHA1(任意切片) 截断 / 简单和 +//! B. 密钥化 CRC 仿射测试: 定长消息下 resp = CRC32_k(slice) = CRC32_0(slice) ^ Δ(len) +//! → 对每个切片起点/长度, 若 resp^CRC32_0(slice) 跨全部样本为常数 Δ 则命中 +//! (Δ 吸收 init/xorout 密钥, 无需事先知道密钥即可判定+恢复有效密钥) +//! C. XOR 常量测试: resp32 ^ chal32@off 跨样本恒定 → 异或掩码方案 +//! D. K 相关: 同一 TSV 的 DMX 保护字节导出会话 K, 与 resp32 字节比对 + +use crate::dmxhash::{crc32, h2b, md5, read_tsv, sha1}; + +// ---------- dmxsend: 主机侧等效播放 (认领 + 加密 DMX, 全协议自证, 2026-09-10) ---------- +// 流程: sub=01 宣告 → 收 89B 注册应答 → AECH 认领 (nonce/B12 自选) → 验证 AERE 0xC0==G(nonce) +// → 30fps 发送 zPprv0TXRRW 加密 DMX (可识别图案), 由接收端日志验证解密正确性 +pub fn dmxsend(target: &str, secs: u64) { + use std::net::UdpSocket; + use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + let sock = UdpSocket::bind("0.0.0.0:0").expect("bind"); + sock.set_read_timeout(Some(Duration::from_millis(400))).unwrap(); + let dst = format!("{}:31772", target); + let mut buf = [0u8; 2048]; + + // 简易 xorshift32 (避免外部依赖) + let mut rng: u32 = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().subsec_nanos() | 1; + let mut next_r = move || { rng ^= rng << 13; rng ^= rng >> 17; rng ^= rng << 5; rng & 0x7FFF_FFFF }; + + // --- 1) sub=01 宣告 → 89B 注册应答 --- + let ann = crate::enet::load_template("announce144"); + sock.send_to(&ann, &dst).unwrap(); + let mut reg_ok = false; + for _ in 0..6 { + if let Ok((n, _)) = sock.recv_from(&mut buf) { + if n == 89 || (n >= 8 && n < 200 && buf[4..8] == [2, 0, 0, 0]) { reg_ok = true; break; } + } else { break; } + } + println!("1) 宣告 sub=01 → 注册应答: {}", if reg_ok { "OK (89B)" } else { "未收到 (不阻塞)" }); + + // --- 2) AECH 认领: nonce/B12 自选, 验证 AERE 0xC0-0xC3 == G(nonce) --- + let nonce: u32 = next_r(); + let b12: u32 = next_r(); + let mut ch = crate::enet::load_template("claim_h112"); + ch[0x88..0x8C].copy_from_slice(&nonce.to_le_bytes()); + ch[0x8C..0x90].copy_from_slice(&b12.to_le_bytes()); + let mut claim_ok = false; + for attempt in 0..3 { + sock.send_to(&ch, &dst).unwrap(); + for _ in 0..6 { + if let Ok((n, src)) = sock.recv_from(&mut buf) { + let b = &buf[..n]; + if n > 0xC4 && b.windows(4).any(|w| w == b"AERE") { + let resp = u32::from_le_bytes([b[0xC0], b[0xC1], b[0xC2], b[0xC3]]); + let want = agios_g(nonce); + claim_ok = resp == want; + println!("2) AECH 认领 (第{}轮): AERE from {} resp=0x{:08x} G(nonce)=0x{:08x} {}", + attempt + 1, src, resp, want, if claim_ok { "★PASS" } else { "FAIL" }); + break; + } + } else { break; } + } + if claim_ok { break; } + } + if !claim_ok { println!("认领失败, 终止"); return; } + println!(" B12=0x{:08x} nonce=0x{:08x}", b12, nonce); + + // --- 3) DMX 加密发送: 30fps, 高识别度图案 --- + let tpl = crate::enet::load_template("dmx_a"); + let mut p1 = [0u8; 512]; + p1[0] = 0xAA; p1[1] = 0x55; + for i in 0..8 { p1[2 + i] = 1u8 << i; } + p1[100] = 0xEF; // ch101 + let mut p2 = [0u8; 512]; + p2[0] = 0x77; p2[511] = 0x33; // ch513 / ch1024 + let expect = "aa550102040810204080"; // 接收端解密后 CH1-10 应为 + println!("3) DMX 发送 {}s @30fps: port1 ch1=AA ch2=55 ch3-10=1..128 ch101=EF; port2 ch513=77 ch1024=33", secs); + println!(" 期望接收端日志 CH1-16 以 {} 开头", expect); + + let start = Instant::now(); + let mut seq: u32 = 1; + let mut sent = 0u64; + while start.elapsed() < Duration::from_secs(secs) { + let r1 = next_r(); + let r2 = next_r(); + let mut f = tpl.clone(); + f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes()); + f[0x22..0x26].copy_from_slice(&r1.to_le_bytes()); + f[0x26..0x2A].copy_from_slice(&r2.to_le_bytes()); + let e1 = agios_dmx_encrypt(&p1, r1, b12); + let e2 = agios_dmx_encrypt(&p2, r2, b12); + f[0x3C..0x23C].copy_from_slice(&e1); + f[0x23C..0x43C].copy_from_slice(&e2); + if sock.send_to(&f, &dst).is_ok() { sent += 1; } + seq += 1; + std::thread::sleep(Duration::from_millis(33)); + } + println!("完成: 发送 {} 帧", sent); +} + +// ---------- AgiosElias: AECH 应答算法 (源自 SymphoLight Ecue.Devices.dll 反编译) ---------- +// 主机验证逻辑 (AgiosElias.i4GrvIBQ6xF): 检查 AERE blob[152] == G(主机 nonce) +// 质询结构 (kxKrvvdOGhh 生成): "AECH" + 92B 随机 + 4B nonce(LE)@0x88 + 4B 实例(LE)@0x8C + 148B 随机 +// G(x) = x ^ TABLE[(x%32 + 1 + (x*13)%30) % 32] — 无密钥, 表内嵌于 DLL (0A8F2B10... 静态数组) +pub const AGIOS_TABLE: [u32; 32] = [ + 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, +]; + +pub fn agios_g(x: u32) -> u32 { + let n = 32u32; + let idx = (x % n + 1 + (x.wrapping_mul(13)) % (n - 2)) % n; + x ^ AGIOS_TABLE[idx as usize] +} + +// ---------- AgiosElias::zPprv0TXRRW — DMX 端口数据逐帧混淆 (旧算法, Butler S2 使用) ---------- +// 反编译 + 数据实证 (2026-09-10, cold2 3893 帧 + fix1 1781 帧全部解密为稳定全零场景): +// rot = (B12 ^ r) & 511 (B12 = AECH 质询 0x8C-0x8F 实例随机, 会话不变; +// r = 帧内 0x22-0x25/0x26-0x29 LE u32, Random.Next() 每帧每端口) +// 加密: out[i] = src[(i+rot)%512] ^ cnt[i] +// cnt[i] = (512-rot-i)&255 (i < 512-rot) +// = (rot-(i-(512-rot)))&255 (i >= 512-rot) +/// zPprv0TXRRW 加密 (主机侧): src 512B → wire 512B +pub fn agios_dmx_encrypt(src: &[u8], r: u32, b12: u32) -> Vec { + let rot = ((b12 ^ r) & 511) as usize; + (0..512).map(|i| { + let cnt = if i < 512 - rot { (512 - rot - i) & 0xFF } else { (rot - (i - (512 - rot))) & 0xFF }; + src[(i + rot) % 512] ^ cnt as u8 + }).collect() +} +/// zPprv0TXRRW 解密 (接收方视角): wire 512B → src 512B +pub fn agios_dmx_decrypt(wire: &[u8], r: u32, b12: u32) -> Vec { + let rot = ((b12 ^ r) & 511) as usize; + let mut src = vec![0u8; 512]; + for i in 0..512 { + let cnt = if i < 512 - rot { (512 - rot - i) & 0xFF } else { (rot - (i - (512 - rot))) & 0xFF }; + src[(i + rot) % 512] = wire[i] ^ cnt as u8; + } + src +} + +// ---------- dmxdec: DMX 帧加密假说验证 (2026-09-10 反编译突破) ---------- +// 反编译实证: iey7k4rDmrsMD1TvRywm::ScheduleUpdate 是唯一 1084B 帧构建者; +// 帧 0x22-0x25 = BBFrDpcLsuZ[0] = EncryptFrameData 返回值 r1 (Random.Next(), MSB=0 已实证), +// 0x26-0x29 = r2。若走加密路径 (jTErDRqkhb1()==true), 通道数据经: +// kWarv47o79k (新): out[i] = src[i] ^ ((512-i)&255) ^ key[(512-i)&3], key = r ^ B12rvMuR8pT +// zPprv0TXRRW (旧): out[i] = src[(i+rot)%512] ^ cnt[i], rot = (B12 ^ r) & 511 +// B12rvMuR8pT = AgiosElias 实例随机 = AECH 质询 0x8C-0x8F (会话内不变)。 +// 验证: 用 r (帧内 0x22-0x25) + B12 (TSV 内 AECH 质询) 解密, 若明文跨帧稳定/稀疏 → 假说成立。 +pub fn dmxdec(tsv: &str) { + let content = read_tsv(tsv); + let mut b12: Option = None; + let mut frames: Vec> = Vec::new(); + for line in content.lines() { + let p: Vec<&str> = line.split('\t').collect(); + if p.len() < 5 || p[4].is_empty() { continue; } + let b = match h2b(p[4]) { Some(b) => b, None => continue }; + if b.len() == 552 && &b[0x28..0x2C] == b"AECH" && b12.is_none() { + b12 = Some(u32::from_le_bytes([b[0x8C], b[0x8D], b[0x8E], b[0x8F]])); + } + if b.len() == 1084 && b[0x20] == 0x01 && b[0x21] == 0x01 { + frames.push(b); + } + } + let b12 = match b12 { Some(v) => v, None => { println!("TSV 内无 AECH 质询, 无法取 B12"); return; } }; + if frames.is_empty() { println!("无 DMX 帧"); return; } + println!("B12 = 0x{:08x}, DMX 帧 {} 个", b12, frames.len()); + + // --- kWarv47o79k 逆运算 (XOR 自逆): src[i] = out[i] ^ ((512-i)&255) ^ key[(512-i)&3] --- + let dec_new = |b: &Vec, off: usize, r: u32| -> Vec { + let key = r ^ b12; + let kb = key.to_le_bytes(); + (0..512).map(|i| { + let j = 512 - i; + b[off + i] ^ (j & 0xFF) as u8 ^ kb[j & 3] + }).collect() + }; + // --- zPprv0TXRRW 逆运算 (共享实现, 见 agios_dmx_decrypt) --- + let dec_old = |b: &Vec, off: usize, r: u32| -> Vec { + agios_dmx_decrypt(&b[off..off + 512], r, b12) + }; + + for (name, is_new) in [("kWarv47o79k(新)", true), ("zPprv0TXRRW(旧)", false)] { + // 解密前 3 帧端口1, 看明文是否稳定/稀疏 + let mut stable = 0; + let mut prev: Option> = None; + let mut first_plain: Vec = Vec::new(); + for (fi, f) in frames.iter().take(50).enumerate() { + let r1 = u32::from_le_bytes([f[0x22], f[0x23], f[0x24], f[0x25]]); + let plain = if is_new { dec_new(f, 0x3C, r1) } else { dec_old(f, 0x3C, r1) }; + if fi == 0 { first_plain = plain.clone(); } + if let Some(p) = &prev { + if *p == plain { stable += 1; } + } + prev = Some(plain); + } + let nonzero = first_plain.iter().filter(|&&v| v != 0).count(); + println!("\n{}: 相邻帧明文相同 {}/49, 首帧明文非零字节 {}/512", name, stable, nonzero); + print!(" 首帧明文 CH1-32: "); + for v in first_plain.iter().take(32) { print!("{:02x}", v); } + println!(); + } + + // --- 全捕获扫描: 双端口解密, 统计非零明文帧 (用户是否发过真实场景) --- + // --- 同时做 encrypt∘decrypt 回环验证 (主机侧复现的正确性) --- + let mut nz_frames = 0; + let mut roundtrip_ok = 0usize; + let mut sample: Option<(usize, Vec, Vec)> = None; + for (fi, f) in frames.iter().enumerate() { + let r1 = u32::from_le_bytes([f[0x22], f[0x23], f[0x24], f[0x25]]); + let r2 = u32::from_le_bytes([f[0x26], f[0x27], f[0x28], f[0x29]]); + let p1 = agios_dmx_decrypt(&f[0x3C..0x3C + 512], r1, b12); + let p2 = agios_dmx_decrypt(&f[0x23C..0x23C + 512], r2, b12); + if agios_dmx_encrypt(&p1, r1, b12) == f[0x3C..0x3C + 512] + && agios_dmx_encrypt(&p2, r2, b12) == f[0x23C..0x23C + 512] + { + roundtrip_ok += 1; + } + let nz = p1.iter().chain(p2.iter()).filter(|&&v| v != 0).count(); + if nz > 0 { + nz_frames += 1; + if sample.is_none() { sample = Some((fi, p1, p2)); } + } + } + println!("\n全捕获双端口解密: 非零明文帧 {}/{}, 加解密回环 {}/{}", nz_frames, frames.len(), roundtrip_ok, frames.len()); + if let Some((fi, p1, p2)) = sample { + let show = |v: &Vec| -> String { + v.iter().enumerate().filter(|(_, &x)| x != 0).take(16) + .map(|(i, &x)| format!("ch{}={}", i + 1, x)).collect::>().join(" ") + }; + println!(" 首个非零帧 #{}: port1[{}] port2[{}]", fi, show(&p1), show(&p2)); + } +} + +// ---------- dmxroll: DMX 帧滚动值/保护字节数据驱动分析 ---------- +// 帧布局 (§4.1): seq@0x1A LE u16, guard1@0x22=(CH1-1)^K, roll1@0x23-0x25, +// guard2@0x26=(CH513-1)^K, roll2@0x27-0x29, node@0x2E +// 检验: roll 是否 = G(seq^K) 之类的公开函数; roll1/roll2 关系; 跨帧步进规律 +pub fn dmxroll(tsv: &str) { + let content = read_tsv(tsv); + let mut frames: Vec<(u16, u8, [u8; 3], u8, [u8; 3], u8, u8)> = Vec::new(); // seq,g1,r1,g2,r2,node,ch1 + for line in content.lines() { + let p: Vec<&str> = line.split('\t').collect(); + if p.len() < 5 || p[4].is_empty() { continue; } + let b = match h2b(p[4]) { Some(b) => b, None => continue }; + if b.len() != 1084 || b[0x20] != 0x01 || b[0x21] != 0x01 { continue; } + let seq = u16::from_le_bytes([b[0x1A], b[0x1B]]); + frames.push((seq, b[0x22], [b[0x23], b[0x24], b[0x25]], b[0x26], [b[0x27], b[0x28], b[0x29]], b[0x2E], b[0x3C])); + } + if frames.is_empty() { println!("无 DMX 帧"); return; } + println!("DMX 帧 {} 个", frames.len()); + + // K 导出 (guard1 ^ (CH1-1)) + let k = frames[0].1 ^ frames[0].6.wrapping_sub(1); + println!("会话 K = 0x{:02x}", k); + + // roll1 vs roll2 关系 + let eq = frames.iter().filter(|f| f.2 == f.4).count(); + println!("roll1==roll2: {}/{}", eq, frames.len()); + + // roll 是否随 seq 确定性变化: 同 seq 是否同 roll + let mut by_seq: std::collections::HashMap> = std::collections::HashMap::new(); + for f in &frames { by_seq.entry(f.0).or_default().push((f.2, f.4)); } + let mut det = 0; let mut nondet = 0; + for (_, v) in &by_seq { + if v.iter().all(|x| *x == v[0]) { det += 1; } else { nondet += 1; } + } + println!("同seq同roll(确定性): {} seqs; 同seq异roll(含随机): {} seqs", det, nondet); + + // 前 16 帧明细 + println!("\nseq | g1 | roll1 | g2 | roll2 | node | G(seq)^? 对照"); + for f in frames.iter().take(16) { + let g = agios_g(f.0 as u32); + println!("{:06} | {:02x} | {:02x}{:02x}{:02x} | {:02x} | {:02x}{:02x}{:02x} | {:02x} | G(seq)={:08x} G(seq^K)={:08x}", + f.0, f.1, f.2[0], f.2[1], f.2[2], f.3, f.4[0], f.4[1], f.4[2], f.5, g, agios_g((f.0 as u32) ^ (k as u32))); + } + + // roll1 24bit 与 seq 的直接关系扫描: roll1 ^ seq / roll1 - seq 恒定? + let r1_24 = |f: &(u16, u8, [u8; 3], u8, [u8; 3], u8, u8)| -> + u32 { (f.2[0] as u32) | (f.2[1] as u32) << 8 | (f.2[2] as u32) << 16 }; + let x0 = r1_24(&frames[0]) ^ frames[0].0 as u32; + let xor_const = frames.iter().all(|f| (r1_24(f) ^ f.0 as u32) == x0); + let d0 = r1_24(&frames[0]).wrapping_sub(frames[0].0 as u32); + let sub_const = frames.iter().all(|f| r1_24(f).wrapping_sub(f.0 as u32) == d0); + println!("\nroll1^seq 恒定: {} (0x{:06x}); roll1-seq 恒定: {} (0x{:06x})", xor_const, x0, sub_const, d0); + + // roll1 逐帧差分规律 + let mut diffs: std::collections::HashMap = std::collections::HashMap::new(); + for w in frames.windows(2) { + if w[1].0 == w[0].0.wrapping_add(1) { + let d = r1_24(&w[1]).wrapping_sub(r1_24(&w[0])); + *diffs.entry(d).or_default() += 1; + } + } + let mut dv: Vec<_> = diffs.iter().collect(); + dv.sort_by_key(|(_, c)| std::cmp::Reverse(**c)); + println!("roll1 逐帧差分 top5: {:?}", dv.iter().take(5).map(|(d, c)| format!("0x{:06x}x{}", d, c)).collect::>()); + + // G(seq), G(seq)^K, G(seq+k) 等与 roll 的匹配率 + let test_fn = |name: &str, f: &dyn Fn(u16) -> u32| { + let hit = frames.iter().filter(|fr| { + let v = f(fr.0); + let lo3 = [v as u8, (v >> 8) as u8, (v >> 16) as u8]; + let hi3 = [(v >> 8) as u8, (v >> 16) as u8, (v >> 24) as u8]; + lo3 == fr.2 || hi3 == fr.2 + }).count(); + println!(" {} 匹配 roll1: {}/{}", name, hit, frames.len()); + }; + println!("\n候选函数 vs roll1:"); + test_fn("G(seq)", &|s| agios_g(s as u32)); + test_fn("G(seq^K)", &|s| agios_g((s as u32) ^ (k as u32))); + test_fn("G(seq+K)", &|s| agios_g((s as u32).wrapping_add(k as u32))); + test_fn("G(seq)^G(K)", &|s| agios_g(s as u32) ^ agios_g(k as u32)); + test_fn("(seq^K)*7", &|s| ((s as u32) ^ (k as u32)).wrapping_mul(7)); + + // --- roll2(seq) == roll1(seq+512)? (端口2 = 端口1 偏移 512) --- + let map1: std::collections::HashMap = frames.iter().map(|f| (f.0, f.2)).collect(); + let mut off_hit = 0; let mut off_tot = 0; + for f in &frames { + if let Some(r) = map1.get(&f.0.wrapping_add(512)) { + off_tot += 1; + if *r == f.4 { off_hit += 1; } + } + } + println!("\nroll2(seq) == roll1(seq+512): {}/{}", off_hit, off_tot); + + // --- PRNG 流分析 (按到达顺序; seq 不重复, "同seq同roll" 判定无效) --- + let r1s: Vec = frames.iter().map(|f| (f.2[0] as u32) | (f.2[1] as u32) << 8 | (f.2[2] as u32) << 16).collect(); + let r2s: Vec = frames.iter().map(|f| (f.4[0] as u32) | (f.4[1] as u32) << 8 | (f.4[2] as u32) << 16).collect(); + // 交叉: roll2[i] == roll1[i+1]? (共享 PRNG, 端口2 取下一个) + let cross = (0..r1s.len() - 1).filter(|&i| r2s[i] == r1s[i + 1]).count(); + println!("roll2[i]==roll1[i+1]: {}/{}", cross, r1s.len() - 1); + + // LCG mod 2^24 求解: r1 = (a*r0+c) mod 2^24 — 从 3 个连续值解 a,c 并全验证 + let solve_lcg = |v: &[u32]| { + let m = 1u64 << 24; + let mut hits = 0; + for start in 0..v.len().saturating_sub(3).min(200) { + let (r0, r1, r2) = (v[start] as u64, v[start + 1] as u64, v[start + 2] as u64); + let d1 = (r1 + m - r0) % m; // r1-r0 + let d2 = (r2 + m - r1) % m; // r2-r1 + if d1 % 2 == 0 { continue; } // 不可逆, 跳过 + // d1 的模逆 (牛顿迭代, mod 2^24) + let mut inv = d1; + for _ in 0..5 { inv = inv.wrapping_mul(2u64.wrapping_sub(d1.wrapping_mul(inv))) % m; } + let a = d2 * inv % m; + let c = (r1 + m - a * r0 % m) % m; + let mut ok = true; + let mut cur = v[start] as u64; + for j in start + 1..v.len().min(start + 500) { + cur = (a * cur + c) % m; + if cur != v[j] as u64 { ok = false; break; } + } + if ok { + println!(" ★ LCG(2^24) 命中 @{}: a=0x{:06x} c=0x{:06x}", start, a, c); + hits += 1; + if hits > 3 { return; } + } + } + if hits == 0 { println!(" LCG(2^24) 无命中"); } + }; + println!("roll1 流 LCG 测试:"); solve_lcg(&r1s); + println!("roll2 流 LCG 测试:"); solve_lcg(&r2s); + + // LCG mod 2^32 (4B 状态, roll 为低 24 位?): 用 r1s 与 r2s 交错序列试 + // 交错序列: roll1[0], roll2[0], roll1[1], roll2[1], ... + let inter: Vec = r1s.iter().zip(r2s.iter()).flat_map(|(a, b)| [*a, *b]).collect(); + println!("交错流 LCG 测试:"); solve_lcg(&inter); + + // xorshift 粗测: r[i+1] ^ r[i] 的汉明重量分布 + let hw: Vec = r1s.windows(2).map(|w| (w[1] ^ w[0]).count_ones()).collect(); + let avg = hw.iter().sum::() as f64 / hw.len() as f64; + println!("roll1 相邻 XOR 平均汉明重: {:.1} (随机期望 12.0)", avg); + + // --- K vs AECH 实例随机 (0x8C-0x8F) / 首个 nonce (0x88-0x8B) 相关测试 --- + // (仅含认领交换的 TSV 有效, 如 cold2/fix1) + let mut inst: Option = None; + let mut nonce1: Option = None; + { + let content2 = read_tsv(tsv); + for line in content2.lines() { + let p: Vec<&str> = line.split('\t').collect(); + if p.len() < 5 || p[4].is_empty() { continue; } + let b = match h2b(p[4]) { Some(b) => b, None => continue }; + if b.len() == 552 && &b[0x28..0x2C] == b"AECH" { + if inst.is_none() { inst = Some(u32::from_le_bytes([b[0x8C], b[0x8D], b[0x8E], b[0x8F]])); } + if nonce1.is_none() { nonce1 = Some(u32::from_le_bytes([b[0x88], b[0x89], b[0x8A], b[0x8B]])); } + if inst.is_some() && nonce1.is_some() { break; } + } + } + } + if let (Some(iv), Some(nv)) = (inst, nonce1) { + println!("\nAECH 实例=0x{:08x} nonce1=0x{:08x} vs K=0x{:02x}", iv, nv, k); + let tests: Vec<(&str, u32)> = vec![ + ("G(inst)", agios_g(iv)), + ("G(nonce1)", agios_g(nv)), + ("(inst^nonce1)*7", (iv ^ nv).wrapping_mul(7)), + ("inst*7", iv.wrapping_mul(7)), + ("G(inst)^G(nonce1)", agios_g(iv) ^ agios_g(nv)), + ("G(inst^nonce1)", agios_g(iv ^ nv)), + ("G(inst+nonce1)", agios_g(iv.wrapping_add(nv))), + ]; + for (name, v) in tests { + let hit = (0..4).any(|s| ((v >> (s * 8)) & 0xFF) == k as u32); + println!(" {}=0x{:08x} {}", name, v, if hit { "★ 含 K 字节!" } else { "" }); + } + } + + // --- roll1(seq) 的 GF(2) 仿射性检验 (16bit 输入空间) --- + // 仿射 iff 对任意 a,b,c: roll(a)^roll(b)^roll(c) == roll(a^b^c) + { + let keys: Vec = map1.keys().cloned().collect(); + let mut rng = 0x12345678u32; + let mut ok = 0; let mut tot = 0; + for _ in 0..2000 { + rng = rng.wrapping_mul(1664525).wrapping_add(1013904223); + let a = keys[(rng >> 8) as usize % keys.len()]; + rng = rng.wrapping_mul(1664525).wrapping_add(1013904223); + let b = keys[(rng >> 8) as usize % keys.len()]; + rng = rng.wrapping_mul(1664525).wrapping_add(1013904223); + let c = keys[(rng >> 8) as usize % keys.len()]; + if let Some(abc) = map1.get(&(a ^ b ^ c)) { + tot += 1; + let (ra, rb, rc) = (map1[&a], map1[&b], map1[&c]); + let x = [ra[0] ^ rb[0] ^ rc[0], ra[1] ^ rb[1] ^ rc[1], ra[2] ^ rb[2] ^ rc[2]]; + if x == *abc { ok += 1; } + } + } + println!("roll1(seq) 仿射性: {}/{} 组通过", ok, tot); + if tot > 0 && ok == tot { + // 解仿射映射: roll(s) = M·s ^ c, 用单比特样本解 16 列 + let zero = map1.get(&0).cloned(); + let ref_pt = keys[0]; + let c0 = map1[&ref_pt]; + let mut cols: Vec<[u8; 3]> = Vec::new(); + let mut solved = true; + for bit in 0..16 { + let want = ref_pt ^ (1u16 << bit); + match map1.get(&want) { + Some(r) => cols.push([r[0] ^ c0[0], r[1] ^ c0[1], r[2] ^ c0[2]]), + None => { solved = false; break; } + } + } + if solved { + println!(" 仿射映射已解出 (参考点 seq={})", ref_pt); + // 全样本验证 + let predict = |s: u16| -> [u8; 3] { + let d = s ^ ref_pt; + let mut r = c0; + for bit in 0..16 { + if d & (1 << bit) != 0 { + r[0] ^= cols[bit][0]; r[1] ^= cols[bit][1]; r[2] ^= cols[bit][2]; + } + } + r + }; + let hit = frames.iter().filter(|f| predict(f.0) == f.2).count(); + println!(" 全样本预测 roll1: {}/{}", hit, frames.len()); + if let Some(z) = zero { println!(" roll(0) = {:02x}{:02x}{:02x}", z[0], z[1], z[2]); } + } else { + println!(" 单比特样本不足, 未解 (捕获窗口内 seq 范围有限)"); + } + } + } +} + +// ---------- 选择密文探测 (aechprobe 子命令): 对真机做 GF(2) 仿射性分析 ---------- +// +// 原理: 若 resp32 = f(challenge) 是 GF(2) 仿射函数 (CRC 类/线性反馈类算法的共性), +// 则 f(x) = L(x) ^ c, 其中 c = f(0), L 为线性映射。用 1952 次单比特翻转查询 +// 可完整学习 L 的矩阵 → 无需密钥即可对任意质询离线计算合法应答。 +// 若不是仿射 (真 MAC), 线性验证步骤会立即暴露。 + +use std::net::UdpSocket; +use std::time::Duration; + +/// 发送一帧质询, 返回应答 0xC0-0xC3 (u32 原生字节序保持 4B 数组) +fn query(s: &UdpSocket, target: &str, frame: &[u8]) -> Option<[u8; 4]> { + for _attempt in 0..2 { + if s.send_to(frame, target).is_err() { return None; } + let mut buf = [0u8; 2048]; + match s.recv_from(&mut buf) { + Ok((n, _)) => { + let b = &buf[..n]; + if n == 552 && &b[0x28..0x2C] == b"AERE" { + return Some([b[0xC0], b[0xC1], b[0xC2], b[0xC3]]); + } + } + Err(_) => continue, + } + } + None +} + +pub fn probe(tsv: &str, target: &str) { + // 从 TSV 取首个 AECH 质询作模板 + let content = read_tsv(tsv); + let mut tmpl: Option> = None; + let mut tmpl_resp: Option<[u8; 4]> = None; + for line in content.lines() { + let p: Vec<&str> = line.split('\t').collect(); + if p.len() < 5 || p[4].is_empty() { continue; } + let b = match h2b(p[4]) { Some(b) => b, None => continue }; + if b.len() == 552 && &b[0..4] == b"ENet" && &b[0x28..0x2C] == b"AECH" && tmpl.is_none() { + tmpl = Some(b.clone()); + } + if b.len() == 552 && &b[0x28..0x2C] == b"AERE" && tmpl_resp.is_none() { + tmpl_resp = Some([b[0xC0], b[0xC1], b[0xC2], b[0xC3]]); + } + if tmpl.is_some() && tmpl_resp.is_some() { break; } + } + let tmpl = tmpl.expect("TSV 中无 AECH 质询模板"); + println!("模板质询已载入, 抓包时应答 resp32={}", tmpl_resp.map_or("?".into(), |r| hexr(&r))); + + let s = UdpSocket::bind("0.0.0.0:0").expect("bind 失败"); + s.set_read_timeout(Some(Duration::from_millis(300))).ok(); + let tgt = format!("{}:31772", target); + + // --- 阶段 1: 确定性验证 (原样重放 ×3) --- + println!("\n--- 阶段 1: 确定性 (原样重放×3) ---"); + let mut base: Option<[u8; 4]> = None; + for i in 0..3 { + match query(&s, &tgt, &tmpl) { + Some(r) => { + println!(" 重放#{}: resp32={}", i + 1, hexr(&r)); + if let Some(b0) = base { if b0 != r { println!(" ✗ 不确定性! 含设备侧随机"); return; } } + base = Some(r); + } + None => { println!(" 重放#{}: 无应答 — 模板被拒?", i + 1); return; } + } + } + let base = base.unwrap(); + if let Some(tr) = tmpl_resp { + println!(" 与抓包值 {} => {}", hexr(&tr), if tr == base { "一致 ✓ (完全确定)" } else { "不同 ✗" }); + } + + // --- 阶段 2: 输入域结构 (哪些区域影响 resp) --- + println!("\n--- 阶段 2: 输入域探测 ---"); + let probe_region = |name: &str, f: &mut dyn FnMut(&mut Vec)| { + let mut m = tmpl.clone(); + f(&mut m); + match query(&s, &tgt, &m) { + Some(r) => println!(" {}: resp32={} {}", name, hexr(&r), + if r == base { "(=基线, 不影响)" } else { "(≠基线, ★影响)" }), + None => println!(" {}: 无应答", name), + } + std::thread::sleep(Duration::from_millis(5)); + }; + probe_region("全 blob 清零 (0x2C-0x123)", &mut |m| { for i in 0x2C..0x124 { m[i] = 0; } }); + probe_region("blob 全 FF", &mut |m| { for i in 0x2C..0x124 { m[i] = 0xFF; } }); + probe_region("仅 part1 清零 (0x2C-0x8B)", &mut |m| { for i in 0x2C..0x8C { m[i] = 0; } }); + probe_region("仅 part2 清零 (0x90-0x123)", &mut |m| { for i in 0x90..0x124 { m[i] = 0; } }); + probe_region("静态缝 0x8C-0x8F 翻bit0", &mut |m| { m[0x8C] ^= 1; }); + probe_region("头部 0x00 翻bit", &mut |m| { m[0x00] ^= 1; }); + probe_region("长度域 0x14 翻bit", &mut |m| { m[0x14] ^= 1; }); + probe_region("主机IP 0x1A 翻bit", &mut |m| { m[0x1A] ^= 1; }); + probe_region("主机MAC 0x1E 翻bit", &mut |m| { m[0x1E] ^= 1; }); + probe_region("0x24 翻bit", &mut |m| { m[0x24] ^= 1; }); + probe_region("尾字节 0x227 翻bit", &mut |m| { m[0x227] ^= 1; }); + + // --- 阶段 3: 仿射性抽样验证 --- + // f(a)^f(b) == f(a^b)^f(0) 对随机 a,b 成立 ⇒ 仿射 + println!("\n--- 阶段 3: 仿射性抽样验证 (8 组) ---"); + let mut seed = 0x9e3779b9u32; + let mut rnd = move || { seed = seed.wrapping_mul(2654435761).wrapping_add(0x9e3779b9); seed }; + // f(0): blob 全零 + let mut z = tmpl.clone(); + for i in 0x2C..0x124 { z[i] = 0; } + let f0 = match query(&s, &tgt, &z) { + Some(r) => r, + None => { println!(" 零 blob 无应答, 仿射验证跳过"); return; } + }; + println!(" f(零blob) = {}", hexr(&f0)); + let mut affine_ok = 0; + for t in 0..8 { + let mut a = tmpl.clone(); let mut b = tmpl.clone(); + for i in 0x2C..0x124 { + if rnd() & 1 != 0 { a[i] ^= 1 << ((rnd() >> 8) & 7); } + if rnd() & 1 != 0 { b[i] ^= 1 << ((rnd() >> 8) & 7); } + } + let mut axb = a.clone(); + for i in 0x2C..0x124 { axb[i] = a[i] ^ b[i] ^ tmpl[i]; } // axb = a^b (相对模板) + // 需要绝对: a^b 的 blob = a.blob ^ b.blob ^ 0? 直接用 a.blob ^ b.blob 作为新 blob + for i in 0x2C..0x124 { axb[i] = a[i] ^ b[i]; } + let fa = query(&s, &tgt, &a); + let fb = query(&s, &tgt, &b); + let faxb = query(&s, &tgt, &axb); + std::thread::sleep(Duration::from_millis(5)); + match (fa, fb, faxb) { + (Some(fa), Some(fb), Some(faxb)) => { + // f(a)^f(b)^f(a^b) 应 == f(0) (仿射) + let mut lhs = [0u8; 4]; + for i in 0..4 { lhs[i] = fa[i] ^ fb[i] ^ faxb[i]; } + let ok = lhs == f0; + if ok { affine_ok += 1; } + println!(" 组{}: f(a)^f(b)^f(a^b)={} vs f(0)={} {}", t + 1, hexr(&lhs), hexr(&f0), + if ok { "✓" } else { "✗ 非仿射!" }); + } + _ => println!(" 组{}: 有无应答缺失, 跳过", t + 1), + } + } + if affine_ok < 8 { + println!("\n仿射性未全过 ({}/8) — 大概率为真 MAC (HMAC/AES 类), 样本无法离线伪造", affine_ok); + println!("退路: 继续中继方案, 或分析固件/dongle 算法"); + return; + } + + // --- 阶段 4: 全量单比特翻转, 学习线性映射矩阵 --- + println!("\n--- 阶段 4: 学习线性映射 (1952 比特 × 1 查询) ---"); + // L(e_i) = f(e_i) ^ f(0), 对 blob 0x2C..0x124 的每比特 + let nbits = (0x124 - 0x2C) * 8; + let mut cols: Vec<[u8; 4]> = Vec::with_capacity(nbits); + let mut fails = 0; + for bit in 0..nbits { + let off = 0x2C + bit / 8; + let mut m = z.clone(); // 基于零 blob + m[off] = 1 << (bit % 8); + match query(&s, &tgt, &m) { + Some(r) => { + let mut c = [0u8; 4]; + for i in 0..4 { c[i] = r[i] ^ f0[i]; } + cols.push(c); + } + None => { fails += 1; cols.push([0; 4]); } + } + if bit % 200 == 0 { println!(" 进度 {}/{} (失败 {})", bit, nbits, fails); } + std::thread::sleep(Duration::from_millis(3)); + } + println!(" 矩阵学习完成: {} 列, 失败 {}", cols.len(), fails); + + // 验证: 对模板质询用矩阵计算, 应得 base + let mut calc = f0; + for bit in 0..nbits { + let off = 0x2C + bit / 8; + if tmpl[off] & (1 << (bit % 8)) != 0 { + for i in 0..4 { calc[i] ^= cols[bit][i]; } + } + } + println!(" 矩阵验证: 计算={} vs 实测={} {}", hexr(&calc), hexr(&base), + if calc == base { "✓✓ 仿射模型成立!" } else { "✗" }); + + // 保存矩阵 (f0 + 列) 供 .7 离线应答用 + let mut out = String::new(); + out.push_str(&format!("# AECH 仿射映射: resp32 = f0 ^ XOR_{{blob bit=1}} col[bit] (目标 {})\n", target)); + out.push_str(&format!("f0={}\n", hexr(&f0))); + for (i, c) in cols.iter().enumerate() { + out.push_str(&format!("{}\n", hexr(c))); + let _ = i; + } + let path = "aech_affine_map.txt"; + std::fs::write(path, &out).ok(); + println!(" 映射已保存: {} ({} 列)", path, cols.len()); +} + +// ---------- probe2: 96B part1 的块/字节影响图谱 ---------- +// resp32 = f(chal[0x2C..0x8C]) 已实证 (probe 阶段2)。96B = 12×8B 分组密码直觉。 +// 测绘: 每字节翻转 1bit 看 resp 雪崩模式; 每 8B 块清零看是否影响; 换序测试。 +pub fn probe2(tsv: &str, target: &str) { + let content = read_tsv(tsv); + let mut tmpl: Option> = None; + for line in content.lines() { + let p: Vec<&str> = line.split('\t').collect(); + if p.len() < 5 || p[4].is_empty() { continue; } + let b = match h2b(p[4]) { Some(b) => b, None => continue }; + if b.len() == 552 && &b[0..4] == b"ENet" && &b[0x28..0x2C] == b"AECH" { + tmpl = Some(b); break; + } + } + let tmpl = tmpl.expect("无模板"); + let s = UdpSocket::bind("0.0.0.0:0").expect("bind 失败"); + s.set_read_timeout(Some(Duration::from_millis(300))).ok(); + let tgt = format!("{}:31772", target); + + let base = match query(&s, &tgt, &tmpl) { + Some(r) => r, + None => { println!("基线无应答"); return; } + }; + println!("基线 resp32={}", hexr(&base)); + + // --- 字节影响图谱: part1 每字节翻 bit0 --- + println!("\n--- 字节影响图谱 (0x2C-0x8B, 翻转 bit0) ---"); + println!("off | resp32 | 变化bit数 | 变化字节位置"); + let mut infl: Vec<(usize, u32)> = Vec::new(); + for off in 0x2C..0x8C { + let mut m = tmpl.clone(); + m[off] ^= 1; + match query(&s, &tgt, &m) { + Some(r) => { + let x = u32::from_be_bytes([r[0] ^ base[0], r[1] ^ base[1], r[2] ^ base[2], r[3] ^ base[3]]); + let nbits = x.count_ones(); + let pos: Vec = (0..4).filter(|&i| r[i] != base[i]).map(|i| format!("B{}", i)).collect(); + println!("0x{:02x} | {} | {:2} | {}", off, hexr(&r), nbits, pos.join("")); + infl.push((off, nbits)); + } + None => println!("0x{:02x} | 无应答", off), + } + std::thread::sleep(Duration::from_millis(3)); + } + let zero_infl: Vec = infl.iter().filter(|(_, n)| *n == 0).map(|(o, _)| *o).collect(); + println!("不影响的字节: {:?}", zero_infl.iter().map(|o| format!("0x{:02x}", o)).collect::>()); + + // --- 8B 块清零图谱 --- + println!("\n--- 8B 块清零图谱 ---"); + for blk in 0..12 { + let mut m = tmpl.clone(); + for i in 0..8 { m[0x2C + blk * 8 + i] = 0; } + match query(&s, &tgt, &m) { + Some(r) => println!(" 块{:2} (0x{:02x}-0x{:02x}) 清零: {} {}", blk, 0x2C + blk * 8, 0x2C + blk * 8 + 7, + hexr(&r), if r == base { "(不影响)" } else { "★影响" }), + None => println!(" 块{:2}: 无应答", blk), + } + std::thread::sleep(Duration::from_millis(3)); + } + + // --- 换序测试 --- + println!("\n--- 换序测试 ---"); + let mut m1 = tmpl.clone(); + m1.swap(0x2C, 0x2D); + match query(&s, &tgt, &m1) { + Some(r) => println!(" 相邻字节交换 (0x2C<->0x2D): {} {}", hexr(&r), if r == base { "(顺序无关!)" } else { "顺序相关" }), + None => println!(" 交换: 无应答"), + } + let mut m2 = tmpl.clone(); + m2[0x2C..0x8C].reverse(); + match query(&s, &tgt, &m2) { + Some(r) => println!(" part1 整体反转: {} {}", hexr(&r), if r == base { "(顺序无关!)" } else { "顺序相关" }), + None => println!(" 反转: 无应答"), + } + // 块交换: 块0 <-> 块1 + let mut m3 = tmpl.clone(); + for i in 0..8 { m3.swap(0x2C + i, 0x34 + i); } + match query(&s, &tgt, &m3) { + Some(r) => println!(" 块0<->块1 交换: {} {}", hexr(&r), if r == base { "(块序无关!)" } else { "块序相关" }), + None => println!(" 块交换: 无应答"), + } + + // --- 整数加性测试: 首字节 +1 (整数) vs 翻 bit0 --- + println!("\n--- 整数加性粗测 ---"); + let mut m4 = tmpl.clone(); + m4[0x2C] = m4[0x2C].wrapping_add(1); + match query(&s, &tgt, &m4) { + Some(r) => println!(" chal[0x2C]+=1: {}", hexr(&r)), + None => println!(" 无应答"), + } +} + +// ---------- identify: f(last4) 的离线候选函数鉴别 ---------- +// probe2 实证: resp32 = f(chal[0x88..0x8C]), 仅 4B 输入。用手头全部 (输入,输出) 对 +// 测试常见 32 位构造 (哈希截断/finalizer/零密钥分组密码), 命中即可离线伪造。 +pub fn identify(tsv: &str) { + // 从 TSV 取 3 轮质询的 last4 + 对应 resp32 (抓包对) + let content = read_tsv(tsv); + let mut chals: Vec<(f64, Vec)> = Vec::new(); + let mut resps: Vec<(f64, Vec)> = Vec::new(); + for line in content.lines() { + let p: Vec<&str> = line.split('\t').collect(); + if p.len() < 5 || p[4].is_empty() { continue; } + let b = match h2b(p[4]) { Some(b) => b, None => continue }; + if b.len() != 552 { continue; } + let t: f64 = match p[0].parse() { Ok(t) => t, Err(_) => continue }; + if &b[0x28..0x2C] == b"AECH" { chals.push((t, b)); } + else if &b[0x28..0x2C] == b"AERE" { resps.push((t, b)); } + } + let mut pairs: Vec<([u8; 4], [u8; 4])> = Vec::new(); + for (rt, rb) in &resps { + let mut best: Option<&(f64, Vec)> = None; + for c in &chals { if c.0 <= *rt && rt - c.0 < 0.5 && best.map_or(true, |b| c.0 > b.0) { best = Some(c); } } + if let Some((_, cb)) = best { + pairs.push(([cb[0x88], cb[0x89], cb[0x8A], cb[0x8B]], [rb[0xC0], rb[0xC1], rb[0xC2], rb[0xC3]])); + } + } + // 追加 probe2 实测对: 零 / 全FF + pairs.push(([0, 0, 0, 0], [0xf6, 0x7f, 0xb7, 0xe3])); + pairs.push(([0xFF, 0xFF, 0xFF, 0xFF], [0x37, 0xac, 0x04, 0x6f])); + println!("鉴别样本 {} 对:", pairs.len()); + let mut g_ok = 0; + for (x, y) in &pairs { + let xu = u32::from_le_bytes(*x); + let g = agios_g(xu).to_le_bytes(); + let ok = g == *y; + if ok { g_ok += 1; } + println!(" {} -> {} | G()={} {}", hexr(x), hexr(y), hexr(&g), if ok { "✓" } else { "✗" }); + } + println!("AgiosElias G 验证: {}/{} 命中", g_ok, pairs.len()); + + let t32 = { + let mut t = [0u32; 256]; + for i in 0..256u32 { let mut c = i; for _ in 0..8 { c = if c & 1 != 0 { (c >> 1) ^ 0xEDB88320 } else { c >> 1 }; } t[i as usize] = c; } + t + }; + // TEA / XTEA (零密钥) + fn tea(v0: u32, v1: u32, rounds: u32) -> (u32, u32) { + let (mut a, mut b, mut sum) = (v0, v1, 0u32); + for _ in 0..rounds { + sum = sum.wrapping_add(0x9E3779B9); + a = a.wrapping_add((b << 4) ^ b.wrapping_add(sum) ^ (b >> 5)); + b = b.wrapping_add((a << 4) ^ a.wrapping_add(sum) ^ (a >> 5)); + } + (a, b) + } + fn xtea(v0: u32, v1: u32, rounds: u32) -> (u32, u32) { + let (mut a, mut b, mut sum) = (v0, v1, 0u32); + let k = [0u32; 4]; + for _ in 0..rounds { + a = a.wrapping_add(((b << 4) ^ (b >> 5)).wrapping_add(b) ^ sum.wrapping_add(k[(sum & 3) as usize])); + sum = sum.wrapping_add(0x9E3779B9); + b = b.wrapping_add(((a << 4) ^ (a >> 5)).wrapping_add(a) ^ sum.wrapping_add(k[((sum >> 11) & 3) as usize])); + } + (a, b) + } + // 32 位 finalizer 系列 + fn fmix32(mut h: u32) -> u32 { h ^= h >> 16; h = h.wrapping_mul(0x85ebca6b); h ^= h >> 13; h = h.wrapping_mul(0xc2b2ae35); h ^= h >> 16; h } + fn splitmix32(mut x: u32) -> u32 { x = x.wrapping_add(0x9e3779b9); x = (x ^ (x >> 16)).wrapping_mul(0x21f0aaad); x = (x ^ (x >> 15)).wrapping_mul(0x735a2d97); x ^ (x >> 15) } + fn xxh_avalanche(mut h: u32) -> u32 { h ^= h >> 15; h = h.wrapping_mul(0x85EBCA77); h ^= h >> 13; h = h.wrapping_mul(0xC2B2AE3D); h ^= h >> 16; h } + fn wang(mut h: u32) -> u32 { h = (h ^ 61) ^ (h >> 16); h = h.wrapping_add(h << 3); h ^= h >> 4; h = h.wrapping_mul(0x27d4eb2d); h ^= h >> 15; h } + fn joaatf(b: &[u8]) -> u32 { let mut h = 0u32; for &x in b { h = h.wrapping_add(x as u32); h = h.wrapping_add(h << 10); h ^= h >> 6; } h = h.wrapping_add(h << 3); h ^= h >> 11; h.wrapping_add(h << 15) } + fn fnv(b: &[u8]) -> u32 { let mut h = 0x811c9dc5u32; for &x in b { h ^= x as u32; h = h.wrapping_mul(0x01000193); } h } + fn murmur3(b: &[u8]) -> u32 { + let mut h = 0u32; let c1 = 0xcc9e2d51u32; let c2 = 0x1b873593u32; + if b.len() >= 4 { + let mut k = u32::from_le_bytes([b[0], b[1], b[2], b[3]]); + k = k.wrapping_mul(c1); k = k.rotate_left(15); k = k.wrapping_mul(c2); + h ^= k; h = h.rotate_left(13); h = h.wrapping_mul(5).wrapping_add(0xe6546b64); + } + h ^= b.len() as u32; fmix32(h) + } + fn xxh32one(b: &[u8]) -> u32 { + // xxHash32 对 4B 输入 (seed=0): h = seed+P5+len; h += k*P3; rotl17; *P4; avalanche + let mut h = 0x165667B1u32.wrapping_add(4); + let k = u32::from_le_bytes([b[0], b[1], b[2], b[3]]); + h = h.wrapping_add(k.wrapping_mul(0xC2B2AE3D)).rotate_left(17).wrapping_mul(0x27D4EB2F); + xxh_avalanche(h) + } + + type F = Box u32>; + let cands: Vec<(&str, F)> = vec![ + ("MD5[0..4]LE", Box::new(move |x| { let m = md5(&x); u32::from_le_bytes([m[0], m[1], m[2], m[3]]) })), + ("MD5[0..4]BE", Box::new(move |x| { let m = md5(&x); u32::from_be_bytes([m[0], m[1], m[2], m[3]]) })), + ("SHA1[0..4]LE", Box::new(move |x| { let m = sha1(&x); u32::from_le_bytes([m[0], m[1], m[2], m[3]]) })), + ("SHA1[0..4]BE", Box::new(move |x| { let m = sha1(&x); u32::from_be_bytes([m[0], m[1], m[2], m[3]]) })), + ("CRC32", Box::new(move |x| crc32(&t32, &x))), + ("Murmur3seed0", Box::new(move |x| murmur3(&x))), + ("xxHash32seed0", Box::new(move |x| xxh32one(&x))), + ("FNV1a32", Box::new(move |x| fnv(&x))), + ("Jenkins", Box::new(move |x| joaatf(&x))), + ("fmix32(xLE)", Box::new(move |x| fmix32(u32::from_le_bytes(x)))), + ("fmix32(xBE)", Box::new(move |x| fmix32(u32::from_be_bytes(x)))), + ("splitmix32(xLE)", Box::new(move |x| splitmix32(u32::from_le_bytes(x)))), + ("splitmix32(xBE)", Box::new(move |x| splitmix32(u32::from_be_bytes(x)))), + ("xxhAvalanche(xLE)", Box::new(move |x| xxh_avalanche(u32::from_le_bytes(x)))), + ("xxhAvalanche(xBE)", Box::new(move |x| xxh_avalanche(u32::from_be_bytes(x)))), + ("Wang(xLE)", Box::new(move |x| wang(u32::from_le_bytes(x)))), + ("Wang(xBE)", Box::new(move |x| wang(u32::from_be_bytes(x)))), + ("TEA16zerok(out0LE)", Box::new(move |x| tea(u32::from_le_bytes(x), 0, 16).0)), + ("TEA32zerok(out0LE)", Box::new(move |x| tea(u32::from_le_bytes(x), 0, 32).0)), + ("XTEA32zerok(out0LE)", Box::new(move |x| xtea(u32::from_le_bytes(x), 0, 32).0)), + ("TEA32zerok(out0BE)", Box::new(move |x| tea(u32::from_be_bytes(x), 0, 32).0)), + ("恒等LE", Box::new(move |x| u32::from_le_bytes(x))), + ("恒等BE", Box::new(move |x| u32::from_be_bytes(x))), + ]; + + println!("\n--- 候选鉴别 (须命中全部 {} 对) ---", pairs.len()); + let mut any = false; + for (name, f) in &cands { + for endian in ["BEout", "LEout"] { + let ok = pairs.iter().all(|(x, y)| { + let v = f(*x); + let out = if endian == "BEout" { v.to_be_bytes() } else { v.to_le_bytes() }; + out == *y + }); + if ok { println!(" ★★★ 命中: {} {}", name, endian); any = true; } + } + } + if !any { println!(" (全部候选均未命中 — f 为带密钥函数的可能性大)"); } +} + +fn hexr(b: &[u8]) -> String { + b.iter().map(|x| format!("{:02x}", x)).collect::() +} + +/// 从 TSV 提取 (质询, 应答) 对: AECH(552B, 含 "AECH"@0x28) 后 50ms 内的 AERE(552B, "AERE"@0x28) +fn load_pairs(tsv: &str) -> Vec<(f64, Vec, Vec)> { + let content = read_tsv(tsv); + let mut chals: Vec<(f64, Vec)> = Vec::new(); + let mut resps: Vec<(f64, Vec)> = Vec::new(); + for line in content.lines() { + let p: Vec<&str> = line.split('\t').collect(); + if p.len() < 5 || p[4].is_empty() { continue; } + let b = match h2b(p[4]) { Some(b) => b, None => continue }; + if b.len() != 552 { continue; } + if &b[0..4] != b"ENet" { continue; } + let t: f64 = match p[0].parse() { Ok(t) => t, Err(_) => continue }; + if &b[0x28..0x2C] == b"AECH" { chals.push((t, b)); } + else if &b[0x28..0x2C] == b"AERE" { resps.push((t, b)); } + } + let mut pairs = Vec::new(); + for (rt, rb) in &resps { + // 最近的先前质询 (500ms 窗, 兼容中继路径 ~140ms 延迟) + let mut best: Option<&(f64, Vec)> = None; + for c in &chals { + if c.0 <= *rt && rt - c.0 < 0.5 { + if best.map_or(true, |b| c.0 > b.0) { best = Some(c); } + } + } + if let Some((ct, cb)) = best { + pairs.push((*ct, cb.clone(), rb.clone())); + } + } + println!(" {}: AECH={} AERE={} -> 配对 {}", tsv, chals.len(), resps.len(), pairs.len()); + pairs +} + +/// 同一 TSV 内提取 DMX 会话 K (保护字节 ^ (CH-1), §4.1) +fn session_k(tsv: &str) -> Option { + let content = read_tsv(tsv); + for line in content.lines() { + let p: Vec<&str> = line.split('\t').collect(); + if p.len() < 5 || p[4].is_empty() { continue; } + let b = match h2b(p[4]) { Some(b) => b, None => continue }; + // DMX 帧: 1084B, 0x20-0x21=0101, 保护字节 0x22 = (CH1-1)^K + if b.len() == 1084 && b[0x20] == 0x01 && b[0x21] == 0x01 { + let ch1 = b[0x3C]; + return Some(b[0x22] ^ ch1.wrapping_sub(1)); + } + } + None +} + +pub fn run(tsvs: &[String]) { + let mut pairs: Vec<(Vec, Vec)> = Vec::new(); + let mut ks: Vec<(String, Option)> = Vec::new(); + for tsv in tsvs { + let ps = load_pairs(tsv); + let k = session_k(tsv); + ks.push((tsv.clone(), k)); + for (_, c, r) in ps { pairs.push((c, r)); } + } + if pairs.len() < 2 { + println!("样本不足 ({} 对), 无法做跨样本测试", pairs.len()); + return; + } + println!("\n=== 共 {} 对样本 ===", pairs.len()); + for (i, (c, r)) in pairs.iter().enumerate() { + println!(" #{}: chal[0x2c..0x30]={} resp32={}", i, + hexr(&c[0x2c..0x30]), hexr(&r[0xC0..0xC4])); + } + for (t, k) in &ks { + println!(" 会话 K: {} -> {}", t, k.map_or("无 DMX".into(), |k| format!("0x{:02x}", k))); + } + + // --- 结构: 应答跨样本变化字节确认 --- + let mut vary = Vec::new(); + for i in 0..552 { + let v = pairs[0].1[i]; + if pairs.iter().any(|(_, r)| r[i] != v) { vary.push(i); } + } + println!("\n应答跨样本变化字节: {:?}", vary.iter().map(|o| format!("0x{:02x}", o)).collect::>()); + + // resp32 两种端序 + let resp_le: Vec = pairs.iter().map(|(_, r)| + u32::from_le_bytes([r[0xC0], r[0xC1], r[0xC2], r[0xC3]])).collect(); + let resp_be: Vec = pairs.iter().map(|(_, r)| + u32::from_be_bytes([r[0xC0], r[0xC1], r[0xC2], r[0xC3]])).collect(); + + // 多种 CRC32 多项式表 (反射式): IEEE / Castagnoli / Koopman / CRC-32M + let polys: [(&str, u32); 4] = [ + ("IEEE", 0xEDB88320), ("CRC32C", 0x82F63B78), ("CRC32K", 0xEB31D82E), ("CRC32M", 0xD5828281)]; + let tables: Vec<(&str, [u32; 256])> = polys.iter().map(|(name, p)| { + let mut t = [0u32; 256]; + for i in 0..256u32 { + let mut c = i; + for _ in 0..8 { c = if c & 1 != 0 { (c >> 1) ^ p } else { c >> 1 }; } + t[i as usize] = c; + } + (*name, t) + }).collect(); + let n = pairs.len(); + + // --- A0: 同轮 质询→应答 字节回显扫描 --- + println!("--- A0: 同轮 chal→resp 回显/复制扫描 ---"); + { + let mut hits = 0; + for ro in 0..552usize { + for co in 0..552usize { + if (0..n).all(|i| pairs[i].1[ro] == pairs[i].0[co]) { + // 排除恒等静态字节 (要求该字节跨样本有变化, 否则无信息量) + let v = pairs[0].1[ro]; + if pairs.iter().any(|(_, r)| r[ro] != v) { + println!(" ★ resp[0x{:02x}] == chal[0x{:02x}] (全样本)", ro, co); + hits += 1; + } + } + } + } + if hits == 0 { println!(" (变化字节无直接回显)"); } + } + + // --- A1: 无密钥 CRC32(多多项式, 任意切片) 直接命中 --- + println!("\n--- A1: 无密钥 CRC32(多項式×切片) 直接匹配 ---"); + let mut hits = 0; + for (pname, tab) in &tables { + for start in 0..552usize { + for len in 1..=(552 - start) { + let c0 = crc32(tab, &pairs[0].0[start..start + len]); + for (ename, resp) in [("LE", &resp_le), ("BE", &resp_be)] { + if c0 == resp[0] && (1..n).all(|i| crc32(tab, &pairs[i].0[start..start + len]) == resp[i]) { + println!(" ★ 命中 {} [{}..{}] {} CRC32=resp32", pname, start, start + len, ename); + hits += 1; + } + } + } + } + } + if hits == 0 { println!(" (无)"); } + + // --- B1: 密钥化 CRC32 仿射测试 (Δ 跨样本恒定, 多項式) --- + println!("--- B1: 密钥化 CRC32 仿射测试 (resp ^ CRC32_0(slice) = Δ 恒定) ---"); + let mut hits = 0; + 'outer: for (pname, tab) in &tables { + for start in 0..552usize { + for len in 1..=(552 - start) { + let d0le = resp_le[0] ^ crc32(tab, &pairs[0].0[start..start + len]); + let d0be = resp_be[0] ^ crc32(tab, &pairs[0].0[start..start + len]); + let okle = (1..n).all(|i| (resp_le[i] ^ crc32(tab, &pairs[i].0[start..start + len])) == d0le); + let okbe = !okle && (1..n).all(|i| (resp_be[i] ^ crc32(tab, &pairs[i].0[start..start + len])) == d0be); + if okle || okbe { + println!(" ★ 命中 {} [{}..{}] {} Δ=0x{:08x}", pname, start, start + len, + if okle { "LE" } else { "BE" }, if okle { d0le } else { d0be }); + hits += 1; + if hits > 20 { println!(" (更多命中省略)"); break 'outer; } + } + } + } + } + if hits == 0 { println!(" (无)"); } + + // --- A2: MD5/SHA1 任意切片, first4/last4 LE/BE --- + println!("--- A2: MD5/SHA1(切片) 截断匹配 ---"); + let mut hits = 0; + for start in 0..552usize { + for len in 1..=(552 - start) { + let m0 = md5(&pairs[0].0[start..start + len]); + let s0 = sha1(&pairs[0].0[start..start + len]); + // 候选值: first4le/be, last4le/be (md5), first4le/be (sha1) + let cands: [(&str, u32); 6] = [ + ("MD5first4LE", u32::from_le_bytes([m0[0], m0[1], m0[2], m0[3]])), + ("MD5first4BE", u32::from_be_bytes([m0[0], m0[1], m0[2], m0[3]])), + ("MD5last4LE", u32::from_le_bytes([m0[12], m0[13], m0[14], m0[15]])), + ("MD5last4BE", u32::from_be_bytes([m0[12], m0[13], m0[14], m0[15]])), + ("SHA1first4LE", u32::from_le_bytes([s0[0], s0[1], s0[2], s0[3]])), + ("SHA1first4BE", u32::from_be_bytes([s0[0], s0[1], s0[2], s0[3]])), + ]; + for (cname, cv) in cands { + for (ename, resp) in [("LE", &resp_le), ("BE", &resp_be)] { + if cv != resp[0] { continue; } + let ok = (1..n).all(|i| { + let m = md5(&pairs[i].0[start..start + len]); + let s = sha1(&pairs[i].0[start..start + len]); + let v = match cname { + "MD5first4LE" => u32::from_le_bytes([m[0], m[1], m[2], m[3]]), + "MD5first4BE" => u32::from_be_bytes([m[0], m[1], m[2], m[3]]), + "MD5last4LE" => u32::from_le_bytes([m[12], m[13], m[14], m[15]]), + "MD5last4BE" => u32::from_be_bytes([m[12], m[13], m[14], m[15]]), + "SHA1first4LE" => u32::from_le_bytes([s[0], s[1], s[2], s[3]]), + _ => u32::from_be_bytes([s[0], s[1], s[2], s[3]]), + }; + v == resp[i] + }); + if ok { + println!(" ★ 命中 [{}..{}] {} = resp32 {}", start, start + len, cname, ename); + hits += 1; + } + } + } + } + } + if hits == 0 { println!(" (无)"); } + + // --- A3: 简单加和 (u8/u16LE/u16BE/u32LE/u32BE 累加, 任意切片) --- + println!("--- A3: 加和校验(切片) ---"); + let mut hits = 0; + for start in 0..552usize { + for len in 1..=(552 - start) { + let sums = |b: &[u8]| -> [u32; 5] { + let mut r = [0u32; 5]; + for (i, &x) in b.iter().enumerate() { + r[0] = r[0].wrapping_add(x as u32); + if i + 1 < b.len() && i % 2 == 0 { r[1] = r[1].wrapping_add(u16::from_le_bytes([x, b[i+1]]) as u32); } + if i + 1 < b.len() && i % 2 == 0 { r[2] = r[2].wrapping_add(u16::from_be_bytes([x, b[i+1]]) as u32); } + if i + 3 < b.len() && i % 4 == 0 { r[3] = r[3].wrapping_add(u32::from_le_bytes([x, b[i+1], b[i+2], b[i+3]])); } + if i + 3 < b.len() && i % 4 == 0 { r[4] = r[4].wrapping_add(u32::from_be_bytes([x, b[i+1], b[i+2], b[i+3]])); } + } + r + }; + let s0 = sums(&pairs[0].0[start..start + len]); + for k in 0..5 { + for (ename, resp) in [("LE", &resp_le), ("BE", &resp_be)] { + if s0[k] != resp[0] { continue; } + if (1..n).all(|i| sums(&pairs[i].0[start..start + len])[k] == resp[i]) { + println!(" ★ 命中 [{}..{}] sum{} {}", start, start + len, k, ename); + hits += 1; + } + } + } + } + } + if hits == 0 { println!(" (无)"); } + + // --- C: XOR 常量: resp32 ^ chal32@off (LE/BE) 跨样本恒定 --- + println!("--- C: XOR 常量掩码测试 ---"); + let mut hits = 0; + for off in 0..548usize { + let cle0 = u32::from_le_bytes([pairs[0].0[off], pairs[0].0[off+1], pairs[0].0[off+2], pairs[0].0[off+3]]); + let cbe0 = u32::from_be_bytes([pairs[0].0[off], pairs[0].0[off+1], pairs[0].0[off+2], pairs[0].0[off+3]]); + let dle = resp_le[0] ^ cle0; let dbe = resp_be[0] ^ cbe0; + let okle = (1..n).all(|i| resp_le[i] ^ u32::from_le_bytes([pairs[i].0[off], pairs[i].0[off+1], pairs[i].0[off+2], pairs[i].0[off+3]]) == dle); + let okbe = (1..n).all(|i| resp_be[i] ^ u32::from_be_bytes([pairs[i].0[off], pairs[i].0[off+1], pairs[i].0[off+2], pairs[i].0[off+3]]) == dbe); + if okle { println!(" ★ resp32LE ^ chal32LE@0x{:02x} = 0x{:08x} (恒定)", off, dle); hits += 1; } + if okbe { println!(" ★ resp32BE ^ chal32BE@0x{:02x} = 0x{:08x} (恒定)", off, dbe); hits += 1; } + } + if hits == 0 { println!(" (无)"); } + + // --- D: resp32 与会话 K 相关 --- + println!("--- D: resp32 字节 vs 会话 K ---"); + for (i, (t, k)) in ks.iter().enumerate() { + if let Some(k) = k { + // 该 tsv 贡献的样本 (按顺序假设每 tsv 样本连续) + let _ = i; + for (j, (_, r)) in pairs.iter().enumerate() { + let hit = (0xC0..0xC4).any(|o| r[o] == *k); + if hit { + println!(" {} K=0x{:02x} 出现在样本#{} resp32 中", t, k, j); + } + } + } + } + + // --- A4: 32 位非加密哈希 (直接匹配 + XOR-Δ 恒定) --- + println!("--- A4: FNV1a32/FNV132/djb2/sdbm/Jenkins/Murmur3/xxHash32 ---"); + fn fnv1a32(b: &[u8]) -> u32 { let mut h = 0x811c9dc5u32; for &x in b { h ^= x as u32; h = h.wrapping_mul(0x01000193); } h } + fn fnv132(b: &[u8]) -> u32 { let mut h = 0x811c9dc5u32; for &x in b { h = h.wrapping_mul(0x01000193); h ^= x as u32; } h } + fn djb2(b: &[u8]) -> u32 { let mut h = 5381u32; for &x in b { h = h.wrapping_mul(33).wrapping_add(x as u32); } h } + fn sdbm(b: &[u8]) -> u32 { let mut h = 0u32; for &x in b { h = (x as u32).wrapping_add(h << 6).wrapping_add(h << 16).wrapping_sub(h); } h } + fn joaat(b: &[u8]) -> u32 { let mut h = 0u32; for &x in b { h = h.wrapping_add(x as u32); h = h.wrapping_add(h << 10); h ^= h >> 6; } h = h.wrapping_add(h << 3); h ^= h >> 11; h.wrapping_add(h << 15) } + fn murmur3_32(b: &[u8], seed: u32) -> u32 { + let mut h = seed; + let c1 = 0xcc9e2d51u32; let c2 = 0x1b873593u32; + let nblocks = b.len() / 4; + for i in 0..nblocks { + let mut k = u32::from_le_bytes([b[i*4], b[i*4+1], b[i*4+2], b[i*4+3]]); + k = k.wrapping_mul(c1); k = k.rotate_left(15); k = k.wrapping_mul(c2); + h ^= k; h = h.rotate_left(13); h = h.wrapping_mul(5).wrapping_add(0xe6546b64); + } + let tail = &b[nblocks*4..]; + let mut k = 0u32; + for (i, &x) in tail.iter().enumerate() { k |= (x as u32) << (8 * i); } + if !tail.is_empty() { k = k.wrapping_mul(c1); k = k.rotate_left(15); k = k.wrapping_mul(c2); h ^= k; } + h ^= b.len() as u32; + h ^= h >> 16; h = h.wrapping_mul(0x85ebca6b); h ^= h >> 13; h = h.wrapping_mul(0xc2b2ae35); h ^= h >> 16; + h + } + fn xxh32(b: &[u8], seed: u32) -> u32 { + const P1: u32 = 0x9E3779B1; const P2: u32 = 0x85EBCA77; const P3: u32 = 0xC2B2AE3D; + const P4: u32 = 0x27D4EB2F; const P5: u32 = 0x165667B1; + let mut i = 0usize; + let mut h: u32; + if b.len() >= 16 { + let mut v1 = seed.wrapping_add(P1).wrapping_add(P2); + let mut v2 = seed.wrapping_add(P2); + let mut v3 = seed; + let mut v4 = seed.wrapping_sub(P1); + while i + 16 <= b.len() { + let rd = |o: usize| u32::from_le_bytes([b[o], b[o+1], b[o+2], b[o+3]]); + v1 = v1.wrapping_add(rd(i).wrapping_mul(P2)).rotate_left(13).wrapping_mul(P1); + v2 = v2.wrapping_add(rd(i+4).wrapping_mul(P2)).rotate_left(13).wrapping_mul(P1); + v3 = v3.wrapping_add(rd(i+8).wrapping_mul(P2)).rotate_left(13).wrapping_mul(P1); + v4 = v4.wrapping_add(rd(i+12).wrapping_mul(P2)).rotate_left(13).wrapping_mul(P1); + i += 16; + } + h = v1.rotate_left(1).wrapping_add(v2.rotate_left(7)).wrapping_add(v3.rotate_left(12)).wrapping_add(v4.rotate_left(18)); + } else { + h = seed.wrapping_add(P5); + } + h = h.wrapping_add(b.len() as u32); + while i + 4 <= b.len() { + h = h.wrapping_add(u32::from_le_bytes([b[i], b[i+1], b[i+2], b[i+3]]).wrapping_mul(P3)).rotate_left(17).wrapping_mul(P4); + i += 4; + } + while i < b.len() { + h = h.wrapping_add((b[i] as u32).wrapping_mul(P5)).rotate_left(11).wrapping_mul(P1); + i += 1; + } + h ^= h >> 15; h = h.wrapping_mul(P2); h ^= h >> 13; h = h.wrapping_mul(P3); h ^= h >> 16; + h + } + let hashes: [(&str, fn(&[u8]) -> u32); 7] = [ + ("FNV1a32", fnv1a32), ("FNV132", fnv132), ("djb2", djb2), ("sdbm", sdbm), + ("Jenkins", joaat), ("Murmur3", |b| murmur3_32(b, 0)), ("xxHash32", |b| xxh32(b, 0))]; + let mut hits = 0; + for (hname, hf) in &hashes { + for start in 0..552usize { + for len in 1..=(552 - start) { + let h0 = hf(&pairs[0].0[start..start + len]); + for (ename, resp) in [("LE", &resp_le), ("BE", &resp_be)] { + if h0 == resp[0] && (1..n).all(|i| hf(&pairs[i].0[start..start + len]) == resp[i]) { + println!(" ★ 直接命中 {} [{}..{}] {}", hname, start, start + len, ename); + hits += 1; + } + // XOR-Δ (hash^key 构造) + let d0 = resp[0] ^ h0; + if (1..n).all(|i| (resp[i] ^ hf(&pairs[i].0[start..start + len])) == d0) { + println!(" ★ XOR-Δ 命中 {} [{}..{}] {} Δ=0x{:08x}", hname, start, start + len, ename, d0); + hits += 1; + } + } + } + } + } + if hits == 0 { println!(" (无)"); } + + println!("\n=== 分析完成 ==="); +} diff --git a/src/dmxhash.rs b/src/dmxhash.rs new file mode 100644 index 0000000..57b2956 --- /dev/null +++ b/src/dmxhash.rs @@ -0,0 +1,1201 @@ +// dmxhash.rs — DMX-A 帧 0x22-0x29 八字节标签算法爆破 + seq@0x1A 验证 +// 用法: enet-validator dmxhash <主机IP> + +// ---------- CRC 表 ---------- +fn crc64_table(poly: u64, reflected: bool) -> [u64; 256] { + let mut t = [0u64; 256]; + for i in 0..256u64 { + let mut c = if reflected { i } else { i << 56 }; + for _ in 0..8 { + c = if reflected { + if c & 1 != 0 { (c >> 1) ^ poly } else { c >> 1 } + } else if c & 0x8000_0000_0000_0000 != 0 { + (c << 1) ^ poly + } else { + c << 1 + }; + } + t[i as usize] = c; + } + t +} +fn crc64(t: &[u64; 256], data: &[u8], init: u64, xorout: u64, reflected: bool) -> u64 { + let mut c = init; + for &b in data { + c = if reflected { + t[((c ^ b as u64) & 0xFF) as usize] ^ (c >> 8) + } else { + t[(((c >> 56) ^ b as u64) & 0xFF) as usize] ^ (c << 8) + }; + } + c ^ xorout +} +pub(crate) fn crc32_table() -> [u32; 256] { + let mut t = [0u32; 256]; + for i in 0..256u32 { + let mut c = i; + for _ in 0..8 { + c = if c & 1 != 0 { (c >> 1) ^ 0xEDB88320 } else { c >> 1 }; + } + t[i as usize] = c; + } + t +} +pub(crate) fn crc32(t: &[u32; 256], data: &[u8]) -> u32 { + let mut c = 0xFFFF_FFFFu32; + for &b in data { + c = t[((c ^ b as u32) & 0xFF) as usize] ^ (c >> 8); + } + c ^ 0xFFFF_FFFF +} +fn fnv1a64(data: &[u8]) -> u64 { + let mut h = 0xcbf29ce484222325u64; + for &b in data { + h ^= b as u64; + h = h.wrapping_mul(0x100000001b3); + } + h +} +fn fnv1_64(data: &[u8]) -> u64 { + let mut h = 0xcbf29ce484222325u64; + for &b in data { + h = h.wrapping_mul(0x100000001b3); + h ^= b as u64; + } + h +} +// xxHash64 (seed 0) +fn xxh64(data: &[u8]) -> u64 { + const P1: u64 = 0x9E3779B185EBCA87; + const P2: u64 = 0xC2B2AE3D27D4EB4F; + const P3: u64 = 0x165667B19E3779F9; + const P4: u64 = 0x85EBCA77C2B2AE63; + const P5: u64 = 0x27D4EB2F165667C5; + let len = data.len() as u64; + let mut i = 0usize; + let mut h: u64; + if data.len() >= 32 { + let mut v1 = P1.wrapping_add(P2); + let mut v2 = P2; + let mut v3 = 0u64; + let mut v4 = P1.wrapping_neg(); + while i + 32 <= data.len() { + let rd = |o: usize| u64::from_le_bytes(data[o..o + 8].try_into().unwrap()); + v1 = (v1.wrapping_add(rd(i).wrapping_mul(P2))).rotate_left(31).wrapping_mul(P1); + v2 = (v2.wrapping_add(rd(i + 8).wrapping_mul(P2))).rotate_left(31).wrapping_mul(P1); + v3 = (v3.wrapping_add(rd(i + 16).wrapping_mul(P2))).rotate_left(31).wrapping_mul(P1); + v4 = (v4.wrapping_add(rd(i + 24).wrapping_mul(P2))).rotate_left(31).wrapping_mul(P1); + i += 32; + } + h = v1.rotate_left(1) + .wrapping_add(v2.rotate_left(7)) + .wrapping_add(v3.rotate_left(12)) + .wrapping_add(v4.rotate_left(18)); + for v in [v1, v2, v3, v4] { + let v = (v.wrapping_mul(P2)).rotate_left(31).wrapping_mul(P1); + h ^= v; + h = h.wrapping_mul(P1).wrapping_add(P4); + } + } else { + h = P5; + } + h = h.wrapping_add(len); + while i + 8 <= data.len() { + let k = u64::from_le_bytes(data[i..i + 8].try_into().unwrap()) + .wrapping_mul(P2) + .rotate_left(31) + .wrapping_mul(P1); + h ^= k; + h = h.rotate_left(27).wrapping_mul(P1).wrapping_add(P4); + i += 8; + } + if i + 4 <= data.len() { + h ^= (u32::from_le_bytes(data[i..i + 4].try_into().unwrap()) as u64).wrapping_mul(P1); + h = h.rotate_left(23).wrapping_mul(P2).wrapping_add(P3); + i += 4; + } + while i < data.len() { + h ^= (data[i] as u64).wrapping_mul(P5); + h = h.rotate_left(11).wrapping_mul(P1); + i += 1; + } + h ^= h >> 33; + h = h.wrapping_mul(P2); + h ^= h >> 29; + h = h.wrapping_mul(P3); + h ^= h >> 32; + h +} + +pub(crate) fn h2b(hex: &str) -> Option> { + if hex.len() % 2 != 0 || hex.is_empty() { + return None; + } + (0..hex.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&hex[i..i + 2], 16).ok()) + .collect() +} + +// ---------- MD5 ---------- +pub(crate) fn md5(data: &[u8]) -> [u8; 16] { + const S: [u32; 64] = [ + 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, + 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, + 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, + 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, + ]; + const K: [u32; 64] = [ + 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501, + 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, + 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, + 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a, + 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, + 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, + 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1, + 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391, + ]; + let mut a0 = 0x67452301u32; + let mut b0 = 0xefcdab89u32; + let mut c0 = 0x98badcfeu32; + let mut d0 = 0x10325476u32; + let mut msg = data.to_vec(); + let bl = (data.len() as u64).wrapping_mul(8); + msg.push(0x80); + while msg.len() % 64 != 56 { + msg.push(0); + } + msg.extend_from_slice(&bl.to_le_bytes()); + for chunk in msg.chunks_exact(64) { + let mut m = [0u32; 16]; + for i in 0..16 { + m[i] = u32::from_le_bytes(chunk[i * 4..i * 4 + 4].try_into().unwrap()); + } + let (mut a, mut b, mut c, mut d) = (a0, b0, c0, d0); + for i in 0..64 { + let (f, g) = match i / 16 { + 0 => ((b & c) | (!b & d), i), + 1 => ((d & b) | (!d & c), (5 * i + 1) % 16), + 2 => (b ^ c ^ d, (3 * i + 5) % 16), + _ => (c ^ (b | !d), (7 * i) % 16), + }; + let tmp = d; + d = c; + c = b; + b = b.wrapping_add( + a.wrapping_add(f).wrapping_add(K[i]).wrapping_add(m[g]).rotate_left(S[i]), + ); + a = tmp; + } + a0 = a0.wrapping_add(a); + b0 = b0.wrapping_add(b); + c0 = c0.wrapping_add(c); + d0 = d0.wrapping_add(d); + } + let mut out = [0u8; 16]; + out[0..4].copy_from_slice(&a0.to_le_bytes()); + out[4..8].copy_from_slice(&b0.to_le_bytes()); + out[8..12].copy_from_slice(&c0.to_le_bytes()); + out[12..16].copy_from_slice(&d0.to_le_bytes()); + out +} + +// ---------- SHA1 ---------- +pub(crate) fn sha1(data: &[u8]) -> [u8; 20] { + let mut h: [u32; 5] = [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0]; + let mut msg = data.to_vec(); + let bl = (data.len() as u64).wrapping_mul(8); + msg.push(0x80); + while msg.len() % 64 != 56 { + msg.push(0); + } + msg.extend_from_slice(&bl.to_be_bytes()); + for chunk in msg.chunks_exact(64) { + let mut w = [0u32; 80]; + for i in 0..16 { + w[i] = u32::from_be_bytes(chunk[i * 4..i * 4 + 4].try_into().unwrap()); + } + for i in 16..80 { + w[i] = (w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]).rotate_left(1); + } + let (mut a, mut b, mut c, mut d, mut e) = (h[0], h[1], h[2], h[3], h[4]); + for i in 0..80 { + let (f, k) = match i / 20 { + 0 => ((b & c) | (!b & d), 0x5A827999u32), + 1 => (b ^ c ^ d, 0x6ED9EBA1), + 2 => ((b & c) | (b & d) | (c & d), 0x8F1BBCDC), + _ => (b ^ c ^ d, 0xCA62C1D6), + }; + let tmp = a + .rotate_left(5) + .wrapping_add(f) + .wrapping_add(e) + .wrapping_add(k) + .wrapping_add(w[i]); + e = d; + d = c; + c = b.rotate_left(30); + b = a; + a = tmp; + } + h[0] = h[0].wrapping_add(a); + h[1] = h[1].wrapping_add(b); + h[2] = h[2].wrapping_add(c); + h[3] = h[3].wrapping_add(d); + h[4] = h[4].wrapping_add(e); + } + let mut out = [0u8; 20]; + for i in 0..5 { + out[i * 4..i * 4 + 4].copy_from_slice(&h[i].to_be_bytes()); + } + out +} + +pub fn run(tsv: &str, host_ip: &str) { + let raw = std::fs::read(tsv).expect("读 TSV 失败"); + // PowerShell 重定向产出 UTF-16LE (BOM ff fe); 兼容 UTF-8 + let content = if raw.starts_with(&[0xFF, 0xFE]) { + let u16s: Vec = raw[2..] + .chunks_exact(2) + .map(|c| u16::from_le_bytes([c[0], c[1]])) + .collect(); + String::from_utf16_lossy(&u16s) + } else { + String::from_utf8_lossy(&raw).into_owned() + }; + let mut frames: Vec<(f64, Vec)> = Vec::new(); + for line in content.lines() { + let p: Vec<&str> = line.split('\t').collect(); + if p.len() < 5 || p[4].trim().is_empty() { + continue; + } + if p[1].trim() != host_ip { + continue; + } + let b = match h2b(p[4].trim()) { + Some(b) => b, + None => continue, + }; + if b.len() != 1084 || b[4] != 4 || b[0x20] != 1 || b[0x21] != 1 { + continue; + } + let t: f64 = p[0].parse().unwrap_or(0.0); + frames.push((t, b)); + } + println!("DMX-A 帧: {}", frames.len()); + if frames.is_empty() { + return; + } + + // --- seq@0x1A 单调性 --- + let mut inc = 0u32; + let mut wrap = 0u32; + let mut gaps = std::collections::HashMap::new(); + for i in 1..frames.len() { + let s0 = u16::from_le_bytes([frames[i - 1].1[0x1A], frames[i - 1].1[0x1B]]); + let s1 = u16::from_le_bytes([frames[i].1[0x1A], frames[i].1[0x1B]]); + let d = s1.wrapping_sub(s0); + *gaps.entry(d).or_insert(0u32) += 1; + if d == 1 { + inc += 1; + } else if s1 < s0 { + wrap += 1; + } + } + let mut gv: Vec<_> = gaps.iter().collect(); + gv.sort_by_key(|(k, _)| **k); + println!( + "seq@0x1A: +1步进={}/{} 回绕={} 步长分布前8: {:?}", + inc, + frames.len() - 1, + wrap, + gv.iter().take(8).collect::>() + ); + + // --- 标签爆破 --- + let ecma = crc64_table(0x42F0E1EBA9EA3693, false); + let iso = crc64_table(0x000000000000001B, true); + let ecma_r = crc64_table(0xC96C5795D7870F42, true); + let c32 = crc32_table(); + let n = frames.len().min(500); + + // (名字, 计算函数) — 输入 (帧, 范围id) + // 范围: 0=data 1=seq||data 2=seq||hdr2C||data 3=marker(0101)||seq||data + let build = |b: &Vec, range: u8| -> Vec { + let mut v = Vec::with_capacity(1100); + match range { + 0 => v.extend_from_slice(&b[0x3C..]), + 1 => { + v.extend_from_slice(&b[0x1A..0x1C]); + v.extend_from_slice(&b[0x3C..]); + } + 2 => { + v.extend_from_slice(&b[0x1A..0x1C]); + v.extend_from_slice(&b[0x2C..0x3C]); + v.extend_from_slice(&b[0x3C..]); + } + 3 => { + v.extend_from_slice(&b[0x20..0x22]); + v.extend_from_slice(&b[0x1A..0x1C]); + v.extend_from_slice(&b[0x3C..]); + } + _ => {} + } + v + }; + let hashes: Vec<(&str, Box u64>)> = vec![ + ("CRC64-ECMA", Box::new(move |d: &[u8]| crc64(&ecma, d, 0, 0, false))), + ("CRC64-XZ", Box::new(move |d: &[u8]| crc64(&iso, d, u64::MAX, u64::MAX, true))), + ("CRC64-ISO", Box::new(move |d: &[u8]| crc64(&iso, d, 0, 0, true))), + ("CRC64-ECMA-r", Box::new(move |d: &[u8]| crc64(&ecma_r, d, 0, 0, true))), + ("FNV1a64", Box::new(fnv1a64)), + ("FNV1-64", Box::new(fnv1_64)), + ("xxHash64", Box::new(xxh64)), + ]; + for (name, hf) in &hashes { + for range in 0..4u8 { + let mut m_le = 0u32; + let mut m_be = 0u32; + for (_, b) in frames.iter().take(n) { + let d = build(b, range); + let h = hf(&d); + let tag = u64::from_le_bytes(b[0x22..0x2A].try_into().unwrap()); + if h == tag { + m_le += 1; + } + if h.swap_bytes() == tag { + m_be += 1; + } + } + if m_le > 0 || m_be > 0 { + println!(" {} range={}: LE匹配={}/{} BE匹配={}/{}", name, range, m_le, n, m_be, n); + } + } + } + // CRC32 对假设: tag = u32_a||u32_b, a/b ∈ {crc32(port1), crc32(port2), crc32(seq||port1), crc32(seq||port2)} + let mut combos: std::collections::HashMap = std::collections::HashMap::new(); + for (_, b) in frames.iter().take(n) { + let p1 = &b[0x3C..0x23C]; + let p2 = &b[0x23C..0x43C]; + let mut s1 = b[0x1A..0x1C].to_vec(); + s1.extend_from_slice(p1); + let mut s2 = b[0x1A..0x1C].to_vec(); + s2.extend_from_slice(p2); + let cands = [ + ("p1", crc32(&c32, p1)), + ("p2", crc32(&c32, p2)), + ("s+p1", crc32(&c32, &s1)), + ("s+p2", crc32(&c32, &s2)), + ("all", crc32(&c32, &b[0x3C..])), + ]; + let ta = u32::from_le_bytes(b[0x22..0x26].try_into().unwrap()); + let tb = u32::from_le_bytes(b[0x26..0x2A].try_into().unwrap()); + for (na, ca) in &cands { + if *ca == ta { + *combos.entry(format!("tag[0:4]=crc32({})", na)).or_insert(0) += 1; + } + if ca.swap_bytes() == ta { + *combos.entry(format!("tag[0:4]=BE crc32({})", na)).or_insert(0) += 1; + } + if *ca == tb { + *combos.entry(format!("tag[4:8]=crc32({})", na)).or_insert(0) += 1; + } + if ca.swap_bytes() == tb { + *combos.entry(format!("tag[4:8]=BE crc32({})", na)).or_insert(0) += 1; + } + } + } + for (k, v) in &combos { + println!(" CRC32对: {} 匹配 {}/{}", k, v, n); + } + println!("爆破完成 ({} 帧样本)", n); + + // --- MD5/SHA1 截断标签假设 --- + // 范围: 0=data 1=seq||data 2=全帧(tag置零) 3=0x20..end(tag置零) + let cranges: Vec<(&str, Box) -> Vec>)> = vec![ + ("data", Box::new(|b: &Vec| b[0x3C..].to_vec())), + ("seq+data", Box::new(|b: &Vec| { + let mut v = b[0x1A..0x1C].to_vec(); + v.extend_from_slice(&b[0x3C..]); + v + })), + ("frame_tag0", Box::new(|b: &Vec| { + let mut v = b.clone(); + for i in 0x22..0x2A { v[i] = 0; } + v + })), + ("0x20e_tag0", Box::new(|b: &Vec| { + let mut v = b[0x20..].to_vec(); + for i in 2..10 { v[i] = 0; } + v + })), + ("hdr14+data", Box::new(|b: &Vec| { + let mut v = b[0x1A..0x22].to_vec(); // seq + 00000000 + 0101 + v.extend_from_slice(&b[0x2A..0x3C]); + v.extend_from_slice(&b[0x3C..]); + v + })), + ]; + let hashers: [(&str, &dyn Fn(&[u8]) -> Vec); 2] = [ + ("MD5", &|d: &[u8]| md5(d).to_vec()), + ("SHA1", &|d: &[u8]| sha1(d).to_vec()), + ]; + for (rname, rf) in &cranges { + for (hname, hf) in &hashers { + let mut hits = [0u32; 4]; // first8, last8, first8BE-rev, last8BE-rev + for (_, b) in frames.iter().take(n) { + let d = rf(b); + let h = hf(&d); + let tag: [u8; 8] = b[0x22..0x2A].try_into().unwrap(); + if h[..8] == tag[..] { hits[0] += 1; } + if h[h.len() - 8..] == tag[..] { hits[1] += 1; } + let mut r8 = h[..8].to_vec(); r8.reverse(); + if r8[..] == tag[..] { hits[2] += 1; } + let mut l8 = h[h.len() - 8..].to_vec(); l8.reverse(); + if l8[..] == tag[..] { hits[3] += 1; } + } + if hits.iter().any(|&x| x > 0) { + println!(" {}({}) 命中: 前8={} 后8={} 前8rev={} 后8rev={} / {}", hname, rname, hits[0], hits[1], hits[2], hits[3], n); + } + } + } + + // --- tag = g(seq)? 常见 index→PRNG 函数 --- + fn splitmix64(mut x: u64) -> u64 { + x = (x ^ (x >> 30)).wrapping_mul(0xBF58476D1CE4E5B9); + x = (x ^ (x >> 27)).wrapping_mul(0x94D049BB133111EB); + x ^ (x >> 31) + } + fn fmix64(mut h: u64) -> u64 { + h ^= h >> 33; + h = h.wrapping_mul(0xff51afd7ed558ccd); + h ^= h >> 33; + h = h.wrapping_mul(0xc4ceb9fe1a85ec53); + h ^ (h >> 33) + } + let seq_funcs: Vec<(&str, Box u64>)> = vec![ + ("splitmix64(seq)", Box::new(splitmix64)), + ("fmix64(seq)", Box::new(fmix64)), + ("splitmix64(seq<<32)", Box::new(|s| splitmix64(s << 32))), + ("fmix64(seq<<32)", Box::new(|s| fmix64(s << 32))), + ("xxh64(seq_le8)", Box::new(|s| xxh64(&s.to_le_bytes()))), + ("fnv1a(seq_le8)", Box::new(|s| fnv1a64(&s.to_le_bytes()))), + ("splitmix64(seq+0x9e37)", Box::new(|s| splitmix64(s.wrapping_add(0x9e3779b97f4a7c15)))), + ]; + for (name, f) in &seq_funcs { + let mut m = 0u32; + for (_, b) in frames.iter().take(n) { + let seq = u16::from_le_bytes([b[0x1A], b[0x1B]]) as u64; + let tag = u64::from_le_bytes(b[0x22..0x2A].try_into().unwrap()); + if f(seq) == tag || f(seq).swap_bytes() == tag { + m += 1; + } + } + if m > 0 { + println!(" {} : 匹配 {}/{}", name, m, n); + } + } + // --- 哈希链: tag[i+1] = f(tag[i])? --- + let chain_funcs: Vec<(&str, Box u64>)> = vec![ + ("splitmix64", Box::new(splitmix64)), + ("fmix64", Box::new(fmix64)), + ("xorshift64*", Box::new(|mut x: u64| { + x ^= x >> 12; + x ^= x << 25; + x ^= x >> 27; + x.wrapping_mul(0x2545F4914F6CDD1D) + })), + ("LCG-Knuth", Box::new(|x: u64| x.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407))), + ]; + for (name, f) in &chain_funcs { + let mut m = 0u32; + for i in 1..n { + let t0 = u64::from_le_bytes(frames[i - 1].1[0x22..0x2A].try_into().unwrap()); + let t1 = u64::from_le_bytes(frames[i].1[0x22..0x2A].try_into().unwrap()); + if f(t0) == t1 { + m += 1; + } + } + if m > 0 { + println!(" 链 {} : 匹配 {}/{}", name, m, n - 1); + } + } + + // --- 分块校验和假设: 1024B 数据切成 N 块, tag[i] = f(块i)? --- + for blk in [64usize, 128, 256] { + let nblk = 1024 / blk; + if nblk > 8 { + continue; + } + let mut sum_ok = vec![0u32; nblk]; + let mut xor_ok = vec![0u32; nblk]; + let mut crc8_ok = vec![0u32; nblk]; + for (_, b) in frames.iter().take(n) { + let data = &b[0x3C..0x43C]; + for i in 0..nblk { + let chunk = &data[i * blk..(i + 1) * blk]; + let s = chunk.iter().fold(0u8, |a, &x| a.wrapping_add(x)); + let x = chunk.iter().fold(0u8, |a, &v| a ^ v); + let mut c = 0u8; + for &v in chunk { + c ^= v; + for _ in 0..8 { + c = if c & 0x80 != 0 { (c << 1) ^ 0x07 } else { c << 1 }; + } + } + let tag = b[0x22 + i]; + if s == tag { + sum_ok[i] += 1; + } + if x == tag { + xor_ok[i] += 1; + } + if c == tag { + crc8_ok[i] += 1; + } + } + } + println!( + "块{}B: sum8命中={:?} xor8命中={:?} crc8命中={:?} (样本{})", + blk, sum_ok, xor_ok, crc8_ok, n + ); + } + + // --- tag[0] = f(数据前缀)? 逐前缀长测试 --- + for plen in [8usize, 16, 32, 64, 128, 256, 512, 1024] { + let mut sum_ok = 0u32; + let mut xor_ok = 0u32; + for (_, b) in frames.iter().take(n) { + let d = &b[0x3C..0x3C + plen]; + if d.iter().fold(0u8, |a, &x| a.wrapping_add(x)) == b[0x22] { + sum_ok += 1; + } + if d.iter().fold(0u8, |a, &v| a ^ v) == b[0x22] { + xor_ok += 1; + } + } + if sum_ok > n as u32 / 2 || xor_ok > n as u32 / 2 { + println!("tag[0] 前缀{}B: sum8={}/{} xor8={}/{}", plen, sum_ok, n, xor_ok, n); + } + } + + // --- 相同通道数据帧的标签对比 (判断 tag 是否含 seq/随机成分) --- + let mut by_data: std::collections::HashMap, Vec<(u16, [u8; 8])>> = std::collections::HashMap::new(); + for (_, b) in frames.iter() { + let seq = u16::from_le_bytes([b[0x1A], b[0x1B]]); + let tag: [u8; 8] = b[0x22..0x2A].try_into().unwrap(); + by_data.entry(b[0x3C..].to_vec()).or_default().push((seq, tag)); + } + let mut shown = 0; + let mut n_same = 0u32; + let mut n_diff = 0u32; + for (d, v) in &by_data { + if v.len() >= 2 { + let t0 = v[0].1; + for (s, t) in v.iter().skip(1) { + let _ = s; + if *t == t0 { + n_same += 1; + } else { + n_diff += 1; + } + } + if shown < 3 { + shown += 1; + println!("相同数据组 ({}帧, 数据首8B={:02x?}):", v.len(), &d[..8]); + for (s, t) in v.iter().take(6) { + println!(" seq={:04x} tag={}", s, t.iter().map(|x| format!("{:02x}", x)).collect::()); + } + } + } + } + println!("全数据相同组内: tag相同={} tag不同={}", n_same, n_diff); + + // --- tag[0] 与 64B 前缀的统计相关性 (对照: 组内匹配率 vs 随机期望 1/256) --- + let mut by_prefix: std::collections::HashMap, Vec> = std::collections::HashMap::new(); + for (_, b) in frames.iter() { + by_prefix.entry(b[0x3C..0x7C].to_vec()).or_default().push(b[0x22]); + } + let mut pairs = 0u64; + let mut match0 = 0u64; + for v in by_prefix.values() { + for i in 0..v.len() { + for j in i + 1..v.len() { + pairs += 1; + if v[i] == v[j] { + match0 += 1; + } + } + } + } + println!( + "64B前缀组内 tag[0] 匹配: {}/{} (随机期望 ~{:.1})", + match0, + pairs, + pairs as f64 / 256.0 + ); + + // --- 逐字节: 哪些 tag 字节由数据前缀决定? --- + let mut by_p2: std::collections::HashMap, Vec<[u8; 8]>> = std::collections::HashMap::new(); + for (_, b) in frames.iter() { + by_p2.entry(b[0x3C..0x7C].to_vec()).or_default().push(b[0x22..0x2A].try_into().unwrap()); + } + let mut per_byte = vec![0u64; 8]; + let mut tot = 0u64; + for v in by_p2.values() { + for i in 0..v.len() { + for j in i + 1..v.len() { + tot += 1; + for k in 0..8 { + if v[i][k] == v[j][k] { + per_byte[k] += 1; + } + } + } + } + } + println!("64B前缀组内逐字节匹配率 (总对数{}):", tot); + for k in 0..8 { + println!(" tag[{}]: {} ({:.1}%)", k, per_byte[k], per_byte[k] as f64 * 100.0 / tot.max(1) as f64); + } + + // --- 前缀究竟多短就够决定 tag[0]? 逐级缩短 --- + for plen in [4usize, 8, 16, 24, 32, 48] { + let mut bp: std::collections::HashMap, Vec> = std::collections::HashMap::new(); + for (_, b) in frames.iter() { + bp.entry(b[0x3C..0x3C + plen].to_vec()).or_default().push(b[0x22]); + } + let mut p2 = 0u64; + let mut m2 = 0u64; + for v in bp.values() { + for i in 0..v.len() { + for j in i + 1..v.len() { + p2 += 1; + if v[i] == v[j] { + m2 += 1; + } + } + } + } + if p2 > 0 { + println!("前缀{}B: tag[0]组内匹配 {}/{} ({:.2}%)", plen, m2, p2, m2 as f64 * 100.0 / p2 as f64); + } + } + + println!("\n连续帧 tag 各窗口 u32(LE) 增量 vs 时间差 (前 14 帧):"); + for i in 1..frames.len().min(15) { + let b0 = &frames[i - 1].1; + let b1 = &frames[i].1; + let dt_us = (frames[i].0 - frames[i - 1].0) * 1e6; + let a0 = u32::from_le_bytes(b0[0x23..0x27].try_into().unwrap()); + let a1 = u32::from_le_bytes(b1[0x23..0x27].try_into().unwrap()); + let c0 = u32::from_le_bytes(b0[0x27..0x2B].try_into().unwrap()); + let c1 = u32::from_le_bytes(b1[0x27..0x2B].try_into().unwrap()); + println!( + " dt={:8.0}us Δtag[3..7]={:12} Δtag[7..11](越界到0x2A)={:12} seq {:04x}->{:04x}", + dt_us, + a1.wrapping_sub(a0), + c1.wrapping_sub(c0), + u16::from_le_bytes([b0[0x1A], b0[0x1B]]), + u16::from_le_bytes([b1[0x1A], b1[0x1B]]) + ); + } + + // --- tag[0] = f(seq_lo) 还是 f(data[0])? 一致性检验 --- + let mut m_seq: std::collections::HashMap = std::collections::HashMap::new(); + let mut seq_ok = 0u32; + let mut seq_tot = 0u32; + let mut m_dat: std::collections::HashMap = std::collections::HashMap::new(); + let mut dat_ok = 0u32; + let mut dat_tot = 0u32; + for (_, b) in frames.iter() { + let slo = b[0x1A]; + let d0 = b[0x3C]; + let t0 = b[0x22]; + match m_seq.get(&slo) { + Some(&v) => { seq_tot += 1; if v == t0 { seq_ok += 1; } } + None => { m_seq.insert(slo, t0); } + } + match m_dat.get(&d0) { + Some(&v) => { dat_tot += 1; if v == t0 { dat_ok += 1; } } + None => { m_dat.insert(d0, t0); } + } + } + println!("tag[0] 一致性: 按 seq_lo 分组 {}/{} ({:.1}%) 按 data[0] 分组 {}/{} ({:.1}%)", + seq_ok, seq_tot, seq_ok as f64 * 100.0 / seq_tot.max(1) as f64, + dat_ok, dat_tot, dat_ok as f64 * 100.0 / dat_tot.max(1) as f64); + + println!("样本 (seq, data[0..4], tag[0]):"); + for (_, b) in frames.iter().take(12) { + let seq = u16::from_le_bytes([b[0x1A], b[0x1B]]); + println!(" seq={:04x} data=[{:02x} {:02x} {:02x} {:02x}] tag0={:02x}", + seq, b[0x3C], b[0x3D], b[0x3E], b[0x3F], b[0x22]); + } + let mut diff_map: std::collections::HashMap = std::collections::HashMap::new(); + let mut xor_map: std::collections::HashMap = std::collections::HashMap::new(); + for (_, b) in frames.iter() { + *diff_map.entry(b[0x22].wrapping_sub(b[0x3C])).or_insert(0) += 1; + *xor_map.entry(b[0x22] ^ b[0x3C]).or_insert(0) += 1; + } + let mut dv: Vec<_> = diff_map.iter().collect(); + dv.sort_by_key(|(_, c)| std::cmp::Reverse(**c)); + let mut xv: Vec<_> = xor_map.iter().collect(); + xv.sort_by_key(|(_, c)| std::cmp::Reverse(**c)); + println!("tag[0]-data[0] 差值 top5: {:?}", dv.iter().take(5).collect::>()); + println!("tag[0]^data[0] top5: {:?}", xv.iter().take(5).collect::>()); + + // --- 完整 data[0]→tag[0] 映射表: 双射? GF(2) 仿射? --- + let mut tab: std::collections::HashMap = std::collections::HashMap::new(); + for (_, b) in frames.iter() { + tab.entry(b[0x3C]).or_insert(b[0x22]); + } + let mut vals: Vec = tab.values().cloned().collect(); + vals.sort(); + vals.dedup(); + println!("映射表: 已观测输入 {}/256, 输出唯一值 {} 个 ({}双射)", + tab.len(), vals.len(), if vals.len() == tab.len() { "" } else { "非" }); + // GF(2) 仿射检验: T(x^y^z) == T(x)^T(y)^T(z)^T(0) + if let Some(&t0) = tab.get(&0u8) { + let mut affine_ok = 0u32; + let mut affine_tot = 0u32; + let keys: Vec = tab.keys().cloned().collect(); + for &x in &keys { + for &y in &keys { + let z = x ^ y; + if let (Some(&tx), Some(&ty), Some(&tz)) = (tab.get(&x), tab.get(&y), tab.get(&z)) { + affine_tot += 1; + if tz == tx ^ ty ^ t0 { + affine_ok += 1; + } + } + } + } + println!("GF(2) 仿射检验: {}/{} ({:.1}%)", affine_ok, affine_tot, + affine_ok as f64 * 100.0 / affine_tot.max(1) as f64); + } + // 低 32 项打印 + let mut pairs2: Vec<(u8, u8)> = tab.iter().map(|(&k, &v)| (k, v)).collect(); + pairs2.sort(); + let show: Vec = pairs2.iter().take(32).map(|(k, v)| format!("{:02x}->{:02x}", k, v)).collect(); + println!("样本映射: {}", show.join(" ")); + // 是否 T(x) = x XOR g(x)? 即 delta=T(x)^x 是否只依赖低位 + let delta0: Vec = pairs2.iter().take(16).map(|(k, v)| format!("{:02x}", k ^ v)).collect(); + println!("delta 前16: {}", delta0.join(" ")); + + // --- 全对扫描: tag[i] 与 data[j] 的函数一致性 (8×1024) --- + // 对每对 (i,j): 若 data[j] 相同的两帧 tag[i] 总相同 → 存在函数关系 + println!("\n=== tag[i] ← data[j] 函数一致性扫描 (>50% 才报告) ==="); + let sample: Vec<&Vec> = frames.iter().take(2000).map(|(_, b)| b).collect(); + for i in 0..8usize { + let mut best: Vec<(usize, u32, u32)> = Vec::new(); // (j, ok, tot) + for j in 0..1024usize { + let mut map: std::collections::HashMap = std::collections::HashMap::new(); + let mut ok = 0u32; + let mut tot = 0u32; + for b in &sample { + let dv = b[0x3C + j]; + let tv = b[0x22 + i]; + match map.get(&dv) { + Some(&v) => { + tot += 1; + if v == tv { + ok += 1; + } + } + None => { + map.insert(dv, tv); + } + } + } + if tot > 0 && ok * 2 > tot { + best.push((j, ok, tot)); + } + } + best.sort_by_key(|(_, ok, tot)| std::cmp::Reverse((*ok) * 1000 / (*tot).max(1))); + for (j, ok, tot) in best.iter().take(3) { + println!(" tag[{}] ← data[{}] (CH{}): {}/{} ({:.1}%)", + i, j, j + 1, ok, tot, *ok as f64 * 100.0 / *tot as f64); + } + } + + // --- T 变换验证: tag[0]=T(data[0]) 与 tag[4]=T(data[512]) 是否同一 T --- + // T(x) = (x-1) XOR K, K 为会话相关字节, 从首帧自动导出 + let k0 = frames[0].1[0x22] ^ frames[0].1[0x3C].wrapping_sub(1); + let k4 = frames[0].1[0x26] ^ frames[0].1[0x23C].wrapping_sub(1); + println!("会话 K: tag[0] K={:02x} tag[4] K={:02x} ({})", k0, k4, if k0 == k4 { "相同" } else { "不同" }); + let mut t0_ok = 0u32; + let mut t4_ok = 0u32; + for b in &sample { + if b[0x3C].wrapping_sub(1) ^ k0 == b[0x22] { + t0_ok += 1; + } + if b[0x23C].wrapping_sub(1) ^ k4 == b[0x26] { + t4_ok += 1; + } + } + println!("T(x)=(x-1)^K 验证: tag[0] {}/{} tag[4] {}/{}", + t0_ok, sample.len(), t4_ok, sample.len()); + + // --- 帧率与 universe 边界 --- + let dt_total = frames.last().unwrap().0 - frames[0].0; + println!("帧率: {:.2}/s ({} 帧 / {:.1}s)", (frames.len() - 1) as f64 / dt_total, frames.len(), dt_total); +} + +// ---------- 形态 B 事件帧结构分析 (evtf 子命令) ---------- +pub(crate) fn read_tsv(tsv: &str) -> String { + let raw = std::fs::read(tsv).expect("读 TSV 失败"); + if raw.starts_with(&[0xFF, 0xFE]) { + let u16s: Vec = raw[2..] + .chunks_exact(2) + .map(|c| u16::from_le_bytes([c[0], c[1]])) + .collect(); + String::from_utf16_lossy(&u16s) + } else { + String::from_utf8_lossy(&raw).into_owned() + } +} + +fn hexr(b: &[u8]) -> String { + b.iter().map(|x| format!("{:02x}", x)).collect::() +} + +pub fn evt(tsv: &str, host_ip: &str, dev_ip: &str, tsv2_opt: Option<&str>) { + let content = read_tsv(tsv); + let mut ev: Vec<(f64, Vec)> = Vec::new(); // 主机→设备 事件帧 + let mut dmx: Vec<(f64, Vec)> = Vec::new(); // 主机→设备 DMX-A 帧 + for line in content.lines() { + let p: Vec<&str> = line.split('\t').collect(); + if p.len() < 5 || p[4].trim().is_empty() { + continue; + } + if p[1].trim() != host_ip || p[2].trim() != dev_ip { + continue; + } + let b = match h2b(p[4].trim()) { + Some(b) => b, + None => continue, + }; + if b.len() != 1084 || b[4] != 4 { + continue; + } + let t: f64 = p[0].parse().unwrap_or(0.0); + if b[0x20] == 2 && b[0x21] == 0 { + ev.push((t, b)); + } else if b[0x20] == 1 && b[0x21] == 1 { + dmx.push((t, b)); + } + } + println!("事件B帧: {} DMX-A帧: {}", ev.len(), dmx.len()); + if ev.is_empty() { + return; + } + + // --- 1. 事件序号 @0x22 (u32 LE) 单调性 --- + let mut inc = 0u32; + let mut other = 0u32; + for i in 1..ev.len() { + let s0 = u32::from_le_bytes(ev[i - 1].1[0x22..0x26].try_into().unwrap()); + let s1 = u32::from_le_bytes(ev[i].1[0x22..0x26].try_into().unwrap()); + if s1.wrapping_sub(s0) == 1 { + inc += 1; + } else { + other += 1; + } + } + println!("事件序号@0x22(u32): +1步进={}/{} 其他={}", inc, ev.len() - 1, other); + + // --- 2. 全字节变异图谱 (每偏移的不同值个数, 折叠输出区间) --- + let n = ev.len().min(1500); + let mut kinds: Vec = Vec::with_capacity(1084); // 0=常量 1=低变(2-4) 2=高变(>4) + for off in 0..1084usize { + let mut seen: std::collections::HashSet = std::collections::HashSet::new(); + for (_, b) in ev.iter().take(n) { + seen.insert(b[off]); + if seen.len() > 4 { + break; + } + } + kinds.push(match seen.len() { + 1 => 0, + 2..=4 => 1, + _ => 2, + }); + } + println!("\n=== 变异图谱 ({} 帧样本) ===", n); + let mut i = 0usize; + while i < 1084 { + let k = kinds[i]; + let mut j = i; + while j < 1084 && kinds[j] == k { + j += 1; + } + let label = match k { + 0 => "常量", + 1 => "低变", + _ => "高变", + }; + if k == 0 { + let vals = &ev[0].1[i..j]; + let show = if j - i <= 24 { + hexr(vals) + } else { + format!("{}…({}B)", hexr(&vals[..16]), j - i) + }; + println!(" 0x{:03x}-0x{:03x} ({}B) {} [{}]", i, j - 1, j - i, label, show); + } else { + println!(" 0x{:03x}-0x{:03x} ({}B) {}", i, j - 1, j - i, label); + } + i = j; + } + + // --- 3. 分类: (flag@0x30, 0x3C, 0x3D) --- + let mut classes: std::collections::HashMap<(u8, u8, u8), Vec> = std::collections::HashMap::new(); + for (idx, (_, b)) in ev.iter().enumerate() { + classes + .entry((b[0x30], b[0x3C], b[0x3D])) + .or_default() + .push(idx); + } + let mut cl: Vec<_> = classes.iter().collect(); + cl.sort_by_key(|(_, v)| std::cmp::Reverse(v.len())); + println!("\n=== 分类 (flag@0x30, 0x3C, 0x3D) ==="); + for ((flag, c3c, c3d), idxs) in cl.iter().take(12) { + let b = &ev[idxs[0]].1; + println!( + " flag={:02x} 0x3C={:02x} 0x3D={:02x} : {}帧 例[0x18-0x6F]={}", + flag, c3c, c3d, idxs.len(), + hexr(&b[0x18..0x70]) + ); + } + + // --- 4. 事件帧尾部 vs DMX-A 通道数据 (最近时间帧对齐比较) --- + if !dmx.is_empty() { + println!("\n=== 事件帧区段 vs 最近 DMX-A 通道数据 匹配率 ==="); + let mut di = 0usize; + let mut checked = 0u32; + let mut matches: std::collections::HashMap = std::collections::HashMap::new(); + // 对每个事件帧(采样), 比较 event[eoff..] 与 dmx[0x3C..] 的逐字节一致性 + for (t, b) in ev.iter().step_by(7).take(200) { + while di + 1 < dmx.len() && dmx[di + 1].0 <= *t { + di += 1; + } + let d = &dmx[di].1; + for eoff in [0x3Cusize, 0x44, 0x50, 0x64, 0x84] { + let maxlen = (1084 - eoff).min(1024); + let mut m = 0u32; + for k in 0..maxlen { + if b[eoff + k] == d[0x3C + k] { + m += 1; + } + } + let e = matches.entry(eoff).or_insert((0, 0)); + e.0 += m; + e.1 += maxlen as u32; + } + checked += 1; + } + for eoff in [0x3Cusize, 0x44, 0x50, 0x64, 0x84] { + if let Some((m, tot)) = matches.get(&eoff) { + println!(" event[0x{:x}..] vs dmx[0x3C..]: {:.1}% ({} 帧)", eoff, *m as f64 * 100.0 / *tot as f64, checked); + } + } + } + + // --- 5. 非 RDM 事件帧的数据区抽样 (0x50 起前 64B ×3例) --- + let others: Vec<&(f64, Vec)> = ev + .iter() + .filter(|(_, b)| !(b[0x3C] == 1 && (b[0x3D] == 0x24 || b[0x3D] == 0x18 || b[0x3D] == 0x19))) + .take(3) + .collect(); + println!("\n=== 非 RDM 事件帧抽样 ({} 类帧中取) ===", others.len()); + for (t, b) in others { + println!(" t={:.3} [0x18-0x57]={}", t, hexr(&b[0x18..0x58])); + println!(" [0x58-0x97]={}", hexr(&b[0x58..0x98])); + } + + // --- 6. 逐偏移精确标注 (cmd24/18/19 flag=00 各 1 例 + flag=01 1 例) --- + println!("\n=== 逐偏移标注 (0x2A-0x70) ==="); + for want in [(0u8, 0x24u8), (0, 0x18), (0, 0x19), (1, 0x24)] { + if let Some((_, b)) = ev.iter().find(|(_, b)| b[0x30] == want.0 && b[0x3D] == want.1) { + println!("--- flag={:02x} cmd={:02x} ---", want.0, want.1); + let mut line = String::new(); + for o in 0x2A..0x70usize { + line.push_str(&format!("{:02x}:{:02x} ", o, b[o])); + if (o - 0x29) % 8 == 0 { + println!(" {}", line); + line.clear(); + } + } + if !line.is_empty() { + println!(" {}", line); + } + } + } + + // --- 7. 通道图案起点: 事件帧[o..] vs 前一帧 DMX-A[0x3C..] 全偏移匹配 --- + if !dmx.is_empty() { + println!("\n=== 事件帧通道区起点定位 (vs 前一帧 DMX-A) ==="); + let mut di = 0usize; + let mut off_score: std::collections::HashMap = std::collections::HashMap::new(); + for (t, b) in ev.iter().step_by(11).take(150) { + while di + 1 < dmx.len() && dmx[di + 1].0 <= *t { + di += 1; + } + let d = &dmx[di].1; + for o in (0x3C..=0x80usize).step_by(2) { + let len = (1084 - o).min(900); + let mut m = 0u32; + for k in 0..len { + if b[o + k] == d[0x3C + k] { + m += 1; + } + } + let e = off_score.entry(o).or_insert((0, 0)); + e.0 += m; + e.1 += len as u32; + } + } + let mut scores: Vec<_> = off_score.iter().collect(); + scores.sort_by_key(|(_, (m, tot))| std::cmp::Reverse((*m as u64) * 1000 / (*tot as u64).max(1))); + for (o, (m, tot)) in scores.iter().take(6) { + println!(" 起点 0x{:x}: 匹配 {:.1}%", o, *m as f64 * 100.0 / *tot as f64); + } + // 同样 vs 后一帧 DMX-A + let mut off_score2: std::collections::HashMap = std::collections::HashMap::new(); + let mut di2 = 0usize; + for (t, b) in ev.iter().step_by(11).take(150) { + while di2 + 1 < dmx.len() && dmx[di2].0 < *t { + di2 += 1; + } + if di2 >= dmx.len() { + break; + } + let d = &dmx[di2].1; + for o in (0x3C..=0x80usize).step_by(2) { + let len = (1084 - o).min(900); + let mut m = 0u32; + for k in 0..len { + if b[o + k] == d[0x3C + k] { + m += 1; + } + } + let e = off_score2.entry(o).or_insert((0, 0)); + e.0 += m; + e.1 += len as u32; + } + } + let mut scores2: Vec<_> = off_score2.iter().collect(); + scores2.sort_by_key(|(_, (m, tot))| std::cmp::Reverse((*m as u64) * 1000 / (*tot as u64).max(1))); + println!("--- vs 后一帧 DMX-A ---"); + for (o, (m, tot)) in scores2.iter().take(3) { + println!(" 起点 0x{:x}: 匹配 {:.1}%", o, *m as f64 * 100.0 / *tot as f64); + } + } + + // --- 8. 事件帧通道签名在 DMX-A 全集中的定位 (24B 精确匹配) --- + if !dmx.is_empty() { + println!("\n=== 事件帧通道签名(24B) 在 DMX-A 全集中的定位 ==="); + let mut found = 0u32; + let mut notfound = 0u32; + let mut deltas: Vec = Vec::new(); + let mut offsets: std::collections::HashMap = std::collections::HashMap::new(); + for (t, b) in ev.iter().step_by(37).take(60) { + let rdm_len = u32::from_le_bytes(b[0x34..0x38].try_into().unwrap()) as usize; + let start = 0x3C + rdm_len; + if start + 24 > 1084 { + continue; + } + let sig = &b[start..start + 24]; + let mut hit: Option<(f64, usize)> = None; + 'outer: for (dt, d) in dmx.iter() { + for o in 0..=(1024 - 24) { + if &d[0x3C + o..0x3C + o + 24] == sig { + hit = Some((*dt, o)); + break 'outer; + } + } + } + match hit { + Some((dt, o)) => { + found += 1; + deltas.push(dt - t); + *offsets.entry(o).or_insert(0) += 1; + } + None => notfound += 1, + } + } + println!(" 找到={} 未找到={}", found, notfound); + if !deltas.is_empty() { + deltas.sort_by(|a, b| a.partial_cmp(b).unwrap()); + println!( + " 时间差(匹配帧-事件帧)s: min={:.3} p50={:.3} max={:.3}", + deltas[0], + deltas[deltas.len() / 2], + deltas[deltas.len() - 1] + ); + let mut ov: Vec<_> = offsets.iter().collect(); + ov.sort_by_key(|(_, c)| std::cmp::Reverse(**c)); + println!(" 匹配偏移分布: {:?}", ov.iter().take(6).collect::>()); + } + } + + // --- 9. 跨抓包定位: 事件帧通道签名 vs 另一网段的 DMX-A (tsv2, 双网段同抓) --- + if let Some(tsv2) = tsv2_opt { + let content2 = read_tsv(tsv2); + let mut dmx2: Vec<(f64, Vec)> = Vec::new(); + for line in content2.lines() { + let p: Vec<&str> = line.split('\t').collect(); + if p.len() < 5 || p[4].trim().is_empty() { + continue; + } + let b = match h2b(p[4].trim()) { + Some(b) => b, + None => continue, + }; + if b.len() != 1084 || b[4] != 4 || b[0x20] != 1 || b[0x21] != 1 { + continue; + } + let t: f64 = p[0].parse().unwrap_or(0.0); + dmx2.push((t, b)); + } + println!("\n=== 跨抓包定位 ({} 帧 DMX-A from tsv2) ===", dmx2.len()); + let mut found = 0u32; + let mut deltas: Vec = Vec::new(); + let mut offsets: std::collections::HashMap = std::collections::HashMap::new(); + for (t, b) in ev.iter().step_by(23).take(100) { + let rdm_len = u32::from_le_bytes(b[0x34..0x38].try_into().unwrap()) as usize; + let start = 0x3C + rdm_len; + if start + 24 > 1084 { + continue; + } + let sig = &b[start..start + 24]; + let mut hit: Option<(f64, usize)> = None; + 'outer: for (dt, d) in dmx2.iter() { + for o in 0..=(1024 - 24) { + if &d[0x3C + o..0x3C + o + 24] == sig { + hit = Some((*dt, o)); + break 'outer; + } + } + } + if let Some((dt, o)) = hit { + found += 1; + deltas.push(dt - t); + *offsets.entry(o).or_insert(0) += 1; + } + } + println!(" 跨包找到={}/100", found); + if !deltas.is_empty() { + deltas.sort_by(|a, b| a.partial_cmp(b).unwrap()); + println!( + " 时间差s: min={:.3} p50={:.3} max={:.3}", + deltas[0], + deltas[deltas.len() / 2], + deltas[deltas.len() - 1] + ); + let mut ov: Vec<_> = offsets.iter().collect(); + ov.sort_by_key(|(_, c)| std::cmp::Reverse(**c)); + println!(" 偏移分布: {:?}", ov.iter().take(6).collect::>()); + } + } +} diff --git a/src/enet.rs b/src/enet.rs new file mode 100644 index 0000000..00a7adf --- /dev/null +++ b/src/enet.rs @@ -0,0 +1,239 @@ +// enet.rs - e:net 协议公共设施 +use std::net::{SocketAddr, UdpSocket}; +use std::time::{Duration, Instant}; + +pub const BIND: &str = "192.168.123.113:31772"; +pub const BUTLER: &str = "192.168.123.1:31772"; +pub const BCAST_SUB: &str = "192.168.123.255:31772"; +pub const BCAST_ALL: &str = "255.255.255.255:31772"; + +// ---------- 瀛楁璇诲啓 (灏忕) ---------- +pub fn g16(b: &[u8], o: usize) -> u16 { b[o] as u16 | (b[o + 1] as u16) << 8 } +pub fn g32(b: &[u8], o: usize) -> u32 { + b[o] as u32 | (b[o + 1] as u32) << 8 | (b[o + 2] as u32) << 16 | (b[o + 3] as u32) << 24 +} +pub fn p16(b: &mut [u8], o: usize, v: u16) { b[o] = v as u8; b[o + 1] = (v >> 8) as u8; } +pub fn p32(b: &mut [u8], o: usize, v: u32) { + b[o] = v as u8; b[o + 1] = (v >> 8) as u8; b[o + 2] = (v >> 16) as u8; b[o + 3] = (v >> 24) as u8; +} +pub fn find(b: &[u8], pat: &[u8]) -> bool { + b.windows(pat.len()).any(|w| w == pat) +} +pub fn hexs(b: &[u8]) -> String { b.iter().map(|x| format!("{:02x}", x)).collect() } + +// ---------- 妯℃澘鍔犺浇 (缂栬瘧鏈熷祵鍏?pcap 瀹炴祴鏍锋湰) ---------- +/// 真机 RDM 应答帧池: 每行 "<载荷长度> " +pub fn load_rdm_pool() -> Vec> { + let txt = include_str!("../samples/rdm_pool.txt"); + let mut out = Vec::new(); + for line in txt.lines() { + let line = line.trim(); + if line.is_empty() { continue; } + if let Some(pos) = line.find(' ') { + let hex = line[pos + 1..].trim(); + let b: Vec = (0..hex.len()).step_by(2) + .map(|i| u8::from_str_radix(&hex[i..i + 2], 16).unwrap()).collect(); + out.push(b); + } + } + out +} + +pub fn load_template(name: &str) -> Vec { + let hex = match name { + "discovery_req" => include_str!("../samples/discovery_req_222.hex"), + "discovery_resp" => include_str!("../samples/discovery_resp_259.hex"), + "announce" => include_str!("../samples/announce_686.hex"), + "announce_live" => include_str!("../samples/announce_live.hex"), + "heartbeat" => include_str!("../samples/heartbeat_78.hex"), + "euce" => include_str!("../samples/euce_582.hex"), + "dmx_a" => include_str!("../samples/dmx_a_1126.hex"), + "event_b" => include_str!("../samples/event_b_1126.hex"), + "ack" => include_str!("../samples/ack_84.hex"), + "rdm_req" => include_str!("../samples/rdm_req_1126.hex"), + "rdm_disc_req" => include_str!("../samples/rdm_disc_req_1126.hex"), + "claim_h112" => include_str!("../samples/claim_h112.hex"), + "claim_h114" => include_str!("../samples/claim_h114.hex"), + "claim_h116" => include_str!("../samples/claim_h116.hex"), + "aerep" => include_str!("../samples/aerep_113.hex"), + "status" => include_str!("../samples/status_84.hex"), + "rdm_pulse_0" => include_str!("../samples/rdm_pulse_101_0.hex"), + "rdm_pulse_1" => include_str!("../samples/rdm_pulse_101_1.hex"), + "rdm_pulse_2" => include_str!("../samples/rdm_pulse_101_2.hex"), + "rdm_pulse_3" => include_str!("../samples/rdm_pulse_101_3.hex"), + "rdm_pulse_4" => include_str!("../samples/rdm_pulse_101_4.hex"), + "rdm_resp111" => include_str!("../samples/rdm_resp_111.hex"), + "rdm_setack68" => include_str!("../samples/rdm_setack_68.hex"), + + "rdm_info119" => include_str!("../samples/rdm_info_119.hex"), + "register89" => include_str!("../samples/register_89.hex"), + "aere594" => include_str!("../samples/aere_594.hex"), + "announce144" => include_str!("../samples/announce_144.hex"), + "rdm_disc_req" => include_str!("../samples/rdm_disc_req_1126.hex"), + _ => panic!("unknown template {}", name), + }; + let h = hex.trim(); + (0..h.len()).step_by(2).map(|i| u8::from_str_radix(&h[i..i + 2], 16).unwrap()).collect() +} + +// ---------- Socket ---------- +pub fn open() -> UdpSocket { + let s = UdpSocket::bind(BIND).expect("bind 192.168.123.113:31772 澶辫触(缃戝彛/绔彛鍗犵敤?)"); + s.set_broadcast(true).unwrap(); + s.set_read_timeout(Some(Duration::from_millis(2))).unwrap(); + s +} + +/// SO_REUSEADDR bind (Windows) +#[cfg(target_os = "windows")] +pub fn open_reuse(bind_addr: &str) -> UdpSocket { + use std::os::windows::io::FromRawSocket; + #[repr(C)] + struct SockAddrIn { sin_family: u16, sin_port: u16, sin_addr: u32, sin_zero: [u8; 8] } + #[repr(C)] + struct WsaData { wversion: u16, whighversion: u16, szdescription: [u8; 257], szsystemstatus: [u8; 129], imaxsockets: u16, imaxudpdg: u16, lpvendorinfo: usize } + #[link(name = "ws2_32")] + extern "system" { + fn WSAStartup(wver: u16, data: *mut WsaData) -> i32; + fn socket(af: i32, ty: i32, proto: i32) -> usize; + fn setsockopt(s: usize, level: i32, optname: i32, optval: *const u8, optlen: i32) -> i32; + fn bind(s: usize, addr: *const SockAddrIn, len: i32) -> i32; + } + unsafe { + let mut wd: WsaData = std::mem::zeroed(); + WSAStartup(0x0202, &mut wd); + let s = socket(2, 2, 17); // AF_INET, SOCK_DGRAM, IPPROTO_UDP + assert!(s != usize::MAX, "ws socket 澶辫触"); + const SOL_SOCKET: i32 = 0xFFFF; + const SO_REUSEADDR: i32 = 4; + const SO_BROADCAST: i32 = 6; + let one: u8 = 1; + setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &one, 1); + setsockopt(s, SOL_SOCKET, SO_BROADCAST, &one, 1); + let (ip, port) = bind_addr.rsplit_once(':').unwrap(); + let o: Vec = ip.split('.').map(|x| x.parse().unwrap()).collect(); + let a = SockAddrIn { + sin_family: 2, + sin_port: port.parse::().unwrap().to_be(), + sin_addr: u32::from_ne_bytes([o[0], o[1], o[2], o[3]]), + sin_zero: [0; 8], + }; + let r = bind(s, &a, std::mem::size_of::() as i32); + if r != 0 { + let err: i32 = { extern "system" { fn WSAGetLastError() -> i32; } WSAGetLastError() }; + panic!("reuse bind {} 澶辫触 r={} WSAE={}", bind_addr, r, err); + } + let u = UdpSocket::from_raw_socket(s as _); + u.set_broadcast(true).unwrap(); + u.set_read_timeout(Some(Duration::from_millis(2))).unwrap(); + u + } +} + +/// Linux: 鏍囧噯 bind 鍗冲厑璁稿杩涚▼ SO_REUSEADDR 鍏卞瓨, 鐩存帴杩斿洖 (daemon 鐢? +#[cfg(not(target_os = "windows"))] +pub fn open_reuse(bind_addr: &str) -> UdpSocket { + use std::net::UdpSocket as S; + use std::os::unix::io::FromRawFd; + #[repr(C)] + struct SockAddrIn { sin_family: u16, sin_port: u16, sin_addr: u32, sin_zero: [u8; 8] } + #[link(name = "c")] + extern "C" { + fn socket(af: i32, ty: i32, proto: i32) -> i32; + fn setsockopt(s: i32, level: i32, optname: i32, optval: *const u8, optlen: u32) -> i32; + fn bind(s: i32, addr: *const SockAddrIn, len: u32) -> i32; + } + const AF_INET: i32 = 2; + const SOCK_DGRAM: i32 = 2; + const IPPROTO_UDP: i32 = 17; + const IPPROTO_IP: i32 = 0; + const IP_TTL: i32 = 2; + const IP_MTU_DISCOVER: i32 = 10; + const SOL_SOCKET: i32 = 1; + const SO_REUSEADDR: i32 = 2; + const SO_BROADCAST: i32 = 6; + unsafe { + let s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + assert!(s >= 0, "socket 澶辫触"); + let one: i32 = 1; + setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &one as *const i32 as *const u8, 4); + setsockopt(s, SOL_SOCKET, SO_BROADCAST, &one as *const i32 as *const u8, 4); + // 鎸囩汗瀵归綈鐪熸満 Butler: TTL=100, 鍏抽棴 DF (PMTUDISC_DONT) + let ttl: i32 = 100; + setsockopt(s, IPPROTO_IP, IP_TTL, &ttl as *const i32 as *const u8, 4); + let pmtu: i32 = 0; // IP_PMTUDISC_DONT + setsockopt(s, IPPROTO_IP, IP_MTU_DISCOVER, &pmtu as *const i32 as *const u8, 4); + let (ip, port) = bind_addr.rsplit_once(':').unwrap(); + let o: Vec = ip.split('.').map(|x| x.parse().unwrap()).collect(); + let a = SockAddrIn { + sin_family: AF_INET as u16, + sin_port: port.parse::().unwrap().to_be(), + sin_addr: u32::from_ne_bytes([o[0], o[1], o[2], o[3]]), + sin_zero: [0; 8], + }; + let r = bind(s, &a, std::mem::size_of::() as u32); + if r != 0 { + panic!("reuse bind {} 澶辫触 r={} errno={}", bind_addr, r, std::io::Error::last_os_error()); + } + let u = S::from_raw_fd(s); + u.set_broadcast(true).unwrap(); + u.set_read_timeout(Some(Duration::from_millis(2))).unwrap(); + u + } +} + +/// 鍦?dur 鏃堕棿绐楀唴鏀堕泦鎵€鏈夊埌杈剧殑 UDP 包) +pub fn recv_window(s: &UdpSocket, dur: Duration) -> Vec<(SocketAddr, Vec)> { + let deadline = Instant::now() + dur; + let mut out = Vec::new(); + loop { + let now = Instant::now(); + if now >= deadline { break; } + s.set_read_timeout(Some(deadline.saturating_duration_since(now))).unwrap(); + let mut buf = [0u8; 2048]; + match s.recv_from(&mut buf) { + Ok((n, a)) => out.push((a, buf[..n].to_vec())), + Err(_) => break, // 瓒呮椂 + } + } + out +} + +// ---------- 鏀跺寘鍒嗙被 ---------- +#[derive(Debug, Clone)] +pub enum Rx { + /// Butler 鍙戠幇搴旂瓟 (259B 甯?/ 217B 璐熻浇) + DiscoveryResp(Vec), + /// Butler ACK (84B 甯?/ 42B 璐熻浇) ack_seq + Ack(u32), + /// Butler 鐘舵€佸抚 (>90B 璐熻浇, 0200+Butl) + Status(Vec), + /// RDM 搴旂瓟 (璐熻浇 60-90B, 0x28 闀垮害 + cc01) + RdmResp(Vec, usize), + /// 涓绘満瀹e憡搴旂瓟/鍏朵粬 ENet type=01 + EnetType1(usize), + /// 鍏跺畠 + Other(usize), +} + +pub fn classify(b: &[u8]) -> Rx { + if b.len() < 8 || &b[0..4] != b"ENet" { + return Rx::Other(b.len()); + } + let t = g32(b, 4); + if t == 1 { return Rx::EnetType1(b.len()); } + if t == 4 { + if b.len() == 42 && g16(b, 0x18) == 2 && &b[0x1A..0x1E] == b"Butl" { + return Rx::Ack(g32(b, 0x20)); + } + if b.len() > 0x2C && g16(b, 0x18) == 2 && &b[0x1A..0x1E] == b"Butl" { + let rlen = g16(b, 0x28) as usize; + if b.len() >= 0x2A + rlen && b[0x2A] == 0xCC && b[0x2B] == 0x01 { + return Rx::RdmResp(b.to_vec(), rlen); + } + return Rx::Status(b.to_vec()); + } + if b.len() == 217 { return Rx::DiscoveryResp(b.to_vec()); } + } + Rx::Other(b.len()) +} diff --git a/src/hostdemo.rs b/src/hostdemo.rs new file mode 100644 index 0000000..94057d2 --- /dev/null +++ b/src/hostdemo.rs @@ -0,0 +1,1731 @@ +//! hostdemo.rs — 主机侧等效示例程序 (2026-09-10): 认领真机 + RDM 发现 + 加密 DMX 控制 +//! +//! 全流程不依赖 SymphoLight: +//! 1. 发现 (type=1, 180B) → 217B 应答 +//! 2. sub=01 宣告 → 89B 注册应答 +//! 3. AECH 认领 ×3 轮 (nonce/B12 自选, AERE[0xC0]==G(nonce) 逐轮验证) +//! ★ 真机 .1 只应答源 IP=192.168.123.113 的 AECH (固件过滤, §3.7) +//! 4. RDM 发现: E1.20 DISC_UNIQUE_BRANCH 二分遍历 + DISC 脉冲解码 (§5.4 逆运算) +//! → 每 UID 发 DISC_MUTE(0002) 收 70B ACK → GET DEVICE_INFO/DMX_START_ADDRESS/DEVICE_LABEL +//! 5. DMX 控制: 30fps zPprv0TXRRW 加密帧 (§4.1), 按发现的灯具地址做呼吸图案 +//! +//! 注意: 真机控灯! 且 SymphoLight 若同时运行会插黑场帧 (两主机流交替 → 闪烁)。 + +use std::net::UdpSocket; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +use crate::aech::{agios_dmx_encrypt, agios_g}; +use crate::enet::{load_template, open_reuse}; + +const HOST_RDM_UID: [u8; 6] = [0x45, 0x3a, 0x7f, 0xff, 0xff, 0xff]; // 与真主机一致 (§5.3) +const BCAST_UID: [u8; 6] = [0xff; 6]; + +/// 真机冷启动实测: AECH 认领前主机先发 102B type=2 身份帧 (cold2 @38.087) +/// 主机 IP@0x1C (LE), MAC@0x20 (模板值即本机 123 网卡) +const IDENT102: [u8; 102] = [ + 0x45, 0x4e, 0x65, 0x74, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x7c, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x66, 0x00, 0x00, 0x00, 0x01, 0x00, 0x71, 0x7b, 0xa8, 0xc0, 0x30, 0x0e, + 0xd5, 0x59, 0xbb, 0x36, 0x1a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +/// ★type=02 sub=07 绑定帧 (52B, sym_claim2 @351.1/353.4 — 认领前 2s 各一发): +/// 主机IP+MAC@0x1A/0x1E, 设备IP+MAC@0x25/0x29, ffff/0000@0x2F (两发唯一差异), 6400 f401@0x31 +/// (0x64=100, 0x01f4=500 — 疑似刷新率参数, 原样保留) +fn build_bind52(flag16: [u8; 2]) -> Vec { + let mut f = Vec::with_capacity(52); + f.extend_from_slice(b"ENet"); + f.extend_from_slice(&2u32.to_le_bytes()); + f.extend_from_slice(&[0x00, 0x00, 0x1c, 0x7c]); + f.extend_from_slice(&[0u8; 4]); + f.extend_from_slice(&[0x00, 0x00, 0x01, 0x00]); + f.extend_from_slice(&52u32.to_le_bytes()); + f.extend_from_slice(&[0x07, 0x00]); // sub=07 + f.extend_from_slice(&[0x71, 0x7b, 0xa8, 0xc0]); // 占位: 主机IP 逆序字节 (patch) + f.extend_from_slice(&[0x30, 0x0e, 0xd5, 0x59, 0xbb, 0x36]); // 主机MAC + f.push(0x00); + f.extend_from_slice(&[0x01, 0x7b, 0xa8, 0xc0]); // 设备IP LE = 192.168.123.1 + f.extend_from_slice(&[0x00, 0x16, 0x1c, 0xf1, 0x2e, 0x39]); // 设备MAC + f.extend_from_slice(&flag16); + f.extend_from_slice(&[0x64, 0x00, 0xf4, 0x01]); + f +} + +fn g16(b: &[u8], o: usize) -> u16 { u16::from_le_bytes([b[o], b[o + 1]]) } +fn g32(b: &[u8], o: usize) -> u32 { u32::from_le_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]]) } +fn uid_u64(u: &[u8; 6]) -> u64 { u64::from_be_bytes([0, 0, u[0], u[1], u[2], u[3], u[4], u[5]]) } +fn u64_uid(v: u64) -> [u8; 6] { let b = v.to_be_bytes(); [b[2], b[3], b[4], b[5], b[6], b[7]] } +fn hexs(b: &[u8]) -> String { b.iter().map(|x| format!("{:02x}", x)).collect() } +fn uid_s(u: &[u8; 6]) -> String { hexs(u) } + +struct Rng(u32); +impl Rng { + fn new() -> Self { Rng(SystemTime::now().duration_since(UNIX_EPOCH).unwrap().subsec_nanos() | 1) } + fn next(&mut self) -> u32 { self.0 ^= self.0 << 13; self.0 ^= self.0 >> 17; self.0 ^= self.0 << 5; self.0 & 0x7FFF_FFFF } + fn byte(&mut self) -> u8 { (self.next() >> 8) as u8 } +} + +/// E1.20 请求构建: 在 1084B 事件帧模板上 patch RDM 消息 (§4.2/§5.3) +/// frame: 0x22 qseq(LE), 0x34=msgLen+1(LE), 0x3C 起 = SubSC/msgLen/destUID/srcUID/TN/... +fn build_rdm_frame(dest: &[u8; 6], tn: u8, qseq: u16, cc: u8, pid: u16, pd: &[u8]) -> Vec { + let mut f = load_template("rdm_disc_req"); // 1084B, flag=00 独立帧形态 + let msg_len = 24 + pd.len(); // CC..PD 末 (不含 checksum) + // ★RDM 请求帧头与 DMX 帧不同 (SL 实时帧对照): 计数器/随机槽位必须清零 + f[0x1A..0x20].copy_from_slice(&[0u8; 6]); // 0x1A 计数器(会话内从0) + 0x1E-0x1F + f[0x24..0x2A].copy_from_slice(&[0u8; 6]); // 0x24-0x29 (DMX 帧的 r1/r2 槽位 = 0) + f[0x22] = (qseq & 0xFF) as u8; + f[0x23] = (qseq >> 8) as u8; + f[0x30] = 0; // flag=00 独立查询帧 (真机只应答此类) + f[0x34..0x38].copy_from_slice(&((msg_len + 1) as u32).to_le_bytes()); + f[0x3C] = 0x01; // SubSC + f[0x3D] = msg_len as u8; + f[0x3E..0x44].copy_from_slice(dest); + f[0x44..0x4A].copy_from_slice(&HOST_RDM_UID); + f[0x4A] = tn; + f[0x4B] = 0x00; // 独立帧 port 字段=00 (模板实证; flag=01 内嵌副本才是 01) + f[0x4C] = 0x00; // msgCount + f[0x4D] = 0; f[0x4E] = 0; // subDevice + f[0x4F] = cc; + f[0x50] = (pid >> 8) as u8; + f[0x51] = (pid & 0xFF) as u8; + f[0x52] = pd.len() as u8; + f[0x53..0x53 + pd.len()].copy_from_slice(pd); + // checksum: 含隐式 0xCC 起始字节, 从 CC 起 msgLen 字节 (不含自身), BE + let mut sum: u16 = 0xCC; + for i in 0x3C..0x3C + msg_len { sum = sum.wrapping_add(f[i] as u16); } + let cs = 0x53 + pd.len(); + f[cs] = (sum >> 8) as u8; + f[cs + 1] = (sum & 0xFF) as u8; + f +} + +/// DISC 脉冲解码 (§5.4 逆运算): data = RDM 数据区 (0x2A 起), 返回 Some(UID) 或 None(冲突/非法) +fn decode_pulse(d: &[u8]) -> Option<[u8; 6]> { + if d.len() < 17 || d[0] != 0xAA { return None; } + let mut uid = [0u8; 6]; + let mut sum: u16 = 0; + for i in 0..6 { + let b1 = d[1 + 2 * i]; + let b2 = d[2 + 2 * i]; + // 编码有效性: b1 高位必含 0xAA 模式, b2 必含 0x55 — 违例即多机冲突 + if b1 & 0xAA != 0xAA || b2 & 0x55 != 0x55 { return None; } + uid[i] = (b1 & 0x55) | (b2 & 0xAA); + sum = sum.wrapping_add(b1 as u16).wrapping_add(b2 as u16); + } + let cs_hi = (d[13] & 0x55) | (d[14] & 0xAA); + let cs_lo = (d[15] & 0x55) | (d[16] & 0xAA); + let cs = ((cs_hi as u16) << 8) | cs_lo as u16; + if cs != sum { return None; } // 冲突/损坏 + Some(uid) +} + +/// 从 Butler→主机 应答帧提取 RDM 数据区 (§5.1): 返回 (is_pulse, data) +fn rdm_region(b: &[u8]) -> Option<(bool, &[u8])> { + if b.len() < 0x2E || &b[0..4] != b"ENet" || g32(b, 4) != 4 { return None; } + if g16(b, 0x18) != 0x0002 { return None; } + if &b[0x1A..0x1E] != b"Butl" { return None; } + let rlen = g16(b, 0x28) as usize; + if b.len() < 0x2A + rlen { return None; } + let d = &b[0x2A..0x2A + rlen]; + Some((d[0] == 0xAA, d)) +} + +pub fn run(target: &str, bind_ip: &str, secs: u64) { + let dst = format!("{}:31772", target); + let sock = open_reuse(&format!("{}:31772", bind_ip)); // 源 123.113:31772 (真机过滤+主机惯例) + sock.set_read_timeout(Some(Duration::from_millis(5))).unwrap(); + let mut rng = Rng::new(); + let mut buf = [0u8; 4096]; + + println!("=== 主机侧等效示例: 目标 {} (源 {}) ===", dst, bind_ip); + + // ---- 1) 发现 ---- + let disc = load_template("discovery_req"); + sock.send_to(&disc, &dst).unwrap(); + let mut dev_ok = false; + for _ in 0..150 { + if let Ok((n, src)) = sock.recv_from(&mut buf) { + let b = &buf[..n]; + if n >= 200 && &b[0..4] == b"ENet" && g32(b, 4) == 1 { + println!("[1] 发现应答 {}B from {} (Butler 在线)", n, src); + dev_ok = true; + break; + } + } + } + if !dev_ok { println!("[1] 发现无应答, 终止"); return; } + + // replay: verbatim 重放 SL 的完整会话帧流 (sym_startup t=101.3..136), 双阶段定位 RDM 门控 + if std::env::var("HOSTDEMO_MODE").unwrap_or_default() == "replay" { + let path = std::env::var("REPLAY_FILE") + .unwrap_or_else(|_| "C:/Users/tonycao/AppData/Local/Temp/opencode/replay_frames.txt".into()); + let data = std::fs::read_to_string(&path).expect("replay file"); + let mut frames: Vec<(f32, String, u16, Vec)> = Vec::new(); + for line in data.lines() { + let p: Vec<&str> = line.split('|').collect(); + if p.len() < 4 { continue; } + let t: f32 = p[0].parse().unwrap(); + let dst = p[1].to_string(); + let port: u16 = p[2].parse().unwrap(); + let hex = &p[3]; + let mut b = Vec::with_capacity(hex.len() / 2); + for i in (0..hex.len()).step_by(2) { b.push(u8::from_str_radix(&hex[i..i + 2], 16).unwrap()); } + frames.push((t, dst, port, b)); + } + let t0 = frames[0].0; + for phase in ["A(无认领 verbatim)", "B(先认领再 verbatim)"] { + println!("[R] === 阶段 {}: 重放 {} 帧 / {:.1}s ===", phase, frames.len(), frames[frames.len() - 1].0 - t0); + if phase.starts_with('B') { + // 快速认领 + let o2: Vec = bind_ip.split('.').map(|x| x.parse().unwrap()).collect(); + let ident = load_template("announce144"); + sock.send_to(&ident, &dst).unwrap(); + std::thread::sleep(Duration::from_millis(300)); + let nonce = rng.next(); + let bc = rng.next(); + let mut ch = load_template("claim_h112"); + ch[0x1A..0x1E].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]); + ch[0x88..0x8C].copy_from_slice(&nonce.to_le_bytes()); + ch[0x8C..0x90].copy_from_slice(&bc.to_le_bytes()); + sock.send_to(&ch, &dst).unwrap(); + std::thread::sleep(Duration::from_millis(500)); + println!("[R] 认领已发 (B12=0x{:08x})", bc); + } + let start = Instant::now(); + let mut recv_n = 0u64; + let mut fi = 0; + while start.elapsed().as_secs_f32() < frames[frames.len() - 1].0 - t0 + 3.0 { + let now = start.elapsed().as_secs_f32(); + while fi < frames.len() && frames[fi].0 - t0 <= now { + let (_, d, port, b) = &frames[fi]; + let target_addr = format!("{}:{}", d, port); + let _ = sock.send_to(b, &target_addr); + fi += 1; + } + while let Ok((n, src)) = sock.recv_from(&mut buf) { + recv_n += 1; + let b = &buf[..n]; + println!(" [R] ← len={} src={} head={}", n, src, hexs(&b[..std::cmp::min(20, n)])); + } + std::thread::sleep(Duration::from_millis(2)); + } + println!("[R] 阶段 {} 结束: 收到 {} 帧", phase, recv_n); + std::thread::sleep(Duration::from_millis(1500)); + // 排空残留 + while sock.recv_from(&mut buf).is_ok() {} + } + return; + } + + // sls: session-continue — 不认领! 直接续用 SL 会话 (B12=0x4366937a), SL 实帧做模板 + if std::env::var("HOSTDEMO_MODE").unwrap_or_default() == "sls" { + let path = std::env::var("REPLAY_FILE") + .unwrap_or_else(|_| "C:/Users/tonycao/AppData/Local/Temp/opencode/replay_frames.txt".into()); + let data = std::fs::read_to_string(&path).expect("replay file"); + let sb12: u32 = 0x4366937a; // SL 22:52 会话 B12 (Butler 会话状态绑定的值) + // 从重放清单提取模板: DMX 数据帧 / RDM branch 请求 + let mut base_dmx: Option> = None; + let mut base_rdm: Option> = None; + let mut max_ctr: u32 = 0; + for line in data.lines() { + let p: Vec<&str> = line.split('|').collect(); + if p.len() < 4 { continue; } + let hex = p[3]; + if hex.len() < 0x50 * 2 { continue; } + let b = |o: usize| u8::from_str_radix(&hex[o * 2..o * 2 + 2], 16).unwrap(); + if hex.len() == 1084 * 2 && b(0x18) == 1 && b(0x20) == 1 && b(0x21) == 1 && b(0x3c) != 1 { + let ctr = g32(&hex.as_bytes(), 0); // placeholder never used + let _ = ctr; + let mut fb = Vec::with_capacity(1084); + for i in (0..hex.len()).step_by(2) { fb.push(u8::from_str_radix(&hex[i..i + 2], 16).unwrap()); } + let c = u32::from_le_bytes([fb[0x1A], fb[0x1B], fb[0x1C], fb[0x1D]]); + if c > max_ctr { max_ctr = c; } + if base_dmx.is_none() { base_dmx = Some(fb); } + } + if hex.len() == 1084 * 2 && b(0x3c) == 1 && b(0x30) == 0 && b(0x50) == 0 && b(0x51) == 1 && base_rdm.is_none() { + let mut fb = Vec::with_capacity(1084); + for i in (0..hex.len()).step_by(2) { fb.push(u8::from_str_radix(&hex[i..i + 2], 16).unwrap()); } + base_rdm = Some(fb); + } + } + let dmx_t = base_dmx.expect("no DMX template in replay file"); + let rdm_t = base_rdm.expect("no RDM template in replay file"); + let ctr0 = max_ctr + 10; + println!("[SLS] 会话续用: B12=0x{:08x}, DMX模板 counter 基线 {}", sb12, ctr0); + println!("[SLS] DMX模板 0x2E-0x3B: {}", hexs(&dmx_t[0x2E..0x3C])); + let known: [[u8; 6]; 3] = [ + [0x01, 0x07, 0x34, 0x67, 0x01, 0x25], + [0x01, 0x07, 0x34, 0x88, 0x09, 0x00], + [0x08, 0x99, 0x50, 0x00, 0x26, 0xc9], + ]; + let start = Instant::now(); + let mut ctr = ctr0; + let mut last_hb = Instant::now() - Duration::from_secs(10); + let mut last_rdm = Instant::now() - Duration::from_secs(10); + let mut step = 0usize; // 0=UN_MUTE, 1..=3=窄分支, 4+=GET + let mut addr_list: Vec = Vec::new(); + let mut resp_n = 0u64; + while start.elapsed() < Duration::from_secs(secs.max(60)) { + let t = start.elapsed().as_secs_f32(); + if last_hb.elapsed() >= Duration::from_secs(6) { + let _ = sock.send_to(&load_template("heartbeat"), &dst); + last_hb = Instant::now(); + } + // RDM 任务每 ~700ms 一步 + if t > 1.0 && last_rdm.elapsed() >= Duration::from_millis(700) { + last_rdm = Instant::now(); + let mut f = rdm_t.clone(); + f[0x1A..0x1E].copy_from_slice(&ctr.to_le_bytes()); + let mut pd: Vec = Vec::new(); + let mut pid: u16 = 0x0001; + let mut dest = [0xffu8; 6]; + match step { + 0 => { pid = 0x0003; } // UN_MUTE 广播 + 1..=3 => { + let u = known[step - 1]; + let v = uid_u64(&u); + pd.extend_from_slice(&u64_uid(v - 1)); + pd.extend_from_slice(&u64_uid(v + 1)); + } + _ => { + // GET DEVICE_INFO 轮询已发现地址 + if (step - 4) < known.len() { + dest = known[step - 4]; + pid = 0x0060; + } else { step = 3; } // 循环重试 GET + } + } + let msg_len = 24 + pd.len(); + f[0x3C] = 0x01; + f[0x3D] = msg_len as u8; + f[0x3E..0x44].copy_from_slice(&dest); + f[0x4A] = (step * 7 + 1) as u8; + f[0x4F] = if pid == 0x0001 { 0x10 } else if pid == 0x0060 { 0x20 } else { 0x10 }; + f[0x50] = (pid >> 8) as u8; + f[0x51] = (pid & 0xFF) as u8; + f[0x52] = pd.len() as u8; + f[0x53..0x53 + pd.len()].copy_from_slice(&pd); + let mut sum: u16 = 0xCC; + for i in 0x3C..0x3C + msg_len { sum = sum.wrapping_add(f[i] as u16); } + let cs = 0x53 + pd.len(); + f[cs] = (sum >> 8) as u8; + f[cs + 1] = (sum & 0xFF) as u8; + let mut f2 = f.clone(); + f2[0x30] = 0x01; + f2[0x49] = f2[0x49].wrapping_sub(1); + let _ = sock.send_to(&f, &dst); + let _ = sock.send_to(&f2, &dst); + println!("[SLS] RDM step={} pid={:04x} 已发 (ctr={})", step, pid, ctr); + step += 1; + } + // 收包 + while let Ok((n, src)) = sock.recv_from(&mut buf) { + if src.ip().to_string() != target { continue; } + let b = &buf[..n]; + resp_n += 1; + if let Some((is_pulse, d)) = rdm_region(b) { + if is_pulse { + if let Some(uid) = decode_pulse(d) { + println!(" [SLS] ★脉冲 UID={}", uid_s(&uid)); + } else { + println!(" [SLS] 脉冲(冲突/损坏)"); + } + } else if d.len() >= 0x18 + 19 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0x60] { + let a = u16::from_be_bytes([d[0x18 + 14], d[0x18 + 15]]); + let fp = u16::from_be_bytes([d[0x18 + 10], d[0x18 + 11]]); + println!(" [SLS] ★DEVICE_INFO srcUID={} DMX地址={} 通道={}", uid_s(&[d[9], d[10], d[11], d[12], d[13], d[14]]), a, fp); + if !addr_list.contains(&a) { addr_list.push(a); } + } else { + println!(" [SLS] RDM应答 len={}", d.len()); + } + } + } + // DMX @30fps: 发现完成前黑场, 之后呼吸 + let mut p1 = [0u8; 512]; + if !addr_list.is_empty() { + for (k, &a) in addr_list.iter().enumerate() { + let phase = t * 1.2 + k as f32 * 2.1; + let v = (((phase.sin() * 0.5 + 0.5) * 255.0) * ((t - 1.0) * 0.5).min(1.0).max(0.0)) as u8; + let ch = a.max(1) - 1; + if ch < 512 { p1[ch as usize] = v; } + } + } + let r1 = rng.next(); + let r2 = rng.next(); + let mut f = dmx_t.clone(); + f[0x1A..0x1E].copy_from_slice(&ctr.to_le_bytes()); + f[0x22..0x26].copy_from_slice(&r1.to_le_bytes()); + f[0x26..0x2A].copy_from_slice(&r2.to_le_bytes()); + f[0x3C..0x23C].copy_from_slice(&agios_dmx_encrypt(&p1, r1, sb12)); + f[0x23C..0x43C].copy_from_slice(&agios_dmx_encrypt(&[0u8; 512], r2, sb12)); + let _ = sock.send_to(&f, &dst); + ctr += 1; + std::thread::sleep(Duration::from_millis(33)); + } + println!("[SLS] 结束: 应答 {} 帧, 控制地址 {:?}", resp_n, addr_list); + return; + } + + // hybrid / show: 认领 + 用 SL 实帧骨架, 替换为我们的 DMX 数据 + 我们构造的 RDM + // 区别: hybrid 保留原 SL 数据地址(101/123/1 可选替换); show 固定地址1-32炫彩呼吸 + let mode = std::env::var("HOSTDEMO_MODE").unwrap_or_default(); + if mode == "hybrid" || mode == "show" { + let is_show = mode == "show"; + let label = if is_show { "SHOW" } else { "H" }; + let path = std::env::var("REPLAY_FILE") + .unwrap_or_else(|_| "C:/Users/tonycao/AppData/Local/Temp/opencode/replay_frames.txt".into()); + let data = std::fs::read_to_string(&path).expect("replay file"); + let mut dmxf: Vec> = Vec::new(); // verbatim DMX 帧 + let mut base_rdm: Option> = None; + for line in data.lines() { + let p: Vec<&str> = line.split('|').collect(); + if p.len() < 4 { continue; } + let hex = p[3]; + if hex.len() != 1084 * 2 { continue; } + let b = |o: usize| u8::from_str_radix(&hex[o * 2..o * 2 + 2], 16).unwrap(); + if b(0x18) == 1 && b(0x20) == 1 && b(0x21) == 1 && b(0x3c) != 1 { + let mut fb = Vec::new(); + for i in (0..hex.len()).step_by(2) { fb.push(u8::from_str_radix(&hex[i..i + 2], 16).unwrap()); } + dmxf.push(fb); + } + if base_rdm.is_none() && b(0x3c) == 1 && b(0x30) == 0 && b(0x50) == 0 && b(0x51) == 1 { + let mut fb = Vec::new(); + for i in (0..hex.len()).step_by(2) { fb.push(u8::from_str_radix(&hex[i..i + 2], 16).unwrap()); } + base_rdm = Some(fb); + } + } + // 认领 (announce144 → AECH) + let o2: Vec = bind_ip.split('.').map(|x| x.parse().unwrap()).collect(); + sock.send_to(&load_template("announce144"), &dst).unwrap(); + std::thread::sleep(Duration::from_millis(300)); + let nonce = rng.next(); + let bc = rng.next(); + let mut ch = load_template("claim_h112"); + ch[0x1A..0x1E].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]); + ch[0x88..0x8C].copy_from_slice(&nonce.to_le_bytes()); + ch[0x8C..0x90].copy_from_slice(&bc.to_le_bytes()); + sock.send_to(&ch, &dst).unwrap(); + std::thread::sleep(Duration::from_millis(400)); + println!("[{}] 认领完成 B12=0x{:08x}; {} DMX 帧骨架 + 我们的 RDM", label, bc, + if is_show { "地址1-32 炫彩呼吸" } else { "verbatim" }); + let rdm_t = base_rdm.expect("no RDM template"); + let dmx_a = load_template("dmx_a"); // show 模式 DMX 用此模板(已验证 HMKJ 能解析) + let known: [[u8; 6]; 3] = [ + [0x01, 0x07, 0x34, 0x67, 0x01, 0x25], + [0x01, 0x07, 0x34, 0x88, 0x09, 0x00], + [0x08, 0x99, 0x50, 0x00, 0x26, 0xc9], + ]; + let start = Instant::now(); + let mut di = 0usize; + let mut last_rdm = Instant::now() - Duration::from_secs(10); + let mut step = 0usize; + let mut addr_list: Vec = Vec::new(); + let mut resp_n = 0u64; + let mut state: Vec<([u8; 6], u16, u16)> = Vec::new(); + let mut last_status = Instant::now() - Duration::from_secs(100); + // ★消融实证: 周期宣告/发现广播是 RDM 代理开关 (nobcast 失败, nodmx 成功) + let disc = load_template("discovery_req"); + let mut ann = load_template("announce"); + ann[0x41..0x45].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]); + let mut last_bcast = Instant::now() - Duration::from_secs(10); + while secs == 0 || start.elapsed() < Duration::from_secs(secs) { + let t = start.elapsed().as_secs_f32(); + if last_bcast.elapsed() >= Duration::from_millis(2500) { + let _ = sock.send_to(&disc, "255.255.255.255:31772"); + let _ = sock.send_to(&ann, &format!("{}:31772", target.rsplitn(2, '.').nth(1).unwrap_or("192.168.123").to_string() + ".255")); + last_bcast = Instant::now(); + } + // verbatim DMX 循环 + // DMX 数据替换 + let mut p1 = [0u8; 512]; + if is_show { + // show: 地址 1-32 炫彩呼吸波 (周期放慢, 更舒缓) + let breathe = (t * 0.45).sin() * 0.5 + 0.5; + for i in 0..32usize { + let wave = ((t * 0.6 + i as f32 * 0.55).sin() * 0.5 + 0.5); + p1[i] = (wave * breathe * 255.0) as u8; + } + } else if std::env::var("SHOW_PATTERN").unwrap_or_default() == "1" { + for (k, &a) in [101u16, 123, 1].iter().enumerate() { + let phase = t * 1.0 + k as f32 * 2.1; + let v = (((phase.sin() * 0.5 + 0.5) * 220.0) * (t * 0.3).min(1.0)) as u8; + p1[(a - 1) as usize] = v; + } + } + let rr1 = rng.next(); + let rr2 = rng.next(); + let mut f = if is_show { + let mut fa = dmx_a.clone(); + fa[0x2E] = 2; fa[0x2F] = 0; // 节点序号 (真机 .1 = 2) + fa + } else { + dmxf[di % dmxf.len()].clone() + }; + f[0x1A..0x1E].copy_from_slice(&(di as u32).to_le_bytes()); + f[0x22..0x26].copy_from_slice(&rr1.to_le_bytes()); + f[0x26..0x2A].copy_from_slice(&rr2.to_le_bytes()); + f[0x3C..0x23C].copy_from_slice(&agios_dmx_encrypt(&p1, rr1, bc)); + f[0x23C..0x43C].copy_from_slice(&agios_dmx_encrypt(&[0u8; 512], rr2, bc)); + di += 1; + let _ = sock.send_to(&f, &dst); + if di % 30 == 0 { let _ = sock.send_to(&load_template("heartbeat"), &dst); } + // RDM 任务 + if t > 2.0 && last_rdm.elapsed() >= Duration::from_millis(700) { + last_rdm = Instant::now(); + let mut f = rdm_t.clone(); + let mut pd: Vec = Vec::new(); + let mut pid: u16 = 0x0001; + let mut dest = [0xffu8; 6]; + let mut cc = 0x10u8; + match step { + 0 => { pid = 0x0003; } + 1..=3 => { + let v = uid_u64(&known[step - 1]); + pd.extend_from_slice(&u64_uid(v - 1)); + pd.extend_from_slice(&u64_uid(v + 1)); + } + _ => { + if (step - 4) < known.len() { dest = known[step - 4]; pid = 0x0060; cc = 0x20; } + else { step = 3; } + } + } + let msg_len = 24 + pd.len(); + f[0x1A..0x1E].copy_from_slice(&(di as u32).to_le_bytes()); + f[0x3D] = msg_len as u8; + f[0x3E..0x44].copy_from_slice(&dest); + f[0x4A] = (step * 7 + 1) as u8; + f[0x4F] = cc; + f[0x50] = (pid >> 8) as u8; + f[0x51] = (pid & 0xFF) as u8; + f[0x52] = pd.len() as u8; + f[0x53..0x53 + pd.len()].copy_from_slice(&pd); + let mut sum: u16 = 0xCC; + for i in 0x3C..0x3C + msg_len { sum = sum.wrapping_add(f[i] as u16); } + let cs = 0x53 + pd.len(); + f[cs] = (sum >> 8) as u8; + f[cs + 1] = (sum & 0xFF) as u8; + let mut f2 = f.clone(); + f2[0x30] = 0x01; + f2[0x49] = f2[0x49].wrapping_sub(1); + let _ = sock.send_to(&f, &dst); + let _ = sock.send_to(&f2, &dst); + println!("[{}] RDM step={} pid={:04x}", label, step, pid); + step += 1; + } + while let Ok((n, src)) = sock.recv_from(&mut buf) { + if src.ip().to_string() != target { continue; } + let b = &buf[..n]; + resp_n += 1; + if let Some((is_pulse, d)) = rdm_region(b) { + if is_show { + // show 模式维护状态表 + if is_pulse { + if let Some(uid) = decode_pulse(d) { + println!(" [{}] ★脉冲 UID={}", label, uid_s(&uid)); + } + } else if d.len() >= 0x18 + 19 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0x60] { + let uid = [d[9], d[10], d[11], d[12], d[13], d[14]]; + let a = u16::from_be_bytes([d[0x18 + 14], d[0x18 + 15]]); + let fp = u16::from_be_bytes([d[0x18 + 10], d[0x18 + 11]]); + if let Some(e) = state.iter_mut().find(|(u, _, _)| *u == uid) { + if e.1 != a || e.2 != fp { + println!(" [{}] ★{} 状态变更: 地址 {}→{} 通道 {}→{}", label, uid_s(&uid), e.1, a, e.2, fp); + e.1 = a; e.2 = fp; + } + } else { + state.push((uid, a, fp)); + println!(" [{}] ★设备注册: {} 地址={} 通道={} 传感器={}", label, uid_s(&uid), a, fp, d[0x18 + 18]); + } + } else if d.len() >= 0x18 + 2 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0xF0] { + let uid = [d[9], d[10], d[11], d[12], d[13], d[14]]; + let a = u16::from_be_bytes([d[0x18], d[0x19]]); + if let Some(e) = state.iter_mut().find(|(u, _, _)| *u == uid) { + if e.1 != a { + println!(" [{}] ★{} START_ADDRESS 变更: {}→{}", label, uid_s(&uid), e.1, a); + e.1 = a; + } + } + } + } else { + if is_pulse { + if let Some(uid) = decode_pulse(d) { println!(" [H] ★脉冲 UID={}", uid_s(&uid)); } + else { println!(" [H] 脉冲(冲突)"); } + } else if d.len() >= 0x18 + 19 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0x60] { + let a = u16::from_be_bytes([d[0x18 + 14], d[0x18 + 15]]); + let fp = u16::from_be_bytes([d[0x18 + 10], d[0x18 + 11]]); + println!(" [H] ★DEVICE_INFO srcUID={} 地址={} 通道={}", uid_s(&[d[9], d[10], d[11], d[12], d[13], d[14]]), a, fp); + if !addr_list.contains(&a) { addr_list.push(a); } + } else { + println!(" [H] RDM应答 len={}", d.len()); + } + } + } + } + // show 模式每 10s 打印状态汇总 + if is_show && last_status.elapsed() >= Duration::from_secs(10) { + last_status = Instant::now(); + let lines: Vec = state.iter().map(|(u, a, fp)| format!("{} @DMX{} ({}ch)", uid_s(u), a, fp)).collect(); + println!("[SHOW] 状态: {} 台 | {} | 收帧{}", state.len(), lines.join(" | "), resp_n); + } + std::thread::sleep(Duration::from_millis(33)); + } + println!("[{}] 结束: 应答 {} 帧, 地址 {:?}", label, resp_n, if is_show { state.iter().map(|(_, a, _)| *a).collect::>() } else { addr_list.clone() }); + return; + } + + // slfull: 认领 + verbatim 重放 SL 会话(循环) + 被动解码 RDM 应答 → 发现灯具并打印地址 + // (绕过未破解的变长 RDM 帧结构: 用 SL 原帧当查询, 我们解码应答) + if std::env::var("HOSTDEMO_MODE").unwrap_or_default() == "slfull" { + let path = std::env::var("REPLAY_FILE") + .unwrap_or_else(|_| "C:/Users/tonycao/AppData/Local/Temp/opencode/replay_frames.txt".into()); + let data = std::fs::read_to_string(&path).expect("replay file"); + let mut frames: Vec<(f32, String, u16, Vec)> = Vec::new(); + for line in data.lines() { + let p: Vec<&str> = line.split('|').collect(); + if p.len() < 4 { continue; } + let t: f32 = p[0].parse().unwrap(); + let dst = p[1].to_string(); + let port: u16 = p[2].parse().unwrap(); + let hex = p[3]; + let mut b = Vec::new(); + for i in (0..hex.len()).step_by(2) { b.push(u8::from_str_radix(&hex[i..i + 2], 16).unwrap()); } + frames.push((t, dst, port, b)); + } + let t0 = frames[0].0; + let dur = frames[frames.len() - 1].0 - t0; + // 认领 + let o2: Vec = bind_ip.split('.').map(|x| x.parse().unwrap()).collect(); + sock.send_to(&load_template("announce144"), &dst).unwrap(); + std::thread::sleep(Duration::from_millis(300)); + let nonce = rng.next(); + let bc = rng.next(); + let mut ch = load_template("claim_h112"); + ch[0x1A..0x1E].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]); + ch[0x88..0x8C].copy_from_slice(&nonce.to_le_bytes()); + ch[0x8C..0x90].copy_from_slice(&bc.to_le_bytes()); + sock.send_to(&ch, &dst).unwrap(); + std::thread::sleep(Duration::from_millis(500)); + println!("[F] 认领完成; 开始循环重放 {} 帧 ({:.1}s/轮), 被动解码 RDM 应答", frames.len(), dur); + let start = Instant::now(); + let mut seen_uids: Vec<[u8; 6]> = Vec::new(); + let mut addr_map: Vec<([u8; 6], u16)> = Vec::new(); + let mut resp_n = 0u64; + let mut pat_ctr: u32 = 0; + while start.elapsed() < Duration::from_secs(secs.max(120)) { + let play_start = Instant::now(); + for (t, d, port, b) in frames.iter() { + let target_t = Duration::from_secs_f32(t - t0); + let now = play_start.elapsed(); + if target_t > now { std::thread::sleep(target_t - now); } + let addr = format!("{}:{}", d, port); + // ★DMX 数据帧: 替换为我们自己的呼吸图案 (用认领 B12 加密 → Butler 解密上灯) + let mut fb = b.clone(); + if fb.len() == 1084 && fb[0x18] == 1 && fb[0x20] == 1 && fb[0x21] == 1 && fb[0x3C] != 1 { + pat_ctr += 1; + let tt = pat_ctr as f32 / 30.0; + let mut p1 = [0u8; 512]; + for (k, &a) in [101u16, 123, 1].iter().enumerate() { + let phase = tt * 1.0 + k as f32 * 2.1; + let v = (((phase.sin() * 0.5 + 0.5) * 220.0) * (tt * 0.3).min(1.0)) as u8; + p1[(a - 1) as usize] = v; + } + let r1 = rng.next(); + let r2 = rng.next(); + fb[0x22..0x26].copy_from_slice(&r1.to_le_bytes()); + fb[0x26..0x2A].copy_from_slice(&r2.to_le_bytes()); + fb[0x3C..0x23C].copy_from_slice(&agios_dmx_encrypt(&p1, r1, bc)); + fb[0x23C..0x43C].copy_from_slice(&agios_dmx_encrypt(&[0u8; 512], r2, bc)); + } + let _ = sock.send_to(&fb, &addr); + // 收包 + while let Ok((n, src)) = sock.recv_from(&mut buf) { + if src.ip().to_string() != target { continue; } + let bb = &buf[..n]; + resp_n += 1; + if let Some((is_pulse, dd)) = rdm_region(bb) { + if is_pulse { + if let Some(uid) = decode_pulse(dd) { + if !seen_uids.contains(&uid) { + seen_uids.push(uid); + println!(" [F] ★发现灯具 UID={}", uid_s(&uid)); + } + } + } else if dd.len() >= 0x18 + 19 && dd[0] == 0xCC && dd[0x15..0x17] == [0x00, 0x60] { + let src_uid = [dd[9], dd[10], dd[11], dd[12], dd[13], dd[14]]; + let a = u16::from_be_bytes([dd[0x18 + 14], dd[0x18 + 15]]); + let fp = u16::from_be_bytes([dd[0x18 + 10], dd[0x18 + 11]]); + if !addr_map.iter().any(|(u, _)| *u == src_uid) { + addr_map.push((src_uid, a)); + println!(" [F] ★{} → DMX地址 {} ({}通道)", uid_s(&src_uid), a, fp); + } + } else if dd.len() >= 0x18 + 2 && dd[0] == 0xCC && dd[0x15..0x17] == [0x00, 0xF0] { + let src_uid = [dd[9], dd[10], dd[11], dd[12], dd[13], dd[14]]; + let a = u16::from_be_bytes([dd[0x18], dd[0x19]]); + if !addr_map.iter().any(|(u, _)| *u == src_uid) { + addr_map.push((src_uid, a)); + println!(" [F] ★{} → DMX_START_ADDRESS={}", uid_s(&src_uid), a); + } + } + } + } + } + } + println!("[F] 结束: 应答 {} 帧; 灯具 {} 台: {:?}; 地址: {:?}", resp_n, seen_uids.len(), + seen_uids.iter().map(uid_s).collect::>(), addr_map); + return; + } + + // slbisect: SL 原帧逐字段突变 — 定位 Butler 拒收我们 RDM 帧的确切字段 + // 突变序列: verbatim(对照) → counter → TN → qseq → 全部 → 纯我们构造 + if std::env::var("HOSTDEMO_MODE").unwrap_or_default() == "slbisect" { + // 认领 + let o2: Vec = bind_ip.split('.').map(|x| x.parse().unwrap()).collect(); + sock.send_to(&load_template("announce144"), &dst).unwrap(); + std::thread::sleep(Duration::from_millis(300)); + let nonce = rng.next(); + let bc = rng.next(); + let mut ch = load_template("claim_h112"); + ch[0x1A..0x1E].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]); + ch[0x88..0x8C].copy_from_slice(&nonce.to_le_bytes()); + ch[0x8C..0x90].copy_from_slice(&bc.to_le_bytes()); + sock.send_to(&ch, &dst).unwrap(); + std::thread::sleep(Duration::from_millis(500)); + println!("[B] 认领完成 B12=0x{:08x}", bc); + // verbatim DMX 背景流 + 突变 branch 查询 + let path = std::env::var("REPLAY_FILE") + .unwrap_or_else(|_| "C:/Users/tonycao/AppData/Local/Temp/opencode/replay_frames.txt".into()); + let data = std::fs::read_to_string(&path).expect("replay file"); + let mut dmxf: Vec> = Vec::new(); + for line in data.lines() { + let p: Vec<&str> = line.split('|').collect(); + if p.len() < 4 { continue; } + let hex = p[3]; + if hex.len() != 1084 * 2 { continue; } + let byte = |o: usize| u8::from_str_radix(&hex[o * 2..o * 2 + 2], 16).unwrap(); + if byte(0x18) == 1 && byte(0x20) == 1 && byte(0x21) == 1 && byte(0x3c) != 1 { + let mut fb = Vec::new(); + for i in (0..hex.len()).step_by(2) { fb.push(u8::from_str_radix(&hex[i..i + 2], 16).unwrap()); } + dmxf.push(fb); + } + } + let sl_branch = std::fs::read_to_string("C:/Users/tonycao/AppData/Local/Temp/opencode/sl_branch_exact.hex") + .unwrap_or_else(|_| String::new()); + let mut slb: Vec = Vec::new(); + { + let h = sl_branch.trim(); + for i in (0..h.len()).step_by(2) { slb.push(u8::from_str_radix(&h[i..i + 2], 16).unwrap()); } + } + // NFS1K2D GET DEVICE_INFO 探针 (被 MUTE 的设备也须应答 GET — 应答可观测) + let sl_get = std::fs::read_to_string("C:/Users/tonycao/AppData/Local/Temp/opencode/sl_get_exact.hex") + .unwrap_or_default(); + let mut slg: Vec = Vec::new(); + { + let h = sl_get.trim(); + for i in (0..h.len()).step_by(2) { slg.push(u8::from_str_radix(&h[i..i + 2], 16).unwrap()); } + } + let known: [[u8; 6]; 3] = [ + [0x01, 0x07, 0x34, 0x67, 0x01, 0x25], + [0x01, 0x07, 0x34, 0x88, 0x09, 0x00], + [0x08, 0x99, 0x50, 0x00, 0x26, 0xc9], + ]; + println!("[B] 开始突变序列 (GET→NFS; 每步 1s DMX 背景 + 发送 + 500ms 侦听)"); + let mut di = 0usize; + let mutations = ["verbatim对照", "counter=ours", "TN=ours", "qseq=ours", "全突变"]; + for round in 0..2 { + for (mi, mname) in mutations.iter().enumerate() { + let seg = Instant::now(); + while seg.elapsed() < Duration::from_millis(1000) { + let f = &dmxf[di % dmxf.len()]; + di += 1; + let _ = sock.send_to(f, &dst); + if di % 30 == 0 { let _ = sock.send_to(&load_template("heartbeat"), &dst); } + std::thread::sleep(Duration::from_millis(33)); + while sock.recv_from(&mut buf).is_ok() {} + } + let mut f = slg.clone(); + let ctr = (di as u32).wrapping_add(9000); + match mi { + 1 => { f[0x1A..0x1E].copy_from_slice(&ctr.to_le_bytes()); } + 2 => { f[0x4A] = 0x77; } + 3 => { f[0x22] = 0x55; f[0x23] = 0x00; } + 4 => { + f[0x1A..0x1E].copy_from_slice(&ctr.to_le_bytes()); + f[0x4A] = 0x78; + f[0x22] = 0x56; f[0x23] = 0x00; + } + _ => {} + } + let _ = sock.send_to(&f, &dst); + let mut f2 = f.clone(); + f2[0x30] = 0x01; + f2[0x49] = f2[0x49].wrapping_sub(1); + let _ = sock.send_to(&f2, &dst); + let mut info = false; + let mut any = 0usize; + let w = Instant::now(); + // ★边发 DMX 边侦听 (Butler 仅在 DMX 流持续时吐应答, 停流即饿死) + while w.elapsed() < Duration::from_millis(500) { + let f = &dmxf[di % dmxf.len()]; + di += 1; + let _ = sock.send_to(f, &dst); + if di % 30 == 0 { let _ = sock.send_to(&load_template("heartbeat"), &dst); } + while let Ok((n, src)) = sock.recv_from(&mut buf) { + if src.ip().to_string() != target { continue; } + let b = &buf[..n]; + any += 1; + if let Some((false, d)) = rdm_region(b) { + if d.len() >= 0x18 + 19 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0x60] { + info = true; + } + } + } + std::thread::sleep(Duration::from_millis(33)); + } + println!(" [B] r{} {:<14} → 收帧 {} DEVICE_INFO={}", round, mname, any, info); + } + } + let _ = known; + return; + } + + // show: 地址 1-32 炫彩呼吸持续播放 + 并行 RDM 状态轮询 (DEVICE_INFO / DMX 地址) + // 旧的 show 实现已合并到上方 hybrid/show 块;保留结构,条件永假 + if false { + let o2: Vec = bind_ip.split('.').map(|x| x.parse().unwrap()).collect(); + let bcast_subnet = format!("{}:31772", target.rsplitn(2, '.').nth(1).unwrap_or("192.168.123").to_string() + ".255"); + // ---- 冷启动前奏 (默认模式同款; RDM 代理门控, 短前奏 RDM 无应答) ---- + let pre_secs: u64 = std::env::var("HOSTDEMO_PRE").ok().and_then(|s| s.parse().ok()).unwrap_or(30); + let disc = load_template("discovery_req"); + let euce = load_template("euce"); + let pre = Instant::now(); + let mut last = Instant::now() - Duration::from_secs(10); + let mut last_euce = Instant::now() - Duration::from_secs(10); + while pre.elapsed() < Duration::from_secs(pre_secs) { + if last.elapsed() >= Duration::from_secs(2) { + let _ = sock.send_to(&disc, "255.255.255.255:31772"); + let mut ann = build_fresh_announce(&mut rng); + ann[0x41..0x45].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]); + let _ = sock.send_to(&ann, &bcast_subnet); + last = Instant::now(); + } + if last_euce.elapsed() >= Duration::from_secs(6) { + let _ = sock.send_to(&euce, "255.255.255.255:31772"); + last_euce = Instant::now(); + } + if let Ok((n, src)) = sock.recv_from(&mut buf) { + println!(" [SHOW/前奏] 收帧 len={} src={}", n, src); + } + } + for flag in [[0xff_u8, 0xff_u8], [0x00, 0x00], [0xff, 0xff], [0x00, 0x00]] { + let mut b = build_bind52(flag); + b[0x1A..0x1E].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]); + sock.send_to(&b, &dst).unwrap(); + std::thread::sleep(Duration::from_millis(320)); + } + std::thread::sleep(Duration::from_millis(1800)); + let mut ident = IDENT102.to_vec(); + ident[0x1C..0x20].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]); + sock.send_to(&ident, &dst).unwrap(); + std::thread::sleep(Duration::from_millis(300)); + // 认领 + let nonce = rng.next(); + let bc = rng.next(); + let mut ch = load_template("claim_h112"); + ch[0x1A..0x1E].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]); + ch[0x88..0x8C].copy_from_slice(&nonce.to_le_bytes()); + ch[0x8C..0x90].copy_from_slice(&bc.to_le_bytes()); + sock.send_to(&ch, &dst).unwrap(); + std::thread::sleep(Duration::from_millis(400)); + println!("[SHOW] 认领完成 B12=0x{:08x}; 地址1-32 炫彩呼吸 + RDM 状态轮询 (Ctrl+C 停止)", bc); + let start = Instant::now(); + // SL 实帧 RDM 模板 (自构造帧 Butler 不响应, SL 帧响应 — 实测结论) + let sl_rdm_tpl: Option> = std::fs::read_to_string("C:/Users/tonycao/AppData/Local/Temp/opencode/sl_get_exact.hex").ok().map(|h| { + let h = h.trim(); + let mut v = Vec::new(); + for i in (0..h.len()).step_by(2) { v.push(u8::from_str_radix(&h[i..i + 2], 16).unwrap()); } + v + }); + + let disc = load_template("discovery_req"); + let mut ann = load_template("announce"); + ann[0x41..0x45].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]); + let known: [[u8; 6]; 3] = [ + [0x01, 0x07, 0x34, 0x67, 0x01, 0x25], + [0x01, 0x07, 0x34, 0x88, 0x09, 0x00], + [0x08, 0x99, 0x50, 0x00, 0x26, 0xc9], + ]; + // 状态表: UID → (DMX地址, 通道数) + let mut state: Vec<([u8; 6], u16, u16)> = Vec::new(); + let mut di = 0usize; + let mut last_bcast = Instant::now() - Duration::from_secs(10); + let mut last_hb = Instant::now() - Duration::from_secs(10); + let mut last_rdm = Instant::now() - Duration::from_secs(10); + let mut poll_idx = 0usize; + let mut addr_req_pending = false; + let mut last_status = Instant::now() - Duration::from_secs(100); + let mut resp_n: u64 = 0; + loop { + // 广播 (RDM 代理开关) + if last_bcast.elapsed() >= Duration::from_millis(2500) { + let _ = sock.send_to(&disc, "255.255.255.255:31772"); + let _ = sock.send_to(&ann, &format!("{}:31772", target.rsplitn(2, '.').nth(1).unwrap_or("192.168.123").to_string() + ".255")); + last_bcast = Instant::now(); + } + // 心跳 + if last_hb.elapsed() >= Duration::from_secs(1) { + let _ = sock.send_to(&load_template("heartbeat"), &dst); + last_hb = Instant::now(); + } + // RDM 轮询: 每 700ms 一步, 轮转 3 台 × (DEVICE_INFO / START_ADDRESS) + if last_rdm.elapsed() >= Duration::from_millis(700) { + last_rdm = Instant::now(); + let uid = known[poll_idx % 3]; + let (pid, cc) = if addr_req_pending { (0x00F0u16, 0x20u8) } else { (0x0060, 0x20) }; + // RDM 请求用 SL 实帧模板 (实测: 自构造帧 Butler 不响应, SL 帧响应) + let mut f = match &sl_rdm_tpl { + Some(t) => { + let mut t = t.clone(); + t[0x1A..0x1E].copy_from_slice(&(di as u32).to_le_bytes()); + t[0x3D] = 24; // msgLen (PDL=0) + t[0x3E..0x44].copy_from_slice(&uid); + t[0x4A] = (poll_idx * 13 + 1) as u8; + t[0x4F] = cc; + t[0x50] = (pid >> 8) as u8; + t[0x51] = (pid & 0xFF) as u8; + t[0x52] = 0; + let mut sum: u16 = 0xCC; + for i in 0x3C..0x3C + 24 { sum = sum.wrapping_add(t[i] as u16); } + t[0x5F] = (sum >> 8) as u8; + t[0x60] = (sum & 0xFF) as u8; + t + } + None => build_rdm_frame(&uid, (poll_idx * 13 + 1) as u8, poll_idx as u16, cc, pid, &[]), + }; + let mut f2 = f.clone(); + f2[0x30] = 0x01; + f2[0x49] = f2[0x49].wrapping_sub(1); + let _ = sock.send_to(&f, &dst); + let _ = sock.send_to(&f2, &dst); + poll_idx += 1; + if poll_idx % 3 == 0 { addr_req_pending = !addr_req_pending; } + } + // 收包: 解析 DEVICE_INFO / START_ADDRESS, 报告变更 + while let Ok((n, src)) = sock.recv_from(&mut buf) { + if src.ip().to_string() != target { continue; } + let b = &buf[..n]; + if let Some((false, d)) = rdm_region(b) { + if d.len() >= 0x18 + 19 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0x60] { + let uid = [d[9], d[10], d[11], d[12], d[13], d[14]]; + let a = u16::from_be_bytes([d[0x18 + 14], d[0x18 + 15]]); + let fp = u16::from_be_bytes([d[0x18 + 10], d[0x18 + 11]]); + match state.iter_mut().find(|(u, _, _)| *u == uid) { + Some(e) => { + if e.1 != a || e.2 != fp { + println!(" [SHOW] ★{} 状态变更: 地址 {}→{} 通道 {}→{}", uid_s(&uid), e.1, a, e.2, fp); + e.1 = a; e.2 = fp; + } + } + None => { + state.push((uid, a, fp)); + println!(" [SHOW] ★设备注册: {} 地址={} 通道={} 传感器={}", uid_s(&uid), a, fp, d[0x18 + 18]); + } + } + } else if d.len() >= 0x18 + 2 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0xF0] { + let uid = [d[9], d[10], d[11], d[12], d[13], d[14]]; + let a = u16::from_be_bytes([d[0x18], d[0x19]]); + if let Some(e) = state.iter_mut().find(|(u, _, _)| *u == uid) { + if e.1 != a { + println!(" [SHOW] ★{} START_ADDRESS 变更: {}→{}", uid_s(&uid), e.1, a); + e.1 = a; + } + } + } + } + } + // 每 10s 打印一次状态汇总 + if last_status.elapsed() >= Duration::from_secs(10) { + last_status = Instant::now(); + let lines: Vec = state.iter().map(|(u, a, fp)| format!("{} @DMX{} ({}ch)", uid_s(u), a, fp)).collect(); + println!("[SHOW] 状态: {} 台 | {} | 收帧{}", state.len(), lines.join(" | "), resp_n); + } + // DMX @30fps: 地址 1-32 彩虹波 × 呼吸包络 + let t = start.elapsed().as_secs_f32(); + di += 1; + let mut p1 = [0u8; 512]; + let breathe = (t * 0.9).sin() * 0.5 + 0.5; // 全局呼吸包络 0..1 + for i in 0..32usize { + let wave = ((t * 1.2 + i as f32 * 0.55).sin() * 0.5 + 0.5); // 行波 + p1[i] = (wave * breathe * 255.0) as u8; + } + let rr1 = rng.next(); + let rr2 = rng.next(); + let mut f = load_template("dmx_a"); + f[0x1A..0x1E].copy_from_slice(&(di as u32).to_le_bytes()); + f[0x22..0x26].copy_from_slice(&rr1.to_le_bytes()); + f[0x26..0x2A].copy_from_slice(&rr2.to_le_bytes()); + f[0x3C..0x23C].copy_from_slice(&agios_dmx_encrypt(&p1, rr1, bc)); + f[0x23C..0x43C].copy_from_slice(&agios_dmx_encrypt(&[0u8; 512], rr2, bc)); + let _ = sock.send_to(&f, &dst); + std::thread::sleep(Duration::from_millis(33)); + } + } + + let mut b12 = rng.next(); + let mut claim_ok = false; + + // claimonly/quick: 最小认领 (announce144 → 47B → AECH) — 实测成功配方, 无绑定/宣告/前奏 + let hd_mode = std::env::var("HOSTDEMO_MODE").unwrap_or_default(); + if hd_mode.starts_with("claim") || hd_mode == "quick" { + let o2: Vec = bind_ip.split('.').map(|x| x.parse().unwrap()).collect(); + let full = hd_mode == "claimfull"; + if full { + // sub=07 绑定帧 ×4 (ffff→0000→ffff→0000, 0.32s 间隔) + for flag in [[0xff_u8, 0xff_u8], [0x00, 0x00], [0xff, 0xff], [0x00, 0x00]] { + let mut b = build_bind52(flag); + b[0x1A..0x1E].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]); + sock.send_to(&b, &dst).unwrap(); + std::thread::sleep(Duration::from_millis(320)); + } + std::thread::sleep(Duration::from_millis(1500)); + } + let mut ident = IDENT102.to_vec(); + ident[0x1C..0x20].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]); + // runs1-3 成功配方: 仅 announce144 (102B 身份帧) 单播一次, 无绑定/EUCE/宣告 + let ident144 = if full { ident } else { load_template("announce144") }; + sock.send_to(&ident144, &dst).unwrap(); + let mut reg = false; + let w = Instant::now(); + while w.elapsed() < Duration::from_millis(400) { + if let Ok((n, src)) = sock.recv_from(&mut buf) { + if src.ip().to_string() == target && n == 47 { reg = true; break; } + } + } + let nonce = rng.next(); + let b12c = rng.next(); + let mut ch = load_template("claim_h112"); + ch[0x1A..0x1E].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]); + ch[0x88..0x8C].copy_from_slice(&nonce.to_le_bytes()); + ch[0x8C..0x90].copy_from_slice(&b12c.to_le_bytes()); + sock.send_to(&ch, &dst).unwrap(); + let w2 = Instant::now(); + while w2.elapsed() < Duration::from_millis(700) { + if let Ok((n, _)) = sock.recv_from(&mut buf) { + let b = &buf[..n]; + if n > 0xC4 && b.windows(4).any(|w| w == b"AERE") { + let resp = g32(b, 0xC0); + println!("CLAIM_PROBE reg89={} AERE resp=0x{:08x} G=0x{:08x} {}", reg, resp, agios_g(nonce), + if resp == agios_g(nonce) { "PASS" } else { "FAIL" }); + if resp == agios_g(nonce) { + // quick 模式: 认领成功后直通 RDM+DMX 阶段 (跳过绑定/宣告/前奏 — 实测有毒) + if hd_mode == "quick" { + b12 = b12c; + claim_ok = true; + break; + } + } + if !claim_ok { return; } + } + } + } + if !claim_ok { + println!("CLAIM_PROBE reg89={} AERE=none FAIL", reg); + return; + } + } + + // takeover: 跳过认领, 用 SL 上次会话的 B12 续发 DMX+心跳, 探会话是否可直接延续 + if std::env::var("HOSTDEMO_MODE").unwrap_or_default() == "takeover" { + let b12_old: u32 = 0x4366937a; // sym_startup @101.278 AECH 的 B12@0x8C (SL 22:52 会话) + println!("[P] takeover: B12=0x{:08x} 续发 15s DMX(ch101 ramp)+心跳+RDM GET, 侦听回包", b12_old); + let tpl = load_template("dmx_a"); + let hb = load_template("heartbeat"); + let known: [u8; 6] = [0x01, 0x07, 0x34, 0x67, 0x01, 0x25]; // NFS1K2D + let start = Instant::now(); + let mut seq: u32 = 5000; // 帧序号跳开, 避免与 SL 旧计数冲突 + let mut last_hb = Instant::now() - Duration::from_secs(10); + let mut rdm_done = false; + while start.elapsed() < Duration::from_secs(15) { + if last_hb.elapsed() >= Duration::from_secs(6) { + let _ = sock.send_to(&hb, &dst); + last_hb = Instant::now(); + println!(" [P] 心跳已发"); + } + if !rdm_done && start.elapsed() > Duration::from_secs(5) { + rdm_done = true; + let f = build_rdm_frame(&known, 1, 1, 0x20, 0x0060, &[]); // GET DEVICE_INFO + sock.send_to(&f, &dst).unwrap(); + println!(" [P] RDM GET DEVICE_INFO 已发"); + } + let t = start.elapsed().as_secs_f32(); + let mut p1 = [0u8; 512]; + p1[100] = (((t * 0.8).sin() * 0.5 + 0.5) * 255.0) as u8; + let p2 = [0u8; 512]; + let r1 = rng.next(); + let r2 = rng.next(); + let mut f = tpl.clone(); + f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes()); + f[0x2E] = 2; f[0x2F] = 0; + f[0x22..0x26].copy_from_slice(&r1.to_le_bytes()); + f[0x26..0x2A].copy_from_slice(&r2.to_le_bytes()); + f[0x3C..0x23C].copy_from_slice(&agios_dmx_encrypt(&p1, r1, b12_old)); + f[0x23C..0x43C].copy_from_slice(&agios_dmx_encrypt(&p2, r2, b12_old)); + let _ = sock.send_to(&f, &dst); + seq += 1; + while let Ok((n, src)) = sock.recv_from(&mut buf) { + let b = &buf[..n]; + println!(" [P] ← len={} src={} head={}", n, src, hexs(&b[..std::cmp::min(16, n)])); + } + std::thread::sleep(Duration::from_millis(33)); + } + return; + } + + // ---- 2) 宣告: 真主机用 644B type=1 广播宣告 (announce 模板), 主机 IP@0x41 LE; + // 再发 102B type=2 身份帧 (cold2 实测: 紧邻 AECH 之前), IP@0x1C LE ---- + let mode = std::env::var("HOSTDEMO_MODE").unwrap_or_default(); + let o: Vec = bind_ip.split('.').map(|x| x.parse().unwrap()).collect(); + let bcast = format!("{}.255:31772", target.rsplitn(2, '.').nth(1).unwrap_or("192.168.123")); + if mode != "skip2" && mode != "noclaim" { + // 冷启动广播前奏 (cold2=38s, SymphoLight 实测 2.5min 成功; 10s 短前奏被拒!) + // ★门控假说: Butler 须收到 ~13+ 轮 discovery/announce 才武装认领应答 + let pre_secs: u64 = std::env::var("HOSTDEMO_PRE").ok().and_then(|s| s.parse().ok()).unwrap_or(45); + // ★宣告帧用 announce_live 模板 + 新鲜时间戳/随机会话 ID (陈旧宣告 = 僵尸主机, 认领被无视) + let euce = load_template("euce"); // 540B, 无主机相关字段 + let pre = Instant::now(); + let mut queries_dbg = 0; + let mut last = Instant::now() - Duration::from_secs(10); + let mut last_euce = Instant::now() - Duration::from_secs(10); + while pre.elapsed() < Duration::from_secs(pre_secs) { + if last.elapsed() >= Duration::from_secs(2) { + let _ = sock.send_to(&disc, "255.255.255.255:31772"); + let mut ann = build_fresh_announce(&mut rng); + ann[0x41..0x45].copy_from_slice(&[o[3], o[2], o[1], o[0]]); // 主机IP逆序字节@0x41 + if queries_dbg == 0 { + queries_dbg = 1; + let live = load_template("announce_live"); + println!(" [dbg] ann timestamp区: ours={}", hexs(&ann[0x230..0x245])); + println!(" [dbg] ann timestamp区: live={}", hexs(&live[0x230..0x245])); + println!(" [dbg] ann sessid区: ours={}", hexs(&ann[0x219..0x226])); + println!(" [dbg] ann sessid区: live={}", hexs(&live[0x219..0x226])); + let diffs: Vec = (0..ann.len()).filter(|&i| ann[i] != live[i]).collect(); + println!(" [dbg] ann 与 live 模板差异偏移 ({}处): {:?}", diffs.len(), diffs); + } + let _ = sock.send_to(&ann, &bcast); + last = Instant::now(); + } + if last_euce.elapsed() >= Duration::from_secs(6) { + // ★EUCE 每 6s (SymphoLight 启动期实测节奏, 30s 节奏认领被拒) + let _ = sock.send_to(&euce, "255.255.255.255:31772"); + last_euce = Instant::now(); + } + // 排空 (217B 发现应答 / 47B 注册等) + if let Ok((n, src)) = sock.recv_from(&mut buf) { + println!(" [dbg] 前奏期收帧 len={} src={}", n, src); + } + } + let mut ident = IDENT102.to_vec(); + ident[0x1C..0x20].copy_from_slice(&[o[3], o[2], o[1], o[0]]); + // ★sub=07 绑定帧 ×4: ffff→0000→ffff→0000 (间隔 ~0.32s, sym_startup @98.8-99.5 实测) + // 1.8s 后发 ident + for flag in [[0xff_u8, 0xff_u8], [0x00, 0x00], [0xff, 0xff], [0x00, 0x00]] { + let mut b = build_bind52(flag); + b[0x1A..0x1E].copy_from_slice(&[o[3], o[2], o[1], o[0]]); + sock.send_to(&b, &dst).unwrap(); + std::thread::sleep(Duration::from_millis(320)); + } + std::thread::sleep(Duration::from_millis(1800)); + let mut ident = IDENT102.to_vec(); + ident[0x1C..0x20].copy_from_slice(&[o[3], o[2], o[1], o[0]]); + sock.send_to(&ident, &dst).unwrap(); + println!("[2] 冷启动前奏完成 ({}s 广播 + 绑定帧×4 + 身份帧)", pre_secs); + } + let mut reg_ok = false; + for _ in 0..10 { + if let Ok((n, _)) = sock.recv_from(&mut buf) { + if n == 89 { reg_ok = true; break; } + } + } + println!("[2] 宣告 → 注册应答: {}", if reg_ok { "OK (89B)" } else { "未收到 (继续)" }); + + // ---- 3) AECH 认领 ×3 (quick 模式已在上面认领成功; noclaim 跳过) ---- + if !claim_ok { + b12 = rng.next(); + if mode == "noclaim" { + println!("[3] noclaim 模式: 跳过 AECH, 直接以 RDM GET 探测连接态"); + claim_ok = true; + } + for round in 0..3 { + if mode == "noclaim" { break; } + let nonce = rng.next(); + let mut ch = load_template("claim_h112"); + ch[0x1A..0x1E].copy_from_slice(&[o[3], o[2], o[1], o[0]]); + ch[0x88..0x8C].copy_from_slice(&nonce.to_le_bytes()); + ch[0x8C..0x90].copy_from_slice(&b12.to_le_bytes()); + sock.send_to(&ch, &dst).unwrap(); + for _ in 0..10 { + if let Ok((n, src)) = sock.recv_from(&mut buf) { + let b = &buf[..n]; + println!(" [dbg] AECH等待中收帧 len={} src={} magic={:?}", n, src, &b[0..4.min(n)]); + if n > 0xC4 && b.windows(4).any(|w| w == b"AERE") { + let resp = g32(b, 0xC0); + let want = agios_g(nonce); + println!("[3] AECH 第{}轮: resp=0x{:08x} G=0x{:08x} {}", round + 1, resp, want, + if resp == want { "★PASS" } else { "FAIL" }); + if resp == want { claim_ok = true; } + break; + } + } + } + } + } + if !claim_ok { println!("[3] 认领失败 (真机只应答源 123.113 的 AECH), 终止"); return; } + println!(" 认领通过, B12=0x{:08x}", b12); + + // quick 模式: 密钥广播 + 统一会话环 — DMX 持续流 (cold2 实证: RDM 代理仅在 DMX 流活跃时工作) + if hd_mode == "quick" { + println!("[K] 1947 许可密钥广播 4s..."); + send_key_burst(&sock); + session_loop(&sock, &dst, target, &mut rng, b12, secs); + return; + } + + // noclaim 连接态探测: 直接 GET DEVICE_INFO 已知 3 灯具, 验证 Butler 是否仍视我们为主机 + if mode == "noclaim" { + let known: [[u8; 6]; 3] = [ + [0x01, 0x07, 0x34, 0x67, 0x01, 0x25], // NFS1K2D DMX101 + [0x01, 0x07, 0x34, 0x88, 0x09, 0x00], // NFM680S DMX112 + [0x08, 0x99, 0x50, 0x00, 0x26, 0xc9], // HMKJ DMX1-32 + ]; + let mut tn0: u8 = rng.byte(); + for (i, uid) in known.iter().enumerate() { + tn0 = tn0.wrapping_add(1); + let f = build_rdm_frame(uid, tn0, (i + 1) as u16, 0x20, 0x0060, &[]); + sock.send_to(&f, &dst).unwrap(); + let w = Instant::now(); + let mut got = false; + while w.elapsed() < Duration::from_millis(500) { + if let Ok((n, src)) = sock.recv_from(&mut buf) { + if src.ip().to_string() != target { continue; } + let b = &buf[..n]; + println!(" [dbg] 收帧 len={} 0x18={:02x}{:02x}", n, b.get(0x19).copied().unwrap_or(0), b.get(0x18).copied().unwrap_or(0)); + if let Some((false, d)) = rdm_region(b) { + if d.len() >= 0x18 + 19 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0x60] { + let a = u16::from_be_bytes([d[0x18 + 14], d[0x18 + 15]]); + println!(" ★ 连接态确认! UID={} DMX地址={}", uid_s(uid), a); + got = true; + break; + } + } + } + } + if !got { println!(" UID={} 无应答", uid_s(uid)); } + } + return; + } + + // ---- 4) RDM 发现 ---- + let mut tn: u8 = rng.byte(); + let mut qseq: u16 = 1; + let mut fixtures: Vec<[u8; 6]> = Vec::new(); + { + // 4a) 先直查已知 UID 验证 RDM 代理通道 (灯具可能仍处 SL 会话留下的 MUTE 态) + let known: [[u8; 6]; 3] = [ + [0x01, 0x07, 0x34, 0x67, 0x01, 0x25], // NFS1K2D DMX101 + [0x01, 0x07, 0x34, 0x88, 0x09, 0x00], // NFM680S DMX112 + [0x08, 0x99, 0x50, 0x00, 0x26, 0xc9], // HMKJ DMX1-32 + ]; + println!("[4a] 直查已知 UID (GET DEVICE_INFO):"); + for uid in &known { + tn = tn.wrapping_add(1); + let f = build_rdm_frame(uid, tn, qseq, 0x20, 0x0060, &[]); + qseq += 1; + sock.send_to(&f, &dst).unwrap(); + let w = Instant::now(); + let mut ok = false; + while w.elapsed() < Duration::from_millis(400) { + if let Ok((n, src)) = sock.recv_from(&mut buf) { + if src.ip().to_string() != target { continue; } + let b = &buf[..n]; + if let Some((false, d)) = rdm_region(b) { + if d.len() >= 0x18 + 19 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0x60] { + let a = u16::from_be_bytes([d[0x18 + 14], d[0x18 + 15]]); + let footprint = u16::from_be_bytes([d[0x18 + 10], d[0x18 + 11]]); + println!(" ★ {} → DMX地址 {} / {}通道", uid_s(uid), a, footprint); + if !fixtures.contains(uid) { fixtures.push(*uid); } + ok = true; + break; + } + } + } + } + if !ok { println!(" {} → 无应答", uid_s(uid)); } + } + // 先广播 DISC_UN_MUTE (PID=0003): 稳态下主机已把全部灯具 MUTE, + // 被 MUTE 的灯具不应答 UNIQUE_BRANCH → 须先解除静音才能重新发现 + tn = tn.wrapping_add(1); + let um = build_rdm_frame(&BCAST_UID, tn, qseq, 0x10, 0x0003, &[]); + qseq += 1; + sock.send_to(&um, &dst).unwrap(); + std::thread::sleep(Duration::from_millis(80)); + println!("[4] 已广播 DISC_UN_MUTE, 开始 UNIQUE_BRANCH 二分遍历"); + let mut stack: Vec<(u64, u64)> = vec![(0, 0xFFFF_FFFF_FFFF)]; + let mut queries = 0; + let walk_start = Instant::now(); + while let Some((lo, hi)) = stack.pop() { + if queries >= 400 || walk_start.elapsed() > Duration::from_secs(120) { println!(" (发现遍历到达上限)"); break; } + let mut pd = Vec::with_capacity(12); + pd.extend_from_slice(&u64_uid(lo)); + pd.extend_from_slice(&u64_uid(hi)); + // 真机实测: 首个应答延迟可达 ~1.4s, 真主机对同一分支连发多次 → 每分支最多 4 次尝试 × 250ms 窗口 + let mut found: Vec<[u8; 6]> = Vec::new(); + let mut collision = false; + for attempt in 0..4 { + queries += 1; + tn = tn.wrapping_add(1); + let f = build_rdm_frame(&BCAST_UID, tn, qseq, 0x10, 0x0001, &pd); // DISC_UNIQUE_BRANCH + qseq += 1; + if queries == 1 { + let t = load_template("rdm_disc_req"); + let diffs: Vec = (0x2A..0x62).filter(|&i| f[i] != t[i]).collect(); + println!(" [dbg] 首帧与模板差异偏移: {:?} (应仅 qseq/TN/srcUID末字节/PD/checksum)", diffs); + } + sock.send_to(&f, &dst).unwrap(); + let w = Instant::now(); + while w.elapsed() < Duration::from_millis(250) { + if let Ok((n, src)) = sock.recv_from(&mut buf) { + let b = &buf[..n]; + if src.ip().to_string() != target { continue; } + if let Some((is_pulse, d)) = rdm_region(b) { + if is_pulse { + match decode_pulse(d) { + Some(uid) => { if !found.contains(&uid) { found.push(uid); } } + None => collision = true, + } + } + } + } + } + if !found.is_empty() || collision { break; } + let _ = attempt; + } + if collision { + let mid = lo + (hi - lo) / 2; + stack.push((mid + 1, hi)); + stack.push((lo, mid)); + } else if found.len() == 1 { + let uid = found[0]; + if found_valid(&uid) { + println!(" [发现] UID={} (深度 {} 次查询后)", uid_s(&uid), queries); + // DISC_MUTE (PID=0002) → 70B 参数 ACK + tn = tn.wrapping_add(1); + let f2 = build_rdm_frame(&uid, tn, qseq, 0x10, 0x0002, &[]); + qseq += 1; + sock.send_to(&f2, &dst).unwrap(); + let w2 = Instant::now(); + let mut acked = false; + while w2.elapsed() < Duration::from_millis(400) { + if let Ok((n, src)) = sock.recv_from(&mut buf) { + if src.ip().to_string() != target { continue; } + let b = &buf[..n]; + if let Some((false, d)) = rdm_region(b) { + if d.len() >= 0x18 && d[0] == 0xCC && d[0x14] == 0x11 && d[0x15..0x17] == [0x00, 0x02] { + acked = true; break; + } + } + } + } + println!(" MUTE(0002) ACK: {}", if acked { "OK" } else { "未收到" }); + fixtures.push(uid); + } + } else if found.len() > 1 { + let mid = lo + (hi - lo) / 2; + stack.push((mid + 1, hi)); + stack.push((lo, mid)); + } + // found==0: 空分支 + } + println!("[4] RDM 发现完成: {} 台灯具, {} 次查询, {:.1}s", fixtures.len(), queries, walk_start.elapsed().as_secs_f32()); + } + + // ---- 4b) 读参数: DEVICE_INFO / DMX_START_ADDRESS / DEVICE_LABEL ---- + let mut addr_list: Vec = Vec::new(); + for uid in &fixtures { + tn = tn.wrapping_add(1); + let f = build_rdm_frame(uid, tn, qseq, 0x20, 0x0060, &[]); // GET DEVICE_INFO + qseq += 1; + sock.send_to(&f, &dst).unwrap(); + let mut dmx_addr: Option = None; + let mut footprint = 0u16; + let mut sensors = 0u8; + let w = Instant::now(); + while w.elapsed() < Duration::from_millis(400) { + if let Ok((n, src)) = sock.recv_from(&mut buf) { + if src.ip().to_string() != target { continue; } + let b = &buf[..n]; + if let Some((false, d)) = rdm_region(b) { + // d: CC@0 ... CC字段@0x14, PID@0x15-16, PDL@0x17, PD@0x18 + if d.len() >= 0x18 + 19 && d[0] == 0xCC && d[0x14] == 0x21 && d[0x15..0x17] == [0x00, 0x60] { + let pd = &d[0x18..0x18 + 19]; + footprint = u16::from_be_bytes([pd[10], pd[11]]); + dmx_addr = Some(u16::from_be_bytes([pd[14], pd[15]])); + sensors = pd[18]; + break; + } + } + } + } + // GET DMX_START_ADDRESS 复核 + tn = tn.wrapping_add(1); + let f2 = build_rdm_frame(uid, tn, qseq, 0x20, 0x00F0, &[]); + qseq += 1; + sock.send_to(&f2, &dst).unwrap(); + let w2 = Instant::now(); + while w2.elapsed() < Duration::from_millis(400) { + if let Ok((n, src)) = sock.recv_from(&mut buf) { + if src.ip().to_string() != target { continue; } + let b = &buf[..n]; + if let Some((false, d)) = rdm_region(b) { + if d.len() >= 0x18 + 2 && d[0] == 0xCC && d[0x14] == 0x21 && d[0x15..0x17] == [0x00, 0xF0] { + let a = u16::from_be_bytes([d[0x18], d[0x19]]); + if dmx_addr.is_none() { dmx_addr = Some(a); } + break; + } + } + } + } + match dmx_addr { + Some(a) => { + println!(" 灯具 {} → DMX 地址 {} (占用 {} 通道, {} 传感器)", uid_s(uid), a, footprint, sensors); + addr_list.push(a); + } + None => println!(" 灯具 {} → 参数读取超时", uid_s(uid)), + } + } + + // ---- 5) DMX 控制: 呼吸图案 (按发现的灯具地址; 无发现则用已知 101/112/1) ---- + if secs == 0 { println!("[5] secs=0, 跳过 DMX 控制"); return; } + if addr_list.is_empty() { addr_list = vec![101, 112, 1]; println!("[5] 无发现结果, 用已知地址 101/112/1"); } + println!("[5] DMX 控制 {}s @30fps: 呼吸图案 @地址 {:?} (zPprv0TXRRW 加密, B12=0x{:08x})", secs, addr_list, b12); + println!(" ⚠ SymphoLight 若同时运行会插黑场帧造成闪烁; 关其输出可得纯净效果"); + let tpl = load_template("dmx_a"); + let hb = load_template("heartbeat"); // 36B type=02 sub=03 心跳 (会话保活, 每 ~6s 单播) + let start = Instant::now(); + let mut seq: u32 = 1; + let mut sent = 0u64; + let mut last_hb = Instant::now(); + while start.elapsed() < Duration::from_secs(secs) { + if last_hb.elapsed() >= Duration::from_secs(6) { + let _ = sock.send_to(&hb, &dst); + last_hb = Instant::now(); + } + let t = start.elapsed().as_secs_f32(); + let mut p1 = [0u8; 512]; + let mut p2 = [0u8; 512]; + for (k, &a) in addr_list.iter().enumerate() { + let phase = t * 1.2 + k as f32 * 2.1; + let v = ((phase.sin() * 0.5 + 0.5) * 255.0) as u8; + let ch = a.max(1) - 1; // 0-based + if ch < 512 { p1[ch as usize] = v; } else if ch < 1024 { p2[(ch - 512) as usize] = v; } + } + let r1 = rng.next(); + let r2 = rng.next(); + let mut f = tpl.clone(); + f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes()); + f[0x2E] = 2; f[0x2F] = 0; // 节点序号 (真机 .1 = 2) + f[0x22..0x26].copy_from_slice(&r1.to_le_bytes()); + f[0x26..0x2A].copy_from_slice(&r2.to_le_bytes()); + f[0x3C..0x23C].copy_from_slice(&agios_dmx_encrypt(&p1, r1, b12)); + f[0x23C..0x43C].copy_from_slice(&agios_dmx_encrypt(&p2, r2, b12)); + if sock.send_to(&f, &dst).is_ok() { sent += 1; } + seq += 1; + std::thread::sleep(Duration::from_millis(33)); + } + println!("[5] 完成: 发送 {} 帧", sent); +} + +fn found_valid(uid: &[u8; 6]) -> bool { + uid.iter().any(|&b| b != 0) && uid.iter().any(|&b| b != 0xFF) +} + +/// 构造"新鲜"宣告帧: announce_live 模板 (今日 SL 实抓) + 每发随机会话 ID@0x221 +/// + 当前本地时间戳@0x239 (年u16LE 月 日 时 分 秒 毫秒u16LE 时区) — Butler 按 +/// 宣告时间戳判主机新鲜度, 陈旧宣告(旧抓包模板)会被当僵尸主机无视 ident/AECH +fn build_fresh_announce(rng: &mut Rng) -> Vec { + let mut a = load_template("announce_live"); // 644B + let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); + // 本地时间分解 (UTC+8, 与实测 tz=02... 用本地时区偏移) + let secs = now + tz_offset_secs(); + let days = secs / 86400; + let rem = secs % 86400; + let (year, month, day) = civil_from_days(days as i64); + let hour = (rem / 3600) as u8; + let min = ((rem % 3600) / 60) as u8; + let sec = (rem % 60) as u8; + let ms = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().subsec_millis(); + // 时间戳字段实际位于 0x235: 年u16LE@0x235 月@0x237 日@0x238 时@0x239 分@0x23A + // 秒@0x23B 毫秒u16LE@0x23C 时区@0x23E (实测 2026-09-10 22:52:50.992 tz=02) + a[0x235] = (year & 0xFF) as u8; + a[0x236] = (year >> 8) as u8; + a[0x237] = month; + a[0x238] = day; + a[0x239] = hour; + a[0x23A] = min; + a[0x23B] = sec; + let msle = (ms as u16).to_le_bytes(); + a[0x23C] = msle[0]; + a[0x23D] = msle[1]; + a[0x23E] = 0x02; // 时区字段 (实测值) + // 4B 随机会话 ID @0x21D (每发宣告都变; 0x221 的 0c 恒定) + let id = rng.next(); + a[0x21D..0x221].copy_from_slice(&id.to_le_bytes()); + a +} + +fn tz_offset_secs() -> u64 { + // 本地时区偏移 (秒): 用 UTC 与本地时间差 + let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64; + let utc_days = (now.div_euclid(86400) + 4) % 7; // 0=周四 + let _ = utc_days; + // 简化: 读环境 TZ 不可靠, 直接用 Windows 时区 API? 用 chrono 不行(无依赖). + // 实测帧 tz 字节恒 02, 时间字段为本地时间 (UTC+8 中 hour=22 对应当地 22 点) + // → 这里返回 +8h; 若部署在其它时区需调整 + 8 * 3600 +} + +fn civil_from_days(z: i64) -> (u16, u8, u8) { + // Howard Hinnant 算法: days since 1970-01-01 → (y, m, d) + let z = z + 719468; + let era = if z >= 0 { z } else { z - 146096 } / 146097; + let doe = (z - era * 146097) as u64; + let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; + let y = yoe as i64 + era * 400; + let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); + let mp = (5 * doy + 2) / 153; + let d = (doy - (153 * mp + 2) / 5 + 1) as u8; + let m = if mp < 10 { mp + 3 } else { mp - 9 } as u8; + (if m <= 2 { (y + 1) as u16 } else { y as u16 }, m, d) +} + +/// ★1947 端口"密钥报文" (allports.pcap 实测): 40B ASCII 许可令牌, SL 每 ~34s 广播一轮 +/// (双目标 21 帧 @200ms) — Butler 在 1947 监听, 疑为 RDM 代理的许可开关 +const KEY40: &[u8] = b"BlZRQphAGxUbi7at8KEfmdZ7TdSB03b+qoC1RIA"; + +fn send_key_burst(sock: &UdpSocket) { + for _ in 0..21 { + let _ = sock.send_to(KEY40, "192.168.123.255:1947"); + let _ = sock.send_to(KEY40, "255.255.255.255:1947"); + std::thread::sleep(Duration::from_millis(200)); + } +} + +/// 统一会话环 (quick): DMX 30fps 持续 + RDM 穿插 +/// 阶段: 0-2s 黑场(设备预热) → UN_MUTE + UNIQUE_BRANCH 二分发现 → MUTE 已发现灯具 +/// → GET DEVICE_INFO/DMX 地址 → 呼吸图案控制 (未发现则用已知地址) +fn session_loop(sock: &UdpSocket, dst: &str, target: &str, rng: &mut Rng, b12: u32, secs: u64) { + let tpl = load_template("dmx_a"); + let hb = load_template("heartbeat"); + let known: [[u8; 6]; 3] = [ + [0x01, 0x07, 0x34, 0x67, 0x01, 0x25], // NFS1K2D DMX101 + [0x01, 0x07, 0x34, 0x88, 0x09, 0x00], // NFM680S DMX112 + [0x08, 0x99, 0x50, 0x00, 0x26, 0xc9], // HMKJ DMX1-32 + ]; + let mut buf = [0u8; 4096]; + let mut fixtures: Vec<[u8; 6]> = Vec::new(); + let mut addr_list: Vec = Vec::new(); + let mut tn: u8 = rng.byte(); + let mut qseq: u16 = 1; + let mut seq: u32 = 1; + // 发现任务队列: ★窄分支优先 (已知灯具各一个只含自己的分支 — Butler 可能拒转全范围查询) + let mut stack: Vec<(u64, u64)> = vec![(0, 0xFFFF_FFFF_FFFF)]; + for u in known.iter() { + let v = uid_u64(u); + stack.push((v - 1, v + 1)); + } + let mut get_queue: Vec<[u8; 6]> = Vec::new(); + let mut muted: Vec<[u8; 6]> = Vec::new(); + let start = Instant::now(); + let mut last_hb = Instant::now(); + let mut last_key_t: f32 = -100.0; + let mut last_rdm = Instant::now() - Duration::from_secs(1); + let mut unmuting = false; + let mut discovering = true; + let mut collecting = false; + let mut collect_until = Instant::now(); + let mut found: Vec<[u8; 6]> = Vec::new(); + let mut collision = false; + let mut current_branch: Option<(u64, u64)> = None; + let mut sent = 0u64; + + println!("[S] 统一会话环启动: DMX 30fps + RDM 穿插 ({}s)", secs); + while start.elapsed() < Duration::from_secs(secs) { + let t = start.elapsed().as_secs_f32(); + + // --- 心跳每 6s + 密钥每 30s + ★EUCE 每 6s (SL 全程 6s 节奏, 许可信标) --- + if last_hb.elapsed() >= Duration::from_secs(6) { + let _ = sock.send_to(&hb, dst); + let euce = load_template("euce"); + let _ = sock.send_to(&euce, "255.255.255.255:31772"); + last_hb = Instant::now(); + } + if t - last_key_t >= 30.0 { + let _ = sock.send_to(KEY40, "192.168.123.255:1947"); + let _ = sock.send_to(KEY40, "255.255.255.255:1947"); + last_key_t = t; + } + + // --- RDM 任务 (每 ~100ms 一发; 窄分支优先, t>3s 即开始快速定位问题) --- + if discovering && t > 3.0 && last_rdm.elapsed() >= Duration::from_millis(100) { + last_rdm = Instant::now(); + if collecting && Instant::now() >= collect_until { + // 本分支收集结束 → 判定 + collecting = false; + if let Some((lo, hi)) = current_branch.take() { + if collision { + let mid = lo + (hi - lo) / 2; + stack.push((mid + 1, hi)); + stack.push((lo, mid)); + println!(" [S] 分支冲突 → 二分"); + } else if found.len() == 1 && found_valid(&found[0]) { + let uid = found[0]; + println!(" [S] ★发现灯具 {}", uid_s(&uid)); + if !fixtures.contains(&uid) { fixtures.push(uid); } + get_queue.push(uid); + // DISC_MUTE 该灯具 + tn = tn.wrapping_add(1); + let f = build_rdm_frame(&uid, tn, qseq, 0x10, 0x0002, &[]); + qseq += 1; + let _ = sock.send_to(&{ let mut f = f; f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes()); seq += 1; f }, dst); + } else if found.len() > 1 { + let mid = lo + (hi - lo) / 2; + stack.push((mid + 1, hi)); + stack.push((lo, mid)); + } + found.clear(); + collision = false; + } + } + if collecting { + // 收集窗口内每 150ms 重发同分支查询 (真主机连发节奏) + if last_rdm.elapsed() >= Duration::from_millis(150) { + last_rdm = Instant::now(); + if let Some((lo, hi)) = current_branch { + let mut pd = Vec::with_capacity(12); + pd.extend_from_slice(&u64_uid(lo)); + pd.extend_from_slice(&u64_uid(hi)); + tn = tn.wrapping_add(1); + let mut f = build_rdm_frame(&BCAST_UID, tn, qseq, 0x10, 0x0001, &pd); + qseq += 1; + f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes()); + seq += 1; + let _ = sock.send_to(&f, dst); + let mut f2 = f.clone(); + f2[0x30] = 0x01; + f2[0x49] = f2[0x49].wrapping_sub(1); + let _ = sock.send_to(&f2, dst); + } + } + } + if !collecting { + if !unmuting { + // DISC_UN_MUTE 广播 + tn = tn.wrapping_add(1); + let f = build_rdm_frame(&BCAST_UID, tn, qseq, 0x10, 0x0003, &[]); + qseq += 1; + let _ = sock.send_to(&{ let mut f = f; f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes()); seq += 1; f }, dst); + unmuting = true; + println!("[S] DISC_UN_MUTE 已发"); + } else if let Some((lo, hi)) = stack.pop() { + let mut pd = Vec::with_capacity(12); + pd.extend_from_slice(&u64_uid(lo)); + pd.extend_from_slice(&u64_uid(hi)); + tn = tn.wrapping_add(1); + let mut f = build_rdm_frame(&BCAST_UID, tn, qseq, 0x10, 0x0001, &pd); + qseq += 1; + // SL 实测: RDM 请求成对发送 (flag=00 + flag=01 副本, 背靠背 26μs) + f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes()); + seq += 1; + let _ = sock.send_to(&f, dst); + let mut f2 = f.clone(); + f2[0x30] = 0x01; + // flag=01 副本的 srcUID 末字节 = fe (SL 实测: ff/fe 成对) + f2[0x49] = f2[0x49].wrapping_sub(1); + let _ = sock.send_to(&f2, dst); + current_branch = Some((lo, hi)); + collecting = true; + collect_until = Instant::now() + Duration::from_millis(500); + } else { + if fixtures.is_empty() && t < 90.0 { + // 未发现 → 重新压栈全范围, 循环重试 (SL 实测: 认领后 ~27.5s RDM 才开始被代理!) + stack.push((0, 0xFFFF_FFFF_FFFF)); + } else { + discovering = false; + println!("[S] 发现完成: {} 台灯具", fixtures.len()); + } + } + } + } + + // --- 收包 (非阻塞): 脉冲/参数应答 --- + while let Ok((n, src)) = sock.recv_from(&mut buf) { + if src.ip().to_string() != target { continue; } + let b = &buf[..n]; + if let Some((is_pulse, d)) = rdm_region(b) { + if is_pulse && collecting { + match decode_pulse(d) { + Some(uid) => { if !found.contains(&uid) { found.push(uid); } } + None => collision = true, + } + } else if !is_pulse && d.len() >= 0x18 + 19 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0x60] { + let addr = u16::from_be_bytes([d[0x18 + 14], d[0x18 + 15]]); + let footprint = u16::from_be_bytes([d[0x18 + 10], d[0x18 + 11]]); + println!(" [S] ★{} → DMX地址 {} / {}通道", uid_s(&[d[9], d[10], d[11], d[12], d[13], d[14]]), addr, footprint); + addr_list.push(addr); + } else if !is_pulse && d.len() >= 0x18 + 2 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0xF0] { + let addr = u16::from_be_bytes([d[0x18], d[0x19]]); + println!(" [S] ★DMX_START_ADDRESS 应答: {}", addr); + addr_list.push(addr); + } + } + } + + // --- GET 队列 (发现后逐台读参数) --- + if !discovering && !get_queue.is_empty() && last_rdm.elapsed() >= Duration::from_millis(120) { + last_rdm = Instant::now(); + let uid = get_queue.remove(0); + tn = tn.wrapping_add(1); + let f = build_rdm_frame(&uid, tn, qseq, 0x20, 0x0060, &[]); + qseq += 1; + let _ = sock.send_to(&{ let mut f = f; f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes()); seq += 1; f }, dst); + let _ = muted; + } + + // --- DMX 帧 @30fps --- + let mut p1 = [0u8; 512]; + let mut p2 = [0u8; 512]; + if !addr_list.is_empty() { + for (k, &a) in addr_list.iter().enumerate() { + let phase = t * 1.2 + k as f32 * 2.1; + let v = (((phase.sin() * 0.5 + 0.5) * 255.0) * (t * 0.5).min(1.0)) as u8; // 淡入 + let ch = a.max(1) - 1; + if ch < 512 { p1[ch as usize] = v; } else if ch < 1024 { p2[(ch - 512) as usize] = v; } + } + } + let r1 = rng.next(); + let r2 = rng.next(); + let mut f = tpl.clone(); + f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes()); + f[0x2E] = 2; f[0x2F] = 0; + f[0x22..0x26].copy_from_slice(&r1.to_le_bytes()); + f[0x26..0x2A].copy_from_slice(&r2.to_le_bytes()); + f[0x3C..0x23C].copy_from_slice(&agios_dmx_encrypt(&p1, r1, b12)); + f[0x23C..0x43C].copy_from_slice(&agios_dmx_encrypt(&p2, r2, b12)); + let _ = sock.send_to(&{ let mut f = f; f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes()); seq += 1; f }, dst); + seq += 1; + sent += 1; + std::thread::sleep(Duration::from_millis(33)); + } + println!("[S] 会话环结束: DMX {} 帧, 发现灯具 {} 台, 控制地址 {:?}", sent, fixtures.len(), addr_list); +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..6e362cf --- /dev/null +++ b/src/main.rs @@ -0,0 +1,194 @@ +// main.rs — enet-validator: e:net 协议独立验证程序 (模拟 e:cue 主机, 连接真机 Butler S2) +mod enet; +mod tests; +mod dmxhash; +mod aech; +mod hostdemo; + +use tests::Item; + +fn main() { + let arg = std::env::args().nth(1).unwrap_or_else(|| "help".into()); + let mut results: Vec = Vec::new(); + + println!("enet-validator — e:net 协议实测验证 (Butler S2 @ 192.168.123.1)"); + println!("================================================================"); + match arg.as_str() { + "discover" => results.push(tests::t1_discover()), + "announce" => results.push(tests::tx_observe("T2", "announce", false)), + "heartbeat" => results.push(tests::tx_observe("T3", "heartbeat", false)), + "euce" => results.push(tests::tx_observe("T4", "euce", false)), + "session" => { + let secs: u64 = std::env::args().nth(2).and_then(|a| a.parse().ok()).unwrap_or(60); + results.extend(tests::t5_t8_session(secs)); + } + "fallback" => results.push(tests::t9_fallback()), + "claim" => results.push(tests::t12_claim_replay()), + "takeover" => { + let secs: u64 = std::env::args().nth(2).and_then(|a| a.parse().ok()).unwrap_or(15); + results.extend(tests::t13_takeover(secs)); + } + "holdbind" => { + // Rust 先 bind, 等待期间由 SymphoLight 认证; 之后 Rust 发帧测试共存 + let s = enet::open_reuse("0.0.0.0:31772"); + println!("Rust 已 bind 31772 (REUSEADDR)。等待 30s (此间启动 SymphoLight 认领)…"); + std::thread::sleep(std::time::Duration::from_secs(30)); + println!("开始发送测试帧 15s…"); + results.extend(tests::t13_takeover_on(s, 15)); + } + "device" => { + // 用法: device [设备IP] [MAC_hex12] [秒] (默认 192.168.123.200, 本机网卡MAC) + let ip_s = std::env::args().nth(2).unwrap_or_else(|| "192.168.123.200".into()); + let mac_s = std::env::args().nth(3).unwrap_or_else(|| "300ed559bb36".into()); + let secs: u64 = std::env::args().nth(4).and_then(|a| a.parse().ok()).unwrap_or(180); + let relay_s = std::env::args().nth(5); // 第5参: 中继目标 ip[:port] (默31772) + let ip: Vec = ip_s.split('.').filter_map(|x| x.parse().ok()).collect(); + let mac: Vec = (0..mac_s.len()).step_by(2).filter_map(|i| u8::from_str_radix(&mac_s[i..i + 2], 16).ok()).collect(); + let relay: Option<([u8; 4], u16)> = relay_s.and_then(|s| { + let (ips, port) = match s.rsplit_once(':') { + Some((a, p)) => (a.to_string(), p.parse::().unwrap_or(31772)), + None => (s, 31772), + }; + let o: Vec = ips.split('.').filter_map(|x| x.parse().ok()).collect(); + if o.len() == 4 { Some(([o[0], o[1], o[2], o[3]], port)) } else { None } + }); + if ip.len() == 4 && mac.len() == 6 { + let mut ipa = [0u8; 4]; ipa.copy_from_slice(&ip); + let mut maca = [0u8; 6]; maca.copy_from_slice(&mac); + results.extend(tests::device2(ipa, maca, secs, relay)); + } else { + println!("device 参数错误"); + } + } + "rdm" => { + let secs: u64 = std::env::args().nth(2).and_then(|a| a.parse().ok()).unwrap_or(30); + results.extend(tests::t10_t11_rdm(secs)); + } + "bridge" => { + // bridge [secs] — AECH 桥 (非123网段设备认领桥) + let lp = std::env::args().nth(2).unwrap_or_else(|| "192.168.58.107:41772".into()); + let rip_s = std::env::args().nth(3).unwrap_or_else(|| "192.168.123.1".into()); + let secs: u64 = std::env::args().nth(4).and_then(|a| a.parse().ok()).unwrap_or(7200); + let (lip_s, lport) = match lp.rsplit_once(':') { + Some((a, p)) => (a.to_string(), p.parse::().unwrap_or(41772)), + None => (lp, 41772), + }; + let lip: Vec = lip_s.split('.').filter_map(|x| x.parse().ok()).collect(); + let rip: Vec = rip_s.split('.').filter_map(|x| x.parse().ok()).collect(); + if lip.len() == 4 && rip.len() == 4 { + tests::bridge([lip[0], lip[1], lip[2], lip[3]], lport, [rip[0], rip[1], rip[2], rip[3]], secs); + } else { + println!("bridge 参数错误"); + } + return; + } + "all" => { + results.push(tests::t1_discover()); + results.push(tests::tx_observe("T2", "announce", false)); + results.push(tests::tx_observe("T3", "heartbeat", false)); + results.push(tests::tx_observe("T4", "euce", false)); + results.extend(tests::t5_t8_session(60)); + results.push(tests::t9_fallback()); + } + "rdmprobe" => { + let target = std::env::args().nth(2).unwrap_or_else(|| "192.168.123.1".into()); + let secs: u64 = std::env::args().nth(3).and_then(|a| a.parse().ok()).unwrap_or(10); + results.push(tests::rdm_probe(&target, secs)); + } + "dmxhash" => { + // dmxhash [主机IP] — DMX-A 帧标签算法爆破 (离线分析, 无网络) + let tsv = std::env::args().nth(2).unwrap_or_else(|| "dmx.tsv".into()); + let host = std::env::args().nth(3).unwrap_or_else(|| "192.168.123.113".into()); + dmxhash::run(&tsv, &host); + return; + } + "evtf" => { + // evtf [主机IP] [设备IP] [tsv2跨包] — 形态B事件帧结构分析 (离线) + let tsv = std::env::args().nth(2).unwrap_or_else(|| "dmx.tsv".into()); + let host = std::env::args().nth(3).unwrap_or_else(|| "192.168.123.113".into()); + let dev = std::env::args().nth(4).unwrap_or_else(|| "192.168.123.1".into()); + let tsv2 = std::env::args().nth(5); + dmxhash::evt(&tsv, &host, &dev, tsv2.as_deref()); + return; + } + "aech" => { + // aech [tsv2 ...] — AECH 密钥协商密码学分析 (离线) + let tsvs: Vec = std::env::args().skip(2).collect(); + if tsvs.is_empty() { + println!("用法: enet-validator aech [tsv2 ...]"); + return; + } + aech::run(&tsvs); + return; + } + "aechprobe" => { + // aechprobe <目标IP> — 选择密文仿射性分析 (在线探测真机) + let tsv = std::env::args().nth(2).unwrap_or_else(|| "cold2.tsv".into()); + let target = std::env::args().nth(3).unwrap_or_else(|| "192.168.123.1".into()); + aech::probe(&tsv, &target); + return; + } + "aechprobe2" => { + // aechprobe2 <目标IP> — part1 块/字节影响图谱 (在线) + let tsv = std::env::args().nth(2).unwrap_or_else(|| "cold2.tsv".into()); + let target = std::env::args().nth(3).unwrap_or_else(|| "192.168.123.1".into()); + aech::probe2(&tsv, &target); + return; + } + "aechid" => { + // aechid — f(last4) 离线候选函数鉴别 + let tsv = std::env::args().nth(2).unwrap_or_else(|| "cold2.tsv".into()); + aech::identify(&tsv); + return; + } + "dmxroll" => { + // dmxroll — DMX 滚动值/保护字节数据驱动分析 (离线) + let tsv = std::env::args().nth(2).unwrap_or_else(|| "dmx.tsv".into()); + aech::dmxroll(&tsv); + return; + } + "dmxdec" => { + // dmxdec — DMX 帧加密假说验证 (kWarv47o79k/zPprv0TXRRW 逆运算, 需 TSV 含 AECH) + let tsv = std::env::args().nth(2).unwrap_or_else(|| "dmx.tsv".into()); + aech::dmxdec(&tsv); + return; + } + "dmxsend" => { + // dmxsend <目标IP> [秒] — 主机侧等效播放: 认领 + 加密 DMX 全链路自证 + let ip = std::env::args().nth(2).unwrap_or_else(|| "192.168.58.7".into()); + let secs: u64 = std::env::args().nth(3).and_then(|s| s.parse().ok()).unwrap_or(30); + aech::dmxsend(&ip, secs); + return; + } + "hostdemo" => { + // hostdemo [目标IP] [秒] — 主机侧等效示例: 认领真机 + RDM 发现 + 加密 DMX 控制 + // 源 IP 固定 192.168.123.113 (真机 .1 的 AECH 固件过滤); 秒=0 只做认领+RDM 不发 DMX + let ip = std::env::args().nth(2).unwrap_or_else(|| "192.168.123.1".into()); + let secs: u64 = std::env::args().nth(3).and_then(|s| s.parse().ok()).unwrap_or(30); + hostdemo::run(&ip, "192.168.123.113", secs); + return; + } + _ => { + println!("用法: enet-validator "); + return; + } + } + + println!("----------------------------------------------------------------"); + let p = results.iter().filter(|r| r.pass).count(); + for r in &results { r.show(); } + println!("================================================================"); + println!("汇总: PASS={} FAIL={}", p, results.len() - p); + + // 写 JSON 报告 + let mut j = String::from("{\n \"target\": \"192.168.123.1\",\n \"results\": [\n"); + for (i, r) in results.iter().enumerate() { + j.push_str(&format!( + " {{ \"id\": \"{}\", \"pass\": {}, \"detail\": \"{}\" }}{}\n", + r.id, r.pass, r.detail.replace('"', "'"), + if i + 1 < results.len() { "," } else { "" })); + } + j.push_str(" ]\n}\n"); + let _ = std::fs::write("validation_report.json", j); + println!("报告已写: validation_report.json"); +} diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..a0a060c --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,905 @@ +// tests.rs — T1..T11 协议验证实现 (判据 = Butler 实际应答) +use crate::enet::*; +use std::collections::HashSet; +use std::net::UdpSocket; +use std::time::{Duration, Instant}; + +pub struct Item { + pub id: &'static str, + pub pass: bool, + pub detail: String, +} +impl Item { + fn new(id: &'static str, pass: bool, detail: String) -> Item { Item { id, pass, detail } } + pub fn show(&self) { + println!(" [{}] {} {}", if self.pass { "PASS" } else { "FAIL" }, self.id, self.detail); + } +} + +// ---------- T1: 发现请求 → Butler 应答 (§3.1→§3.2) ---------- +pub fn t1_discover() -> Item { + let s = open(); + let req = load_template("discovery_req"); + let _ = s.send_to(&req, BCAST_SUB); + let _ = s.send_to(&req, BCAST_ALL); + let replies = recv_window(&s, Duration::from_secs(4)); + let mut detail = format!("收到{}包; ", replies.len()); + for (_, b) in &replies { + if b.len() == 217 && &b[0..4] == b"ENet" { + let len_ok = g32(b, 0x14) == 217; + let name_ok = find(b, b"Butler S2"); + let mac_ok = find(b, &[0x00, 0x16, 0x1c, 0xf1, 0x2e, 0x39]); + let pass = len_ok && name_ok && mac_ok; + detail.push_str(&format!( + "应答217B: len字段={} 名字={} MAC={}", + len_ok, name_ok, mac_ok + )); + return Item::new("T1", pass, detail); + } + } + detail.push_str("未收到 217B 发现应答"); + Item::new("T1", false, detail) +} + +// ---------- T2/T3/T4: 单向帧发送 → 观察会话反应 ---------- +pub fn tx_observe(id: &'static str, tpl: &str, expect_reply: bool) -> Item { + let s = open(); + let f = load_template(tpl); + let dst = if tpl == "heartbeat" { BCAST_SUB } else { BCAST_ALL }; + let n = s.send_to(&f, dst).expect("send"); + let replies = recv_window(&s, Duration::from_secs(3)); + let mut detail = format!("发送{}B→{}; 收到{}包", n, dst, replies.len()); + let mut pass = true; + if expect_reply { + pass = !replies.is_empty(); + if !pass { detail.push_str("; 无应答"); } + } + Item::new(id, pass, detail) +} + +// ---------- T5-T8: 会话 (DMX + 事件×2 + ACK配对 + 状态帧) ---------- +pub struct SessionStats { + pub dmx_sent: u64, + pub evt_sent: u64, + pub acks: u64, + pub ack_matched: u64, + pub status: u64, + pub rdm: u64, + pub other: u64, +} + +pub fn session(secs: u64, mutate_channels: bool) -> (SessionStats, Vec<(u32, u64)>) { + let s = open(); + session_on(s, secs, mutate_channels, true) +} + +/// 会话核心 (可注入 socket): takeover 模式跳过 preamble 且使用抢 bind 的 socket +pub fn session_on(s: UdpSocket, secs: u64, mutate_channels: bool, preamble: bool) -> (SessionStats, Vec<(u32, u64)>) { + if preamble { + // --- 前置认领 (§6 时序): 发现请求 → 宣告帧 → 短暂等待 --- + let disc = load_template("discovery_req"); + let ann = load_template("announce"); + let _ = s.send_to(&disc, BCAST_SUB); + let _ = s.send_to(&disc, BCAST_ALL); + let _ = s.send_to(&ann, BCAST_ALL); + let _ = recv_window(&s, Duration::from_secs(2)); // 吸收发现应答/宣告反应 + } + let dmx_t = load_template("dmx_a"); + let evt_t = load_template("event_b"); + let mut dmx_seq = g16(&dmx_t, 0x1A).wrapping_add(1); + let mut evt_seq = g32(&evt_t, 0x22).wrapping_add(1); + let tick = Duration::from_millis(25); + let start = Instant::now(); + let mut st = SessionStats { dmx_sent: 0, evt_sent: 0, acks: 0, ack_matched: 0, status: 0, rdm: 0, other: 0 }; + let mut recent: HashSet = HashSet::new(); + let mut timeline: Vec<(u32, u64)> = Vec::new(); + let mut tick_n: u64 = 0; + while start.elapsed().as_secs() < secs { + // --- DMX 帧 (形态A) --- + let mut d = dmx_t.clone(); + p16(&mut d, 0x1A, dmx_seq); + let tms = tick_n * 25; + for i in 0..8 { d[0x22 + i] = ((tms >> (i * 8)) & 0xFF) as u8; } + if mutate_channels { + let ph = start.elapsed().as_secs(); + for i in 0..1024usize { + d[0x3C + i] = match ph { + 0..=9 => 0, + 10..=19 => 255, + _ => (((1024usize).wrapping_sub(i + (tick_n as usize) * 4)) & 0xFF) as u8, + }; + } + } + let _ = s.send_to(&d, BUTLER); + st.dmx_sent += 1; + dmx_seq = dmx_seq.wrapping_add(1); + // --- 事件帧 ×2 (形态B) --- + for _ in 0..2 { + let mut e = evt_t.clone(); + p32(&mut e, 0x22, evt_seq); + recent.insert(evt_seq); + if recent.len() > 64 { recent.remove(&(evt_seq.wrapping_sub(64))); } + let _ = s.send_to(&e, BUTLER); + st.evt_sent += 1; + evt_seq = evt_seq.wrapping_add(1); + } + // --- 收集应答 --- + for (_, b) in recv_window(&s, tick) { + match classify(&b) { + Rx::Ack(sq) => { + st.acks += 1; + if recent.contains(&sq) { st.ack_matched += 1; } + timeline.push((sq, tick_n)); + } + Rx::Status(_) => st.status += 1, + Rx::RdmResp(_, _) => st.rdm += 1, + Rx::DiscoveryResp(_) | Rx::EnetType1(_) => {} + Rx::Other(_) => st.other += 1, + } + } + tick_n += 1; + } + (st, timeline) +} + +pub fn t5_t8_session(secs: u64) -> Vec { + println!(" 会话运行 {}s (DMX→事件×2, 25ms/tick)…", secs); + let (st, _) = session(secs, true); + let rate_dmx = st.dmx_sent as f64 / secs as f64; + let pair = if st.acks > 0 { st.ack_matched as f64 / st.acks as f64 } else { 0.0 }; + vec![ + Item::new("T5", st.dmx_sent > secs * 30 && st.status > 0, + format!("DMX发送={} (~{:.0}Hz) 状态帧={}", st.dmx_sent, rate_dmx, st.status)), + Item::new("T6", pair >= 0.95, + format!("ACK={} 配对={} ({:.1}%)", st.acks, st.ack_matched, pair * 100.0)), + Item::new("T7", st.status > 0, format!("状态帧={}", st.status)), + Item::new("T8", st.dmx_sent > secs * 30 && st.acks > secs, + format!("会话{}s无中断", secs)), + ] +} + +// ---------- T9: 回落与恢复 ---------- +pub fn t9_fallback() -> Item { + println!(" 阶段1: 会话 10s…"); + let (a, _) = session(10, false); + println!(" 阶段2: 静默 12s (只收不发)…"); + let s = open(); + let quiet = recv_window(&s, Duration::from_secs(12)); + let mut quiet_status = 0usize; + for (_, b) in &quiet { + if let Rx::Status(_) = classify(b) { quiet_status += 1; } + } + println!(" 阶段3: 恢复会话 8s…"); + let (b, _) = session(8, false); + let pass = a.acks > 5 && b.acks > 5; // 恢复后 ACK 恢复 = 重新接管成功 + Item::new("T9", pass, format!( + "会话前段ACK={}; 静默期收{}包(状态{}); 恢复后ACK={}", + a.acks, quiet.len(), quiet_status, b.acks)) +} + +// ---------- T12(实验): AECHA 认领重放 ---------- +pub fn t12_claim_replay() -> Item { + let s = open(); + let tick = Duration::from_millis(25); + let c1 = load_template("claim_h112"); + let c2 = load_template("claim_h114"); + let c3 = load_template("claim_h116"); + let mut aereps = 0u64; + let mut last_detail = String::new(); + for (i, f) in [&c1, &c2, &c3].into_iter().enumerate() { + let _ = s.send_to(f, BUTLER); + let replies = recv_window(&s, Duration::from_millis(800)); + let mut got = false; + for (_, b) in &replies { + if find(b, b"AEREP") { aereps += 1; got = true; } + } + last_detail.push_str(&format!("[{}] 应答{}包{} ", i + 1, replies.len(), if got { "+AEREP" } else { "" })); + } + // 若 3 轮完成 → 立即试探 DMX: 发 10 个 tick 看是否出现 ACK + let mut acks = 0u64; + if aereps >= 1 { + let dmx_t = load_template("dmx_a"); + let evt_t = load_template("event_b"); + let mut dmx_seq = g16(&dmx_t, 0x1A).wrapping_add(1); + let mut evt_seq = g32(&evt_t, 0x22).wrapping_add(1); + for _ in 0..10 { + let mut d = dmx_t.clone(); + p16(&mut d, 0x1A, dmx_seq); + let _ = s.send_to(&d, BUTLER); + dmx_seq = dmx_seq.wrapping_add(1); + for _ in 0..2 { + let mut e = evt_t.clone(); + p32(&mut e, 0x22, evt_seq); + evt_seq += 1; + let _ = s.send_to(&e, BUTLER); + } + for (_, b) in recv_window(&s, tick) { + if let Rx::Ack(_) = classify(&b) { acks += 1; } + } + } + } + last_detail.push_str(&format!("; AEREP={} 试探ACK={}", aereps, acks)); + Item::new("T12", acks > 0, last_detail) +} + +/// takeover 变体: 使用外部传入的已 bind socket +pub fn t13_takeover_on(s: UdpSocket, secs: u64) -> Vec { + println!(" 发帧 {}s (共存模式)…", secs); + let (st, _) = session_on(s, secs, true, false); + let pair = if st.acks > 0 { st.ack_matched as f64 / st.acks as f64 } else { 0.0 }; + vec![ + Item::new("T13a", st.dmx_sent > secs * 20, format!("DMX发送={}", st.dmx_sent)), + Item::new("T13b", st.acks > 0, format!("Rust收到ACK={} 配对={:.1}% 状态={}", + st.acks, pair * 100.0, st.status)), + ] +} + +// ---------- T13(实验): 挂起 SymphoLight 后 Rust 抢占接管会话 ---------- +pub fn t13_takeover(secs: u64) -> Vec { + // 用抢 bind 的 socket 跑会话核心 (无 preamble: 认证已由 SymphoLight 完成) + let s = open_reuse(BIND); + println!(" 接管会话 {}s (SO_REUSEADDR 模式)…", secs); + let (st, _) = session_on(s, secs, true, false); + let pair = if st.acks > 0 { st.ack_matched as f64 / st.acks as f64 } else { 0.0 }; + vec![ + Item::new("T13a", st.dmx_sent > secs * 20, format!("接管后 DMX发送={}", st.dmx_sent)), + Item::new("T13b", st.acks > 0, format!("ACK={} 配对={} ({:.1}%) 状态={}", + st.acks, st.ack_matched, pair * 100.0, st.status)), + ] +} + +// ---------- 设备端模拟 v2 (Secondary IP, 与真 Butler 并存) ---------- +// 双 socket: wildcard(REUSEADDR) 收广播发现; dev_ip:31772 收会话/发应答(源IP正确) +pub fn device2(dev_ip: [u8; 4], dev_mac: [u8; 6], secs: u64, relay: Option<([u8; 4], u16)>) -> Vec { + let mut items = Vec::new(); + let bind_addr = format!("{}.{}.{}.{}:31772", dev_ip[0], dev_ip[1], dev_ip[2], dev_ip[3]); + let sess = open_reuse(&bind_addr); + // wildcard(REUSEADDR) 收广播发现; 注意: 必须先启动本程序、后启动 SymphoLight + let disc = std::panic::catch_unwind(|| open_reuse("0.0.0.0:31772")).ok(); + // AECH 中继 socket: 优先绑 113 (本机 123 网段时直接中继 .1); 非本网段则绑自身 IP (经桥中继) + let relay_sock: Option = relay.map(|_| { + std::panic::catch_unwind(|| open_reuse("192.168.123.113:0")) + .unwrap_or_else(|_| open_reuse(&format!("{}.{}.{}.{}:0", dev_ip[0], dev_ip[1], dev_ip[2], dev_ip[3]))) + }); + sess.set_read_timeout(Some(Duration::from_millis(2))).unwrap(); + println!(" 虚拟 Butler @ {} (MAC {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}), 运行 {}s", + bind_addr, dev_mac[0], dev_mac[1], dev_mac[2], dev_mac[3], dev_mac[4], dev_mac[5], secs); + + let resp_t = load_template("discovery_resp"); + let aerep_t = load_template("aerep"); + let ack_t = load_template("ack"); + let status_t = load_template("status"); // 84B "Butl" 设备状态遥测帧 + let reg89_t = load_template("register89"); // 89B 主机注册应答 (sub=02) + let aere594_t = load_template("aere594"); // 594B AECH 认领应答 (sub=06, AERE) + let ann144_t = load_template("announce144"); // 144B 宣告 (sub=01, 中继唤醒用) + // RDM 应答模板: 5种DISC脉冲(101B) + 分支应答(111B) + 参数信息(119B) + let pulse_ts = vec![load_template("rdm_pulse_0"), load_template("rdm_pulse_1"), + load_template("rdm_pulse_2"), load_template("rdm_pulse_3"), load_template("rdm_pulse_4")]; + let rdm111_t = load_template("rdm_resp111"); + let rdm119_t = load_template("rdm_info119"); + let setack68_t = load_template("rdm_setack68"); // RDM SET 应答模板 (CC=31, PDL=0, 68B, set123b 实证) + // 真机 .1 完整 RDM 应答帧池 (178 唯一帧: 56/59/70/77/87B 全部应答词表) + let rdm_pool = crate::enet::load_rdm_pool(); + println!(" RDM 帧池: {} 帧", rdm_pool.len()); + // PID→应答模板映射 (cmd=18=RDM GET, 查询 PID 在 0x50, 应答含 21) + let mut pid_map: std::collections::HashMap<[u8; 2], Vec> = std::collections::HashMap::new(); + // SET 写路径状态: 主机 SET (CC=0x30) 的值按 (UID, PID) 落存, 后续 GET 同 PID 回显新值 + // (真机行为: SET 立即生效, 主机 +33ms GET 验证 — set123b 实证, §5.6) + let mut set_store: std::collections::HashMap<([u8; 6], [u8; 2]), Vec> = std::collections::HashMap::new(); + for line in include_str!("../samples/pid_map.txt").lines() { + let line = line.trim(); + if line.len() < 8 { continue; } + let pid_hex = &line[..4]; + let frame_hex = line[5..].trim(); + let pb: Vec = (0..pid_hex.len()).step_by(2).map(|i| u8::from_str_radix(&pid_hex[i..i + 2], 16).unwrap()).collect(); + let fb: Vec = (0..frame_hex.len()).step_by(2).map(|i| u8::from_str_radix(&frame_hex[i..i + 2], 16).unwrap()).collect(); + if pb.len() == 2 { pid_map.insert([pb[0], pb[1]], fb); } + } + println!(" PID 映射: {} 个", pid_map.len()); + + let start = Instant::now(); + let mut n_disc = 0u64; let mut n_aecha = 0u64; let mut n_dmx = 0u64; + let mut n_evt = 0u64; let mut n_ack = 0u64; let mut n_other = 0u64; + let mut n_status = 0u64; let mut status_seq = 0x62u32; + let mut n_rdm_pulse = 0u64; let mut n_rdm_111 = 0u64; let mut n_rdm_119 = 0u64; + let mut n_rdm_set = 0u64; + let mut n_announce = 0u64; + let mut pulse_idx = 0usize; let mut pulse_seq = 0x0f0bu32; let mut info_ctr = 0xc0u8; + let mut pulse_step: u32 = 2; + let mut pool_idx = 0usize; + let mut cur_pulse = 0usize; let mut branch_ctr = 0u64; + let mut cur_fixture = 0usize; + let mut rdm_req_dumped = 0u32; + let mut n_reg = 0u64; let mut n_claim = 0u64; let mut claim_nonce: u32 = 0x51f31ab9; + let mut dmx_b12: Option = None; // AECH 质询 0x8C-0x8F 实例随机 → DMX 解密 rot 基值 + let mut n_rdm_relay = 0u64; let mut n_rdm_fwd = 0u64; + let mut pending_rdm_bridge = [0u8; 6]; + // AECH 中继: (认领者地址, 主机IP LE, 主机MAC) — 质询转发真机后暂存 + let mut pending_claim: Option<(std::net::SocketAddr, [u8;4], [u8;6])> = None; + let mut last_pulse = Instant::now() - Duration::from_secs(10); + let mut first_channels: Option<[u8; 32]> = None; + let mut last_channels: [u8; 32] = [0; 32]; + let mut ch_changes = 0u64; + let mut last_ch_show = Instant::now() - Duration::from_secs(10); + let mut peer: Option = None; + // RDM 应答延迟队列 (真机灯具经 RS485 应答延迟 12-48ms, 中位 29ms — 瞬时应答是假指纹) + let mut rdm_queue: Vec<(Instant, Vec, std::net::SocketAddr)> = Vec::new(); + let mut delay_lcg: u32 = 0x9e3779b9; + let mut next_delay = move |base_ms: u64, span_ms: u64| -> u64 { + delay_lcg = delay_lcg.wrapping_mul(2654435761).wrapping_add(0x9e3779b9); + base_ms + ((delay_lcg >> 16) as u64 % span_ms) + }; + // 42B 遥测: 真机 ~每 115ms 上报一次"已收事件计数" (0x20-0x23 严格递增, + // 增量=事件B帧数, ss123 实证 +2959 计数 ≈ 2963 事件) + let mut event_ctr: u32 = 0; + let mut ack42_sent_val: u32 = 0; + let mut last_ack42 = Instant::now() - Duration::from_secs(1); + let mut evt_dst: Option = None; + + while start.elapsed().as_secs() < secs { + // --- 真机无自发状态帧 (168s 仅 1 帧 42B, 已全部删除突发遥测) --- + // --- 到期的 RDM 延迟应答 --- + if !rdm_queue.is_empty() { + let now = Instant::now(); + let mut i = 0; + while i < rdm_queue.len() { + if rdm_queue[i].0 <= now { + let (_, r, dst) = rdm_queue.remove(i); + let _ = sess.send_to(&r, dst); + } else { i += 1; } + } + } + // --- 42B 遥测: 真机 ~每 115ms 上报一次"已收事件计数" (ss123 实证) --- + if last_ack42.elapsed() > Duration::from_millis(115) && event_ctr != ack42_sent_val { + if let Some(dst) = evt_dst { + let mut a = ack_t.to_vec(); + p32(&mut a, 0x20, event_ctr); + let _ = sess.send_to(&a, dst); + ack42_sent_val = event_ctr; + last_ack42 = Instant::now(); + } + } + let mut buf = [0u8; 2048]; + // --- wildcard socket: 收广播发现 (仅初始发现阶段用) --- + if let Some(ref d) = disc { + if let Ok((n, src)) = d.recv_from(&mut buf) { + let b = &buf[..n]; + if b.len() == 180 && &b[0..4] == b"ENet" && g32(b, 4) == 1 { + // 真机处理延迟 ~160ms (指纹对齐) + std::thread::sleep(Duration::from_millis(150)); + let mut r = resp_t.clone(); + patch_dev(&mut r, dev_ip, dev_mac); + patch_259_host(&mut r, src, b); + let _ = sess.send_to(&r, src); + n_disc += 1; + println!(" [{}] 发现请求 type1 from {} len={}", chrono_t(start), src, b.len()); + } else if b.len() >= 0x1A && &b[0..4] == b"ENet" && g32(b, 4) == 2 && g16(b, 0x18) == 1 { + // 广播主机宣告 sub=01 → 也回 89B 注册帧 (从 sess 口发出) + let mut r = reg89_t.clone(); + let ip_le = [dev_ip[3], dev_ip[2], dev_ip[1], dev_ip[0]]; + r[0x1A..0x1E].copy_from_slice(&ip_le); + r[0x1E..0x24].copy_from_slice(&dev_mac); + if b.len() > 0x24 { + r[0x25..0x29].copy_from_slice(&b[0x1A..0x1E]); + r[0x29..0x2F].copy_from_slice(&b[0x1E..0x24]); + } + let _ = sess.send_to(&r, src); + n_reg += 1; + } + } + } + // --- 中继回程轮询: 真机 AERE 回到 relay_sock (113:随机端口) --- + if let Some(rs) = relay_sock.as_ref() { + if let Ok((n, src)) = rs.recv_from(&mut buf) { + let b2 = &buf[..n]; + if b2.len() >= 8 && &b2[0..4] == b"ENet" { + // --- RDM 应答回程: 真机灯具的 type=04 应答 (脉冲59B/分支69B/参数77B) → 发给主机 --- + if g32(b2, 4) == 4 && b2.len() >= 40 { + if let Some(p) = peer { + let mut r = b2.to_vec(); + // cc01 帧内含 bridge-id (0x2D-0x32), 替换为查询的 bridge-id + if r.len() > 0x33 && r[0x2A] == 0xCC { + r[0x2D..0x33].copy_from_slice(&pending_rdm_bridge); + } + let _ = sess.send_to(&r, p); + n_rdm_fwd += 1; + if n_rdm_fwd <= 4 { println!(" [{}] RDM 应答回程 → 主机 ({}B)", chrono_t(start), r.len()); } + } + } + if let (Some((pc, hip, hmac)), Some(_)) = (pending_claim, relay) { + if b2.len() >= 552 && g32(b2, 4) == 2 && g16(b2, 0x18) == 6 { let mut r = b2.to_vec(); + let ip_le = [dev_ip[3], dev_ip[2], dev_ip[1], dev_ip[0]]; + r[0x1A..0x1E].copy_from_slice(&ip_le); + r[0x41..0x45].copy_from_slice(&ip_le); + r[0x1E..0x24].copy_from_slice(&dev_mac); + r[0x49..0x4F].copy_from_slice(&dev_mac); + r[0x25..0x28].copy_from_slice(&hip[0..3]); + r[0x2C..0x2F].copy_from_slice(&hmac[3..6]); + let _ = sess.send_to(&r, pc); + pending_claim = None; + n_claim += 1; + println!(" [{}] ★★ 真机 AERE 中继成功 → {} #{}", chrono_t(start), pc, n_claim); + } + } + } + } + } + // --- 会话 socket: 单播发现 / AECHA / DMX / 事件 --- + let (n, src) = match sess.recv_from(&mut buf) { + Ok(x) => x, + Err(_) => continue, + }; + let b = &buf[..n]; + if b.len() < 8 || &b[0..4] != b"ENet" { continue; } + peer = Some(src); // 记录对端(SymphoLight)用于遥测上行 + let t = g32(b, 4); + if t == 1 && b.len() == 180 { + let mut r = resp_t.clone(); + patch_dev(&mut r, dev_ip, dev_mac); + patch_259_host(&mut r, src, b); + let _ = sess.send_to(&r, src); + n_disc += 1; + println!(" [{}] 发现请求 type1 from {} len={}", chrono_t(start), src, b.len()); + } else if t == 2 { + let sub = if b.len() >= 0x1A { g16(b, 0x18) } else { 0 }; + if sub == 5 && find(b, b"AECH") { + if let (Some((rip, rport)), Some(rs)) = (relay, relay_sock.as_ref()) { + // --- 中继模式: 先向中继目标发 sub=01 宣告唤醒注册态, 再转发质询 --- + let hip = [b[0x1A], b[0x1B], b[0x1C], b[0x1D]]; + let hmac = [b[0x1E], b[0x1F], b[0x20], b[0x21], b[0x22], b[0x23]]; + pending_claim = Some((src, hip, hmac)); + let raddr = format!("{}.{}.{}.{}:{}", rip[0], rip[1], rip[2], rip[3], rport); + // 宣告帧: 0x1A-1D 主机IP(LE), 0x1E-23 主机MAC + let mut ann = ann144_t.clone(); + ann[0x1A..0x1E].copy_from_slice(&hip); + ann[0x1E..0x24].copy_from_slice(&hmac); + let _ = rs.send_to(&ann, &raddr); + std::thread::sleep(Duration::from_millis(30)); + let _ = rs.send_to(b, &raddr); + println!(" [{}] AECH 质询中继 → 真机 {} (先宣告唤醒)", chrono_t(start), raddr); + } else { + // --- AECH 认领质询 (594B sub=05) → AERE 应答 (sub=06) --- + // 质询帧内: 0x1A-1D 主机IP(LE), 0x1E-23 主机MAC, 0x28 "AECH" + let mut r = aere594_t.clone(); + // 设备 IP (LE) 0x1A 与 0x41; 设备 MAC 0x1E 与 0x49 + let ip_le = [dev_ip[3], dev_ip[2], dev_ip[1], dev_ip[0]]; + r[0x1A..0x1E].copy_from_slice(&ip_le); + r[0x41..0x45].copy_from_slice(&ip_le); + r[0x1E..0x24].copy_from_slice(&dev_mac); + r[0x49..0x4F].copy_from_slice(&dev_mac); + // AERE 0x25-27 = 主机IP前3字节(LE), 0x2C-2E = 主机MAC后3字节 + if b.len() > 0x24 { + r[0x25..0x28].copy_from_slice(&b[0x1A..0x1D]); + r[0x2C..0x2F].copy_from_slice(&b[0x21..0x24]); + } + // 0xC0-0xC3 = AgiosElias G(nonce@0x88-0x8B LE) — 算法已从 SymphoLight + // Ecue.Devices.dll 反编译还原 (无密钥! G(x)=x^TABLE[(x%32+1+(x*13)%30)%32]), + // 主机按同式验证 AERE blob[152]。无需真机中继。 + let nonce = if b.len() > 0x8C { g32(b, 0x88) } else { 0 }; + p32(&mut r, 0xC0, crate::aech::agios_g(nonce)); + // 0x8C-0x8F = AgiosElias 实例随机 B12 — DMX 帧混淆 rot=(B12^r)&511 的会话基值 + if b.len() > 0x90 { dmx_b12 = Some(g32(b, 0x8C)); } + let _ = sess.send_to(&r, src); + n_claim += 1; + println!(" [{}] ★ AECH 认领质询 → 本地计算 AERE (G({:08x})={:08x}) #{}", + chrono_t(start), nonce, crate::aech::agios_g(nonce), n_claim); + } + } else if sub == 1 { + // --- 主机身份宣告 sub=01 (102B) → 89B 注册应答 sub=02 --- + // 宣告内: 0x1A-1D 主机IP(LE), 0x1E-23 主机MAC + let mut r = reg89_t.clone(); + let ip_le = [dev_ip[3], dev_ip[2], dev_ip[1], dev_ip[0]]; + r[0x1A..0x1E].copy_from_slice(&ip_le); + r[0x1E..0x24].copy_from_slice(&dev_mac); + if b.len() > 0x24 { + r[0x25..0x29].copy_from_slice(&b[0x1A..0x1E]); // 主机IP(LE) + r[0x29..0x2F].copy_from_slice(&b[0x1E..0x24]); // 主机MAC + } + let _ = sess.send_to(&r, src); + n_reg += 1; + if n_reg <= 3 { println!(" [{}] 主机宣告 sub=01 → 回 89B 注册帧", chrono_t(start)); } + } else { + // --- 已认领心跳 sub=03 (36B, 每~6s): 真机从不应答 (cold2 实证), 静默 --- + n_status += 1; // 仅计数 + } + } else if t == 4 && b.len() == 1084 { + if g16(b, 0x20) != 0x0101 { + event_ctr = event_ctr.wrapping_add(1); // 事件B帧计数 (含 RDM 查询, 42B 遥测用) + evt_dst = Some(src); + } + if g16(b, 0x20) == 0x0101 { + n_dmx += 1; + // DMX 数据经 zPprv0TXRRW 逐帧混淆 (§4.1): r=0x22-0x25 LE, rot=(B12^r)&511 + let ch16 = match dmx_b12 { + Some(b12) => { + let r1 = g32(b, 0x22); + let plain = crate::aech::agios_dmx_decrypt(&b[0x3C..0x3C + 512], r1, b12); + hexs(&plain[..16]) + } + None => hexs(&b[0x3C..0x3C + 16]), // 未认领前: 原始线值 + }; + let mut ch = [0u8; 32]; + ch.copy_from_slice(&b[0x3C..0x3C + 32]); + if first_channels.is_none() { first_channels = Some(ch); } + if ch != last_channels { ch_changes += 1; last_channels = ch; } + if last_ch_show.elapsed() > Duration::from_secs(5) { + last_ch_show = Instant::now(); + println!(" [{}] DMX #{} CH1-16: {}", chrono_t(start), n_dmx, ch16); + } + } else if b.len() > 0x44 && b[0x3C] == 0x01 && (b[0x3D] == 0x24 || b[0x3D] == 0x18 || b[0x3D] == 0x19 || b[0x3D] == 0x1a) { + // --- RDM 请求 (0x20=0200, 0x3C=01, 0x3D=cmd, 0x3E+=目标UID) --- + let mut uid = [0u8; 6]; + uid.copy_from_slice(&b[0x3E..0x44]); + // RDM 一律本地生成式应答 (全新UID灯具); relay 仅用于 AECH 认领 + if rdm_req_dumped < 3 { + rdm_req_dumped += 1; + let dump_len = std::cmp::min(0x80, b.len()); + println!(" [RDM-REQ#{}] len={} hex[0..{}]={}", rdm_req_dumped, b.len(), dump_len, hexs(&b[..dump_len])); + } + if b[0x3D] == 0x19 && rdm_req_dumped < 8 { + rdm_req_dumped += 1; + let dump_len = std::cmp::min(0x90, b.len()); + println!(" [RDM-19#{}] len={} hex[0..{}]={}", rdm_req_dumped, b.len(), dump_len, hexs(&b[..dump_len])); + } + if b[0x30] != 0x00 { + // flag=01 = DMX 内嵌副本帧: 真机完全不答, 忽略 + } else if b[0x3D] == 0x24 { + // --- DISC_UNIQUE_BRANCH: 分支范围 lo@0x53-0x58 hi@0x59-0x5E (PID 0001@0x50, PDL=12@0x52) --- + // 真机灯具只应答 UID 落在分支范围内的查询 (ss123 实证二分收敛); + // 此前无视范围全部应答 → 主机二分水远无法排空分支 → 51/s 发现洪水, 稳态轮询不启动 + if b.len() > 0x5E { + let lo = &b[0x53..0x59]; let hi = &b[0x59..0x5F]; + for fx in FIXTURES.iter() { + if &fx[..] >= lo && &fx[..] <= hi { + let pulse = rdm_encode_uid(fx); + let mut r: Vec = Vec::with_capacity(59); + r.extend_from_slice(&pulse_ts[0][..0x2A]); // 模板头 + // 脉冲 seq = 设备本地自增序号 (真机行为, 非回显!) + p32(&mut r, 0x20, pulse_seq); + pulse_seq = pulse_seq.wrapping_add(pulse_step); + pulse_step = 1 + (pulse_step * 7 + 0x1d) % 3; // 每帧 +1..3 + r.extend_from_slice(&pulse); + let due = Instant::now() + Duration::from_millis(next_delay(2, 40)); + rdm_queue.push((due, r, src)); + n_rdm_pulse += 1; + } + } + if n_rdm_pulse <= 5 || n_rdm_pulse % 100 == 0 { + println!(" [RDM] 分支发现范围内应答 #{}", n_rdm_pulse); + } + } + } else if b[0x3D] == 0x18 && uid == [0xff; 6] { + // 广播 UID 的发现命令 (广播 MUTE 0003 等): 真机不应答, 静默 + } else if b[0x3D] == 0x18 { + // cmd=18 = RDM GET. PID 在 0x50-0x51. + // PID=0002 = DISC_UN_MUTE → 70B ACK (CC=11, PDL=2, PD=0000) — 冷启动抓包实证 + let pid = if b.len() > 0x51 { [b[0x50], b[0x51]] } else { [0, 0] }; + let mirror = mirror_uid(&uid); + // 优先帧池中镜像灯具的同 PID 应答 (GET_RESP=0x21, 数据自洽); 其次 pid_map; 兜底 111 + let pool_hit = rdm_pool.iter().find(|f| + f.len() >= 0x42 && f[0x2A] == 0xCC && f[0x33..0x39] == mirror + && f[0x3E] == 0x21 && f[0x3F] == pid[0] && f[0x40] == pid[1]); + let mut r = if let Some(t) = pool_hit { + t.clone() + } else if let Some(t) = pid_map.get(&pid) { + t.clone() + } else { + rdm111_t.clone() + }; + if r.len() > 0x39 { r[0x33..0x39].copy_from_slice(&uid); } // src UID = 虚拟灯具 + // dest UID (0x2D-0x32) = 查询方 bridge-id; seq 回显; TN 回显查询 TN(0x4A) + if b.len() > 0x4A && r.len() > 0x33 { r[0x2D..0x33].copy_from_slice(&b[0x44..0x4A]); } + r[0x20] = b[0x22]; r[0x21] = b[0x23]; + if b.len() > 0x4B && r.len() > 0x3A { r[0x39] = b[0x4A]; } + // SET 过的 PID 回显新值 (真机: SET 立即生效, §5.6) + if let Some(v) = set_store.get(&(uid, pid)) { + let pdl = if r.len() > 0x41 { r[0x41] as usize } else { 0 }; + if pdl == v.len() && r.len() >= 0x42 + pdl { + r[0x42..0x42 + pdl].copy_from_slice(v); + } + } + rdm_checksum(&mut r); // patch 后必须重算 (真机 1396/1396 有效) + let due = Instant::now() + Duration::from_millis(next_delay(15, 25)); + n_rdm_111 += 1; + if n_rdm_111 <= 6 || n_rdm_111 % 100 == 0 { + println!(" [RDM] GET UID={:02x}{:02x}{:02x}{:02x}{:02x}{:02x} PID={:02x}{:02x} → {}B", + uid[0], uid[1], uid[2], uid[3], uid[4], uid[5], pid[0], pid[1], r.len()); + } + rdm_queue.push((due, r, src)); + } else if b[0x4F] == 0x30 { + // --- RDM SET (CC=0x30, msgLen=0x1a+): 真机回 CC=0x31 ACK, RT=00, PDL=0, + // 68B, TN/UID 回显, checksum 有效, 延迟 18-25ms (set123b 实证, §5.6) --- + let mut r = setack68_t.clone(); + if r.len() > 0x39 { r[0x33..0x39].copy_from_slice(&uid); } // src UID = 虚拟灯具 + if b.len() > 0x4A && r.len() > 0x33 { r[0x2D..0x33].copy_from_slice(&b[0x44..0x4A]); } // dest = 查询方 + r[0x20] = b[0x22]; r[0x21] = b[0x23]; // seq 回显 + if b.len() > 0x4B && r.len() > 0x3A { r[0x39] = b[0x4A]; } // TN 回显 + if b.len() > 0x51 && r.len() > 0x41 { r[0x3F] = b[0x50]; r[0x40] = b[0x51]; } // PID 回显 + rdm_checksum(&mut r); // patch 后必须重算 + let due = Instant::now() + Duration::from_millis(next_delay(15, 25)); + let pid = if b.len() > 0x51 { [b[0x50], b[0x51]] } else { [0, 0] }; + // 应用写入值 (PDL@0x52, PD@0x53): 后续 GET 同 PID 回显新值 (§5.6 主机验证模式) + let pdl = if b.len() > 0x52 { b[0x52] as usize } else { 0 }; + if pdl > 0 && b.len() >= 0x53 + pdl { + set_store.insert((uid, pid), b[0x53..0x53 + pdl].to_vec()); + } + rdm_queue.push((due, r, src)); + n_rdm_set += 1; + if n_rdm_set <= 6 || n_rdm_set % 100 == 0 { + println!(" [RDM] SET UID={:02x}{:02x}{:02x}{:02x}{:02x}{:02x} PID={:02x}{:02x} PD={} → 68B ACK #{}", + uid[0], uid[1], uid[2], uid[3], uid[4], uid[5], pid[0], pid[1], + hexs(set_store.get(&(uid, pid)).map(|v| &v[..]).unwrap_or(&[])), n_rdm_set); + } + } else { + // cmd=19/1a (SET类/传感器轮询 SENSOR_VALUE 0201): PID@0x50, PD@0x53 + // 按镜像灯具 + PID + PD[0](传感器序号) 从帧池取自恰应答 + let pid = if b.len() > 0x51 { [b[0x50], b[0x51]] } else { [0, 0] }; + let pd0 = if b.len() > 0x53 { b[0x53] } else { 0 }; + let mirror = mirror_uid(&uid); + let pool_hit = rdm_pool.iter().find(|f| + f.len() >= 0x43 && f[0x2A] == 0xCC && f[0x33..0x39] == mirror + && f[0x3F] == pid[0] && f[0x40] == pid[1] && f[0x42] == pd0); + let mut r = if let Some(t) = pool_hit { + t.clone() + } else { + rdm119_t.clone() + }; + if r.len() > 0x39 { r[0x33..0x39].copy_from_slice(&uid); } + if b.len() > 0x4A && r.len() > 0x33 { r[0x2D..0x33].copy_from_slice(&b[0x44..0x4A]); } + r[0x20] = b[0x22]; r[0x21] = b[0x23]; + if b.len() > 0x4B && r.len() > 0x3A { r[0x39] = b[0x4A]; } + rdm_checksum(&mut r); + let due = Instant::now() + Duration::from_millis(next_delay(15, 25)); + let rlen = r.len(); + rdm_queue.push((due, r, src)); + n_rdm_119 += 1; + if n_rdm_119 <= 6 || n_rdm_119 % 100 == 0 { + println!(" [RDM] 参数/传感器 UID={:02x}{:02x}{:02x}{:02x}{:02x}{:02x} PID={:02x}{:02x} PD0={:02x} → {}B #{}", + uid[0], uid[1], uid[2], uid[3], uid[4], uid[5], pid[0], pid[1], pd0, rlen, n_rdm_119); + } + } + } else { + // 非 RDM 事件B帧: 真机只计数, 由 42B 周期遥测上报, 无即时 ACK (ss123 实证) + n_evt += 1; + } + } else { + n_other += 1; + if n_other <= 12 { + println!(" [?] 其他帧 src={} len={} type={} hex[0..48]={}", src, b.len(), t, hexs(&b[..std::cmp::min(48, b.len())])); + } + } + } + items.push(Item::new("U1", n_disc > 0, format!("发现应答={} 遥测={} RDM脉冲={} RDM111={} RDM119={}", n_disc, n_status, n_rdm_pulse, n_rdm_111, n_rdm_119))); + items.push(Item::new("U2", n_aecha > 0, format!("AECHA 认领交换={} 轮 (AEREP 已回)", n_aecha))); + items.push(Item::new("U3", n_dmx > 0, format!("明文 DMX 帧接收={}", n_dmx))); + items.push(Item::new("U4", n_ack > 0, format!("事件帧={} ACK发送={}", n_evt, n_ack))); + items.push(Item::new("U5", n_dmx > secs * 10, format!( + "通道值: 首32CH={:?} 变化={} 其他帧={}", first_channels.unwrap_or([0; 32]), ch_changes, n_other))); + items +} + +// ---------- RDM 广播发现重放探测: 向目标重放真机 RDM 广播请求, 抓取并分类应答 ---------- +pub fn rdm_probe(target: &str, secs: u64) -> Item { + let req_t = load_template("rdm_disc_req"); + let bind = "0.0.0.0:0"; + let s = UdpSocket::bind(bind).expect("rdmprobe bind 失败"); + s.set_broadcast(true).unwrap(); + s.set_read_timeout(Some(Duration::from_millis(120))).unwrap(); + let dst = format!("{}:31772", target); + println!(" rdmprobe → {} 运行 {}s", dst, secs); + let start = Instant::now(); + let mut seq: u32 = 0x090f; + let mut sent = 0u64; + let mut resp_lens: std::collections::HashMap = std::collections::HashMap::new(); + let mut first_resp: Option> = None; + let mut delays: Vec = Vec::new(); + let mut last_send = Instant::now() - Duration::from_secs(10); + let mut pending: Option<(Instant, u32)> = None; + while start.elapsed().as_secs() < secs { + // 每 500ms 发一次请求 + if last_send.elapsed() > Duration::from_millis(500) { + let mut r = req_t.clone(); + p32(&mut r, 0x20, seq); + let _ = s.send_to(&r, &dst); + pending = Some((Instant::now(), seq)); + seq = seq.wrapping_add(1); + sent += 1; + last_send = Instant::now(); + } + let mut buf = [0u8; 2048]; + match s.recv_from(&mut buf) { + Ok((n, src)) => { + let b = &buf[..n].to_vec(); + *resp_lens.entry(n).or_insert(0) += 1; + if first_resp.is_none() { first_resp = Some(b.clone()); } + if let Some((t0, sq)) = pending { + if n >= 0x22 && g32(b, 4) == 4 { + let echo = g16(b, 0x20); + let expect = ((sq >> 16) as u16) ^ 0; // seq低16位在0x22 + let _ = expect; + delays.push((t0.elapsed().as_millis() as u32)); + println!(" [{}] 应答 src={} len={} seqEcho=0x{:04x} (请求seq低16=0x{:04x}) 延迟={}ms", + chrono_t(start), src, n, echo, (sq & 0xffff) as u16, t0.elapsed().as_millis()); + } else { + println!(" [{}] 应答 src={} len={} type={}", chrono_t(start), src, n, if n>=8 { g32(b,4) } else { 0 }); + } + pending = None; + } + } + Err(_) => {} + } + } + let mut lens_desc = String::new(); + let mut keys: Vec<_> = resp_lens.keys().cloned().collect(); + keys.sort(); + for k in keys { lens_desc.push_str(&format!(" {}B×{}", k, resp_lens[&k])); } + let avg_delay = if delays.is_empty() { 0 } else { delays.iter().sum::() / delays.len() as u32 }; + Item::new("RDMP", sent > 0 && !resp_lens.is_empty(), + format!("请求={} 应答[{}] 平均延迟={}ms", sent, lens_desc.trim(), avg_delay)) +} + +// ---------- RDM DISC 脉冲编码 (E1.20) ---------- +/// 虚拟灯具: 全新 UID (同厂 0107 系列, 主机未注册过的序列号) +const FIXTURES: [[u8; 6]; 3] = [ + [0x01, 0x07, 0x34, 0x67, 0x01, 0x30], + [0x01, 0x07, 0x34, 0x67, 0x01, 0x31], + [0x01, 0x07, 0x34, 0x67, 0x01, 0x32], +]; +/// 镜像映射: 虚拟 UID → 真机灯具 UID (参数数据按镜像灯具从 rdm_pool 取, +/// 使 3 台虚拟灯具拥有各自自洽的 DMX 地址/标签/传感器数, 与 .1 总线同构) +const FIXTURE_MIRROR: [[u8; 6]; 3] = [ + [0x01, 0x07, 0x34, 0x67, 0x01, 0x25], // NFS1K2DxxxBx DMX101-102 23传感器 + [0x01, 0x07, 0x34, 0x88, 0x09, 0x00], // NFM680SxxxGx DMX111 17传感器 + [0x08, 0x99, 0x50, 0x00, 0x26, 0xc9], // HMKJ DMX1-32 4传感器 +]; +fn mirror_uid(uid: &[u8; 6]) -> [u8; 6] { + for (i, f) in FIXTURES.iter().enumerate() { + if f == uid { return FIXTURE_MIRROR[i]; } + } + *uid +} + +// E1.20 checksum: 从 CC(0x2A) 起 msgLen(0x2C) 字节的 16-bit 和, BE 写于 [0x2A+msgLen] +// (冷启动抓包验证: 真机 1396/1396 全部满足此规则) +fn rdm_checksum(r: &mut [u8]) { + if r.len() < 0x2E || r[0x2A] != 0xCC { return; } + let msg_len = r[0x2C] as usize; + if r.len() < 0x2A + msg_len + 2 { return; } + let mut sum: u16 = 0; + for i in 0x2A..0x2A + msg_len { sum = sum.wrapping_add(r[i] as u16); } + r[0x2A + msg_len] = (sum >> 8) as u8; + r[0x2A + msg_len + 1] = (sum & 0xFF) as u8; +} + +fn rdm_encode_uid(uid: &[u8; 6]) -> [u8; 17] { + let mut out = [0u8; 17]; + out[0] = 0xAA; // 分隔符 + let mut sum: u16 = 0; + for (i, &b) in uid.iter().enumerate() { + let b1 = 0xAA | (b & 0x55); + let b2 = 0x55 | (b & 0xAA); + out[1 + 2 * i] = b1; + out[2 + 2 * i] = b2; + sum += b1 as u16 + b2 as u16; + } + let hi = (sum >> 8) as u8; + let lo = (sum & 0xFF) as u8; + out[13] = 0xAA | (hi & 0x55); + out[14] = 0x55 | (hi & 0xAA); + out[15] = 0xAA | (lo & 0x55); + out[16] = 0x55 | (lo & 0xAA); + out +} + +// ---------- 259 应答的 Connected To 动态化: 从发现请求提取请求方主机 IP+MAC ---------- +fn patch_259_host(r: &mut [u8], src: std::net::SocketAddr, req: &[u8]) { + if let std::net::IpAddr::V4(v4) = src.ip() { + let o = v4.octets(); + let ip_le = [o[3], o[2], o[1], o[0]]; + // host IP (0x4F-0x52) = 请求方 IP (LE) + if r.len() > 0x53 { r[0x4F..0x53].copy_from_slice(&ip_le); } + // host MAC (0x53-0x58): 请求里 hostIP(LE) 后 +8 处 (IP4 + 00ffffff掩码 + MAC6) + for i in 0..req.len().saturating_sub(14) { + if req[i..i + 4] == ip_le && req[i + 4..i + 8] == [0x00, 0xff, 0xff, 0xff] { + if r.len() > 0x59 { r[0x53..0x59].copy_from_slice(&req[i + 8..i + 14]); } + break; + } + } + } +} + +// ---------- bridge: AECH 双向桥 (非123网段设备的认领桥) ---------- +// 监听 listen_ip:listen_port 收远端设备的 AECH, 以源 IP=123.113 转发给真机, 再把真机 AERE 回传给远端设备 +pub fn bridge(listen_ip: [u8; 4], listen_port: u16, real_ip: [u8; 4], secs: u64) { + let listen_addr = format!("{}.{}.{}.{}:{}", listen_ip[0], listen_ip[1], listen_ip[2], listen_ip[3], listen_port); + let listen = open_reuse(&listen_addr); + let to_real = open_reuse("192.168.123.113:0"); // 源=113, 真机才应答 + let real_addr = format!("{}.{}.{}.{}:31772", real_ip[0], real_ip[1], real_ip[2], real_ip[3]); + println!(" AECH 桥 @ {} ↔ {} (真机源 123.113)", listen_addr, real_addr); + let mut pending: Option = None; + let start = Instant::now(); + let mut n_fwd = 0u64; let mut n_back = 0u64; + while start.elapsed().as_secs() < secs { + let mut buf = [0u8; 2048]; + // 远端设备 -> 真机 (AECH) + if let Ok((n, src)) = listen.recv_from(&mut buf) { + let b = &buf[..n]; + if b.len() >= 8 && &b[0..4] == b"ENet" && g32(b, 4) == 2 && find(b, b"AECH") { + pending = Some(src); + let _ = to_real.send_to(b, &real_addr); + n_fwd += 1; + println!(" [{}] 桥: {} → 真机 (AECH #{})", chrono_t(start), src, n_fwd); + } + } + // 真机 -> 远端设备 (AERE) + if let Ok((n, src)) = to_real.recv_from(&mut buf) { + let b = &buf[..n]; + if b.len() >= 8 && &b[0..4] == b"ENet" && g32(b, 4) == 2 { + if let Some(p) = pending { + let _ = listen.send_to(b, p); + n_back += 1; + println!(" [{}] 桥: 真机 → {} (AERE #{})", chrono_t(start), p, n_back); + } + } + } + } +} + +fn chrono_t(start: Instant) -> String { format!("{:.1}s", start.elapsed().as_secs_f64()) } + +fn patch_dev(b: &mut [u8], ip: [u8; 4], mac: [u8; 6]) { + // 找 Butler IP LE (01 7b a8 c0) 替换 — 字段为小端存储: 192.168.123.200 → c8 7b a8 c0 + for i in 0..b.len().saturating_sub(4) { + if b[i] == 0x01 && b[i + 1] == 0x7b && b[i + 2] == 0xa8 && b[i + 3] == 0xc0 { + b[i] = ip[3]; b[i + 1] = ip[2]; b[i + 2] = ip[1]; b[i + 3] = ip[0]; + break; + } + } + // 找 Butler MAC 替换 + for i in 0..b.len().saturating_sub(6) { + if b[i] == 0x00 && b[i + 1] == 0x16 && b[i + 2] == 0x1c && b[i + 3] == 0xf1 && b[i + 4] == 0x2e && b[i + 5] == 0x39 { + b[i..i + 6].copy_from_slice(&mac); + break; + } + } +} + +// ---------- T10/T11: RDM 探索 ---------- +pub fn t10_t11_rdm(secs: u64) -> Vec { + let mut items = Vec::new(); + let s = open(); + let req = load_template("rdm_req"); + // T10: 发送 RDM 请求事件帧模板 (DISC 帧), 解析 Butler RDM 应答 + println!(" 发送 RDM 请求模板帧并监听 {}s…", secs); + let deadline = Instant::now() + Duration::from_secs(secs); + let mut uids: HashSet = HashSet::new(); + let mut rdm_n = 0u64; + let mut disc_n = 0u64; + let mut req_seq = g32(&req, 0x22); + while Instant::now() < deadline { + let mut r = req.clone(); + p32(&mut r, 0x22, req_seq); + req_seq = req_seq.wrapping_add(1); + let _ = s.send_to(&r, BUTLER); + for (_, b) in recv_window(&s, Duration::from_millis(60)) { + match classify(&b) { + Rx::RdmResp(_, rlen) => { + rdm_n += 1; + if rlen >= 17 && b[0x2A] == 0xAA { disc_n += 1; } // 发现脉冲 + if rlen >= 18 && b[0x2A] == 0xCC { + let uid = hexs(&b[0x32..0x38]); + uids.insert(uid); + } + } + _ => {} + } + } + } + items.push(Item::new("T10", rdm_n > 0, + format!("RDM应答={} (发现脉冲={}, 设备UID={})", rdm_n, disc_n, + uids.iter().cloned().collect::>().join(" ")))); + // T11: 写地址 (PID 0x0330) —— e:cue 私有封装下 PID 未知, 仅记录探索结论 + items.push(Item::new("T11", false, + "写地址 SET DMX_START_ADDRESS: e:net 私有 PID 未确认, 需结合 GUI 写地址抓包比对 (见文档 §5.4)".to_string())); + items +} diff --git a/validation_report.json b/validation_report.json new file mode 100644 index 0000000..5123c2f --- /dev/null +++ b/validation_report.json @@ -0,0 +1,10 @@ +{ + "target": "192.168.123.1", + "results": [ + { "id": "U1", "pass": true, "detail": "发现应答=6898 遥测=19980 RDM脉冲=0 RDM111=0 RDM119=0" }, + { "id": "U2", "pass": false, "detail": "AECHA 认领交换=0 轮 (AEREP 已回)" }, + { "id": "U3", "pass": true, "detail": "明文 DMX 帧接收=18958" }, + { "id": "U4", "pass": false, "detail": "事件帧=0 ACK发送=0" }, + { "id": "U5", "pass": false, "detail": "通道值: 首32CH=[45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14] 变化=18897 其他帧=2329" } + ] +}