Compare commits

..

7 Commits

Author SHA1 Message Date
OpenCode
50b04eb0d8 merge: integrate morning session history (scripts/tools/docs) with current fully-decoded codebase
Conflict resolution: keep current (afternoon) versions for code/README/samples
- they contain the fully-cracked protocol + pure self-constructed show mode;
morning session's analysis scripts (scripts/session/*), DLL tools (tools/*)
and protocol docs (docs/enet_protocol.md) are merged in intact.
2026-09-11 15:31:10 +08:00
OpenCode
c555a2252f feat: show mode claim frame now fully self-constructed (zero opaque blocks)
build_claim_frame(): 552B AECH claim from scratch - envelope + host IP/MAC +
AECH + zeroed opaque regions + nonce@0x88 + B12@0x8C. Verified on real Butler:
AERE PASS, 3 fixtures online. Show mode no longer needs claim_h112 template.
2026-09-11 15:03:09 +08:00
OpenCode
44ff080ae6 feat: decode remaining frame fields (claim/AERE/announce/discovery-resp/EUCE/events)
- claim 552B: full layout; 240B opaque blocks proven UNCHECKED by Butler
  (CLAIM_ZERO=1 ablation: zeroed blocks -> AERE still PASS) -> claim can be
  built from scratch with zeros
- AERE 552B: Butler IP/MAC/netmask, fixture UID@0x33, slow counter@0xB1/0xB5,
  resp@0xC0; only resp+counter vary across captures
- announce 644B: session GUID, UTF-16BE show-file path, full timestamp
  @0x239 (y/mo/d/h/mi/s/ms; live sample decodes to 22:52:50.992 matching SL
  session), ephemeral port@0x25C, enable flags
- discovery-resp 217B: device name, serial blob, network block, config block
- EUCE 540B: 'EUCE' magic, version 5, username+computername+GUID, host MAC
- event_b: type=4 1084B RDM-uplink frame identified (RDM response @0x3C)
- analyze: new 'xor' subcommand with periodicity analysis
2026-09-11 14:49:39 +08:00
OpenCode
f6b1f104ef docs: decode heartbeat/discovery-req fields; expand unresolved-items list 2026-09-11 14:07:29 +08:00
OpenCode
8938b15691 fix: dual-port DMX output in show mode; AERE claim verification; SHOW_DIAG mode
- Root cause of 'steady white': fixture on Butler port2, our data only went to port1
  (port data regions are independent: port1 @0x3C, port2 @0x23C)
- show mode now mirrors pattern to both ports; SHOW_DIAG=1 gives 3s off/on blink test
- Claim now verifies AERE response and warns on failure
- Docs updated with port layout note; README updated
2026-09-11 13:53:21 +08:00
OpenCode
9be449fda8 feat: crack all DMX/RDM frame fields; show mode now fully self-constructed
- analyze subcommand: offline replay/template field variance, diff, counter analysis
- DMX header fully decoded: port mask @0x20, port lens u16LE=512 @0x34/0x36, counter +1/frame
- RDM request frame fully decoded: counter=0, 0x20=02, 0x2A=0xCC, msgLen=24+PDL,
  checksum=0xCC+sum(0x3C..0x53+PDL) @0x53+PDL, pair copy rules
- Fix historical misreadings: RDM always starts @0x3C (not variable); no session-level
  frame check exists - earlier failures were wrong template + checksum off-by-one
- show mode: build_dmx_frame/build_rdm_frame2/rdm_pair_copy, zero template dependency
- Decode 47B register response and 42B event frames; doc enet_protocol_current.md rewritten
2026-09-11 12:53:36 +08:00
OpenCode
9995335d74 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.
2026-09-11 12:03:43 +08:00
13 changed files with 1227 additions and 318 deletions

View File

@ -53,6 +53,23 @@ enet-validator dmxroll <tsv> # 滚动值分析(离线)
enet-validator aechprobe <tsv> <ip> # AECH 在线探测 enet-validator aechprobe <tsv> <ip> # 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 通道灯具),**零模板纯自构造帧**(`build_dmx_frame`),**port1+port2 双口同时输出**(灯具接任一口均可控)
- RDM:每 700ms 轮询 3 台已知 UID,实时打印 `DEVICE_INFO` 与 `DMX_START_ADDRESS`,**纯自构造请求帧**(`build_rdm_frame2`)
- 运行无需任何抓包/replay 文件;DMX/RDM 帧全字段已破解(见 docs/enet_protocol_current.md)
## 已破解要点(详见 docs/enet_protocol.md) ## 已破解要点(详见 docs/enet_protocol.md)
- **AECH 认领**:`G(x) = x ^ TABLE[(x%32 + 1 + (x*13)%30) % 32]`(无密钥,表内嵌 DLL) - **AECH 认领**:`G(x) = x ^ TABLE[(x%32 + 1 + (x*13)%30) % 32]`(无密钥,表内嵌 DLL)

View File

