Compare commits
7 Commits
9580613ab2
...
50b04eb0d8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50b04eb0d8 | ||
|
|
c555a2252f | ||
|
|
44ff080ae6 | ||
|
|
f6b1f104ef | ||
|
|
8938b15691 | ||
|
|
9be449fda8 | ||
|
|
9995335d74 |
17
README.md
17
README.md
|
|
@ -53,6 +53,23 @@ enet-validator dmxroll <tsv> # 滚动值分析(离线)
|
|||
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)
|
||||
|
||||
- **AECH 认领**:`G(x) = x ^ TABLE[(x%32 + 1 + (x*13)%30) % 32]`(无密钥,表内嵌 DLL)
|
||||
|
|
|
|||
245
docs/enet_protocol_current.md
Normal file
245
docs/enet_protocol_current.md
Normal 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
266
src/analyze.rs
Normal 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]),
|
||||
}
|
||||
}
|
||||
520
src/hostdemo.rs
520
src/hostdemo.rs
|
|
@ -1,4 +1,4 @@
|
|||
//! hostdemo.rs — 主机侧等效示例程序 (2026-09-10): 认领真机 + RDM 发现 + 加密 DMX 控制
|
||||
//! hostdemo.rs — 主机侧等效示例程序 (2026-09-10): 认领真机 + RDM 发现 + 加密 DMX 控制
|
||||
//!
|
||||
//! 全流程不依赖 SymphoLight:
|
||||
//! 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
|
||||
}
|
||||
|
||||
/// ★从零构造 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(冲突/非法)
|
||||
fn decode_pulse(d: &[u8]) -> Option<[u8; 6]> {
|
||||
if d.len() < 17 || d[0] != 0xAA { return None; }
|
||||
|
|
@ -369,44 +440,65 @@ pub fn run(target: &str, bind_ip: &str, secs: u64) {
|
|||
return;
|
||||
}
|
||||
|
||||
// hybrid: 认领 + verbatim SL DMX 帧 + 我们构造的 RDM 帧 — 二分定位毒帧
|
||||
if std::env::var("HOSTDEMO_MODE").unwrap_or_default() == "hybrid" {
|
||||
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<u8>> = Vec::new(); // verbatim DMX 帧
|
||||
// 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" };
|
||||
// show: 全字段已破解, 零模板纯自构造; hybrid: 仍用 SL 实帧骨架做对照
|
||||
let mut dmxf: Vec<Vec<u8>> = Vec::new();
|
||||
let mut base_rdm: Option<Vec<u8>> = 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);
|
||||
if !is_show {
|
||||
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");
|
||||
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)
|
||||
// 认领 (announce144 → AECH; claim 帧纯自构造: 不透明区填 0, CLAIM_ZERO 消融已证实 Butler 不校验)
|
||||
let o2: Vec<u8> = 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());
|
||||
let o2a = [o2[0], o2[1], o2[2], o2[3]];
|
||||
let ch = build_claim_frame(o2a, nonce, bc);
|
||||
sock.send_to(&ch, &dst).unwrap();
|
||||
std::thread::sleep(Duration::from_millis(400));
|
||||
println!("[H] 认领完成 B12=0x{:08x}; verbatim DMX {} 帧 + 我们的 RDM", bc, dmxf.len());
|
||||
let rdm_t = base_rdm.expect("no RDM template");
|
||||
// ★验证 AERE 应答 (RDM 不加密, 认领失败 RDM 照样通但 DMX 解密失败 → 灯白色兜底)
|
||||
let mut aere_ok = false;
|
||||
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] = [
|
||||
[0x01, 0x07, 0x34, 0x67, 0x01, 0x25],
|
||||
[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 mut di = 0usize;
|
||||
let mut qseq: u16 = 0;
|
||||
let mut last_rdm = Instant::now() - Duration::from_secs(10);
|
||||
let mut step = 0usize;
|
||||
let mut addr_list: Vec<u16> = 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 start.elapsed() < Duration::from_secs(secs.max(90)) {
|
||||
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");
|
||||
|
|
@ -431,28 +526,49 @@ pub fn run(target: &str, bind_ip: &str, secs: u64) {
|
|||
last_bcast = Instant::now();
|
||||
}
|
||||
// verbatim DMX 循环
|
||||
// ★纯自构造 DMX 帧 (认领 B12 加密呼吸图案) — 彻底摆脱 SL 原帧依赖
|
||||
// DMX 数据替换
|
||||
let mut p1 = [0u8; 512];
|
||||
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;
|
||||
if std::env::var("SHOW_DIAG").unwrap_or_default() == "1" {
|
||||
// 诊断: ch1-32 全灭3s / 全亮3s 交替 — 验证 DMX 是否到达灯具
|
||||
let v = if (t as u32 / 3) % 2 == 0 { 0u8 } else { 255u8 };
|
||||
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 rr2 = rng.next();
|
||||
let mut f = dmxf[di % dmxf.len()].clone(); // 帧骨架仍取 SL 帧(含未破解字段), 数据/随机全替换
|
||||
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));
|
||||
// show 模式: 图案同时输出到 port1+port2 (灯具可能在任一口, 实测 port2 出灯)
|
||||
let mut p2 = [0u8; 512];
|
||||
if is_show || std::env::var("SHOW_PORT2").unwrap_or_default() == "1" { p2 = p1; }
|
||||
let f = if is_show {
|
||||
// ★纯自构造 DMX 帧 (零模板)
|
||||
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;
|
||||
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<u8> = Vec::new();
|
||||
let mut pid: u16 = 0x0001;
|
||||
let mut dest = [0xffu8; 6];
|
||||
|
|
@ -469,27 +585,38 @@ pub fn run(target: &str, bind_ip: &str, secs: u64) {
|
|||
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 tn = (step * 7 + 1) as u8;
|
||||
let (f, f2) = if is_show {
|
||||
// ★纯自构造 RDM 帧 (零模板) + 成对副本
|
||||
qseq = qseq.wrapping_add(1);
|
||||
let f = build_rdm_frame2(&dest, tn, qseq, cc, pid, &pd);
|
||||
let f2 = rdm_pair_copy(&f);
|
||||
(f, f2)
|
||||
} else {
|
||||
let mut f = rdm_t.clone().unwrap();
|
||||
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] = 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..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(&f2, &dst);
|
||||
println!("[H] RDM step={} pid={:04x}", step, pid);
|
||||
println!("[{}] RDM step={} pid={:04x}", label, step, pid);
|
||||
step += 1;
|
||||
}
|
||||
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];
|
||||
resp_n += 1;
|
||||
if let Some((is_pulse, d)) = rdm_region(b) {
|
||||
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); }
|
||||
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 {
|
||||
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));
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -561,7 +725,7 @@ pub fn run(target: &str, bind_ip: &str, secs: u64) {
|
|||
let now = play_start.elapsed();
|
||||
if target_t > now { std::thread::sleep(target_t - now); }
|
||||
let addr = format!("{}:{}", d, port);
|
||||
// ★DMX 数据帧: 替换为我们自己的呼吸图案 (用认领 B12 加密 → Butler 解密后上灯)
|
||||
// ★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;
|
||||
|
|
@ -731,6 +895,188 @@ pub fn run(target: &str, bind_ip: &str, secs: u64) {
|
|||
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 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[0x88..0x8C].copy_from_slice(&nonce.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();
|
||||
let w2 = Instant::now();
|
||||
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 穿插
|
||||
/// 阶段: 0-2s 黑场(设备预热) → UN_MUTE + UNIQUE_BRANCH 二分发现 → MUTE 已发现灯具
|
||||
/// → GET DEVICE_INFO/DMX 地址 → 呼吸图案控制 (未发现则用已知地址)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ mod tests;
|
|||
mod dmxhash;
|
||||
mod aech;
|
||||
mod hostdemo;
|
||||
mod analyze;
|
||||
|
||||
use tests::Item;
|
||||
|
||||
|
|
@ -168,6 +169,12 @@ fn main() {
|
|||
hostdemo::run(&ip, "192.168.123.113", secs);
|
||||
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>");
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"target": "192.168.123.1",
|
||||
"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": "U3", "pass": true, "detail": "明文 DMX 帧接收=206609" },
|
||||
{ "id": "U3", "pass": true, "detail": "明文 DMX 帧接收=18958" },
|
||||
{ "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" }
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user