@ -0,0 +1,245 @@
# e:net 协议当前还原状态(截至 2026-09-11,全面逆向后)
本文档汇总基于抓包与当前程序实现的协议还原状态。
**2026-09-11 全面逆向结论:DMX 帧与 RDM 请求帧已 100% 字段破解,可零模板纯自构造。**
---
## 一、UDP/31772 通用信封(全部帧)
| 偏移 | 含义 |
|---|---|
| 0x00-0x03 | `"ENet"` magic |
| 0x04-0x07 | 帧类型 u32 LE(1=发现, 2=注册/宣告, 4=数据/RDM) |
| 0x08-0x09 | 00 00(常量) |
| 0x0A-0x0B | 目标端口 u16 LE = 31772 (0x7c1c) |
| 0x0C-0x11 | 全 0 |
| 0x12-0x13 | 01 00(常量) |
| 0x14-0x15 | 帧总长 u16 LE(如 1084=0x043c, 47=0x002f, 42=0x002a) |
| 0x16-0x17 | 00 00 |
| 0x18-0x19 | 子类型 u16 LE(01=主机→Butler 数据, 02=Butler→主机应答/注册回执) |
---
## 二、DMX 数据帧(1084B,主机→Butler,全破解)
| 偏移 | 含义 | 备注 |
|---|---|---|
| 0x00-0x19 | 通用信封 | type=4, 子类型=01 |
| 0x1A-0x1D | 帧计数器 u32 LE | **每帧 +1,任意起始值**(1031 帧实证);与 r1/r2 无关 |
| 0x1E-0x1F | 00 00 | 常量 |
| 0x20-0x21 | 01 01 | 端口使能 mask(port1+port2) |
| 0x22-0x25 | r1 u32 LE | 随机数,port1 加密种子 |
| 0x26-0x29 | r2 u32 LE | 随机数,port2 加密种子 |
| 0x2A-0x2D | 00 00 00 00 | 常量 |
| 0x2E-0x2F | 01 00 | 输出口对序号(SL 抓包=01;实测 01/02 均被接受) |
> ⚠ **两个 DMX 口数据区独立**:port1 @0x3C、port2 @0x23C。灯具接在哪个口就吃哪个口的数据。
> 实测灯具在 port2 时只发 port1 会导致灯具保持旧值/兜底白色——**show 模式已改为双口同时输出**。
| 0x30-0x31 | 00 00 | 常量 |
| 0x32-0x33 | 01 00 | 常量 |
| 0x34-0x35 | 00 02 | port1 数据长度 = 512(u16 LE) |
| 0x36-0x37 | 00 02 | port2 数据长度 = 512(u16 LE) |
| 0x38-0x3B | 01 00 00 00 | 常量 |
| 0x3C-0x23B | port1 密文 512B | `agios_dmx_encrypt(p1, r1, B12)` |
| 0x23C-0x43B | port2 密文 512B | `agios_dmx_encrypt(p2, r2, B12)` |
**加密**:`rot=(B12^r)&511; out[i]=src[(i+rot)%512]^cnt[i]`(zPprv0TXRRW 序列)
---
## 三、RDM 请求帧(1084B,主机→Butler,全破解)
| 偏移 | 含义 | 备注 |
|---|---|---|
| 0x00-0x19 | 通用信封 | type=4, 子类型=01 |
| 0x1A-0x1F | **全 0** | RDM 帧计数器恒 0(424 帧实证) |
| 0x20 | **02** | RDM 请求标志(DMX 帧为 01) |
| 0x21 | 00 | 常量 |
| 0x22-0x23 | qseq u16 LE | 查询序号,逐请求 +1 |
| 0x24-0x29 | 全 0 | DMX 帧的 r1/r2 槽位,RDM 清零 |
| 0x2A | **CC** | 常量(隐式 RDM StartCode 的外显位置) |
| 0x2B-0x2D | 00 00 00 | 常量 |
| 0x2E-0x2F | 01 00 | 常量 |
| 0x30 | 00 / 01 | 成对发送标志:主帧=00,紧跟的副本=01 |
| 0x31-0x33 | 00 00 00 | 常量 |
| 0x34-0x37 | msgLen+1 u32 LE | 如 PDL=0 时 = 25 (0x19) |
| 0x38-0x3B | 01 00 00 00 | 常量 |
| 0x3C | 01 | RDM SubStartCode |
| 0x3D | msgLen | = 24+PDL(含隐式 0xCC 起始码,不含 checksum) |
| 0x3E-0x43 | 目标 UID | 广播=ff×6 |
| 0x44-0x49 | 源 UID | 主机=45 3a 7f ff ff ff(副本末字节 -1) |
| 0x4A | TN | 事务号 |
| 0x4B | port | 主帧=00,副本=01 |
| 0x4C | msgCount | 00 |
| 0x4D-0x4E | subDevice | 00 00 |
| 0x4F | CC | 命令类(0x10=Discovery, 0x20=GET, 0x30=SET) |
| 0x50-0x51 | PID | BE(0001=UNIQUE_BRANCH, 0002=MUTE, 0003=UN_MUTE, 0060=DEVICE_INFO, 00F0=DMX_START_ADDRESS…) |
| 0x52 | PDL | PD 长度 |
| 0x53.. | PD | 参数数据 |
| 0x53+PDL | checksum u16 BE | `0xCC + Σ(0x3C .. 0x53+PDL)` |
| checksum 之后 | 任意填充 | Butler 不读(实测垃圾/零均可) |
**成对副本规则**(SL 实测):主帧之后紧跟一副本,差异仅 0x30=01、0x4B=01、srcUID 末字节 -1,**checksum 不重算**。
---
## 四、RDM 应答帧(Butler→主机,已可解码)
- 信封子类型=02,0x1A-0x1D = `"Butl"` 标记。
- 0x28-0x29 = RDM 数据区长度 u16 LE;数据区 @0x2A。
- 数据区首字节 0xAA = DISC 脉冲(0xAA/0x55 位交织编码,checksum 在 17B 内);否则为标准 RDM 应答(CC@0, CC字段@0x14, PID@0x15-0x16, PDL@0x17, PD@0x18)。
- DEVICE_INFO PD 布局(19B):protocolVer(2) model(2) category(2) swVer(4) **footprint(2)@+10** personality(2)@+12 **DMX地址(2)@+14** 保留(2) **传感器数@+18**。
---
## 五、注册应答 47B(Butler→主机,线上 89B,全破解)
| 偏移 | 含义 |
|---|---|
| 0x00-0x19 | 通用信封:type=2, 子类型=02, len@0x14=47 (0x2f) |
| 0x1A-0x1D | 设备 IP(LE 逆序字节)= 192.168.123.1 |
| 0x1E-0x23 | 设备 MAC = 00 16 1c f1 2e 39 |
| 0x24 | 00 |
| 0x25-0x28 | 主机 IP(LE 逆序)= 192.168.123.113 |
| 0x29-0x2E | 主机 MAC = 30 0e d5 59 bb 36 |
## 六、42B 事件帧(Butler→主机,结构破解)
type=4, 子类型=02, len=42 (0x2a), `"Butl"`@0x1A:
- 0x1E-0x1F = 01 00;0x20-0x21 = 事件码 u16 LE(已见 0x0318=ack_84 / 0x0062=status_84),0x22-0x23 = 00 00;0x24-0x27 = 01 00 00 00;0x28-0x29 = 00 00。
- 事件码具体语义未解码(不影响功能)。
## 六b、心跳 36B(主机→Butler,全破解)
type=2, 子类型=03, len@0x14=36 (0x24):
- 0x1A-0x1D = 主机 IP(LE 逆序)
- 0x1E-0x23 = 主机 MAC
- 周期 ~1-6s,会话保活。
## 六c、发现请求 180B(部分破解)
type=1, len=180 (0xb4):0x1A 起为主机名 ASCII(如 "tony"),其余字段未解码。
## 六d、发现应答 217B(Butler→主机,结构破解)
type=1, 子类型=0f, len=217 (0xd9):
- 0x1A-0x22 = 设备名 ASCII `"Butler S2"`
- 0x29-0x37 = 设备序列号/GUID 样字节(15B,常量)
- 0x3A-0x3C = 00 15 00(设备类型/能力?,与 AERE 同位置同值)
- 0x40-0x4E = 网络块:`01 | Butler IP(LE) | 掩码(00ffffff=255.255.255.0) | Butler MAC | 00`
- 0x59-0x5C = 1a 00 00 00 (26);0x5D-0x5F = 01 01 fa;0x60-0x63 = 0c 00 02 00
- 0xAC-0xC8 = Butler 配置块(与 AERE 共享,见七b)
- 两份抓包逐字节一致 → 除配置块慢变计数器外全静态。
## 六e、EUCE 广播 540B(主机→广播,结构破解)
magic `"EUCE"`(非 ENet 信封!),0x0C = 05 00 00 00(版本5?):
- 0x11C 起 = ASCII 串:`<用户名><计算机名>-<机器GUID>`(如 `tonycaoDESKTOP-PED2LVD-f7f4a043-17ec-4156-812b-909bdc99f2ff`)
- 0x216-0x21B = 主机 MAC
- 其余全 0。即 e:cue 主机身份广播,暖会话下非必需。
## 六f、1084B RDM 上行帧(Butler→主机,type=4,结构识别)
len=1084, 子类型=01, 0x20=02, 0x2A=CC(与 RDM 请求同布局),0x3C 起为标准 RDM 应答消息
(SubSC=01, msgLen@0x3D, dest=ff×6, src=灯具 UID…)。即 Butler 以 1084 信封转发 RDM 应答的另一形态
(与 69B 小帧 rdm_resp 并存)。
---
## 七、AECH 认领质询帧 552B(主机→Butler,功能全破解)
type=2, 子类型=05, len=552 (0x228):
| 偏移 | 含义 | 备注 |
|---|---|---|
| 0x1A-0x1D | 主机 IP(LE 逆序) | |
| 0x1E-0x23 | 主机 MAC | |
| 0x24-0x27 | 00 00 00 00 | |
| 0x28-0x2B | `"AECH"` | |
| 0x2C-0x87 | **不透明区 A(92B)** | 每轮高熵不同;XOR 无周期 → 真随机或密文 |
| 0x88-0x8B | nonce u32 LE | 主机自选,每轮不同 |
| 0x8C-0x8F | B12 u32 LE | 主机自选,会话内恒定(DMX 混淆 rot 基值) |
| 0x90-0x123 | **不透明区 B(148B)** | 同 A |
| 0x124-0x227 | 全 0 填充 | |
**★消融实证(CLAIM_ZERO=1)**:不透明区 A+B 全清零后认领照样 AERE PASS →
**Butler 只读 nonce/B12/头部,240B 不透明区不校验**(疑为 SL 软件侧证书/许可 blob,与 Butler 无关)。
→ 认领帧可零模板纯自构造(不透明区填 0 即可)。
## 七b、AERE 认领应答 552B(Butler→主机,结构破解)
type=2, 子类型=06, len=552,同会话多轮/跨时段 diff 仅 0xC0-0xC3 + 配置块计数器变化:
| 偏移 | 含义 |
|---|---|
| 0x1A-0x1D | Butler IP(LE 逆序)= 192.168.123.1 |
| 0x1E-0x23 | Butler MAC = 00 16 1c f1 2e 39 |
| 0x24-0x27 | 00 71 7b a8(主机 IP 前 3 字节回显?待跨主机验证) |
| 0x28-0x2B | `"AERE"` |
| 0x2C-0x2E | 59 bb 36 = 主机 MAC 低 3 字节(回显?) |
| 0x2F-0x32 | 7f ff ff fd/ff = 端口/状态位掩码(跨时段微变) |
| 0x33-0x38 | **灯具 UID**(首个应答者;跨时段见 010734880900 / 010734670125) |
| 0x39-0x3C | 00 15 00 0a(与发现应答同位置同值) |
| 0x40-0x4E | 网络块:01 \| Butler IP \| 掩码 \| Butler MAC \| 00 |
| 0x59-0x63 | 1a 00 00 00 01 01 fa 0c 00 02 00 |
| 0xAC-0xC8 | Butler 配置块:`00 <ctr> 01 00 00 00 / 00 <ctr> 00 00 00 <ctr> 00 00 00 1c 00 1c 00 1d 00 1d / 00 1c 00 1c 00 f0 0a f0 0a`(ctr=慢变计数器 0x23→0x24;0x1c=28/0x1d=29 疑固件版本;0x0af0=2800 ×2) |
| 0xC0-0xC3 | **resp = G(nonce)**(认领验证值) |
| 0xC4-0xC8 | 00 f0 0a f0 0a |
| 其余 | 全 0 |
## 七c、announce 644B(主机→广播,结构破解)
type=1, 子类型=15, len=644 (0x284):
- 0x3A-0x3D = 1a 00 00 00 (26)
- 0x40-0x4E = 主机网络块:01 | 主机IP(LE) | 掩码 | 主机MAC | 00
- 0x162-0x175 = 会话 GUID 样随机 20B(announce_live 为全 0)
- 0x176-0x17A = 日期头:年 u16LE(0x07ea=2026) + 月 + 日(部分)
- 0x17D-0x1F7 = **UTF-16BE 字符串:SL show 文件路径**(`C:\Users\<user>\AppData\Local\ecue\SYMPHOLIGHT 5.5\CurrentShow`,announce_live 为全 0)
- 0x211-0x215 / 0x219-0x21C / 0x221-0x225 = 3 组 5B 会话随机
- 0x239-0x241 = **完整时间戳**:年 u16LE, 月, 日, 时, 分, 秒, 毫秒 u16LE(announce_live 解码得 22:52:50.992 与 SL 会话时间吻合 ✓)
- 0x25C-0x25D = 临时端口 u16LE(每会话不同:7200 / 2200)
- 0x268 / 0x274 / 0x27C / 0x280 = 4 个使能 flag(01 00 00 00)
---
## 八、历史误区更正(2026-09-11)
1. ~~"RDM 请求消息起始位置可变 (UN_MUTE@0x3A vs BRANCH@0x3C)"~~ → **错误**。424 帧实证全部 SubSC@0x3C,结构固定。
2. ~~"Butler 帧级校验会话一致性,verbatim 才能过"~~ → **错误**。纯自构造帧全通。之前失败根因:
- `sl_get_exact.hex` 是 DMX 数据帧被误当 RDM 模板;
- 旧 `build_rdm_frame` checksum 求和范围多算 1 字节(含模板残留的旧 checksum 槽位)。
3. ~~"0x2E 是节点序号且必须为 2"~~ → 实测 SL 原帧恒为 01,设 02 也被接受,Butler 不校验此字段。
---
## 九、遗留未解码项(不阻塞功能)
| 项 | 状态 |
|---|---|
| 1947 端口 40B 许可令牌 | 固定 ASCII 常量 `BlZRQphAGxUbi7at8KEfmdZ7TdSB03b+qoC1RIA`,SL 每 ~34s 广播 21 帧双目标;语义未知(常量本身排除与狗绑定);暖会话**非必需**;冷启动必需性未验证 |
| 42B 事件码 0x0318(ack)/0x0062(status) 语义 | 结构已解,语义未解码 |
| bind52 flag ffff/0000 语义 | 结构已解(主机+设备 IP/MAC, 0x64/0x01f4 疑速率参数);flag 疑绑定/解绑;暖会话**非必需** |
| AERE 0x24-0x27 / 0x2C-0x2E 回显字段 | 疑主机 IP/MAC 部分回显,需跨主机抓包证实 |
| AERE/发现应答配置块细节(0x1c/0x1d/0x0af0) | 疑固件版本/端口配置,语义未坐实 |
| claim 不透明区 240B 真实内容 | Butler 不校验(已消融证实);疑 SL 软件侧证书/许可 blob,与协议功能无关 |
| Butler 冷启动是否必须完整前奏(45s 广播+bind+ident+EUCE+1947) | 未验证(最有价值的遗留实验:Butler 断电重启后跑 show) |
| NFM680S 通道数漂移(曾见 2ch/1ch) | 近期稳定 1ch,疑早期解析误判;未复现深究 |
| announce 时间戳/会话 ID 是否参与校验 | 按实测填充新鲜值即可;严格性未消融 |
**已证伪的旧猜测**(曾列入待办,本轮排除):
- ~~RDM 变长起始~~ → 固定 0x3C
- ~~会话级帧校验~~ → 不存在
- ~~0x2E 严格校验~~ → 不校验(1/2 均接受)
- ~~claim 帧 240B 高熵区被 Butler 校验~~ → 清零照样 PASS
---
## 十、纯自构造实现
`src/hostdemo.rs`:
- `build_dmx_frame()` — 零模板 DMX 帧
- `build_rdm_frame2()` — 零模板 RDM 请求帧
- `rdm_pair_copy()` — 成对副本
`HOSTDEMO_MODE=show` 已切换到纯自构造,**运行不再依赖 replay 文件或任何抓包模板**(除 announce144/claim 等静态注册帧,这些也可后续自构造化)。

266
src/analyze.rs Normal file
View File

@ -0,0 +1,266 @@
// analyze.rs — 离线抓包/模板分析工具
// 子命令:
// analyze dump <hexfile> <start> <end> 按偏移 dump 十六进制
// analyze diff <file1> <file2> 逐字节对比两个 hex 文件
// analyze hdr replay DMX/RDM 帧头字段方差分析
// analyze rdm replay 中 RDM 请求帧结构分析 (变长前置区)
// analyze ctr DMX 帧计数器序列分析
pub fn load_hex(path: &str) -> Vec<u8> {
let s = std::fs::read_to_string(path).unwrap_or_else(|_| panic!("无法读取 {}", path));
let s: String = s.chars().filter(|c| c.is_ascii_hexdigit()).collect();
(0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect()
}
pub fn load_replay(path: &str) -> Vec<Vec<u8>> {
let data = std::fs::read_to_string(path).unwrap_or_else(|_| panic!("无法读取 {}", path));
let mut out = Vec::new();
for line in data.lines() {
let p: Vec<&str> = line.split('|').collect();
if p.len() < 4 { continue; }
let hex = p[3].trim();
if hex.len() % 2 != 0 { continue; }
let mut b = Vec::with_capacity(hex.len() / 2);
let mut ok = true;
for i in (0..hex.len()).step_by(2) {
match u8::from_str_radix(&hex[i..i + 2], 16) {
Ok(v) => b.push(v),
Err(_) => { ok = false; break; }
}
}
if ok { out.push(b); }
}
out
}
fn hx(b: &[u8]) -> String {
b.iter().map(|x| format!("{:02x}", x)).collect::<Vec<_>>().join(" ")
}
fn default_replay() -> String {
std::env::var("REPLAY_FILE")
.unwrap_or_else(|_| "C:/Users/tonycao/AppData/Local/Temp/opencode/replay_frames.txt".into())
}
fn dump(args: &[String]) {
let b = load_hex(&args[0]);
let start: usize = args[1].trim_start_matches("0x").parse().unwrap_or_else(|_| usize::from_str_radix(args[1].trim_start_matches("0x"), 16).unwrap());
let end: usize = args.get(2).map(|s| s.trim_start_matches("0x").parse().unwrap_or_else(|_| usize::from_str_radix(s.trim_start_matches("0x"), 16).unwrap())).unwrap_or(b.len());
println!("{} len={} bytes", args[0], b.len());
let mut i = start;
while i < end.min(b.len()) {
let e = (i + 16).min(end).min(b.len());
println!(" 0x{:03x}: {}", i, hx(&b[i..e]));
i = e;
}
}
fn diff(args: &[String]) {
let a = load_hex(&args[0]);
let b = load_hex(&args[1]);
println!("A: {} ({} bytes)", args[0], a.len());
println!("B: {} ({} bytes)", args[1], b.len());
let n = a.len().min(b.len());
let diffs: Vec<usize> = (0..n).filter(|&i| a[i] != b[i]).collect();
println!("共同前缀 {} bytes 中差异 {} 处:", n, diffs.len());
for i in &diffs {
println!(" 0x{:03x}: A={:02x} B={:02x}", i, a[*i], b[*i]);
}
if a.len() != b.len() { println!("长度不同: A={} B={}", a.len(), b.len()); }
}
fn hdr() {
let frames = load_replay(&default_replay());
println!("replay 帧总数: {}", frames.len());
// 分类
let dmx: Vec<&Vec<u8>> = frames.iter()
.filter(|f| f.len() == 1084 && f[0x18] == 1 && f[0x20] == 1 && f[0x21] == 1)
.collect();
let rdm: Vec<&Vec<u8>> = frames.iter()
.filter(|f| f.len() == 1084 && f[0x18] == 1 && f[0x20] == 2)
.collect();
println!("DMX 数据帧: {}, RDM 请求帧(0x3C==01): {}", dmx.len(), rdm.len());
if let Some(f0) = dmx.first() {
println!("\n=== DMX 帧头 (帧#1) ===");
for r in (0..0x3C).step_by(16) {
println!(" 0x{:03x}: {}", r, hx(&f0[r..(r + 16).min(f0.len())]));
}
// 每偏移方差
println!("\n=== DMX 帧头逐偏移方差 (0x00-0x3B) ===");
for i in 0..0x3C {
let mut vals: Vec<u8> = dmx.iter().map(|f| f[i]).collect();
vals.sort();
vals.dedup();
if vals.len() == 1 {
println!(" 0x{:03x}: 常量 {:02x}", i, vals[0]);
} else if vals.len() <= 4 {
println!(" 0x{:03x}: {}种 {:?}", i, vals.len(), vals);
} else {
let min = vals.first().unwrap();
let max = vals.last().unwrap();
println!(" 0x{:03x}: 变量 {}种 [{:02x}..{:02x}]", i, vals.len(), min, max);
}
}
}
if let Some(f0) = rdm.first() {
println!("\n=== RDM 请求帧头+消息区 (帧#1) ===");
for r in (0..0x70).step_by(16) {
println!(" 0x{:03x}: {}", r, hx(&f0[r..(r + 16).min(f0.len())]));
}
println!("\n=== RDM 帧头逐偏移方差 (0x00-0x3B) ===");
for i in 0..0x3C {
let mut vals: Vec<u8> = rdm.iter().map(|f| f[i]).collect();
vals.sort();
vals.dedup();
if vals.len() == 1 {
println!(" 0x{:03x}: 常量 {:02x}", i, vals[0]);
} else if vals.len() <= 4 {
println!(" 0x{:03x}: {}种 {:?}", i, vals.len(), vals);
} else {
let min = vals.first().unwrap();
let max = vals.last().unwrap();
println!(" 0x{:03x}: 变量 {}种 [{:02x}..{:02x}]", i, vals.len(), min, max);
}
}
}
}
fn rdm() {
let frames = load_replay(&default_replay());
let rdm: Vec<&Vec<u8>> = frames.iter()
.filter(|f| f.len() == 1084 && f[0x18] == 1 && f[0x20] == 2)
.collect();
println!("RDM 请求帧(0x20==02): {}", rdm.len());
// 按 PID 分组 (假设 RDM @0x3C: CC@0x4F, PID@0x50-51)
let mut by_pid: std::collections::HashMap<u16, Vec<&Vec<u8>>> = std::collections::HashMap::new();
for f in &rdm {
let pid = ((f[0x50] as u16) << 8) | f[0x51] as u16;
by_pid.entry(pid).or_default().push(f);
}
let mut pids: Vec<u16> = by_pid.keys().cloned().collect();
pids.sort();
for pid in &pids {
let g = &by_pid[pid];
println!("\n=== PID=0x{:04x} ({} 帧) ===", pid, g.len());
for (k, f) in g.iter().take(2).enumerate() {
println!(" 样本{}:", k + 1);
for r in (0x2A..0x70).step_by(16) {
println!(" 0x{:03x}: {}", r, hx(&f[r..(r + 16).min(f.len())]));
}
}
// 消息长度字段候选: 0x3D (msgLen), 0x34-0x38
let ml: Vec<u8> = g.iter().map(|f| f[0x3D]).collect();
let mut u: Vec<u8> = ml.clone();
u.sort(); u.dedup();
println!(" 0x3D(msgLen)取值: {:?}", u);
}
// 检查是否存在 RDM 起始不在 0x3C 的帧 (其它 0x20 取值)
let mut by_20: std::collections::HashMap<u8, usize> = std::collections::HashMap::new();
for f in &frames {
if f.len() == 1084 && f[0x18] == 1 {
*by_20.entry(f[0x20]).or_insert(0) += 1;
}
}
println!("\n0x20 取值分布: {:?}", by_20);
}
fn ctr() {
let frames = load_replay(&default_replay());
let dmx: Vec<&Vec<u8>> = frames.iter()
.filter(|f| f.len() == 1084 && f[0x18] == 1 && f[0x20] == 1 && f[0x21] == 1)
.collect();
println!("DMX 数据帧: {}", dmx.len());
let ctrs: Vec<u32> = dmx.iter()
.map(|f| u32::from_le_bytes([f[0x1A], f[0x1B], f[0x1C], f[0x1D]]))
.collect();
println!("首个计数器: {}, 末个: {}", ctrs.first().unwrap_or(&0), ctrs.last().unwrap_or(&0));
// 增量分布
let mut deltas: std::collections::HashMap<u32, u32> = std::collections::HashMap::new();
for w in ctrs.windows(2) {
let d = w[1].wrapping_sub(w[0]);
*deltas.entry(d).or_insert(0) += 1;
}
let mut ds: Vec<(u32, u32)> = deltas.into_iter().collect();
ds.sort_by_key(|(_, n)| std::cmp::Reverse(*n));
println!("增量分布 (前10):");
for (d, n) in ds.iter().take(10) {
println!(" +{}: {} 次", d, n);
}
// RDM 帧的计数器
let rdm: Vec<&Vec<u8>> = frames.iter()
.filter(|f| f.len() == 1084 && f[0x18] == 1 && f[0x20] == 2)
.collect();
let rctrs: Vec<u32> = rdm.iter()
.map(|f| u32::from_le_bytes([f[0x1A], f[0x1B], f[0x1C], f[0x1D]]))
.collect();
println!("RDM 帧计数器范围: {:?} .. {:?} (共{}帧)",
rctrs.first(), rctrs.last(), rctrs.len());
}
pub fn run(args: &[String]) {
if args.is_empty() {
println!("analyze dump <hexfile> <start> [end] | diff <a> <b> | hdr | rdm | ctr");
return;
}
match args[0].as_str() {
"dump" => dump(&args[1..]),
"diff" => diff(&args[1..]),
"hdr" => hdr(),
"rdm" => rdm(),
"ctr" => ctr(),
// gendmx: 同输入生成 从零构造 vs dmx_a模板补丁 两版帧并逐字节 diff
"gendmx" => {
let p1 = [0x11u8; 512];
let p2 = [0u8; 512];
let r1: u32 = 0x15198c1a;
let r2: u32 = 0x1676d663;
let b12: u32 = 0x11223344;
let ctr: u32 = 0x197;
let gen = crate::hostdemo::build_dmx_frame(&p1, &p2, r1, r2, b12, ctr);
// 模板版: dmx_a + 同样的补丁 (复刻已验证工作版本)
let mut tpl = load_hex("samples/dmx_a_1126.hex");
tpl[0x1A..0x1E].copy_from_slice(&ctr.to_le_bytes());
tpl[0x2E] = 2; tpl[0x2F] = 0;
tpl[0x22..0x26].copy_from_slice(&r1.to_le_bytes());
tpl[0x26..0x2A].copy_from_slice(&r2.to_le_bytes());
let enc1 = crate::aech::agios_dmx_encrypt(&p1, r1, b12);
let enc2 = crate::aech::agios_dmx_encrypt(&p2, r2, b12);
tpl[0x3C..0x23C].copy_from_slice(&enc1);
tpl[0x23C..0x43C].copy_from_slice(&enc2);
println!("gen len={} tpl len={}", gen.len(), tpl.len());
let n = gen.len().min(tpl.len());
let diffs: Vec<usize> = (0..n).filter(|&i| gen[i] != tpl[i]).collect();
println!("差异 {} 处:", diffs.len());
for i in &diffs {
println!(" 0x{:03x}: gen={:02x} tpl={:02x}", i, gen[*i], tpl[*i]);
}
if diffs.is_empty() { println!("两版逐字节一致"); }
}
_ if args[0] == "xor" => {
// analyze xor <a> <b> [start] [end] — 逐字节 XOR + 周期性分析
let a = load_hex(&args[1]);
let b = load_hex(&args[2]);
let pf = |s: &String| usize::from_str_radix(s.trim_start_matches("0x"), 16).unwrap();
let start = args.get(3).map(|s| pf(s)).unwrap_or(0);
let end = args.get(4).map(|s| pf(s)).unwrap_or(usize::MAX);
let n = a.len().min(b.len()).min(end);
let x: Vec<u8> = (start..n).map(|i| a[i] ^ b[i]).collect();
println!("XOR {} ^ {} [0x{:x}..0x{:x}):", args[1], args[2], start, n);
let mut r = 0;
while r < x.len() {
let e = (r + 16).min(x.len());
println!(" 0x{:03x}: {}", start + r, hx(&x[r..e]));
r = e;
}
println!("周期性 (x[i]==x[i+p] 比例, 随机基线~0.4%):");
for p in [1usize, 2, 4, 8, 16, 20, 24, 28, 32, 40, 44, 48, 64] {
if x.len() <= p { continue; }
let m = (0..x.len() - p).filter(|&i| x[i] == x[i + p]).count();
println!(" p={}: {}/{} ({:.1}%)", p, m, x.len() - p, 100.0 * m as f64 / (x.len() - p) as f64);
}
}
_ => println!("未知 analyze 子命令 {}", args[0]),
}
}

View File

@ -1,4 +1,4 @@
//! hostdemo.rs — 主机侧等效示例程序 (2026-09-10): 认领真机 + RDM 发现 + 加密 DMX 控制 //! hostdemo.rs — 主机侧等效示例程序 (2026-09-10): 认领真机 + RDM 发现 + 加密 DMX 控制
//! //!
//! 全流程不依赖 SymphoLight: //! 全流程不依赖 SymphoLight:
//! 1. 发现 (type=1, 180B) → 217B 应答 //! 1. 发现 (type=1, 180B) → 217B 应答
@ -102,6 +102,77 @@ fn build_rdm_frame(dest: &[u8; 6], tn: u8, qseq: u16, cc: u8, pid: u16, pd: &[u8
f f
} }
/// ★从零构造 DMX 数据帧 (1084B, 全字段已破解 — analyze hdr 实证):
/// 帧头全部常量已知, 仅 0x1A 计数器(+1/帧) 与 0x22/0x26 r1/r2(加密随机) 变化
pub fn build_dmx_frame(p1: &[u8; 512], p2: &[u8; 512], r1: u32, r2: u32, b12: u32, ctr: u32) -> Vec<u8> {
let mut f = vec![0u8; 1084];
f[0..4].copy_from_slice(b"ENet");
f[4] = 4; // type=4
f[0x0A] = 0x1c; f[0x0B] = 0x7c; // 目标端口 31772 (LE)
f[0x12] = 1; // 常量 01 00
f[0x14] = 0x3c; f[0x15] = 0x04; // 帧长 1084 (u16 LE)
f[0x18] = 1; // 常量 01 00
f[0x1A..0x1E].copy_from_slice(&ctr.to_le_bytes()); // 帧计数器 (+1/帧, 任意起始)
f[0x20] = 1; f[0x21] = 1; // 端口使能 mask: port1+port2
f[0x22..0x26].copy_from_slice(&r1.to_le_bytes());
f[0x26..0x2A].copy_from_slice(&r2.to_le_bytes());
f[0x2E] = 2; // 输出口对序号 (SL 抓包=01, 本机实测 01/02 均可; 关键在数据区按端口分开)
f[0x32] = 1; // 常量
f[0x35] = 2; f[0x37] = 2; // port1/port2 数据长度 = 512 (u16 LE ×2)
f[0x38] = 1; // 常量 01 00 00 00
f[0x3C..0x23C].copy_from_slice(&agios_dmx_encrypt(p1, r1, b12));
f[0x23C..0x43C].copy_from_slice(&agios_dmx_encrypt(p2, r2, b12));
f
}
/// ★从零构造 RDM 请求帧 (1084B, 全字段已破解 — analyze rdm 实证):
/// 计数器恒 0; 0x20=02 为 RDM 请求标志; 0x2A=0xCC 常量; checksum=0xCC+Σ(0x3C..0x53+PDL) @0x53+PDL
/// msgLen=24+PDL (含隐式 0xCC 起始码); checksum 后任意填充 (Butler 不读)
fn build_rdm_frame2(dest: &[u8; 6], tn: u8, qseq: u16, cc: u8, pid: u16, pd: &[u8]) -> Vec<u8> {
let msg_len = 24 + pd.len();
let mut f = vec![0u8; 1084];
f[0..4].copy_from_slice(b"ENet");
f[4] = 4;
f[0x0A] = 0x1c; f[0x0B] = 0x7c;
f[0x12] = 1;
f[0x14] = 0x3c; f[0x15] = 0x04;
f[0x18] = 1;
// 0x1A-0x1F: 计数器恒 0
f[0x20] = 2; // RDM 请求标志
f[0x22..0x24].copy_from_slice(&qseq.to_le_bytes());
f[0x2A] = 0xCC; // 常量
f[0x2E] = 1;
// 0x30: 00 主帧 (成对副本=01)
f[0x34..0x38].copy_from_slice(&((msg_len + 1) as u32).to_le_bytes());
f[0x38] = 1;
// RDM 消息 @0x3C: SubSC/msgLen/dest/src/TN/port/msgCount/subDev/CC/PID/PDL/PD
f[0x3C] = 1;
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[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..0x53 + pd.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
}
/// RDM 成对副本 (SL 实测: flag@0x30=01, port@0x4B=01, srcUID 末字节-1, checksum 不重算)
fn rdm_pair_copy(f: &[u8]) -> Vec<u8> {
let mut f2 = f.to_vec();
f2[0x30] = 0x01;
f2[0x4B] = 0x01;
f2[0x49] = f2[0x49].wrapping_sub(1);
f2
}
/// DISC 脉冲解码 (§5.4 逆运算): data = RDM 数据区 (0x2A 起), 返回 Some(UID) 或 None(冲突/非法) /// DISC 脉冲解码 (§5.4 逆运算): data = RDM 数据区 (0x2A 起), 返回 Some(UID) 或 None(冲突/非法)
fn decode_pulse(d: &[u8]) -> Option<[u8; 6]> { fn decode_pulse(d: &[u8]) -> Option<[u8; 6]> {
if d.len() < 17 || d[0] != 0xAA { return None; } if d.len() < 17 || d[0] != 0xAA { return None; }
@ -369,44 +440,65 @@ pub fn run(target: &str, bind_ip: &str, secs: u64) {
return; return;
} }
// hybrid: 认领 + verbatim SL DMX 帧 + 我们构造的 RDM 帧 — 二分定位毒帧 // hybrid / show: 认领 + 用 SL 实帧骨架, 替换为我们的 DMX 数据 + 我们构造的 RDM
if std::env::var("HOSTDEMO_MODE").unwrap_or_default() == "hybrid" { // 区别: hybrid 保留原 SL 数据地址(101/123/1 可选替换); show 固定地址1-32炫彩呼吸
let path = std::env::var("REPLAY_FILE") let mode = std::env::var("HOSTDEMO_MODE").unwrap_or_default();
.unwrap_or_else(|_| "C:/Users/tonycao/AppData/Local/Temp/opencode/replay_frames.txt".into()); if mode == "hybrid" || mode == "show" {
let data = std::fs::read_to_string(&path).expect("replay file"); let is_show = mode == "show";
let mut dmxf: Vec<Vec<u8>> = Vec::new(); // verbatim DMX 帧 let label = if is_show { "SHOW" } else { "H" };
// show: 全字段已破解, 零模板纯自构造; hybrid: 仍用 SL 实帧骨架做对照
let mut dmxf: Vec<Vec<u8>> = Vec::new();
let mut base_rdm: Option<Vec<u8>> = None; let mut base_rdm: Option<Vec<u8>> = None;
for line in data.lines() { if !is_show {
let p: Vec<&str> = line.split('|').collect(); let path = std::env::var("REPLAY_FILE")
if p.len() < 4 { continue; } .unwrap_or_else(|_| "C:/Users/tonycao/AppData/Local/Temp/opencode/replay_frames.txt".into());
let hex = p[3]; let data = std::fs::read_to_string(&path).expect("replay file");
if hex.len() != 1084 * 2 { continue; } for line in data.lines() {
let b = |o: usize| u8::from_str_radix(&hex[o * 2..o * 2 + 2], 16).unwrap(); let p: Vec<&str> = line.split('|').collect();
if b(0x18) == 1 && b(0x20) == 1 && b(0x21) == 1 && b(0x3c) != 1 { if p.len() < 4 { continue; }
let mut fb = Vec::new(); let hex = p[3];
for i in (0..hex.len()).step_by(2) { fb.push(u8::from_str_radix(&hex[i..i + 2], 16).unwrap()); } if hex.len() != 1084 * 2 { continue; }
dmxf.push(fb); 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 {
if base_rdm.is_none() && b(0x3c) == 1 && b(0x30) == 0 && b(0x50) == 0 && b(0x51) == 1 { let mut fb = Vec::new();
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()); }
for i in (0..hex.len()).step_by(2) { fb.push(u8::from_str_radix(&hex[i..i + 2], 16).unwrap()); } dmxf.push(fb);
base_rdm = Some(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) // 认领 (announce144 → AECH; claim 帧纯自构造: 不透明区填 0, CLAIM_ZERO 消融已证实 Butler 不校验)
let o2: Vec<u8> = bind_ip.split('.').map(|x| x.parse().unwrap()).collect(); let o2: Vec<u8> = bind_ip.split('.').map(|x| x.parse().unwrap()).collect();
sock.send_to(&load_template("announce144"), &dst).unwrap(); sock.send_to(&load_template("announce144"), &dst).unwrap();
std::thread::sleep(Duration::from_millis(300)); std::thread::sleep(Duration::from_millis(300));
let nonce = rng.next(); let nonce = rng.next();
let bc = rng.next(); let bc = rng.next();
let mut ch = load_template("claim_h112"); let o2a = [o2[0], o2[1], o2[2], o2[3]];
ch[0x1A..0x1E].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]); let ch = build_claim_frame(o2a, nonce, bc);
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(); sock.send_to(&ch, &dst).unwrap();
std::thread::sleep(Duration::from_millis(400)); // ★验证 AERE 应答 (RDM 不加密, 认领失败 RDM 照样通但 DMX 解密失败 → 灯白色兜底)
println!("[H] 认领完成 B12=0x{:08x}; verbatim DMX {} 帧 + 我们的 RDM", bc, dmxf.len()); let mut aere_ok = false;
let rdm_t = base_rdm.expect("no RDM template"); let w = Instant::now();
while w.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(|x| x == b"AERE") {
let resp = g32(b, 0xC0);
let want = agios_g(nonce);
aere_ok = resp == want;
println!("[{}] AERE resp=0x{:08x} want=0x{:08x} {}", label, resp, want, if aere_ok { "★PASS" } else { "FAIL" });
break;
}
}
}
if !aere_ok { println!("[{}] ⚠ 未收到有效 AERE — 认领可能失败, DMX 将无法解密!", label); }
println!("[{}] 认领完成 B12=0x{:08x}; {}", label, bc,
if is_show { "地址1-32 炫彩呼吸 (零模板纯自构造 DMX+RDM)" } else { "verbatim DMX + 模板 RDM" });
let rdm_t: Option<Vec<u8>> = base_rdm;
let known: [[u8; 6]; 3] = [ let known: [[u8; 6]; 3] = [
[0x01, 0x07, 0x34, 0x67, 0x01, 0x25], [0x01, 0x07, 0x34, 0x67, 0x01, 0x25],
[0x01, 0x07, 0x34, 0x88, 0x09, 0x00], [0x01, 0x07, 0x34, 0x88, 0x09, 0x00],
@ -414,16 +506,19 @@ pub fn run(target: &str, bind_ip: &str, secs: u64) {
]; ];
let start = Instant::now(); let start = Instant::now();
let mut di = 0usize; let mut di = 0usize;
let mut qseq: u16 = 0;
let mut last_rdm = Instant::now() - Duration::from_secs(10); let mut last_rdm = Instant::now() - Duration::from_secs(10);
let mut step = 0usize; let mut step = 0usize;
let mut addr_list: Vec<u16> = Vec::new(); let mut addr_list: Vec<u16> = Vec::new();
let mut resp_n = 0u64; 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 成功) // ★消融实证: 周期宣告/发现广播是 RDM 代理开关 (nobcast 失败, nodmx 成功)
let disc = load_template("discovery_req"); let disc = load_template("discovery_req");
let mut ann = load_template("announce"); let mut ann = load_template("announce");
ann[0x41..0x45].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]); ann[0x41..0x45].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]);
let mut last_bcast = Instant::now() - Duration::from_secs(10); let mut last_bcast = Instant::now() - Duration::from_secs(10);
while start.elapsed() < Duration::from_secs(secs.max(90)) { while secs == 0 || start.elapsed() < Duration::from_secs(secs) {
let t = start.elapsed().as_secs_f32(); let t = start.elapsed().as_secs_f32();
if last_bcast.elapsed() >= Duration::from_millis(2500) { if last_bcast.elapsed() >= Duration::from_millis(2500) {
let _ = sock.send_to(&disc, "255.255.255.255:31772"); let _ = sock.send_to(&disc, "255.255.255.255:31772");
@ -431,28 +526,49 @@ pub fn run(target: &str, bind_ip: &str, secs: u64) {
last_bcast = Instant::now(); last_bcast = Instant::now();
} }
// verbatim DMX 循环 // verbatim DMX 循环
// ★纯自构造 DMX 帧 (认领 B12 加密呼吸图案) — 彻底摆脱 SL 原帧依赖 // DMX 数据替换
let mut p1 = [0u8; 512]; let mut p1 = [0u8; 512];
for (k, &a) in [101u16, 123, 1].iter().enumerate() { if std::env::var("SHOW_DIAG").unwrap_or_default() == "1" {
let phase = t * 1.0 + k as f32 * 2.1; // 诊断: ch1-32 全灭3s / 全亮3s 交替 — 验证 DMX 是否到达灯具
let v = (((phase.sin() * 0.5 + 0.5) * 220.0) * (t * 0.3).min(1.0)) as u8; let v = if (t as u32 / 3) % 2 == 0 { 0u8 } else { 255u8 };
p1[(a - 1) as usize] = v; for i in 0..32 { p1[i] = v; }
} else 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 rr1 = rng.next();
let rr2 = rng.next(); let rr2 = rng.next();
let mut f = dmxf[di % dmxf.len()].clone(); // 帧骨架仍取 SL 帧(含未破解字段), 数据/随机全替换 // show 模式: 图案同时输出到 port1+port2 (灯具可能在任一口, 实测 port2 出灯)
f[0x1A..0x1E].copy_from_slice(&(di as u32).to_le_bytes()); let mut p2 = [0u8; 512];
f[0x22..0x26].copy_from_slice(&rr1.to_le_bytes()); if is_show || std::env::var("SHOW_PORT2").unwrap_or_default() == "1" { p2 = p1; }
f[0x26..0x2A].copy_from_slice(&rr2.to_le_bytes()); let f = if is_show {
f[0x3C..0x23C].copy_from_slice(&agios_dmx_encrypt(&p1, rr1, bc)); // ★纯自构造 DMX 帧 (零模板)
f[0x23C..0x43C].copy_from_slice(&agios_dmx_encrypt(&[0u8; 512], rr2, bc)); build_dmx_frame(&p1, &p2, rr1, rr2, bc, di as u32)
} else {
let mut fx = dmxf[di % dmxf.len()].clone();
fx[0x1A..0x1E].copy_from_slice(&(di as u32).to_le_bytes());
fx[0x22..0x26].copy_from_slice(&rr1.to_le_bytes());
fx[0x26..0x2A].copy_from_slice(&rr2.to_le_bytes());
fx[0x3C..0x23C].copy_from_slice(&agios_dmx_encrypt(&p1, rr1, bc));
fx[0x23C..0x43C].copy_from_slice(&agios_dmx_encrypt(&[0u8; 512], rr2, bc));
fx
};
di += 1; di += 1;
let _ = sock.send_to(&f, &dst); let _ = sock.send_to(&f, &dst);
if di % 30 == 0 { let _ = sock.send_to(&load_template("heartbeat"), &dst); } if di % 30 == 0 { let _ = sock.send_to(&load_template("heartbeat"), &dst); }
// RDM 任务 // RDM 任务
if t > 2.0 && last_rdm.elapsed() >= Duration::from_millis(700) { if t > 2.0 && last_rdm.elapsed() >= Duration::from_millis(700) {
last_rdm = Instant::now(); last_rdm = Instant::now();
let mut f = rdm_t.clone();
let mut pd: Vec<u8> = Vec::new(); let mut pd: Vec<u8> = Vec::new();
let mut pid: u16 = 0x0001; let mut pid: u16 = 0x0001;
let mut dest = [0xffu8; 6]; let mut dest = [0xffu8; 6];
@ -469,27 +585,38 @@ pub fn run(target: &str, bind_ip: &str, secs: u64) {
else { step = 3; } else { step = 3; }
} }
} }
let msg_len = 24 + pd.len(); let tn = (step * 7 + 1) as u8;
f[0x1A..0x1E].copy_from_slice(&(di as u32).to_le_bytes()); let (f, f2) = if is_show {
f[0x3D] = msg_len as u8; // ★纯自构造 RDM 帧 (零模板) + 成对副本
f[0x3E..0x44].copy_from_slice(&dest); qseq = qseq.wrapping_add(1);
f[0x4A] = (step * 7 + 1) as u8; let f = build_rdm_frame2(&dest, tn, qseq, cc, pid, &pd);
f[0x4F] = cc; let f2 = rdm_pair_copy(&f);
f[0x50] = (pid >> 8) as u8; (f, f2)
f[0x51] = (pid & 0xFF) as u8; } else {
f[0x52] = pd.len() as u8; let mut f = rdm_t.clone().unwrap();
f[0x53..0x53 + pd.len()].copy_from_slice(&pd); let msg_len = 24 + pd.len();
let mut sum: u16 = 0xCC; f[0x1A..0x1E].copy_from_slice(&(di as u32).to_le_bytes());
for i in 0x3C..0x3C + msg_len { sum = sum.wrapping_add(f[i] as u16); } f[0x3D] = msg_len as u8;
let cs = 0x53 + pd.len(); f[0x3E..0x44].copy_from_slice(&dest);
f[cs] = (sum >> 8) as u8; f[0x4A] = tn;
f[cs + 1] = (sum & 0xFF) as u8; f[0x4F] = cc;
let mut f2 = f.clone(); f[0x50] = (pid >> 8) as u8;
f2[0x30] = 0x01; f[0x51] = (pid & 0xFF) as u8;
f2[0x49] = f2[0x49].wrapping_sub(1); 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);
(f, f2)
};
let _ = sock.send_to(&f, &dst); let _ = sock.send_to(&f, &dst);
let _ = sock.send_to(&f2, &dst); let _ = sock.send_to(&f2, &dst);
println!("[H] RDM step={} pid={:04x}", step, pid); println!("[{}] RDM step={} pid={:04x}", label, step, pid);
step += 1; step += 1;
} }
while let Ok((n, src)) = sock.recv_from(&mut buf) { while let Ok((n, src)) = sock.recv_from(&mut buf) {
@ -497,22 +624,59 @@ pub fn run(target: &str, bind_ip: &str, secs: u64) {
let b = &buf[..n]; let b = &buf[..n];
resp_n += 1; resp_n += 1;
if let Some((is_pulse, d)) = rdm_region(b) { if let Some((is_pulse, d)) = rdm_region(b) {
if is_pulse { if is_show {
if let Some(uid) = decode_pulse(d) { println!(" [H] ★脉冲 UID={}", uid_s(&uid)); } // show 模式维护状态表
else { println!(" [H] 脉冲(冲突)"); } if is_pulse {
} else if d.len() >= 0x18 + 19 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0x60] { if let Some(uid) = decode_pulse(d) {
let a = u16::from_be_bytes([d[0x18 + 14], d[0x18 + 15]]); println!(" [{}] ★脉冲 UID={}", label, uid_s(&uid));
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); } else if d.len() >= 0x18 + 19 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0x60] {
if !addr_list.contains(&a) { addr_list.push(a); } 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 { } else {
println!(" [H] RDM应答 len={}", d.len()); 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<String> = 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)); std::thread::sleep(Duration::from_millis(33));
} }
println!("[H] 结束: 应答 {} 帧, 地址 {:?}", resp_n, addr_list); println!("[{}] 结束: 应答 {} 帧, 地址 {:?}", label, resp_n, if is_show { state.iter().map(|(_, a, _)| *a).collect::<Vec<_>>() } else { addr_list.clone() });
return; return;
} }
@ -561,7 +725,7 @@ pub fn run(target: &str, bind_ip: &str, secs: u64) {
let now = play_start.elapsed(); let now = play_start.elapsed();
if target_t > now { std::thread::sleep(target_t - now); } if target_t > now { std::thread::sleep(target_t - now); }
let addr = format!("{}:{}", d, port); let addr = format!("{}:{}", d, port);
// ★DMX 数据帧: 替换为我们自己的呼吸图案 (用认领 B12 加密 → Butler 解密后上灯) // ★DMX 数据帧: 替换为我们自己的呼吸图案 (用认领 B12 加密 → Butler 解密上灯)
let mut fb = b.clone(); let mut fb = b.clone();
if fb.len() == 1084 && fb[0x18] == 1 && fb[0x20] == 1 && fb[0x21] == 1 && fb[0x3C] != 1 { if fb.len() == 1084 && fb[0x18] == 1 && fb[0x20] == 1 && fb[0x21] == 1 && fb[0x3C] != 1 {
pat_ctr += 1; pat_ctr += 1;
@ -731,6 +895,188 @@ pub fn run(target: &str, bind_ip: &str, secs: u64) {
return; return;
} }
// show: 地址 1-32 炫彩呼吸持续播放 + 并行 RDM 状态轮询 (DEVICE_INFO / DMX 地址)
// 旧的 show 实现已合并到上方 hybrid/show 块;保留结构,条件永假
if false {
let o2: Vec<u8> = 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<Vec<u8>> = 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<String> = 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 b12 = rng.next();
let mut claim_ok = false; let mut claim_ok = false;
@ -767,6 +1113,12 @@ pub fn run(target: &str, bind_ip: &str, secs: u64) {
ch[0x1A..0x1E].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]); 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[0x88..0x8C].copy_from_slice(&nonce.to_le_bytes());
ch[0x8C..0x90].copy_from_slice(&b12c.to_le_bytes()); ch[0x8C..0x90].copy_from_slice(&b12c.to_le_bytes());
// CLAIM_ZERO=1: 清零不透明区 (0x2C-0x87, 0x90-0x123), 消融验证 Butler 是否校验
if std::env::var("CLAIM_ZERO").unwrap_or_default() == "1" {
for i in 0x2C..0x88 { ch[i] = 0; }
for i in 0x90..0x124 { ch[i] = 0; }
println!("CLAIM_ZERO: 不透明区已清零 (240B)");
}
sock.send_to(&ch, &dst).unwrap(); sock.send_to(&ch, &dst).unwrap();
let w2 = Instant::now(); let w2 = Instant::now();
while w2.elapsed() < Duration::from_millis(700) { while w2.elapsed() < Duration::from_millis(700) {
@ -1279,6 +1631,28 @@ fn send_key_burst(sock: &UdpSocket) {
} }
} }
/// ★AECH 认领质询帧 (552B) 从零构造 — 2026-09-11 全字段破解:
/// 不透明区 A(0x2C-0x87)/B(0x90-0x123) Butler 不校验 (CLAIM_ZERO 消融实证: 清零后 AERE 仍 PASS), 填 0
fn build_claim_frame(host_ip: [u8; 4], nonce: u32, b12: u32) -> Vec<u8> {
let mut f = Vec::with_capacity(552);
f.extend_from_slice(b"ENet");
f.extend_from_slice(&2u32.to_le_bytes()); // type=2
f.extend_from_slice(&[0x00, 0x00, 0x1c, 0x7c]); // 目标端口 31772
f.extend_from_slice(&[0u8; 4]);
f.extend_from_slice(&[0x00, 0x00, 0x01, 0x00]);
f.extend_from_slice(&552u32.to_le_bytes()); // 0x14 len
f.extend_from_slice(&[0x05, 0x00]); // 0x18 sub=05
f.extend_from_slice(&[host_ip[3], host_ip[2], host_ip[1], host_ip[0]]); // 0x1A 主机IP 逆序
f.extend_from_slice(&[0x30, 0x0e, 0xd5, 0x59, 0xbb, 0x36]); // 0x1E 主机MAC (本机)
f.extend_from_slice(&[0u8; 4]); // 0x24
f.extend_from_slice(b"AECH"); // 0x28
f.extend_from_slice(&[0u8; 0x88 - 0x2C]); // 0x2C-0x87 不透明区A = 0
f.extend_from_slice(&nonce.to_le_bytes()); // 0x88 nonce
f.extend_from_slice(&b12.to_le_bytes()); // 0x8C B12 (DMX rot 基值)
f.extend_from_slice(&[0u8; 552 - 0x90]); // 0x90-0x227 不透明区B+零填充
f
}
/// 统一会话环 (quick): DMX 30fps 持续 + RDM 穿插 /// 统一会话环 (quick): DMX 30fps 持续 + RDM 穿插
/// 阶段: 0-2s 黑场(设备预热) → UN_MUTE + UNIQUE_BRANCH 二分发现 → MUTE 已发现灯具 /// 阶段: 0-2s 黑场(设备预热) → UN_MUTE + UNIQUE_BRANCH 二分发现 → MUTE 已发现灯具
/// → GET DEVICE_INFO/DMX 地址 → 呼吸图案控制 (未发现则用已知地址) /// → GET DEVICE_INFO/DMX 地址 → 呼吸图案控制 (未发现则用已知地址)

View File

@ -4,6 +4,7 @@ mod tests;
mod dmxhash; mod dmxhash;
mod aech; mod aech;
mod hostdemo; mod hostdemo;
mod analyze;
use tests::Item; use tests::Item;
@ -168,6 +169,12 @@ fn main() {
hostdemo::run(&ip, "192.168.123.113", secs); hostdemo::run(&ip, "192.168.123.113", secs);
return; return;
} }
"analyze" => {
// analyze dump|diff|hdr|rdm|ctr — 离线抓包/模板分析
let args: Vec<String> = std::env::args().skip(2).collect();
analyze::run(&args);
return;
}
_ => { _ => {
println!("用法: enet-validator <discover|announce|heartbeat|euce|session [秒]|fallback|claim|rdm [秒]|all>"); println!("用法: enet-validator <discover|announce|heartbeat|euce|session [秒]|fallback|claim|rdm [秒]|all>");
return; return;

View File

@ -1,10 +1,10 @@
{ {
"target": "192.168.123.1", "target": "192.168.123.1",
"results": [ "results": [
{ "id": "U1", "pass": true, "detail": "发现应答=2426 遥测=21662 RDM脉冲=208904 RDM111=278460 RDM119=0" }, { "id": "U1", "pass": true, "detail": "发现应答=6898 遥测=19980 RDM脉冲=0 RDM111=0 RDM119=0" },
{ "id": "U2", "pass": false, "detail": "AECHA 认领交换=0 轮 (AEREP 已回)" }, { "id": "U2", "pass": false, "detail": "AECHA 认领交换=0 轮 (AEREP 已回)" },
{ "id": "U3", "pass": true, "detail": "明文 DMX 帧接收=206609" }, { "id": "U3", "pass": true, "detail": "明文 DMX 帧接收=18958" },
{ "id": "U4", "pass": false, "detail": "事件帧=0 ACK发送=0" }, { "id": "U4", "pass": false, "detail": "事件帧=0 ACK发送=0" },
{ "id": "U5", "pass": true, "detail": "通道值: 首32CH=[175, 174, 173, 172, 171, 170, 169, 168, 167, 166, 165, 164, 163, 162, 161, 160, 159, 158, 157, 156, 155, 154, 153, 152, 151, 150, 149, 148, 147, 146, 145, 144] 变化=205895 其他帧=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" }
] ]
} }