From e248a979e855d0e5517a5133e8e3e7f308c9bd04 Mon Sep 17 00:00:00 2001 From: tonycao Date: Fri, 11 Sep 2026 01:02:39 +0800 Subject: [PATCH 1/7] enet-validator: e:net protocol reverse + host-side emulation (AECH claim / encrypted DMX / RDM discovery, hostdemo) --- .gitignore | 4 + Cargo.lock | 7 + Cargo.toml | 10 + samples/ack_84.hex | 1 + samples/aere_594.hex | 1 + samples/aerep_113.hex | 1 + samples/aerep_115.hex | 1 + samples/aerep_117.hex | 1 + samples/announce_144.hex | 1 + samples/announce_686.hex | 1 + samples/announce_live.hex | 1 + samples/claim_h112.hex | 1 + samples/claim_h114.hex | 1 + samples/claim_h116.hex | 1 + samples/discovery_req_222.hex | 1 + samples/discovery_resp_259.hex | 1 + samples/discovery_resp_259_orig.hex | 1 + samples/discovery_resp_259_rdm.hex | 1 + samples/dmx_a_1126.hex | 1 + samples/euce_582.hex | 1 + samples/event_b_1126.hex | 1 + samples/heartbeat_78.hex | 1 + samples/pid_map.txt | 50 ++ samples/rdm_coldpulse_0.hex | 1 + samples/rdm_coldpulse_1.hex | 1 + samples/rdm_coldpulse_2.hex | 1 + samples/rdm_coldpulse_3.hex | 1 + samples/rdm_coldpulse_4.hex | 1 + samples/rdm_disc_req_1126.hex | 1 + samples/rdm_info_119.hex | 1 + samples/rdm_pool.txt | 178 ++++ samples/rdm_pulse_101.hex | 1 + samples/rdm_pulse_101_0.hex | 1 + samples/rdm_pulse_101_1.hex | 1 + samples/rdm_pulse_101_2.hex | 1 + samples/rdm_pulse_101_3.hex | 1 + samples/rdm_pulse_101_4.hex | 1 + samples/rdm_req_1126.hex | 1 + samples/rdm_resp_111.hex | 1 + samples/rdm_setack_68.hex | 1 + samples/register_89.hex | 1 + samples/status_84.hex | 1 + src/aech.rs | 1226 +++++++++++++++++++++++++++ src/dmxhash.rs | 1201 ++++++++++++++++++++++++++ src/enet.rs | 239 ++++++ src/hostdemo.rs | 909 ++++++++++++++++++++ src/main.rs | 194 +++++ src/tests.rs | 905 ++++++++++++++++++++ validation_report.json | 10 + 49 files changed, 4970 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 samples/ack_84.hex create mode 100644 samples/aere_594.hex create mode 100644 samples/aerep_113.hex create mode 100644 samples/aerep_115.hex create mode 100644 samples/aerep_117.hex create mode 100644 samples/announce_144.hex create mode 100644 samples/announce_686.hex create mode 100644 samples/announce_live.hex create mode 100644 samples/claim_h112.hex create mode 100644 samples/claim_h114.hex create mode 100644 samples/claim_h116.hex create mode 100644 samples/discovery_req_222.hex create mode 100644 samples/discovery_resp_259.hex create mode 100644 samples/discovery_resp_259_orig.hex create mode 100644 samples/discovery_resp_259_rdm.hex create mode 100644 samples/dmx_a_1126.hex create mode 100644 samples/euce_582.hex create mode 100644 samples/event_b_1126.hex create mode 100644 samples/heartbeat_78.hex create mode 100644 samples/pid_map.txt create mode 100644 samples/rdm_coldpulse_0.hex create mode 100644 samples/rdm_coldpulse_1.hex create mode 100644 samples/rdm_coldpulse_2.hex create mode 100644 samples/rdm_coldpulse_3.hex create mode 100644 samples/rdm_coldpulse_4.hex create mode 100644 samples/rdm_disc_req_1126.hex create mode 100644 samples/rdm_info_119.hex create mode 100644 samples/rdm_pool.txt create mode 100644 samples/rdm_pulse_101.hex create mode 100644 samples/rdm_pulse_101_0.hex create mode 100644 samples/rdm_pulse_101_1.hex create mode 100644 samples/rdm_pulse_101_2.hex create mode 100644 samples/rdm_pulse_101_3.hex create mode 100644 samples/rdm_pulse_101_4.hex create mode 100644 samples/rdm_req_1126.hex create mode 100644 samples/rdm_resp_111.hex create mode 100644 samples/rdm_setack_68.hex create mode 100644 samples/register_89.hex create mode 100644 samples/status_84.hex create mode 100644 src/aech.rs create mode 100644 src/dmxhash.rs create mode 100644 src/enet.rs create mode 100644 src/hostdemo.rs create mode 100644 src/main.rs create mode 100644 src/tests.rs create mode 100644 validation_report.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1073d60 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +*.log +*.pcap +*.tsv diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..5879716 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "enet-validator" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..43ee166 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "enet-validator" +version = "0.1.0" +edition = "2021" + +[dependencies] +# 闆跺閮ㄤ緷璧栵細std::net::UdpSocket + std::fs 瓒冲 + +[profile.release] +opt-level = 2 diff --git a/samples/ack_84.hex b/samples/ack_84.hex new file mode 100644 index 0000000..d79721e --- /dev/null +++ b/samples/ack_84.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001002a00000002004275746c010018030000010000000000 \ No newline at end of file diff --git a/samples/aere_594.hex b/samples/aere_594.hex new file mode 100644 index 0000000..b5e707e --- /dev/null +++ b/samples/aere_594.hex @@ -0,0 +1 @@ +454e65740200000000001c7c0000000000000100280200000600017ba8c000161cf12e3900717ba84145524559bb367fffffff0107346701250015000a00000001017ba8c000ffffff00161cf12e39000000000000000000001a0000000101fa0c000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000023010000000024000000240000001c001c001d001d8361bf1d00f00af00a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/samples/aerep_113.hex b/samples/aerep_113.hex new file mode 100644 index 0000000..c05a30d --- /dev/null +++ b/samples/aerep_113.hex @@ -0,0 +1 @@ +454e65740200000000001c7c0000000000000100280200000600017ba8c000161cf12e3900717ba84145524559bb367ffffffd0107348809000015000a00000001017ba8c000ffffff00161cf12e39000000000000000000001a0000000101fa0c000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000023010000000023000000230000001c001c001d001d5904e1c100f00af00a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/samples/aerep_115.hex b/samples/aerep_115.hex new file mode 100644 index 0000000..8a0d423 --- /dev/null +++ b/samples/aerep_115.hex @@ -0,0 +1 @@ +454e65740200000000001c7c0000000000000100280200000600017ba8c000161cf12e3900717ba84145524559bb367ffffffd0107348809000015000a00000001017ba8c000ffffff00161cf12e39000000000000000000001a0000000101fa0c000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000023010000000023000000230000001c001c001d001d51cf405000f00af00a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/samples/aerep_117.hex b/samples/aerep_117.hex new file mode 100644 index 0000000..36c4f0b --- /dev/null +++ b/samples/aerep_117.hex @@ -0,0 +1 @@ +454e65740200000000001c7c0000000000000100280200000600017ba8c000161cf12e3900717ba84145524559bb367ffffffd0107348809000015000a00000001017ba8c000ffffff00161cf12e39000000000000000000001a0000000101fa0c000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000023010000000023000000230000001c001c001d001dfce6923d00f00af00a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/samples/announce_144.hex b/samples/announce_144.hex new file mode 100644 index 0000000..3d03fa8 --- /dev/null +++ b/samples/announce_144.hex @@ -0,0 +1 @@ +454e65740200000000001c7c0000000000000100660000000100717ba8c0300ed559bb361a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/samples/announce_686.hex b/samples/announce_686.hex new file mode 100644 index 0000000..3066eb9 --- /dev/null +++ b/samples/announce_686.hex @@ -0,0 +1 @@ +454e65740100000000001c7c000000000000010084020000150000000000000000000000000000000000000000000000000000000000000000001a000000000001717ba8c000ffffff300ed559bb3600000000000000000000000000000001ce43050500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000043a0f4f7ec175641812b909bdc99f2ff4000000000010003000000000000000000000000000100030000000000000000000000000074006f006e007900630061006f004400450053004b0054004f0050002d0050004500440032004c00560044002d00660037006600340061003000340033002d0031003700650063002d0034003100350036002d0038003100320062002d0039003000390062006400630039003900660032006600660000000000000000000000911f343cf708bf342e8e519da944bd8a5a4a24433989ea070902001819410343003a005c00550073006500720073005c0074006f006e007900630061006f005c0041007000700044006100740061005c004c006f00630061006c005c0065006300750065005c00530059004d00500048004f004c004900470048005400200035002e0035005c00430075007200720065006e007400530068006f0077000000000000000000000000000000000000000000d00df1e81900000081710b0e000000008ee4f7f6190000001e00000000000000010000005a00ffffea07090717182f16010200000000000000000000000002000000000000000000000000201c0000000000000000000001000000000000000000000001000000000000000100000001000000 \ No newline at end of file diff --git a/samples/announce_live.hex b/samples/announce_live.hex new file mode 100644 index 0000000..5391f6b --- /dev/null +++ b/samples/announce_live.hex @@ -0,0 +1 @@ +454e65740100000000001c7c000000000000010084020000150000000000000000000000000000000000000000000000000000000000000000001a000000000001717ba8c000ffffff300ed559bb3600000000000000000000000000000001ce43050500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000043a0f4f7ec175641812b909bdc99f2ff4000000000010003000000000000000000000000000100030000000000000000000000000074006f006e007900630061006f004400450053004b0054004f0050002d0050004500440032004c00560044002d00660037006600340061003000340033002d0031003700650063002d0034003100350036002d0038003100320062002d0039003000390062006400630039003900660032006600660000000000000000000000911f0000000000000000000000000000000000000000d00701010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e1bf21b20c0000001e00000000000000010000005a00ffffea07090a163432e003020000000000000000000000000200000000000000000000000098080000000000000000000000000000000000000000000000000000000000000000000000000000 diff --git a/samples/claim_h112.hex b/samples/claim_h112.hex new file mode 100644 index 0000000..7a7191e --- /dev/null +++ b/samples/claim_h112.hex @@ -0,0 +1 @@ +454e65740200000000001c7c0000000000000100280200000500717ba8c0300ed559bb36000000004145434841cad262b4c0d9aea3e3c1cc0898bffec74c63aca7b51e5727a7ae330ca7d61e162e57e9fa86044d898e5f16a8249dd5d6ab360eedef3a8cab7a3a0d12a6a1fb0da2b5d940d50340481523195508af0309d95c6071c73027f4ca3e80e2545f3b9bb5327e4cf63be3ec6408056ec49400a7f6c6d7cc2136a3cb37043849418945728ee5769783f351da3f2cf1ffa99445455bb87a01585bd3949ab415f20299abdac393e0de1e69240274fd8cf445a422a2bef3ed880e44193212e1fccede5fd8a681163c16b46626f294788d5bcfdf4ef69272330eac22cb7e79adcc3c6c55231525e5722e670bed58ccfbf496b7be633393063c6c2bc9fa0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/samples/claim_h114.hex b/samples/claim_h114.hex new file mode 100644 index 0000000..6d0fd5d --- /dev/null +++ b/samples/claim_h114.hex @@ -0,0 +1 @@ +454e65740200000000001c7c0000000000000100280200000500717ba8c0300ed559bb3600000000414543484bbd9d079aea853e547835e96c208e8685db41b02e35c7ba4998a96619b7e95beb13e3aa6ee5bde275b5413f897ac67277b5e64f87493016f5e2bd01411e259c8ed9fd593de4189f1d5f3a78f38830cfd236ee63d099d4a2e2cdb48b5f2e2a1f9bb5327e27bca0578fa4b0af7c6d35939c4731280eb8226db1ee2f0a30c2ccf4055a77702f8d7761bee3e94ed7815265015ac68967ac9429aa47d9398d9626cdd8bca921f5c4640fd0d0692aced495309bca09d5fb438c59c64dc6e8b33dd428bc1c751bd7306f3cf6b6b89742695bd6b1a99ec38d51d1952f505ba7fe7b5c92fb416eb25e7a586a5accde458af5165df2f0370a9f119b6b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/samples/claim_h116.hex b/samples/claim_h116.hex new file mode 100644 index 0000000..85e3f14 --- /dev/null +++ b/samples/claim_h116.hex @@ -0,0 +1 @@ +454e65740200000000001c7c0000000000000100280200000500717ba8c0300ed559bb360000000041454348e0eb87d5c73e7a5a269b01faa87737503368c0728c0b3a39fdb50e4452f3eaa602687e8ed195928e64ea645915624a7800ba6ca8daf9bdd5b14dd8122f360833b15af840f9a87e9dd6320ea1a6e1239a6b9644978641cb09aaa8dc834a22993e9bb5327ebd805e2de231a8bb6a0a7e573bced2a8ea1c2ed02a3da799f170ac6f8fee9550cbfae056d3e0bf7539682f6117ec5cb76f231f8e7a0c597fd8c53b7284382c7c742e8b40ee7cd50a5db897c10d45820413f4006bcf07d6bfa0607d0da44cf0003cb3ede8611cc9f34d1430c17598719342365e9037c0ad6c57cc9f8dfec76510c796855a5899a3f72a0d1ebad6144a08eeeaca6d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/samples/discovery_req_222.hex b/samples/discovery_req_222.hex new file mode 100644 index 0000000..364ca7b --- /dev/null +++ b/samples/discovery_req_222.hex @@ -0,0 +1 @@ +454e65740100000000001c7c0000000000000100b40000000100746f6e7963616f4445534b544f502d504544324c56442d66376634613034332d1a000000000002717ba8c000ffffff300ed559bb3600000000000000000000000000000001ce430505000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000900 \ No newline at end of file diff --git a/samples/discovery_resp_259.hex b/samples/discovery_resp_259.hex new file mode 100644 index 0000000..8c6fdad --- /dev/null +++ b/samples/discovery_resp_259.hex @@ -0,0 +1 @@ +454e65740100000000001c7c0000000000000100d90000000f004275746c657220533200000000001b00cc0119453a7ffffff30107346701250015000a00000001017ba8c000ffffff00161cf12e396b3aa8c0300ed559bb351a0000000101fa0c00020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002301000000001e0000001e000000240024002400240020002000720d720d1e0000000a0000000000000000000a00 \ No newline at end of file diff --git a/samples/discovery_resp_259_orig.hex b/samples/discovery_resp_259_orig.hex new file mode 100644 index 0000000..37fc0f0 --- /dev/null +++ b/samples/discovery_resp_259_orig.hex @@ -0,0 +1 @@ +454e65740100000000001c7c0000000000000100d90000000f004275746c657220533200000000000000aaaa5dbbddfa55aa55ae77ebddaf570015000a00000001017ba8c000ffffff00161cf12e39000000000000000000001a0000000101fa0c000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000023010000000024000000240000001c001c001d001d001c001c00f00af00a00000000000000000000000000000000 diff --git a/samples/discovery_resp_259_rdm.hex b/samples/discovery_resp_259_rdm.hex new file mode 100644 index 0000000..37fc0f0 --- /dev/null +++ b/samples/discovery_resp_259_rdm.hex @@ -0,0 +1 @@ +454e65740100000000001c7c0000000000000100d90000000f004275746c657220533200000000000000aaaa5dbbddfa55aa55ae77ebddaf570015000a00000001017ba8c000ffffff00161cf12e39000000000000000000001a0000000101fa0c000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000023010000000024000000240000001c001c001d001d001c001c00f00af00a00000000000000000000000000000000 diff --git a/samples/dmx_a_1126.hex b/samples/dmx_a_1126.hex new file mode 100644 index 0000000..2eb4c1d --- /dev/null +++ b/samples/dmx_a_1126.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001003c040000010097010000000001011a8c191563d67616000000000100000001000002000201000000abaaa9a8a7a6a5a4a3a2a1a09f9e9d9c9b9a999897969594939291908f8e8d8c8b8a898887868584838281807f7e7d7c7b7a797877767574737271706f6e6d6c6b6a696867666564636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0efeeedecebeae9e8e7e6e5e4e3e2e1e0dfdedddcdbdad9d8d7d6d5d4d3d2d1d0cfcecdcccbcac9c8c7c6c5c4c3c2c1c0bfbebdbcbbbab9b8b7b6b5b4b3b2b1b0afaeadacabaaa9a8a7a6a5a4a3a2a1a09f9e9d9c9b9a999897969594939291908f8e8d8c8b8a898887868584838281807f7e7d7c7b7a797877767574737271706f6e6d6c6b6a696867666564636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a0908070605040302015554535251504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a090807060504030201d4d3d2d1d0cfcecdcccbcac9c8c7c6c5c4c3c2c1c0bfbebdbcbbbab9b8b7b6b5b4b3b2b1b0afaeadacabaaa9a8a7a6a5a4a3a2a1a09f9e9d9c9b9a999897969594939291908f8e8d8c8b8a898887868584838281807f7e7d7c7b7a797877767574737271706f6e6d6c6b6a696867666564636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0efeeedecebeae9e8e7e6e5e4e3e2e1e0dfdedddcdbdad9d8d7d6d5d4d3d2d1d0cfcecdcccbcac9c8c7c6c5c4c3c2c1c0bfbebdbcbbbab9b8b7b6b5b4b3b2b1b0afaeadacabaaa9a8a7a6a5a4a3a2a1a09f9e9d9c9b9a999897969594939291908f8e8d8c8b8a898887868584838281807f7e7d7c7b7a797877767574737271706f6e6d6c6b6a696867666564636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a0908070605040302012c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a090807060504030201 \ No newline at end of file diff --git a/samples/euce_582.hex b/samples/euce_582.hex new file mode 100644 index 0000000..30b6d8f --- /dev/null +++ b/samples/euce_582.hex @@ -0,0 +1 @@ +4555434500000000000000000500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000746f6e7963616f4445534b544f502d504544324c56442d66376634613034332d313765632d343135362d383132622d3930396264633939663266660000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300ed559bb36 \ No newline at end of file diff --git a/samples/event_b_1126.hex b/samples/event_b_1126.hex new file mode 100644 index 0000000..794806b --- /dev/null +++ b/samples/event_b_1126.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001003c040000010000000000000002001903000000000000cc00000001000100000025000000010000000124ffffffffffff453a7ffffffe96010000001000010c80007ffffffffffffffffffe158e1b1a191817161514131211100f0e0d0c0b0a09080706050403020100fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0efeeedecebeae9e8e7e6e5e4e3e2e1e0dfdedddcdbdad9d8d7d6d5d4d3d2d1d0cfcecdcccbcac9c8c7c6c5c4c3c2c1c0bfbebdbcbbbab9b8b7b6b5b4b3b2b1b0afaeadacabaaa9a8a7a6a5a4a3a2a1a09f9e9d9c9b9a999897969594939291908f8e8d8c8b8a898887868584838281807f7e7d7c7b7a797877767574737271706f6e6d6c6b6a696867666564636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a090807060504030201c0bfbebdbcbbbab9b8b7b6b5b4b3b2b1b0afaeadacabaaa9a8a7a6a5a4a3a2a1a09f9e9d9c9b9a999897969594939291908f8e8d8c8b8a898887868584838281807f7e7d7c7b7a797877767574737271706f6e6d6c6b6a696867666564636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a0908070605040302018b8a898887868584838281807f7e7d7c7b7a797877767574737271706f6e6d6c6b6a696867666564636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a0908070605040302017574737271706f6e6d6c6b6a696867666564636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0efeeedecebeae9e8e7e6e5e4e3e2e1e0dfdedddcdbdad9d8d7d6d5d4d3d2d1d0cfcecdcccbcac9c8c7c6c5c4c3c2c1c0bfbebdbcbbbab9b8b7b6b5b4b3b2b1b0afaeadacabaaa9a8a7a6a5a4a3a2a1a09f9e9d9c9b9a999897969594939291908f8e8d8c8b8a898887868584838281807f7e7d7c7b7a797877767574737271706f6e6d6c6b6a696867666564636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a090807060504030201 \ No newline at end of file diff --git a/samples/heartbeat_78.hex b/samples/heartbeat_78.hex new file mode 100644 index 0000000..ba0f9bc --- /dev/null +++ b/samples/heartbeat_78.hex @@ -0,0 +1 @@ +454e65740200000000001c7c0000000000000100240000000300717ba8c0300ed559bb36 \ No newline at end of file diff --git a/samples/pid_map.txt b/samples/pid_map.txt new file mode 100644 index 0000000..5e02db8 --- /dev/null +++ b/samples/pid_map.txt @@ -0,0 +1,50 @@ +0080 454e65740400000000001c7c00000000000001005200000002004275746c000098000000000000002800cc0126453a7fffffff01073467012584000000002100800e4e46532d314b32447878784278200b32 +0504 454e65740400000000001c7c00000000000001003b0000000200017ba8c001000d010000000000001100aaab55af57be75aaddab5daa55ae57efd7 +8013 454e65740400000000001c7c00000000000001004500000002004275746c0000a2000000000000001b00cc0119453a7fffffff0107346701258e00000000218013010106ee +8074 454e65740400000000001c7c00000000000001004500000002004275746c0000b9000000000000001b00cc0119453a7fffffff010734670125a50000000021807401000765 +0060 454e65740400000000001c7c00000000000001005700000002004275746c000021010000000000002d00cc012b453a7fffffff0107348809000c00000000210060130100020005090001000100010101006f00001106f6 +8021 454e65740400000000001c7c00000000000001004500000002004275746c0000a4000000000000001b00cc0119453a7fffffff0107346701259000000000218021010106fe +8064 454e65740400000000001c7c00000000000001004600000002004275746c0000b3000000000000001c00cc011a453a7fffffff0107346701259f000000002180640200000751 +00c0 454e65740400000000001c7c00000000000001005700000002004275746c000096000000000000002d00cc012b453a7fffffff01073467012582000000002100c0134e46532d314b324478787842782056312e30310c90 +8051 454e65740400000000001c7c00000000000001004500000002004275746c0000ac000000000000001b00cc0119453a7fffffff010734670125980000000021805101000735 +8072 454e65740400000000001c7c00000000000001004500000002004275746c0000b7000000000000001b00cc0119453a7fffffff010734670125a30000000021807201000761 +8063 454e65740400000000001c7c00000000000001004600000002004275746c0000b2000000000000001c00cc011a453a7fffffff0107346701259e0000000021806302154d07b1 +8084 454e65740400000000001c7c00000000000001004600000002004275746c0000bf000000000000001c00cc011a453a7fffffff010734670125aa0000000021808402005c07d8 +8050 454e65740400000000001c7c00000000000001004500000002004275746c0000ab000000000000001b00cc0119453a7fffffff010734670125970000000021805001010734 +8071 454e65740400000000001c7c00000000000001004500000002004275746c0000b6000000000000001b00cc0119453a7fffffff010734670125a2000000002180710100075f +0082 454e65740400000000001c7c00000000000001005000000002004275746c00009a000000000000002600cc0124453a7fffffff01073467012586000000002100820c4e4653314b324478787842780ae5 +8041 454e65740400000000001c7c00000000000001004500000002004275746c0000a8000000000000001b00cc0119453a7fffffff010734670125940000000021804101fe081f +0003 454e65740400000000001c7c00000000000001005700000002004275746c00005c000000000000002d00cc012b453a7fffffff010734670125510000000021006013010001030509000100010002010100650000170736 +0401 454e65740400000000001c7c00000000000001004800000002004275746c00009c000000000000001e00cc011c453a7fffffff0107346701258800000000210401040000000d066c +8090 454e65740400000000001c7c00000000000001004500000002004275746c0000c2000000000000001b00cc0119453a7fffffff010734670125ad00000000218090013207bb +8053 454e65740400000000001c7c00000000000001004500000002004275746c0000ae000000000000001b00cc0119453a7fffffff0107346701259a0000000021805301000739 +8010 454e65740400000000001c7c00000000000001004500000002004275746c0000a0000000000000001b00cc0119453a7fffffff0107346701258c00000000218010010106e9 +8042 454e65740400000000001c7c00000000000001004500000002004275746c0000a9000000000000001b00cc0119453a7fffffff010734670125950000000021804201ff0822 +8061 454e65740400000000001c7c00000000000001004800000002004275746c0000b0000000000000001e00cc011c453a7fffffff0107346701259c00000000218061040000a3650857 +8081 454e65740400000000001c7c00000000000001004800000002004275746c00007c010000000000001e00cc011c453a7fffffff0107348809005e00000000218081040008a67b085e +0405 454e65740400000000001c7c00000000000001004800000002004275746c00009f000000000000001e00cc011c453a7fffffff0107346701258b00000000210405040000006906cf +00f0 454e65740400000000001c7c0000000000000100460000000200017ba8c000002e020000000000001c00cc011a453a7fffffff0107346701250f000000002100f00200650732 +0002 454e65740400000000001c7c00000000000001004600000002004275746c000020010000000000001c00cc011a453a7fffffff0107348809000b0000000011000202000005cf +0403 454e65740400000000001c7c00000000000001004500000002004275746c00009e000000000000001b00cc0119453a7fffffff0107346701258a00000000210403017f06dc +8060 454e65740400000000001c7c00000000000001004700000002004275746c0000af000000000000001d00cc011b453a7fffffff0107346701259b000000002180600309334707ce +8022 454e65740400000000001c7c00000000000001004500000002004275746c0000a5000000000000001b00cc0119453a7fffffff010734670125910000000021802201010700 +8065 454e65740400000000001c7c00000000000001004800000002004275746c0000b4000000000000001e00cc011c453a7fffffff010734670125a00000000021806504000000000757 +8085 454e65740400000000001c7c00000000000001004800000002004275746c0000c0000000000000001e00cc011c453a7fffffff010734670125ab0000000021808504000000040786 +8011 454e65740400000000001c7c00000000000001004500000002004275746c0000a1000000000000001b00cc0119453a7fffffff0107346701258d00000000218011013c0726 +8070 454e65740400000000001c7c00000000000001004500000002004275746c0000b5000000000000001b00cc0119453a7fffffff010734670125a1000000002180700100075d +8040 454e65740400000000001c7c00000000000001004500000002004275746c0000a7000000000000001b00cc0119453a7fffffff010734670125930000000021804001010720 +8023 454e65740400000000001c7c00000000000001004500000002004275746c0000a6000000000000001b00cc0119453a7fffffff010734670125920000000021802301000701 +8083 454e65740400000000001c7c00000000000001004600000002004275746c0000be000000000000001c00cc011a453a7fffffff010734670125a9000000002180830208f60878 +8052 454e65740400000000001c7c00000000000001004500000002004275746c0000ad000000000000001b00cc0119453a7fffffff010734670125990000000021805201010738 +1000 454e65740400000000001c7c00000000000001004500000002004275746c000097000000000000001b00cc0119453a7fffffff01073467012583000000002110000100065f +0081 454e65740400000000001c7c00000000000001005f00000002004275746c000099000000000000003500cc0133453a7fffffff01073467012585000000002100811b496e76656e74726f6e6963732848616e677a686f75292c496e632e1111 +8043 454e65740400000000001c7c00000000000001004500000002004275746c0000aa000000000000001b00cc0119453a7fffffff010734670125960000000021804301ff0824 +8087 454e65740400000000001c7c00000000000001004800000002004275746c0000c1000000000000001e00cc011c453a7fffffff010734670125ac0000000021808704000000d30858 +0050 454e65740400000000001c7c00000000000001004e0000000200017ba8c00000fd010000000000002400cc0122453a7fffffff0899500026c9de000000002100500a020002010081008204030932 +8073 454e65740400000000001c7c00000000000001004500000002004275746c0000b8000000000000001b00cc0119453a7fffffff010734670125a40000000021807301000763 +8082 454e65740400000000001c7c00000000000001004500000002004275746c0000bd000000000000001b00cc0119453a7fffffff010734670125a80000000021808201210797 +0400 454e65740400000000001c7c00000000000001004800000002004275746c00009b000000000000001e00cc011c453a7fffffff010734670125870000000021040004000000c80725 +8080 454e65740400000000001c7c00000000000001004700000002004275746c0000ba000000000000001d00cc011b453a7fffffff010734670125a6000000002180800300006907df +8062 454e65740400000000001c7c00000000000001004500000002004275746c0000b1000000000000001b00cc0119453a7fffffff0107346701259d00000000218062017f07ca +0402 454e65740400000000001c7c00000000000001004800000002004275746c00009d000000000000001e00cc011c453a7fffffff010734670125890000000021040204000933840721 +8020 454e65740400000000001c7c00000000000001004500000002004275746c0000a3000000000000001b00cc0119453a7fffffff0107346701258f00000000218020010006fb diff --git a/samples/rdm_coldpulse_0.hex b/samples/rdm_coldpulse_0.hex new file mode 100644 index 0000000..fafb893 --- /dev/null +++ b/samples/rdm_coldpulse_0.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001003b00000002004275746c0100bc010000000000001100aaaa5dbbddfa55aa55ae77ebddaf57fadf \ No newline at end of file diff --git a/samples/rdm_coldpulse_1.hex b/samples/rdm_coldpulse_1.hex new file mode 100644 index 0000000..93d2e0b --- /dev/null +++ b/samples/rdm_coldpulse_1.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001003b00000002004275746c010036000000000000001100aaab55af57be75ef77ab55af75ae57ebd7 \ No newline at end of file diff --git a/samples/rdm_coldpulse_2.hex b/samples/rdm_coldpulse_2.hex new file mode 100644 index 0000000..0f51d37 --- /dev/null +++ b/samples/rdm_coldpulse_2.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001003b0000000200017ba8c00100fb000000000000001100aaab55af57be75aaddab5daa55ae57efd7 \ No newline at end of file diff --git a/samples/rdm_coldpulse_3.hex b/samples/rdm_coldpulse_3.hex new file mode 100644 index 0000000..a1123d2 --- /dev/null +++ b/samples/rdm_coldpulse_3.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001003b00000002004275746c01009c050000000000001100fefefeffffffddffbfbfbfbfbfbf57efd7 \ No newline at end of file diff --git a/samples/rdm_coldpulse_4.hex b/samples/rdm_coldpulse_4.hex new file mode 100644 index 0000000..0109267 --- /dev/null +++ b/samples/rdm_coldpulse_4.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001003b0000000200017ba8c001001b000000000000001100fefefefefefffffff7f7bfbfff35ddedd7 \ No newline at end of file diff --git a/samples/rdm_disc_req_1126.hex b/samples/rdm_disc_req_1126.hex new file mode 100644 index 0000000..2966fa9 --- /dev/null +++ b/samples/rdm_disc_req_1126.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001003c04000001000000000000000200090f000000000000cc00000001000000000025000000010000000124ffffffffffff453a7ffffffd86000000001000010c089950001767089950001f660e6c050500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000043a0f4f7ec175641812b909bdc99f2ff4000000000010003000000000000000000000000000100030000000000000000000000000074006f006e007900630061006f004400450053004b0054004f0050002d0050004500440032004c00560044002d00660037006600340061003000340033002d0031003700650063002d0034003100350036002d0038003100320062002d0039003000390062006400630039003900660032006600660000000000000000000000911f343cf708bf342e8e519da944bd8a5a4a24433989ea070902001819410343003a005c00550073006500720073005c0074006f006e007900630061006f005c0041007000700044006100740061005c004c006f00630061006c005c0065006300750065005c00530059004d00500048004f004c004900470048005400200035002e0035005c00430075007200720065006e007400530068006f0077000000000000000000000000000000000000000000d00df1e819000000f6d1c94c090000003737b635230000001e00000000000000010000005a00ffffea0709080a1a2245020200000000000000000000000002000000000000000000000000000000000000000000000000010000000100000000000000010000000000000001000000010000002f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a090807060504030201898887868584838281807f7e7d7c7b7a797877767574737271706f6e6d6c6b6a696867666564636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0efeeedecebeae9e8e7e6e5e4e3e2e1e0dfdedddcdbdad9d8d7d6d5d4d3d2d1d0cfcecdcccbcac9c8c7c6c5c4c3c2c1c0bfbebdbcbbbab9b8b7b6b5b4b3b2b1b0afaeadacabaaa9a8a7a6a5a4a3a2a1a09f9e9d9c9b9a999897969594939291908f8e8d8c8b8a898887868584838281807f7e7d7c7b7a797877767574737271706f6e6d6c6b6a696867666564636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a090807060504030201 \ No newline at end of file diff --git a/samples/rdm_info_119.hex b/samples/rdm_info_119.hex new file mode 100644 index 0000000..9df8925 --- /dev/null +++ b/samples/rdm_info_119.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001004d00000002004275746c00007e0f0000000000002300cc0121453a7ffffffd010734670125c00000000021020109000280000000000000071f \ No newline at end of file diff --git a/samples/rdm_pool.txt b/samples/rdm_pool.txt new file mode 100644 index 0000000..ca3f387 --- /dev/null +++ b/samples/rdm_pool.txt @@ -0,0 +1,178 @@ +59 454e65740400000000001c7c00000000000001003b00000002004275746c01000b0f0000000000001100aaaa5dbbddfa55aa55ae77ebddaf57fadf +70 454e65740400000000001c7c00000000000001004600000002004275746c00002f0f0000000000001c00cc011a453a7ffffffd0899500026c99900000000110002020000076e +87 454e65740400000000001c7c00000000000001005700000002004275746c0000310f0000000000002d00cc012b453a7ffffffd0899500026c99a0000000021006013010001000100010009050020000000010000040836 +59 454e65740400000000001c7c00000000000001003b00000002004275746c0100790f0000000000001100aaff7fffffaa5feaf5bad7ab7dab57ab77 +77 454e65740400000000001c7c00000000000001004d00000002004275746c00007e0f0000000000002300cc0121453a7ffffffd010734670125c00000000021020109000280000000000000071f +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000800f0000000000002300cc0121453a7ffffffd010734670125c1000000002102010901001b00000000000006ba +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000830f0000000000002300cc0121453a7ffffffd010734670125c200000000210201090208dd08bc094000000893 +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000850f0000000000002300cc0121453a7ffffffd010734670125c30000000021020109030010000505c900000786 +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000870f0000000000002300cc0121453a7ffffffd010734670125c4000000002102010904003200000000000006d7 +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000890f0000000000002300cc0121453a7ffffffd010734670125c500000000210201090500040004087f00000736 +77 454e65740400000000001c7c00000000000001004d00000002004275746c00008b0f0000000000002300cc0121453a7ffffffd010734670125c6000000002102010906000000000000000006a9 +77 454e65740400000000001c7c00000000000001004d00000002004275746c00008d0f0000000000002300cc0121453a7ffffffd010734670125c70000000021020109070019000b0d20000006fc +77 454e65740400000000001c7c00000000000001004d00000002004275746c00008f0f0000000000002300cc0121453a7ffffffd010734670125c8000000002102010908000000000000000006ad +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000910f0000000000002300cc0121453a7ffffffd010734670125c9000000002102010909000b00000000000006ba +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000930f0000000000002300cc0121453a7ffffffd010734670125ca00000000210201090a000000000000000006b1 +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000950f0000000000002300cc0121453a7ffffffd010734670125cb00000000210201090b002900000000000006dc +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000960f0000000000002300cc0121453a7ffffffd010734670125cc00000000210201090c007f0000000000000734 +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000980f0000000000002300cc0121453a7ffffffd010734670125cd00000000210201090d0503002a163200000731 +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000990f0000000000002300cc0121453a7ffffffd010734670125ce00000000210201090e0000000000c700000780 +77 454e65740400000000001c7c00000000000001004d00000002004275746c00009b0f0000000000002300cc0121453a7ffffffd010734670125cf00000000210201090f00000000037e0000073c +77 454e65740400000000001c7c00000000000001004d00000002004275746c00009d0f0000000000002300cc0121453a7ffffffd010734670125d0000000002102010910000000000000000006bd +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000a00f0000000000002300cc0121453a7ffffffd010734670125d1000000002102010911001200000000000006d1 +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000a20f0000000000002300cc0121453a7ffffffd010734670125d2000000002102010912007f0000000000000740 +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000a30f0000000000002300cc0121453a7ffffffd010734670125d3000000002102010913051c0044163b00000779 +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000a60f0000000000002300cc0121453a7ffffffd010734670125d4000000002102010914000000000015000006da +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000a70f0000000000002300cc0121453a7ffffffd010734670125d500000000210201091500000000004200000709 +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000a90f0000000000002300cc0121453a7ffffffd010734670125d6000000002102010916000000000000000006c9 +70 454e65740400000000001c7c00000000000001004600000002004275746c0000ab0f0000000000001c00cc011a453a7ffffffd0899500026c9d7000000002100f002000108ab +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000ac0f0000000000002300cc0121453a7ffffffd0899500026c9d80000000021020109000ce400000000000008bc +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000ae0f0000000000002300cc0121453a7ffffffd0899500026c9d90000000021020109010945000000000000081c +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000b00f0000000000002300cc0121453a7ffffffd0899500026c9da000000002102010902001800000000000007e8 +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000b20f0000000000002300cc0121453a7ffffffd0899500026c9db00000000210201090320db00000000000008cd +70 454e65740400000000001c7c00000000000001004600000002004275746c0000b50f0000000000001c00cc011a453a7ffffffd010734880900dc000000002100f0020070080c +69 454e65740400000000001c7c00000000000001004500000002004275746c0000b70f0000000000001b00cc0119453a7ffffffd010734880900dd00000000218011013c0778 +69 454e65740400000000001c7c00000000000001004500000002004275746c0000b90f0000000000001b00cc0119453a7ffffffd010734880900de000000002180200100074c +69 454e65740400000000001c7c00000000000001004500000002004275746c0000ba0f0000000000001b00cc0119453a7ffffffd010734880900df000000002180210101074f +69 454e65740400000000001c7c00000000000001004500000002004275746c0000bc0f0000000000001b00cc0119453a7ffffffd010734880900e00000000021802201010751 +69 454e65740400000000001c7c00000000000001004500000002004275746c0000be0f0000000000001b00cc0119453a7ffffffd010734880900e10000000021802301000752 +69 454e65740400000000001c7c00000000000001004500000002004275746c0000bf0f0000000000001b00cc0119453a7ffffffd010734880900e20000000021804001010771 +69 454e65740400000000001c7c00000000000001004500000002004275746c0000c10f0000000000001b00cc0119453a7ffffffd010734880900e30000000021804101fe0870 +69 454e65740400000000001c7c00000000000001004500000002004275746c0000c30f0000000000001b00cc0119453a7ffffffd010734880900e40000000021804201ff0873 +69 454e65740400000000001c7c00000000000001004500000002004275746c0000c50f0000000000001b00cc0119453a7ffffffd010734880900e50000000021804301ff0875 +69 454e65740400000000001c7c00000000000001004500000002004275746c0000c70f0000000000001b00cc0119453a7ffffffd010734880900e60000000021805001010785 +69 454e65740400000000001c7c00000000000001004500000002004275746c0000c90f0000000000001b00cc0119453a7ffffffd010734880900e70000000021805101000786 +69 454e65740400000000001c7c00000000000001004500000002004275746c0000cb0f0000000000001b00cc0119453a7ffffffd010734880900e80000000021805201010789 +69 454e65740400000000001c7c00000000000001004500000002004275746c0000ce0f0000000000001b00cc0119453a7ffffffd010734880900e9000000002180530100078a +71 454e65740400000000001c7c00000000000001004700000002004275746c0000cf0f0000000000001d00cc011b453a7ffffffd010734880900ea00000000218060030006d8087a +72 454e65740400000000001c7c00000000000001004800000002004275746c0000d10f0000000000001e00cc011c453a7ffffffd010734880900eb000000002180610400039c2a0869 +69 454e65740400000000001c7c00000000000001004500000002004275746c0000d30f0000000000001b00cc0119453a7ffffffd010734880900ec0000000021806201d80874 +70 454e65740400000000001c7c00000000000001004600000002004275746c0000d50f0000000000001c00cc011a453a7ffffffd010734880900ed0000000021806302005e07fe +70 454e65740400000000001c7c00000000000001004600000002004275746c0000d60f0000000000001c00cc011a453a7ffffffd010734880900ee0000000021806402000007a2 +72 454e65740400000000001c7c00000000000001004800000002004275746c0000d80f0000000000001e00cc011c453a7ffffffd010734880900ef00000000218065040000000007a8 +69 454e65740400000000001c7c00000000000001004500000002004275746c0000da0f0000000000001b00cc0119453a7ffffffd010734880900f000000000218070010007ae +69 454e65740400000000001c7c00000000000001004500000002004275746c0000db0f0000000000001b00cc0119453a7ffffffd010734880900f100000000218071010007b0 +69 454e65740400000000001c7c00000000000001004500000002004275746c0000de0f0000000000001b00cc0119453a7ffffffd010734880900f200000000218072010007b2 +69 454e65740400000000001c7c00000000000001004500000002004275746c0000e00f0000000000001b00cc0119453a7ffffffd010734880900f300000000218073010007b4 +69 454e65740400000000001c7c00000000000001004500000002004275746c0000e10f0000000000001b00cc0119453a7ffffffd010734880900f400000000218074010007b6 +71 454e65740400000000001c7c00000000000001004700000002004275746c0000e30f0000000000001d00cc011b453a7ffffffd010734880900f50000000021808003000075083c +72 454e65740400000000001c7c00000000000001004800000002004275746c0000e50f0000000000001e00cc011c453a7ffffffd010734880900f6000000002180810400082b820880 +69 454e65740400000000001c7c00000000000001004500000002004275746c0000e70f0000000000001b00cc0119453a7ffffffd010734880900f700000000218082011b07e2 +70 454e65740400000000001c7c00000000000001004600000002004275746c0000e90f0000000000001c00cc011a453a7ffffffd010734880900f8000000002180830208fe08d1 +70 454e65740400000000001c7c00000000000001004600000002004275746c0000eb0f0000000000001c00cc011a453a7ffffffd010734880900f90000000021808402000007cd +72 454e65740400000000001c7c00000000000001004800000002004275746c0000ed0f0000000000001e00cc011c453a7ffffffd010734880900fa00000000218085040000000407d7 +72 454e65740400000000001c7c00000000000001004800000002004275746c0000ee0f0000000000001e00cc011c453a7ffffffd010734880900fb00000000218087040000000007d6 +69 454e65740400000000001c7c00000000000001004500000002004275746c0000f10f0000000000001b00cc0119453a7ffffffd010734880900fc000000002180900134080e +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000f20f0000000000002300cc0121453a7ffffffd010734880900fd000000002102010900021700000000000006f7 +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000f40f0000000000002300cc0121453a7ffffffd010734880900fe000000002102010901001b00000000000006fb +77 454e65740400000000001c7c00000000000001004d00000002004275746c0100f60f0000000000002300cc0121453a7ffffffd010734880900ff00000000210201090208fe08cc096100000926 +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000f80f0000000000002300cc0121453a7ffffffd0107348809000000000000210201090300000000024e00000634 +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000fa0f0000000000002300cc0121453a7ffffffd010734880900010000000021020109040034000000000000061a +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000fc0f0000000000002300cc0121453a7ffffffd01073488090002000000002102010905000400040405000005f9 +77 454e65740400000000001c7c00000000000001004d00000002004275746c0000fe0f0000000000002300cc0121453a7ffffffd01073488090003000000002102010906000000000000000005ea +77 454e65740400000000001c7c00000000000001004d00000002004275746c000000100000000000002300cc0121453a7ffffffd0107348809000400000000210201090700000000054e0000063f +77 454e65740400000000001c7c00000000000001004d00000002004275746c000003100000000000002300cc0121453a7ffffffd01073488090005000000002102010908000000000000000005ee +77 454e65740400000000001c7c00000000000001004d00000002004275746c000004100000000000002300cc0121453a7ffffffd0107348809000600000000210201090900630000000000000653 +77 454e65740400000000001c7c00000000000001004d00000002004275746c000007100000000000002300cc0121453a7ffffffd0107348809000700000000210201090a000000000000000005f2 +77 454e65740400000000001c7c00000000000001004d00000002004275746c000008100000000000002300cc0121453a7ffffffd0107348809000800000000210201090b00ec00000000000006e0 +77 454e65740400000000001c7c00000000000001004d00000002004275746c00000a100000000000002300cc0121453a7ffffffd0107348809000900000000210201090c000000000000000005f6 +77 454e65740400000000001c7c00000000000001004d00000002004275746c00000c100000000000002300cc0121453a7ffffffd0107348809000a00000000210201090d005c00081536000006a7 +77 454e65740400000000001c7c00000000000001004d00000002004275746c00000e100000000000002300cc0121453a7ffffffd0107348809000b00000000210201090e00000000057c0000067b +77 454e65740400000000001c7c00000000000001004d00000002004275746c000010100000000000002300cc0121453a7ffffffd0107348809000c00000000210201090f000000001dbb000006d4 +77 454e65740400000000001c7c00000000000001004d00000002004275746c000012100000000000002300cc0121453a7ffffffd0107348809000d000000002102010910000000000000000005fe +69 454e65740400000000001c7c00000000000001004500000002004275746c000014100000000000001b00cc0119453a7ffffffd0107346701250e00000000210403017f065e +69 454e65740400000000001c7c00000000000001004500000002004275746c000016100000000000001b00cc0119453a7ffffffd0899500026c90f00000000210403010106f8 +69 454e65740400000000001c7c00000000000001004500000002004275746c000019100000000000001b00cc0119453a7ffffffd0107348809001000000000210403017f0664 +65 454e65740400000000001c7c00000000000001004100000002004275746c01001f100000000000001700fefefefefefffffffdf5fdeff7ddf7ddd5f775f5d7ffdf +66 454e65740400000000001c7c00000000000001004200000002004275746c010021100000000000001800fefefefffffffcfdddfd7ffbffb5f5aeae7bffdf7f75ffff +66 454e65740400000000001c7c00000000000001004200000002004275746c010023100000000000001800fefefffffdfffff7ba357fd7ddf75d55bbdff7dfffef9ddf +67 454e65740400000000001c7c00000000000001004300000002004275746c010025100000000000001900feffffffffffffffabd5353dff5fabf7ddf57debd7bd75ffff +57 454e65740400000000001c7c00000000000001003900000002004275746c010028100000000000000f00fefeffffffffffffd5ffffffffdfff +59 454e65740400000000001c7c00000000000001003b00000002004275746c01002c100000000000001100aaab55af57be75efffab5deff5aed7ffff +62 454e65740400000000001c7c00000000000001003e00000002004275746c010030100000000000001400fefefefefefefefffeffabffffffffffae57efd7 +66 454e65740400000000001c7c00000000000001004200000002004275746c010032100000000000001800fefefeffffffffffffffff7d7f757ffff7bdfdeff7b5dfff +54 454e65740400000000001c7c00000000000001003600000002004275746c010034100000000000000c00fefe75fffffffbbbbbafdfff +60 454e65740400000000001c7c00000000000001003c00000002004275746c010036100000000000001200fefefefeffff5fffdff57d7ff7ff3fddedd7 +54 454e65740400000000001c7c00000000000001003600000002004275746c010038100000000000000c00fefefffff5fffbfbfbffffd7 +58 454e65740400000000001c7c00000000000001003a00000002004275746c01003e100000000000001000fefefefffdff5ffff7f7fddf7f57efd7 +66 454e65740400000000001c7c00000000000001004200000002004275746c010042100000000000001800fefefefefffffffffffffffdff75ff57f7fddf5f7357efd7 +56 454e65740400000000001c7c00000000000001003800000002004275746c010044100000000000000e00fff57dfdfffffddf7dfd5fff7fff +61 454e65740400000000001c7c00000000000001003d00000002004275746c010046100000000000001300fefefefffffffdb7fdf375f5fffdfbbf57efd7 +60 454e65740400000000001c7c00000000000001003c00000002004275746c010048100000000000001200fefefefefeffffffeed7ffbfbfffcd777bd7 +53 454e65740400000000001c7c00000000000001003500000002004275746c01004c100000000000000b00fefefeffffffffffb5dfff +64 454e65740400000000001c7c00000000000001004000000002004275746c010051100000000000001600fefefefefefefffffffff777f777f7ff7f55ae57efd7 +70 454e65740400000000001c7c00000000000001004600000002004275746c010052100000000000001c00fefefefefefefeffffffffbeffffbfd7bef7badfabfdeb55ae57efd7 +53 454e65740400000000001c7c00000000000001003500000002004275746c010058100000000000000b00febfffbfbfffffffbff5fd +50 454e65740400000000001c7c00000000000001003200000002004275746c01005c100000000000000800fefdfffffffffffd +59 454e65740400000000001c7c00000000000001003b00000002004275746c01005f100000000000001100aaab55af57be75ef77ab55af75ae57ebd7 +70 454e65740400000000001c7c00000000000001004600000002004275746c0000a7100000000000001c00cc011a453a7ffffffd01073467012557000000001100020200000615 +87 454e65740400000000001c7c00000000000001005700000002004275746c0000aa100000000000002d00cc012b453a7ffffffd01073467012558000000002100601301000103050900010001000201010065000017073b +59 454e65740400000000001c7c00000000000001003b00000002004275746c0100be100000000000001100aaab55af57be75aaddab5daa55ae57efd7 +70 454e65740400000000001c7c00000000000001004600000002004275746c000006110000000000001c00cc011a453a7ffffffd0107348809008800000000110002020000064a +87 454e65740400000000001c7c00000000000001005700000002004275746c000008110000000000002d00cc012b453a7ffffffd010734880900890000000021006013010002000509000100010001010100700000110772 +70 454e65740400000000001c7c00000000000001004600000002004275746c0000b6110000000000001c00cc011a453a7ffffffd0899500026c9df0000000011000202000007b4 +87 454e65740400000000001c7c00000000000001005700000002004275746c0000b7110000000000002d00cc012b453a7ffffffd0899500026c9e0000000002100601301000100010001000905002000000001000004087c +70 454e65740400000000001c7c00000000000001004600000002004275746c000006120000000000001c00cc011a453a7ffffffd01073467012508000000002100f00200650729 +69 454e65740400000000001c7c00000000000001004500000002004275746c000008120000000000001b00cc0119453a7ffffffd010734670125090000000021801001010664 +69 454e65740400000000001c7c00000000000001004500000002004275746c00000b120000000000001b00cc0119453a7ffffffd0107346701250a00000000218011013c06a1 +69 454e65740400000000001c7c00000000000001004500000002004275746c00000d120000000000001b00cc0119453a7fbffffd0107346701250b0000000021801301010669 +69 454e65740400000000001c7c00000000000001004500000002004275746c00000d120000000000001b00cc0119453a7ffffffd0107346701250b0000000021801301010669 +69 454e65740400000000001c7c00000000000001004500000002004275746c000010120000000000001b00cc0119453a7ffffffd0107346701250c0000000021802001000676 +69 454e65740400000000001c7c00000000000001004500000002004275746c000011120000000000001b00cc0119453a7ffffffd0107346701250d0000000021802101010679 +69 454e65740400000000001c7c00000000000001004500000002004275746c000013120000000000001b00cc0119453a7ffffffd0107346701250e000000002180220101067b +69 454e65740400000000001c7c00000000000001004500000002004275746c000015120000000000001b00cc0119453a7ffffffd0107346701250f000000002180230100067c +69 454e65740400000000001c7c00000000000001004500000002004275746c000017120000000000001b00cc0119453a7ffffffd01073467012510000000002180400101069b +69 454e65740400000000001c7c00000000000001004500000002004275746c000019120000000000001b00cc0119453a7ffffffd010734670125110000000021804101fe079a +69 454e65740400000000001c7c00000000000001004500000002004275746c00001b120000000000001b00cc0119453a7ffffffd010734670125120000000021804201ff079d +69 454e65740400000000001c7c00000000000001004500000002004275746c00001d120000000000001b00cc0119453a7ffffffd010734670125130000000021804301ff079f +69 454e65740400000000001c7c00000000000001004500000002004275746c00001f120000000000001b00cc0119453a7ffffffd0107346701251400000000218050010106af +69 454e65740400000000001c7c00000000000001004500000002004275746c000021120000000000001b00cc0119453a7ffffffd0107346701251500000000218051010006b0 +69 454e65740400000000001c7c00000000000001004500000002004275746c000023120000000000001b00cc0119453a7ffffffd0107346701251600000000218052010106b3 +69 454e65740400000000001c7c00000000000001004500000002004275746c000025120000000000001b00cc0119453a7ffffffd0107346701251700000000218053010006b4 +71 454e65740400000000001c7c00000000000001004700000002004275746c000027120000000000001d00cc011b453a7ffffffd0107346701251800000000218060030933470749 +72 454e65740400000000001c7c00000000000001004800000002004275746c000029120000000000001e00cc011c453a7ffffffd0107346701251900000000218061040000a36507d2 +69 454e65740400000000001c7c00000000000001004500000002004275746c00002b120000000000001b00cc0119453a7ffffffd0107346701251a00000000218062017f0745 +70 454e65740400000000001c7c00000000000001004600000002004275746c00002e120000000000001c00cc011a453a7ffffffd0107346701251b0000000021806302050306d2 +70 454e65740400000000001c7c00000000000001004600000002004275746c000030120000000000001c00cc011a453a7ffffffd0107346701251c0000000021806402000006cc +72 454e65740400000000001c7c00000000000001004800000002004275746c000031120000000000001e00cc011c453a7ffffffd0107346701251d00000000218065040000000006d2 +69 454e65740400000000001c7c00000000000001004500000002004275746c000033120000000000001b00cc0119453a7ffffffd0107346701251e00000000218070010006d8 +69 454e65740400000000001c7c00000000000001004500000002004275746c000035120000000000001b00cc0119453a7ffffffd0107346701251f00000000218071010006da +69 454e65740400000000001c7c00000000000001004500000002004275746c000037120000000000001b00cc0119453a7ffffffd0107346701252000000000218072010006dc +69 454e65740400000000001c7c00000000000001004500000002004275746c000039120000000000001b00cc0119453a7ffffffd0107346701252100000000218073010006de +69 454e65740400000000001c7c00000000000001004500000002004275746c00003b120000000000001b00cc0119453a7ffffffd0107346701252200000000218074010006e0 +71 454e65740400000000001c7c00000000000001004700000002004275746c00003d120000000000001d00cc011b453a7ffffffd010734670125230000000021808003000069075a +72 454e65740400000000001c7c00000000000001004800000002004275746c000040120000000000001e00cc011c453a7ffffffd0107346701252400000000218081040009c587084a +69 454e65740400000000001c7c00000000000001004500000002004275746c000041120000000000001b00cc0119453a7ffffffd0107346701252500000000218082011b070c +70 454e65740400000000001c7c00000000000001004600000002004275746c000043120000000000001c00cc011a453a7ffffffd01073467012526000000002180830208e507e2 +70 454e65740400000000001c7c00000000000001004600000002004275746c000045120000000000001c00cc011a453a7ffffffd010734670125270000000021808402000b0702 +72 454e65740400000000001c7c00000000000001004800000002004275746c000047120000000000001e00cc011c453a7ffffffd010734670125280000000021808504000000040701 +72 454e65740400000000001c7c00000000000001004800000002004275746c000049120000000000001e00cc011c453a7ffffffd010734670125290000000021808704000000190719 +69 454e65740400000000001c7c00000000000001004500000002004275746c00004b120000000000001b00cc0119453a7ffffffd0107346701252a0000000021809001320736 +77 454e65740400000000001c7c00000000000001004d00000002004275746c00004d120000000000002300cc0121453a7ffffffd0107346701252b0000000021020109000280000000000000068a +77 454e65740400000000001c7c00000000000001004d00000002004275746c00004f120000000000002300cc0121453a7ffffffd0107346701252c000000002102010901001b0000000000000625 +77 454e65740400000000001c7c00000000000001004d00000002004275746c000051120000000000002300cc0121453a7ffffffd0107346701252d00000000210201090208dd08bc0940000007fe +77 454e65740400000000001c7c00000000000001004d00000002004275746c000053120000000000002300cc0121453a7ffffffd0107346701252e0000000021020109030010000505c9000006f1 +77 454e65740400000000001c7c00000000000001004d00000002004275746c000055120000000000002300cc0121453a7ffffffd0107346701252f00000000210201090400320000000000000642 +77 454e65740400000000001c7c00000000000001004d00000002004275746c000057120000000000002300cc0121453a7ffffffd0107346701253000000000210201090500040004087f000006a1 +77 454e65740400000000001c7c00000000000001004d00000002004275746c000059120000000000002300cc0121453a7ffffffd0107346701253100000000210201090600000000000000000614 +77 454e65740400000000001c7c00000000000001004d00000002004275746c00005b120000000000002300cc0121453a7ffffffd010734670125320000000021020109070019000b0d2000000667 +77 454e65740400000000001c7c00000000000001004d00000002004275746c00005d120000000000002300cc0121453a7ffffffd0107346701253300000000210201090800000000000000000618 +77 454e65740400000000001c7c00000000000001004d00000002004275746c00005e120000000000002300cc0121453a7ffffffd010734670125340000000021020109090010000000000000062a +77 454e65740400000000001c7c00000000000001004d00000002004275746c000060120000000000002300cc0121453a7ffffffd0107346701253500000000210201090a0000000000000000061c +77 454e65740400000000001c7c00000000000001004d00000002004275746c000062120000000000002300cc0121453a7ffffffd0107346701253600000000210201090b00290000000000000647 +77 454e65740400000000001c7c00000000000001004d00000002004275746c000064120000000000002300cc0121453a7ffffffd0107346701253700000000210201090c007f000000000000069f +77 454e65740400000000001c7c00000000000001004d00000002004275746c000066120000000000002300cc0121453a7ffffffd0107346701253800000000210201090d04fa002a163200000792 +77 454e65740400000000001c7c00000000000001004d00000002004275746c000068120000000000002300cc0121453a7ffffffd0107346701253900000000210201090e0000000000c7000006eb +77 454e65740400000000001c7c00000000000001004d00000002004275746c00006a120000000000002300cc0121453a7ffffffd0107346701253a00000000210201090f00000000037e000006a7 +77 454e65740400000000001c7c00000000000001004d00000002004275746c00006c120000000000002300cc0121453a7ffffffd0107346701253b00000000210201091000000000000000000628 +77 454e65740400000000001c7c00000000000001004d00000002004275746c00006e120000000000002300cc0121453a7ffffffd0107346701253c0000000021020109110012000000000000063c +77 454e65740400000000001c7c00000000000001004d00000002004275746c000070120000000000002300cc0121453a7ffffffd0107346701253d000000002102010912007f00000000000006ab +77 454e65740400000000001c7c00000000000001004d00000002004275746c000073120000000000002300cc0121453a7ffffffd0107346701253e000000002102010913051c0044163b000006e4 +77 454e65740400000000001c7c00000000000001004d00000002004275746c000074120000000000002300cc0121453a7ffffffd0107346701253f00000000210201091400000000001500000645 +77 454e65740400000000001c7c00000000000001004d00000002004275746c000076120000000000002300cc0121453a7ffffffd0107346701254000000000210201091500000000004200000674 +77 454e65740400000000001c7c00000000000001004d00000002004275746c000078120000000000002300cc0121453a7ffffffd0107346701254100000000210201091600000000000000000634 +70 454e65740400000000001c7c00000000000001004600000002004275746c00007a120000000000001c00cc011a453a7ffffffd0899500026c942000000002100f00200010816 +77 454e65740400000000001c7c00000000000001004d00000002004275746c00007c120000000000002300cc0121453a7ffffffd0899500026c9430000000021020109000ce40000000000000827 +77 454e65740400000000001c7c00000000000001004d00000002004275746c00007f120000000000002300cc0121453a7ffffffd0899500026c94400000000210201090109450000000000000787 +77 454e65740400000000001c7c00000000000001004d00000002004275746c000080120000000000002300cc0121453a7ffffffd0899500026c94500000000210201090200180000000000000753 +77 454e65740400000000001c7c00000000000001004d00000002004275746c000082120000000000002300cc0121453a7ffffffd0899500026c94600000000210201090320dd000000000000083a diff --git a/samples/rdm_pulse_101.hex b/samples/rdm_pulse_101.hex new file mode 100644 index 0000000..e0a4d08 --- /dev/null +++ b/samples/rdm_pulse_101.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001003b00000002004275746c01000b0f0000000000001100aaaa5dbbddfa55aa55ae77ebddaf57fadf \ No newline at end of file diff --git a/samples/rdm_pulse_101_0.hex b/samples/rdm_pulse_101_0.hex new file mode 100644 index 0000000..e0a4d08 --- /dev/null +++ b/samples/rdm_pulse_101_0.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001003b00000002004275746c01000b0f0000000000001100aaaa5dbbddfa55aa55ae77ebddaf57fadf \ No newline at end of file diff --git a/samples/rdm_pulse_101_1.hex b/samples/rdm_pulse_101_1.hex new file mode 100644 index 0000000..a103f91 --- /dev/null +++ b/samples/rdm_pulse_101_1.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001003b00000002004275746c01005f100000000000001100aaab55af57be75ef77ab55af75ae57ebd7 \ No newline at end of file diff --git a/samples/rdm_pulse_101_2.hex b/samples/rdm_pulse_101_2.hex new file mode 100644 index 0000000..911a5fb --- /dev/null +++ b/samples/rdm_pulse_101_2.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001003b00000002004275746c0100be100000000000001100aaab55af57be75aaddab5daa55ae57efd7 \ No newline at end of file diff --git a/samples/rdm_pulse_101_3.hex b/samples/rdm_pulse_101_3.hex new file mode 100644 index 0000000..e89032c --- /dev/null +++ b/samples/rdm_pulse_101_3.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001003b00000002004275746c0100790f0000000000001100aaff7fffffaa5feaf5bad7ab7dab57ab77 \ No newline at end of file diff --git a/samples/rdm_pulse_101_4.hex b/samples/rdm_pulse_101_4.hex new file mode 100644 index 0000000..f1a1f3d --- /dev/null +++ b/samples/rdm_pulse_101_4.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001003b00000002004275746c01002c100000000000001100aaab55af57be75efffab5deff5aed7ffff \ No newline at end of file diff --git a/samples/rdm_req_1126.hex b/samples/rdm_req_1126.hex new file mode 100644 index 0000000..95dfb42 --- /dev/null +++ b/samples/rdm_req_1126.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001003c040000010000000000000002007f0f000000000000cc00000001000100000025000000010000000124ffffffffffff453a7ffffffccc010000001000010c80007ffffffffffffffffffe15c291908f8e8d8c8b8a898887868584838281807f7e7d7c7b7a797877767574737271706f6e6d6c6b6a696867666564636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0efeeedecebeae9e8e7e6e5e4e3e2e1e0dfdedddcdbdad9d8d7d6d5d4d3d2d1d0cfcecdcccbcac9c8c7c6c5c4c3c2c1c0bfbebdbcbbbab9b8b7b6b5b4b3b2b1b0afaeadacabaaa9a8a7a6a5a4a3a2a1a09f9e9d9c9b9a999897969594939291908f8e8d8c8b8a898887868584838281807f7e7d7c7b7a797877767574737271706f6e6d6c6b6a696867666564636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a0908070605040302014a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a090807060504030201c0bfbebdbcbbbab9b8b7b6b5b4b3b2b1b0afaeadacabaaa9a8a7a6a5a4a3a2a1a09f9e9d9c9b9a999897969594939291908f8e8d8c8b8a898887868584838281807f7e7d7c7b7a797877767574737271706f6e6d6c6b6a696867666564636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0efeeedecebeae9e8e7e6e5e4e3e2e1e0dfdedddcdbdad9d8d7d6d5d4d3d2d1d0cfcecdcccbcac9c8c7c6c5c4c3c2c1c0bfbebdbcbbbab9b8b7b6b5b4b3b2b1b0afaeadacabaaa9a8a7a6a5a4a3a2a1a09f9e9d9c9b9a999897969594939291908f8e8d8c8b8a898887868584838281807f7e7d7c7b7a797877767574737271706f6e6d6c6b6a696867666564636261605f5e5d5c5b5a595857565554535251504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a090807060504030201403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a090807060504030201 \ No newline at end of file diff --git a/samples/rdm_resp_111.hex b/samples/rdm_resp_111.hex new file mode 100644 index 0000000..d4e6827 --- /dev/null +++ b/samples/rdm_resp_111.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001004500000002004275746c0000b70f0000000000001b00cc0119453a7ffffffd010734880900dd00000000218011013c0778 \ No newline at end of file diff --git a/samples/rdm_setack_68.hex b/samples/rdm_setack_68.hex new file mode 100644 index 0000000..22a9f67 --- /dev/null +++ b/samples/rdm_setack_68.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001004400000002004275746c00006efa0600000000001a00cc0118453a7fffffff010734880900b1000000003100f000077f \ No newline at end of file diff --git a/samples/register_89.hex b/samples/register_89.hex new file mode 100644 index 0000000..cbde05c --- /dev/null +++ b/samples/register_89.hex @@ -0,0 +1 @@ +454e65740200000000001c7c00000000000001002f0000000200017ba8c000161cf12e3900717ba8c0300ed559bb36 \ No newline at end of file diff --git a/samples/status_84.hex b/samples/status_84.hex new file mode 100644 index 0000000..b60121c --- /dev/null +++ b/samples/status_84.hex @@ -0,0 +1 @@ +454e65740400000000001c7c00000000000001002a00000002004275746c010062000000010000000000 diff --git a/src/aech.rs b/src/aech.rs new file mode 100644 index 0000000..b7a8148 --- /dev/null +++ b/src/aech.rs @@ -0,0 +1,1226 @@ +//! AECH 密钥协商密码学分析 (aech 子命令) +//! +//! 已知结构 (§3.7): 主机发 552B sub=05 "AECH" 质询 (0x2c-0x8b 96B + 0x90-0x123 148B +//! 随机 blob, 共 244B), 真机回 552B sub=06 "AERE", 跨轮唯一变化 = 0xC0-0xC3 (4B)。 +//! 核心问题: resp32 = f(challenge, 设备密钥) 是什么函数? +//! +//! 分析策略 (对全部质询/应答对): +//! A. 无密钥直接命中: CRC32(任意切片) / MD5/SHA1(任意切片) 截断 / 简单和 +//! B. 密钥化 CRC 仿射测试: 定长消息下 resp = CRC32_k(slice) = CRC32_0(slice) ^ Δ(len) +//! → 对每个切片起点/长度, 若 resp^CRC32_0(slice) 跨全部样本为常数 Δ 则命中 +//! (Δ 吸收 init/xorout 密钥, 无需事先知道密钥即可判定+恢复有效密钥) +//! C. XOR 常量测试: resp32 ^ chal32@off 跨样本恒定 → 异或掩码方案 +//! D. K 相关: 同一 TSV 的 DMX 保护字节导出会话 K, 与 resp32 字节比对 + +use crate::dmxhash::{crc32, h2b, md5, read_tsv, sha1}; + +// ---------- dmxsend: 主机侧等效播放 (认领 + 加密 DMX, 全协议自证, 2026-09-10) ---------- +// 流程: sub=01 宣告 → 收 89B 注册应答 → AECH 认领 (nonce/B12 自选) → 验证 AERE 0xC0==G(nonce) +// → 30fps 发送 zPprv0TXRRW 加密 DMX (可识别图案), 由接收端日志验证解密正确性 +pub fn dmxsend(target: &str, secs: u64) { + use std::net::UdpSocket; + use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + let sock = UdpSocket::bind("0.0.0.0:0").expect("bind"); + sock.set_read_timeout(Some(Duration::from_millis(400))).unwrap(); + let dst = format!("{}:31772", target); + let mut buf = [0u8; 2048]; + + // 简易 xorshift32 (避免外部依赖) + let mut rng: u32 = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().subsec_nanos() | 1; + let mut next_r = move || { rng ^= rng << 13; rng ^= rng >> 17; rng ^= rng << 5; rng & 0x7FFF_FFFF }; + + // --- 1) sub=01 宣告 → 89B 注册应答 --- + let ann = crate::enet::load_template("announce144"); + sock.send_to(&ann, &dst).unwrap(); + let mut reg_ok = false; + for _ in 0..6 { + if let Ok((n, _)) = sock.recv_from(&mut buf) { + if n == 89 || (n >= 8 && n < 200 && buf[4..8] == [2, 0, 0, 0]) { reg_ok = true; break; } + } else { break; } + } + println!("1) 宣告 sub=01 → 注册应答: {}", if reg_ok { "OK (89B)" } else { "未收到 (不阻塞)" }); + + // --- 2) AECH 认领: nonce/B12 自选, 验证 AERE 0xC0-0xC3 == G(nonce) --- + let nonce: u32 = next_r(); + let b12: u32 = next_r(); + let mut ch = crate::enet::load_template("claim_h112"); + ch[0x88..0x8C].copy_from_slice(&nonce.to_le_bytes()); + ch[0x8C..0x90].copy_from_slice(&b12.to_le_bytes()); + let mut claim_ok = false; + for attempt in 0..3 { + sock.send_to(&ch, &dst).unwrap(); + for _ in 0..6 { + if let Ok((n, src)) = sock.recv_from(&mut buf) { + let b = &buf[..n]; + if n > 0xC4 && b.windows(4).any(|w| w == b"AERE") { + let resp = u32::from_le_bytes([b[0xC0], b[0xC1], b[0xC2], b[0xC3]]); + let want = agios_g(nonce); + claim_ok = resp == want; + println!("2) AECH 认领 (第{}轮): AERE from {} resp=0x{:08x} G(nonce)=0x{:08x} {}", + attempt + 1, src, resp, want, if claim_ok { "★PASS" } else { "FAIL" }); + break; + } + } else { break; } + } + if claim_ok { break; } + } + if !claim_ok { println!("认领失败, 终止"); return; } + println!(" B12=0x{:08x} nonce=0x{:08x}", b12, nonce); + + // --- 3) DMX 加密发送: 30fps, 高识别度图案 --- + let tpl = crate::enet::load_template("dmx_a"); + let mut p1 = [0u8; 512]; + p1[0] = 0xAA; p1[1] = 0x55; + for i in 0..8 { p1[2 + i] = 1u8 << i; } + p1[100] = 0xEF; // ch101 + let mut p2 = [0u8; 512]; + p2[0] = 0x77; p2[511] = 0x33; // ch513 / ch1024 + let expect = "aa550102040810204080"; // 接收端解密后 CH1-10 应为 + println!("3) DMX 发送 {}s @30fps: port1 ch1=AA ch2=55 ch3-10=1..128 ch101=EF; port2 ch513=77 ch1024=33", secs); + println!(" 期望接收端日志 CH1-16 以 {} 开头", expect); + + let start = Instant::now(); + let mut seq: u32 = 1; + let mut sent = 0u64; + while start.elapsed() < Duration::from_secs(secs) { + let r1 = next_r(); + let r2 = next_r(); + let mut f = tpl.clone(); + f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes()); + f[0x22..0x26].copy_from_slice(&r1.to_le_bytes()); + f[0x26..0x2A].copy_from_slice(&r2.to_le_bytes()); + let e1 = agios_dmx_encrypt(&p1, r1, b12); + let e2 = agios_dmx_encrypt(&p2, r2, b12); + f[0x3C..0x23C].copy_from_slice(&e1); + f[0x23C..0x43C].copy_from_slice(&e2); + if sock.send_to(&f, &dst).is_ok() { sent += 1; } + seq += 1; + std::thread::sleep(Duration::from_millis(33)); + } + println!("完成: 发送 {} 帧", sent); +} + +// ---------- AgiosElias: AECH 应答算法 (源自 SymphoLight Ecue.Devices.dll 反编译) ---------- +// 主机验证逻辑 (AgiosElias.i4GrvIBQ6xF): 检查 AERE blob[152] == G(主机 nonce) +// 质询结构 (kxKrvvdOGhh 生成): "AECH" + 92B 随机 + 4B nonce(LE)@0x88 + 4B 实例(LE)@0x8C + 148B 随机 +// G(x) = x ^ TABLE[(x%32 + 1 + (x*13)%30) % 32] — 无密钥, 表内嵌于 DLL (0A8F2B10... 静态数组) +pub const AGIOS_TABLE: [u32; 32] = [ + 0x1008de00, 0xe3b77ff6, 0x31183e1c, 0x90fb53c8, 0xff10ba5c, 0x030bc4b6, 0xe22935c8, 0x10e8ede1, + 0x55f409a0, 0x3eb0b9a3, 0xf7333308, 0xedf07810, 0x8abf4078, 0x60ec0545, 0x1f4645c8, 0x54094148, + 0x04c0f500, 0xfabe50bb, 0xf3f3e54c, 0x6c2a02cb, 0xa2546e60, 0x4f6ae10e, 0x51dfe8d8, 0x2eedb0ec, + 0x74006928, 0x48fade70, 0xe5346e9c, 0x1c3fe0bc, 0xf6700700, 0x6b7f1ba0, 0xa1143cf4, 0x3be8426c, +]; + +pub fn agios_g(x: u32) -> u32 { + let n = 32u32; + let idx = (x % n + 1 + (x.wrapping_mul(13)) % (n - 2)) % n; + x ^ AGIOS_TABLE[idx as usize] +} + +// ---------- AgiosElias::zPprv0TXRRW — DMX 端口数据逐帧混淆 (旧算法, Butler S2 使用) ---------- +// 反编译 + 数据实证 (2026-09-10, cold2 3893 帧 + fix1 1781 帧全部解密为稳定全零场景): +// rot = (B12 ^ r) & 511 (B12 = AECH 质询 0x8C-0x8F 实例随机, 会话不变; +// r = 帧内 0x22-0x25/0x26-0x29 LE u32, Random.Next() 每帧每端口) +// 加密: out[i] = src[(i+rot)%512] ^ cnt[i] +// cnt[i] = (512-rot-i)&255 (i < 512-rot) +// = (rot-(i-(512-rot)))&255 (i >= 512-rot) +/// zPprv0TXRRW 加密 (主机侧): src 512B → wire 512B +pub fn agios_dmx_encrypt(src: &[u8], r: u32, b12: u32) -> Vec { + let rot = ((b12 ^ r) & 511) as usize; + (0..512).map(|i| { + let cnt = if i < 512 - rot { (512 - rot - i) & 0xFF } else { (rot - (i - (512 - rot))) & 0xFF }; + src[(i + rot) % 512] ^ cnt as u8 + }).collect() +} +/// zPprv0TXRRW 解密 (接收方视角): wire 512B → src 512B +pub fn agios_dmx_decrypt(wire: &[u8], r: u32, b12: u32) -> Vec { + let rot = ((b12 ^ r) & 511) as usize; + let mut src = vec![0u8; 512]; + for i in 0..512 { + let cnt = if i < 512 - rot { (512 - rot - i) & 0xFF } else { (rot - (i - (512 - rot))) & 0xFF }; + src[(i + rot) % 512] = wire[i] ^ cnt as u8; + } + src +} + +// ---------- dmxdec: DMX 帧加密假说验证 (2026-09-10 反编译突破) ---------- +// 反编译实证: iey7k4rDmrsMD1TvRywm::ScheduleUpdate 是唯一 1084B 帧构建者; +// 帧 0x22-0x25 = BBFrDpcLsuZ[0] = EncryptFrameData 返回值 r1 (Random.Next(), MSB=0 已实证), +// 0x26-0x29 = r2。若走加密路径 (jTErDRqkhb1()==true), 通道数据经: +// kWarv47o79k (新): out[i] = src[i] ^ ((512-i)&255) ^ key[(512-i)&3], key = r ^ B12rvMuR8pT +// zPprv0TXRRW (旧): out[i] = src[(i+rot)%512] ^ cnt[i], rot = (B12 ^ r) & 511 +// B12rvMuR8pT = AgiosElias 实例随机 = AECH 质询 0x8C-0x8F (会话内不变)。 +// 验证: 用 r (帧内 0x22-0x25) + B12 (TSV 内 AECH 质询) 解密, 若明文跨帧稳定/稀疏 → 假说成立。 +pub fn dmxdec(tsv: &str) { + let content = read_tsv(tsv); + let mut b12: Option = None; + let mut frames: Vec> = Vec::new(); + for line in content.lines() { + let p: Vec<&str> = line.split('\t').collect(); + if p.len() < 5 || p[4].is_empty() { continue; } + let b = match h2b(p[4]) { Some(b) => b, None => continue }; + if b.len() == 552 && &b[0x28..0x2C] == b"AECH" && b12.is_none() { + b12 = Some(u32::from_le_bytes([b[0x8C], b[0x8D], b[0x8E], b[0x8F]])); + } + if b.len() == 1084 && b[0x20] == 0x01 && b[0x21] == 0x01 { + frames.push(b); + } + } + let b12 = match b12 { Some(v) => v, None => { println!("TSV 内无 AECH 质询, 无法取 B12"); return; } }; + if frames.is_empty() { println!("无 DMX 帧"); return; } + println!("B12 = 0x{:08x}, DMX 帧 {} 个", b12, frames.len()); + + // --- kWarv47o79k 逆运算 (XOR 自逆): src[i] = out[i] ^ ((512-i)&255) ^ key[(512-i)&3] --- + let dec_new = |b: &Vec, off: usize, r: u32| -> Vec { + let key = r ^ b12; + let kb = key.to_le_bytes(); + (0..512).map(|i| { + let j = 512 - i; + b[off + i] ^ (j & 0xFF) as u8 ^ kb[j & 3] + }).collect() + }; + // --- zPprv0TXRRW 逆运算 (共享实现, 见 agios_dmx_decrypt) --- + let dec_old = |b: &Vec, off: usize, r: u32| -> Vec { + agios_dmx_decrypt(&b[off..off + 512], r, b12) + }; + + for (name, is_new) in [("kWarv47o79k(新)", true), ("zPprv0TXRRW(旧)", false)] { + // 解密前 3 帧端口1, 看明文是否稳定/稀疏 + let mut stable = 0; + let mut prev: Option> = None; + let mut first_plain: Vec = Vec::new(); + for (fi, f) in frames.iter().take(50).enumerate() { + let r1 = u32::from_le_bytes([f[0x22], f[0x23], f[0x24], f[0x25]]); + let plain = if is_new { dec_new(f, 0x3C, r1) } else { dec_old(f, 0x3C, r1) }; + if fi == 0 { first_plain = plain.clone(); } + if let Some(p) = &prev { + if *p == plain { stable += 1; } + } + prev = Some(plain); + } + let nonzero = first_plain.iter().filter(|&&v| v != 0).count(); + println!("\n{}: 相邻帧明文相同 {}/49, 首帧明文非零字节 {}/512", name, stable, nonzero); + print!(" 首帧明文 CH1-32: "); + for v in first_plain.iter().take(32) { print!("{:02x}", v); } + println!(); + } + + // --- 全捕获扫描: 双端口解密, 统计非零明文帧 (用户是否发过真实场景) --- + // --- 同时做 encrypt∘decrypt 回环验证 (主机侧复现的正确性) --- + let mut nz_frames = 0; + let mut roundtrip_ok = 0usize; + let mut sample: Option<(usize, Vec, Vec)> = None; + for (fi, f) in frames.iter().enumerate() { + let r1 = u32::from_le_bytes([f[0x22], f[0x23], f[0x24], f[0x25]]); + let r2 = u32::from_le_bytes([f[0x26], f[0x27], f[0x28], f[0x29]]); + let p1 = agios_dmx_decrypt(&f[0x3C..0x3C + 512], r1, b12); + let p2 = agios_dmx_decrypt(&f[0x23C..0x23C + 512], r2, b12); + if agios_dmx_encrypt(&p1, r1, b12) == f[0x3C..0x3C + 512] + && agios_dmx_encrypt(&p2, r2, b12) == f[0x23C..0x23C + 512] + { + roundtrip_ok += 1; + } + let nz = p1.iter().chain(p2.iter()).filter(|&&v| v != 0).count(); + if nz > 0 { + nz_frames += 1; + if sample.is_none() { sample = Some((fi, p1, p2)); } + } + } + println!("\n全捕获双端口解密: 非零明文帧 {}/{}, 加解密回环 {}/{}", nz_frames, frames.len(), roundtrip_ok, frames.len()); + if let Some((fi, p1, p2)) = sample { + let show = |v: &Vec| -> String { + v.iter().enumerate().filter(|(_, &x)| x != 0).take(16) + .map(|(i, &x)| format!("ch{}={}", i + 1, x)).collect::>().join(" ") + }; + println!(" 首个非零帧 #{}: port1[{}] port2[{}]", fi, show(&p1), show(&p2)); + } +} + +// ---------- dmxroll: DMX 帧滚动值/保护字节数据驱动分析 ---------- +// 帧布局 (§4.1): seq@0x1A LE u16, guard1@0x22=(CH1-1)^K, roll1@0x23-0x25, +// guard2@0x26=(CH513-1)^K, roll2@0x27-0x29, node@0x2E +// 检验: roll 是否 = G(seq^K) 之类的公开函数; roll1/roll2 关系; 跨帧步进规律 +pub fn dmxroll(tsv: &str) { + let content = read_tsv(tsv); + let mut frames: Vec<(u16, u8, [u8; 3], u8, [u8; 3], u8, u8)> = Vec::new(); // seq,g1,r1,g2,r2,node,ch1 + for line in content.lines() { + let p: Vec<&str> = line.split('\t').collect(); + if p.len() < 5 || p[4].is_empty() { continue; } + let b = match h2b(p[4]) { Some(b) => b, None => continue }; + if b.len() != 1084 || b[0x20] != 0x01 || b[0x21] != 0x01 { continue; } + let seq = u16::from_le_bytes([b[0x1A], b[0x1B]]); + frames.push((seq, b[0x22], [b[0x23], b[0x24], b[0x25]], b[0x26], [b[0x27], b[0x28], b[0x29]], b[0x2E], b[0x3C])); + } + if frames.is_empty() { println!("无 DMX 帧"); return; } + println!("DMX 帧 {} 个", frames.len()); + + // K 导出 (guard1 ^ (CH1-1)) + let k = frames[0].1 ^ frames[0].6.wrapping_sub(1); + println!("会话 K = 0x{:02x}", k); + + // roll1 vs roll2 关系 + let eq = frames.iter().filter(|f| f.2 == f.4).count(); + println!("roll1==roll2: {}/{}", eq, frames.len()); + + // roll 是否随 seq 确定性变化: 同 seq 是否同 roll + let mut by_seq: std::collections::HashMap> = std::collections::HashMap::new(); + for f in &frames { by_seq.entry(f.0).or_default().push((f.2, f.4)); } + let mut det = 0; let mut nondet = 0; + for (_, v) in &by_seq { + if v.iter().all(|x| *x == v[0]) { det += 1; } else { nondet += 1; } + } + println!("同seq同roll(确定性): {} seqs; 同seq异roll(含随机): {} seqs", det, nondet); + + // 前 16 帧明细 + println!("\nseq | g1 | roll1 | g2 | roll2 | node | G(seq)^? 对照"); + for f in frames.iter().take(16) { + let g = agios_g(f.0 as u32); + println!("{:06} | {:02x} | {:02x}{:02x}{:02x} | {:02x} | {:02x}{:02x}{:02x} | {:02x} | G(seq)={:08x} G(seq^K)={:08x}", + f.0, f.1, f.2[0], f.2[1], f.2[2], f.3, f.4[0], f.4[1], f.4[2], f.5, g, agios_g((f.0 as u32) ^ (k as u32))); + } + + // roll1 24bit 与 seq 的直接关系扫描: roll1 ^ seq / roll1 - seq 恒定? + let r1_24 = |f: &(u16, u8, [u8; 3], u8, [u8; 3], u8, u8)| -> + u32 { (f.2[0] as u32) | (f.2[1] as u32) << 8 | (f.2[2] as u32) << 16 }; + let x0 = r1_24(&frames[0]) ^ frames[0].0 as u32; + let xor_const = frames.iter().all(|f| (r1_24(f) ^ f.0 as u32) == x0); + let d0 = r1_24(&frames[0]).wrapping_sub(frames[0].0 as u32); + let sub_const = frames.iter().all(|f| r1_24(f).wrapping_sub(f.0 as u32) == d0); + println!("\nroll1^seq 恒定: {} (0x{:06x}); roll1-seq 恒定: {} (0x{:06x})", xor_const, x0, sub_const, d0); + + // roll1 逐帧差分规律 + let mut diffs: std::collections::HashMap = std::collections::HashMap::new(); + for w in frames.windows(2) { + if w[1].0 == w[0].0.wrapping_add(1) { + let d = r1_24(&w[1]).wrapping_sub(r1_24(&w[0])); + *diffs.entry(d).or_default() += 1; + } + } + let mut dv: Vec<_> = diffs.iter().collect(); + dv.sort_by_key(|(_, c)| std::cmp::Reverse(**c)); + println!("roll1 逐帧差分 top5: {:?}", dv.iter().take(5).map(|(d, c)| format!("0x{:06x}x{}", d, c)).collect::>()); + + // G(seq), G(seq)^K, G(seq+k) 等与 roll 的匹配率 + let test_fn = |name: &str, f: &dyn Fn(u16) -> u32| { + let hit = frames.iter().filter(|fr| { + let v = f(fr.0); + let lo3 = [v as u8, (v >> 8) as u8, (v >> 16) as u8]; + let hi3 = [(v >> 8) as u8, (v >> 16) as u8, (v >> 24) as u8]; + lo3 == fr.2 || hi3 == fr.2 + }).count(); + println!(" {} 匹配 roll1: {}/{}", name, hit, frames.len()); + }; + println!("\n候选函数 vs roll1:"); + test_fn("G(seq)", &|s| agios_g(s as u32)); + test_fn("G(seq^K)", &|s| agios_g((s as u32) ^ (k as u32))); + test_fn("G(seq+K)", &|s| agios_g((s as u32).wrapping_add(k as u32))); + test_fn("G(seq)^G(K)", &|s| agios_g(s as u32) ^ agios_g(k as u32)); + test_fn("(seq^K)*7", &|s| ((s as u32) ^ (k as u32)).wrapping_mul(7)); + + // --- roll2(seq) == roll1(seq+512)? (端口2 = 端口1 偏移 512) --- + let map1: std::collections::HashMap = frames.iter().map(|f| (f.0, f.2)).collect(); + let mut off_hit = 0; let mut off_tot = 0; + for f in &frames { + if let Some(r) = map1.get(&f.0.wrapping_add(512)) { + off_tot += 1; + if *r == f.4 { off_hit += 1; } + } + } + println!("\nroll2(seq) == roll1(seq+512): {}/{}", off_hit, off_tot); + + // --- PRNG 流分析 (按到达顺序; seq 不重复, "同seq同roll" 判定无效) --- + let r1s: Vec = frames.iter().map(|f| (f.2[0] as u32) | (f.2[1] as u32) << 8 | (f.2[2] as u32) << 16).collect(); + let r2s: Vec = frames.iter().map(|f| (f.4[0] as u32) | (f.4[1] as u32) << 8 | (f.4[2] as u32) << 16).collect(); + // 交叉: roll2[i] == roll1[i+1]? (共享 PRNG, 端口2 取下一个) + let cross = (0..r1s.len() - 1).filter(|&i| r2s[i] == r1s[i + 1]).count(); + println!("roll2[i]==roll1[i+1]: {}/{}", cross, r1s.len() - 1); + + // LCG mod 2^24 求解: r1 = (a*r0+c) mod 2^24 — 从 3 个连续值解 a,c 并全验证 + let solve_lcg = |v: &[u32]| { + let m = 1u64 << 24; + let mut hits = 0; + for start in 0..v.len().saturating_sub(3).min(200) { + let (r0, r1, r2) = (v[start] as u64, v[start + 1] as u64, v[start + 2] as u64); + let d1 = (r1 + m - r0) % m; // r1-r0 + let d2 = (r2 + m - r1) % m; // r2-r1 + if d1 % 2 == 0 { continue; } // 不可逆, 跳过 + // d1 的模逆 (牛顿迭代, mod 2^24) + let mut inv = d1; + for _ in 0..5 { inv = inv.wrapping_mul(2u64.wrapping_sub(d1.wrapping_mul(inv))) % m; } + let a = d2 * inv % m; + let c = (r1 + m - a * r0 % m) % m; + let mut ok = true; + let mut cur = v[start] as u64; + for j in start + 1..v.len().min(start + 500) { + cur = (a * cur + c) % m; + if cur != v[j] as u64 { ok = false; break; } + } + if ok { + println!(" ★ LCG(2^24) 命中 @{}: a=0x{:06x} c=0x{:06x}", start, a, c); + hits += 1; + if hits > 3 { return; } + } + } + if hits == 0 { println!(" LCG(2^24) 无命中"); } + }; + println!("roll1 流 LCG 测试:"); solve_lcg(&r1s); + println!("roll2 流 LCG 测试:"); solve_lcg(&r2s); + + // LCG mod 2^32 (4B 状态, roll 为低 24 位?): 用 r1s 与 r2s 交错序列试 + // 交错序列: roll1[0], roll2[0], roll1[1], roll2[1], ... + let inter: Vec = r1s.iter().zip(r2s.iter()).flat_map(|(a, b)| [*a, *b]).collect(); + println!("交错流 LCG 测试:"); solve_lcg(&inter); + + // xorshift 粗测: r[i+1] ^ r[i] 的汉明重量分布 + let hw: Vec = r1s.windows(2).map(|w| (w[1] ^ w[0]).count_ones()).collect(); + let avg = hw.iter().sum::() as f64 / hw.len() as f64; + println!("roll1 相邻 XOR 平均汉明重: {:.1} (随机期望 12.0)", avg); + + // --- K vs AECH 实例随机 (0x8C-0x8F) / 首个 nonce (0x88-0x8B) 相关测试 --- + // (仅含认领交换的 TSV 有效, 如 cold2/fix1) + let mut inst: Option = None; + let mut nonce1: Option = None; + { + let content2 = read_tsv(tsv); + for line in content2.lines() { + let p: Vec<&str> = line.split('\t').collect(); + if p.len() < 5 || p[4].is_empty() { continue; } + let b = match h2b(p[4]) { Some(b) => b, None => continue }; + if b.len() == 552 && &b[0x28..0x2C] == b"AECH" { + if inst.is_none() { inst = Some(u32::from_le_bytes([b[0x8C], b[0x8D], b[0x8E], b[0x8F]])); } + if nonce1.is_none() { nonce1 = Some(u32::from_le_bytes([b[0x88], b[0x89], b[0x8A], b[0x8B]])); } + if inst.is_some() && nonce1.is_some() { break; } + } + } + } + if let (Some(iv), Some(nv)) = (inst, nonce1) { + println!("\nAECH 实例=0x{:08x} nonce1=0x{:08x} vs K=0x{:02x}", iv, nv, k); + let tests: Vec<(&str, u32)> = vec![ + ("G(inst)", agios_g(iv)), + ("G(nonce1)", agios_g(nv)), + ("(inst^nonce1)*7", (iv ^ nv).wrapping_mul(7)), + ("inst*7", iv.wrapping_mul(7)), + ("G(inst)^G(nonce1)", agios_g(iv) ^ agios_g(nv)), + ("G(inst^nonce1)", agios_g(iv ^ nv)), + ("G(inst+nonce1)", agios_g(iv.wrapping_add(nv))), + ]; + for (name, v) in tests { + let hit = (0..4).any(|s| ((v >> (s * 8)) & 0xFF) == k as u32); + println!(" {}=0x{:08x} {}", name, v, if hit { "★ 含 K 字节!" } else { "" }); + } + } + + // --- roll1(seq) 的 GF(2) 仿射性检验 (16bit 输入空间) --- + // 仿射 iff 对任意 a,b,c: roll(a)^roll(b)^roll(c) == roll(a^b^c) + { + let keys: Vec = map1.keys().cloned().collect(); + let mut rng = 0x12345678u32; + let mut ok = 0; let mut tot = 0; + for _ in 0..2000 { + rng = rng.wrapping_mul(1664525).wrapping_add(1013904223); + let a = keys[(rng >> 8) as usize % keys.len()]; + rng = rng.wrapping_mul(1664525).wrapping_add(1013904223); + let b = keys[(rng >> 8) as usize % keys.len()]; + rng = rng.wrapping_mul(1664525).wrapping_add(1013904223); + let c = keys[(rng >> 8) as usize % keys.len()]; + if let Some(abc) = map1.get(&(a ^ b ^ c)) { + tot += 1; + let (ra, rb, rc) = (map1[&a], map1[&b], map1[&c]); + let x = [ra[0] ^ rb[0] ^ rc[0], ra[1] ^ rb[1] ^ rc[1], ra[2] ^ rb[2] ^ rc[2]]; + if x == *abc { ok += 1; } + } + } + println!("roll1(seq) 仿射性: {}/{} 组通过", ok, tot); + if tot > 0 && ok == tot { + // 解仿射映射: roll(s) = M·s ^ c, 用单比特样本解 16 列 + let zero = map1.get(&0).cloned(); + let ref_pt = keys[0]; + let c0 = map1[&ref_pt]; + let mut cols: Vec<[u8; 3]> = Vec::new(); + let mut solved = true; + for bit in 0..16 { + let want = ref_pt ^ (1u16 << bit); + match map1.get(&want) { + Some(r) => cols.push([r[0] ^ c0[0], r[1] ^ c0[1], r[2] ^ c0[2]]), + None => { solved = false; break; } + } + } + if solved { + println!(" 仿射映射已解出 (参考点 seq={})", ref_pt); + // 全样本验证 + let predict = |s: u16| -> [u8; 3] { + let d = s ^ ref_pt; + let mut r = c0; + for bit in 0..16 { + if d & (1 << bit) != 0 { + r[0] ^= cols[bit][0]; r[1] ^= cols[bit][1]; r[2] ^= cols[bit][2]; + } + } + r + }; + let hit = frames.iter().filter(|f| predict(f.0) == f.2).count(); + println!(" 全样本预测 roll1: {}/{}", hit, frames.len()); + if let Some(z) = zero { println!(" roll(0) = {:02x}{:02x}{:02x}", z[0], z[1], z[2]); } + } else { + println!(" 单比特样本不足, 未解 (捕获窗口内 seq 范围有限)"); + } + } + } +} + +// ---------- 选择密文探测 (aechprobe 子命令): 对真机做 GF(2) 仿射性分析 ---------- +// +// 原理: 若 resp32 = f(challenge) 是 GF(2) 仿射函数 (CRC 类/线性反馈类算法的共性), +// 则 f(x) = L(x) ^ c, 其中 c = f(0), L 为线性映射。用 1952 次单比特翻转查询 +// 可完整学习 L 的矩阵 → 无需密钥即可对任意质询离线计算合法应答。 +// 若不是仿射 (真 MAC), 线性验证步骤会立即暴露。 + +use std::net::UdpSocket; +use std::time::Duration; + +/// 发送一帧质询, 返回应答 0xC0-0xC3 (u32 原生字节序保持 4B 数组) +fn query(s: &UdpSocket, target: &str, frame: &[u8]) -> Option<[u8; 4]> { + for _attempt in 0..2 { + if s.send_to(frame, target).is_err() { return None; } + let mut buf = [0u8; 2048]; + match s.recv_from(&mut buf) { + Ok((n, _)) => { + let b = &buf[..n]; + if n == 552 && &b[0x28..0x2C] == b"AERE" { + return Some([b[0xC0], b[0xC1], b[0xC2], b[0xC3]]); + } + } + Err(_) => continue, + } + } + None +} + +pub fn probe(tsv: &str, target: &str) { + // 从 TSV 取首个 AECH 质询作模板 + let content = read_tsv(tsv); + let mut tmpl: Option> = None; + let mut tmpl_resp: Option<[u8; 4]> = None; + for line in content.lines() { + let p: Vec<&str> = line.split('\t').collect(); + if p.len() < 5 || p[4].is_empty() { continue; } + let b = match h2b(p[4]) { Some(b) => b, None => continue }; + if b.len() == 552 && &b[0..4] == b"ENet" && &b[0x28..0x2C] == b"AECH" && tmpl.is_none() { + tmpl = Some(b.clone()); + } + if b.len() == 552 && &b[0x28..0x2C] == b"AERE" && tmpl_resp.is_none() { + tmpl_resp = Some([b[0xC0], b[0xC1], b[0xC2], b[0xC3]]); + } + if tmpl.is_some() && tmpl_resp.is_some() { break; } + } + let tmpl = tmpl.expect("TSV 中无 AECH 质询模板"); + println!("模板质询已载入, 抓包时应答 resp32={}", tmpl_resp.map_or("?".into(), |r| hexr(&r))); + + let s = UdpSocket::bind("0.0.0.0:0").expect("bind 失败"); + s.set_read_timeout(Some(Duration::from_millis(300))).ok(); + let tgt = format!("{}:31772", target); + + // --- 阶段 1: 确定性验证 (原样重放 ×3) --- + println!("\n--- 阶段 1: 确定性 (原样重放×3) ---"); + let mut base: Option<[u8; 4]> = None; + for i in 0..3 { + match query(&s, &tgt, &tmpl) { + Some(r) => { + println!(" 重放#{}: resp32={}", i + 1, hexr(&r)); + if let Some(b0) = base { if b0 != r { println!(" ✗ 不确定性! 含设备侧随机"); return; } } + base = Some(r); + } + None => { println!(" 重放#{}: 无应答 — 模板被拒?", i + 1); return; } + } + } + let base = base.unwrap(); + if let Some(tr) = tmpl_resp { + println!(" 与抓包值 {} => {}", hexr(&tr), if tr == base { "一致 ✓ (完全确定)" } else { "不同 ✗" }); + } + + // --- 阶段 2: 输入域结构 (哪些区域影响 resp) --- + println!("\n--- 阶段 2: 输入域探测 ---"); + let probe_region = |name: &str, f: &mut dyn FnMut(&mut Vec)| { + let mut m = tmpl.clone(); + f(&mut m); + match query(&s, &tgt, &m) { + Some(r) => println!(" {}: resp32={} {}", name, hexr(&r), + if r == base { "(=基线, 不影响)" } else { "(≠基线, ★影响)" }), + None => println!(" {}: 无应答", name), + } + std::thread::sleep(Duration::from_millis(5)); + }; + probe_region("全 blob 清零 (0x2C-0x123)", &mut |m| { for i in 0x2C..0x124 { m[i] = 0; } }); + probe_region("blob 全 FF", &mut |m| { for i in 0x2C..0x124 { m[i] = 0xFF; } }); + probe_region("仅 part1 清零 (0x2C-0x8B)", &mut |m| { for i in 0x2C..0x8C { m[i] = 0; } }); + probe_region("仅 part2 清零 (0x90-0x123)", &mut |m| { for i in 0x90..0x124 { m[i] = 0; } }); + probe_region("静态缝 0x8C-0x8F 翻bit0", &mut |m| { m[0x8C] ^= 1; }); + probe_region("头部 0x00 翻bit", &mut |m| { m[0x00] ^= 1; }); + probe_region("长度域 0x14 翻bit", &mut |m| { m[0x14] ^= 1; }); + probe_region("主机IP 0x1A 翻bit", &mut |m| { m[0x1A] ^= 1; }); + probe_region("主机MAC 0x1E 翻bit", &mut |m| { m[0x1E] ^= 1; }); + probe_region("0x24 翻bit", &mut |m| { m[0x24] ^= 1; }); + probe_region("尾字节 0x227 翻bit", &mut |m| { m[0x227] ^= 1; }); + + // --- 阶段 3: 仿射性抽样验证 --- + // f(a)^f(b) == f(a^b)^f(0) 对随机 a,b 成立 ⇒ 仿射 + println!("\n--- 阶段 3: 仿射性抽样验证 (8 组) ---"); + let mut seed = 0x9e3779b9u32; + let mut rnd = move || { seed = seed.wrapping_mul(2654435761).wrapping_add(0x9e3779b9); seed }; + // f(0): blob 全零 + let mut z = tmpl.clone(); + for i in 0x2C..0x124 { z[i] = 0; } + let f0 = match query(&s, &tgt, &z) { + Some(r) => r, + None => { println!(" 零 blob 无应答, 仿射验证跳过"); return; } + }; + println!(" f(零blob) = {}", hexr(&f0)); + let mut affine_ok = 0; + for t in 0..8 { + let mut a = tmpl.clone(); let mut b = tmpl.clone(); + for i in 0x2C..0x124 { + if rnd() & 1 != 0 { a[i] ^= 1 << ((rnd() >> 8) & 7); } + if rnd() & 1 != 0 { b[i] ^= 1 << ((rnd() >> 8) & 7); } + } + let mut axb = a.clone(); + for i in 0x2C..0x124 { axb[i] = a[i] ^ b[i] ^ tmpl[i]; } // axb = a^b (相对模板) + // 需要绝对: a^b 的 blob = a.blob ^ b.blob ^ 0? 直接用 a.blob ^ b.blob 作为新 blob + for i in 0x2C..0x124 { axb[i] = a[i] ^ b[i]; } + let fa = query(&s, &tgt, &a); + let fb = query(&s, &tgt, &b); + let faxb = query(&s, &tgt, &axb); + std::thread::sleep(Duration::from_millis(5)); + match (fa, fb, faxb) { + (Some(fa), Some(fb), Some(faxb)) => { + // f(a)^f(b)^f(a^b) 应 == f(0) (仿射) + let mut lhs = [0u8; 4]; + for i in 0..4 { lhs[i] = fa[i] ^ fb[i] ^ faxb[i]; } + let ok = lhs == f0; + if ok { affine_ok += 1; } + println!(" 组{}: f(a)^f(b)^f(a^b)={} vs f(0)={} {}", t + 1, hexr(&lhs), hexr(&f0), + if ok { "✓" } else { "✗ 非仿射!" }); + } + _ => println!(" 组{}: 有无应答缺失, 跳过", t + 1), + } + } + if affine_ok < 8 { + println!("\n仿射性未全过 ({}/8) — 大概率为真 MAC (HMAC/AES 类), 样本无法离线伪造", affine_ok); + println!("退路: 继续中继方案, 或分析固件/dongle 算法"); + return; + } + + // --- 阶段 4: 全量单比特翻转, 学习线性映射矩阵 --- + println!("\n--- 阶段 4: 学习线性映射 (1952 比特 × 1 查询) ---"); + // L(e_i) = f(e_i) ^ f(0), 对 blob 0x2C..0x124 的每比特 + let nbits = (0x124 - 0x2C) * 8; + let mut cols: Vec<[u8; 4]> = Vec::with_capacity(nbits); + let mut fails = 0; + for bit in 0..nbits { + let off = 0x2C + bit / 8; + let mut m = z.clone(); // 基于零 blob + m[off] = 1 << (bit % 8); + match query(&s, &tgt, &m) { + Some(r) => { + let mut c = [0u8; 4]; + for i in 0..4 { c[i] = r[i] ^ f0[i]; } + cols.push(c); + } + None => { fails += 1; cols.push([0; 4]); } + } + if bit % 200 == 0 { println!(" 进度 {}/{} (失败 {})", bit, nbits, fails); } + std::thread::sleep(Duration::from_millis(3)); + } + println!(" 矩阵学习完成: {} 列, 失败 {}", cols.len(), fails); + + // 验证: 对模板质询用矩阵计算, 应得 base + let mut calc = f0; + for bit in 0..nbits { + let off = 0x2C + bit / 8; + if tmpl[off] & (1 << (bit % 8)) != 0 { + for i in 0..4 { calc[i] ^= cols[bit][i]; } + } + } + println!(" 矩阵验证: 计算={} vs 实测={} {}", hexr(&calc), hexr(&base), + if calc == base { "✓✓ 仿射模型成立!" } else { "✗" }); + + // 保存矩阵 (f0 + 列) 供 .7 离线应答用 + let mut out = String::new(); + out.push_str(&format!("# AECH 仿射映射: resp32 = f0 ^ XOR_{{blob bit=1}} col[bit] (目标 {})\n", target)); + out.push_str(&format!("f0={}\n", hexr(&f0))); + for (i, c) in cols.iter().enumerate() { + out.push_str(&format!("{}\n", hexr(c))); + let _ = i; + } + let path = "aech_affine_map.txt"; + std::fs::write(path, &out).ok(); + println!(" 映射已保存: {} ({} 列)", path, cols.len()); +} + +// ---------- probe2: 96B part1 的块/字节影响图谱 ---------- +// resp32 = f(chal[0x2C..0x8C]) 已实证 (probe 阶段2)。96B = 12×8B 分组密码直觉。 +// 测绘: 每字节翻转 1bit 看 resp 雪崩模式; 每 8B 块清零看是否影响; 换序测试。 +pub fn probe2(tsv: &str, target: &str) { + let content = read_tsv(tsv); + let mut tmpl: Option> = None; + for line in content.lines() { + let p: Vec<&str> = line.split('\t').collect(); + if p.len() < 5 || p[4].is_empty() { continue; } + let b = match h2b(p[4]) { Some(b) => b, None => continue }; + if b.len() == 552 && &b[0..4] == b"ENet" && &b[0x28..0x2C] == b"AECH" { + tmpl = Some(b); break; + } + } + let tmpl = tmpl.expect("无模板"); + let s = UdpSocket::bind("0.0.0.0:0").expect("bind 失败"); + s.set_read_timeout(Some(Duration::from_millis(300))).ok(); + let tgt = format!("{}:31772", target); + + let base = match query(&s, &tgt, &tmpl) { + Some(r) => r, + None => { println!("基线无应答"); return; } + }; + println!("基线 resp32={}", hexr(&base)); + + // --- 字节影响图谱: part1 每字节翻 bit0 --- + println!("\n--- 字节影响图谱 (0x2C-0x8B, 翻转 bit0) ---"); + println!("off | resp32 | 变化bit数 | 变化字节位置"); + let mut infl: Vec<(usize, u32)> = Vec::new(); + for off in 0x2C..0x8C { + let mut m = tmpl.clone(); + m[off] ^= 1; + match query(&s, &tgt, &m) { + Some(r) => { + let x = u32::from_be_bytes([r[0] ^ base[0], r[1] ^ base[1], r[2] ^ base[2], r[3] ^ base[3]]); + let nbits = x.count_ones(); + let pos: Vec = (0..4).filter(|&i| r[i] != base[i]).map(|i| format!("B{}", i)).collect(); + println!("0x{:02x} | {} | {:2} | {}", off, hexr(&r), nbits, pos.join("")); + infl.push((off, nbits)); + } + None => println!("0x{:02x} | 无应答", off), + } + std::thread::sleep(Duration::from_millis(3)); + } + let zero_infl: Vec = infl.iter().filter(|(_, n)| *n == 0).map(|(o, _)| *o).collect(); + println!("不影响的字节: {:?}", zero_infl.iter().map(|o| format!("0x{:02x}", o)).collect::>()); + + // --- 8B 块清零图谱 --- + println!("\n--- 8B 块清零图谱 ---"); + for blk in 0..12 { + let mut m = tmpl.clone(); + for i in 0..8 { m[0x2C + blk * 8 + i] = 0; } + match query(&s, &tgt, &m) { + Some(r) => println!(" 块{:2} (0x{:02x}-0x{:02x}) 清零: {} {}", blk, 0x2C + blk * 8, 0x2C + blk * 8 + 7, + hexr(&r), if r == base { "(不影响)" } else { "★影响" }), + None => println!(" 块{:2}: 无应答", blk), + } + std::thread::sleep(Duration::from_millis(3)); + } + + // --- 换序测试 --- + println!("\n--- 换序测试 ---"); + let mut m1 = tmpl.clone(); + m1.swap(0x2C, 0x2D); + match query(&s, &tgt, &m1) { + Some(r) => println!(" 相邻字节交换 (0x2C<->0x2D): {} {}", hexr(&r), if r == base { "(顺序无关!)" } else { "顺序相关" }), + None => println!(" 交换: 无应答"), + } + let mut m2 = tmpl.clone(); + m2[0x2C..0x8C].reverse(); + match query(&s, &tgt, &m2) { + Some(r) => println!(" part1 整体反转: {} {}", hexr(&r), if r == base { "(顺序无关!)" } else { "顺序相关" }), + None => println!(" 反转: 无应答"), + } + // 块交换: 块0 <-> 块1 + let mut m3 = tmpl.clone(); + for i in 0..8 { m3.swap(0x2C + i, 0x34 + i); } + match query(&s, &tgt, &m3) { + Some(r) => println!(" 块0<->块1 交换: {} {}", hexr(&r), if r == base { "(块序无关!)" } else { "块序相关" }), + None => println!(" 块交换: 无应答"), + } + + // --- 整数加性测试: 首字节 +1 (整数) vs 翻 bit0 --- + println!("\n--- 整数加性粗测 ---"); + let mut m4 = tmpl.clone(); + m4[0x2C] = m4[0x2C].wrapping_add(1); + match query(&s, &tgt, &m4) { + Some(r) => println!(" chal[0x2C]+=1: {}", hexr(&r)), + None => println!(" 无应答"), + } +} + +// ---------- identify: f(last4) 的离线候选函数鉴别 ---------- +// probe2 实证: resp32 = f(chal[0x88..0x8C]), 仅 4B 输入。用手头全部 (输入,输出) 对 +// 测试常见 32 位构造 (哈希截断/finalizer/零密钥分组密码), 命中即可离线伪造。 +pub fn identify(tsv: &str) { + // 从 TSV 取 3 轮质询的 last4 + 对应 resp32 (抓包对) + let content = read_tsv(tsv); + let mut chals: Vec<(f64, Vec)> = Vec::new(); + let mut resps: Vec<(f64, Vec)> = Vec::new(); + for line in content.lines() { + let p: Vec<&str> = line.split('\t').collect(); + if p.len() < 5 || p[4].is_empty() { continue; } + let b = match h2b(p[4]) { Some(b) => b, None => continue }; + if b.len() != 552 { continue; } + let t: f64 = match p[0].parse() { Ok(t) => t, Err(_) => continue }; + if &b[0x28..0x2C] == b"AECH" { chals.push((t, b)); } + else if &b[0x28..0x2C] == b"AERE" { resps.push((t, b)); } + } + let mut pairs: Vec<([u8; 4], [u8; 4])> = Vec::new(); + for (rt, rb) in &resps { + let mut best: Option<&(f64, Vec)> = None; + for c in &chals { if c.0 <= *rt && rt - c.0 < 0.5 && best.map_or(true, |b| c.0 > b.0) { best = Some(c); } } + if let Some((_, cb)) = best { + pairs.push(([cb[0x88], cb[0x89], cb[0x8A], cb[0x8B]], [rb[0xC0], rb[0xC1], rb[0xC2], rb[0xC3]])); + } + } + // 追加 probe2 实测对: 零 / 全FF + pairs.push(([0, 0, 0, 0], [0xf6, 0x7f, 0xb7, 0xe3])); + pairs.push(([0xFF, 0xFF, 0xFF, 0xFF], [0x37, 0xac, 0x04, 0x6f])); + println!("鉴别样本 {} 对:", pairs.len()); + let mut g_ok = 0; + for (x, y) in &pairs { + let xu = u32::from_le_bytes(*x); + let g = agios_g(xu).to_le_bytes(); + let ok = g == *y; + if ok { g_ok += 1; } + println!(" {} -> {} | G()={} {}", hexr(x), hexr(y), hexr(&g), if ok { "✓" } else { "✗" }); + } + println!("AgiosElias G 验证: {}/{} 命中", g_ok, pairs.len()); + + let t32 = { + let mut t = [0u32; 256]; + for i in 0..256u32 { let mut c = i; for _ in 0..8 { c = if c & 1 != 0 { (c >> 1) ^ 0xEDB88320 } else { c >> 1 }; } t[i as usize] = c; } + t + }; + // TEA / XTEA (零密钥) + fn tea(v0: u32, v1: u32, rounds: u32) -> (u32, u32) { + let (mut a, mut b, mut sum) = (v0, v1, 0u32); + for _ in 0..rounds { + sum = sum.wrapping_add(0x9E3779B9); + a = a.wrapping_add((b << 4) ^ b.wrapping_add(sum) ^ (b >> 5)); + b = b.wrapping_add((a << 4) ^ a.wrapping_add(sum) ^ (a >> 5)); + } + (a, b) + } + fn xtea(v0: u32, v1: u32, rounds: u32) -> (u32, u32) { + let (mut a, mut b, mut sum) = (v0, v1, 0u32); + let k = [0u32; 4]; + for _ in 0..rounds { + a = a.wrapping_add(((b << 4) ^ (b >> 5)).wrapping_add(b) ^ sum.wrapping_add(k[(sum & 3) as usize])); + sum = sum.wrapping_add(0x9E3779B9); + b = b.wrapping_add(((a << 4) ^ (a >> 5)).wrapping_add(a) ^ sum.wrapping_add(k[((sum >> 11) & 3) as usize])); + } + (a, b) + } + // 32 位 finalizer 系列 + fn fmix32(mut h: u32) -> u32 { h ^= h >> 16; h = h.wrapping_mul(0x85ebca6b); h ^= h >> 13; h = h.wrapping_mul(0xc2b2ae35); h ^= h >> 16; h } + fn splitmix32(mut x: u32) -> u32 { x = x.wrapping_add(0x9e3779b9); x = (x ^ (x >> 16)).wrapping_mul(0x21f0aaad); x = (x ^ (x >> 15)).wrapping_mul(0x735a2d97); x ^ (x >> 15) } + fn xxh_avalanche(mut h: u32) -> u32 { h ^= h >> 15; h = h.wrapping_mul(0x85EBCA77); h ^= h >> 13; h = h.wrapping_mul(0xC2B2AE3D); h ^= h >> 16; h } + fn wang(mut h: u32) -> u32 { h = (h ^ 61) ^ (h >> 16); h = h.wrapping_add(h << 3); h ^= h >> 4; h = h.wrapping_mul(0x27d4eb2d); h ^= h >> 15; h } + fn joaatf(b: &[u8]) -> u32 { let mut h = 0u32; for &x in b { h = h.wrapping_add(x as u32); h = h.wrapping_add(h << 10); h ^= h >> 6; } h = h.wrapping_add(h << 3); h ^= h >> 11; h.wrapping_add(h << 15) } + fn fnv(b: &[u8]) -> u32 { let mut h = 0x811c9dc5u32; for &x in b { h ^= x as u32; h = h.wrapping_mul(0x01000193); } h } + fn murmur3(b: &[u8]) -> u32 { + let mut h = 0u32; let c1 = 0xcc9e2d51u32; let c2 = 0x1b873593u32; + if b.len() >= 4 { + let mut k = u32::from_le_bytes([b[0], b[1], b[2], b[3]]); + k = k.wrapping_mul(c1); k = k.rotate_left(15); k = k.wrapping_mul(c2); + h ^= k; h = h.rotate_left(13); h = h.wrapping_mul(5).wrapping_add(0xe6546b64); + } + h ^= b.len() as u32; fmix32(h) + } + fn xxh32one(b: &[u8]) -> u32 { + // xxHash32 对 4B 输入 (seed=0): h = seed+P5+len; h += k*P3; rotl17; *P4; avalanche + let mut h = 0x165667B1u32.wrapping_add(4); + let k = u32::from_le_bytes([b[0], b[1], b[2], b[3]]); + h = h.wrapping_add(k.wrapping_mul(0xC2B2AE3D)).rotate_left(17).wrapping_mul(0x27D4EB2F); + xxh_avalanche(h) + } + + type F = Box u32>; + let cands: Vec<(&str, F)> = vec![ + ("MD5[0..4]LE", Box::new(move |x| { let m = md5(&x); u32::from_le_bytes([m[0], m[1], m[2], m[3]]) })), + ("MD5[0..4]BE", Box::new(move |x| { let m = md5(&x); u32::from_be_bytes([m[0], m[1], m[2], m[3]]) })), + ("SHA1[0..4]LE", Box::new(move |x| { let m = sha1(&x); u32::from_le_bytes([m[0], m[1], m[2], m[3]]) })), + ("SHA1[0..4]BE", Box::new(move |x| { let m = sha1(&x); u32::from_be_bytes([m[0], m[1], m[2], m[3]]) })), + ("CRC32", Box::new(move |x| crc32(&t32, &x))), + ("Murmur3seed0", Box::new(move |x| murmur3(&x))), + ("xxHash32seed0", Box::new(move |x| xxh32one(&x))), + ("FNV1a32", Box::new(move |x| fnv(&x))), + ("Jenkins", Box::new(move |x| joaatf(&x))), + ("fmix32(xLE)", Box::new(move |x| fmix32(u32::from_le_bytes(x)))), + ("fmix32(xBE)", Box::new(move |x| fmix32(u32::from_be_bytes(x)))), + ("splitmix32(xLE)", Box::new(move |x| splitmix32(u32::from_le_bytes(x)))), + ("splitmix32(xBE)", Box::new(move |x| splitmix32(u32::from_be_bytes(x)))), + ("xxhAvalanche(xLE)", Box::new(move |x| xxh_avalanche(u32::from_le_bytes(x)))), + ("xxhAvalanche(xBE)", Box::new(move |x| xxh_avalanche(u32::from_be_bytes(x)))), + ("Wang(xLE)", Box::new(move |x| wang(u32::from_le_bytes(x)))), + ("Wang(xBE)", Box::new(move |x| wang(u32::from_be_bytes(x)))), + ("TEA16zerok(out0LE)", Box::new(move |x| tea(u32::from_le_bytes(x), 0, 16).0)), + ("TEA32zerok(out0LE)", Box::new(move |x| tea(u32::from_le_bytes(x), 0, 32).0)), + ("XTEA32zerok(out0LE)", Box::new(move |x| xtea(u32::from_le_bytes(x), 0, 32).0)), + ("TEA32zerok(out0BE)", Box::new(move |x| tea(u32::from_be_bytes(x), 0, 32).0)), + ("恒等LE", Box::new(move |x| u32::from_le_bytes(x))), + ("恒等BE", Box::new(move |x| u32::from_be_bytes(x))), + ]; + + println!("\n--- 候选鉴别 (须命中全部 {} 对) ---", pairs.len()); + let mut any = false; + for (name, f) in &cands { + for endian in ["BEout", "LEout"] { + let ok = pairs.iter().all(|(x, y)| { + let v = f(*x); + let out = if endian == "BEout" { v.to_be_bytes() } else { v.to_le_bytes() }; + out == *y + }); + if ok { println!(" ★★★ 命中: {} {}", name, endian); any = true; } + } + } + if !any { println!(" (全部候选均未命中 — f 为带密钥函数的可能性大)"); } +} + +fn hexr(b: &[u8]) -> String { + b.iter().map(|x| format!("{:02x}", x)).collect::() +} + +/// 从 TSV 提取 (质询, 应答) 对: AECH(552B, 含 "AECH"@0x28) 后 50ms 内的 AERE(552B, "AERE"@0x28) +fn load_pairs(tsv: &str) -> Vec<(f64, Vec, Vec)> { + let content = read_tsv(tsv); + let mut chals: Vec<(f64, Vec)> = Vec::new(); + let mut resps: Vec<(f64, Vec)> = Vec::new(); + for line in content.lines() { + let p: Vec<&str> = line.split('\t').collect(); + if p.len() < 5 || p[4].is_empty() { continue; } + let b = match h2b(p[4]) { Some(b) => b, None => continue }; + if b.len() != 552 { continue; } + if &b[0..4] != b"ENet" { continue; } + let t: f64 = match p[0].parse() { Ok(t) => t, Err(_) => continue }; + if &b[0x28..0x2C] == b"AECH" { chals.push((t, b)); } + else if &b[0x28..0x2C] == b"AERE" { resps.push((t, b)); } + } + let mut pairs = Vec::new(); + for (rt, rb) in &resps { + // 最近的先前质询 (500ms 窗, 兼容中继路径 ~140ms 延迟) + let mut best: Option<&(f64, Vec)> = None; + for c in &chals { + if c.0 <= *rt && rt - c.0 < 0.5 { + if best.map_or(true, |b| c.0 > b.0) { best = Some(c); } + } + } + if let Some((ct, cb)) = best { + pairs.push((*ct, cb.clone(), rb.clone())); + } + } + println!(" {}: AECH={} AERE={} -> 配对 {}", tsv, chals.len(), resps.len(), pairs.len()); + pairs +} + +/// 同一 TSV 内提取 DMX 会话 K (保护字节 ^ (CH-1), §4.1) +fn session_k(tsv: &str) -> Option { + let content = read_tsv(tsv); + for line in content.lines() { + let p: Vec<&str> = line.split('\t').collect(); + if p.len() < 5 || p[4].is_empty() { continue; } + let b = match h2b(p[4]) { Some(b) => b, None => continue }; + // DMX 帧: 1084B, 0x20-0x21=0101, 保护字节 0x22 = (CH1-1)^K + if b.len() == 1084 && b[0x20] == 0x01 && b[0x21] == 0x01 { + let ch1 = b[0x3C]; + return Some(b[0x22] ^ ch1.wrapping_sub(1)); + } + } + None +} + +pub fn run(tsvs: &[String]) { + let mut pairs: Vec<(Vec, Vec)> = Vec::new(); + let mut ks: Vec<(String, Option)> = Vec::new(); + for tsv in tsvs { + let ps = load_pairs(tsv); + let k = session_k(tsv); + ks.push((tsv.clone(), k)); + for (_, c, r) in ps { pairs.push((c, r)); } + } + if pairs.len() < 2 { + println!("样本不足 ({} 对), 无法做跨样本测试", pairs.len()); + return; + } + println!("\n=== 共 {} 对样本 ===", pairs.len()); + for (i, (c, r)) in pairs.iter().enumerate() { + println!(" #{}: chal[0x2c..0x30]={} resp32={}", i, + hexr(&c[0x2c..0x30]), hexr(&r[0xC0..0xC4])); + } + for (t, k) in &ks { + println!(" 会话 K: {} -> {}", t, k.map_or("无 DMX".into(), |k| format!("0x{:02x}", k))); + } + + // --- 结构: 应答跨样本变化字节确认 --- + let mut vary = Vec::new(); + for i in 0..552 { + let v = pairs[0].1[i]; + if pairs.iter().any(|(_, r)| r[i] != v) { vary.push(i); } + } + println!("\n应答跨样本变化字节: {:?}", vary.iter().map(|o| format!("0x{:02x}", o)).collect::>()); + + // resp32 两种端序 + let resp_le: Vec = pairs.iter().map(|(_, r)| + u32::from_le_bytes([r[0xC0], r[0xC1], r[0xC2], r[0xC3]])).collect(); + let resp_be: Vec = pairs.iter().map(|(_, r)| + u32::from_be_bytes([r[0xC0], r[0xC1], r[0xC2], r[0xC3]])).collect(); + + // 多种 CRC32 多项式表 (反射式): IEEE / Castagnoli / Koopman / CRC-32M + let polys: [(&str, u32); 4] = [ + ("IEEE", 0xEDB88320), ("CRC32C", 0x82F63B78), ("CRC32K", 0xEB31D82E), ("CRC32M", 0xD5828281)]; + let tables: Vec<(&str, [u32; 256])> = polys.iter().map(|(name, p)| { + let mut t = [0u32; 256]; + for i in 0..256u32 { + let mut c = i; + for _ in 0..8 { c = if c & 1 != 0 { (c >> 1) ^ p } else { c >> 1 }; } + t[i as usize] = c; + } + (*name, t) + }).collect(); + let n = pairs.len(); + + // --- A0: 同轮 质询→应答 字节回显扫描 --- + println!("--- A0: 同轮 chal→resp 回显/复制扫描 ---"); + { + let mut hits = 0; + for ro in 0..552usize { + for co in 0..552usize { + if (0..n).all(|i| pairs[i].1[ro] == pairs[i].0[co]) { + // 排除恒等静态字节 (要求该字节跨样本有变化, 否则无信息量) + let v = pairs[0].1[ro]; + if pairs.iter().any(|(_, r)| r[ro] != v) { + println!(" ★ resp[0x{:02x}] == chal[0x{:02x}] (全样本)", ro, co); + hits += 1; + } + } + } + } + if hits == 0 { println!(" (变化字节无直接回显)"); } + } + + // --- A1: 无密钥 CRC32(多多项式, 任意切片) 直接命中 --- + println!("\n--- A1: 无密钥 CRC32(多項式×切片) 直接匹配 ---"); + let mut hits = 0; + for (pname, tab) in &tables { + for start in 0..552usize { + for len in 1..=(552 - start) { + let c0 = crc32(tab, &pairs[0].0[start..start + len]); + for (ename, resp) in [("LE", &resp_le), ("BE", &resp_be)] { + if c0 == resp[0] && (1..n).all(|i| crc32(tab, &pairs[i].0[start..start + len]) == resp[i]) { + println!(" ★ 命中 {} [{}..{}] {} CRC32=resp32", pname, start, start + len, ename); + hits += 1; + } + } + } + } + } + if hits == 0 { println!(" (无)"); } + + // --- B1: 密钥化 CRC32 仿射测试 (Δ 跨样本恒定, 多項式) --- + println!("--- B1: 密钥化 CRC32 仿射测试 (resp ^ CRC32_0(slice) = Δ 恒定) ---"); + let mut hits = 0; + 'outer: for (pname, tab) in &tables { + for start in 0..552usize { + for len in 1..=(552 - start) { + let d0le = resp_le[0] ^ crc32(tab, &pairs[0].0[start..start + len]); + let d0be = resp_be[0] ^ crc32(tab, &pairs[0].0[start..start + len]); + let okle = (1..n).all(|i| (resp_le[i] ^ crc32(tab, &pairs[i].0[start..start + len])) == d0le); + let okbe = !okle && (1..n).all(|i| (resp_be[i] ^ crc32(tab, &pairs[i].0[start..start + len])) == d0be); + if okle || okbe { + println!(" ★ 命中 {} [{}..{}] {} Δ=0x{:08x}", pname, start, start + len, + if okle { "LE" } else { "BE" }, if okle { d0le } else { d0be }); + hits += 1; + if hits > 20 { println!(" (更多命中省略)"); break 'outer; } + } + } + } + } + if hits == 0 { println!(" (无)"); } + + // --- A2: MD5/SHA1 任意切片, first4/last4 LE/BE --- + println!("--- A2: MD5/SHA1(切片) 截断匹配 ---"); + let mut hits = 0; + for start in 0..552usize { + for len in 1..=(552 - start) { + let m0 = md5(&pairs[0].0[start..start + len]); + let s0 = sha1(&pairs[0].0[start..start + len]); + // 候选值: first4le/be, last4le/be (md5), first4le/be (sha1) + let cands: [(&str, u32); 6] = [ + ("MD5first4LE", u32::from_le_bytes([m0[0], m0[1], m0[2], m0[3]])), + ("MD5first4BE", u32::from_be_bytes([m0[0], m0[1], m0[2], m0[3]])), + ("MD5last4LE", u32::from_le_bytes([m0[12], m0[13], m0[14], m0[15]])), + ("MD5last4BE", u32::from_be_bytes([m0[12], m0[13], m0[14], m0[15]])), + ("SHA1first4LE", u32::from_le_bytes([s0[0], s0[1], s0[2], s0[3]])), + ("SHA1first4BE", u32::from_be_bytes([s0[0], s0[1], s0[2], s0[3]])), + ]; + for (cname, cv) in cands { + for (ename, resp) in [("LE", &resp_le), ("BE", &resp_be)] { + if cv != resp[0] { continue; } + let ok = (1..n).all(|i| { + let m = md5(&pairs[i].0[start..start + len]); + let s = sha1(&pairs[i].0[start..start + len]); + let v = match cname { + "MD5first4LE" => u32::from_le_bytes([m[0], m[1], m[2], m[3]]), + "MD5first4BE" => u32::from_be_bytes([m[0], m[1], m[2], m[3]]), + "MD5last4LE" => u32::from_le_bytes([m[12], m[13], m[14], m[15]]), + "MD5last4BE" => u32::from_be_bytes([m[12], m[13], m[14], m[15]]), + "SHA1first4LE" => u32::from_le_bytes([s[0], s[1], s[2], s[3]]), + _ => u32::from_be_bytes([s[0], s[1], s[2], s[3]]), + }; + v == resp[i] + }); + if ok { + println!(" ★ 命中 [{}..{}] {} = resp32 {}", start, start + len, cname, ename); + hits += 1; + } + } + } + } + } + if hits == 0 { println!(" (无)"); } + + // --- A3: 简单加和 (u8/u16LE/u16BE/u32LE/u32BE 累加, 任意切片) --- + println!("--- A3: 加和校验(切片) ---"); + let mut hits = 0; + for start in 0..552usize { + for len in 1..=(552 - start) { + let sums = |b: &[u8]| -> [u32; 5] { + let mut r = [0u32; 5]; + for (i, &x) in b.iter().enumerate() { + r[0] = r[0].wrapping_add(x as u32); + if i + 1 < b.len() && i % 2 == 0 { r[1] = r[1].wrapping_add(u16::from_le_bytes([x, b[i+1]]) as u32); } + if i + 1 < b.len() && i % 2 == 0 { r[2] = r[2].wrapping_add(u16::from_be_bytes([x, b[i+1]]) as u32); } + if i + 3 < b.len() && i % 4 == 0 { r[3] = r[3].wrapping_add(u32::from_le_bytes([x, b[i+1], b[i+2], b[i+3]])); } + if i + 3 < b.len() && i % 4 == 0 { r[4] = r[4].wrapping_add(u32::from_be_bytes([x, b[i+1], b[i+2], b[i+3]])); } + } + r + }; + let s0 = sums(&pairs[0].0[start..start + len]); + for k in 0..5 { + for (ename, resp) in [("LE", &resp_le), ("BE", &resp_be)] { + if s0[k] != resp[0] { continue; } + if (1..n).all(|i| sums(&pairs[i].0[start..start + len])[k] == resp[i]) { + println!(" ★ 命中 [{}..{}] sum{} {}", start, start + len, k, ename); + hits += 1; + } + } + } + } + } + if hits == 0 { println!(" (无)"); } + + // --- C: XOR 常量: resp32 ^ chal32@off (LE/BE) 跨样本恒定 --- + println!("--- C: XOR 常量掩码测试 ---"); + let mut hits = 0; + for off in 0..548usize { + let cle0 = u32::from_le_bytes([pairs[0].0[off], pairs[0].0[off+1], pairs[0].0[off+2], pairs[0].0[off+3]]); + let cbe0 = u32::from_be_bytes([pairs[0].0[off], pairs[0].0[off+1], pairs[0].0[off+2], pairs[0].0[off+3]]); + let dle = resp_le[0] ^ cle0; let dbe = resp_be[0] ^ cbe0; + let okle = (1..n).all(|i| resp_le[i] ^ u32::from_le_bytes([pairs[i].0[off], pairs[i].0[off+1], pairs[i].0[off+2], pairs[i].0[off+3]]) == dle); + let okbe = (1..n).all(|i| resp_be[i] ^ u32::from_be_bytes([pairs[i].0[off], pairs[i].0[off+1], pairs[i].0[off+2], pairs[i].0[off+3]]) == dbe); + if okle { println!(" ★ resp32LE ^ chal32LE@0x{:02x} = 0x{:08x} (恒定)", off, dle); hits += 1; } + if okbe { println!(" ★ resp32BE ^ chal32BE@0x{:02x} = 0x{:08x} (恒定)", off, dbe); hits += 1; } + } + if hits == 0 { println!(" (无)"); } + + // --- D: resp32 与会话 K 相关 --- + println!("--- D: resp32 字节 vs 会话 K ---"); + for (i, (t, k)) in ks.iter().enumerate() { + if let Some(k) = k { + // 该 tsv 贡献的样本 (按顺序假设每 tsv 样本连续) + let _ = i; + for (j, (_, r)) in pairs.iter().enumerate() { + let hit = (0xC0..0xC4).any(|o| r[o] == *k); + if hit { + println!(" {} K=0x{:02x} 出现在样本#{} resp32 中", t, k, j); + } + } + } + } + + // --- A4: 32 位非加密哈希 (直接匹配 + XOR-Δ 恒定) --- + println!("--- A4: FNV1a32/FNV132/djb2/sdbm/Jenkins/Murmur3/xxHash32 ---"); + fn fnv1a32(b: &[u8]) -> u32 { let mut h = 0x811c9dc5u32; for &x in b { h ^= x as u32; h = h.wrapping_mul(0x01000193); } h } + fn fnv132(b: &[u8]) -> u32 { let mut h = 0x811c9dc5u32; for &x in b { h = h.wrapping_mul(0x01000193); h ^= x as u32; } h } + fn djb2(b: &[u8]) -> u32 { let mut h = 5381u32; for &x in b { h = h.wrapping_mul(33).wrapping_add(x as u32); } h } + fn sdbm(b: &[u8]) -> u32 { let mut h = 0u32; for &x in b { h = (x as u32).wrapping_add(h << 6).wrapping_add(h << 16).wrapping_sub(h); } h } + fn joaat(b: &[u8]) -> u32 { let mut h = 0u32; for &x in b { h = h.wrapping_add(x as u32); h = h.wrapping_add(h << 10); h ^= h >> 6; } h = h.wrapping_add(h << 3); h ^= h >> 11; h.wrapping_add(h << 15) } + fn murmur3_32(b: &[u8], seed: u32) -> u32 { + let mut h = seed; + let c1 = 0xcc9e2d51u32; let c2 = 0x1b873593u32; + let nblocks = b.len() / 4; + for i in 0..nblocks { + let mut k = u32::from_le_bytes([b[i*4], b[i*4+1], b[i*4+2], b[i*4+3]]); + k = k.wrapping_mul(c1); k = k.rotate_left(15); k = k.wrapping_mul(c2); + h ^= k; h = h.rotate_left(13); h = h.wrapping_mul(5).wrapping_add(0xe6546b64); + } + let tail = &b[nblocks*4..]; + let mut k = 0u32; + for (i, &x) in tail.iter().enumerate() { k |= (x as u32) << (8 * i); } + if !tail.is_empty() { k = k.wrapping_mul(c1); k = k.rotate_left(15); k = k.wrapping_mul(c2); h ^= k; } + h ^= b.len() as u32; + h ^= h >> 16; h = h.wrapping_mul(0x85ebca6b); h ^= h >> 13; h = h.wrapping_mul(0xc2b2ae35); h ^= h >> 16; + h + } + fn xxh32(b: &[u8], seed: u32) -> u32 { + const P1: u32 = 0x9E3779B1; const P2: u32 = 0x85EBCA77; const P3: u32 = 0xC2B2AE3D; + const P4: u32 = 0x27D4EB2F; const P5: u32 = 0x165667B1; + let mut i = 0usize; + let mut h: u32; + if b.len() >= 16 { + let mut v1 = seed.wrapping_add(P1).wrapping_add(P2); + let mut v2 = seed.wrapping_add(P2); + let mut v3 = seed; + let mut v4 = seed.wrapping_sub(P1); + while i + 16 <= b.len() { + let rd = |o: usize| u32::from_le_bytes([b[o], b[o+1], b[o+2], b[o+3]]); + v1 = v1.wrapping_add(rd(i).wrapping_mul(P2)).rotate_left(13).wrapping_mul(P1); + v2 = v2.wrapping_add(rd(i+4).wrapping_mul(P2)).rotate_left(13).wrapping_mul(P1); + v3 = v3.wrapping_add(rd(i+8).wrapping_mul(P2)).rotate_left(13).wrapping_mul(P1); + v4 = v4.wrapping_add(rd(i+12).wrapping_mul(P2)).rotate_left(13).wrapping_mul(P1); + i += 16; + } + h = v1.rotate_left(1).wrapping_add(v2.rotate_left(7)).wrapping_add(v3.rotate_left(12)).wrapping_add(v4.rotate_left(18)); + } else { + h = seed.wrapping_add(P5); + } + h = h.wrapping_add(b.len() as u32); + while i + 4 <= b.len() { + h = h.wrapping_add(u32::from_le_bytes([b[i], b[i+1], b[i+2], b[i+3]]).wrapping_mul(P3)).rotate_left(17).wrapping_mul(P4); + i += 4; + } + while i < b.len() { + h = h.wrapping_add((b[i] as u32).wrapping_mul(P5)).rotate_left(11).wrapping_mul(P1); + i += 1; + } + h ^= h >> 15; h = h.wrapping_mul(P2); h ^= h >> 13; h = h.wrapping_mul(P3); h ^= h >> 16; + h + } + let hashes: [(&str, fn(&[u8]) -> u32); 7] = [ + ("FNV1a32", fnv1a32), ("FNV132", fnv132), ("djb2", djb2), ("sdbm", sdbm), + ("Jenkins", joaat), ("Murmur3", |b| murmur3_32(b, 0)), ("xxHash32", |b| xxh32(b, 0))]; + let mut hits = 0; + for (hname, hf) in &hashes { + for start in 0..552usize { + for len in 1..=(552 - start) { + let h0 = hf(&pairs[0].0[start..start + len]); + for (ename, resp) in [("LE", &resp_le), ("BE", &resp_be)] { + if h0 == resp[0] && (1..n).all(|i| hf(&pairs[i].0[start..start + len]) == resp[i]) { + println!(" ★ 直接命中 {} [{}..{}] {}", hname, start, start + len, ename); + hits += 1; + } + // XOR-Δ (hash^key 构造) + let d0 = resp[0] ^ h0; + if (1..n).all(|i| (resp[i] ^ hf(&pairs[i].0[start..start + len])) == d0) { + println!(" ★ XOR-Δ 命中 {} [{}..{}] {} Δ=0x{:08x}", hname, start, start + len, ename, d0); + hits += 1; + } + } + } + } + } + if hits == 0 { println!(" (无)"); } + + println!("\n=== 分析完成 ==="); +} diff --git a/src/dmxhash.rs b/src/dmxhash.rs new file mode 100644 index 0000000..57b2956 --- /dev/null +++ b/src/dmxhash.rs @@ -0,0 +1,1201 @@ +// dmxhash.rs — DMX-A 帧 0x22-0x29 八字节标签算法爆破 + seq@0x1A 验证 +// 用法: enet-validator dmxhash <主机IP> + +// ---------- CRC 表 ---------- +fn crc64_table(poly: u64, reflected: bool) -> [u64; 256] { + let mut t = [0u64; 256]; + for i in 0..256u64 { + let mut c = if reflected { i } else { i << 56 }; + for _ in 0..8 { + c = if reflected { + if c & 1 != 0 { (c >> 1) ^ poly } else { c >> 1 } + } else if c & 0x8000_0000_0000_0000 != 0 { + (c << 1) ^ poly + } else { + c << 1 + }; + } + t[i as usize] = c; + } + t +} +fn crc64(t: &[u64; 256], data: &[u8], init: u64, xorout: u64, reflected: bool) -> u64 { + let mut c = init; + for &b in data { + c = if reflected { + t[((c ^ b as u64) & 0xFF) as usize] ^ (c >> 8) + } else { + t[(((c >> 56) ^ b as u64) & 0xFF) as usize] ^ (c << 8) + }; + } + c ^ xorout +} +pub(crate) fn crc32_table() -> [u32; 256] { + let mut t = [0u32; 256]; + for i in 0..256u32 { + let mut c = i; + for _ in 0..8 { + c = if c & 1 != 0 { (c >> 1) ^ 0xEDB88320 } else { c >> 1 }; + } + t[i as usize] = c; + } + t +} +pub(crate) fn crc32(t: &[u32; 256], data: &[u8]) -> u32 { + let mut c = 0xFFFF_FFFFu32; + for &b in data { + c = t[((c ^ b as u32) & 0xFF) as usize] ^ (c >> 8); + } + c ^ 0xFFFF_FFFF +} +fn fnv1a64(data: &[u8]) -> u64 { + let mut h = 0xcbf29ce484222325u64; + for &b in data { + h ^= b as u64; + h = h.wrapping_mul(0x100000001b3); + } + h +} +fn fnv1_64(data: &[u8]) -> u64 { + let mut h = 0xcbf29ce484222325u64; + for &b in data { + h = h.wrapping_mul(0x100000001b3); + h ^= b as u64; + } + h +} +// xxHash64 (seed 0) +fn xxh64(data: &[u8]) -> u64 { + const P1: u64 = 0x9E3779B185EBCA87; + const P2: u64 = 0xC2B2AE3D27D4EB4F; + const P3: u64 = 0x165667B19E3779F9; + const P4: u64 = 0x85EBCA77C2B2AE63; + const P5: u64 = 0x27D4EB2F165667C5; + let len = data.len() as u64; + let mut i = 0usize; + let mut h: u64; + if data.len() >= 32 { + let mut v1 = P1.wrapping_add(P2); + let mut v2 = P2; + let mut v3 = 0u64; + let mut v4 = P1.wrapping_neg(); + while i + 32 <= data.len() { + let rd = |o: usize| u64::from_le_bytes(data[o..o + 8].try_into().unwrap()); + v1 = (v1.wrapping_add(rd(i).wrapping_mul(P2))).rotate_left(31).wrapping_mul(P1); + v2 = (v2.wrapping_add(rd(i + 8).wrapping_mul(P2))).rotate_left(31).wrapping_mul(P1); + v3 = (v3.wrapping_add(rd(i + 16).wrapping_mul(P2))).rotate_left(31).wrapping_mul(P1); + v4 = (v4.wrapping_add(rd(i + 24).wrapping_mul(P2))).rotate_left(31).wrapping_mul(P1); + i += 32; + } + h = v1.rotate_left(1) + .wrapping_add(v2.rotate_left(7)) + .wrapping_add(v3.rotate_left(12)) + .wrapping_add(v4.rotate_left(18)); + for v in [v1, v2, v3, v4] { + let v = (v.wrapping_mul(P2)).rotate_left(31).wrapping_mul(P1); + h ^= v; + h = h.wrapping_mul(P1).wrapping_add(P4); + } + } else { + h = P5; + } + h = h.wrapping_add(len); + while i + 8 <= data.len() { + let k = u64::from_le_bytes(data[i..i + 8].try_into().unwrap()) + .wrapping_mul(P2) + .rotate_left(31) + .wrapping_mul(P1); + h ^= k; + h = h.rotate_left(27).wrapping_mul(P1).wrapping_add(P4); + i += 8; + } + if i + 4 <= data.len() { + h ^= (u32::from_le_bytes(data[i..i + 4].try_into().unwrap()) as u64).wrapping_mul(P1); + h = h.rotate_left(23).wrapping_mul(P2).wrapping_add(P3); + i += 4; + } + while i < data.len() { + h ^= (data[i] as u64).wrapping_mul(P5); + h = h.rotate_left(11).wrapping_mul(P1); + i += 1; + } + h ^= h >> 33; + h = h.wrapping_mul(P2); + h ^= h >> 29; + h = h.wrapping_mul(P3); + h ^= h >> 32; + h +} + +pub(crate) fn h2b(hex: &str) -> Option> { + if hex.len() % 2 != 0 || hex.is_empty() { + return None; + } + (0..hex.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&hex[i..i + 2], 16).ok()) + .collect() +} + +// ---------- MD5 ---------- +pub(crate) fn md5(data: &[u8]) -> [u8; 16] { + const S: [u32; 64] = [ + 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, + 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, + 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, + 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, + ]; + const K: [u32; 64] = [ + 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501, + 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, + 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, + 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a, + 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, + 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, + 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1, + 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391, + ]; + let mut a0 = 0x67452301u32; + let mut b0 = 0xefcdab89u32; + let mut c0 = 0x98badcfeu32; + let mut d0 = 0x10325476u32; + let mut msg = data.to_vec(); + let bl = (data.len() as u64).wrapping_mul(8); + msg.push(0x80); + while msg.len() % 64 != 56 { + msg.push(0); + } + msg.extend_from_slice(&bl.to_le_bytes()); + for chunk in msg.chunks_exact(64) { + let mut m = [0u32; 16]; + for i in 0..16 { + m[i] = u32::from_le_bytes(chunk[i * 4..i * 4 + 4].try_into().unwrap()); + } + let (mut a, mut b, mut c, mut d) = (a0, b0, c0, d0); + for i in 0..64 { + let (f, g) = match i / 16 { + 0 => ((b & c) | (!b & d), i), + 1 => ((d & b) | (!d & c), (5 * i + 1) % 16), + 2 => (b ^ c ^ d, (3 * i + 5) % 16), + _ => (c ^ (b | !d), (7 * i) % 16), + }; + let tmp = d; + d = c; + c = b; + b = b.wrapping_add( + a.wrapping_add(f).wrapping_add(K[i]).wrapping_add(m[g]).rotate_left(S[i]), + ); + a = tmp; + } + a0 = a0.wrapping_add(a); + b0 = b0.wrapping_add(b); + c0 = c0.wrapping_add(c); + d0 = d0.wrapping_add(d); + } + let mut out = [0u8; 16]; + out[0..4].copy_from_slice(&a0.to_le_bytes()); + out[4..8].copy_from_slice(&b0.to_le_bytes()); + out[8..12].copy_from_slice(&c0.to_le_bytes()); + out[12..16].copy_from_slice(&d0.to_le_bytes()); + out +} + +// ---------- SHA1 ---------- +pub(crate) fn sha1(data: &[u8]) -> [u8; 20] { + let mut h: [u32; 5] = [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0]; + let mut msg = data.to_vec(); + let bl = (data.len() as u64).wrapping_mul(8); + msg.push(0x80); + while msg.len() % 64 != 56 { + msg.push(0); + } + msg.extend_from_slice(&bl.to_be_bytes()); + for chunk in msg.chunks_exact(64) { + let mut w = [0u32; 80]; + for i in 0..16 { + w[i] = u32::from_be_bytes(chunk[i * 4..i * 4 + 4].try_into().unwrap()); + } + for i in 16..80 { + w[i] = (w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]).rotate_left(1); + } + let (mut a, mut b, mut c, mut d, mut e) = (h[0], h[1], h[2], h[3], h[4]); + for i in 0..80 { + let (f, k) = match i / 20 { + 0 => ((b & c) | (!b & d), 0x5A827999u32), + 1 => (b ^ c ^ d, 0x6ED9EBA1), + 2 => ((b & c) | (b & d) | (c & d), 0x8F1BBCDC), + _ => (b ^ c ^ d, 0xCA62C1D6), + }; + let tmp = a + .rotate_left(5) + .wrapping_add(f) + .wrapping_add(e) + .wrapping_add(k) + .wrapping_add(w[i]); + e = d; + d = c; + c = b.rotate_left(30); + b = a; + a = tmp; + } + h[0] = h[0].wrapping_add(a); + h[1] = h[1].wrapping_add(b); + h[2] = h[2].wrapping_add(c); + h[3] = h[3].wrapping_add(d); + h[4] = h[4].wrapping_add(e); + } + let mut out = [0u8; 20]; + for i in 0..5 { + out[i * 4..i * 4 + 4].copy_from_slice(&h[i].to_be_bytes()); + } + out +} + +pub fn run(tsv: &str, host_ip: &str) { + let raw = std::fs::read(tsv).expect("读 TSV 失败"); + // PowerShell 重定向产出 UTF-16LE (BOM ff fe); 兼容 UTF-8 + let content = if raw.starts_with(&[0xFF, 0xFE]) { + let u16s: Vec = raw[2..] + .chunks_exact(2) + .map(|c| u16::from_le_bytes([c[0], c[1]])) + .collect(); + String::from_utf16_lossy(&u16s) + } else { + String::from_utf8_lossy(&raw).into_owned() + }; + let mut frames: Vec<(f64, Vec)> = Vec::new(); + for line in content.lines() { + let p: Vec<&str> = line.split('\t').collect(); + if p.len() < 5 || p[4].trim().is_empty() { + continue; + } + if p[1].trim() != host_ip { + continue; + } + let b = match h2b(p[4].trim()) { + Some(b) => b, + None => continue, + }; + if b.len() != 1084 || b[4] != 4 || b[0x20] != 1 || b[0x21] != 1 { + continue; + } + let t: f64 = p[0].parse().unwrap_or(0.0); + frames.push((t, b)); + } + println!("DMX-A 帧: {}", frames.len()); + if frames.is_empty() { + return; + } + + // --- seq@0x1A 单调性 --- + let mut inc = 0u32; + let mut wrap = 0u32; + let mut gaps = std::collections::HashMap::new(); + for i in 1..frames.len() { + let s0 = u16::from_le_bytes([frames[i - 1].1[0x1A], frames[i - 1].1[0x1B]]); + let s1 = u16::from_le_bytes([frames[i].1[0x1A], frames[i].1[0x1B]]); + let d = s1.wrapping_sub(s0); + *gaps.entry(d).or_insert(0u32) += 1; + if d == 1 { + inc += 1; + } else if s1 < s0 { + wrap += 1; + } + } + let mut gv: Vec<_> = gaps.iter().collect(); + gv.sort_by_key(|(k, _)| **k); + println!( + "seq@0x1A: +1步进={}/{} 回绕={} 步长分布前8: {:?}", + inc, + frames.len() - 1, + wrap, + gv.iter().take(8).collect::>() + ); + + // --- 标签爆破 --- + let ecma = crc64_table(0x42F0E1EBA9EA3693, false); + let iso = crc64_table(0x000000000000001B, true); + let ecma_r = crc64_table(0xC96C5795D7870F42, true); + let c32 = crc32_table(); + let n = frames.len().min(500); + + // (名字, 计算函数) — 输入 (帧, 范围id) + // 范围: 0=data 1=seq||data 2=seq||hdr2C||data 3=marker(0101)||seq||data + let build = |b: &Vec, range: u8| -> Vec { + let mut v = Vec::with_capacity(1100); + match range { + 0 => v.extend_from_slice(&b[0x3C..]), + 1 => { + v.extend_from_slice(&b[0x1A..0x1C]); + v.extend_from_slice(&b[0x3C..]); + } + 2 => { + v.extend_from_slice(&b[0x1A..0x1C]); + v.extend_from_slice(&b[0x2C..0x3C]); + v.extend_from_slice(&b[0x3C..]); + } + 3 => { + v.extend_from_slice(&b[0x20..0x22]); + v.extend_from_slice(&b[0x1A..0x1C]); + v.extend_from_slice(&b[0x3C..]); + } + _ => {} + } + v + }; + let hashes: Vec<(&str, Box u64>)> = vec![ + ("CRC64-ECMA", Box::new(move |d: &[u8]| crc64(&ecma, d, 0, 0, false))), + ("CRC64-XZ", Box::new(move |d: &[u8]| crc64(&iso, d, u64::MAX, u64::MAX, true))), + ("CRC64-ISO", Box::new(move |d: &[u8]| crc64(&iso, d, 0, 0, true))), + ("CRC64-ECMA-r", Box::new(move |d: &[u8]| crc64(&ecma_r, d, 0, 0, true))), + ("FNV1a64", Box::new(fnv1a64)), + ("FNV1-64", Box::new(fnv1_64)), + ("xxHash64", Box::new(xxh64)), + ]; + for (name, hf) in &hashes { + for range in 0..4u8 { + let mut m_le = 0u32; + let mut m_be = 0u32; + for (_, b) in frames.iter().take(n) { + let d = build(b, range); + let h = hf(&d); + let tag = u64::from_le_bytes(b[0x22..0x2A].try_into().unwrap()); + if h == tag { + m_le += 1; + } + if h.swap_bytes() == tag { + m_be += 1; + } + } + if m_le > 0 || m_be > 0 { + println!(" {} range={}: LE匹配={}/{} BE匹配={}/{}", name, range, m_le, n, m_be, n); + } + } + } + // CRC32 对假设: tag = u32_a||u32_b, a/b ∈ {crc32(port1), crc32(port2), crc32(seq||port1), crc32(seq||port2)} + let mut combos: std::collections::HashMap = std::collections::HashMap::new(); + for (_, b) in frames.iter().take(n) { + let p1 = &b[0x3C..0x23C]; + let p2 = &b[0x23C..0x43C]; + let mut s1 = b[0x1A..0x1C].to_vec(); + s1.extend_from_slice(p1); + let mut s2 = b[0x1A..0x1C].to_vec(); + s2.extend_from_slice(p2); + let cands = [ + ("p1", crc32(&c32, p1)), + ("p2", crc32(&c32, p2)), + ("s+p1", crc32(&c32, &s1)), + ("s+p2", crc32(&c32, &s2)), + ("all", crc32(&c32, &b[0x3C..])), + ]; + let ta = u32::from_le_bytes(b[0x22..0x26].try_into().unwrap()); + let tb = u32::from_le_bytes(b[0x26..0x2A].try_into().unwrap()); + for (na, ca) in &cands { + if *ca == ta { + *combos.entry(format!("tag[0:4]=crc32({})", na)).or_insert(0) += 1; + } + if ca.swap_bytes() == ta { + *combos.entry(format!("tag[0:4]=BE crc32({})", na)).or_insert(0) += 1; + } + if *ca == tb { + *combos.entry(format!("tag[4:8]=crc32({})", na)).or_insert(0) += 1; + } + if ca.swap_bytes() == tb { + *combos.entry(format!("tag[4:8]=BE crc32({})", na)).or_insert(0) += 1; + } + } + } + for (k, v) in &combos { + println!(" CRC32对: {} 匹配 {}/{}", k, v, n); + } + println!("爆破完成 ({} 帧样本)", n); + + // --- MD5/SHA1 截断标签假设 --- + // 范围: 0=data 1=seq||data 2=全帧(tag置零) 3=0x20..end(tag置零) + let cranges: Vec<(&str, Box) -> Vec>)> = vec![ + ("data", Box::new(|b: &Vec| b[0x3C..].to_vec())), + ("seq+data", Box::new(|b: &Vec| { + let mut v = b[0x1A..0x1C].to_vec(); + v.extend_from_slice(&b[0x3C..]); + v + })), + ("frame_tag0", Box::new(|b: &Vec| { + let mut v = b.clone(); + for i in 0x22..0x2A { v[i] = 0; } + v + })), + ("0x20e_tag0", Box::new(|b: &Vec| { + let mut v = b[0x20..].to_vec(); + for i in 2..10 { v[i] = 0; } + v + })), + ("hdr14+data", Box::new(|b: &Vec| { + let mut v = b[0x1A..0x22].to_vec(); // seq + 00000000 + 0101 + v.extend_from_slice(&b[0x2A..0x3C]); + v.extend_from_slice(&b[0x3C..]); + v + })), + ]; + let hashers: [(&str, &dyn Fn(&[u8]) -> Vec); 2] = [ + ("MD5", &|d: &[u8]| md5(d).to_vec()), + ("SHA1", &|d: &[u8]| sha1(d).to_vec()), + ]; + for (rname, rf) in &cranges { + for (hname, hf) in &hashers { + let mut hits = [0u32; 4]; // first8, last8, first8BE-rev, last8BE-rev + for (_, b) in frames.iter().take(n) { + let d = rf(b); + let h = hf(&d); + let tag: [u8; 8] = b[0x22..0x2A].try_into().unwrap(); + if h[..8] == tag[..] { hits[0] += 1; } + if h[h.len() - 8..] == tag[..] { hits[1] += 1; } + let mut r8 = h[..8].to_vec(); r8.reverse(); + if r8[..] == tag[..] { hits[2] += 1; } + let mut l8 = h[h.len() - 8..].to_vec(); l8.reverse(); + if l8[..] == tag[..] { hits[3] += 1; } + } + if hits.iter().any(|&x| x > 0) { + println!(" {}({}) 命中: 前8={} 后8={} 前8rev={} 后8rev={} / {}", hname, rname, hits[0], hits[1], hits[2], hits[3], n); + } + } + } + + // --- tag = g(seq)? 常见 index→PRNG 函数 --- + fn splitmix64(mut x: u64) -> u64 { + x = (x ^ (x >> 30)).wrapping_mul(0xBF58476D1CE4E5B9); + x = (x ^ (x >> 27)).wrapping_mul(0x94D049BB133111EB); + x ^ (x >> 31) + } + fn fmix64(mut h: u64) -> u64 { + h ^= h >> 33; + h = h.wrapping_mul(0xff51afd7ed558ccd); + h ^= h >> 33; + h = h.wrapping_mul(0xc4ceb9fe1a85ec53); + h ^ (h >> 33) + } + let seq_funcs: Vec<(&str, Box u64>)> = vec![ + ("splitmix64(seq)", Box::new(splitmix64)), + ("fmix64(seq)", Box::new(fmix64)), + ("splitmix64(seq<<32)", Box::new(|s| splitmix64(s << 32))), + ("fmix64(seq<<32)", Box::new(|s| fmix64(s << 32))), + ("xxh64(seq_le8)", Box::new(|s| xxh64(&s.to_le_bytes()))), + ("fnv1a(seq_le8)", Box::new(|s| fnv1a64(&s.to_le_bytes()))), + ("splitmix64(seq+0x9e37)", Box::new(|s| splitmix64(s.wrapping_add(0x9e3779b97f4a7c15)))), + ]; + for (name, f) in &seq_funcs { + let mut m = 0u32; + for (_, b) in frames.iter().take(n) { + let seq = u16::from_le_bytes([b[0x1A], b[0x1B]]) as u64; + let tag = u64::from_le_bytes(b[0x22..0x2A].try_into().unwrap()); + if f(seq) == tag || f(seq).swap_bytes() == tag { + m += 1; + } + } + if m > 0 { + println!(" {} : 匹配 {}/{}", name, m, n); + } + } + // --- 哈希链: tag[i+1] = f(tag[i])? --- + let chain_funcs: Vec<(&str, Box u64>)> = vec![ + ("splitmix64", Box::new(splitmix64)), + ("fmix64", Box::new(fmix64)), + ("xorshift64*", Box::new(|mut x: u64| { + x ^= x >> 12; + x ^= x << 25; + x ^= x >> 27; + x.wrapping_mul(0x2545F4914F6CDD1D) + })), + ("LCG-Knuth", Box::new(|x: u64| x.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407))), + ]; + for (name, f) in &chain_funcs { + let mut m = 0u32; + for i in 1..n { + let t0 = u64::from_le_bytes(frames[i - 1].1[0x22..0x2A].try_into().unwrap()); + let t1 = u64::from_le_bytes(frames[i].1[0x22..0x2A].try_into().unwrap()); + if f(t0) == t1 { + m += 1; + } + } + if m > 0 { + println!(" 链 {} : 匹配 {}/{}", name, m, n - 1); + } + } + + // --- 分块校验和假设: 1024B 数据切成 N 块, tag[i] = f(块i)? --- + for blk in [64usize, 128, 256] { + let nblk = 1024 / blk; + if nblk > 8 { + continue; + } + let mut sum_ok = vec![0u32; nblk]; + let mut xor_ok = vec![0u32; nblk]; + let mut crc8_ok = vec![0u32; nblk]; + for (_, b) in frames.iter().take(n) { + let data = &b[0x3C..0x43C]; + for i in 0..nblk { + let chunk = &data[i * blk..(i + 1) * blk]; + let s = chunk.iter().fold(0u8, |a, &x| a.wrapping_add(x)); + let x = chunk.iter().fold(0u8, |a, &v| a ^ v); + let mut c = 0u8; + for &v in chunk { + c ^= v; + for _ in 0..8 { + c = if c & 0x80 != 0 { (c << 1) ^ 0x07 } else { c << 1 }; + } + } + let tag = b[0x22 + i]; + if s == tag { + sum_ok[i] += 1; + } + if x == tag { + xor_ok[i] += 1; + } + if c == tag { + crc8_ok[i] += 1; + } + } + } + println!( + "块{}B: sum8命中={:?} xor8命中={:?} crc8命中={:?} (样本{})", + blk, sum_ok, xor_ok, crc8_ok, n + ); + } + + // --- tag[0] = f(数据前缀)? 逐前缀长测试 --- + for plen in [8usize, 16, 32, 64, 128, 256, 512, 1024] { + let mut sum_ok = 0u32; + let mut xor_ok = 0u32; + for (_, b) in frames.iter().take(n) { + let d = &b[0x3C..0x3C + plen]; + if d.iter().fold(0u8, |a, &x| a.wrapping_add(x)) == b[0x22] { + sum_ok += 1; + } + if d.iter().fold(0u8, |a, &v| a ^ v) == b[0x22] { + xor_ok += 1; + } + } + if sum_ok > n as u32 / 2 || xor_ok > n as u32 / 2 { + println!("tag[0] 前缀{}B: sum8={}/{} xor8={}/{}", plen, sum_ok, n, xor_ok, n); + } + } + + // --- 相同通道数据帧的标签对比 (判断 tag 是否含 seq/随机成分) --- + let mut by_data: std::collections::HashMap, Vec<(u16, [u8; 8])>> = std::collections::HashMap::new(); + for (_, b) in frames.iter() { + let seq = u16::from_le_bytes([b[0x1A], b[0x1B]]); + let tag: [u8; 8] = b[0x22..0x2A].try_into().unwrap(); + by_data.entry(b[0x3C..].to_vec()).or_default().push((seq, tag)); + } + let mut shown = 0; + let mut n_same = 0u32; + let mut n_diff = 0u32; + for (d, v) in &by_data { + if v.len() >= 2 { + let t0 = v[0].1; + for (s, t) in v.iter().skip(1) { + let _ = s; + if *t == t0 { + n_same += 1; + } else { + n_diff += 1; + } + } + if shown < 3 { + shown += 1; + println!("相同数据组 ({}帧, 数据首8B={:02x?}):", v.len(), &d[..8]); + for (s, t) in v.iter().take(6) { + println!(" seq={:04x} tag={}", s, t.iter().map(|x| format!("{:02x}", x)).collect::()); + } + } + } + } + println!("全数据相同组内: tag相同={} tag不同={}", n_same, n_diff); + + // --- tag[0] 与 64B 前缀的统计相关性 (对照: 组内匹配率 vs 随机期望 1/256) --- + let mut by_prefix: std::collections::HashMap, Vec> = std::collections::HashMap::new(); + for (_, b) in frames.iter() { + by_prefix.entry(b[0x3C..0x7C].to_vec()).or_default().push(b[0x22]); + } + let mut pairs = 0u64; + let mut match0 = 0u64; + for v in by_prefix.values() { + for i in 0..v.len() { + for j in i + 1..v.len() { + pairs += 1; + if v[i] == v[j] { + match0 += 1; + } + } + } + } + println!( + "64B前缀组内 tag[0] 匹配: {}/{} (随机期望 ~{:.1})", + match0, + pairs, + pairs as f64 / 256.0 + ); + + // --- 逐字节: 哪些 tag 字节由数据前缀决定? --- + let mut by_p2: std::collections::HashMap, Vec<[u8; 8]>> = std::collections::HashMap::new(); + for (_, b) in frames.iter() { + by_p2.entry(b[0x3C..0x7C].to_vec()).or_default().push(b[0x22..0x2A].try_into().unwrap()); + } + let mut per_byte = vec![0u64; 8]; + let mut tot = 0u64; + for v in by_p2.values() { + for i in 0..v.len() { + for j in i + 1..v.len() { + tot += 1; + for k in 0..8 { + if v[i][k] == v[j][k] { + per_byte[k] += 1; + } + } + } + } + } + println!("64B前缀组内逐字节匹配率 (总对数{}):", tot); + for k in 0..8 { + println!(" tag[{}]: {} ({:.1}%)", k, per_byte[k], per_byte[k] as f64 * 100.0 / tot.max(1) as f64); + } + + // --- 前缀究竟多短就够决定 tag[0]? 逐级缩短 --- + for plen in [4usize, 8, 16, 24, 32, 48] { + let mut bp: std::collections::HashMap, Vec> = std::collections::HashMap::new(); + for (_, b) in frames.iter() { + bp.entry(b[0x3C..0x3C + plen].to_vec()).or_default().push(b[0x22]); + } + let mut p2 = 0u64; + let mut m2 = 0u64; + for v in bp.values() { + for i in 0..v.len() { + for j in i + 1..v.len() { + p2 += 1; + if v[i] == v[j] { + m2 += 1; + } + } + } + } + if p2 > 0 { + println!("前缀{}B: tag[0]组内匹配 {}/{} ({:.2}%)", plen, m2, p2, m2 as f64 * 100.0 / p2 as f64); + } + } + + println!("\n连续帧 tag 各窗口 u32(LE) 增量 vs 时间差 (前 14 帧):"); + for i in 1..frames.len().min(15) { + let b0 = &frames[i - 1].1; + let b1 = &frames[i].1; + let dt_us = (frames[i].0 - frames[i - 1].0) * 1e6; + let a0 = u32::from_le_bytes(b0[0x23..0x27].try_into().unwrap()); + let a1 = u32::from_le_bytes(b1[0x23..0x27].try_into().unwrap()); + let c0 = u32::from_le_bytes(b0[0x27..0x2B].try_into().unwrap()); + let c1 = u32::from_le_bytes(b1[0x27..0x2B].try_into().unwrap()); + println!( + " dt={:8.0}us Δtag[3..7]={:12} Δtag[7..11](越界到0x2A)={:12} seq {:04x}->{:04x}", + dt_us, + a1.wrapping_sub(a0), + c1.wrapping_sub(c0), + u16::from_le_bytes([b0[0x1A], b0[0x1B]]), + u16::from_le_bytes([b1[0x1A], b1[0x1B]]) + ); + } + + // --- tag[0] = f(seq_lo) 还是 f(data[0])? 一致性检验 --- + let mut m_seq: std::collections::HashMap = std::collections::HashMap::new(); + let mut seq_ok = 0u32; + let mut seq_tot = 0u32; + let mut m_dat: std::collections::HashMap = std::collections::HashMap::new(); + let mut dat_ok = 0u32; + let mut dat_tot = 0u32; + for (_, b) in frames.iter() { + let slo = b[0x1A]; + let d0 = b[0x3C]; + let t0 = b[0x22]; + match m_seq.get(&slo) { + Some(&v) => { seq_tot += 1; if v == t0 { seq_ok += 1; } } + None => { m_seq.insert(slo, t0); } + } + match m_dat.get(&d0) { + Some(&v) => { dat_tot += 1; if v == t0 { dat_ok += 1; } } + None => { m_dat.insert(d0, t0); } + } + } + println!("tag[0] 一致性: 按 seq_lo 分组 {}/{} ({:.1}%) 按 data[0] 分组 {}/{} ({:.1}%)", + seq_ok, seq_tot, seq_ok as f64 * 100.0 / seq_tot.max(1) as f64, + dat_ok, dat_tot, dat_ok as f64 * 100.0 / dat_tot.max(1) as f64); + + println!("样本 (seq, data[0..4], tag[0]):"); + for (_, b) in frames.iter().take(12) { + let seq = u16::from_le_bytes([b[0x1A], b[0x1B]]); + println!(" seq={:04x} data=[{:02x} {:02x} {:02x} {:02x}] tag0={:02x}", + seq, b[0x3C], b[0x3D], b[0x3E], b[0x3F], b[0x22]); + } + let mut diff_map: std::collections::HashMap = std::collections::HashMap::new(); + let mut xor_map: std::collections::HashMap = std::collections::HashMap::new(); + for (_, b) in frames.iter() { + *diff_map.entry(b[0x22].wrapping_sub(b[0x3C])).or_insert(0) += 1; + *xor_map.entry(b[0x22] ^ b[0x3C]).or_insert(0) += 1; + } + let mut dv: Vec<_> = diff_map.iter().collect(); + dv.sort_by_key(|(_, c)| std::cmp::Reverse(**c)); + let mut xv: Vec<_> = xor_map.iter().collect(); + xv.sort_by_key(|(_, c)| std::cmp::Reverse(**c)); + println!("tag[0]-data[0] 差值 top5: {:?}", dv.iter().take(5).collect::>()); + println!("tag[0]^data[0] top5: {:?}", xv.iter().take(5).collect::>()); + + // --- 完整 data[0]→tag[0] 映射表: 双射? GF(2) 仿射? --- + let mut tab: std::collections::HashMap = std::collections::HashMap::new(); + for (_, b) in frames.iter() { + tab.entry(b[0x3C]).or_insert(b[0x22]); + } + let mut vals: Vec = tab.values().cloned().collect(); + vals.sort(); + vals.dedup(); + println!("映射表: 已观测输入 {}/256, 输出唯一值 {} 个 ({}双射)", + tab.len(), vals.len(), if vals.len() == tab.len() { "" } else { "非" }); + // GF(2) 仿射检验: T(x^y^z) == T(x)^T(y)^T(z)^T(0) + if let Some(&t0) = tab.get(&0u8) { + let mut affine_ok = 0u32; + let mut affine_tot = 0u32; + let keys: Vec = tab.keys().cloned().collect(); + for &x in &keys { + for &y in &keys { + let z = x ^ y; + if let (Some(&tx), Some(&ty), Some(&tz)) = (tab.get(&x), tab.get(&y), tab.get(&z)) { + affine_tot += 1; + if tz == tx ^ ty ^ t0 { + affine_ok += 1; + } + } + } + } + println!("GF(2) 仿射检验: {}/{} ({:.1}%)", affine_ok, affine_tot, + affine_ok as f64 * 100.0 / affine_tot.max(1) as f64); + } + // 低 32 项打印 + let mut pairs2: Vec<(u8, u8)> = tab.iter().map(|(&k, &v)| (k, v)).collect(); + pairs2.sort(); + let show: Vec = pairs2.iter().take(32).map(|(k, v)| format!("{:02x}->{:02x}", k, v)).collect(); + println!("样本映射: {}", show.join(" ")); + // 是否 T(x) = x XOR g(x)? 即 delta=T(x)^x 是否只依赖低位 + let delta0: Vec = pairs2.iter().take(16).map(|(k, v)| format!("{:02x}", k ^ v)).collect(); + println!("delta 前16: {}", delta0.join(" ")); + + // --- 全对扫描: tag[i] 与 data[j] 的函数一致性 (8×1024) --- + // 对每对 (i,j): 若 data[j] 相同的两帧 tag[i] 总相同 → 存在函数关系 + println!("\n=== tag[i] ← data[j] 函数一致性扫描 (>50% 才报告) ==="); + let sample: Vec<&Vec> = frames.iter().take(2000).map(|(_, b)| b).collect(); + for i in 0..8usize { + let mut best: Vec<(usize, u32, u32)> = Vec::new(); // (j, ok, tot) + for j in 0..1024usize { + let mut map: std::collections::HashMap = std::collections::HashMap::new(); + let mut ok = 0u32; + let mut tot = 0u32; + for b in &sample { + let dv = b[0x3C + j]; + let tv = b[0x22 + i]; + match map.get(&dv) { + Some(&v) => { + tot += 1; + if v == tv { + ok += 1; + } + } + None => { + map.insert(dv, tv); + } + } + } + if tot > 0 && ok * 2 > tot { + best.push((j, ok, tot)); + } + } + best.sort_by_key(|(_, ok, tot)| std::cmp::Reverse((*ok) * 1000 / (*tot).max(1))); + for (j, ok, tot) in best.iter().take(3) { + println!(" tag[{}] ← data[{}] (CH{}): {}/{} ({:.1}%)", + i, j, j + 1, ok, tot, *ok as f64 * 100.0 / *tot as f64); + } + } + + // --- T 变换验证: tag[0]=T(data[0]) 与 tag[4]=T(data[512]) 是否同一 T --- + // T(x) = (x-1) XOR K, K 为会话相关字节, 从首帧自动导出 + let k0 = frames[0].1[0x22] ^ frames[0].1[0x3C].wrapping_sub(1); + let k4 = frames[0].1[0x26] ^ frames[0].1[0x23C].wrapping_sub(1); + println!("会话 K: tag[0] K={:02x} tag[4] K={:02x} ({})", k0, k4, if k0 == k4 { "相同" } else { "不同" }); + let mut t0_ok = 0u32; + let mut t4_ok = 0u32; + for b in &sample { + if b[0x3C].wrapping_sub(1) ^ k0 == b[0x22] { + t0_ok += 1; + } + if b[0x23C].wrapping_sub(1) ^ k4 == b[0x26] { + t4_ok += 1; + } + } + println!("T(x)=(x-1)^K 验证: tag[0] {}/{} tag[4] {}/{}", + t0_ok, sample.len(), t4_ok, sample.len()); + + // --- 帧率与 universe 边界 --- + let dt_total = frames.last().unwrap().0 - frames[0].0; + println!("帧率: {:.2}/s ({} 帧 / {:.1}s)", (frames.len() - 1) as f64 / dt_total, frames.len(), dt_total); +} + +// ---------- 形态 B 事件帧结构分析 (evtf 子命令) ---------- +pub(crate) fn read_tsv(tsv: &str) -> String { + let raw = std::fs::read(tsv).expect("读 TSV 失败"); + if raw.starts_with(&[0xFF, 0xFE]) { + let u16s: Vec = raw[2..] + .chunks_exact(2) + .map(|c| u16::from_le_bytes([c[0], c[1]])) + .collect(); + String::from_utf16_lossy(&u16s) + } else { + String::from_utf8_lossy(&raw).into_owned() + } +} + +fn hexr(b: &[u8]) -> String { + b.iter().map(|x| format!("{:02x}", x)).collect::() +} + +pub fn evt(tsv: &str, host_ip: &str, dev_ip: &str, tsv2_opt: Option<&str>) { + let content = read_tsv(tsv); + let mut ev: Vec<(f64, Vec)> = Vec::new(); // 主机→设备 事件帧 + let mut dmx: Vec<(f64, Vec)> = Vec::new(); // 主机→设备 DMX-A 帧 + for line in content.lines() { + let p: Vec<&str> = line.split('\t').collect(); + if p.len() < 5 || p[4].trim().is_empty() { + continue; + } + if p[1].trim() != host_ip || p[2].trim() != dev_ip { + continue; + } + let b = match h2b(p[4].trim()) { + Some(b) => b, + None => continue, + }; + if b.len() != 1084 || b[4] != 4 { + continue; + } + let t: f64 = p[0].parse().unwrap_or(0.0); + if b[0x20] == 2 && b[0x21] == 0 { + ev.push((t, b)); + } else if b[0x20] == 1 && b[0x21] == 1 { + dmx.push((t, b)); + } + } + println!("事件B帧: {} DMX-A帧: {}", ev.len(), dmx.len()); + if ev.is_empty() { + return; + } + + // --- 1. 事件序号 @0x22 (u32 LE) 单调性 --- + let mut inc = 0u32; + let mut other = 0u32; + for i in 1..ev.len() { + let s0 = u32::from_le_bytes(ev[i - 1].1[0x22..0x26].try_into().unwrap()); + let s1 = u32::from_le_bytes(ev[i].1[0x22..0x26].try_into().unwrap()); + if s1.wrapping_sub(s0) == 1 { + inc += 1; + } else { + other += 1; + } + } + println!("事件序号@0x22(u32): +1步进={}/{} 其他={}", inc, ev.len() - 1, other); + + // --- 2. 全字节变异图谱 (每偏移的不同值个数, 折叠输出区间) --- + let n = ev.len().min(1500); + let mut kinds: Vec = Vec::with_capacity(1084); // 0=常量 1=低变(2-4) 2=高变(>4) + for off in 0..1084usize { + let mut seen: std::collections::HashSet = std::collections::HashSet::new(); + for (_, b) in ev.iter().take(n) { + seen.insert(b[off]); + if seen.len() > 4 { + break; + } + } + kinds.push(match seen.len() { + 1 => 0, + 2..=4 => 1, + _ => 2, + }); + } + println!("\n=== 变异图谱 ({} 帧样本) ===", n); + let mut i = 0usize; + while i < 1084 { + let k = kinds[i]; + let mut j = i; + while j < 1084 && kinds[j] == k { + j += 1; + } + let label = match k { + 0 => "常量", + 1 => "低变", + _ => "高变", + }; + if k == 0 { + let vals = &ev[0].1[i..j]; + let show = if j - i <= 24 { + hexr(vals) + } else { + format!("{}…({}B)", hexr(&vals[..16]), j - i) + }; + println!(" 0x{:03x}-0x{:03x} ({}B) {} [{}]", i, j - 1, j - i, label, show); + } else { + println!(" 0x{:03x}-0x{:03x} ({}B) {}", i, j - 1, j - i, label); + } + i = j; + } + + // --- 3. 分类: (flag@0x30, 0x3C, 0x3D) --- + let mut classes: std::collections::HashMap<(u8, u8, u8), Vec> = std::collections::HashMap::new(); + for (idx, (_, b)) in ev.iter().enumerate() { + classes + .entry((b[0x30], b[0x3C], b[0x3D])) + .or_default() + .push(idx); + } + let mut cl: Vec<_> = classes.iter().collect(); + cl.sort_by_key(|(_, v)| std::cmp::Reverse(v.len())); + println!("\n=== 分类 (flag@0x30, 0x3C, 0x3D) ==="); + for ((flag, c3c, c3d), idxs) in cl.iter().take(12) { + let b = &ev[idxs[0]].1; + println!( + " flag={:02x} 0x3C={:02x} 0x3D={:02x} : {}帧 例[0x18-0x6F]={}", + flag, c3c, c3d, idxs.len(), + hexr(&b[0x18..0x70]) + ); + } + + // --- 4. 事件帧尾部 vs DMX-A 通道数据 (最近时间帧对齐比较) --- + if !dmx.is_empty() { + println!("\n=== 事件帧区段 vs 最近 DMX-A 通道数据 匹配率 ==="); + let mut di = 0usize; + let mut checked = 0u32; + let mut matches: std::collections::HashMap = std::collections::HashMap::new(); + // 对每个事件帧(采样), 比较 event[eoff..] 与 dmx[0x3C..] 的逐字节一致性 + for (t, b) in ev.iter().step_by(7).take(200) { + while di + 1 < dmx.len() && dmx[di + 1].0 <= *t { + di += 1; + } + let d = &dmx[di].1; + for eoff in [0x3Cusize, 0x44, 0x50, 0x64, 0x84] { + let maxlen = (1084 - eoff).min(1024); + let mut m = 0u32; + for k in 0..maxlen { + if b[eoff + k] == d[0x3C + k] { + m += 1; + } + } + let e = matches.entry(eoff).or_insert((0, 0)); + e.0 += m; + e.1 += maxlen as u32; + } + checked += 1; + } + for eoff in [0x3Cusize, 0x44, 0x50, 0x64, 0x84] { + if let Some((m, tot)) = matches.get(&eoff) { + println!(" event[0x{:x}..] vs dmx[0x3C..]: {:.1}% ({} 帧)", eoff, *m as f64 * 100.0 / *tot as f64, checked); + } + } + } + + // --- 5. 非 RDM 事件帧的数据区抽样 (0x50 起前 64B ×3例) --- + let others: Vec<&(f64, Vec)> = ev + .iter() + .filter(|(_, b)| !(b[0x3C] == 1 && (b[0x3D] == 0x24 || b[0x3D] == 0x18 || b[0x3D] == 0x19))) + .take(3) + .collect(); + println!("\n=== 非 RDM 事件帧抽样 ({} 类帧中取) ===", others.len()); + for (t, b) in others { + println!(" t={:.3} [0x18-0x57]={}", t, hexr(&b[0x18..0x58])); + println!(" [0x58-0x97]={}", hexr(&b[0x58..0x98])); + } + + // --- 6. 逐偏移精确标注 (cmd24/18/19 flag=00 各 1 例 + flag=01 1 例) --- + println!("\n=== 逐偏移标注 (0x2A-0x70) ==="); + for want in [(0u8, 0x24u8), (0, 0x18), (0, 0x19), (1, 0x24)] { + if let Some((_, b)) = ev.iter().find(|(_, b)| b[0x30] == want.0 && b[0x3D] == want.1) { + println!("--- flag={:02x} cmd={:02x} ---", want.0, want.1); + let mut line = String::new(); + for o in 0x2A..0x70usize { + line.push_str(&format!("{:02x}:{:02x} ", o, b[o])); + if (o - 0x29) % 8 == 0 { + println!(" {}", line); + line.clear(); + } + } + if !line.is_empty() { + println!(" {}", line); + } + } + } + + // --- 7. 通道图案起点: 事件帧[o..] vs 前一帧 DMX-A[0x3C..] 全偏移匹配 --- + if !dmx.is_empty() { + println!("\n=== 事件帧通道区起点定位 (vs 前一帧 DMX-A) ==="); + let mut di = 0usize; + let mut off_score: std::collections::HashMap = std::collections::HashMap::new(); + for (t, b) in ev.iter().step_by(11).take(150) { + while di + 1 < dmx.len() && dmx[di + 1].0 <= *t { + di += 1; + } + let d = &dmx[di].1; + for o in (0x3C..=0x80usize).step_by(2) { + let len = (1084 - o).min(900); + let mut m = 0u32; + for k in 0..len { + if b[o + k] == d[0x3C + k] { + m += 1; + } + } + let e = off_score.entry(o).or_insert((0, 0)); + e.0 += m; + e.1 += len as u32; + } + } + let mut scores: Vec<_> = off_score.iter().collect(); + scores.sort_by_key(|(_, (m, tot))| std::cmp::Reverse((*m as u64) * 1000 / (*tot as u64).max(1))); + for (o, (m, tot)) in scores.iter().take(6) { + println!(" 起点 0x{:x}: 匹配 {:.1}%", o, *m as f64 * 100.0 / *tot as f64); + } + // 同样 vs 后一帧 DMX-A + let mut off_score2: std::collections::HashMap = std::collections::HashMap::new(); + let mut di2 = 0usize; + for (t, b) in ev.iter().step_by(11).take(150) { + while di2 + 1 < dmx.len() && dmx[di2].0 < *t { + di2 += 1; + } + if di2 >= dmx.len() { + break; + } + let d = &dmx[di2].1; + for o in (0x3C..=0x80usize).step_by(2) { + let len = (1084 - o).min(900); + let mut m = 0u32; + for k in 0..len { + if b[o + k] == d[0x3C + k] { + m += 1; + } + } + let e = off_score2.entry(o).or_insert((0, 0)); + e.0 += m; + e.1 += len as u32; + } + } + let mut scores2: Vec<_> = off_score2.iter().collect(); + scores2.sort_by_key(|(_, (m, tot))| std::cmp::Reverse((*m as u64) * 1000 / (*tot as u64).max(1))); + println!("--- vs 后一帧 DMX-A ---"); + for (o, (m, tot)) in scores2.iter().take(3) { + println!(" 起点 0x{:x}: 匹配 {:.1}%", o, *m as f64 * 100.0 / *tot as f64); + } + } + + // --- 8. 事件帧通道签名在 DMX-A 全集中的定位 (24B 精确匹配) --- + if !dmx.is_empty() { + println!("\n=== 事件帧通道签名(24B) 在 DMX-A 全集中的定位 ==="); + let mut found = 0u32; + let mut notfound = 0u32; + let mut deltas: Vec = Vec::new(); + let mut offsets: std::collections::HashMap = std::collections::HashMap::new(); + for (t, b) in ev.iter().step_by(37).take(60) { + let rdm_len = u32::from_le_bytes(b[0x34..0x38].try_into().unwrap()) as usize; + let start = 0x3C + rdm_len; + if start + 24 > 1084 { + continue; + } + let sig = &b[start..start + 24]; + let mut hit: Option<(f64, usize)> = None; + 'outer: for (dt, d) in dmx.iter() { + for o in 0..=(1024 - 24) { + if &d[0x3C + o..0x3C + o + 24] == sig { + hit = Some((*dt, o)); + break 'outer; + } + } + } + match hit { + Some((dt, o)) => { + found += 1; + deltas.push(dt - t); + *offsets.entry(o).or_insert(0) += 1; + } + None => notfound += 1, + } + } + println!(" 找到={} 未找到={}", found, notfound); + if !deltas.is_empty() { + deltas.sort_by(|a, b| a.partial_cmp(b).unwrap()); + println!( + " 时间差(匹配帧-事件帧)s: min={:.3} p50={:.3} max={:.3}", + deltas[0], + deltas[deltas.len() / 2], + deltas[deltas.len() - 1] + ); + let mut ov: Vec<_> = offsets.iter().collect(); + ov.sort_by_key(|(_, c)| std::cmp::Reverse(**c)); + println!(" 匹配偏移分布: {:?}", ov.iter().take(6).collect::>()); + } + } + + // --- 9. 跨抓包定位: 事件帧通道签名 vs 另一网段的 DMX-A (tsv2, 双网段同抓) --- + if let Some(tsv2) = tsv2_opt { + let content2 = read_tsv(tsv2); + let mut dmx2: Vec<(f64, Vec)> = Vec::new(); + for line in content2.lines() { + let p: Vec<&str> = line.split('\t').collect(); + if p.len() < 5 || p[4].trim().is_empty() { + continue; + } + let b = match h2b(p[4].trim()) { + Some(b) => b, + None => continue, + }; + if b.len() != 1084 || b[4] != 4 || b[0x20] != 1 || b[0x21] != 1 { + continue; + } + let t: f64 = p[0].parse().unwrap_or(0.0); + dmx2.push((t, b)); + } + println!("\n=== 跨抓包定位 ({} 帧 DMX-A from tsv2) ===", dmx2.len()); + let mut found = 0u32; + let mut deltas: Vec = Vec::new(); + let mut offsets: std::collections::HashMap = std::collections::HashMap::new(); + for (t, b) in ev.iter().step_by(23).take(100) { + let rdm_len = u32::from_le_bytes(b[0x34..0x38].try_into().unwrap()) as usize; + let start = 0x3C + rdm_len; + if start + 24 > 1084 { + continue; + } + let sig = &b[start..start + 24]; + let mut hit: Option<(f64, usize)> = None; + 'outer: for (dt, d) in dmx2.iter() { + for o in 0..=(1024 - 24) { + if &d[0x3C + o..0x3C + o + 24] == sig { + hit = Some((*dt, o)); + break 'outer; + } + } + } + if let Some((dt, o)) = hit { + found += 1; + deltas.push(dt - t); + *offsets.entry(o).or_insert(0) += 1; + } + } + println!(" 跨包找到={}/100", found); + if !deltas.is_empty() { + deltas.sort_by(|a, b| a.partial_cmp(b).unwrap()); + println!( + " 时间差s: min={:.3} p50={:.3} max={:.3}", + deltas[0], + deltas[deltas.len() / 2], + deltas[deltas.len() - 1] + ); + let mut ov: Vec<_> = offsets.iter().collect(); + ov.sort_by_key(|(_, c)| std::cmp::Reverse(**c)); + println!(" 偏移分布: {:?}", ov.iter().take(6).collect::>()); + } + } +} diff --git a/src/enet.rs b/src/enet.rs new file mode 100644 index 0000000..00a7adf --- /dev/null +++ b/src/enet.rs @@ -0,0 +1,239 @@ +// enet.rs - e:net 协议公共设施 +use std::net::{SocketAddr, UdpSocket}; +use std::time::{Duration, Instant}; + +pub const BIND: &str = "192.168.123.113:31772"; +pub const BUTLER: &str = "192.168.123.1:31772"; +pub const BCAST_SUB: &str = "192.168.123.255:31772"; +pub const BCAST_ALL: &str = "255.255.255.255:31772"; + +// ---------- 瀛楁璇诲啓 (灏忕) ---------- +pub fn g16(b: &[u8], o: usize) -> u16 { b[o] as u16 | (b[o + 1] as u16) << 8 } +pub fn g32(b: &[u8], o: usize) -> u32 { + b[o] as u32 | (b[o + 1] as u32) << 8 | (b[o + 2] as u32) << 16 | (b[o + 3] as u32) << 24 +} +pub fn p16(b: &mut [u8], o: usize, v: u16) { b[o] = v as u8; b[o + 1] = (v >> 8) as u8; } +pub fn p32(b: &mut [u8], o: usize, v: u32) { + b[o] = v as u8; b[o + 1] = (v >> 8) as u8; b[o + 2] = (v >> 16) as u8; b[o + 3] = (v >> 24) as u8; +} +pub fn find(b: &[u8], pat: &[u8]) -> bool { + b.windows(pat.len()).any(|w| w == pat) +} +pub fn hexs(b: &[u8]) -> String { b.iter().map(|x| format!("{:02x}", x)).collect() } + +// ---------- 妯℃澘鍔犺浇 (缂栬瘧鏈熷祵鍏?pcap 瀹炴祴鏍锋湰) ---------- +/// 真机 RDM 应答帧池: 每行 "<载荷长度> " +pub fn load_rdm_pool() -> Vec> { + let txt = include_str!("../samples/rdm_pool.txt"); + let mut out = Vec::new(); + for line in txt.lines() { + let line = line.trim(); + if line.is_empty() { continue; } + if let Some(pos) = line.find(' ') { + let hex = line[pos + 1..].trim(); + let b: Vec = (0..hex.len()).step_by(2) + .map(|i| u8::from_str_radix(&hex[i..i + 2], 16).unwrap()).collect(); + out.push(b); + } + } + out +} + +pub fn load_template(name: &str) -> Vec { + let hex = match name { + "discovery_req" => include_str!("../samples/discovery_req_222.hex"), + "discovery_resp" => include_str!("../samples/discovery_resp_259.hex"), + "announce" => include_str!("../samples/announce_686.hex"), + "announce_live" => include_str!("../samples/announce_live.hex"), + "heartbeat" => include_str!("../samples/heartbeat_78.hex"), + "euce" => include_str!("../samples/euce_582.hex"), + "dmx_a" => include_str!("../samples/dmx_a_1126.hex"), + "event_b" => include_str!("../samples/event_b_1126.hex"), + "ack" => include_str!("../samples/ack_84.hex"), + "rdm_req" => include_str!("../samples/rdm_req_1126.hex"), + "rdm_disc_req" => include_str!("../samples/rdm_disc_req_1126.hex"), + "claim_h112" => include_str!("../samples/claim_h112.hex"), + "claim_h114" => include_str!("../samples/claim_h114.hex"), + "claim_h116" => include_str!("../samples/claim_h116.hex"), + "aerep" => include_str!("../samples/aerep_113.hex"), + "status" => include_str!("../samples/status_84.hex"), + "rdm_pulse_0" => include_str!("../samples/rdm_pulse_101_0.hex"), + "rdm_pulse_1" => include_str!("../samples/rdm_pulse_101_1.hex"), + "rdm_pulse_2" => include_str!("../samples/rdm_pulse_101_2.hex"), + "rdm_pulse_3" => include_str!("../samples/rdm_pulse_101_3.hex"), + "rdm_pulse_4" => include_str!("../samples/rdm_pulse_101_4.hex"), + "rdm_resp111" => include_str!("../samples/rdm_resp_111.hex"), + "rdm_setack68" => include_str!("../samples/rdm_setack_68.hex"), + + "rdm_info119" => include_str!("../samples/rdm_info_119.hex"), + "register89" => include_str!("../samples/register_89.hex"), + "aere594" => include_str!("../samples/aere_594.hex"), + "announce144" => include_str!("../samples/announce_144.hex"), + "rdm_disc_req" => include_str!("../samples/rdm_disc_req_1126.hex"), + _ => panic!("unknown template {}", name), + }; + let h = hex.trim(); + (0..h.len()).step_by(2).map(|i| u8::from_str_radix(&h[i..i + 2], 16).unwrap()).collect() +} + +// ---------- Socket ---------- +pub fn open() -> UdpSocket { + let s = UdpSocket::bind(BIND).expect("bind 192.168.123.113:31772 澶辫触(缃戝彛/绔彛鍗犵敤?)"); + s.set_broadcast(true).unwrap(); + s.set_read_timeout(Some(Duration::from_millis(2))).unwrap(); + s +} + +/// SO_REUSEADDR bind (Windows) +#[cfg(target_os = "windows")] +pub fn open_reuse(bind_addr: &str) -> UdpSocket { + use std::os::windows::io::FromRawSocket; + #[repr(C)] + struct SockAddrIn { sin_family: u16, sin_port: u16, sin_addr: u32, sin_zero: [u8; 8] } + #[repr(C)] + struct WsaData { wversion: u16, whighversion: u16, szdescription: [u8; 257], szsystemstatus: [u8; 129], imaxsockets: u16, imaxudpdg: u16, lpvendorinfo: usize } + #[link(name = "ws2_32")] + extern "system" { + fn WSAStartup(wver: u16, data: *mut WsaData) -> i32; + fn socket(af: i32, ty: i32, proto: i32) -> usize; + fn setsockopt(s: usize, level: i32, optname: i32, optval: *const u8, optlen: i32) -> i32; + fn bind(s: usize, addr: *const SockAddrIn, len: i32) -> i32; + } + unsafe { + let mut wd: WsaData = std::mem::zeroed(); + WSAStartup(0x0202, &mut wd); + let s = socket(2, 2, 17); // AF_INET, SOCK_DGRAM, IPPROTO_UDP + assert!(s != usize::MAX, "ws socket 澶辫触"); + const SOL_SOCKET: i32 = 0xFFFF; + const SO_REUSEADDR: i32 = 4; + const SO_BROADCAST: i32 = 6; + let one: u8 = 1; + setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &one, 1); + setsockopt(s, SOL_SOCKET, SO_BROADCAST, &one, 1); + let (ip, port) = bind_addr.rsplit_once(':').unwrap(); + let o: Vec = ip.split('.').map(|x| x.parse().unwrap()).collect(); + let a = SockAddrIn { + sin_family: 2, + sin_port: port.parse::().unwrap().to_be(), + sin_addr: u32::from_ne_bytes([o[0], o[1], o[2], o[3]]), + sin_zero: [0; 8], + }; + let r = bind(s, &a, std::mem::size_of::() as i32); + if r != 0 { + let err: i32 = { extern "system" { fn WSAGetLastError() -> i32; } WSAGetLastError() }; + panic!("reuse bind {} 澶辫触 r={} WSAE={}", bind_addr, r, err); + } + let u = UdpSocket::from_raw_socket(s as _); + u.set_broadcast(true).unwrap(); + u.set_read_timeout(Some(Duration::from_millis(2))).unwrap(); + u + } +} + +/// Linux: 鏍囧噯 bind 鍗冲厑璁稿杩涚▼ SO_REUSEADDR 鍏卞瓨, 鐩存帴杩斿洖 (daemon 鐢? +#[cfg(not(target_os = "windows"))] +pub fn open_reuse(bind_addr: &str) -> UdpSocket { + use std::net::UdpSocket as S; + use std::os::unix::io::FromRawFd; + #[repr(C)] + struct SockAddrIn { sin_family: u16, sin_port: u16, sin_addr: u32, sin_zero: [u8; 8] } + #[link(name = "c")] + extern "C" { + fn socket(af: i32, ty: i32, proto: i32) -> i32; + fn setsockopt(s: i32, level: i32, optname: i32, optval: *const u8, optlen: u32) -> i32; + fn bind(s: i32, addr: *const SockAddrIn, len: u32) -> i32; + } + const AF_INET: i32 = 2; + const SOCK_DGRAM: i32 = 2; + const IPPROTO_UDP: i32 = 17; + const IPPROTO_IP: i32 = 0; + const IP_TTL: i32 = 2; + const IP_MTU_DISCOVER: i32 = 10; + const SOL_SOCKET: i32 = 1; + const SO_REUSEADDR: i32 = 2; + const SO_BROADCAST: i32 = 6; + unsafe { + let s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + assert!(s >= 0, "socket 澶辫触"); + let one: i32 = 1; + setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &one as *const i32 as *const u8, 4); + setsockopt(s, SOL_SOCKET, SO_BROADCAST, &one as *const i32 as *const u8, 4); + // 鎸囩汗瀵归綈鐪熸満 Butler: TTL=100, 鍏抽棴 DF (PMTUDISC_DONT) + let ttl: i32 = 100; + setsockopt(s, IPPROTO_IP, IP_TTL, &ttl as *const i32 as *const u8, 4); + let pmtu: i32 = 0; // IP_PMTUDISC_DONT + setsockopt(s, IPPROTO_IP, IP_MTU_DISCOVER, &pmtu as *const i32 as *const u8, 4); + let (ip, port) = bind_addr.rsplit_once(':').unwrap(); + let o: Vec = ip.split('.').map(|x| x.parse().unwrap()).collect(); + let a = SockAddrIn { + sin_family: AF_INET as u16, + sin_port: port.parse::().unwrap().to_be(), + sin_addr: u32::from_ne_bytes([o[0], o[1], o[2], o[3]]), + sin_zero: [0; 8], + }; + let r = bind(s, &a, std::mem::size_of::() as u32); + if r != 0 { + panic!("reuse bind {} 澶辫触 r={} errno={}", bind_addr, r, std::io::Error::last_os_error()); + } + let u = S::from_raw_fd(s); + u.set_broadcast(true).unwrap(); + u.set_read_timeout(Some(Duration::from_millis(2))).unwrap(); + u + } +} + +/// 鍦?dur 鏃堕棿绐楀唴鏀堕泦鎵€鏈夊埌杈剧殑 UDP 包) +pub fn recv_window(s: &UdpSocket, dur: Duration) -> Vec<(SocketAddr, Vec)> { + let deadline = Instant::now() + dur; + let mut out = Vec::new(); + loop { + let now = Instant::now(); + if now >= deadline { break; } + s.set_read_timeout(Some(deadline.saturating_duration_since(now))).unwrap(); + let mut buf = [0u8; 2048]; + match s.recv_from(&mut buf) { + Ok((n, a)) => out.push((a, buf[..n].to_vec())), + Err(_) => break, // 瓒呮椂 + } + } + out +} + +// ---------- 鏀跺寘鍒嗙被 ---------- +#[derive(Debug, Clone)] +pub enum Rx { + /// Butler 鍙戠幇搴旂瓟 (259B 甯?/ 217B 璐熻浇) + DiscoveryResp(Vec), + /// Butler ACK (84B 甯?/ 42B 璐熻浇) ack_seq + Ack(u32), + /// Butler 鐘舵€佸抚 (>90B 璐熻浇, 0200+Butl) + Status(Vec), + /// RDM 搴旂瓟 (璐熻浇 60-90B, 0x28 闀垮害 + cc01) + RdmResp(Vec, usize), + /// 涓绘満瀹e憡搴旂瓟/鍏朵粬 ENet type=01 + EnetType1(usize), + /// 鍏跺畠 + Other(usize), +} + +pub fn classify(b: &[u8]) -> Rx { + if b.len() < 8 || &b[0..4] != b"ENet" { + return Rx::Other(b.len()); + } + let t = g32(b, 4); + if t == 1 { return Rx::EnetType1(b.len()); } + if t == 4 { + if b.len() == 42 && g16(b, 0x18) == 2 && &b[0x1A..0x1E] == b"Butl" { + return Rx::Ack(g32(b, 0x20)); + } + if b.len() > 0x2C && g16(b, 0x18) == 2 && &b[0x1A..0x1E] == b"Butl" { + let rlen = g16(b, 0x28) as usize; + if b.len() >= 0x2A + rlen && b[0x2A] == 0xCC && b[0x2B] == 0x01 { + return Rx::RdmResp(b.to_vec(), rlen); + } + return Rx::Status(b.to_vec()); + } + if b.len() == 217 { return Rx::DiscoveryResp(b.to_vec()); } + } + Rx::Other(b.len()) +} diff --git a/src/hostdemo.rs b/src/hostdemo.rs new file mode 100644 index 0000000..bece818 --- /dev/null +++ b/src/hostdemo.rs @@ -0,0 +1,909 @@ +//! hostdemo.rs — 主机侧等效示例程序 (2026-09-10): 认领真机 + RDM 发现 + 加密 DMX 控制 +//! +//! 全流程不依赖 SymphoLight: +//! 1. 发现 (type=1, 180B) → 217B 应答 +//! 2. sub=01 宣告 → 89B 注册应答 +//! 3. AECH 认领 ×3 轮 (nonce/B12 自选, AERE[0xC0]==G(nonce) 逐轮验证) +//! ★ 真机 .1 只应答源 IP=192.168.123.113 的 AECH (固件过滤, §3.7) +//! 4. RDM 发现: E1.20 DISC_UNIQUE_BRANCH 二分遍历 + DISC 脉冲解码 (§5.4 逆运算) +//! → 每 UID 发 DISC_MUTE(0002) 收 70B ACK → GET DEVICE_INFO/DMX_START_ADDRESS/DEVICE_LABEL +//! 5. DMX 控制: 30fps zPprv0TXRRW 加密帧 (§4.1), 按发现的灯具地址做呼吸图案 +//! +//! 注意: 真机控灯! 且 SymphoLight 若同时运行会插黑场帧 (两主机流交替 → 闪烁)。 + +use std::net::UdpSocket; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +use crate::aech::{agios_dmx_encrypt, agios_g}; +use crate::enet::{load_template, open_reuse}; + +const HOST_RDM_UID: [u8; 6] = [0x45, 0x3a, 0x7f, 0xff, 0xff, 0xff]; // 与真主机一致 (§5.3) +const BCAST_UID: [u8; 6] = [0xff; 6]; + +/// 真机冷启动实测: AECH 认领前主机先发 102B type=2 身份帧 (cold2 @38.087) +/// 主机 IP@0x1C (LE), MAC@0x20 (模板值即本机 123 网卡) +const IDENT102: [u8; 102] = [ + 0x45, 0x4e, 0x65, 0x74, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x7c, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x66, 0x00, 0x00, 0x00, 0x01, 0x00, 0x71, 0x7b, 0xa8, 0xc0, 0x30, 0x0e, + 0xd5, 0x59, 0xbb, 0x36, 0x1a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +/// ★type=02 sub=07 绑定帧 (52B, sym_claim2 @351.1/353.4 — 认领前 2s 各一发): +/// 主机IP+MAC@0x1A/0x1E, 设备IP+MAC@0x25/0x29, ffff/0000@0x2F (两发唯一差异), 6400 f401@0x31 +/// (0x64=100, 0x01f4=500 — 疑似刷新率参数, 原样保留) +fn build_bind52(flag16: [u8; 2]) -> Vec { + let mut f = Vec::with_capacity(52); + f.extend_from_slice(b"ENet"); + f.extend_from_slice(&2u32.to_le_bytes()); + f.extend_from_slice(&[0x00, 0x00, 0x1c, 0x7c]); + f.extend_from_slice(&[0u8; 4]); + f.extend_from_slice(&[0x00, 0x00, 0x01, 0x00]); + f.extend_from_slice(&52u32.to_le_bytes()); + f.extend_from_slice(&[0x07, 0x00]); // sub=07 + f.extend_from_slice(&[0x71, 0x7b, 0xa8, 0xc0]); // 占位: 主机IP 逆序字节 (patch) + f.extend_from_slice(&[0x30, 0x0e, 0xd5, 0x59, 0xbb, 0x36]); // 主机MAC + f.push(0x00); + f.extend_from_slice(&[0x01, 0x7b, 0xa8, 0xc0]); // 设备IP LE = 192.168.123.1 + f.extend_from_slice(&[0x00, 0x16, 0x1c, 0xf1, 0x2e, 0x39]); // 设备MAC + f.extend_from_slice(&flag16); + f.extend_from_slice(&[0x64, 0x00, 0xf4, 0x01]); + f +} + +fn g16(b: &[u8], o: usize) -> u16 { u16::from_le_bytes([b[o], b[o + 1]]) } +fn g32(b: &[u8], o: usize) -> u32 { u32::from_le_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]]) } +fn uid_u64(u: &[u8; 6]) -> u64 { u64::from_be_bytes([0, 0, u[0], u[1], u[2], u[3], u[4], u[5]]) } +fn u64_uid(v: u64) -> [u8; 6] { let b = v.to_be_bytes(); [b[2], b[3], b[4], b[5], b[6], b[7]] } +fn hexs(b: &[u8]) -> String { b.iter().map(|x| format!("{:02x}", x)).collect() } +fn uid_s(u: &[u8; 6]) -> String { hexs(u) } + +struct Rng(u32); +impl Rng { + fn new() -> Self { Rng(SystemTime::now().duration_since(UNIX_EPOCH).unwrap().subsec_nanos() | 1) } + fn next(&mut self) -> u32 { self.0 ^= self.0 << 13; self.0 ^= self.0 >> 17; self.0 ^= self.0 << 5; self.0 & 0x7FFF_FFFF } + fn byte(&mut self) -> u8 { (self.next() >> 8) as u8 } +} + +/// E1.20 请求构建: 在 1084B 事件帧模板上 patch RDM 消息 (§4.2/§5.3) +/// frame: 0x22 qseq(LE), 0x34=msgLen+1(LE), 0x3C 起 = SubSC/msgLen/destUID/srcUID/TN/... +fn build_rdm_frame(dest: &[u8; 6], tn: u8, qseq: u16, cc: u8, pid: u16, pd: &[u8]) -> Vec { + let mut f = load_template("rdm_disc_req"); // 1084B, flag=00 独立帧形态 + let msg_len = 24 + pd.len(); // CC..PD 末 (不含 checksum) + // ★RDM 请求帧头与 DMX 帧不同 (SL 实时帧对照): 计数器/随机槽位必须清零 + f[0x1A..0x20].copy_from_slice(&[0u8; 6]); // 0x1A 计数器(会话内从0) + 0x1E-0x1F + f[0x24..0x2A].copy_from_slice(&[0u8; 6]); // 0x24-0x29 (DMX 帧的 r1/r2 槽位 = 0) + f[0x22] = (qseq & 0xFF) as u8; + f[0x23] = (qseq >> 8) as u8; + f[0x30] = 0; // flag=00 独立查询帧 (真机只应答此类) + f[0x34..0x38].copy_from_slice(&((msg_len + 1) as u32).to_le_bytes()); + f[0x3C] = 0x01; // SubSC + f[0x3D] = msg_len as u8; + f[0x3E..0x44].copy_from_slice(dest); + f[0x44..0x4A].copy_from_slice(&HOST_RDM_UID); + f[0x4A] = tn; + f[0x4B] = 0x00; // 独立帧 port 字段=00 (模板实证; flag=01 内嵌副本才是 01) + f[0x4C] = 0x00; // msgCount + f[0x4D] = 0; f[0x4E] = 0; // subDevice + f[0x4F] = cc; + f[0x50] = (pid >> 8) as u8; + f[0x51] = (pid & 0xFF) as u8; + f[0x52] = pd.len() as u8; + f[0x53..0x53 + pd.len()].copy_from_slice(pd); + // checksum: 含隐式 0xCC 起始字节, 从 CC 起 msgLen 字节 (不含自身), BE + let mut sum: u16 = 0xCC; + for i in 0x3C..0x3C + msg_len { sum = sum.wrapping_add(f[i] as u16); } + let cs = 0x53 + pd.len(); + f[cs] = (sum >> 8) as u8; + f[cs + 1] = (sum & 0xFF) as u8; + f +} + +/// DISC 脉冲解码 (§5.4 逆运算): data = RDM 数据区 (0x2A 起), 返回 Some(UID) 或 None(冲突/非法) +fn decode_pulse(d: &[u8]) -> Option<[u8; 6]> { + if d.len() < 17 || d[0] != 0xAA { return None; } + let mut uid = [0u8; 6]; + let mut sum: u16 = 0; + for i in 0..6 { + let b1 = d[1 + 2 * i]; + let b2 = d[2 + 2 * i]; + // 编码有效性: b1 高位必含 0xAA 模式, b2 必含 0x55 — 违例即多机冲突 + if b1 & 0xAA != 0xAA || b2 & 0x55 != 0x55 { return None; } + uid[i] = (b1 & 0x55) | (b2 & 0xAA); + sum = sum.wrapping_add(b1 as u16).wrapping_add(b2 as u16); + } + let cs_hi = (d[13] & 0x55) | (d[14] & 0xAA); + let cs_lo = (d[15] & 0x55) | (d[16] & 0xAA); + let cs = ((cs_hi as u16) << 8) | cs_lo as u16; + if cs != sum { return None; } // 冲突/损坏 + Some(uid) +} + +/// 从 Butler→主机 应答帧提取 RDM 数据区 (§5.1): 返回 (is_pulse, data) +fn rdm_region(b: &[u8]) -> Option<(bool, &[u8])> { + if b.len() < 0x2E || &b[0..4] != b"ENet" || g32(b, 4) != 4 { return None; } + if g16(b, 0x18) != 0x0002 { return None; } + if &b[0x1A..0x1E] != b"Butl" { return None; } + let rlen = g16(b, 0x28) as usize; + if b.len() < 0x2A + rlen { return None; } + let d = &b[0x2A..0x2A + rlen]; + Some((d[0] == 0xAA, d)) +} + +pub fn run(target: &str, bind_ip: &str, secs: u64) { + let dst = format!("{}:31772", target); + let sock = open_reuse(&format!("{}:31772", bind_ip)); // 源 123.113:31772 (真机过滤+主机惯例) + sock.set_read_timeout(Some(Duration::from_millis(5))).unwrap(); + let mut rng = Rng::new(); + let mut buf = [0u8; 4096]; + + println!("=== 主机侧等效示例: 目标 {} (源 {}) ===", dst, bind_ip); + + // ---- 1) 发现 ---- + let disc = load_template("discovery_req"); + sock.send_to(&disc, &dst).unwrap(); + let mut dev_ok = false; + for _ in 0..150 { + if let Ok((n, src)) = sock.recv_from(&mut buf) { + let b = &buf[..n]; + if n >= 200 && &b[0..4] == b"ENet" && g32(b, 4) == 1 { + println!("[1] 发现应答 {}B from {} (Butler 在线)", n, src); + dev_ok = true; + break; + } + } + } + if !dev_ok { println!("[1] 发现无应答, 终止"); return; } + + let mut b12 = rng.next(); + let mut claim_ok = false; + + // claimonly/quick: 最小认领 (announce144 → 47B → AECH) — 实测成功配方, 无绑定/宣告/前奏 + let hd_mode = std::env::var("HOSTDEMO_MODE").unwrap_or_default(); + if hd_mode.starts_with("claim") || hd_mode == "quick" { + let o2: Vec = bind_ip.split('.').map(|x| x.parse().unwrap()).collect(); + let full = hd_mode == "claimfull"; + if full { + // sub=07 绑定帧 ×4 (ffff→0000→ffff→0000, 0.32s 间隔) + for flag in [[0xff_u8, 0xff_u8], [0x00, 0x00], [0xff, 0xff], [0x00, 0x00]] { + let mut b = build_bind52(flag); + b[0x1A..0x1E].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]); + sock.send_to(&b, &dst).unwrap(); + std::thread::sleep(Duration::from_millis(320)); + } + std::thread::sleep(Duration::from_millis(1500)); + } + let mut ident = IDENT102.to_vec(); + ident[0x1C..0x20].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]); + // runs1-3 成功配方: 仅 announce144 (102B 身份帧) 单播一次, 无绑定/EUCE/宣告 + let ident144 = if full { ident } else { load_template("announce144") }; + sock.send_to(&ident144, &dst).unwrap(); + let mut reg = false; + let w = Instant::now(); + while w.elapsed() < Duration::from_millis(400) { + if let Ok((n, src)) = sock.recv_from(&mut buf) { + if src.ip().to_string() == target && n == 47 { reg = true; break; } + } + } + let nonce = rng.next(); + let b12c = rng.next(); + let mut ch = load_template("claim_h112"); + ch[0x1A..0x1E].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]); + ch[0x88..0x8C].copy_from_slice(&nonce.to_le_bytes()); + ch[0x8C..0x90].copy_from_slice(&b12c.to_le_bytes()); + sock.send_to(&ch, &dst).unwrap(); + let w2 = Instant::now(); + while w2.elapsed() < Duration::from_millis(700) { + if let Ok((n, _)) = sock.recv_from(&mut buf) { + let b = &buf[..n]; + if n > 0xC4 && b.windows(4).any(|w| w == b"AERE") { + let resp = g32(b, 0xC0); + println!("CLAIM_PROBE reg89={} AERE resp=0x{:08x} G=0x{:08x} {}", reg, resp, agios_g(nonce), + if resp == agios_g(nonce) { "PASS" } else { "FAIL" }); + if resp == agios_g(nonce) { + // quick 模式: 认领成功后直通 RDM+DMX 阶段 (跳过绑定/宣告/前奏 — 实测有毒) + if hd_mode == "quick" { + b12 = b12c; + claim_ok = true; + break; + } + } + if !claim_ok { return; } + } + } + } + if !claim_ok { + println!("CLAIM_PROBE reg89={} AERE=none FAIL", reg); + return; + } + } + + // takeover: 跳过认领, 用 SL 上次会话的 B12 续发 DMX+心跳, 探会话是否可直接延续 + if std::env::var("HOSTDEMO_MODE").unwrap_or_default() == "takeover" { + let b12_old: u32 = 0x4366937a; // sym_startup @101.278 AECH 的 B12@0x8C (SL 22:52 会话) + println!("[P] takeover: B12=0x{:08x} 续发 15s DMX(ch101 ramp)+心跳+RDM GET, 侦听回包", b12_old); + let tpl = load_template("dmx_a"); + let hb = load_template("heartbeat"); + let known: [u8; 6] = [0x01, 0x07, 0x34, 0x67, 0x01, 0x25]; // NFS1K2D + let start = Instant::now(); + let mut seq: u32 = 5000; // 帧序号跳开, 避免与 SL 旧计数冲突 + let mut last_hb = Instant::now() - Duration::from_secs(10); + let mut rdm_done = false; + while start.elapsed() < Duration::from_secs(15) { + if last_hb.elapsed() >= Duration::from_secs(6) { + let _ = sock.send_to(&hb, &dst); + last_hb = Instant::now(); + println!(" [P] 心跳已发"); + } + if !rdm_done && start.elapsed() > Duration::from_secs(5) { + rdm_done = true; + let f = build_rdm_frame(&known, 1, 1, 0x20, 0x0060, &[]); // GET DEVICE_INFO + sock.send_to(&f, &dst).unwrap(); + println!(" [P] RDM GET DEVICE_INFO 已发"); + } + let t = start.elapsed().as_secs_f32(); + let mut p1 = [0u8; 512]; + p1[100] = (((t * 0.8).sin() * 0.5 + 0.5) * 255.0) as u8; + let p2 = [0u8; 512]; + let r1 = rng.next(); + let r2 = rng.next(); + let mut f = tpl.clone(); + f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes()); + f[0x2E] = 2; f[0x2F] = 0; + f[0x22..0x26].copy_from_slice(&r1.to_le_bytes()); + f[0x26..0x2A].copy_from_slice(&r2.to_le_bytes()); + f[0x3C..0x23C].copy_from_slice(&agios_dmx_encrypt(&p1, r1, b12_old)); + f[0x23C..0x43C].copy_from_slice(&agios_dmx_encrypt(&p2, r2, b12_old)); + let _ = sock.send_to(&f, &dst); + seq += 1; + while let Ok((n, src)) = sock.recv_from(&mut buf) { + let b = &buf[..n]; + println!(" [P] ← len={} src={} head={}", n, src, hexs(&b[..std::cmp::min(16, n)])); + } + std::thread::sleep(Duration::from_millis(33)); + } + return; + } + + // ---- 2) 宣告: 真主机用 644B type=1 广播宣告 (announce 模板), 主机 IP@0x41 LE; + // 再发 102B type=2 身份帧 (cold2 实测: 紧邻 AECH 之前), IP@0x1C LE ---- + let mode = std::env::var("HOSTDEMO_MODE").unwrap_or_default(); + let o: Vec = bind_ip.split('.').map(|x| x.parse().unwrap()).collect(); + let bcast = format!("{}.255:31772", target.rsplitn(2, '.').nth(1).unwrap_or("192.168.123")); + if mode != "skip2" && mode != "noclaim" { + // 冷启动广播前奏 (cold2=38s, SymphoLight 实测 2.5min 成功; 10s 短前奏被拒!) + // ★门控假说: Butler 须收到 ~13+ 轮 discovery/announce 才武装认领应答 + let pre_secs: u64 = std::env::var("HOSTDEMO_PRE").ok().and_then(|s| s.parse().ok()).unwrap_or(45); + // ★宣告帧用 announce_live 模板 + 新鲜时间戳/随机会话 ID (陈旧宣告 = 僵尸主机, 认领被无视) + let euce = load_template("euce"); // 540B, 无主机相关字段 + let pre = Instant::now(); + let mut queries_dbg = 0; + let mut last = Instant::now() - Duration::from_secs(10); + let mut last_euce = Instant::now() - Duration::from_secs(10); + while pre.elapsed() < Duration::from_secs(pre_secs) { + if last.elapsed() >= Duration::from_secs(2) { + let _ = sock.send_to(&disc, "255.255.255.255:31772"); + let mut ann = build_fresh_announce(&mut rng); + ann[0x41..0x45].copy_from_slice(&[o[3], o[2], o[1], o[0]]); // 主机IP逆序字节@0x41 + if queries_dbg == 0 { + queries_dbg = 1; + let live = load_template("announce_live"); + println!(" [dbg] ann timestamp区: ours={}", hexs(&ann[0x230..0x245])); + println!(" [dbg] ann timestamp区: live={}", hexs(&live[0x230..0x245])); + println!(" [dbg] ann sessid区: ours={}", hexs(&ann[0x219..0x226])); + println!(" [dbg] ann sessid区: live={}", hexs(&live[0x219..0x226])); + let diffs: Vec = (0..ann.len()).filter(|&i| ann[i] != live[i]).collect(); + println!(" [dbg] ann 与 live 模板差异偏移 ({}处): {:?}", diffs.len(), diffs); + } + let _ = sock.send_to(&ann, &bcast); + last = Instant::now(); + } + if last_euce.elapsed() >= Duration::from_secs(6) { + // ★EUCE 每 6s (SymphoLight 启动期实测节奏, 30s 节奏认领被拒) + let _ = sock.send_to(&euce, "255.255.255.255:31772"); + last_euce = Instant::now(); + } + // 排空 (217B 发现应答 / 47B 注册等) + if let Ok((n, src)) = sock.recv_from(&mut buf) { + println!(" [dbg] 前奏期收帧 len={} src={}", n, src); + } + } + let mut ident = IDENT102.to_vec(); + ident[0x1C..0x20].copy_from_slice(&[o[3], o[2], o[1], o[0]]); + // ★sub=07 绑定帧 ×4: ffff→0000→ffff→0000 (间隔 ~0.32s, sym_startup @98.8-99.5 实测) + // 1.8s 后发 ident + for flag in [[0xff_u8, 0xff_u8], [0x00, 0x00], [0xff, 0xff], [0x00, 0x00]] { + let mut b = build_bind52(flag); + b[0x1A..0x1E].copy_from_slice(&[o[3], o[2], o[1], o[0]]); + sock.send_to(&b, &dst).unwrap(); + std::thread::sleep(Duration::from_millis(320)); + } + std::thread::sleep(Duration::from_millis(1800)); + let mut ident = IDENT102.to_vec(); + ident[0x1C..0x20].copy_from_slice(&[o[3], o[2], o[1], o[0]]); + sock.send_to(&ident, &dst).unwrap(); + println!("[2] 冷启动前奏完成 ({}s 广播 + 绑定帧×4 + 身份帧)", pre_secs); + } + let mut reg_ok = false; + for _ in 0..10 { + if let Ok((n, _)) = sock.recv_from(&mut buf) { + if n == 89 { reg_ok = true; break; } + } + } + println!("[2] 宣告 → 注册应答: {}", if reg_ok { "OK (89B)" } else { "未收到 (继续)" }); + + // ---- 3) AECH 认领 ×3 (quick 模式已在上面认领成功; noclaim 跳过) ---- + if !claim_ok { + b12 = rng.next(); + if mode == "noclaim" { + println!("[3] noclaim 模式: 跳过 AECH, 直接以 RDM GET 探测连接态"); + claim_ok = true; + } + for round in 0..3 { + if mode == "noclaim" { break; } + let nonce = rng.next(); + let mut ch = load_template("claim_h112"); + ch[0x1A..0x1E].copy_from_slice(&[o[3], o[2], o[1], o[0]]); + ch[0x88..0x8C].copy_from_slice(&nonce.to_le_bytes()); + ch[0x8C..0x90].copy_from_slice(&b12.to_le_bytes()); + sock.send_to(&ch, &dst).unwrap(); + for _ in 0..10 { + if let Ok((n, src)) = sock.recv_from(&mut buf) { + let b = &buf[..n]; + println!(" [dbg] AECH等待中收帧 len={} src={} magic={:?}", n, src, &b[0..4.min(n)]); + if n > 0xC4 && b.windows(4).any(|w| w == b"AERE") { + let resp = g32(b, 0xC0); + let want = agios_g(nonce); + println!("[3] AECH 第{}轮: resp=0x{:08x} G=0x{:08x} {}", round + 1, resp, want, + if resp == want { "★PASS" } else { "FAIL" }); + if resp == want { claim_ok = true; } + break; + } + } + } + } + } + if !claim_ok { println!("[3] 认领失败 (真机只应答源 123.113 的 AECH), 终止"); return; } + println!(" 认领通过, B12=0x{:08x}", b12); + + // quick 模式: 密钥广播 + 统一会话环 — DMX 持续流 (cold2 实证: RDM 代理仅在 DMX 流活跃时工作) + if hd_mode == "quick" { + println!("[K] 1947 许可密钥广播 4s..."); + send_key_burst(&sock); + session_loop(&sock, &dst, target, &mut rng, b12, secs); + return; + } + + // noclaim 连接态探测: 直接 GET DEVICE_INFO 已知 3 灯具, 验证 Butler 是否仍视我们为主机 + if mode == "noclaim" { + let known: [[u8; 6]; 3] = [ + [0x01, 0x07, 0x34, 0x67, 0x01, 0x25], // NFS1K2D DMX101 + [0x01, 0x07, 0x34, 0x88, 0x09, 0x00], // NFM680S DMX112 + [0x08, 0x99, 0x50, 0x00, 0x26, 0xc9], // HMKJ DMX1-32 + ]; + let mut tn0: u8 = rng.byte(); + for (i, uid) in known.iter().enumerate() { + tn0 = tn0.wrapping_add(1); + let f = build_rdm_frame(uid, tn0, (i + 1) as u16, 0x20, 0x0060, &[]); + sock.send_to(&f, &dst).unwrap(); + let w = Instant::now(); + let mut got = false; + while w.elapsed() < Duration::from_millis(500) { + if let Ok((n, src)) = sock.recv_from(&mut buf) { + if src.ip().to_string() != target { continue; } + let b = &buf[..n]; + println!(" [dbg] 收帧 len={} 0x18={:02x}{:02x}", n, b.get(0x19).copied().unwrap_or(0), b.get(0x18).copied().unwrap_or(0)); + if let Some((false, d)) = rdm_region(b) { + if d.len() >= 0x18 + 19 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0x60] { + let a = u16::from_be_bytes([d[0x18 + 14], d[0x18 + 15]]); + println!(" ★ 连接态确认! UID={} DMX地址={}", uid_s(uid), a); + got = true; + break; + } + } + } + } + if !got { println!(" UID={} 无应答", uid_s(uid)); } + } + return; + } + + // ---- 4) RDM 发现 ---- + let mut tn: u8 = rng.byte(); + let mut qseq: u16 = 1; + let mut fixtures: Vec<[u8; 6]> = Vec::new(); + { + // 4a) 先直查已知 UID 验证 RDM 代理通道 (灯具可能仍处 SL 会话留下的 MUTE 态) + let known: [[u8; 6]; 3] = [ + [0x01, 0x07, 0x34, 0x67, 0x01, 0x25], // NFS1K2D DMX101 + [0x01, 0x07, 0x34, 0x88, 0x09, 0x00], // NFM680S DMX112 + [0x08, 0x99, 0x50, 0x00, 0x26, 0xc9], // HMKJ DMX1-32 + ]; + println!("[4a] 直查已知 UID (GET DEVICE_INFO):"); + for uid in &known { + tn = tn.wrapping_add(1); + let f = build_rdm_frame(uid, tn, qseq, 0x20, 0x0060, &[]); + qseq += 1; + sock.send_to(&f, &dst).unwrap(); + let w = Instant::now(); + let mut ok = false; + while w.elapsed() < Duration::from_millis(400) { + if let Ok((n, src)) = sock.recv_from(&mut buf) { + if src.ip().to_string() != target { continue; } + let b = &buf[..n]; + if let Some((false, d)) = rdm_region(b) { + if d.len() >= 0x18 + 19 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0x60] { + let a = u16::from_be_bytes([d[0x18 + 14], d[0x18 + 15]]); + let footprint = u16::from_be_bytes([d[0x18 + 10], d[0x18 + 11]]); + println!(" ★ {} → DMX地址 {} / {}通道", uid_s(uid), a, footprint); + if !fixtures.contains(uid) { fixtures.push(*uid); } + ok = true; + break; + } + } + } + } + if !ok { println!(" {} → 无应答", uid_s(uid)); } + } + // 先广播 DISC_UN_MUTE (PID=0003): 稳态下主机已把全部灯具 MUTE, + // 被 MUTE 的灯具不应答 UNIQUE_BRANCH → 须先解除静音才能重新发现 + tn = tn.wrapping_add(1); + let um = build_rdm_frame(&BCAST_UID, tn, qseq, 0x10, 0x0003, &[]); + qseq += 1; + sock.send_to(&um, &dst).unwrap(); + std::thread::sleep(Duration::from_millis(80)); + println!("[4] 已广播 DISC_UN_MUTE, 开始 UNIQUE_BRANCH 二分遍历"); + let mut stack: Vec<(u64, u64)> = vec![(0, 0xFFFF_FFFF_FFFF)]; + let mut queries = 0; + let walk_start = Instant::now(); + while let Some((lo, hi)) = stack.pop() { + if queries >= 400 || walk_start.elapsed() > Duration::from_secs(120) { println!(" (发现遍历到达上限)"); break; } + let mut pd = Vec::with_capacity(12); + pd.extend_from_slice(&u64_uid(lo)); + pd.extend_from_slice(&u64_uid(hi)); + // 真机实测: 首个应答延迟可达 ~1.4s, 真主机对同一分支连发多次 → 每分支最多 4 次尝试 × 250ms 窗口 + let mut found: Vec<[u8; 6]> = Vec::new(); + let mut collision = false; + for attempt in 0..4 { + queries += 1; + tn = tn.wrapping_add(1); + let f = build_rdm_frame(&BCAST_UID, tn, qseq, 0x10, 0x0001, &pd); // DISC_UNIQUE_BRANCH + qseq += 1; + if queries == 1 { + let t = load_template("rdm_disc_req"); + let diffs: Vec = (0x2A..0x62).filter(|&i| f[i] != t[i]).collect(); + println!(" [dbg] 首帧与模板差异偏移: {:?} (应仅 qseq/TN/srcUID末字节/PD/checksum)", diffs); + } + sock.send_to(&f, &dst).unwrap(); + let w = Instant::now(); + while w.elapsed() < Duration::from_millis(250) { + if let Ok((n, src)) = sock.recv_from(&mut buf) { + let b = &buf[..n]; + if src.ip().to_string() != target { continue; } + if let Some((is_pulse, d)) = rdm_region(b) { + if is_pulse { + match decode_pulse(d) { + Some(uid) => { if !found.contains(&uid) { found.push(uid); } } + None => collision = true, + } + } + } + } + } + if !found.is_empty() || collision { break; } + let _ = attempt; + } + if collision { + let mid = lo + (hi - lo) / 2; + stack.push((mid + 1, hi)); + stack.push((lo, mid)); + } else if found.len() == 1 { + let uid = found[0]; + if found_valid(&uid) { + println!(" [发现] UID={} (深度 {} 次查询后)", uid_s(&uid), queries); + // DISC_MUTE (PID=0002) → 70B 参数 ACK + tn = tn.wrapping_add(1); + let f2 = build_rdm_frame(&uid, tn, qseq, 0x10, 0x0002, &[]); + qseq += 1; + sock.send_to(&f2, &dst).unwrap(); + let w2 = Instant::now(); + let mut acked = false; + while w2.elapsed() < Duration::from_millis(400) { + if let Ok((n, src)) = sock.recv_from(&mut buf) { + if src.ip().to_string() != target { continue; } + let b = &buf[..n]; + if let Some((false, d)) = rdm_region(b) { + if d.len() >= 0x18 && d[0] == 0xCC && d[0x14] == 0x11 && d[0x15..0x17] == [0x00, 0x02] { + acked = true; break; + } + } + } + } + println!(" MUTE(0002) ACK: {}", if acked { "OK" } else { "未收到" }); + fixtures.push(uid); + } + } else if found.len() > 1 { + let mid = lo + (hi - lo) / 2; + stack.push((mid + 1, hi)); + stack.push((lo, mid)); + } + // found==0: 空分支 + } + println!("[4] RDM 发现完成: {} 台灯具, {} 次查询, {:.1}s", fixtures.len(), queries, walk_start.elapsed().as_secs_f32()); + } + + // ---- 4b) 读参数: DEVICE_INFO / DMX_START_ADDRESS / DEVICE_LABEL ---- + let mut addr_list: Vec = Vec::new(); + for uid in &fixtures { + tn = tn.wrapping_add(1); + let f = build_rdm_frame(uid, tn, qseq, 0x20, 0x0060, &[]); // GET DEVICE_INFO + qseq += 1; + sock.send_to(&f, &dst).unwrap(); + let mut dmx_addr: Option = None; + let mut footprint = 0u16; + let mut sensors = 0u8; + let w = Instant::now(); + while w.elapsed() < Duration::from_millis(400) { + if let Ok((n, src)) = sock.recv_from(&mut buf) { + if src.ip().to_string() != target { continue; } + let b = &buf[..n]; + if let Some((false, d)) = rdm_region(b) { + // d: CC@0 ... CC字段@0x14, PID@0x15-16, PDL@0x17, PD@0x18 + if d.len() >= 0x18 + 19 && d[0] == 0xCC && d[0x14] == 0x21 && d[0x15..0x17] == [0x00, 0x60] { + let pd = &d[0x18..0x18 + 19]; + footprint = u16::from_be_bytes([pd[10], pd[11]]); + dmx_addr = Some(u16::from_be_bytes([pd[14], pd[15]])); + sensors = pd[18]; + break; + } + } + } + } + // GET DMX_START_ADDRESS 复核 + tn = tn.wrapping_add(1); + let f2 = build_rdm_frame(uid, tn, qseq, 0x20, 0x00F0, &[]); + qseq += 1; + sock.send_to(&f2, &dst).unwrap(); + let w2 = Instant::now(); + while w2.elapsed() < Duration::from_millis(400) { + if let Ok((n, src)) = sock.recv_from(&mut buf) { + if src.ip().to_string() != target { continue; } + let b = &buf[..n]; + if let Some((false, d)) = rdm_region(b) { + if d.len() >= 0x18 + 2 && d[0] == 0xCC && d[0x14] == 0x21 && d[0x15..0x17] == [0x00, 0xF0] { + let a = u16::from_be_bytes([d[0x18], d[0x19]]); + if dmx_addr.is_none() { dmx_addr = Some(a); } + break; + } + } + } + } + match dmx_addr { + Some(a) => { + println!(" 灯具 {} → DMX 地址 {} (占用 {} 通道, {} 传感器)", uid_s(uid), a, footprint, sensors); + addr_list.push(a); + } + None => println!(" 灯具 {} → 参数读取超时", uid_s(uid)), + } + } + + // ---- 5) DMX 控制: 呼吸图案 (按发现的灯具地址; 无发现则用已知 101/112/1) ---- + if secs == 0 { println!("[5] secs=0, 跳过 DMX 控制"); return; } + if addr_list.is_empty() { addr_list = vec![101, 112, 1]; println!("[5] 无发现结果, 用已知地址 101/112/1"); } + println!("[5] DMX 控制 {}s @30fps: 呼吸图案 @地址 {:?} (zPprv0TXRRW 加密, B12=0x{:08x})", secs, addr_list, b12); + println!(" ⚠ SymphoLight 若同时运行会插黑场帧造成闪烁; 关其输出可得纯净效果"); + let tpl = load_template("dmx_a"); + let hb = load_template("heartbeat"); // 36B type=02 sub=03 心跳 (会话保活, 每 ~6s 单播) + let start = Instant::now(); + let mut seq: u32 = 1; + let mut sent = 0u64; + let mut last_hb = Instant::now(); + while start.elapsed() < Duration::from_secs(secs) { + if last_hb.elapsed() >= Duration::from_secs(6) { + let _ = sock.send_to(&hb, &dst); + last_hb = Instant::now(); + } + let t = start.elapsed().as_secs_f32(); + let mut p1 = [0u8; 512]; + let mut p2 = [0u8; 512]; + for (k, &a) in addr_list.iter().enumerate() { + let phase = t * 1.2 + k as f32 * 2.1; + let v = ((phase.sin() * 0.5 + 0.5) * 255.0) as u8; + let ch = a.max(1) - 1; // 0-based + if ch < 512 { p1[ch as usize] = v; } else if ch < 1024 { p2[(ch - 512) as usize] = v; } + } + let r1 = rng.next(); + let r2 = rng.next(); + let mut f = tpl.clone(); + f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes()); + f[0x2E] = 2; f[0x2F] = 0; // 节点序号 (真机 .1 = 2) + f[0x22..0x26].copy_from_slice(&r1.to_le_bytes()); + f[0x26..0x2A].copy_from_slice(&r2.to_le_bytes()); + f[0x3C..0x23C].copy_from_slice(&agios_dmx_encrypt(&p1, r1, b12)); + f[0x23C..0x43C].copy_from_slice(&agios_dmx_encrypt(&p2, r2, b12)); + if sock.send_to(&f, &dst).is_ok() { sent += 1; } + seq += 1; + std::thread::sleep(Duration::from_millis(33)); + } + println!("[5] 完成: 发送 {} 帧", sent); +} + +fn found_valid(uid: &[u8; 6]) -> bool { + uid.iter().any(|&b| b != 0) && uid.iter().any(|&b| b != 0xFF) +} + +/// 构造"新鲜"宣告帧: announce_live 模板 (今日 SL 实抓) + 每发随机会话 ID@0x221 +/// + 当前本地时间戳@0x239 (年u16LE 月 日 时 分 秒 毫秒u16LE 时区) — Butler 按 +/// 宣告时间戳判主机新鲜度, 陈旧宣告(旧抓包模板)会被当僵尸主机无视 ident/AECH +fn build_fresh_announce(rng: &mut Rng) -> Vec { + let mut a = load_template("announce_live"); // 644B + let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); + // 本地时间分解 (UTC+8, 与实测 tz=02... 用本地时区偏移) + let secs = now + tz_offset_secs(); + let days = secs / 86400; + let rem = secs % 86400; + let (year, month, day) = civil_from_days(days as i64); + let hour = (rem / 3600) as u8; + let min = ((rem % 3600) / 60) as u8; + let sec = (rem % 60) as u8; + let ms = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().subsec_millis(); + // 时间戳字段实际位于 0x235: 年u16LE@0x235 月@0x237 日@0x238 时@0x239 分@0x23A + // 秒@0x23B 毫秒u16LE@0x23C 时区@0x23E (实测 2026-09-10 22:52:50.992 tz=02) + a[0x235] = (year & 0xFF) as u8; + a[0x236] = (year >> 8) as u8; + a[0x237] = month; + a[0x238] = day; + a[0x239] = hour; + a[0x23A] = min; + a[0x23B] = sec; + let msle = (ms as u16).to_le_bytes(); + a[0x23C] = msle[0]; + a[0x23D] = msle[1]; + a[0x23E] = 0x02; // 时区字段 (实测值) + // 4B 随机会话 ID @0x21D (每发宣告都变; 0x221 的 0c 恒定) + let id = rng.next(); + a[0x21D..0x221].copy_from_slice(&id.to_le_bytes()); + a +} + +fn tz_offset_secs() -> u64 { + // 本地时区偏移 (秒): 用 UTC 与本地时间差 + let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64; + let utc_days = (now.div_euclid(86400) + 4) % 7; // 0=周四 + let _ = utc_days; + // 简化: 读环境 TZ 不可靠, 直接用 Windows 时区 API? 用 chrono 不行(无依赖). + // 实测帧 tz 字节恒 02, 时间字段为本地时间 (UTC+8 中 hour=22 对应当地 22 点) + // → 这里返回 +8h; 若部署在其它时区需调整 + 8 * 3600 +} + +fn civil_from_days(z: i64) -> (u16, u8, u8) { + // Howard Hinnant 算法: days since 1970-01-01 → (y, m, d) + let z = z + 719468; + let era = if z >= 0 { z } else { z - 146096 } / 146097; + let doe = (z - era * 146097) as u64; + let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; + let y = yoe as i64 + era * 400; + let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); + let mp = (5 * doy + 2) / 153; + let d = (doy - (153 * mp + 2) / 5 + 1) as u8; + let m = if mp < 10 { mp + 3 } else { mp - 9 } as u8; + (if m <= 2 { (y + 1) as u16 } else { y as u16 }, m, d) +} + +/// ★1947 端口"密钥报文" (allports.pcap 实测): 40B ASCII 许可令牌, SL 每 ~34s 广播一轮 +/// (双目标 21 帧 @200ms) — Butler 在 1947 监听, 疑为 RDM 代理的许可开关 +const KEY40: &[u8] = b"BlZRQphAGxUbi7at8KEfmdZ7TdSB03b+qoC1RIA"; + +fn send_key_burst(sock: &UdpSocket) { + for _ in 0..21 { + let _ = sock.send_to(KEY40, "192.168.123.255:1947"); + let _ = sock.send_to(KEY40, "255.255.255.255:1947"); + std::thread::sleep(Duration::from_millis(200)); + } +} + +/// 统一会话环 (quick): DMX 30fps 持续 + RDM 穿插 +/// 阶段: 0-2s 黑场(设备预热) → UN_MUTE + UNIQUE_BRANCH 二分发现 → MUTE 已发现灯具 +/// → GET DEVICE_INFO/DMX 地址 → 呼吸图案控制 (未发现则用已知地址) +fn session_loop(sock: &UdpSocket, dst: &str, target: &str, rng: &mut Rng, b12: u32, secs: u64) { + let tpl = load_template("dmx_a"); + let hb = load_template("heartbeat"); + let known: [[u8; 6]; 3] = [ + [0x01, 0x07, 0x34, 0x67, 0x01, 0x25], // NFS1K2D DMX101 + [0x01, 0x07, 0x34, 0x88, 0x09, 0x00], // NFM680S DMX112 + [0x08, 0x99, 0x50, 0x00, 0x26, 0xc9], // HMKJ DMX1-32 + ]; + let mut buf = [0u8; 4096]; + let mut fixtures: Vec<[u8; 6]> = Vec::new(); + let mut addr_list: Vec = Vec::new(); + let mut tn: u8 = rng.byte(); + let mut qseq: u16 = 1; + let mut seq: u32 = 1; + // 发现任务队列: (dest, cc, pid, pd) — None = 分支查询由 walk 状态机生成 + let mut stack: Vec<(u64, u64)> = vec![(0, 0xFFFF_FFFF_FFFF)]; + let mut get_queue: Vec<[u8; 6]> = Vec::new(); + let mut muted: Vec<[u8; 6]> = Vec::new(); + let start = Instant::now(); + let mut last_hb = Instant::now(); + let mut last_key_t: f32 = -100.0; + let mut last_rdm = Instant::now() - Duration::from_secs(1); + let mut unmuting = false; + let mut discovering = true; + let mut collecting = false; + let mut collect_until = Instant::now(); + let mut found: Vec<[u8; 6]> = Vec::new(); + let mut collision = false; + let mut current_branch: Option<(u64, u64)> = None; + let mut sent = 0u64; + + println!("[S] 统一会话环启动: DMX 30fps + RDM 穿插 ({}s)", secs); + while start.elapsed() < Duration::from_secs(secs) { + let t = start.elapsed().as_secs_f32(); + + // --- 心跳每 6s + 密钥每 30s + ★EUCE 每 6s (SL 全程 6s 节奏, 许可信标) --- + if last_hb.elapsed() >= Duration::from_secs(6) { + let _ = sock.send_to(&hb, dst); + let euce = load_template("euce"); + let _ = sock.send_to(&euce, "255.255.255.255:31772"); + last_hb = Instant::now(); + } + if t - last_key_t >= 30.0 { + let _ = sock.send_to(KEY40, "192.168.123.255:1947"); + let _ = sock.send_to(KEY40, "255.255.255.255:1947"); + last_key_t = t; + } + + // --- RDM 任务 (每 ~100ms 一发; ★SL 实测: 认领后 27.5s 纯 DMX, RDM 过早发可能重置武装计时) --- + if discovering && t > 30.0 && last_rdm.elapsed() >= Duration::from_millis(100) { + last_rdm = Instant::now(); + if collecting && Instant::now() >= collect_until { + // 本分支收集结束 → 判定 + collecting = false; + if let Some((lo, hi)) = current_branch.take() { + if collision { + let mid = lo + (hi - lo) / 2; + stack.push((mid + 1, hi)); + stack.push((lo, mid)); + println!(" [S] 分支冲突 → 二分"); + } else if found.len() == 1 && found_valid(&found[0]) { + let uid = found[0]; + println!(" [S] ★发现灯具 {}", uid_s(&uid)); + if !fixtures.contains(&uid) { fixtures.push(uid); } + get_queue.push(uid); + // DISC_MUTE 该灯具 + tn = tn.wrapping_add(1); + let f = build_rdm_frame(&uid, tn, qseq, 0x10, 0x0002, &[]); + qseq += 1; + let _ = sock.send_to(&{ let mut f = f; f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes()); seq += 1; f }, dst); + } else if found.len() > 1 { + let mid = lo + (hi - lo) / 2; + stack.push((mid + 1, hi)); + stack.push((lo, mid)); + } + found.clear(); + collision = false; + } + } + if collecting { + // 收集窗口内每 150ms 重发同分支查询 (真主机连发节奏) + if last_rdm.elapsed() >= Duration::from_millis(150) { + last_rdm = Instant::now(); + if let Some((lo, hi)) = current_branch { + let mut pd = Vec::with_capacity(12); + pd.extend_from_slice(&u64_uid(lo)); + pd.extend_from_slice(&u64_uid(hi)); + tn = tn.wrapping_add(1); + let mut f = build_rdm_frame(&BCAST_UID, tn, qseq, 0x10, 0x0001, &pd); + qseq += 1; + f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes()); + seq += 1; + let _ = sock.send_to(&f, dst); + let mut f2 = f.clone(); + f2[0x30] = 0x01; + let _ = sock.send_to(&f2, dst); + } + } + } + if !collecting { + if !unmuting { + // DISC_UN_MUTE 广播 + tn = tn.wrapping_add(1); + let f = build_rdm_frame(&BCAST_UID, tn, qseq, 0x10, 0x0003, &[]); + qseq += 1; + let _ = sock.send_to(&{ let mut f = f; f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes()); seq += 1; f }, dst); + unmuting = true; + println!("[S] DISC_UN_MUTE 已发"); + } else if let Some((lo, hi)) = stack.pop() { + let mut pd = Vec::with_capacity(12); + pd.extend_from_slice(&u64_uid(lo)); + pd.extend_from_slice(&u64_uid(hi)); + tn = tn.wrapping_add(1); + let mut f = build_rdm_frame(&BCAST_UID, tn, qseq, 0x10, 0x0001, &pd); + qseq += 1; + // SL 实测: RDM 请求成对发送 (flag=00 + flag=01 副本, 背靠背 26μs) + f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes()); + seq += 1; + let _ = sock.send_to(&f, dst); + let mut f2 = f.clone(); + f2[0x30] = 0x01; + let _ = sock.send_to(&f2, dst); + current_branch = Some((lo, hi)); + collecting = true; + collect_until = Instant::now() + Duration::from_millis(500); + } else { + if fixtures.is_empty() && t < 90.0 { + // 未发现 → 重新压栈全范围, 循环重试 (SL 实测: 认领后 ~27.5s RDM 才开始被代理!) + stack.push((0, 0xFFFF_FFFF_FFFF)); + } else { + discovering = false; + println!("[S] 发现完成: {} 台灯具", fixtures.len()); + } + } + } + } + + // --- 收包 (非阻塞): 脉冲/参数应答 --- + while let Ok((n, src)) = sock.recv_from(&mut buf) { + if src.ip().to_string() != target { continue; } + let b = &buf[..n]; + if let Some((is_pulse, d)) = rdm_region(b) { + if is_pulse && collecting { + match decode_pulse(d) { + Some(uid) => { if !found.contains(&uid) { found.push(uid); } } + None => collision = true, + } + } else if !is_pulse && d.len() >= 0x18 + 19 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0x60] { + let addr = u16::from_be_bytes([d[0x18 + 14], d[0x18 + 15]]); + let footprint = u16::from_be_bytes([d[0x18 + 10], d[0x18 + 11]]); + println!(" [S] ★{} → DMX地址 {} / {}通道", uid_s(&[d[9], d[10], d[11], d[12], d[13], d[14]]), addr, footprint); + addr_list.push(addr); + } else if !is_pulse && d.len() >= 0x18 + 2 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0xF0] { + let addr = u16::from_be_bytes([d[0x18], d[0x19]]); + println!(" [S] ★DMX_START_ADDRESS 应答: {}", addr); + addr_list.push(addr); + } + } + } + + // --- GET 队列 (发现后逐台读参数) --- + if !discovering && !get_queue.is_empty() && last_rdm.elapsed() >= Duration::from_millis(120) { + last_rdm = Instant::now(); + let uid = get_queue.remove(0); + tn = tn.wrapping_add(1); + let f = build_rdm_frame(&uid, tn, qseq, 0x20, 0x0060, &[]); + qseq += 1; + let _ = sock.send_to(&{ let mut f = f; f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes()); seq += 1; f }, dst); + let _ = muted; + } + + // --- DMX 帧 @30fps --- + let mut p1 = [0u8; 512]; + let mut p2 = [0u8; 512]; + if !addr_list.is_empty() { + for (k, &a) in addr_list.iter().enumerate() { + let phase = t * 1.2 + k as f32 * 2.1; + let v = (((phase.sin() * 0.5 + 0.5) * 255.0) * (t * 0.5).min(1.0)) as u8; // 淡入 + let ch = a.max(1) - 1; + if ch < 512 { p1[ch as usize] = v; } else if ch < 1024 { p2[(ch - 512) as usize] = v; } + } + } + let r1 = rng.next(); + let r2 = rng.next(); + let mut f = tpl.clone(); + f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes()); + f[0x2E] = 2; f[0x2F] = 0; + f[0x22..0x26].copy_from_slice(&r1.to_le_bytes()); + f[0x26..0x2A].copy_from_slice(&r2.to_le_bytes()); + f[0x3C..0x23C].copy_from_slice(&agios_dmx_encrypt(&p1, r1, b12)); + f[0x23C..0x43C].copy_from_slice(&agios_dmx_encrypt(&p2, r2, b12)); + let _ = sock.send_to(&{ let mut f = f; f[0x1A..0x1E].copy_from_slice(&seq.to_le_bytes()); seq += 1; f }, dst); + seq += 1; + sent += 1; + std::thread::sleep(Duration::from_millis(33)); + } + println!("[S] 会话环结束: DMX {} 帧, 发现灯具 {} 台, 控制地址 {:?}", sent, fixtures.len(), addr_list); +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..6e362cf --- /dev/null +++ b/src/main.rs @@ -0,0 +1,194 @@ +// main.rs — enet-validator: e:net 协议独立验证程序 (模拟 e:cue 主机, 连接真机 Butler S2) +mod enet; +mod tests; +mod dmxhash; +mod aech; +mod hostdemo; + +use tests::Item; + +fn main() { + let arg = std::env::args().nth(1).unwrap_or_else(|| "help".into()); + let mut results: Vec = Vec::new(); + + println!("enet-validator — e:net 协议实测验证 (Butler S2 @ 192.168.123.1)"); + println!("================================================================"); + match arg.as_str() { + "discover" => results.push(tests::t1_discover()), + "announce" => results.push(tests::tx_observe("T2", "announce", false)), + "heartbeat" => results.push(tests::tx_observe("T3", "heartbeat", false)), + "euce" => results.push(tests::tx_observe("T4", "euce", false)), + "session" => { + let secs: u64 = std::env::args().nth(2).and_then(|a| a.parse().ok()).unwrap_or(60); + results.extend(tests::t5_t8_session(secs)); + } + "fallback" => results.push(tests::t9_fallback()), + "claim" => results.push(tests::t12_claim_replay()), + "takeover" => { + let secs: u64 = std::env::args().nth(2).and_then(|a| a.parse().ok()).unwrap_or(15); + results.extend(tests::t13_takeover(secs)); + } + "holdbind" => { + // Rust 先 bind, 等待期间由 SymphoLight 认证; 之后 Rust 发帧测试共存 + let s = enet::open_reuse("0.0.0.0:31772"); + println!("Rust 已 bind 31772 (REUSEADDR)。等待 30s (此间启动 SymphoLight 认领)…"); + std::thread::sleep(std::time::Duration::from_secs(30)); + println!("开始发送测试帧 15s…"); + results.extend(tests::t13_takeover_on(s, 15)); + } + "device" => { + // 用法: device [设备IP] [MAC_hex12] [秒] (默认 192.168.123.200, 本机网卡MAC) + let ip_s = std::env::args().nth(2).unwrap_or_else(|| "192.168.123.200".into()); + let mac_s = std::env::args().nth(3).unwrap_or_else(|| "300ed559bb36".into()); + let secs: u64 = std::env::args().nth(4).and_then(|a| a.parse().ok()).unwrap_or(180); + let relay_s = std::env::args().nth(5); // 第5参: 中继目标 ip[:port] (默31772) + let ip: Vec = ip_s.split('.').filter_map(|x| x.parse().ok()).collect(); + let mac: Vec = (0..mac_s.len()).step_by(2).filter_map(|i| u8::from_str_radix(&mac_s[i..i + 2], 16).ok()).collect(); + let relay: Option<([u8; 4], u16)> = relay_s.and_then(|s| { + let (ips, port) = match s.rsplit_once(':') { + Some((a, p)) => (a.to_string(), p.parse::().unwrap_or(31772)), + None => (s, 31772), + }; + let o: Vec = ips.split('.').filter_map(|x| x.parse().ok()).collect(); + if o.len() == 4 { Some(([o[0], o[1], o[2], o[3]], port)) } else { None } + }); + if ip.len() == 4 && mac.len() == 6 { + let mut ipa = [0u8; 4]; ipa.copy_from_slice(&ip); + let mut maca = [0u8; 6]; maca.copy_from_slice(&mac); + results.extend(tests::device2(ipa, maca, secs, relay)); + } else { + println!("device 参数错误"); + } + } + "rdm" => { + let secs: u64 = std::env::args().nth(2).and_then(|a| a.parse().ok()).unwrap_or(30); + results.extend(tests::t10_t11_rdm(secs)); + } + "bridge" => { + // bridge [secs] — AECH 桥 (非123网段设备认领桥) + let lp = std::env::args().nth(2).unwrap_or_else(|| "192.168.58.107:41772".into()); + let rip_s = std::env::args().nth(3).unwrap_or_else(|| "192.168.123.1".into()); + let secs: u64 = std::env::args().nth(4).and_then(|a| a.parse().ok()).unwrap_or(7200); + let (lip_s, lport) = match lp.rsplit_once(':') { + Some((a, p)) => (a.to_string(), p.parse::().unwrap_or(41772)), + None => (lp, 41772), + }; + let lip: Vec = lip_s.split('.').filter_map(|x| x.parse().ok()).collect(); + let rip: Vec = rip_s.split('.').filter_map(|x| x.parse().ok()).collect(); + if lip.len() == 4 && rip.len() == 4 { + tests::bridge([lip[0], lip[1], lip[2], lip[3]], lport, [rip[0], rip[1], rip[2], rip[3]], secs); + } else { + println!("bridge 参数错误"); + } + return; + } + "all" => { + results.push(tests::t1_discover()); + results.push(tests::tx_observe("T2", "announce", false)); + results.push(tests::tx_observe("T3", "heartbeat", false)); + results.push(tests::tx_observe("T4", "euce", false)); + results.extend(tests::t5_t8_session(60)); + results.push(tests::t9_fallback()); + } + "rdmprobe" => { + let target = std::env::args().nth(2).unwrap_or_else(|| "192.168.123.1".into()); + let secs: u64 = std::env::args().nth(3).and_then(|a| a.parse().ok()).unwrap_or(10); + results.push(tests::rdm_probe(&target, secs)); + } + "dmxhash" => { + // dmxhash [主机IP] — DMX-A 帧标签算法爆破 (离线分析, 无网络) + let tsv = std::env::args().nth(2).unwrap_or_else(|| "dmx.tsv".into()); + let host = std::env::args().nth(3).unwrap_or_else(|| "192.168.123.113".into()); + dmxhash::run(&tsv, &host); + return; + } + "evtf" => { + // evtf [主机IP] [设备IP] [tsv2跨包] — 形态B事件帧结构分析 (离线) + let tsv = std::env::args().nth(2).unwrap_or_else(|| "dmx.tsv".into()); + let host = std::env::args().nth(3).unwrap_or_else(|| "192.168.123.113".into()); + let dev = std::env::args().nth(4).unwrap_or_else(|| "192.168.123.1".into()); + let tsv2 = std::env::args().nth(5); + dmxhash::evt(&tsv, &host, &dev, tsv2.as_deref()); + return; + } + "aech" => { + // aech [tsv2 ...] — AECH 密钥协商密码学分析 (离线) + let tsvs: Vec = std::env::args().skip(2).collect(); + if tsvs.is_empty() { + println!("用法: enet-validator aech [tsv2 ...]"); + return; + } + aech::run(&tsvs); + return; + } + "aechprobe" => { + // aechprobe <目标IP> — 选择密文仿射性分析 (在线探测真机) + let tsv = std::env::args().nth(2).unwrap_or_else(|| "cold2.tsv".into()); + let target = std::env::args().nth(3).unwrap_or_else(|| "192.168.123.1".into()); + aech::probe(&tsv, &target); + return; + } + "aechprobe2" => { + // aechprobe2 <目标IP> — part1 块/字节影响图谱 (在线) + let tsv = std::env::args().nth(2).unwrap_or_else(|| "cold2.tsv".into()); + let target = std::env::args().nth(3).unwrap_or_else(|| "192.168.123.1".into()); + aech::probe2(&tsv, &target); + return; + } + "aechid" => { + // aechid — f(last4) 离线候选函数鉴别 + let tsv = std::env::args().nth(2).unwrap_or_else(|| "cold2.tsv".into()); + aech::identify(&tsv); + return; + } + "dmxroll" => { + // dmxroll — DMX 滚动值/保护字节数据驱动分析 (离线) + let tsv = std::env::args().nth(2).unwrap_or_else(|| "dmx.tsv".into()); + aech::dmxroll(&tsv); + return; + } + "dmxdec" => { + // dmxdec — DMX 帧加密假说验证 (kWarv47o79k/zPprv0TXRRW 逆运算, 需 TSV 含 AECH) + let tsv = std::env::args().nth(2).unwrap_or_else(|| "dmx.tsv".into()); + aech::dmxdec(&tsv); + return; + } + "dmxsend" => { + // dmxsend <目标IP> [秒] — 主机侧等效播放: 认领 + 加密 DMX 全链路自证 + let ip = std::env::args().nth(2).unwrap_or_else(|| "192.168.58.7".into()); + let secs: u64 = std::env::args().nth(3).and_then(|s| s.parse().ok()).unwrap_or(30); + aech::dmxsend(&ip, secs); + return; + } + "hostdemo" => { + // hostdemo [目标IP] [秒] — 主机侧等效示例: 认领真机 + RDM 发现 + 加密 DMX 控制 + // 源 IP 固定 192.168.123.113 (真机 .1 的 AECH 固件过滤); 秒=0 只做认领+RDM 不发 DMX + let ip = std::env::args().nth(2).unwrap_or_else(|| "192.168.123.1".into()); + let secs: u64 = std::env::args().nth(3).and_then(|s| s.parse().ok()).unwrap_or(30); + hostdemo::run(&ip, "192.168.123.113", secs); + return; + } + _ => { + println!("用法: enet-validator "); + return; + } + } + + println!("----------------------------------------------------------------"); + let p = results.iter().filter(|r| r.pass).count(); + for r in &results { r.show(); } + println!("================================================================"); + println!("汇总: PASS={} FAIL={}", p, results.len() - p); + + // 写 JSON 报告 + let mut j = String::from("{\n \"target\": \"192.168.123.1\",\n \"results\": [\n"); + for (i, r) in results.iter().enumerate() { + j.push_str(&format!( + " {{ \"id\": \"{}\", \"pass\": {}, \"detail\": \"{}\" }}{}\n", + r.id, r.pass, r.detail.replace('"', "'"), + if i + 1 < results.len() { "," } else { "" })); + } + j.push_str(" ]\n}\n"); + let _ = std::fs::write("validation_report.json", j); + println!("报告已写: validation_report.json"); +} diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..a0a060c --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,905 @@ +// tests.rs — T1..T11 协议验证实现 (判据 = Butler 实际应答) +use crate::enet::*; +use std::collections::HashSet; +use std::net::UdpSocket; +use std::time::{Duration, Instant}; + +pub struct Item { + pub id: &'static str, + pub pass: bool, + pub detail: String, +} +impl Item { + fn new(id: &'static str, pass: bool, detail: String) -> Item { Item { id, pass, detail } } + pub fn show(&self) { + println!(" [{}] {} {}", if self.pass { "PASS" } else { "FAIL" }, self.id, self.detail); + } +} + +// ---------- T1: 发现请求 → Butler 应答 (§3.1→§3.2) ---------- +pub fn t1_discover() -> Item { + let s = open(); + let req = load_template("discovery_req"); + let _ = s.send_to(&req, BCAST_SUB); + let _ = s.send_to(&req, BCAST_ALL); + let replies = recv_window(&s, Duration::from_secs(4)); + let mut detail = format!("收到{}包; ", replies.len()); + for (_, b) in &replies { + if b.len() == 217 && &b[0..4] == b"ENet" { + let len_ok = g32(b, 0x14) == 217; + let name_ok = find(b, b"Butler S2"); + let mac_ok = find(b, &[0x00, 0x16, 0x1c, 0xf1, 0x2e, 0x39]); + let pass = len_ok && name_ok && mac_ok; + detail.push_str(&format!( + "应答217B: len字段={} 名字={} MAC={}", + len_ok, name_ok, mac_ok + )); + return Item::new("T1", pass, detail); + } + } + detail.push_str("未收到 217B 发现应答"); + Item::new("T1", false, detail) +} + +// ---------- T2/T3/T4: 单向帧发送 → 观察会话反应 ---------- +pub fn tx_observe(id: &'static str, tpl: &str, expect_reply: bool) -> Item { + let s = open(); + let f = load_template(tpl); + let dst = if tpl == "heartbeat" { BCAST_SUB } else { BCAST_ALL }; + let n = s.send_to(&f, dst).expect("send"); + let replies = recv_window(&s, Duration::from_secs(3)); + let mut detail = format!("发送{}B→{}; 收到{}包", n, dst, replies.len()); + let mut pass = true; + if expect_reply { + pass = !replies.is_empty(); + if !pass { detail.push_str("; 无应答"); } + } + Item::new(id, pass, detail) +} + +// ---------- T5-T8: 会话 (DMX + 事件×2 + ACK配对 + 状态帧) ---------- +pub struct SessionStats { + pub dmx_sent: u64, + pub evt_sent: u64, + pub acks: u64, + pub ack_matched: u64, + pub status: u64, + pub rdm: u64, + pub other: u64, +} + +pub fn session(secs: u64, mutate_channels: bool) -> (SessionStats, Vec<(u32, u64)>) { + let s = open(); + session_on(s, secs, mutate_channels, true) +} + +/// 会话核心 (可注入 socket): takeover 模式跳过 preamble 且使用抢 bind 的 socket +pub fn session_on(s: UdpSocket, secs: u64, mutate_channels: bool, preamble: bool) -> (SessionStats, Vec<(u32, u64)>) { + if preamble { + // --- 前置认领 (§6 时序): 发现请求 → 宣告帧 → 短暂等待 --- + let disc = load_template("discovery_req"); + let ann = load_template("announce"); + let _ = s.send_to(&disc, BCAST_SUB); + let _ = s.send_to(&disc, BCAST_ALL); + let _ = s.send_to(&ann, BCAST_ALL); + let _ = recv_window(&s, Duration::from_secs(2)); // 吸收发现应答/宣告反应 + } + let dmx_t = load_template("dmx_a"); + let evt_t = load_template("event_b"); + let mut dmx_seq = g16(&dmx_t, 0x1A).wrapping_add(1); + let mut evt_seq = g32(&evt_t, 0x22).wrapping_add(1); + let tick = Duration::from_millis(25); + let start = Instant::now(); + let mut st = SessionStats { dmx_sent: 0, evt_sent: 0, acks: 0, ack_matched: 0, status: 0, rdm: 0, other: 0 }; + let mut recent: HashSet = HashSet::new(); + let mut timeline: Vec<(u32, u64)> = Vec::new(); + let mut tick_n: u64 = 0; + while start.elapsed().as_secs() < secs { + // --- DMX 帧 (形态A) --- + let mut d = dmx_t.clone(); + p16(&mut d, 0x1A, dmx_seq); + let tms = tick_n * 25; + for i in 0..8 { d[0x22 + i] = ((tms >> (i * 8)) & 0xFF) as u8; } + if mutate_channels { + let ph = start.elapsed().as_secs(); + for i in 0..1024usize { + d[0x3C + i] = match ph { + 0..=9 => 0, + 10..=19 => 255, + _ => (((1024usize).wrapping_sub(i + (tick_n as usize) * 4)) & 0xFF) as u8, + }; + } + } + let _ = s.send_to(&d, BUTLER); + st.dmx_sent += 1; + dmx_seq = dmx_seq.wrapping_add(1); + // --- 事件帧 ×2 (形态B) --- + for _ in 0..2 { + let mut e = evt_t.clone(); + p32(&mut e, 0x22, evt_seq); + recent.insert(evt_seq); + if recent.len() > 64 { recent.remove(&(evt_seq.wrapping_sub(64))); } + let _ = s.send_to(&e, BUTLER); + st.evt_sent += 1; + evt_seq = evt_seq.wrapping_add(1); + } + // --- 收集应答 --- + for (_, b) in recv_window(&s, tick) { + match classify(&b) { + Rx::Ack(sq) => { + st.acks += 1; + if recent.contains(&sq) { st.ack_matched += 1; } + timeline.push((sq, tick_n)); + } + Rx::Status(_) => st.status += 1, + Rx::RdmResp(_, _) => st.rdm += 1, + Rx::DiscoveryResp(_) | Rx::EnetType1(_) => {} + Rx::Other(_) => st.other += 1, + } + } + tick_n += 1; + } + (st, timeline) +} + +pub fn t5_t8_session(secs: u64) -> Vec { + println!(" 会话运行 {}s (DMX→事件×2, 25ms/tick)…", secs); + let (st, _) = session(secs, true); + let rate_dmx = st.dmx_sent as f64 / secs as f64; + let pair = if st.acks > 0 { st.ack_matched as f64 / st.acks as f64 } else { 0.0 }; + vec![ + Item::new("T5", st.dmx_sent > secs * 30 && st.status > 0, + format!("DMX发送={} (~{:.0}Hz) 状态帧={}", st.dmx_sent, rate_dmx, st.status)), + Item::new("T6", pair >= 0.95, + format!("ACK={} 配对={} ({:.1}%)", st.acks, st.ack_matched, pair * 100.0)), + Item::new("T7", st.status > 0, format!("状态帧={}", st.status)), + Item::new("T8", st.dmx_sent > secs * 30 && st.acks > secs, + format!("会话{}s无中断", secs)), + ] +} + +// ---------- T9: 回落与恢复 ---------- +pub fn t9_fallback() -> Item { + println!(" 阶段1: 会话 10s…"); + let (a, _) = session(10, false); + println!(" 阶段2: 静默 12s (只收不发)…"); + let s = open(); + let quiet = recv_window(&s, Duration::from_secs(12)); + let mut quiet_status = 0usize; + for (_, b) in &quiet { + if let Rx::Status(_) = classify(b) { quiet_status += 1; } + } + println!(" 阶段3: 恢复会话 8s…"); + let (b, _) = session(8, false); + let pass = a.acks > 5 && b.acks > 5; // 恢复后 ACK 恢复 = 重新接管成功 + Item::new("T9", pass, format!( + "会话前段ACK={}; 静默期收{}包(状态{}); 恢复后ACK={}", + a.acks, quiet.len(), quiet_status, b.acks)) +} + +// ---------- T12(实验): AECHA 认领重放 ---------- +pub fn t12_claim_replay() -> Item { + let s = open(); + let tick = Duration::from_millis(25); + let c1 = load_template("claim_h112"); + let c2 = load_template("claim_h114"); + let c3 = load_template("claim_h116"); + let mut aereps = 0u64; + let mut last_detail = String::new(); + for (i, f) in [&c1, &c2, &c3].into_iter().enumerate() { + let _ = s.send_to(f, BUTLER); + let replies = recv_window(&s, Duration::from_millis(800)); + let mut got = false; + for (_, b) in &replies { + if find(b, b"AEREP") { aereps += 1; got = true; } + } + last_detail.push_str(&format!("[{}] 应答{}包{} ", i + 1, replies.len(), if got { "+AEREP" } else { "" })); + } + // 若 3 轮完成 → 立即试探 DMX: 发 10 个 tick 看是否出现 ACK + let mut acks = 0u64; + if aereps >= 1 { + let dmx_t = load_template("dmx_a"); + let evt_t = load_template("event_b"); + let mut dmx_seq = g16(&dmx_t, 0x1A).wrapping_add(1); + let mut evt_seq = g32(&evt_t, 0x22).wrapping_add(1); + for _ in 0..10 { + let mut d = dmx_t.clone(); + p16(&mut d, 0x1A, dmx_seq); + let _ = s.send_to(&d, BUTLER); + dmx_seq = dmx_seq.wrapping_add(1); + for _ in 0..2 { + let mut e = evt_t.clone(); + p32(&mut e, 0x22, evt_seq); + evt_seq += 1; + let _ = s.send_to(&e, BUTLER); + } + for (_, b) in recv_window(&s, tick) { + if let Rx::Ack(_) = classify(&b) { acks += 1; } + } + } + } + last_detail.push_str(&format!("; AEREP={} 试探ACK={}", aereps, acks)); + Item::new("T12", acks > 0, last_detail) +} + +/// takeover 变体: 使用外部传入的已 bind socket +pub fn t13_takeover_on(s: UdpSocket, secs: u64) -> Vec { + println!(" 发帧 {}s (共存模式)…", secs); + let (st, _) = session_on(s, secs, true, false); + let pair = if st.acks > 0 { st.ack_matched as f64 / st.acks as f64 } else { 0.0 }; + vec![ + Item::new("T13a", st.dmx_sent > secs * 20, format!("DMX发送={}", st.dmx_sent)), + Item::new("T13b", st.acks > 0, format!("Rust收到ACK={} 配对={:.1}% 状态={}", + st.acks, pair * 100.0, st.status)), + ] +} + +// ---------- T13(实验): 挂起 SymphoLight 后 Rust 抢占接管会话 ---------- +pub fn t13_takeover(secs: u64) -> Vec { + // 用抢 bind 的 socket 跑会话核心 (无 preamble: 认证已由 SymphoLight 完成) + let s = open_reuse(BIND); + println!(" 接管会话 {}s (SO_REUSEADDR 模式)…", secs); + let (st, _) = session_on(s, secs, true, false); + let pair = if st.acks > 0 { st.ack_matched as f64 / st.acks as f64 } else { 0.0 }; + vec![ + Item::new("T13a", st.dmx_sent > secs * 20, format!("接管后 DMX发送={}", st.dmx_sent)), + Item::new("T13b", st.acks > 0, format!("ACK={} 配对={} ({:.1}%) 状态={}", + st.acks, st.ack_matched, pair * 100.0, st.status)), + ] +} + +// ---------- 设备端模拟 v2 (Secondary IP, 与真 Butler 并存) ---------- +// 双 socket: wildcard(REUSEADDR) 收广播发现; dev_ip:31772 收会话/发应答(源IP正确) +pub fn device2(dev_ip: [u8; 4], dev_mac: [u8; 6], secs: u64, relay: Option<([u8; 4], u16)>) -> Vec { + let mut items = Vec::new(); + let bind_addr = format!("{}.{}.{}.{}:31772", dev_ip[0], dev_ip[1], dev_ip[2], dev_ip[3]); + let sess = open_reuse(&bind_addr); + // wildcard(REUSEADDR) 收广播发现; 注意: 必须先启动本程序、后启动 SymphoLight + let disc = std::panic::catch_unwind(|| open_reuse("0.0.0.0:31772")).ok(); + // AECH 中继 socket: 优先绑 113 (本机 123 网段时直接中继 .1); 非本网段则绑自身 IP (经桥中继) + let relay_sock: Option = relay.map(|_| { + std::panic::catch_unwind(|| open_reuse("192.168.123.113:0")) + .unwrap_or_else(|_| open_reuse(&format!("{}.{}.{}.{}:0", dev_ip[0], dev_ip[1], dev_ip[2], dev_ip[3]))) + }); + sess.set_read_timeout(Some(Duration::from_millis(2))).unwrap(); + println!(" 虚拟 Butler @ {} (MAC {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}), 运行 {}s", + bind_addr, dev_mac[0], dev_mac[1], dev_mac[2], dev_mac[3], dev_mac[4], dev_mac[5], secs); + + let resp_t = load_template("discovery_resp"); + let aerep_t = load_template("aerep"); + let ack_t = load_template("ack"); + let status_t = load_template("status"); // 84B "Butl" 设备状态遥测帧 + let reg89_t = load_template("register89"); // 89B 主机注册应答 (sub=02) + let aere594_t = load_template("aere594"); // 594B AECH 认领应答 (sub=06, AERE) + let ann144_t = load_template("announce144"); // 144B 宣告 (sub=01, 中继唤醒用) + // RDM 应答模板: 5种DISC脉冲(101B) + 分支应答(111B) + 参数信息(119B) + let pulse_ts = vec![load_template("rdm_pulse_0"), load_template("rdm_pulse_1"), + load_template("rdm_pulse_2"), load_template("rdm_pulse_3"), load_template("rdm_pulse_4")]; + let rdm111_t = load_template("rdm_resp111"); + let rdm119_t = load_template("rdm_info119"); + let setack68_t = load_template("rdm_setack68"); // RDM SET 应答模板 (CC=31, PDL=0, 68B, set123b 实证) + // 真机 .1 完整 RDM 应答帧池 (178 唯一帧: 56/59/70/77/87B 全部应答词表) + let rdm_pool = crate::enet::load_rdm_pool(); + println!(" RDM 帧池: {} 帧", rdm_pool.len()); + // PID→应答模板映射 (cmd=18=RDM GET, 查询 PID 在 0x50, 应答含 21) + let mut pid_map: std::collections::HashMap<[u8; 2], Vec> = std::collections::HashMap::new(); + // SET 写路径状态: 主机 SET (CC=0x30) 的值按 (UID, PID) 落存, 后续 GET 同 PID 回显新值 + // (真机行为: SET 立即生效, 主机 +33ms GET 验证 — set123b 实证, §5.6) + let mut set_store: std::collections::HashMap<([u8; 6], [u8; 2]), Vec> = std::collections::HashMap::new(); + for line in include_str!("../samples/pid_map.txt").lines() { + let line = line.trim(); + if line.len() < 8 { continue; } + let pid_hex = &line[..4]; + let frame_hex = line[5..].trim(); + let pb: Vec = (0..pid_hex.len()).step_by(2).map(|i| u8::from_str_radix(&pid_hex[i..i + 2], 16).unwrap()).collect(); + let fb: Vec = (0..frame_hex.len()).step_by(2).map(|i| u8::from_str_radix(&frame_hex[i..i + 2], 16).unwrap()).collect(); + if pb.len() == 2 { pid_map.insert([pb[0], pb[1]], fb); } + } + println!(" PID 映射: {} 个", pid_map.len()); + + let start = Instant::now(); + let mut n_disc = 0u64; let mut n_aecha = 0u64; let mut n_dmx = 0u64; + let mut n_evt = 0u64; let mut n_ack = 0u64; let mut n_other = 0u64; + let mut n_status = 0u64; let mut status_seq = 0x62u32; + let mut n_rdm_pulse = 0u64; let mut n_rdm_111 = 0u64; let mut n_rdm_119 = 0u64; + let mut n_rdm_set = 0u64; + let mut n_announce = 0u64; + let mut pulse_idx = 0usize; let mut pulse_seq = 0x0f0bu32; let mut info_ctr = 0xc0u8; + let mut pulse_step: u32 = 2; + let mut pool_idx = 0usize; + let mut cur_pulse = 0usize; let mut branch_ctr = 0u64; + let mut cur_fixture = 0usize; + let mut rdm_req_dumped = 0u32; + let mut n_reg = 0u64; let mut n_claim = 0u64; let mut claim_nonce: u32 = 0x51f31ab9; + let mut dmx_b12: Option = None; // AECH 质询 0x8C-0x8F 实例随机 → DMX 解密 rot 基值 + let mut n_rdm_relay = 0u64; let mut n_rdm_fwd = 0u64; + let mut pending_rdm_bridge = [0u8; 6]; + // AECH 中继: (认领者地址, 主机IP LE, 主机MAC) — 质询转发真机后暂存 + let mut pending_claim: Option<(std::net::SocketAddr, [u8;4], [u8;6])> = None; + let mut last_pulse = Instant::now() - Duration::from_secs(10); + let mut first_channels: Option<[u8; 32]> = None; + let mut last_channels: [u8; 32] = [0; 32]; + let mut ch_changes = 0u64; + let mut last_ch_show = Instant::now() - Duration::from_secs(10); + let mut peer: Option = None; + // RDM 应答延迟队列 (真机灯具经 RS485 应答延迟 12-48ms, 中位 29ms — 瞬时应答是假指纹) + let mut rdm_queue: Vec<(Instant, Vec, std::net::SocketAddr)> = Vec::new(); + let mut delay_lcg: u32 = 0x9e3779b9; + let mut next_delay = move |base_ms: u64, span_ms: u64| -> u64 { + delay_lcg = delay_lcg.wrapping_mul(2654435761).wrapping_add(0x9e3779b9); + base_ms + ((delay_lcg >> 16) as u64 % span_ms) + }; + // 42B 遥测: 真机 ~每 115ms 上报一次"已收事件计数" (0x20-0x23 严格递增, + // 增量=事件B帧数, ss123 实证 +2959 计数 ≈ 2963 事件) + let mut event_ctr: u32 = 0; + let mut ack42_sent_val: u32 = 0; + let mut last_ack42 = Instant::now() - Duration::from_secs(1); + let mut evt_dst: Option = None; + + while start.elapsed().as_secs() < secs { + // --- 真机无自发状态帧 (168s 仅 1 帧 42B, 已全部删除突发遥测) --- + // --- 到期的 RDM 延迟应答 --- + if !rdm_queue.is_empty() { + let now = Instant::now(); + let mut i = 0; + while i < rdm_queue.len() { + if rdm_queue[i].0 <= now { + let (_, r, dst) = rdm_queue.remove(i); + let _ = sess.send_to(&r, dst); + } else { i += 1; } + } + } + // --- 42B 遥测: 真机 ~每 115ms 上报一次"已收事件计数" (ss123 实证) --- + if last_ack42.elapsed() > Duration::from_millis(115) && event_ctr != ack42_sent_val { + if let Some(dst) = evt_dst { + let mut a = ack_t.to_vec(); + p32(&mut a, 0x20, event_ctr); + let _ = sess.send_to(&a, dst); + ack42_sent_val = event_ctr; + last_ack42 = Instant::now(); + } + } + let mut buf = [0u8; 2048]; + // --- wildcard socket: 收广播发现 (仅初始发现阶段用) --- + if let Some(ref d) = disc { + if let Ok((n, src)) = d.recv_from(&mut buf) { + let b = &buf[..n]; + if b.len() == 180 && &b[0..4] == b"ENet" && g32(b, 4) == 1 { + // 真机处理延迟 ~160ms (指纹对齐) + std::thread::sleep(Duration::from_millis(150)); + let mut r = resp_t.clone(); + patch_dev(&mut r, dev_ip, dev_mac); + patch_259_host(&mut r, src, b); + let _ = sess.send_to(&r, src); + n_disc += 1; + println!(" [{}] 发现请求 type1 from {} len={}", chrono_t(start), src, b.len()); + } else if b.len() >= 0x1A && &b[0..4] == b"ENet" && g32(b, 4) == 2 && g16(b, 0x18) == 1 { + // 广播主机宣告 sub=01 → 也回 89B 注册帧 (从 sess 口发出) + let mut r = reg89_t.clone(); + let ip_le = [dev_ip[3], dev_ip[2], dev_ip[1], dev_ip[0]]; + r[0x1A..0x1E].copy_from_slice(&ip_le); + r[0x1E..0x24].copy_from_slice(&dev_mac); + if b.len() > 0x24 { + r[0x25..0x29].copy_from_slice(&b[0x1A..0x1E]); + r[0x29..0x2F].copy_from_slice(&b[0x1E..0x24]); + } + let _ = sess.send_to(&r, src); + n_reg += 1; + } + } + } + // --- 中继回程轮询: 真机 AERE 回到 relay_sock (113:随机端口) --- + if let Some(rs) = relay_sock.as_ref() { + if let Ok((n, src)) = rs.recv_from(&mut buf) { + let b2 = &buf[..n]; + if b2.len() >= 8 && &b2[0..4] == b"ENet" { + // --- RDM 应答回程: 真机灯具的 type=04 应答 (脉冲59B/分支69B/参数77B) → 发给主机 --- + if g32(b2, 4) == 4 && b2.len() >= 40 { + if let Some(p) = peer { + let mut r = b2.to_vec(); + // cc01 帧内含 bridge-id (0x2D-0x32), 替换为查询的 bridge-id + if r.len() > 0x33 && r[0x2A] == 0xCC { + r[0x2D..0x33].copy_from_slice(&pending_rdm_bridge); + } + let _ = sess.send_to(&r, p); + n_rdm_fwd += 1; + if n_rdm_fwd <= 4 { println!(" [{}] RDM 应答回程 → 主机 ({}B)", chrono_t(start), r.len()); } + } + } + if let (Some((pc, hip, hmac)), Some(_)) = (pending_claim, relay) { + if b2.len() >= 552 && g32(b2, 4) == 2 && g16(b2, 0x18) == 6 { let mut r = b2.to_vec(); + let ip_le = [dev_ip[3], dev_ip[2], dev_ip[1], dev_ip[0]]; + r[0x1A..0x1E].copy_from_slice(&ip_le); + r[0x41..0x45].copy_from_slice(&ip_le); + r[0x1E..0x24].copy_from_slice(&dev_mac); + r[0x49..0x4F].copy_from_slice(&dev_mac); + r[0x25..0x28].copy_from_slice(&hip[0..3]); + r[0x2C..0x2F].copy_from_slice(&hmac[3..6]); + let _ = sess.send_to(&r, pc); + pending_claim = None; + n_claim += 1; + println!(" [{}] ★★ 真机 AERE 中继成功 → {} #{}", chrono_t(start), pc, n_claim); + } + } + } + } + } + // --- 会话 socket: 单播发现 / AECHA / DMX / 事件 --- + let (n, src) = match sess.recv_from(&mut buf) { + Ok(x) => x, + Err(_) => continue, + }; + let b = &buf[..n]; + if b.len() < 8 || &b[0..4] != b"ENet" { continue; } + peer = Some(src); // 记录对端(SymphoLight)用于遥测上行 + let t = g32(b, 4); + if t == 1 && b.len() == 180 { + let mut r = resp_t.clone(); + patch_dev(&mut r, dev_ip, dev_mac); + patch_259_host(&mut r, src, b); + let _ = sess.send_to(&r, src); + n_disc += 1; + println!(" [{}] 发现请求 type1 from {} len={}", chrono_t(start), src, b.len()); + } else if t == 2 { + let sub = if b.len() >= 0x1A { g16(b, 0x18) } else { 0 }; + if sub == 5 && find(b, b"AECH") { + if let (Some((rip, rport)), Some(rs)) = (relay, relay_sock.as_ref()) { + // --- 中继模式: 先向中继目标发 sub=01 宣告唤醒注册态, 再转发质询 --- + let hip = [b[0x1A], b[0x1B], b[0x1C], b[0x1D]]; + let hmac = [b[0x1E], b[0x1F], b[0x20], b[0x21], b[0x22], b[0x23]]; + pending_claim = Some((src, hip, hmac)); + let raddr = format!("{}.{}.{}.{}:{}", rip[0], rip[1], rip[2], rip[3], rport); + // 宣告帧: 0x1A-1D 主机IP(LE), 0x1E-23 主机MAC + let mut ann = ann144_t.clone(); + ann[0x1A..0x1E].copy_from_slice(&hip); + ann[0x1E..0x24].copy_from_slice(&hmac); + let _ = rs.send_to(&ann, &raddr); + std::thread::sleep(Duration::from_millis(30)); + let _ = rs.send_to(b, &raddr); + println!(" [{}] AECH 质询中继 → 真机 {} (先宣告唤醒)", chrono_t(start), raddr); + } else { + // --- AECH 认领质询 (594B sub=05) → AERE 应答 (sub=06) --- + // 质询帧内: 0x1A-1D 主机IP(LE), 0x1E-23 主机MAC, 0x28 "AECH" + let mut r = aere594_t.clone(); + // 设备 IP (LE) 0x1A 与 0x41; 设备 MAC 0x1E 与 0x49 + let ip_le = [dev_ip[3], dev_ip[2], dev_ip[1], dev_ip[0]]; + r[0x1A..0x1E].copy_from_slice(&ip_le); + r[0x41..0x45].copy_from_slice(&ip_le); + r[0x1E..0x24].copy_from_slice(&dev_mac); + r[0x49..0x4F].copy_from_slice(&dev_mac); + // AERE 0x25-27 = 主机IP前3字节(LE), 0x2C-2E = 主机MAC后3字节 + if b.len() > 0x24 { + r[0x25..0x28].copy_from_slice(&b[0x1A..0x1D]); + r[0x2C..0x2F].copy_from_slice(&b[0x21..0x24]); + } + // 0xC0-0xC3 = AgiosElias G(nonce@0x88-0x8B LE) — 算法已从 SymphoLight + // Ecue.Devices.dll 反编译还原 (无密钥! G(x)=x^TABLE[(x%32+1+(x*13)%30)%32]), + // 主机按同式验证 AERE blob[152]。无需真机中继。 + let nonce = if b.len() > 0x8C { g32(b, 0x88) } else { 0 }; + p32(&mut r, 0xC0, crate::aech::agios_g(nonce)); + // 0x8C-0x8F = AgiosElias 实例随机 B12 — DMX 帧混淆 rot=(B12^r)&511 的会话基值 + if b.len() > 0x90 { dmx_b12 = Some(g32(b, 0x8C)); } + let _ = sess.send_to(&r, src); + n_claim += 1; + println!(" [{}] ★ AECH 认领质询 → 本地计算 AERE (G({:08x})={:08x}) #{}", + chrono_t(start), nonce, crate::aech::agios_g(nonce), n_claim); + } + } else if sub == 1 { + // --- 主机身份宣告 sub=01 (102B) → 89B 注册应答 sub=02 --- + // 宣告内: 0x1A-1D 主机IP(LE), 0x1E-23 主机MAC + let mut r = reg89_t.clone(); + let ip_le = [dev_ip[3], dev_ip[2], dev_ip[1], dev_ip[0]]; + r[0x1A..0x1E].copy_from_slice(&ip_le); + r[0x1E..0x24].copy_from_slice(&dev_mac); + if b.len() > 0x24 { + r[0x25..0x29].copy_from_slice(&b[0x1A..0x1E]); // 主机IP(LE) + r[0x29..0x2F].copy_from_slice(&b[0x1E..0x24]); // 主机MAC + } + let _ = sess.send_to(&r, src); + n_reg += 1; + if n_reg <= 3 { println!(" [{}] 主机宣告 sub=01 → 回 89B 注册帧", chrono_t(start)); } + } else { + // --- 已认领心跳 sub=03 (36B, 每~6s): 真机从不应答 (cold2 实证), 静默 --- + n_status += 1; // 仅计数 + } + } else if t == 4 && b.len() == 1084 { + if g16(b, 0x20) != 0x0101 { + event_ctr = event_ctr.wrapping_add(1); // 事件B帧计数 (含 RDM 查询, 42B 遥测用) + evt_dst = Some(src); + } + if g16(b, 0x20) == 0x0101 { + n_dmx += 1; + // DMX 数据经 zPprv0TXRRW 逐帧混淆 (§4.1): r=0x22-0x25 LE, rot=(B12^r)&511 + let ch16 = match dmx_b12 { + Some(b12) => { + let r1 = g32(b, 0x22); + let plain = crate::aech::agios_dmx_decrypt(&b[0x3C..0x3C + 512], r1, b12); + hexs(&plain[..16]) + } + None => hexs(&b[0x3C..0x3C + 16]), // 未认领前: 原始线值 + }; + let mut ch = [0u8; 32]; + ch.copy_from_slice(&b[0x3C..0x3C + 32]); + if first_channels.is_none() { first_channels = Some(ch); } + if ch != last_channels { ch_changes += 1; last_channels = ch; } + if last_ch_show.elapsed() > Duration::from_secs(5) { + last_ch_show = Instant::now(); + println!(" [{}] DMX #{} CH1-16: {}", chrono_t(start), n_dmx, ch16); + } + } else if b.len() > 0x44 && b[0x3C] == 0x01 && (b[0x3D] == 0x24 || b[0x3D] == 0x18 || b[0x3D] == 0x19 || b[0x3D] == 0x1a) { + // --- RDM 请求 (0x20=0200, 0x3C=01, 0x3D=cmd, 0x3E+=目标UID) --- + let mut uid = [0u8; 6]; + uid.copy_from_slice(&b[0x3E..0x44]); + // RDM 一律本地生成式应答 (全新UID灯具); relay 仅用于 AECH 认领 + if rdm_req_dumped < 3 { + rdm_req_dumped += 1; + let dump_len = std::cmp::min(0x80, b.len()); + println!(" [RDM-REQ#{}] len={} hex[0..{}]={}", rdm_req_dumped, b.len(), dump_len, hexs(&b[..dump_len])); + } + if b[0x3D] == 0x19 && rdm_req_dumped < 8 { + rdm_req_dumped += 1; + let dump_len = std::cmp::min(0x90, b.len()); + println!(" [RDM-19#{}] len={} hex[0..{}]={}", rdm_req_dumped, b.len(), dump_len, hexs(&b[..dump_len])); + } + if b[0x30] != 0x00 { + // flag=01 = DMX 内嵌副本帧: 真机完全不答, 忽略 + } else if b[0x3D] == 0x24 { + // --- DISC_UNIQUE_BRANCH: 分支范围 lo@0x53-0x58 hi@0x59-0x5E (PID 0001@0x50, PDL=12@0x52) --- + // 真机灯具只应答 UID 落在分支范围内的查询 (ss123 实证二分收敛); + // 此前无视范围全部应答 → 主机二分水远无法排空分支 → 51/s 发现洪水, 稳态轮询不启动 + if b.len() > 0x5E { + let lo = &b[0x53..0x59]; let hi = &b[0x59..0x5F]; + for fx in FIXTURES.iter() { + if &fx[..] >= lo && &fx[..] <= hi { + let pulse = rdm_encode_uid(fx); + let mut r: Vec = Vec::with_capacity(59); + r.extend_from_slice(&pulse_ts[0][..0x2A]); // 模板头 + // 脉冲 seq = 设备本地自增序号 (真机行为, 非回显!) + p32(&mut r, 0x20, pulse_seq); + pulse_seq = pulse_seq.wrapping_add(pulse_step); + pulse_step = 1 + (pulse_step * 7 + 0x1d) % 3; // 每帧 +1..3 + r.extend_from_slice(&pulse); + let due = Instant::now() + Duration::from_millis(next_delay(2, 40)); + rdm_queue.push((due, r, src)); + n_rdm_pulse += 1; + } + } + if n_rdm_pulse <= 5 || n_rdm_pulse % 100 == 0 { + println!(" [RDM] 分支发现范围内应答 #{}", n_rdm_pulse); + } + } + } else if b[0x3D] == 0x18 && uid == [0xff; 6] { + // 广播 UID 的发现命令 (广播 MUTE 0003 等): 真机不应答, 静默 + } else if b[0x3D] == 0x18 { + // cmd=18 = RDM GET. PID 在 0x50-0x51. + // PID=0002 = DISC_UN_MUTE → 70B ACK (CC=11, PDL=2, PD=0000) — 冷启动抓包实证 + let pid = if b.len() > 0x51 { [b[0x50], b[0x51]] } else { [0, 0] }; + let mirror = mirror_uid(&uid); + // 优先帧池中镜像灯具的同 PID 应答 (GET_RESP=0x21, 数据自洽); 其次 pid_map; 兜底 111 + let pool_hit = rdm_pool.iter().find(|f| + f.len() >= 0x42 && f[0x2A] == 0xCC && f[0x33..0x39] == mirror + && f[0x3E] == 0x21 && f[0x3F] == pid[0] && f[0x40] == pid[1]); + let mut r = if let Some(t) = pool_hit { + t.clone() + } else if let Some(t) = pid_map.get(&pid) { + t.clone() + } else { + rdm111_t.clone() + }; + if r.len() > 0x39 { r[0x33..0x39].copy_from_slice(&uid); } // src UID = 虚拟灯具 + // dest UID (0x2D-0x32) = 查询方 bridge-id; seq 回显; TN 回显查询 TN(0x4A) + if b.len() > 0x4A && r.len() > 0x33 { r[0x2D..0x33].copy_from_slice(&b[0x44..0x4A]); } + r[0x20] = b[0x22]; r[0x21] = b[0x23]; + if b.len() > 0x4B && r.len() > 0x3A { r[0x39] = b[0x4A]; } + // SET 过的 PID 回显新值 (真机: SET 立即生效, §5.6) + if let Some(v) = set_store.get(&(uid, pid)) { + let pdl = if r.len() > 0x41 { r[0x41] as usize } else { 0 }; + if pdl == v.len() && r.len() >= 0x42 + pdl { + r[0x42..0x42 + pdl].copy_from_slice(v); + } + } + rdm_checksum(&mut r); // patch 后必须重算 (真机 1396/1396 有效) + let due = Instant::now() + Duration::from_millis(next_delay(15, 25)); + n_rdm_111 += 1; + if n_rdm_111 <= 6 || n_rdm_111 % 100 == 0 { + println!(" [RDM] GET UID={:02x}{:02x}{:02x}{:02x}{:02x}{:02x} PID={:02x}{:02x} → {}B", + uid[0], uid[1], uid[2], uid[3], uid[4], uid[5], pid[0], pid[1], r.len()); + } + rdm_queue.push((due, r, src)); + } else if b[0x4F] == 0x30 { + // --- RDM SET (CC=0x30, msgLen=0x1a+): 真机回 CC=0x31 ACK, RT=00, PDL=0, + // 68B, TN/UID 回显, checksum 有效, 延迟 18-25ms (set123b 实证, §5.6) --- + let mut r = setack68_t.clone(); + if r.len() > 0x39 { r[0x33..0x39].copy_from_slice(&uid); } // src UID = 虚拟灯具 + if b.len() > 0x4A && r.len() > 0x33 { r[0x2D..0x33].copy_from_slice(&b[0x44..0x4A]); } // dest = 查询方 + r[0x20] = b[0x22]; r[0x21] = b[0x23]; // seq 回显 + if b.len() > 0x4B && r.len() > 0x3A { r[0x39] = b[0x4A]; } // TN 回显 + if b.len() > 0x51 && r.len() > 0x41 { r[0x3F] = b[0x50]; r[0x40] = b[0x51]; } // PID 回显 + rdm_checksum(&mut r); // patch 后必须重算 + let due = Instant::now() + Duration::from_millis(next_delay(15, 25)); + let pid = if b.len() > 0x51 { [b[0x50], b[0x51]] } else { [0, 0] }; + // 应用写入值 (PDL@0x52, PD@0x53): 后续 GET 同 PID 回显新值 (§5.6 主机验证模式) + let pdl = if b.len() > 0x52 { b[0x52] as usize } else { 0 }; + if pdl > 0 && b.len() >= 0x53 + pdl { + set_store.insert((uid, pid), b[0x53..0x53 + pdl].to_vec()); + } + rdm_queue.push((due, r, src)); + n_rdm_set += 1; + if n_rdm_set <= 6 || n_rdm_set % 100 == 0 { + println!(" [RDM] SET UID={:02x}{:02x}{:02x}{:02x}{:02x}{:02x} PID={:02x}{:02x} PD={} → 68B ACK #{}", + uid[0], uid[1], uid[2], uid[3], uid[4], uid[5], pid[0], pid[1], + hexs(set_store.get(&(uid, pid)).map(|v| &v[..]).unwrap_or(&[])), n_rdm_set); + } + } else { + // cmd=19/1a (SET类/传感器轮询 SENSOR_VALUE 0201): PID@0x50, PD@0x53 + // 按镜像灯具 + PID + PD[0](传感器序号) 从帧池取自恰应答 + let pid = if b.len() > 0x51 { [b[0x50], b[0x51]] } else { [0, 0] }; + let pd0 = if b.len() > 0x53 { b[0x53] } else { 0 }; + let mirror = mirror_uid(&uid); + let pool_hit = rdm_pool.iter().find(|f| + f.len() >= 0x43 && f[0x2A] == 0xCC && f[0x33..0x39] == mirror + && f[0x3F] == pid[0] && f[0x40] == pid[1] && f[0x42] == pd0); + let mut r = if let Some(t) = pool_hit { + t.clone() + } else { + rdm119_t.clone() + }; + if r.len() > 0x39 { r[0x33..0x39].copy_from_slice(&uid); } + if b.len() > 0x4A && r.len() > 0x33 { r[0x2D..0x33].copy_from_slice(&b[0x44..0x4A]); } + r[0x20] = b[0x22]; r[0x21] = b[0x23]; + if b.len() > 0x4B && r.len() > 0x3A { r[0x39] = b[0x4A]; } + rdm_checksum(&mut r); + let due = Instant::now() + Duration::from_millis(next_delay(15, 25)); + let rlen = r.len(); + rdm_queue.push((due, r, src)); + n_rdm_119 += 1; + if n_rdm_119 <= 6 || n_rdm_119 % 100 == 0 { + println!(" [RDM] 参数/传感器 UID={:02x}{:02x}{:02x}{:02x}{:02x}{:02x} PID={:02x}{:02x} PD0={:02x} → {}B #{}", + uid[0], uid[1], uid[2], uid[3], uid[4], uid[5], pid[0], pid[1], pd0, rlen, n_rdm_119); + } + } + } else { + // 非 RDM 事件B帧: 真机只计数, 由 42B 周期遥测上报, 无即时 ACK (ss123 实证) + n_evt += 1; + } + } else { + n_other += 1; + if n_other <= 12 { + println!(" [?] 其他帧 src={} len={} type={} hex[0..48]={}", src, b.len(), t, hexs(&b[..std::cmp::min(48, b.len())])); + } + } + } + items.push(Item::new("U1", n_disc > 0, format!("发现应答={} 遥测={} RDM脉冲={} RDM111={} RDM119={}", n_disc, n_status, n_rdm_pulse, n_rdm_111, n_rdm_119))); + items.push(Item::new("U2", n_aecha > 0, format!("AECHA 认领交换={} 轮 (AEREP 已回)", n_aecha))); + items.push(Item::new("U3", n_dmx > 0, format!("明文 DMX 帧接收={}", n_dmx))); + items.push(Item::new("U4", n_ack > 0, format!("事件帧={} ACK发送={}", n_evt, n_ack))); + items.push(Item::new("U5", n_dmx > secs * 10, format!( + "通道值: 首32CH={:?} 变化={} 其他帧={}", first_channels.unwrap_or([0; 32]), ch_changes, n_other))); + items +} + +// ---------- RDM 广播发现重放探测: 向目标重放真机 RDM 广播请求, 抓取并分类应答 ---------- +pub fn rdm_probe(target: &str, secs: u64) -> Item { + let req_t = load_template("rdm_disc_req"); + let bind = "0.0.0.0:0"; + let s = UdpSocket::bind(bind).expect("rdmprobe bind 失败"); + s.set_broadcast(true).unwrap(); + s.set_read_timeout(Some(Duration::from_millis(120))).unwrap(); + let dst = format!("{}:31772", target); + println!(" rdmprobe → {} 运行 {}s", dst, secs); + let start = Instant::now(); + let mut seq: u32 = 0x090f; + let mut sent = 0u64; + let mut resp_lens: std::collections::HashMap = std::collections::HashMap::new(); + let mut first_resp: Option> = None; + let mut delays: Vec = Vec::new(); + let mut last_send = Instant::now() - Duration::from_secs(10); + let mut pending: Option<(Instant, u32)> = None; + while start.elapsed().as_secs() < secs { + // 每 500ms 发一次请求 + if last_send.elapsed() > Duration::from_millis(500) { + let mut r = req_t.clone(); + p32(&mut r, 0x20, seq); + let _ = s.send_to(&r, &dst); + pending = Some((Instant::now(), seq)); + seq = seq.wrapping_add(1); + sent += 1; + last_send = Instant::now(); + } + let mut buf = [0u8; 2048]; + match s.recv_from(&mut buf) { + Ok((n, src)) => { + let b = &buf[..n].to_vec(); + *resp_lens.entry(n).or_insert(0) += 1; + if first_resp.is_none() { first_resp = Some(b.clone()); } + if let Some((t0, sq)) = pending { + if n >= 0x22 && g32(b, 4) == 4 { + let echo = g16(b, 0x20); + let expect = ((sq >> 16) as u16) ^ 0; // seq低16位在0x22 + let _ = expect; + delays.push((t0.elapsed().as_millis() as u32)); + println!(" [{}] 应答 src={} len={} seqEcho=0x{:04x} (请求seq低16=0x{:04x}) 延迟={}ms", + chrono_t(start), src, n, echo, (sq & 0xffff) as u16, t0.elapsed().as_millis()); + } else { + println!(" [{}] 应答 src={} len={} type={}", chrono_t(start), src, n, if n>=8 { g32(b,4) } else { 0 }); + } + pending = None; + } + } + Err(_) => {} + } + } + let mut lens_desc = String::new(); + let mut keys: Vec<_> = resp_lens.keys().cloned().collect(); + keys.sort(); + for k in keys { lens_desc.push_str(&format!(" {}B×{}", k, resp_lens[&k])); } + let avg_delay = if delays.is_empty() { 0 } else { delays.iter().sum::() / delays.len() as u32 }; + Item::new("RDMP", sent > 0 && !resp_lens.is_empty(), + format!("请求={} 应答[{}] 平均延迟={}ms", sent, lens_desc.trim(), avg_delay)) +} + +// ---------- RDM DISC 脉冲编码 (E1.20) ---------- +/// 虚拟灯具: 全新 UID (同厂 0107 系列, 主机未注册过的序列号) +const FIXTURES: [[u8; 6]; 3] = [ + [0x01, 0x07, 0x34, 0x67, 0x01, 0x30], + [0x01, 0x07, 0x34, 0x67, 0x01, 0x31], + [0x01, 0x07, 0x34, 0x67, 0x01, 0x32], +]; +/// 镜像映射: 虚拟 UID → 真机灯具 UID (参数数据按镜像灯具从 rdm_pool 取, +/// 使 3 台虚拟灯具拥有各自自洽的 DMX 地址/标签/传感器数, 与 .1 总线同构) +const FIXTURE_MIRROR: [[u8; 6]; 3] = [ + [0x01, 0x07, 0x34, 0x67, 0x01, 0x25], // NFS1K2DxxxBx DMX101-102 23传感器 + [0x01, 0x07, 0x34, 0x88, 0x09, 0x00], // NFM680SxxxGx DMX111 17传感器 + [0x08, 0x99, 0x50, 0x00, 0x26, 0xc9], // HMKJ DMX1-32 4传感器 +]; +fn mirror_uid(uid: &[u8; 6]) -> [u8; 6] { + for (i, f) in FIXTURES.iter().enumerate() { + if f == uid { return FIXTURE_MIRROR[i]; } + } + *uid +} + +// E1.20 checksum: 从 CC(0x2A) 起 msgLen(0x2C) 字节的 16-bit 和, BE 写于 [0x2A+msgLen] +// (冷启动抓包验证: 真机 1396/1396 全部满足此规则) +fn rdm_checksum(r: &mut [u8]) { + if r.len() < 0x2E || r[0x2A] != 0xCC { return; } + let msg_len = r[0x2C] as usize; + if r.len() < 0x2A + msg_len + 2 { return; } + let mut sum: u16 = 0; + for i in 0x2A..0x2A + msg_len { sum = sum.wrapping_add(r[i] as u16); } + r[0x2A + msg_len] = (sum >> 8) as u8; + r[0x2A + msg_len + 1] = (sum & 0xFF) as u8; +} + +fn rdm_encode_uid(uid: &[u8; 6]) -> [u8; 17] { + let mut out = [0u8; 17]; + out[0] = 0xAA; // 分隔符 + let mut sum: u16 = 0; + for (i, &b) in uid.iter().enumerate() { + let b1 = 0xAA | (b & 0x55); + let b2 = 0x55 | (b & 0xAA); + out[1 + 2 * i] = b1; + out[2 + 2 * i] = b2; + sum += b1 as u16 + b2 as u16; + } + let hi = (sum >> 8) as u8; + let lo = (sum & 0xFF) as u8; + out[13] = 0xAA | (hi & 0x55); + out[14] = 0x55 | (hi & 0xAA); + out[15] = 0xAA | (lo & 0x55); + out[16] = 0x55 | (lo & 0xAA); + out +} + +// ---------- 259 应答的 Connected To 动态化: 从发现请求提取请求方主机 IP+MAC ---------- +fn patch_259_host(r: &mut [u8], src: std::net::SocketAddr, req: &[u8]) { + if let std::net::IpAddr::V4(v4) = src.ip() { + let o = v4.octets(); + let ip_le = [o[3], o[2], o[1], o[0]]; + // host IP (0x4F-0x52) = 请求方 IP (LE) + if r.len() > 0x53 { r[0x4F..0x53].copy_from_slice(&ip_le); } + // host MAC (0x53-0x58): 请求里 hostIP(LE) 后 +8 处 (IP4 + 00ffffff掩码 + MAC6) + for i in 0..req.len().saturating_sub(14) { + if req[i..i + 4] == ip_le && req[i + 4..i + 8] == [0x00, 0xff, 0xff, 0xff] { + if r.len() > 0x59 { r[0x53..0x59].copy_from_slice(&req[i + 8..i + 14]); } + break; + } + } + } +} + +// ---------- bridge: AECH 双向桥 (非123网段设备的认领桥) ---------- +// 监听 listen_ip:listen_port 收远端设备的 AECH, 以源 IP=123.113 转发给真机, 再把真机 AERE 回传给远端设备 +pub fn bridge(listen_ip: [u8; 4], listen_port: u16, real_ip: [u8; 4], secs: u64) { + let listen_addr = format!("{}.{}.{}.{}:{}", listen_ip[0], listen_ip[1], listen_ip[2], listen_ip[3], listen_port); + let listen = open_reuse(&listen_addr); + let to_real = open_reuse("192.168.123.113:0"); // 源=113, 真机才应答 + let real_addr = format!("{}.{}.{}.{}:31772", real_ip[0], real_ip[1], real_ip[2], real_ip[3]); + println!(" AECH 桥 @ {} ↔ {} (真机源 123.113)", listen_addr, real_addr); + let mut pending: Option = None; + let start = Instant::now(); + let mut n_fwd = 0u64; let mut n_back = 0u64; + while start.elapsed().as_secs() < secs { + let mut buf = [0u8; 2048]; + // 远端设备 -> 真机 (AECH) + if let Ok((n, src)) = listen.recv_from(&mut buf) { + let b = &buf[..n]; + if b.len() >= 8 && &b[0..4] == b"ENet" && g32(b, 4) == 2 && find(b, b"AECH") { + pending = Some(src); + let _ = to_real.send_to(b, &real_addr); + n_fwd += 1; + println!(" [{}] 桥: {} → 真机 (AECH #{})", chrono_t(start), src, n_fwd); + } + } + // 真机 -> 远端设备 (AERE) + if let Ok((n, src)) = to_real.recv_from(&mut buf) { + let b = &buf[..n]; + if b.len() >= 8 && &b[0..4] == b"ENet" && g32(b, 4) == 2 { + if let Some(p) = pending { + let _ = listen.send_to(b, p); + n_back += 1; + println!(" [{}] 桥: 真机 → {} (AERE #{})", chrono_t(start), p, n_back); + } + } + } + } +} + +fn chrono_t(start: Instant) -> String { format!("{:.1}s", start.elapsed().as_secs_f64()) } + +fn patch_dev(b: &mut [u8], ip: [u8; 4], mac: [u8; 6]) { + // 找 Butler IP LE (01 7b a8 c0) 替换 — 字段为小端存储: 192.168.123.200 → c8 7b a8 c0 + for i in 0..b.len().saturating_sub(4) { + if b[i] == 0x01 && b[i + 1] == 0x7b && b[i + 2] == 0xa8 && b[i + 3] == 0xc0 { + b[i] = ip[3]; b[i + 1] = ip[2]; b[i + 2] = ip[1]; b[i + 3] = ip[0]; + break; + } + } + // 找 Butler MAC 替换 + for i in 0..b.len().saturating_sub(6) { + if b[i] == 0x00 && b[i + 1] == 0x16 && b[i + 2] == 0x1c && b[i + 3] == 0xf1 && b[i + 4] == 0x2e && b[i + 5] == 0x39 { + b[i..i + 6].copy_from_slice(&mac); + break; + } + } +} + +// ---------- T10/T11: RDM 探索 ---------- +pub fn t10_t11_rdm(secs: u64) -> Vec { + let mut items = Vec::new(); + let s = open(); + let req = load_template("rdm_req"); + // T10: 发送 RDM 请求事件帧模板 (DISC 帧), 解析 Butler RDM 应答 + println!(" 发送 RDM 请求模板帧并监听 {}s…", secs); + let deadline = Instant::now() + Duration::from_secs(secs); + let mut uids: HashSet = HashSet::new(); + let mut rdm_n = 0u64; + let mut disc_n = 0u64; + let mut req_seq = g32(&req, 0x22); + while Instant::now() < deadline { + let mut r = req.clone(); + p32(&mut r, 0x22, req_seq); + req_seq = req_seq.wrapping_add(1); + let _ = s.send_to(&r, BUTLER); + for (_, b) in recv_window(&s, Duration::from_millis(60)) { + match classify(&b) { + Rx::RdmResp(_, rlen) => { + rdm_n += 1; + if rlen >= 17 && b[0x2A] == 0xAA { disc_n += 1; } // 发现脉冲 + if rlen >= 18 && b[0x2A] == 0xCC { + let uid = hexs(&b[0x32..0x38]); + uids.insert(uid); + } + } + _ => {} + } + } + } + items.push(Item::new("T10", rdm_n > 0, + format!("RDM应答={} (发现脉冲={}, 设备UID={})", rdm_n, disc_n, + uids.iter().cloned().collect::>().join(" ")))); + // T11: 写地址 (PID 0x0330) —— e:cue 私有封装下 PID 未知, 仅记录探索结论 + items.push(Item::new("T11", false, + "写地址 SET DMX_START_ADDRESS: e:net 私有 PID 未确认, 需结合 GUI 写地址抓包比对 (见文档 §5.4)".to_string())); + items +} diff --git a/validation_report.json b/validation_report.json new file mode 100644 index 0000000..7829d80 --- /dev/null +++ b/validation_report.json @@ -0,0 +1,10 @@ +{ + "target": "192.168.123.1", + "results": [ + { "id": "U1", "pass": true, "detail": "发现应答=2426 遥测=21662 RDM脉冲=208904 RDM111=278460 RDM119=0" }, + { "id": "U2", "pass": false, "detail": "AECHA 认领交换=0 轮 (AEREP 已回)" }, + { "id": "U3", "pass": true, "detail": "明文 DMX 帧接收=206609" }, + { "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" } + ] +} From a98930aa7153a01e2922536e2dbf941041cc86ba Mon Sep 17 00:00:00 2001 From: tonycao Date: Fri, 11 Sep 2026 01:08:47 +0800 Subject: [PATCH 2/7] docs+scripts: enet_protocol.md, verification plan, PS verify suite, session analysis scripts, DLL tools, README --- README.md | 69 + docs/enet_protocol.md | 710 ++++++++ docs/rust_verification_plan.md | 85 + scripts/Verify-EnetProtocol.ps1 | 1902 +++++++++++++++++++++ scripts/Verify-Rdm.ps1 | 38 + scripts/enet_lib.ps1 | 47 + scripts/session/analyze_ack.ps1 | 55 + scripts/session/analyze_aech.ps1 | 48 + scripts/session/analyze_branch.ps1 | 74 + scripts/session/analyze_branch2.ps1 | 74 + scripts/session/analyze_ck.ps1 | 81 + scripts/session/analyze_claim_seq.ps1 | 56 + scripts/session/analyze_cold.ps1 | 240 +++ scripts/session/analyze_cold2.ps1 | 142 ++ scripts/session/analyze_coldstart_seq.ps1 | 31 + scripts/session/analyze_dmx1.ps1 | 70 + scripts/session/analyze_dmx2.ps1 | 82 + scripts/session/analyze_dmx3.ps1 | 96 ++ scripts/session/analyze_fix2.ps1 | 81 + scripts/session/analyze_mute.ps1 | 69 + scripts/session/analyze_set.ps1 | 57 + scripts/session/analyze_setb.ps1 | 57 + scripts/session/analyze_ss.ps1 | 157 ++ scripts/session/analyze_ss2.ps1 | 81 + scripts/session/analyze_startup.ps1 | 14 + scripts/session/cap_hostdemo.ps1 | 11 + scripts/session/cat_runbutler.ps1 | 9 + scripts/session/check_desc.ps1 | 25 + scripts/session/check_fix1_aech.ps1 | 12 + scripts/session/check_log.ps1 | 9 + scripts/session/check_log2.ps1 | 9 + scripts/session/check_log3.ps1 | 9 + scripts/session/check_log4.ps1 | 9 + scripts/session/check_log5.ps1 | 9 + scripts/session/check_proc.ps1 | 11 + scripts/session/check_proc2.ps1 | 9 + scripts/session/check_procs.ps1 | 9 + scripts/session/check_set.ps1 | 9 + scripts/session/check_status.ps1 | 11 + scripts/session/check_ubuntu_git.ps1 | 10 + scripts/session/cold2_order.ps1 | 24 + scripts/session/deploy_g.ps1 | 38 + scripts/session/deploy_set.ps1 | 39 + scripts/session/deploy_set2.ps1 | 31 + scripts/session/deploy_tests.ps1 | 29 + scripts/session/diff_ann2.ps1 | 49 + scripts/session/diff_ann3.ps1 | 24 + scripts/session/diff_announce.ps1 | 31 + scripts/session/enum_preclaim.ps1 | 20 + scripts/session/extract_symclaim.ps1 | 13 + scripts/session/find_claim2.ps1 | 8 + scripts/session/find_sub07.ps1 | 9 + scripts/session/find_sub07b.ps1 | 7 + scripts/session/grep_aech.ps1 | 11 + scripts/session/listen_113.ps1 | 18 + scripts/session/mystery60.ps1 | 11 + scripts/session/post_claim.ps1 | 11 + scripts/session/probe_loop.ps1 | 20 + scripts/session/probe_loop2.ps1 | 20 + scripts/session/push_try.ps1 | 8 + scripts/session/rdm_tail.ps1 | 11 + scripts/session/rdm_tail2.ps1 | 11 + scripts/session/read_symclose.ps1 | 16 + scripts/session/read_symclose2.ps1 | 16 + scripts/session/restore_butler.ps1 | 9 + scripts/session/run_hd3.ps1 | 11 + scripts/session/run_hd4.ps1 | 4 + scripts/session/start_butler.ps1 | 13 + scripts/session/start_local.ps1 | 23 + scripts/session/sync_docs.ps1 | 52 + scripts/session/sync_push.ps1 | 42 + scripts/session/sync_push2.ps1 | 36 + scripts/session/test_g.ps1 | 47 + scripts/session/test_newmac.ps1 | 14 + scripts/session/test_norelay.ps1 | 14 + scripts/session/test_setack.ps1 | 52 + scripts/session/test_setget.ps1 | 60 + scripts/session/verify_dmxsend.ps1 | 11 + scripts/session/verify_frame.ps1 | 27 + scripts/session/watch_claim.ps1 | 11 + scripts/session/watch_sym_claim.ps1 | 16 + scripts/session/webui_probe.ps1 | 11 + tools/GetTable.cs | 28 + tools/IlDump.cs | 71 + tools/Scan.cs | 30 + 85 files changed, 5634 insertions(+) create mode 100644 README.md create mode 100644 docs/enet_protocol.md create mode 100644 docs/rust_verification_plan.md create mode 100644 scripts/Verify-EnetProtocol.ps1 create mode 100644 scripts/Verify-Rdm.ps1 create mode 100644 scripts/enet_lib.ps1 create mode 100644 scripts/session/analyze_ack.ps1 create mode 100644 scripts/session/analyze_aech.ps1 create mode 100644 scripts/session/analyze_branch.ps1 create mode 100644 scripts/session/analyze_branch2.ps1 create mode 100644 scripts/session/analyze_ck.ps1 create mode 100644 scripts/session/analyze_claim_seq.ps1 create mode 100644 scripts/session/analyze_cold.ps1 create mode 100644 scripts/session/analyze_cold2.ps1 create mode 100644 scripts/session/analyze_coldstart_seq.ps1 create mode 100644 scripts/session/analyze_dmx1.ps1 create mode 100644 scripts/session/analyze_dmx2.ps1 create mode 100644 scripts/session/analyze_dmx3.ps1 create mode 100644 scripts/session/analyze_fix2.ps1 create mode 100644 scripts/session/analyze_mute.ps1 create mode 100644 scripts/session/analyze_set.ps1 create mode 100644 scripts/session/analyze_setb.ps1 create mode 100644 scripts/session/analyze_ss.ps1 create mode 100644 scripts/session/analyze_ss2.ps1 create mode 100644 scripts/session/analyze_startup.ps1 create mode 100644 scripts/session/cap_hostdemo.ps1 create mode 100644 scripts/session/cat_runbutler.ps1 create mode 100644 scripts/session/check_desc.ps1 create mode 100644 scripts/session/check_fix1_aech.ps1 create mode 100644 scripts/session/check_log.ps1 create mode 100644 scripts/session/check_log2.ps1 create mode 100644 scripts/session/check_log3.ps1 create mode 100644 scripts/session/check_log4.ps1 create mode 100644 scripts/session/check_log5.ps1 create mode 100644 scripts/session/check_proc.ps1 create mode 100644 scripts/session/check_proc2.ps1 create mode 100644 scripts/session/check_procs.ps1 create mode 100644 scripts/session/check_set.ps1 create mode 100644 scripts/session/check_status.ps1 create mode 100644 scripts/session/check_ubuntu_git.ps1 create mode 100644 scripts/session/cold2_order.ps1 create mode 100644 scripts/session/deploy_g.ps1 create mode 100644 scripts/session/deploy_set.ps1 create mode 100644 scripts/session/deploy_set2.ps1 create mode 100644 scripts/session/deploy_tests.ps1 create mode 100644 scripts/session/diff_ann2.ps1 create mode 100644 scripts/session/diff_ann3.ps1 create mode 100644 scripts/session/diff_announce.ps1 create mode 100644 scripts/session/enum_preclaim.ps1 create mode 100644 scripts/session/extract_symclaim.ps1 create mode 100644 scripts/session/find_claim2.ps1 create mode 100644 scripts/session/find_sub07.ps1 create mode 100644 scripts/session/find_sub07b.ps1 create mode 100644 scripts/session/grep_aech.ps1 create mode 100644 scripts/session/listen_113.ps1 create mode 100644 scripts/session/mystery60.ps1 create mode 100644 scripts/session/post_claim.ps1 create mode 100644 scripts/session/probe_loop.ps1 create mode 100644 scripts/session/probe_loop2.ps1 create mode 100644 scripts/session/push_try.ps1 create mode 100644 scripts/session/rdm_tail.ps1 create mode 100644 scripts/session/rdm_tail2.ps1 create mode 100644 scripts/session/read_symclose.ps1 create mode 100644 scripts/session/read_symclose2.ps1 create mode 100644 scripts/session/restore_butler.ps1 create mode 100644 scripts/session/run_hd3.ps1 create mode 100644 scripts/session/run_hd4.ps1 create mode 100644 scripts/session/start_butler.ps1 create mode 100644 scripts/session/start_local.ps1 create mode 100644 scripts/session/sync_docs.ps1 create mode 100644 scripts/session/sync_push.ps1 create mode 100644 scripts/session/sync_push2.ps1 create mode 100644 scripts/session/test_g.ps1 create mode 100644 scripts/session/test_newmac.ps1 create mode 100644 scripts/session/test_norelay.ps1 create mode 100644 scripts/session/test_setack.ps1 create mode 100644 scripts/session/test_setget.ps1 create mode 100644 scripts/session/verify_dmxsend.ps1 create mode 100644 scripts/session/verify_frame.ps1 create mode 100644 scripts/session/watch_claim.ps1 create mode 100644 scripts/session/watch_sym_claim.ps1 create mode 100644 scripts/session/webui_probe.ps1 create mode 100644 tools/GetTable.cs create mode 100644 tools/IlDump.cs create mode 100644 tools/Scan.cs diff --git a/README.md b/README.md new file mode 100644 index 0000000..621a3a2 --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# enet-validator — e:cue SymphoLight ↔ Butler e:net 协议逆向与复现 + +对 e:cue SymphoLight 5.5 与 Butler S2 之间 UDP/31772 私有协议(e:net)的完整逆向: +**AECH 认领算法破解、DMX 帧混淆破解、RDM over e:net 解码**,并分别实现了 +**设备端仿真**(虚拟 Butler)与**主机端仿真**(hostdemo 直接接管真机)。 + +## 目录 + +``` +├── src/ Rust 源码(std only,无外部依赖) +│ ├── main.rs 子命令入口 +│ ├── enet.rs e:net 帧模板 / SO_REUSEADDR 绑定 +│ ├── tests.rs 虚拟 Butler 设备端(AECH 本地应答 + RDM 灯具仿真 + 解密日志) +│ ├── aech.rs AgiosElias.G 认领算法、DMX zPprv0TXRRW 混淆加解密、dmxdec/dmxsend +│ ├── hostdemo.rs 主机侧等效示例(真机接管:认领→RDM 发现→加密 DMX 控制) +│ └── dmxhash.rs 早期帧标签分析 +├── samples/ 37 个真实抓包帧样本(hex) +├── docs/ +│ ├── enet_protocol.md ★协议逆向文档(帧布局/算法/实证,最核心) +│ └── rust_verification_plan.md 验证计划 +├── scripts/ +│ ├── Verify-EnetProtocol.ps1 协议验证套件 +│ ├── Verify-Rdm.ps1 RDM 验证 +│ ├── enet_lib.ps1 PS 公共库 +│ └── session/ 逆向过程中的会话脚本(抓包/分析/部署/推送) +└── tools/ DLL 反编译辅助工具(C#) + ├── IlDump.cs / GetTable.cs / Scan.cs +``` + +## 构建 + +``` +cargo build --release # std only, 无外部依赖 +``` + +## 子命令(设备端仿真,跑在任意机器,绑 58 网段示例) + +``` +enet-validator device # 虚拟 Butler 主循环(192.168.58.7) +enet-validator discover|announce|heartbeat|euce|session|all +``` + +## 子命令(主机侧 / 离线分析) + +``` +enet-validator hostdemo <目标IP> [秒] # 主机侧等效:认领→RDM 发现→加密 DMX 控制 + # 环境变量 HOSTDEMO_MODE=quick 最小认领+统一会话环 + # =claimonly 最小认领探针 + # HOSTDEMO_PRE=<秒> 前奏时长(默认45) +enet-validator dmxsend <目标IP> [秒] # 主机侧加密 DMX 播放(自证闭环) +enet-validator dmxdec # DMX 帧解密验证(离线) +enet-validator dmxroll # 滚动值分析(离线) +enet-validator aechprobe # AECH 在线探测 +``` + +## 已破解要点(详见 docs/enet_protocol.md) + +- **AECH 认领**:`G(x) = x ^ TABLE[(x%32 + 1 + (x*13)%30) % 32]`(无密钥,表内嵌 DLL) +- **DMX 混淆**:`r=Random.Next()`;`out[i]=src[(i+rot)%512]^cnt[i]`,`rot=(B12^r)&511` +- **RDM**:标准 E1.20 内嵌 type=04 帧(0x3C 起,免 0xCC 起始字节,checksum 含隐含 0xCC) +- **1947 端口**:40B ASCII 许可令牌广播(`BlZRQphA…`,每 ~34s 一轮) +- 真机固件过滤:AECH 仅应答源 IP=192.168.123.113 + +## Ubuntu 虚拟 Butler 部署 + +``` +scp -r enet-validator/ user@192.168.58.7:~/ +ssh user@192.168.58.7 "cd ~/enet-validator && cargo build --release && ~/run_butler_local.sh" +``` diff --git a/docs/enet_protocol.md b/docs/enet_protocol.md new file mode 100644 index 0000000..19d865f --- /dev/null +++ b/docs/enet_protocol.md @@ -0,0 +1,710 @@ +# e:net 协议复原文档(e:cue SymphoLight 5.5 ↔ Butler S2) + +> 基于实测抓包复原(2026-09-08 初版,Wireshark/tshark 抓取于"以太网 3")。 +> 抓包环境:SymphoLight 5.5 (192.168.123.113) ↔ Butler S2 固件 2.0.3322 (192.168.123.1)。 +> 样本:discovery.pcap(启动/发现阶段 56 帧)、dmx40s.pcap(工程驱动阶段 ~5900 帧)。 +> +> **2026-09-09 重大更新**:基于真机冷启动全程抓包 `cold2.pcap`(168.7s / 19471 帧, +> SymphoLight 冷启动 → 发现 → 注册 → AECH 认领 → RDM 全程)与虚拟 Butler 验证抓包 +> `fix1.pcap`。补全设备上线协议链(§3.6/§3.7),RDM 层完全解码(§5 重写), +> 新增虚拟 Butler 实现与验证(§11)。 +> +> **2026-09-10 更新**:稳态双网段对比(ss123/ss58)+ P1 修复验证(fix2/fix3)。 +> DMX 帧完全解码(§4.1 重写:seq/保护字节 `(CH-1)^K`/节点序号/端口布局); +> 42B 帧重定性为事件计数遥测(§4.3,推翻"逐帧 ACK");发现收敛与稳态轮询 +> 对齐(§11.3);新增 §11.5 DMX 解码里程碑、§12 缺口清单与攻关路线图。 +> +> **尺寸口径约定**:本文所有帧长如无特别说明均为 **UDP 负载字节数**。 +> 以太网整帧长 = UDP 负载 + 42(Eth14 + IP20 + UDP8)。历史记录中 +> "89B 注册帧"=47B 负载、"594B AECH"=552B 负载、"259B 发现应答"=217B 负载、 +> "101B 脉冲"=59B 负载、"84B ACK"=42B 负载(§4.3 遥测帧),均为同一帧的两种口径。 + +--- + +## 1. 总览 + +| 项目 | 值 | +|------|-----| +| 传输层 | UDP(无 TCP) | +| 主数据端口 | **31772** (0x7C1C),双向,主机与设备均使用该端口收发 | +| 密钥/许可端口 | **1947**,主机→广播,周期性,静态负载 | +| 帧签名 | 负载前 4 字节 = ASCII `"ENet"`(`45 4E 65 74`) | +| 字节序 | 混合:长度/计数器字段为 **小端 (LE)**,端口字段为 **大端 (BE)** | +| 加密 | **DMX 通道数据经 zPprv0TXRRW 逐帧混淆**(§4.1,已完全破解可双向复现):rot=(B12^r)&511,B12=AECH 实例随机(会话),r=Random.Next()(每帧每端口,帧内 0x22/0x26)。发现帧含部分随机材料 | +| Wireshark 识别 | 无 dissector,全部显示为 `data` | + +--- + +## 2. e:net 通用帧头(所有 31772 报文) + +UDP 负载布局(偏移为负载内字节偏移): + +``` +偏移 长度 示例(主机DMX帧) 含义 +0x00 4 45 4E 65 74 魔数 "ENet"(另见 §3.4 的 "EUCE" 第二魔数) +0x04 4 04 00 00 00 消息类型 (LE uint32):01=发现/宣告, 02=主机标识心跳, 04=会话数据 +0x08 4 00 00 1C 7C 端口 31772 (BE uint16 前置 0000) +0x0C 6 00 00 00 00 00 00 保留 (实测恒 0) +0x12 2 01 00 协议版本/标志 (实测恒 0x0001) +0x14 4 3C 04 00 00 负载总长 (LE uint32) = UDP负载字节数 +0x18 2 01 00 固定字段 (实测所有 ENet 会话帧恒 0x0001) +0x1A 2 97 01 DMX 帧序号 (LE uint16,仅 DMX 数据帧;0x197→0x198→…) +0x1C ... 类型专属字段 +``` + +已实测的长度字段对应关系: +| 帧总长(以太网) | UDP负载长 | 0x14 处 LE 值 | +|---------------|-----------|---------------| +| 222 / 259 / 540 / 686 / 1126 | 180/217/498/644/1084 | 0xB4/0xD9/0x1F2/0x284/0x43C ✓ | +| 84 / 171(Butler→主机) | 42/129 | 0x2A/0x81 ✓ | + +--- + +## 3. 消息类型 01 —— 设备发现 / 宣告 + +### 3.1 主机发现请求(222 B,主机→255.255.255.255 广播) +- 负载 180 B。0x18 处 `B4 00 00 00`(=180)。 +- 负载中含:主机 IP (LE `71 7B A8 C0`=192.168.123.113)、主机 MAC、 + 机器名+GUID ASCII(如 `tonycaoDESKTOP-PED2LVD-f7f4a043-1a`)、尾部 `09 00`。 + +### 3.2 Butler 发现应答(259 B,Butler→主机单播) +- 负载 217 B。含设备名 ASCII `"Butler S2"`、Butler IP (LE `7B A8 C0 00` 区域)、 + Butler MAC (`00 16 1C F1 2E 39`)、固件信息区 + (`23 01 00 00 00 00 24 00 00 00 24 00 00 00 1C 00 ...` 版本/参数块)、 + DMX 时序参数区(`F0 0A`=BRK 100µs? `F0 0A`、MAB 等)。 + +### 3.3 主机宣告帧(540 B / 686 B,主机→广播,周期 ~30s) +- type=01,686 B 版负载 644 B(0x284 ✓)。含: + - 主机 IP、广播地址、主机 MAC + - 16 B 随机/密钥材料:`43 A0 F4 F7 EC 17 56 41 81 2B 90 9B DC 99 F2 FF` + - **UTF-16LE** 主机名+GUID:`tonycaoDESKTOP-PED2LVD-f7f4a043-17ec-4156-812b-909bdc99f2ff` + - `D0 07` = 2000(LE,疑似辅助端口) + - 时间戳 `EA 07 09 07 17 14 12 F4` = 2026-09-07 17:14:20.244(年/月/日/时/分/秒/毫秒) + - 尾部 `5A 00 FF FF` + +### 3.4 主机标识心跳(36 B 负载 / 78 B 帧,type=02 sub=03,主机→广播,~6-30s 周期) + +``` +0x14 24000000 负载长 36 +0x18 0300 子类型 3 +0x1A 717BA8C0 主机 IP 192.168.123.113 (LE) +0x1E 300ED559BB36 主机 MAC +``` + +★2026-09-09 实测(cold2.pcap):认领后每 ~6s 一发,**设备从不应答**。 +(旧虚拟 Butler 实现误回 42B 状态帧,已列入修正项。) + +### 3.5 "EUCE" 帧(582 B,非 ENet 魔数,主机→广播,~30s 周期) + +- 魔数 `45 55 43 45` = ASCII **"EUCE"**(与 "ENet" 并存的第二种 e:cue 载荷签名)。 +- `EUCE 0000 0000 0000 0000 0500 0000 …`(**0x0C 处 LE `05`**=类型/版本 5),其余大量零。 +- 40s 窗口观测 6 帧(帧号 668/1576/2530/3421/4309/5222,间隔 ~908 帧 ≈ 30s), + 与 type=02 心跳交替出现,属主机的周期性身份/发现通告族。 + +### 3.6 设备注册(type=02 sub=01 → sub=02)—— 认领前的关键步骤 ★2026-09-09 补全 + +冷启动实测(cold2.pcap,t=38.087): + +``` +主机 → Butler: 102B type=02 sub=01 主机身份宣告 + 0x1A-0x1D 主机 IP (LE) + 0x1E-0x23 主机 MAC +Butler → 主机: 47B type=02 sub=02 注册应答 (10ms 内) + 0x1A-0x1D 设备 IP (LE) 例: 017ba8c0 = 192.168.123.1 + 0x1E-0x23 设备 MAC 例: 00161cf12e39 + 0x24 00 + 0x25-0x28 主机 IP (LE) 例: 717ba8c0 = 192.168.123.113 + 0x29-0x2E 主机 MAC 例: 300ed559bb36 +``` + +47B 注册应答完整样本: +`454e6574 02000000 00001c7c 00000000 0000 0100 2f000000 0200 017ba8c0 00161cf12e39 00717ba8c0 300ed559bb36` + +广播的 sub=01 宣告(255.255.255.255)同样触发设备回 sub=02 注册帧。 + +### 3.7 AECH 认领(type=02 sub=05 → sub=06)—— 加密会话建立 ★★2026-09-10 完全破解 + +注册完成后主机立即发起认领(cold2.pcap,t=38.105,**连续 3 轮**,设备 **1ms** 内应答): + +``` +主机 → Butler: 552B type=02 sub=05 AECH 质询 (含 "AECH"=41454348 @0x28) + 0x1A-0x1D 主机 IP (LE) + 0x1E-0x23 主机 MAC + 0x28-0x2B "AECH" + 0x2C-0x87 92B 纯随机填充 (设备完全忽略!) + 0x88-0x8B ★ 4B 质询 nonce (LE) — 唯一影响应答的输入! + 0x8C-0x8F ★ 4B 主机实例随机 B12 (LE, 会话内不变, 会话间不同) + — 兼任 DMX 混淆基值: rot=(B12^r)&511 (§4.1) + 0x90-0x123 148B 纯随机填充 (设备完全忽略!) +Butler → 主机: 552B type=02 sub=06 AERE 应答 (含 "AERE"=41455245 @0x28) + 0x1A-0x1D / 0x41-0x44 设备 IP (LE, 两处) + 0x1E-0x23 / 0x49-0x4E 设备 MAC (两处) + 0x25-0x27 主机 IP 前3字节 (LE) + 0x2C-0x2E 主机 MAC 后3字节 + 0xC0-0xC3 ★ 应答 = G(nonce) — 算法见下 + 其余 设备身份串 + RDM 灯具摘要 + 259 式信息块 (全部静态) +``` + +**★应答算法(源自 SymphoLight `Ecue.Devices.dll` 反编译,类 `Ecue.Devices.ENet.Protocol.AgiosElias`)**: + +``` +G(x) = x ^ TABLE[(x%32 + 1 + (x*13)%30) % 32] (u32 环绕乘法) +TABLE = 32×u32 常量表, 内嵌于 DLL 静态数组 (0A8F2B10..., 128B): + 1008de00 e3b77ff6 31183e1c 90fb53c8 ff10ba5c 030bc4b6 e22935c8 10e8ede1 + 55f409a0 3eb0b9a3 f7333308 edf07810 8abf4078 60ec0545 1f4645c8 54094148 + 04c0f500 fabe50bb f3f3e54c 6c2a02cb a2546e60 4f6ae10e 51dfe8d8 2eedb0ec + 74006928 48fade70 e5346e9c 1c3fe0bc f6700700 6b7f1ba0 a1143cf4 3be8426c +主机验证 (AgiosElias.i4GrvIBQ6xF): AERE blob[152] (= 帧 0xC0) == G(主机 nonce) +``` + +**无密钥、无 dongle 参与**——纯混淆级算法。黑盒探测(aechprobe/aechprobe2 子命令)实证: +仅 0x88-0x8B 影响应答(全雪崩),确定性;离线 G 对 cold2+fix1 共 10/10 样本命中; +虚拟 Butler 已实现本地计算(`aech::agios_g`),随机 nonce 测试 5/5 PASS。 +**终极实测(2026-09-10 14:29)**:SymphoLight 重启后 .7 在无桥/无中继条件下完成 +3 轮 AECH 本地应答 → 89B 注册 → DMX 流恢复 → RDM 灯具重新注册,全流程独立。 +**认领不再依赖真机中继**(中继/桥代码保留作对比,桥已退役)。 + +**实测过滤行为**:真机 .1 只应答**源 IP=192.168.123.113** 的 AECH(任意源端口均可), +其他源 IP 的质询直接丢弃(这是真机固件行为;虚拟设备无需此过滤)。 + +认领通过后:主机开始 DMX 会话(§4),心跳 sub=03(36B,每 ~6s,**设备不应答**)。 + +--- + +## 4. 消息类型 04 —— 会话数据(连接后的主通道) + +type=04 的帧在 0x20 处有 2 字节**子类型/方向标记**: +- `01 01` = 主机→设备 **DMX 通道数据**(形态 A) +- `02 00` = 事件/状态帧(形态 B,双向:主机发事件,Butler 周期回 42B 计数遥测 §4.3) + +### 4.1 形态 A:DMX 通道数据帧(1126 B,主机→Butler,~30 Hz)★2026-09-10 完全解码(含混淆算法) + +``` +负载偏移 长度 示例 含义 +0x00 4 454E6574 "ENet" +0x04 4 04000000 type=4 +0x08 4 00001C7C 端口 31772 +0x0C 6 000000000000 保留 +0x12 2 0100 版本/标志 +0x14 4 3C040000 负载长 1084 +0x18 2 0100 wnor8bDK5nr u16 = 1 (DMX 数据消息) +0x1A 4 4CB20000 ★ 帧序号 aOKr8qAKOAa LE u32,逐帧 +1(高 2B 恒 0) +0x1E 2 0000 OcpmOpr u16 = 0 +0x20 2 0101 cbY0uIrZR2S3QxGbNbNC × 2 (每端口 1B, 均=1) +0x22 4 6E100C2D ★ r1 = Random.Next() LE u32 (每帧每端口随机, MSB=0) +0x26 4 80681200 ★ r2 = Random.Next() LE u32 (端口2) +0x2A 2 0000 端口状态字节 × 2 (universe 状态表查得, 通常 0) +0x2C 2 0400 端口1 ID = 4 (LE, 两设备同值) +0x2E 2 0200/0300 ★ 端口2 ID = 节点序号 LE: 真机 .1=2, 虚拟 .7=3 +0x30 2 0000 端口索引0 (LE) +0x32 2 0100 端口索引1 (LE) +0x34 2 0002 端口1 通道数 = 512 (LE! 00 02, 旧文档"BE"系误读) +0x36 2 0002 端口2 通道数 = 512 (LE) +0x38 2 0100 固定 (01 00) +0x3A 2 0000 固定 +0x3C 512 ★ 端口1 DMX 通道 1-512 — zPprv0TXRRW 混淆 (见下, 非明文!) +0x23C 512 ★ 端口2 DMX 通道 513-1024 — 同式混淆 (用 r2) +``` + +**★通道数据混淆(2026-09-10 反编译+实证完全破解;旧文档"明文/保护字节/滚动值"系误判)**: + +构建链(反编译 `iey7k4rDmrsMD1TvRywm::ScheduleUpdate` → `ENet2MessageCreator::QJiBkgLcunE` +→ `vpFbA8r8ibe5ncT1Lum7::CopyToBuffer`,60B 头结构逐字段与线值吻合): + +``` +r = Random.Next() # 每帧每端口独立, u31 (MSB 恒 0), 写入 0x22/0x26 +rot = (B12 ^ r) & 511 # B12 = AECH 质询 0x8C-0x8F 实例随机 (AgiosElias.B12rvMuR8pT, 会话不变) +out[i] = src[(i+rot) % 512] ^ cnt[i] # src = 真实通道值 + cnt[i] = (512-rot-i) & 255 (i < 512-rot) + = (rot-(i-(512-rot))) & 255 (i >= 512-rot) +``` + +接收方解密:`src[(i+rot)%512] = wire[i] ^ cnt[i]`(rot 由帧内 r 与会话 B12 重建)。 + +**实证**:cold2(B12=0x1e67079f)3893 帧 + fix1(B12=0x70959ecf)1781 帧全部解密为 +跨帧稳定的全零场景(测试期间主机发的是 blackout),加解密回环 5674/5674 逐字节一致 +(`aech::agios_dmx_encrypt`/`agios_dmx_decrypt`,`dmxdec` 子命令)。 +**"保护字节 (CH-1)^K" 系涌现巧合**:src=0 时 wire[0x3C]=(512-rot)&255,代数化简得 +K = ~B12 & 0xFF(cold2: ~0x9f=0x60 ✓、fix1: ~0xcf=0x30 ✓、ss123: K=0x70 → B12l=0x8f); +0.5% 失配 = rot%256==0 的边界帧。宿主侧另有 `kWarv47o79k`(新算法, +`out[i]=src[i]^((512-i)&255)^key[(512-i)&3]`, key=r^B12,CP7rDoD7VL4() 标志选择)—— +Butler S2 走旧算法。 + +**端口映射**:工程 universe 号(U6/U2、U1/U5)**不上线**,主机按端口序拼包,Butler 解密后按 +port1/port2 各 512B 直接映射到 RS485 输出。实测帧率 29.8/s(中位间隔 33ms),稳态恒定。 + +### 4.2 形态 B:事件/状态帧(主机→Butler,1126 B)★2026-09-10 完全解码 + +> 速率随主机活动模式变化:纯 DMX 播放时 A:B ≈ 1:2(dmx40s: 1207A+2336B/40s); +> RDM 激活稳态时 B ≈ 24.7/s、A 恒定 29.8/s(ss123)。旧版"A·B·B 固定节奏" +> 仅为其中一种模式。 +> +> **帧构成 = [事件头 0x00-0x3B] + [标准 E1.20 RDM 消息] + [通道图案填充至 1084B]**。 +> ss123 实证:RDM 激活稳态下 2963/2963 事件帧 100% 为 RDM 查询 +> (cmd24×1886 + cmd18×645 + cmd19×352 + flag=01 内嵌副本×80),无非 RDM 事件帧。 + +``` +负载偏移 长度 示例 含义 +0x00-0x17 24 … 通用头 ("ENet"/type=4/端口/负载长 1084) +0x18 4 01000000 固定 +0x1C 4 00000000 固定 +0x20 2 0200 子类型 = 事件 +0x22 4 65890000 ★ 事件序号 u32 LE,严格 +1(2962/2962 无例外) +0x26 4 00000000 固定 +0x2A 4 CC000000 常量 0xCC +0x2E 2 0100 事件类标记(DMX-A 帧此位为节点序号 2/3) +0x30 1 00/01 ★ flag:00=独立查询帧(设备应答); + 01=DMX 内嵌副本(设备不应答),此时 0x32=01 +0x31 3 000000 固定 +0x34 4 25000000 ★ RDM 消息长度 LE u32 = E1.20 msgLen+1(含 SubSC) +0x38 4 01000000 疑似 RDM 总线/端口号(LE,本样本恒 1) +0x3C … ★ 标准 E1.20 消息(免 0xCC 起始字节): + 3C:SubSC=01 3D:msgLen 3E-43:destUID 44-49:srcUID + 4A:TN 4B:port 4C:msgCount 4D-4E:subDevice 4F:CC + 50-51:PID(BE) 52:PDL 53+:PD 末尾:checksum(BE) +… … ★ 通道图案填充:从 0x3C+RDM长 到帧尾,约 1011-1031B +``` + +**填充区性质(2026-09-10 实证)**:外观为 DMX 通道图案(渐变波形),但 24B +签名在本网段与跨网段 DMX-A 流中均只命中**周期性效果的自相似片段** +(时间差中位 -50s、偏移散乱)→ 与任何实时 universe 数据无系统对应, +判定为**主机缓冲复用/填充,无协议语义**。Butler 只需解析 RDM 消息即可 +(虚拟 Butler 实现即如此)。 + +**E1.20 字段实证对齐**(逐偏移标注): +- 0x4F = Command Class:cmd24/18 → 0x10(DISCOVERY)、cmd19 → 0x20(GET)、cmd=1a → 0x30(SET) +- cmd24 → PID=0001 UNIQUE_BRANCH(PD=分支 lo/hi 12B);cmd18 → PID=0002 UN_MUTE + (PDL=0);cmd19 → PID=0201 SENSOR_VALUE(PD=传感器序号) +- RDM 长度字段实证:cmd24→37、cmd18→25、cmd19→26(= 24 头 + PDL + 1) +- flag=01 内嵌副本:srcUID 末字节 fe(独立帧为 ff)、0x4B(port)=01 + +### 4.3 Butler 42B 事件计数遥测帧(Butler→主机,~每 115ms 一帧) + +``` +0x14 2A000000 负载长 42 +0x18 0200 子类型 +0x1A 4275746C 设备名前缀 "Butl" +0x1E 0100 +0x20 4 xxxxxxxx ★ Butler 本地"已收事件B帧"累计计数(LE, 严格递增) +0x24 6 010000000000 +``` + +★2026-09-10 更正(ss123 实证):0x20-0x23 **不是** "被确认事件帧的序号", +而是 Butler 自己累计收到的事件B帧数(120s 内 +2959 ≈ 实收事件 2963; +相邻帧差 +1~+10,1039/1039 严格递增)。该帧以 ~8.7/s(中位间隔 35ms, +有事件才发)周期上报,与具体查询**无逐帧配对关系**——旧结论"ACK seq = +事件 seq - 1"是"计数器≈主机事件序号"的自然巧合,叠加分析脚本 +PowerShell 运算符优先级 bug(`$a.seq -eq $q.seq - 1` 解析为 +`($a.seq -eq $q.seq) - 1`)导致的假相关。收到非 RDM 事件B帧**不应**即时 +回 42B。 + +### 4.4 Butler 状态帧(171 B 等,Butler→主机,周期性) + +与 42B 遥测同构:`ENet` + type=04 + `0200` + `"Butl"` + `0100` + 事件计数器(0x317…) + +`0000` + 状态块(首字节 `57`=87…,尾部补零至 129 B 负载)。 +观测到的状态帧长度分布:171/177-247 B(约 60 余种长度,随状态内容伸缩)。 + +★2026-09-09 补注(cold2.pcap):上述"状态帧"实际多为 RDM 应答帧(69-129B 负载); +认领后的稳态会话中,真机**几乎不发送任何自发状态帧**。旧虚拟 Butler 的 +"每 3.5s 突发 10 帧 84B"为错误指纹。 + +★2026-09-10 再补(ss123):稳态下真机**确有** 42B 帧,但性质是 §4.3 的 +事件计数遥测(~8.7/s,有事件才发),既非状态帧也非逐帧 ACK。cold2 的 +168s 仅 1 帧是因为冷启动窗口内事件量小。 + +### 4.5 流量节奏(实测 40 s 窗口) + +| 流量 | 包数 | 说明 | +|------|------|------| +| 主机→Butler 1126 B | 3543 | 形态A 1207 + 形态B 2336(该窗口 A:B≈1:2;稳态 RDM 窗口 A 29.8/s 恒定、B ~24.7/s) | +| Butler→主机 84 B(负载42B) | 1250 | 事件计数遥测(§4.3,非逐帧 ACK) | +| Butler→主机 状态帧 | ~700 | 各长度状态/心跳 | +| 主机→广播 1947 | 42 | 密钥报文,~1 Hz 成对 | +| 主机→广播 686 B | 13 | type=01 宣告,~3 s 间隔 | +| 主机→广播 78 B | 7 | type=02 标识心跳,~30 s 间隔 | +| 主机→广播 582 B | 6 | "EUCE" 帧,~30 s 间隔 | + +## 5. RDM over e:net ★2026-09-09 完全解码(推翻旧版"私有变体"结论) + +> 触发条件:SymphoLight RDM 窗口 → Butler 端口 Enable RDM → Discover; +> 认领通过后主机持续进行后台 RDM 发现/轮询(无需人工触发)。 +> 旧版"e:cue 私有变体、字段序与 E1.20 不一致"结论系解析偏移错误—— +> **e:net 封装的 RDM 负载就是标准 E1.20 报文**(含标准 checksum,真机 1396/1396 验证通过)。 + +RDM 流量全部复用 type=04 会话帧,无独立消息类型。 +主机→设备的 RDM 查询帧完整布局见 **§4.2**(事件头 + 免 0xCC 起始的 E1.20 消息 +@0x3C + 通道图案填充)。 + +### 5.1 e:net 封装(RDM 应答,Butler → 主机) + +``` +0x00 4 "ENet" +0x04 4 04000000 type=4 +0x08 4 00001C7C 端口 +0x0C 6 00…00 保留 +0x12 2 0100 +0x14 4 负载总长 (LE) +0x18 2 0200 子类型 +0x1A 4 "Butl" (4275746C) +0x1E 2 0000 / 0100 标志 +0x20 4 序号 (LE):参数应答 = 回显查询 0x22-0x23;DISC 脉冲 = 设备本地自增 +0x24 4 00000000 +0x28 2 ★ RDM 数据区长度 (LE) = msgLen + 2 +0x2A ... ★ RDM 数据区 + - 标准 E1.20 报文: CC 01 ... + - DISC 脉冲: AA <9-bit 编码 16B> (无 CC01, 见 §5.4) +``` + +### 5.2 标准 E1.20 字段布局(0x2A 起) + +``` +0x2A CC Start Code (SC_RDM) +0x2B 01 Sub Start Code +0x2C msgLen Message Length = CC 起到最后一个 PD 字节的字节数 (不含 checksum) +0x2D 6B Dest UID = 主机 RDM 控制器 UID (实测 45:3a:7f:ff:ff:ff) +0x33 6B Src UID = 灯具 UID (应答时) +0x39 TN Transaction Number — 应答回显查询的 TN +0x3A Response Type (00=ACK) +0x3B Message Count +0x3C 2B Sub-Device (0000) +0x3E CC Command Class (0x11=DISC_RESP, 0x21=GET_RESP, 0x31=SET_RESP) +0x3F 2B PID (BE) +0x41 PDL +0x42 PD (PDL 字节) +末尾 2B Checksum = 从 CC(0x2A) 起 msgLen 字节的 16-bit 和, 大端 +``` + +**checksum 规则(已验证 1396/1396)**:`sum16(CC..最后一个PD字节) == checksum(BE)`。 +任何模板 patch(改 UID/TN/bridge-id)后**必须重算**,否则主机按 E1.20 规范丢弃。 +(注意 PowerShell 陷阱:`-shl` 保持类型宽度,`[byte] -shl 8` 恒得 0,验证脚本须先转 int。) + +### 5.3 主机 → Butler 的 RDM 请求(type=04,"cmd" @0x3D 实为 E1.20 msgLen) + +请求帧两种形态:独立短帧(~112B 负载)与 1084B DMX 内嵌帧(0x30 flag=01)。 +**实测真机只应答 flag=00 的帧**,flag=01 为 DMX 流内嵌副本,不答。 + +★2026-09-10 更正(SET 抓包实证):**0x3D 不是 e:net 命令字节,是 E1.20 msgLen**。 +旧称 cmd=24/18/19/1a 恰为各类请求的 msgLen:0x24=36(UNIQUE_BRANCH, PDL=12)、 +0x18=24(PDL=0 的 DISC/GET)、0x19=25(GET SENSOR_VALUE, PDL=1)、 +0x1A=26(SET DMX_START_ADDRESS, PDL=2)。命令语义由 CC@0x4F 决定 +(0x10 DISCOVERY / 0x20 GET / 0x30 SET)。下文沿用"cmd=xx"仅作 msgLen 类别简写。 + +``` +0x20 2 子类型 0200 +0x22 2 查询序号 (LE) — 应答在 0x20-0x21 回显 +0x30 1 flag: 00=独立帧(应答), 01=DMX内嵌副本(不答) +0x3C 1 SubSC = 01 +0x3D 1 E1.20 msgLen(非命令字节!) +0x3E 6 目标灯具 UID (发现时为分支下界, 全范围=ffffffffffff) +0x44 6 主机 RDM 控制器 UID (bridge-id, 453a7fffffff) +0x4A 1 TN (Transaction Number) +0x4F 1 Command Class: 0x10=DISCOVERY / 0x20=GET / 0x30=SET +0x50 2 ★ PID (BE) +0x52 1 PDL;0x53+ PD;末尾 checksum (BE) +``` + +### 5.4 DISC 脉冲(DISC_UNIQUE_BRANCH 响应)—— 编码已破解 + +脉冲不是标准 RDM 包,是 E1.20 发现响应的 9-bit 线路编码(17B): + +``` +AA 分隔符前缀 +<16B: 8×2B 编码 UID> 每字节 b → byte1=0xAA|(b&0x55), byte2=0x55|(b&0xAA) + (GF(2) 仿射求解, 13 样本全过; 解码: b=(b1&0x55)|(b2&0xAA)) +<4B: 2×2B 编码 checksum> checksum=16 位编码后 UID 字节的和, 先高字节后低字节 +``` + +- 脉冲帧 e:net 序号(0x20)= **设备本地自增序号**(非回显查询!),每次 +1..3。 +- 实测脉冲时延 2-48ms(中位 29ms);应答率 ~40%(967/2419,含 mute 效果)。 + +### 5.5 发现/验证语义(实测裁决) + +| 主机查询 | 真机应答 | 说明 | +|---|---|---| +| cmd=24 全范围 UNIQUE_BRANCH (UID=ffffffffffff) | DISC 脉冲(59B 负载) | 未静音灯具应答 | +| cmd=18 PID=**0002** (DISC_UN_MUTE) | **70B 参数 ACK**: `CC=11 PID=0002 PDL=2 PD=0000` + 有效 checksum,延迟 17-29ms | ⚠️ **不是脉冲!** 虚拟 Butler 曾错答脉冲导致主机死循环重试(947 次),灯具永不注册 | +| cmd=18 PID=**0003** 广播 MUTE (UID=ffff…) | **无应答**(0/10) | 广播静音不应答;780 条 flag=01 0003 为 DMX 内嵌广播 MUTE | +| cmd=18 PID=0060 (DEVICE_INFO) | 87B,每灯具每轮 ~10 次/3min | 注册后主机每 ~1min 重读 | +| cmd=18 PID=00f0 (DMX_START_ADDRESS) | 70B | 同上节奏 | +| cmd=18 PID=0050 (SUPPORTED_PARAMETERS) | 78/178/182B | — | +| cmd=18 PID=80xx(厂商-specific 40 余个) | 69-72B,100% 应答 | 延迟 12-35ms(中位 29ms) | + +**主机 RDM 流程**:后台持续全范围发现(~14-24/s)→ 对新 UID 发 UN_MUTE(0002) 验证 +→ 拿到合法 ACK 后读 DEVICE_INFO(0060)/标签/参数注册进拓扑 → 每 ~1min 轮询。 +**若 0002 应答非法,主机永远停在 0002 重试,不会进入参数读取阶段。** + +### 5.6 SET 写路径(★2026-09-10 set123b.pcap 实证,GUI 改地址触发) + +**抓取方法**:SymphoLight RDM 窗口改真机灯具 DMX 地址(NFM680S `010734880900` +111→114→111→112),双网段同抓。 + +**SET 请求**(主机→Butler,1084B 事件帧,flag=00): +- CC=**0x30**@0x4F、PID=00f0@0x50 (BE)、PDL=2@0x52、PD@0x53 = **DMX 地址 BE u16** +- 0x3D msgLen=26 (=24+2),0x34 RDM 长=27 (=msgLen+1) +- 完整布局与 GET 相同(§4.2/§5.3),仅 CC/PDL/PD 不同 + +**SET 应答**(Butler→主机,68B 负载): +- CC=**0x31**@0x3E (SET_COMMAND_RESPONSE)、RT=**00 (ACK)**@0x3A、**PDL=0**(无参数数据) +- TN 回显(查询 0x4A → 应答 0x39)、checksum 有效、延迟 18-25ms(RS485 指纹) + +**主机行为**:SET 后 **+33ms 立即 GET 同一 PID 验证**(TN 连续 +1),应答中 +新地址已生效;此后稳态轮询按新地址继续。三次 SET (114/111/112) 模式完全一致。 + +**虚拟 Butler 实现(2026-09-10 上线,GUI 改地址实测通过)**:SET (CC=0x30) 一律回 +68B ACK(`rdm_setack_68.hex` 模板,TN/UID/seq/PID 回显 + checksum 重算,延迟 15-25ms), +并按 `(UID, PID) → PD` 落存 `set_store`;后续 GET 同 PID 时以存储值覆盖模板 PD 再重算 +checksum。**只 ACK 不落存会被主机的 SET→GET 验证识破**(GUI 显示改地址无效)。 +该机制对所有 PID 通用(地址/标签等均可写)。 + +**残余**:厂商 PID 0x0200 族的 SET 写路径未触发(GUI 未暴露对应操作); +IDENTIFY_DEVICE 等其他 SET PID 未专项。 + +--- + +## 6. 端口 1947 —— 密钥/许可报文 + +- 主机→`192.168.123.255` 与 `255.255.255.255` 成对广播,源端口 53685/53687。 +- 负载恒定 40 B,ASCII Base64: + `BlZRQphAGxUbi7at8KEfmdZ7TdSB903b+qoC1RIA` +- Base64 解码 = 30 B 高熵二进制(`06 56 51 42 98 40 1B 15 1B 8B B6 AD F0 A1 1F 99 D6 7B 4D D4 81 F7 4D DB FA AA 02 D5 12 00`)。 +- 长时间观察内容**不变** → 静态许可/身份令牌,非会话密钥。 +- 与 OLA 社区所述 "key message" 一致。DMX 数据并不依赖其加密(见 §4.1 明文结论)。 + +--- + +## 7. 会话建立时序(★2026-09-09 以 cold2.pcap 冷启动实测更新) + +``` +t=0~23s 主机 Butler S2 + │ 01 发现请求 180B 广播(~每3-5s) ─▶│ + │◀────── 01 发现应答 217B (设备名/MAC/IP/ConnectedTo) │ +t=38.087 │ 02 宣告 sub=01 102B ─────────▶│ + │◀────── 02 注册 sub=02 47B (10ms) │ ★ 认领前关键步骤 +t=38.105 │ 02 AECH sub=05 552B ─────────▶│ + │◀────── 02 AERE sub=06 552B (1ms) │ ×3 轮 (38.105/38.111/38.140) +t=40+ │ 04 DMX帧 A(seq) ─────────────▶│ + │ 04 事件帧 B ──────────────────▶│ + │◀─── 04 计数遥测 42B (~115ms) ────│ … A 29.8/s 恒定 + B 随模式 … + │ 04 RDM 查询(cmd24/18) ────────▶│ 后台持续发现/轮询 + │◀─────── 04 RDM 脉冲/参数应答 ────│ 延迟 12-48ms +t=44+ │ 02 心跳 sub=03 36B (每~6s) ───▶│ ★ 设备不应答 +``` + +Butler 若 ~数秒收不到主机数据则自动回落 standalone 模式(播 SD 卡 show)。 + +--- + +## 8. ESP32 模拟可行性结论 + +| 需求 | 结论 | +|------|------| +| 接收 DMX 数据 | ✅ **可行**:明文。监听 UDP 31772,过滤 `454E6574 04000000` + 0x20 处 `0101`,从负载 0x3C 取 1024 B 通道数据 | +| 维持连接 | ⚠️ 需模拟:周期回复 ACK(0x2A 负载、递增序号)+ 状态帧,防止主机判定离线;同时需应答 type=01 发现(259B 应答帧格式见 §3.2) | +| 阻止 standalone 抢发 | 停止收 DMX 后设备(真 Butler)会播 SD show;ESP32 自身无此问题 | +| 1947 端口 | 无需实现(仅主机广播,设备不监听) | +| 加密 | 无需解密——通道数据明文 | + +ESP32 最小实现 = **被动接收**:绑 31772 收形态A 帧、取 0x3C 起 1024 B 即得到全通道值; +要长期稳定在线再补 ACK/状态/发现应答三类模拟帧(结构均已在本文档给出)。 + +--- + +## 9. 抓包样本索引 + +| 文件 | 内容 | +|------|------| +| `C:\Users\tonycao\AppData\Local\Temp\opencode\discovery.pcap` | 启动/发现阶段 56 帧 | +| `C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s.pcap` | 驱动阶段 40s,5903 帧 | +| `dmx_one_hex.txt` | 1126 B DMX 帧完整 hex ×2 | +| `rdm3.pcap` | RDM 会话(Enable RDM + Discover,DUT 停发后抓取) | +| `f686.txt` | 686 B 宣告帧完整 hex | +| `cold2.pcap` ★ | **真机 .1 冷启动全程**(168.7s/19471 帧:发现→注册→AECH→RDM),2026-09-09 | +| `fix1.pcap` ★ | **虚拟 Butler .7 验证**(58 网段,P0 修复后主机读 DEVICE_INFO 全程),2026-09-09 | +| `ss123.pcap` / `ss58.pcap` ★ | **稳态双网段对比**(120s:.1 真机 vs .7 虚拟),2026-09-10 | +| `fix2.pcap` / `fix3.pcap` ★ | **P1 修复后 .7 验证**(cmd24 收敛/稳态轮询/42B 遥测),2026-09-10 | + +## 10. 待确认项(低置信度字段) + +- ~~0x22 处 8 B 字段~~ → **已破解**(§4.1,2026-09-10):0x22/0x26 = 两端口首通道 + 保护字节 `(CH-1) XOR K`(K 会话相关,三会话验证 2000/2000);残余开放: + 0x23-0x25/0x27-0x29 共 6B 为会话密钥派生滚动值(标准哈希/计数器/时间戳 + 全部排除),破解需先还原 AECH 密钥协商;K 与 AECH 凭证字节的对应关系未考 +- ~~形态 B 事件帧内部布局~~ → **已完全解码**(§4.2,2026-09-10):事件头 + + 标准 E1.20 消息 @0x3C + 通道图案填充(缓冲复用,无协议语义); + 事件序号 u32@0x22 严格 +1(2962/2962,旧"偶发交换/重号"为抓包顺序现象)。 + 残余细节:0x38 疑似总线/端口号(样本恒 1,多端口语境未验证);flag=01 + 内嵌副本的生成规则(srcUID 末字节 fe、port=01) +- Butler 应答帧(217B 负载)内的版本/时序参数块逐字段含义 +- "EUCE" 帧内部布局(仅确认魔数与 05 版本字节) +- ~~RDM 数据区内层字段序~~ → 已完全解码,标准 E1.20(§5.2,2026-09-09) +- cmd=19/1a(SET 类,厂商 PID 0x0200 族)的请求/应答布局(§5.6) +- RDM / show 上传 / 配置写回报文:RDM 已覆盖(§5),show 上传与配置写回未触发 + +--- + +## 11. 虚拟 Butler 实现与验证(★2026-09-09) + +### 11.1 已实现能力(Rust,enet-validator) + +- 发现应答(217B,Connected To 动态取请求方主机 IP/MAC,网段自适应) +- 注册应答(sub=02,47B) +- **AECH 认领本地计算(2026-09-10 起)**:虚拟设备收 AECH → 取 nonce@0x88-0x8B(LE) → + 本地算 `G(nonce)`(§3.7,无密钥)写入 AERE 0xC0-0xC3 → 直接应答,**无需真机中继/桥**。 + (旧中继路径保留:经源 113 socket 转发真机 .1 算 AERE;非 123 网段曾需 Windows 桥。) +- RDM 生成式应答:全新 UID 灯具的 DISC 脉冲(§5.4 编码器)+ pid_map 模板参数应答 +- 高速主循环:socket 读超时 2ms(原 80/100ms 时吞吐仅 ~5/s,无法支撑发现收敛) +- DMX 接收:形态 A 帧按 §4.1 布局;0x22/0x26 = 每端口随机 r,通道数据经 + zPprv0TXRRW 混淆 → **已实现实时解密**(AECH 认领时取 B12=质询 0x8C-0x8F, + `agios_dmx_decrypt` 还原真实通道值后打日志;旧日志里的 "CH1-16" 递减序列 + 实为 blackout 全零场景的密文形态) + +### 11.2 灯具注册根因与修复(P0) + +**症状**:虚拟 Butler 被认领、DMX 流入、主机能发现全新 UID,但 RDM 拓扑里 +.7 的总线始终 "no devices";主机只对 .7 反复发 PID=0002(947 次)从不读 DEVICE_INFO。 + +**根因(cold2.pcap 实证)**: +1. 虚拟 Butler 把 PID=0002(DISC_UN_MUTE)错答成 DISC 脉冲; + 真机应答为 70B 参数 ACK(§5.5)。主机拿不到合法 UN_MUTE ACK → 死循环重试。 +2. 所有模板应答 patch UID 后未重算 E1.20 checksum(§5.2)→ 主机按规范丢弃。 + +**修复**:0002 走 pid_map 70B 模板 + TN 回显(查询 0x4A → 应答 0x39)+ +全部 RDM 应答 patch 后重算 checksum。 + +**验证结果(fix1.pcap + GUI)**:主机对 .7 的查询变为 `0002×772 + 0060×771` +(3 个新 UID 各 257 次 DEVICE_INFO,0002:0060 = 1:1 与真机模式一致), +.7 应答 checksum 777/777 有效;SymphoLight RDM 拓扑显示 +**Butler S2(.7) → Port 1 (U1) — 3 devices**,灯具注册成功。 + +### 11.3 稳态对齐修复(P1,2026-09-10 全部落地) + +**根因(ss123/ss58 双网段对比实证)**:cmd=24 是标准 DISC_UNIQUE_BRANCH +(分支范围 lo@0x53-0x58 / hi@0x59-0x5E),真机灯具只答 UID 在范围内的查询 +(二分收敛);虚拟 Butler 无视范围全答 → 主机二分永不排空 → 发现洪水 +51/s(真机仅 16/s)→ 稳态轮询对 .7 永不启动。 + +**已修复项**: +1. cmd24 分支范围过滤,仅范围内灯具各答一个脉冲(延时入队); +2. RDM 应答延迟队列:脉冲 2-40ms / 参数 15-40ms(LCG),对齐真机 RS485 + 延迟指纹(中位 29ms); +3. 心跳 sub=03 改静默(真机从不应答);删除 84B 突发遥测; +4. 参数应答按镜像灯具取帧池:FIXTURE_MIRROR 130→125 / 131→80900 / + 132→26c9,cmd=19/1a 再按 PD[0](传感器序号)匹配; +5. 42B 改为周期遥测(§4.3):event_ctr 累计事件B帧,~115ms 上报一次, + 不再即时 ACK。 + +**验证(fix2/fix3.pcap)**:cmd24 率 51/s → **8.5/s**(真机 15.7/s,洪水消除); +主机对 .7 启动稳态轮询:0002/0060 每灯具每周期 + **0201 SENSOR_VALUE +23/17/4 次,与镜像灯具传感器数完全一致**;脉冲 4.9/s(真机 6.2/s); +42B 遥测 6.5/s 计数严格递增(真机 8.7/s);RDM 应答 checksum 全有效。 + +**残余开放项(不影响注册与轮询)**:42B 精确节流规则未明(真机差值 +1~+10 +不平滑);mute 状态机语义未明(真机灯具在广播 0003 后反而恢复应答,未实现 +mute 亦无碍);cmd=19/1a 中 SET 类厂商 PID(0x0200 族)写路径未专项。 + +### 11.4 运维要点 + +- 模板全部 `include_str!` 编译期内嵌,改 samples/*.hex 必须重新编译部署 +- 部署:分块 base64 + Invoke-SSHCommand(Set-SFTPItem 不可用);.7 用 `~/run_butler.sh` 启动 +- 进程窗口:用 86400s(24h)避免反复到期退出 +- ~~桥若退出,.7 认领即卡死~~ → 2026-09-10 起本地计算 G(§3.7),桥/中继不再是必需 +- .200(Windows 次 IP)的 Butler 必须与 .7 错开 MAC,避免冲突 +- 离线分析:`enet-validator dmxdec ` — DMX 帧 zPprv0TXRRW 解密 + 加解密回环验证 + (需 TSV 含 AECH 质询以取 B12);`dmxroll ` — 滚动值/保护字节历史分析; + tshark 导出 TSV 经 PowerShell 重定向会落为 UTF-16LE(带 BOM) + +### 11.5 DMX 帧解码里程碑(2026-09-10,§4.1)★混淆算法完全破解 + +- 60B 帧头逐字段反编译对账(`vpFbA8r8ibe5ncT1Lum7` 结构体,Pack=2): + 帧序号 u32@0x1A、cbY0u×2@0x20、r1/r2@0x22/0x26、端口ID@0x2C/0x2E、 + 端口索引@0x30/0x32、通道数 512 LE@0x34/0x36; +- **通道数据非明文**——zPprv0TXRRW 逐帧混淆:rot=(B12^r)&511,r=Random.Next() 每帧每端口, + B12=AECH 实例随机;cold2+fix1 共 5674 帧全部解密为稳定 blackout 场景, + 加解密回环 5674/5674 逐字节一致; +- 旧结论 "(CH-1)^K 保护字节/6B 滚动值/会话密钥 K" 全部证伪——K=~B12&0xFF 为 + 计数器结构的涌现巧合(0.5% 失配=rot%256==0 边界);滚动值=Random.Next() 本身; +- 主机侧复现要素齐备:B12 主机自选、r 随机、加解密双向实现(`dmxdec` 验证); +- 帧率 29.8/s 恒定,A:B 比例随模式变化(推翻"A·B·B 固定节奏 ~40Hz")。 + +### 11.6 主机侧等效播放实测(2026-09-10,档 4 首个里程碑)★ + +`dmxsend` 子命令实现**完整主机侧流程**(不依赖 SymphoLight): +1. sub=01 宣告 → 收 89B 注册应答 ✓ +2. AECH 认领(nonce/B12 均自选随机)→ .7 本地 G 应答 `AERE[0xC0]==G(nonce)` ✓ +3. 30fps 发送 zPprv0TXRRW 加密 DMX(583 帧,高识别度图案 ch1=AA/ch2=55/ch3-10=1..128/ + ch101=EF/ch513=77/ch1024=33)→ **.7 实时解密日志精确还原** `CH1-16: aa550102040810204080…` ✓ + +全链路(认领→注册→加密→解密)闭环自证。对真机 .1 仿冒的唯一额外约束: +AECH 质询须源自 192.168.123.113(真机固件过滤,§3.7)。 + +### 11.7 hostdemo:真机 .1 主机侧仿真(2026-09-10 晚)★进行中 + +`hostdemo [目标IP] [秒]` 子命令(src/hostdemo.rs):完整主机等效流程——发现 → +冷启动前奏(discovery/announce 广播 ×10s + EUCE)→ 102B 身份帧 → AECH×3 → +DISC_UN_MUTE 广播 → UNIQUE_BRANCH 二分遍历(含 DISC 脉冲解码,§5.4 逆运算) +→ 逐 UID DISC_MUTE → GET DEVICE_INFO/DMX_START_ADDRESS → 30fps 加密 DMX 呼吸控制。 + +**实测进展**: +- ✅ 发现应答(217B)每次都正常——设备在任何状态下都答发现 +- ✅ **AECH 认领真机 3 次完整通过**(3 轮 G(nonce) 验证 PASS,nonce/B12 自选随机) +- ⏳ RDM 发现/控制段:尚未跑到(受下方认领门控阻断) +- cold2 时序佐证:首个 RDM 请求在认领后 54ms 发出;首个 RDM 应答与首个 42B + 遥测同步出现于认领后 1.37s(此前主机已连发 ~14 次 UNIQUE_BRANCH—— + 冷启动应答延迟大属正常,hostdemo 每分支按 4 次 ×250ms 重试) + +**★新发现:真机认领门控/锁死行为(§3.7 补遗)** +连续 3 次成功认领但**不发任何会话流量**(无 DMX/心跳)后,.1 进入锁定态: +- 对 ident102 不回 47B 注册应答、对 AECH 质询完全不答—— + **连 cold2 原始 ident/质询的逐字节重放也一并忽略**(设备端状态,非帧构造问题) +- 发现请求(type=1)仍正常应答(225B) +- SymphoLight 于锁定约 55-70 分钟后认领成功(19:2x,期间它先广播了 >90s 才发起) +- SymphoLight 关闭(20:04,无释放帧、无 AECH,DMX 流直接消失)后 15 分钟仍锁定 +- 暂定模型:**会话保持 ≈ 55-60 分钟**(自最后一次会话流量起算),期间拒答一切新认领 + +--- + +## 12. 缺口清单与攻关路线图(2026-09-10) + +### 档 1 — 核心缺口(阻塞完整复现) + +| 缺口 | 位置 | 影响 | +|---|---|---| +| ~~AECH 密钥协商/凭证算法~~ | §3.7 | **已破解(2026-09-10)**:反编译 SymphoLight Ecue.Devices.dll 得 AgiosElias.G —— 无密钥公开算法,本地计算应答,10/10 样本 + 在线 5/5 验证。**同日 SymphoLight 重启无中继终极实测通过**(14:29:35,3 轮本地 G 应答 + 89B 注册 + DMX 恢复 + RDM 灯具重新注册,Windows 桥已退役)| +| ~~会话 K 派生关系~~ | §4.1 | **证伪并消解(2026-09-10)**:K 不是密钥——是 zPprv0TXRRW 计数器结构的涌现巧合,K=~B12&0xFF(cold2/fix1 精确吻合)| +| ~~DMX 帧 6B 滚动值/通道加密~~(0x22-0x29 + 0x3C/0x23C 数据区) | §4.1 | **已破解(2026-09-10)**:r=Random.Next() 每帧每端口;数据 zPprv0TXRRW 混淆,rot=(B12^r)&511;5674 帧解密+回环 100% | +| ~~形态 B 事件帧内部布局~~ | §4.2 | **已完全解码(2026-09-10)**:事件头 + 标准 E1.20 @0x3C + 通道图案填充(无协议语义);残余细节见 §10 | + +### 档 2 — 写路径/配置类(从未触发过捕获) + +- ~~RDM SET 写路径(DMX_START_ADDRESS)~~ → **已解码(§5.6,2026-09-10)**: + CC=0x30 请求 / CC=0x31 ACK PDL=0 应答 / SET 后 +33ms GET 验证; + 残余:厂商 PID 0x0200 族 SET、IDENTIFY_DEVICE 等其他 SET PID +- **show 上传 / SD 卡 / standalone 配置写回**:协议存在性已知,报文从未捕获 +- **节点序号(0x2E)分配机制**:仅从 2 个样本推断(.1=2/.7=3),注册顺序还是工程配置未验证 + +### 档 3 — 辅助帧细节 + +- **EUCE 帧**(§3.5):仅确认魔数 + 版本字节,内部布局空白 +- **217B 发现应答**(§3.2):版本/时序参数块逐字段含义不全 +- **1947 许可报文**(§6):静态 token 已记录,Butler 是否/如何校验未知 +- 行为指纹级小问题(不影响功能):42B 遥测精确节流规则(差值 +1~+10)、形态 B 序号偶发交换/重号、RDM mute 状态机语义 + +### 档 4 — 主机侧仿真(协议要素齐备,首个里程碑已实测 ★§11.6) + +文档主要覆盖**设备侧**(Butler 视角收/答)。主机侧全部协议要素已解码且 +`dmxsend` 已实测闭环(§11.6:认领→注册→加密 DMX→对端精确解密); +剩余工作为产品化(工程解析、场景调度、RDM 主轮询、ESP32 移植)。 +§8 的 ESP32 章节也只写了"消费 DMX"的被动模式。 + +### 建议攻关顺序 + +1. ~~形态 B 事件帧布局~~ —— **已完成(2026-09-10,§4.2)**; +2. ~~AECH 密钥协商~~ —— **已完成(2026-09-10,§3.7:反编译 AgiosElias,无密钥 G 算法)**; +3. ~~RDM SET 写路径~~ —— **已完成(2026-09-10,§5.6)**; +4. ~~K 派生 + 6B 滚动值 + DMX 加密~~ —— **已完成(2026-09-10,§4.1)**:反编译 + ScheduleUpdate 构建链,zPprv0TXRRW 混淆完全破解(rot=(B12^r)&511), + "K" 证实为 ~B12&0xFF 涌现巧合;5674 帧解密+回环 100%,加解密双向实现; +5. 顺带修正:0x3D "cmd 字节"实为 E1.20 msgLen(§5.3 更正)。 + +**至此档 1 全部关闭。** 剩余开放项均为非阻塞细节(档 2/3):show 上传/SD 写回 +(未捕获)、节点序号分配机制、EUCE 内部布局、42B 节流细则、mute 状态机。 diff --git a/docs/rust_verification_plan.md b/docs/rust_verification_plan.md new file mode 100644 index 0000000..4a5197e --- /dev/null +++ b/docs/rust_verification_plan.md @@ -0,0 +1,85 @@ +# e:net 协议 Rust 独立验证程序 —— 实施计划 + +> 目标:关闭 SymphoLight 5.5,用 Rust 程序独立扮演 e:cue 主机,按《enet_protocol.md》 +> 对每一条协议做**实测验证**(连接真机 Butler S2 192.168.123.1,以其真实应答为判据)。 + +--- + +## 0. 前置条件 + +| 项 | 动作 | 状态 | +|----|------|------| +| P1 | 关闭 SymphoLight 5.5(PID 检查 + Stop-Process) | 待执行 | +| P2 | 确认 Butler 在线(ping 192.168.123.1) | 已通 | +| P3 | 网口"以太网 3" = 192.168.123.113/24(Rust 程序绑定此地址) | 已确认 | +| P4 | 抓包样本齐备(discovery/dmx40s/rdm3 classic pcap)用于报文参数提取 | 已有 | +| P5 | 注意:SymphoLight 关闭后 Butler 回落 standalone 播 SD show(DMX 输出仍活动),Rust 程序需完成"发现→认领"接管 | 已知 | + +--- + +## 1. 技术选型与工程结构 + +``` +enet-validator/ +├── Cargo.toml # clap(CLI) anyhow hex rand;(无 async,std::net::UdpSocket 足够) +├── src/ +│ ├── main.rs # 子命令入口:discover / dmux / session / rdm / all +│ ├── enet.rs # ENet 帧构造/解析(§2 通用头:magic+type+port+len) +│ ├── discovery.rs # §3:type=01 发现请求/应答解析、宣告帧、type=02 心跳、EUCE 帧 +│ ├── dmx.rs # §4.1:形态A DMX 帧(60B头+1024B通道,序号0x1A、时间戳0x22) +│ ├── event.rs # §4.2-4.4:形态B 事件帧、ACK 配对、状态帧解析 +│ ├── rdm.rs # §5:RDM 事件帧封装(0x4A cc01)、应答解析(0x28长度+cc01)、UID/TN +│ └── report.rs # 验证报告输出(JSON + 控制台 PASS/FAIL) +└── samples/ # 从 pcap 提取的基准报文(hex 常量,参数化模板) +``` + +报文构造策略:**以实测样本为模板**(非凭空拼字段)——从 pcap 提取 222B 发现请求、 +686B 宣告、1126B DMX 帧、1126B 事件帧的原始字节,替换可变字段(序号/时间戳/通道值)。 + +--- + +## 2. 验证项分解(对应协议文档章节,判据=Butler 实际行为) + +| ID | 协议条目(文档章节) | 程序动作 | PASS 判据 | +|----|---------------------|----------|-----------| +| T1 | 发现请求 222B 广播(§3.1→§3.2) | 发 type=01 请求 → 监听 31772 | 收到 259B 应答;含 ASCII "Butler S2"、MAC `00:16:1c:f1:2e:39`、负载长字段 0xD9 ✓ | +| T2 | 主机宣告 686B(§3.3) | 发 type=01 宣告(主机名GUID/时间/密钥材料按模板) | 不破坏会话;后续 Butler 状态流正常 | +| T3 | type=02 心跳 78B(§3.4) | 发主机 IP+MAC 标识帧 | 同上,无异常 | +| T4 | EUCE 帧 582B(§3.5) | 发 EUCE 魔数帧(版本05@0x0C) | 同上(观察性验证) | +| T5 | DMX 数据帧(§4.1) | 按 A·B·B 节奏发形态A:序号 0x1A 递增、0x22 时间戳变化、0x3C 起 1024B 通道值(先全0→渐变图案) | Butler 持续 ACK/状态应答(不回落 standalone);7 段显示离开 standalone | +| T6 | 事件帧 + ACK 配对(§4.2/4.3) | 每帧 DMX 配 2 事件帧,记录事件序号 0x22 与 ACK 序号 0x20 | ACK 配对率 ≥95%;序号规律与文档一致 | +| T7 | 状态帧解析(§4.4) | 被动接收 171-247B 状态帧 | 长度字段 0x14 自洽率 100%;"Butl"前缀 100% | +| T8 | 会话保持(§6) | 持续会话 ≥60s | 全程无中断;通道值变化(全0↔全255↔渐变)期间 ACK 持续 | +| T9 | 回落行为(§6 注) | 停发 10s 后观察 | Butler 回落 standalone(状态帧模式变化)→ 再恢复会话成功 | +| T10 | RDM 应答解析(§5.1-5.2) | 主动监听(若 Butler 已 Enable RDM)或发送 RDM 请求事件帧(0x4A cc01 模板) | 收到 cc01 起始帧;0x28 长度自洽;提取灯具 UID | +| T11 | 写地址探索(§5.4) | 在 RDM 请求模板中尝试标准 PID 0x0330 SET 与私有 PID 0x2100 族 | 观察 Butler 是否下发 RDM 应答(记录 PID 行为,产出逆向数据) | + +判据统一由程序自动记录(报文 hex + 时间戳 + PASS/FAIL),输出 `validation_report.json`。 + +--- + +## 3. 执行阶段 + +- **阶段0 准备**(P1-P5):关 SymphoLight、(可选)抓 10s standalone 对照流量存档。 +- **阶段1 骨架**:Rust 工程 + pcap 样本提取器(PowerShell 导出基准 hex 常量)。 +- **阶段2 单向验证**:T1→T2/T3/T4(每项独立子命令,互不依赖)。 +- **阶段3 会话验证**:T5→T6→T7→T8(核心:接管 Butler,验证 DMX/ACK/状态全链路)。 +- **阶段4 行为验证**:T9 回落与恢复。 +- **阶段5 RDM 探索**:T10/T11(可选,依赖 Butler RDM 端口已启用状态)。 +- **阶段6 报告**:validation_report.json + 更新 enet_protocol.md(修正/补充实测发现)。 + +## 4. 风险与对策 + +| 风险 | 对策 | +|------|------| +| Butler 在 standalone 播 show 干扰观察 | T5 认领成功即接管 DMX;必要时临时拔 SD 卡(用户决定) | +| DUT(ESP32-P4) 仍在总线上 | 验证前确认 DUT 状态:保持暂停(download mode)或已恢复;勿冲突 | +| 报文字段拼错导致 Butler 无响应 | 全部以 pcap 样本为模板参数化;T1 失败时 diff 实发与样本 | +| 端口 31772 被占(SymphoLight 残留) | P1 确认进程退出;程序绑定失败即报错 | +| RDM 未启用时 T10/T11 无效 | 作为可选阶段,跳过不影响主结论 | + +## 5. 交付物 + +1. Rust 工程(可执行 `enet-validator.exe`,子命令式) +2. `validation_report.json` + 控制台报告 +3. `enet_protocol.md` 增补(以 Rust 实测修正/确认各字段) diff --git a/scripts/Verify-EnetProtocol.ps1 b/scripts/Verify-EnetProtocol.ps1 new file mode 100644 index 0000000..9e0197f --- /dev/null +++ b/scripts/Verify-EnetProtocol.ps1 @@ -0,0 +1,1902 @@ +# Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2) +param( + [string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap", + [string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap" +) +$ErrorActionPreference = 'Stop' +$script:pass = 0; $script:fail = 0 +function Check($name, $cond, $detail) { + if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) } + else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) } +} +function Read-Pcap($path) { + $b = [IO.File]::ReadAllBytes($path) + function U32([byte[]]$x, [int]$o, [bool]$sw) { + if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] } + return [BitConverter]::ToUInt32($x, $o) + } + $magic = U32 $b 0 $false + $swap = ($magic -eq 0xa1b2c3d4) + $pkts = New-Object System.Collections.Generic.List[object] + $o = 24 + while ($o + 16 -le $b.Length) { + $incl = U32 $b ($o+8) $swap + if ($o + 16 + $incl -gt $b.Length) { break } + $data = New-Object byte[] $incl + [Array]::Copy($b, $o+16, $data, 0, $incl) + $pkts.Add([pscustomobject]@{ Len = $incl; Data = $data }) + $o += 16 + $incl + } + return $pkts +} +function UdpPayload($pkt) { + $d = $pkt.Data + if ($d.Length -lt 42) { return $null } + $ethType = ([uint32]$d[12] -shl 8) -bor $d[13] + $off = 14 + if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 } + if ($ethType -ne 0x0800) { return $null } + $ihl = ($d[$off] -band 0x0F) * 4 + if ($d[$off + 9] -ne 17) { return $null } + $ipStart = $off; $udp = $off + $ihl + $src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15] + $dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19] + $sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1] + $dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3] + $pl = New-Object byte[] ($d.Length - $udp - 8) + [Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length) + return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl } +} +function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) } +function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) } +function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' } + +Write-Host "=== 载入 $Pcap ===" +$pkts = Read-Pcap $Pcap +Write-Host ("总包数: {0}" -f $pkts.Count) +$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } } + +# ---- A1: 31772 帧魔数只允许 ENet / EUCE ---- +$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 }) +$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 } +$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr" + +# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ---- +$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) +$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count) + +# ---- A3: ENet 帧类型仅 1/2/4 ---- +$types = $enet | Group-Object { LE32 $_.Payload 4 } +$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr" + +# ---- A4: 形态A DMX 帧 ---- +$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 }) +Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count) +Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024" +$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) }) +$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } } +Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count) +$tsSame = 0 +for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } } +Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0" + +# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ---- +$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' }) +$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' }) +Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count) +$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) }) +$hbMono = $true; $swaps = 0 +for ($i=1; $i -lt $hostSeq.Count; $i++) { + if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序) + elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败 +} +Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps) +$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) }) +$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count +Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched) +$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' }) +Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count) + +# ---- A6: 1947 密钥报文 ---- +$k = @($msgs | Where-Object { $_.Dport -eq 1947 }) +if ($k.Count -gt 0) { + $h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload) + $allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0 + Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0) + Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌" +} else { Check "A6 1947负载恒定" $false "| 无样本" } + +# ---- A7: A·B·B 节奏 ---- +$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21) +$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join '' +Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat" + +# ---- C1: type=02 主机标识心跳 ---- +$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 }) +$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' }) +Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count) + +# ---- C2: EUCE 帧 ---- +$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' }) +$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 }) +Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count) + +# ---- 发现阶段 ---- +Write-Host "`n=== 载入 $PcapDiscovery ===" +$pkts2 = Read-Pcap $PcapDiscovery +$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } } +$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 }) +$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 }) +Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count) +$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 }) +$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' }) +Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count) +$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致" +$hasHost = $false +foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } } +Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在" + + +# ---- R1-R3: RDM 帧 (rdm3.pcap) ---- +Write-Host "`n=== 载入 RDM 抓包 ===" +$rdmPcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\rdm3.pcap" +if (Test-Path $rdmPcap) { + $pkts3 = Read-Pcap $rdmPcap + $msgs3 = @(); foreach ($p in $pkts3) { $u = UdpPayload $p; if ($u) { $msgs3 += $u } } + $rdm = @($msgs3 | Where-Object { # Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2) +param( + [string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap", + [string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap" +) +$ErrorActionPreference = 'Stop' +$script:pass = 0; $script:fail = 0 +function Check($name, $cond, $detail) { + if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) } + else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) } +} +function Read-Pcap($path) { + $b = [IO.File]::ReadAllBytes($path) + function U32([byte[]]$x, [int]$o, [bool]$sw) { + if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] } + return [BitConverter]::ToUInt32($x, $o) + } + $magic = U32 $b 0 $false + $swap = ($magic -eq 0xa1b2c3d4) + $pkts = New-Object System.Collections.Generic.List[object] + $o = 24 + while ($o + 16 -le $b.Length) { + $incl = U32 $b ($o+8) $swap + if ($o + 16 + $incl -gt $b.Length) { break } + $data = New-Object byte[] $incl + [Array]::Copy($b, $o+16, $data, 0, $incl) + $pkts.Add([pscustomobject]@{ Len = $incl; Data = $data }) + $o += 16 + $incl + } + return $pkts +} +function UdpPayload($pkt) { + $d = $pkt.Data + if ($d.Length -lt 42) { return $null } + $ethType = ([uint32]$d[12] -shl 8) -bor $d[13] + $off = 14 + if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 } + if ($ethType -ne 0x0800) { return $null } + $ihl = ($d[$off] -band 0x0F) * 4 + if ($d[$off + 9] -ne 17) { return $null } + $ipStart = $off; $udp = $off + $ihl + $src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15] + $dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19] + $sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1] + $dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3] + $pl = New-Object byte[] ($d.Length - $udp - 8) + [Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length) + return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl } +} +function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) } +function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) } +function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' } + +Write-Host "=== 载入 $Pcap ===" +$pkts = Read-Pcap $Pcap +Write-Host ("总包数: {0}" -f $pkts.Count) +$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } } + +# ---- A1: 31772 帧魔数只允许 ENet / EUCE ---- +$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 }) +$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 } +$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr" + +# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ---- +$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) +$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count) + +# ---- A3: ENet 帧类型仅 1/2/4 ---- +$types = $enet | Group-Object { LE32 $_.Payload 4 } +$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr" + +# ---- A4: 形态A DMX 帧 ---- +$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 }) +Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count) +Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024" +$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) }) +$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } } +Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count) +$tsSame = 0 +for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } } +Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0" + +# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ---- +$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' }) +$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' }) +Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count) +$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) }) +$hbMono = $true; $swaps = 0 +for ($i=1; $i -lt $hostSeq.Count; $i++) { + if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序) + elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败 +} +Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps) +$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) }) +$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count +Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched) +$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' }) +Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count) + +# ---- A6: 1947 密钥报文 ---- +$k = @($msgs | Where-Object { $_.Dport -eq 1947 }) +if ($k.Count -gt 0) { + $h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload) + $allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0 + Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0) + Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌" +} else { Check "A6 1947负载恒定" $false "| 无样本" } + +# ---- A7: A·B·B 节奏 ---- +$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21) +$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join '' +Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat" + +# ---- C1: type=02 主机标识心跳 ---- +$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 }) +$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' }) +Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count) + +# ---- C2: EUCE 帧 ---- +$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' }) +$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 }) +Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count) + +# ---- 发现阶段 ---- +Write-Host "`n=== 载入 $PcapDiscovery ===" +$pkts2 = Read-Pcap $PcapDiscovery +$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } } +$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 }) +$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 }) +Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count) +$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 }) +$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' }) +Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count) +$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致" +$hasHost = $false +foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } } +Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在" + +Write-Host "`n========================================" +Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail) +if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" } +.Src -eq '192.168.123.1' -and (LE32 # Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2) +param( + [string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap", + [string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap" +) +$ErrorActionPreference = 'Stop' +$script:pass = 0; $script:fail = 0 +function Check($name, $cond, $detail) { + if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) } + else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) } +} +function Read-Pcap($path) { + $b = [IO.File]::ReadAllBytes($path) + function U32([byte[]]$x, [int]$o, [bool]$sw) { + if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] } + return [BitConverter]::ToUInt32($x, $o) + } + $magic = U32 $b 0 $false + $swap = ($magic -eq 0xa1b2c3d4) + $pkts = New-Object System.Collections.Generic.List[object] + $o = 24 + while ($o + 16 -le $b.Length) { + $incl = U32 $b ($o+8) $swap + if ($o + 16 + $incl -gt $b.Length) { break } + $data = New-Object byte[] $incl + [Array]::Copy($b, $o+16, $data, 0, $incl) + $pkts.Add([pscustomobject]@{ Len = $incl; Data = $data }) + $o += 16 + $incl + } + return $pkts +} +function UdpPayload($pkt) { + $d = $pkt.Data + if ($d.Length -lt 42) { return $null } + $ethType = ([uint32]$d[12] -shl 8) -bor $d[13] + $off = 14 + if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 } + if ($ethType -ne 0x0800) { return $null } + $ihl = ($d[$off] -band 0x0F) * 4 + if ($d[$off + 9] -ne 17) { return $null } + $ipStart = $off; $udp = $off + $ihl + $src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15] + $dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19] + $sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1] + $dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3] + $pl = New-Object byte[] ($d.Length - $udp - 8) + [Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length) + return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl } +} +function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) } +function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) } +function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' } + +Write-Host "=== 载入 $Pcap ===" +$pkts = Read-Pcap $Pcap +Write-Host ("总包数: {0}" -f $pkts.Count) +$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } } + +# ---- A1: 31772 帧魔数只允许 ENet / EUCE ---- +$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 }) +$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 } +$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr" + +# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ---- +$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) +$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count) + +# ---- A3: ENet 帧类型仅 1/2/4 ---- +$types = $enet | Group-Object { LE32 $_.Payload 4 } +$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr" + +# ---- A4: 形态A DMX 帧 ---- +$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 }) +Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count) +Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024" +$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) }) +$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } } +Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count) +$tsSame = 0 +for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } } +Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0" + +# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ---- +$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' }) +$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' }) +Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count) +$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) }) +$hbMono = $true; $swaps = 0 +for ($i=1; $i -lt $hostSeq.Count; $i++) { + if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序) + elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败 +} +Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps) +$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) }) +$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count +Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched) +$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' }) +Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count) + +# ---- A6: 1947 密钥报文 ---- +$k = @($msgs | Where-Object { $_.Dport -eq 1947 }) +if ($k.Count -gt 0) { + $h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload) + $allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0 + Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0) + Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌" +} else { Check "A6 1947负载恒定" $false "| 无样本" } + +# ---- A7: A·B·B 节奏 ---- +$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21) +$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join '' +Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat" + +# ---- C1: type=02 主机标识心跳 ---- +$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 }) +$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' }) +Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count) + +# ---- C2: EUCE 帧 ---- +$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' }) +$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 }) +Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count) + +# ---- 发现阶段 ---- +Write-Host "`n=== 载入 $PcapDiscovery ===" +$pkts2 = Read-Pcap $PcapDiscovery +$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } } +$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 }) +$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 }) +Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count) +$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 }) +$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' }) +Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count) +$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致" +$hasHost = $false +foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } } +Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在" + +Write-Host "`n========================================" +Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail) +if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" } +.Payload 4) -eq 4 -and (Hex # Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2) +param( + [string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap", + [string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap" +) +$ErrorActionPreference = 'Stop' +$script:pass = 0; $script:fail = 0 +function Check($name, $cond, $detail) { + if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) } + else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) } +} +function Read-Pcap($path) { + $b = [IO.File]::ReadAllBytes($path) + function U32([byte[]]$x, [int]$o, [bool]$sw) { + if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] } + return [BitConverter]::ToUInt32($x, $o) + } + $magic = U32 $b 0 $false + $swap = ($magic -eq 0xa1b2c3d4) + $pkts = New-Object System.Collections.Generic.List[object] + $o = 24 + while ($o + 16 -le $b.Length) { + $incl = U32 $b ($o+8) $swap + if ($o + 16 + $incl -gt $b.Length) { break } + $data = New-Object byte[] $incl + [Array]::Copy($b, $o+16, $data, 0, $incl) + $pkts.Add([pscustomobject]@{ Len = $incl; Data = $data }) + $o += 16 + $incl + } + return $pkts +} +function UdpPayload($pkt) { + $d = $pkt.Data + if ($d.Length -lt 42) { return $null } + $ethType = ([uint32]$d[12] -shl 8) -bor $d[13] + $off = 14 + if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 } + if ($ethType -ne 0x0800) { return $null } + $ihl = ($d[$off] -band 0x0F) * 4 + if ($d[$off + 9] -ne 17) { return $null } + $ipStart = $off; $udp = $off + $ihl + $src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15] + $dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19] + $sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1] + $dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3] + $pl = New-Object byte[] ($d.Length - $udp - 8) + [Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length) + return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl } +} +function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) } +function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) } +function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' } + +Write-Host "=== 载入 $Pcap ===" +$pkts = Read-Pcap $Pcap +Write-Host ("总包数: {0}" -f $pkts.Count) +$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } } + +# ---- A1: 31772 帧魔数只允许 ENet / EUCE ---- +$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 }) +$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 } +$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr" + +# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ---- +$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) +$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count) + +# ---- A3: ENet 帧类型仅 1/2/4 ---- +$types = $enet | Group-Object { LE32 $_.Payload 4 } +$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr" + +# ---- A4: 形态A DMX 帧 ---- +$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 }) +Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count) +Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024" +$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) }) +$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } } +Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count) +$tsSame = 0 +for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } } +Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0" + +# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ---- +$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' }) +$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' }) +Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count) +$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) }) +$hbMono = $true; $swaps = 0 +for ($i=1; $i -lt $hostSeq.Count; $i++) { + if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序) + elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败 +} +Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps) +$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) }) +$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count +Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched) +$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' }) +Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count) + +# ---- A6: 1947 密钥报文 ---- +$k = @($msgs | Where-Object { $_.Dport -eq 1947 }) +if ($k.Count -gt 0) { + $h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload) + $allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0 + Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0) + Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌" +} else { Check "A6 1947负载恒定" $false "| 无样本" } + +# ---- A7: A·B·B 节奏 ---- +$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21) +$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join '' +Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat" + +# ---- C1: type=02 主机标识心跳 ---- +$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 }) +$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' }) +Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count) + +# ---- C2: EUCE 帧 ---- +$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' }) +$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 }) +Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count) + +# ---- 发现阶段 ---- +Write-Host "`n=== 载入 $PcapDiscovery ===" +$pkts2 = Read-Pcap $PcapDiscovery +$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } } +$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 }) +$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 }) +Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count) +$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 }) +$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' }) +Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count) +$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致" +$hasHost = $false +foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } } +Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在" + +Write-Host "`n========================================" +Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail) +if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" } +.Payload 0x1A 4) -eq '4275746c' -and (Hex # Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2) +param( + [string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap", + [string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap" +) +$ErrorActionPreference = 'Stop' +$script:pass = 0; $script:fail = 0 +function Check($name, $cond, $detail) { + if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) } + else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) } +} +function Read-Pcap($path) { + $b = [IO.File]::ReadAllBytes($path) + function U32([byte[]]$x, [int]$o, [bool]$sw) { + if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] } + return [BitConverter]::ToUInt32($x, $o) + } + $magic = U32 $b 0 $false + $swap = ($magic -eq 0xa1b2c3d4) + $pkts = New-Object System.Collections.Generic.List[object] + $o = 24 + while ($o + 16 -le $b.Length) { + $incl = U32 $b ($o+8) $swap + if ($o + 16 + $incl -gt $b.Length) { break } + $data = New-Object byte[] $incl + [Array]::Copy($b, $o+16, $data, 0, $incl) + $pkts.Add([pscustomobject]@{ Len = $incl; Data = $data }) + $o += 16 + $incl + } + return $pkts +} +function UdpPayload($pkt) { + $d = $pkt.Data + if ($d.Length -lt 42) { return $null } + $ethType = ([uint32]$d[12] -shl 8) -bor $d[13] + $off = 14 + if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 } + if ($ethType -ne 0x0800) { return $null } + $ihl = ($d[$off] -band 0x0F) * 4 + if ($d[$off + 9] -ne 17) { return $null } + $ipStart = $off; $udp = $off + $ihl + $src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15] + $dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19] + $sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1] + $dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3] + $pl = New-Object byte[] ($d.Length - $udp - 8) + [Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length) + return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl } +} +function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) } +function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) } +function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' } + +Write-Host "=== 载入 $Pcap ===" +$pkts = Read-Pcap $Pcap +Write-Host ("总包数: {0}" -f $pkts.Count) +$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } } + +# ---- A1: 31772 帧魔数只允许 ENet / EUCE ---- +$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 }) +$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 } +$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr" + +# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ---- +$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) +$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count) + +# ---- A3: ENet 帧类型仅 1/2/4 ---- +$types = $enet | Group-Object { LE32 $_.Payload 4 } +$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr" + +# ---- A4: 形态A DMX 帧 ---- +$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 }) +Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count) +Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024" +$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) }) +$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } } +Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count) +$tsSame = 0 +for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } } +Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0" + +# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ---- +$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' }) +$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' }) +Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count) +$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) }) +$hbMono = $true; $swaps = 0 +for ($i=1; $i -lt $hostSeq.Count; $i++) { + if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序) + elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败 +} +Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps) +$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) }) +$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count +Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched) +$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' }) +Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count) + +# ---- A6: 1947 密钥报文 ---- +$k = @($msgs | Where-Object { $_.Dport -eq 1947 }) +if ($k.Count -gt 0) { + $h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload) + $allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0 + Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0) + Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌" +} else { Check "A6 1947负载恒定" $false "| 无样本" } + +# ---- A7: A·B·B 节奏 ---- +$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21) +$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join '' +Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat" + +# ---- C1: type=02 主机标识心跳 ---- +$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 }) +$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' }) +Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count) + +# ---- C2: EUCE 帧 ---- +$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' }) +$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 }) +Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count) + +# ---- 发现阶段 ---- +Write-Host "`n=== 载入 $PcapDiscovery ===" +$pkts2 = Read-Pcap $PcapDiscovery +$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } } +$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 }) +$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 }) +Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count) +$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 }) +$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' }) +Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count) +$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致" +$hasHost = $false +foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } } +Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在" + +Write-Host "`n========================================" +Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail) +if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" } +.Payload 0x2A 2) -eq 'cc01' }) + Check "R1 RDM帧=Butler→主机+cc01" ($rdm.Count -gt 0) ("| n={0} 长度分布={1}" -f $rdm.Count, (($rdm | Group-Object { # Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2) +param( + [string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap", + [string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap" +) +$ErrorActionPreference = 'Stop' +$script:pass = 0; $script:fail = 0 +function Check($name, $cond, $detail) { + if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) } + else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) } +} +function Read-Pcap($path) { + $b = [IO.File]::ReadAllBytes($path) + function U32([byte[]]$x, [int]$o, [bool]$sw) { + if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] } + return [BitConverter]::ToUInt32($x, $o) + } + $magic = U32 $b 0 $false + $swap = ($magic -eq 0xa1b2c3d4) + $pkts = New-Object System.Collections.Generic.List[object] + $o = 24 + while ($o + 16 -le $b.Length) { + $incl = U32 $b ($o+8) $swap + if ($o + 16 + $incl -gt $b.Length) { break } + $data = New-Object byte[] $incl + [Array]::Copy($b, $o+16, $data, 0, $incl) + $pkts.Add([pscustomobject]@{ Len = $incl; Data = $data }) + $o += 16 + $incl + } + return $pkts +} +function UdpPayload($pkt) { + $d = $pkt.Data + if ($d.Length -lt 42) { return $null } + $ethType = ([uint32]$d[12] -shl 8) -bor $d[13] + $off = 14 + if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 } + if ($ethType -ne 0x0800) { return $null } + $ihl = ($d[$off] -band 0x0F) * 4 + if ($d[$off + 9] -ne 17) { return $null } + $ipStart = $off; $udp = $off + $ihl + $src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15] + $dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19] + $sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1] + $dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3] + $pl = New-Object byte[] ($d.Length - $udp - 8) + [Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length) + return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl } +} +function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) } +function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) } +function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' } + +Write-Host "=== 载入 $Pcap ===" +$pkts = Read-Pcap $Pcap +Write-Host ("总包数: {0}" -f $pkts.Count) +$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } } + +# ---- A1: 31772 帧魔数只允许 ENet / EUCE ---- +$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 }) +$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 } +$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr" + +# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ---- +$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) +$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count) + +# ---- A3: ENet 帧类型仅 1/2/4 ---- +$types = $enet | Group-Object { LE32 $_.Payload 4 } +$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr" + +# ---- A4: 形态A DMX 帧 ---- +$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 }) +Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count) +Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024" +$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) }) +$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } } +Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count) +$tsSame = 0 +for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } } +Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0" + +# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ---- +$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' }) +$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' }) +Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count) +$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) }) +$hbMono = $true; $swaps = 0 +for ($i=1; $i -lt $hostSeq.Count; $i++) { + if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序) + elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败 +} +Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps) +$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) }) +$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count +Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched) +$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' }) +Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count) + +# ---- A6: 1947 密钥报文 ---- +$k = @($msgs | Where-Object { $_.Dport -eq 1947 }) +if ($k.Count -gt 0) { + $h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload) + $allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0 + Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0) + Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌" +} else { Check "A6 1947负载恒定" $false "| 无样本" } + +# ---- A7: A·B·B 节奏 ---- +$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21) +$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join '' +Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat" + +# ---- C1: type=02 主机标识心跳 ---- +$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 }) +$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' }) +Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count) + +# ---- C2: EUCE 帧 ---- +$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' }) +$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 }) +Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count) + +# ---- 发现阶段 ---- +Write-Host "`n=== 载入 $PcapDiscovery ===" +$pkts2 = Read-Pcap $PcapDiscovery +$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } } +$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 }) +$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 }) +Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count) +$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 }) +$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' }) +Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count) +$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致" +$hasHost = $false +foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } } +Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在" + +Write-Host "`n========================================" +Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail) +if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" } +.Payload.Length } | ForEach-Object { "$(# Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2) +param( + [string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap", + [string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap" +) +$ErrorActionPreference = 'Stop' +$script:pass = 0; $script:fail = 0 +function Check($name, $cond, $detail) { + if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) } + else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) } +} +function Read-Pcap($path) { + $b = [IO.File]::ReadAllBytes($path) + function U32([byte[]]$x, [int]$o, [bool]$sw) { + if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] } + return [BitConverter]::ToUInt32($x, $o) + } + $magic = U32 $b 0 $false + $swap = ($magic -eq 0xa1b2c3d4) + $pkts = New-Object System.Collections.Generic.List[object] + $o = 24 + while ($o + 16 -le $b.Length) { + $incl = U32 $b ($o+8) $swap + if ($o + 16 + $incl -gt $b.Length) { break } + $data = New-Object byte[] $incl + [Array]::Copy($b, $o+16, $data, 0, $incl) + $pkts.Add([pscustomobject]@{ Len = $incl; Data = $data }) + $o += 16 + $incl + } + return $pkts +} +function UdpPayload($pkt) { + $d = $pkt.Data + if ($d.Length -lt 42) { return $null } + $ethType = ([uint32]$d[12] -shl 8) -bor $d[13] + $off = 14 + if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 } + if ($ethType -ne 0x0800) { return $null } + $ihl = ($d[$off] -band 0x0F) * 4 + if ($d[$off + 9] -ne 17) { return $null } + $ipStart = $off; $udp = $off + $ihl + $src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15] + $dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19] + $sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1] + $dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3] + $pl = New-Object byte[] ($d.Length - $udp - 8) + [Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length) + return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl } +} +function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) } +function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) } +function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' } + +Write-Host "=== 载入 $Pcap ===" +$pkts = Read-Pcap $Pcap +Write-Host ("总包数: {0}" -f $pkts.Count) +$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } } + +# ---- A1: 31772 帧魔数只允许 ENet / EUCE ---- +$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 }) +$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 } +$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr" + +# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ---- +$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) +$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count) + +# ---- A3: ENet 帧类型仅 1/2/4 ---- +$types = $enet | Group-Object { LE32 $_.Payload 4 } +$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr" + +# ---- A4: 形态A DMX 帧 ---- +$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 }) +Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count) +Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024" +$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) }) +$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } } +Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count) +$tsSame = 0 +for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } } +Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0" + +# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ---- +$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' }) +$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' }) +Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count) +$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) }) +$hbMono = $true; $swaps = 0 +for ($i=1; $i -lt $hostSeq.Count; $i++) { + if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序) + elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败 +} +Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps) +$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) }) +$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count +Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched) +$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' }) +Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count) + +# ---- A6: 1947 密钥报文 ---- +$k = @($msgs | Where-Object { $_.Dport -eq 1947 }) +if ($k.Count -gt 0) { + $h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload) + $allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0 + Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0) + Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌" +} else { Check "A6 1947负载恒定" $false "| 无样本" } + +# ---- A7: A·B·B 节奏 ---- +$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21) +$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join '' +Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat" + +# ---- C1: type=02 主机标识心跳 ---- +$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 }) +$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' }) +Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count) + +# ---- C2: EUCE 帧 ---- +$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' }) +$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 }) +Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count) + +# ---- 发现阶段 ---- +Write-Host "`n=== 载入 $PcapDiscovery ===" +$pkts2 = Read-Pcap $PcapDiscovery +$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } } +$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 }) +$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 }) +Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count) +$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 }) +$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' }) +Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count) +$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致" +$hasHost = $false +foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } } +Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在" + +Write-Host "`n========================================" +Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail) +if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" } +.Name)x$(# Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2) +param( + [string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap", + [string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap" +) +$ErrorActionPreference = 'Stop' +$script:pass = 0; $script:fail = 0 +function Check($name, $cond, $detail) { + if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) } + else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) } +} +function Read-Pcap($path) { + $b = [IO.File]::ReadAllBytes($path) + function U32([byte[]]$x, [int]$o, [bool]$sw) { + if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] } + return [BitConverter]::ToUInt32($x, $o) + } + $magic = U32 $b 0 $false + $swap = ($magic -eq 0xa1b2c3d4) + $pkts = New-Object System.Collections.Generic.List[object] + $o = 24 + while ($o + 16 -le $b.Length) { + $incl = U32 $b ($o+8) $swap + if ($o + 16 + $incl -gt $b.Length) { break } + $data = New-Object byte[] $incl + [Array]::Copy($b, $o+16, $data, 0, $incl) + $pkts.Add([pscustomobject]@{ Len = $incl; Data = $data }) + $o += 16 + $incl + } + return $pkts +} +function UdpPayload($pkt) { + $d = $pkt.Data + if ($d.Length -lt 42) { return $null } + $ethType = ([uint32]$d[12] -shl 8) -bor $d[13] + $off = 14 + if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 } + if ($ethType -ne 0x0800) { return $null } + $ihl = ($d[$off] -band 0x0F) * 4 + if ($d[$off + 9] -ne 17) { return $null } + $ipStart = $off; $udp = $off + $ihl + $src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15] + $dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19] + $sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1] + $dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3] + $pl = New-Object byte[] ($d.Length - $udp - 8) + [Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length) + return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl } +} +function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) } +function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) } +function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' } + +Write-Host "=== 载入 $Pcap ===" +$pkts = Read-Pcap $Pcap +Write-Host ("总包数: {0}" -f $pkts.Count) +$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } } + +# ---- A1: 31772 帧魔数只允许 ENet / EUCE ---- +$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 }) +$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 } +$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr" + +# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ---- +$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) +$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count) + +# ---- A3: ENet 帧类型仅 1/2/4 ---- +$types = $enet | Group-Object { LE32 $_.Payload 4 } +$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr" + +# ---- A4: 形态A DMX 帧 ---- +$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 }) +Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count) +Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024" +$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) }) +$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } } +Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count) +$tsSame = 0 +for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } } +Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0" + +# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ---- +$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' }) +$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' }) +Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count) +$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) }) +$hbMono = $true; $swaps = 0 +for ($i=1; $i -lt $hostSeq.Count; $i++) { + if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序) + elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败 +} +Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps) +$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) }) +$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count +Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched) +$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' }) +Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count) + +# ---- A6: 1947 密钥报文 ---- +$k = @($msgs | Where-Object { $_.Dport -eq 1947 }) +if ($k.Count -gt 0) { + $h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload) + $allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0 + Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0) + Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌" +} else { Check "A6 1947负载恒定" $false "| 无样本" } + +# ---- A7: A·B·B 节奏 ---- +$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21) +$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join '' +Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat" + +# ---- C1: type=02 主机标识心跳 ---- +$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 }) +$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' }) +Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count) + +# ---- C2: EUCE 帧 ---- +$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' }) +$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 }) +Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count) + +# ---- 发现阶段 ---- +Write-Host "`n=== 载入 $PcapDiscovery ===" +$pkts2 = Read-Pcap $PcapDiscovery +$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } } +$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 }) +$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 }) +Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count) +$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 }) +$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' }) +Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count) +$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致" +$hasHost = $false +foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } } +Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在" + +Write-Host "`n========================================" +Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail) +if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" } +.Count)" }) -join ',')) + $badRdmLen = @($rdm | Where-Object { [int](LE16 # Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2) +param( + [string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap", + [string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap" +) +$ErrorActionPreference = 'Stop' +$script:pass = 0; $script:fail = 0 +function Check($name, $cond, $detail) { + if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) } + else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) } +} +function Read-Pcap($path) { + $b = [IO.File]::ReadAllBytes($path) + function U32([byte[]]$x, [int]$o, [bool]$sw) { + if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] } + return [BitConverter]::ToUInt32($x, $o) + } + $magic = U32 $b 0 $false + $swap = ($magic -eq 0xa1b2c3d4) + $pkts = New-Object System.Collections.Generic.List[object] + $o = 24 + while ($o + 16 -le $b.Length) { + $incl = U32 $b ($o+8) $swap + if ($o + 16 + $incl -gt $b.Length) { break } + $data = New-Object byte[] $incl + [Array]::Copy($b, $o+16, $data, 0, $incl) + $pkts.Add([pscustomobject]@{ Len = $incl; Data = $data }) + $o += 16 + $incl + } + return $pkts +} +function UdpPayload($pkt) { + $d = $pkt.Data + if ($d.Length -lt 42) { return $null } + $ethType = ([uint32]$d[12] -shl 8) -bor $d[13] + $off = 14 + if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 } + if ($ethType -ne 0x0800) { return $null } + $ihl = ($d[$off] -band 0x0F) * 4 + if ($d[$off + 9] -ne 17) { return $null } + $ipStart = $off; $udp = $off + $ihl + $src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15] + $dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19] + $sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1] + $dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3] + $pl = New-Object byte[] ($d.Length - $udp - 8) + [Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length) + return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl } +} +function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) } +function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) } +function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' } + +Write-Host "=== 载入 $Pcap ===" +$pkts = Read-Pcap $Pcap +Write-Host ("总包数: {0}" -f $pkts.Count) +$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } } + +# ---- A1: 31772 帧魔数只允许 ENet / EUCE ---- +$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 }) +$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 } +$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr" + +# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ---- +$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) +$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count) + +# ---- A3: ENet 帧类型仅 1/2/4 ---- +$types = $enet | Group-Object { LE32 $_.Payload 4 } +$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr" + +# ---- A4: 形态A DMX 帧 ---- +$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 }) +Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count) +Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024" +$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) }) +$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } } +Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count) +$tsSame = 0 +for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } } +Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0" + +# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ---- +$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' }) +$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' }) +Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count) +$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) }) +$hbMono = $true; $swaps = 0 +for ($i=1; $i -lt $hostSeq.Count; $i++) { + if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序) + elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败 +} +Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps) +$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) }) +$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count +Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched) +$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' }) +Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count) + +# ---- A6: 1947 密钥报文 ---- +$k = @($msgs | Where-Object { $_.Dport -eq 1947 }) +if ($k.Count -gt 0) { + $h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload) + $allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0 + Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0) + Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌" +} else { Check "A6 1947负载恒定" $false "| 无样本" } + +# ---- A7: A·B·B 节奏 ---- +$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21) +$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join '' +Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat" + +# ---- C1: type=02 主机标识心跳 ---- +$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 }) +$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' }) +Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count) + +# ---- C2: EUCE 帧 ---- +$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' }) +$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 }) +Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count) + +# ---- 发现阶段 ---- +Write-Host "`n=== 载入 $PcapDiscovery ===" +$pkts2 = Read-Pcap $PcapDiscovery +$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } } +$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 }) +$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 }) +Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count) +$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 }) +$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' }) +Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count) +$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致" +$hasHost = $false +foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } } +Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在" + +Write-Host "`n========================================" +Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail) +if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" } +.Payload 0x28) -ne (# Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2) +param( + [string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap", + [string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap" +) +$ErrorActionPreference = 'Stop' +$script:pass = 0; $script:fail = 0 +function Check($name, $cond, $detail) { + if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) } + else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) } +} +function Read-Pcap($path) { + $b = [IO.File]::ReadAllBytes($path) + function U32([byte[]]$x, [int]$o, [bool]$sw) { + if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] } + return [BitConverter]::ToUInt32($x, $o) + } + $magic = U32 $b 0 $false + $swap = ($magic -eq 0xa1b2c3d4) + $pkts = New-Object System.Collections.Generic.List[object] + $o = 24 + while ($o + 16 -le $b.Length) { + $incl = U32 $b ($o+8) $swap + if ($o + 16 + $incl -gt $b.Length) { break } + $data = New-Object byte[] $incl + [Array]::Copy($b, $o+16, $data, 0, $incl) + $pkts.Add([pscustomobject]@{ Len = $incl; Data = $data }) + $o += 16 + $incl + } + return $pkts +} +function UdpPayload($pkt) { + $d = $pkt.Data + if ($d.Length -lt 42) { return $null } + $ethType = ([uint32]$d[12] -shl 8) -bor $d[13] + $off = 14 + if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 } + if ($ethType -ne 0x0800) { return $null } + $ihl = ($d[$off] -band 0x0F) * 4 + if ($d[$off + 9] -ne 17) { return $null } + $ipStart = $off; $udp = $off + $ihl + $src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15] + $dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19] + $sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1] + $dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3] + $pl = New-Object byte[] ($d.Length - $udp - 8) + [Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length) + return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl } +} +function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) } +function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) } +function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' } + +Write-Host "=== 载入 $Pcap ===" +$pkts = Read-Pcap $Pcap +Write-Host ("总包数: {0}" -f $pkts.Count) +$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } } + +# ---- A1: 31772 帧魔数只允许 ENet / EUCE ---- +$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 }) +$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 } +$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr" + +# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ---- +$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) +$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count) + +# ---- A3: ENet 帧类型仅 1/2/4 ---- +$types = $enet | Group-Object { LE32 $_.Payload 4 } +$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr" + +# ---- A4: 形态A DMX 帧 ---- +$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 }) +Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count) +Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024" +$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) }) +$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } } +Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count) +$tsSame = 0 +for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } } +Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0" + +# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ---- +$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' }) +$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' }) +Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count) +$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) }) +$hbMono = $true; $swaps = 0 +for ($i=1; $i -lt $hostSeq.Count; $i++) { + if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序) + elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败 +} +Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps) +$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) }) +$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count +Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched) +$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' }) +Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count) + +# ---- A6: 1947 密钥报文 ---- +$k = @($msgs | Where-Object { $_.Dport -eq 1947 }) +if ($k.Count -gt 0) { + $h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload) + $allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0 + Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0) + Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌" +} else { Check "A6 1947负载恒定" $false "| 无样本" } + +# ---- A7: A·B·B 节奏 ---- +$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21) +$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join '' +Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat" + +# ---- C1: type=02 主机标识心跳 ---- +$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 }) +$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' }) +Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count) + +# ---- C2: EUCE 帧 ---- +$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' }) +$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 }) +Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count) + +# ---- 发现阶段 ---- +Write-Host "`n=== 载入 $PcapDiscovery ===" +$pkts2 = Read-Pcap $PcapDiscovery +$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } } +$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 }) +$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 }) +Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count) +$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 }) +$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' }) +Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count) +$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致" +$hasHost = $false +foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } } +Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在" + +Write-Host "`n========================================" +Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail) +if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" } +.Payload.Length - 0x2A) }) + Check "R2 RDM长度字段0x28" ($badRdmLen.Count -eq 0) ("| {0}/{1} 一致" -f ($rdm.Count - $badRdmLen.Count), $rdm.Count) + $uids = @($rdm | ForEach-Object { (Hex # Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2) +param( + [string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap", + [string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap" +) +$ErrorActionPreference = 'Stop' +$script:pass = 0; $script:fail = 0 +function Check($name, $cond, $detail) { + if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) } + else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) } +} +function Read-Pcap($path) { + $b = [IO.File]::ReadAllBytes($path) + function U32([byte[]]$x, [int]$o, [bool]$sw) { + if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] } + return [BitConverter]::ToUInt32($x, $o) + } + $magic = U32 $b 0 $false + $swap = ($magic -eq 0xa1b2c3d4) + $pkts = New-Object System.Collections.Generic.List[object] + $o = 24 + while ($o + 16 -le $b.Length) { + $incl = U32 $b ($o+8) $swap + if ($o + 16 + $incl -gt $b.Length) { break } + $data = New-Object byte[] $incl + [Array]::Copy($b, $o+16, $data, 0, $incl) + $pkts.Add([pscustomobject]@{ Len = $incl; Data = $data }) + $o += 16 + $incl + } + return $pkts +} +function UdpPayload($pkt) { + $d = $pkt.Data + if ($d.Length -lt 42) { return $null } + $ethType = ([uint32]$d[12] -shl 8) -bor $d[13] + $off = 14 + if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 } + if ($ethType -ne 0x0800) { return $null } + $ihl = ($d[$off] -band 0x0F) * 4 + if ($d[$off + 9] -ne 17) { return $null } + $ipStart = $off; $udp = $off + $ihl + $src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15] + $dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19] + $sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1] + $dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3] + $pl = New-Object byte[] ($d.Length - $udp - 8) + [Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length) + return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl } +} +function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) } +function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) } +function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' } + +Write-Host "=== 载入 $Pcap ===" +$pkts = Read-Pcap $Pcap +Write-Host ("总包数: {0}" -f $pkts.Count) +$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } } + +# ---- A1: 31772 帧魔数只允许 ENet / EUCE ---- +$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 }) +$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 } +$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr" + +# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ---- +$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) +$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count) + +# ---- A3: ENet 帧类型仅 1/2/4 ---- +$types = $enet | Group-Object { LE32 $_.Payload 4 } +$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr" + +# ---- A4: 形态A DMX 帧 ---- +$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 }) +Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count) +Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024" +$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) }) +$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } } +Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count) +$tsSame = 0 +for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } } +Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0" + +# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ---- +$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' }) +$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' }) +Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count) +$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) }) +$hbMono = $true; $swaps = 0 +for ($i=1; $i -lt $hostSeq.Count; $i++) { + if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序) + elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败 +} +Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps) +$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) }) +$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count +Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched) +$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' }) +Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count) + +# ---- A6: 1947 密钥报文 ---- +$k = @($msgs | Where-Object { $_.Dport -eq 1947 }) +if ($k.Count -gt 0) { + $h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload) + $allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0 + Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0) + Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌" +} else { Check "A6 1947负载恒定" $false "| 无样本" } + +# ---- A7: A·B·B 节奏 ---- +$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21) +$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join '' +Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat" + +# ---- C1: type=02 主机标识心跳 ---- +$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 }) +$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' }) +Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count) + +# ---- C2: EUCE 帧 ---- +$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' }) +$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 }) +Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count) + +# ---- 发现阶段 ---- +Write-Host "`n=== 载入 $PcapDiscovery ===" +$pkts2 = Read-Pcap $PcapDiscovery +$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } } +$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 }) +$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 }) +Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count) +$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 }) +$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' }) +Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count) +$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致" +$hasHost = $false +foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } } +Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在" + +Write-Host "`n========================================" +Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail) +if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" } +.Payload 0x32 6) } | Sort-Object -Unique) + Check "R3 灯具UID(Src@0x32)" ($uids.Count -ge 1) ("| UID: {0}" -f ($uids -join ' ')) + $disc = @($msgs3 | Where-Object { # Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2) +param( + [string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap", + [string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap" +) +$ErrorActionPreference = 'Stop' +$script:pass = 0; $script:fail = 0 +function Check($name, $cond, $detail) { + if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) } + else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) } +} +function Read-Pcap($path) { + $b = [IO.File]::ReadAllBytes($path) + function U32([byte[]]$x, [int]$o, [bool]$sw) { + if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] } + return [BitConverter]::ToUInt32($x, $o) + } + $magic = U32 $b 0 $false + $swap = ($magic -eq 0xa1b2c3d4) + $pkts = New-Object System.Collections.Generic.List[object] + $o = 24 + while ($o + 16 -le $b.Length) { + $incl = U32 $b ($o+8) $swap + if ($o + 16 + $incl -gt $b.Length) { break } + $data = New-Object byte[] $incl + [Array]::Copy($b, $o+16, $data, 0, $incl) + $pkts.Add([pscustomobject]@{ Len = $incl; Data = $data }) + $o += 16 + $incl + } + return $pkts +} +function UdpPayload($pkt) { + $d = $pkt.Data + if ($d.Length -lt 42) { return $null } + $ethType = ([uint32]$d[12] -shl 8) -bor $d[13] + $off = 14 + if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 } + if ($ethType -ne 0x0800) { return $null } + $ihl = ($d[$off] -band 0x0F) * 4 + if ($d[$off + 9] -ne 17) { return $null } + $ipStart = $off; $udp = $off + $ihl + $src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15] + $dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19] + $sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1] + $dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3] + $pl = New-Object byte[] ($d.Length - $udp - 8) + [Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length) + return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl } +} +function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) } +function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) } +function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' } + +Write-Host "=== 载入 $Pcap ===" +$pkts = Read-Pcap $Pcap +Write-Host ("总包数: {0}" -f $pkts.Count) +$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } } + +# ---- A1: 31772 帧魔数只允许 ENet / EUCE ---- +$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 }) +$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 } +$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr" + +# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ---- +$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) +$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count) + +# ---- A3: ENet 帧类型仅 1/2/4 ---- +$types = $enet | Group-Object { LE32 $_.Payload 4 } +$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr" + +# ---- A4: 形态A DMX 帧 ---- +$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 }) +Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count) +Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024" +$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) }) +$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } } +Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count) +$tsSame = 0 +for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } } +Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0" + +# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ---- +$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' }) +$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' }) +Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count) +$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) }) +$hbMono = $true; $swaps = 0 +for ($i=1; $i -lt $hostSeq.Count; $i++) { + if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序) + elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败 +} +Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps) +$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) }) +$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count +Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched) +$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' }) +Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count) + +# ---- A6: 1947 密钥报文 ---- +$k = @($msgs | Where-Object { $_.Dport -eq 1947 }) +if ($k.Count -gt 0) { + $h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload) + $allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0 + Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0) + Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌" +} else { Check "A6 1947负载恒定" $false "| 无样本" } + +# ---- A7: A·B·B 节奏 ---- +$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21) +$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join '' +Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat" + +# ---- C1: type=02 主机标识心跳 ---- +$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 }) +$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' }) +Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count) + +# ---- C2: EUCE 帧 ---- +$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' }) +$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 }) +Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count) + +# ---- 发现阶段 ---- +Write-Host "`n=== 载入 $PcapDiscovery ===" +$pkts2 = Read-Pcap $PcapDiscovery +$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } } +$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 }) +$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 }) +Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count) +$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 }) +$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' }) +Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count) +$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致" +$hasHost = $false +foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } } +Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在" + +Write-Host "`n========================================" +Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail) +if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" } +.Src -eq '192.168.123.1' -and (Hex # Verify-EnetProtocol.ps1 — 按 enet_protocol.md 的断言逐条验证 pcap (v2) +param( + [string]$Pcap = "C:\Users\tonycao\AppData\Local\Temp\opencode\dmx40s_classic.pcap", + [string]$PcapDiscovery = "C:\Users\tonycao\AppData\Local\Temp\opencode\discovery_classic.pcap" +) +$ErrorActionPreference = 'Stop' +$script:pass = 0; $script:fail = 0 +function Check($name, $cond, $detail) { + if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) } + else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) } +} +function Read-Pcap($path) { + $b = [IO.File]::ReadAllBytes($path) + function U32([byte[]]$x, [int]$o, [bool]$sw) { + if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] } + return [BitConverter]::ToUInt32($x, $o) + } + $magic = U32 $b 0 $false + $swap = ($magic -eq 0xa1b2c3d4) + $pkts = New-Object System.Collections.Generic.List[object] + $o = 24 + while ($o + 16 -le $b.Length) { + $incl = U32 $b ($o+8) $swap + if ($o + 16 + $incl -gt $b.Length) { break } + $data = New-Object byte[] $incl + [Array]::Copy($b, $o+16, $data, 0, $incl) + $pkts.Add([pscustomobject]@{ Len = $incl; Data = $data }) + $o += 16 + $incl + } + return $pkts +} +function UdpPayload($pkt) { + $d = $pkt.Data + if ($d.Length -lt 42) { return $null } + $ethType = ([uint32]$d[12] -shl 8) -bor $d[13] + $off = 14 + if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 } + if ($ethType -ne 0x0800) { return $null } + $ihl = ($d[$off] -band 0x0F) * 4 + if ($d[$off + 9] -ne 17) { return $null } + $ipStart = $off; $udp = $off + $ihl + $src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15] + $dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19] + $sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1] + $dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3] + $pl = New-Object byte[] ($d.Length - $udp - 8) + [Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length) + return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl } +} +function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) } +function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) } +function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' } + +Write-Host "=== 载入 $Pcap ===" +$pkts = Read-Pcap $Pcap +Write-Host ("总包数: {0}" -f $pkts.Count) +$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } } + +# ---- A1: 31772 帧魔数只允许 ENet / EUCE ---- +$m31772 = @($msgs | Where-Object { $_.Sport -eq 31772 -or $_.Dport -eq 31772 }) +$magic = $m31772 | Group-Object { Hex $_.Payload 0 4 } +$magicStr = ($magic | ForEach-Object { "{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A1 魔数仅ENet/EUCE" (($magic | Where-Object { $_.Name -notin @('454e6574','45554345') }).Count -eq 0) "| $magicStr" + +# ---- A2: ENet 帧 0x14 长度字段 == 负载长 ---- +$enet = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) +$badLen = @($enet | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "A2 ENet长度字段" ($badLen.Count -eq 0) ("| {0}/{1} 帧一致" -f ($enet.Count - $badLen.Count), $enet.Count) + +# ---- A3: ENet 帧类型仅 1/2/4 ---- +$types = $enet | Group-Object { LE32 $_.Payload 4 } +$typeStr = ($types | ForEach-Object { "type{0}=x{1}" -f $_.Name, $_.Count }) -join ' ' +Check "A3 类型仅01/02/04" (($types | Where-Object { $_.Name -notin @('1','2','4') }).Count -eq 0) "| $typeStr" + +# ---- A4: 形态A DMX 帧 ---- +$a = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 0x0101 }) +Check "A4a 负载1084B" (($a | Where-Object { $_.Payload.Length -ne 1084 }).Count -eq 0) ("| 形态A帧={0}" -f $a.Count) +Check "A4b 数据区1024B" ((1084 - 0x3C) -eq 1024) "| 1084-60=1024" +$seqs = @($a | ForEach-Object { [int](LE16 $_.Payload 0x1A) }) +$mono = $true; for ($i=1; $i -lt $seqs.Count; $i++) { if ($seqs[$i] -ne (($seqs[$i-1]+1) -band 0xFFFF)) { $mono = $false } } +Check "A4c DMX序号(0x1A,2B)严格递增" $mono ("| 0x{0:X4}→0x{1:X4} n={2}" -f $seqs[0], $seqs[-1], $seqs.Count) +$tsSame = 0 +for ($i=1; $i -lt $a.Count; $i++) { if ((Hex $a[$i].Payload 0x22 8) -eq (Hex $a[$i-1].Payload 0x22 8)) { $tsSame++ } } +Check "A4d 时间戳逐帧变化" ($tsSame -eq 0) "| 重复=0" + +# ---- A5: 形态B 事件帧(主机: 子类型0200在0x20, 序号0x22) + Butler ACK/状态(0x18=0200, 'Butl'0x1A, 序号0x20) ---- +$hostB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x20) -eq 2 -and $_.Src -eq '192.168.123.113' }) +$devB = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 4 -and (LE16 $_.Payload 0x18) -eq 2 -and $_.Src -eq '192.168.123.1' }) +Check "A5a 方向分布" ($hostB.Count -gt 0 -and $devB.Count -gt 0) ("| 主机事件={0} 设备ACK/状态={1}" -f $hostB.Count, $devB.Count) +$hostSeq = @($hostB | Where-Object { $_.Payload.Length -ge 0x26 } | ForEach-Object { [int](LE32 $_.Payload 0x22) }) +$hbMono = $true; $swaps = 0 +for ($i=1; $i -lt $hostSeq.Count; $i++) { + if ($hostSeq[$i] -eq ($hostSeq[$i-1] - 1)) { $swaps++ } # 相邻交换(乱序) + elseif ($hostSeq[$i] -lt $hostSeq[$i-1]) { $hbMono = $false } # 其它回退=失败 +} +Check "A5b 事件序号递增(允许相邻交换)" $hbMono ("| 0x{0:X8}→0x{1:X8} 交换={2}" -f $hostSeq[0], $hostSeq[-1], $swaps) +$ackSeq = @($devB | Where-Object { $_.Payload.Length -ge 0x24 } | ForEach-Object { [int](LE32 $_.Payload 0x20) }) +$matched = @($ackSeq | Where-Object { $hostSeq -contains $_ }).Count +Check "A5c ACK序号∈事件序号集" ($matched -gt 0) ("| ACK/状态={0} 命中={1}" -f $ackSeq.Count, $matched) +$butl = @($devB | Where-Object { $_.Payload.Length -ge 0x1E -and (Hex $_.Payload 0x1A 4) -eq '4275746c' }) +Check "A5d 设备帧含'Butl'" ($butl.Count -eq $devB.Count) ("| {0}/{1}" -f $butl.Count, $devB.Count) + +# ---- A6: 1947 密钥报文 ---- +$k = @($msgs | Where-Object { $_.Dport -eq 1947 }) +if ($k.Count -gt 0) { + $h0 = [Text.Encoding]::ASCII.GetString($k[0].Payload) + $allSame = @($k | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -ne $h0 }).Count -eq 0 + Check "A6 1947负载恒定" $allSame ("| n={0} Base64={1}" -f $k.Count, $h0) + Check "A6b Base64解码30B" ([Convert]::FromBase64String($h0).Length -eq 30) "| 静态令牌" +} else { Check "A6 1947负载恒定" $false "| 无样本" } + +# ---- A7: A·B·B 节奏 ---- +$sess = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and (LE32 $_.Payload 4) -eq 4 -and $_.Src -eq '192.168.123.113' } | Select-Object -First 21) +$pat = ($sess | ForEach-Object { if ((LE16 $_.Payload 0x20) -eq 0x0101) { 'A' } else { 'B' } }) -join '' +Check "A7 ABB节奏开头" $pat.StartsWith('ABB') "| 前21帧: $pat" + +# ---- C1: type=02 主机标识心跳 ---- +$hb = @($enet | Where-Object { (LE32 $_.Payload 4) -eq 2 }) +$hbOk = @($hb | Where-Object { $_.Payload.Length -eq 36 -and (Hex $_.Payload 0x1A 4) -eq '717ba8c0' -and (Hex $_.Payload 0x1E 6) -eq '300ed559bb36' }) +Check "C1 type02心跳=36B含主机IP/MAC" ($hbOk.Count -eq $hb.Count -and $hb.Count -gt 0) ("| n={0} 命中={1}" -f $hb.Count, $hbOk.Count) + +# ---- C2: EUCE 帧 ---- +$e = @($m31772 | Where-Object { (Hex $_.Payload 0 4) -eq '45554345' }) +$eOk = @($e | Where-Object { (LE32 $_.Payload 0x0C) -eq 5 }) +Check "C2 EUCE帧版本05@0x0C" ($eOk.Count -eq $e.Count -and $e.Count -gt 0) ("| n={0} 命中={1}" -f $e.Count, $eOk.Count) + +# ---- 发现阶段 ---- +Write-Host "`n=== 载入 $PcapDiscovery ===" +$pkts2 = Read-Pcap $PcapDiscovery +$msgs2 = @(); foreach ($p in $pkts2) { $u = UdpPayload $p; if ($u) { $msgs2 += $u } } +$d1 = @($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' -and $_.Dport -eq 31772 }) +$disc = @($d1 | Where-Object { (LE32 $_.Payload 4) -eq 1 }) +Check "B1 type=01发现帧存在" ($disc.Count -gt 0) ("| type01={0}" -f $disc.Count) +$resp = @($msgs2 | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Dport -eq 31772 }) +$hasName = @($resp | Where-Object { [Text.Encoding]::ASCII.GetString($_.Payload) -match 'Butler S2' }) +Check "B2 应答含'Butler S2'" ($hasName.Count -gt 0) ("| Butler应答={0} 含名={1}" -f $resp.Count, $hasName.Count) +$badL2 = @(($msgs2 | Where-Object { (Hex $_.Payload 0 4) -eq '454e6574' }) | Where-Object { [int](LE32 $_.Payload 0x14) -ne $_.Payload.Length }) +Check "B3 发现帧长度字段" ($badL2.Count -eq 0) "| 全部一致" +$hasHost = $false +foreach ($u in ($msgs2 | Where-Object { $_.Payload.Length -gt 300 })) { if ([Text.Encoding]::Unicode.GetString($u.Payload) -match 'tonycao') { $hasHost = $true } } +Check "B4 宣告帧UTF-16主机名" $hasHost "| >300B帧存在" + +Write-Host "`n========================================" +Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail) +if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" } +.Payload 0x2A 4) -eq 'aaaa5dbb' }) + Check "R4 DISC发现响应脉冲" ($disc.Count -gt 0) ("| n={0}" -f $disc.Count) +} else { Check "R1 RDM帧" $false "| rdm3.pcap 不存在" } +Write-Host " +========================================" +Write-Host ("验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail) +if ($script:fail -eq 0) { Write-Host ">>> 协议文档全部断言验证通过 <<<" } else { Write-Host ">>> 存在未通过断言 <<<" } diff --git a/scripts/Verify-Rdm.ps1 b/scripts/Verify-Rdm.ps1 new file mode 100644 index 0000000..6dc50ee --- /dev/null +++ b/scripts/Verify-Rdm.ps1 @@ -0,0 +1,38 @@ +# Verify-Rdm.ps1 — 验证 enet_protocol.md §5 RDM 断言 +$ErrorActionPreference = 'Stop' +$script:pass = 0; $script:fail = 0 +function Check($name, $cond, $detail) { + if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) } + else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) } +} +. "C:\Users\tonycao\work\light\enet_lib.ps1" + +$pkts = Read-Pcap "C:\Users\tonycao\AppData\Local\Temp\opencode\rdm3_classic.pcap" +$msgs = @(); foreach ($p in $pkts) { $u = UdpPayload $p; if ($u) { $msgs += $u } } +Write-Host ("RDM 抓包总包数: {0}" -f $msgs.Count) + +# R1: RDM 应答帧 = Butler→主机 + "Butl" + cc01 起始 +$rdm = @($msgs | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Payload.Length -gt 0x2C -and (LE32 $_.Payload 4) -eq 4 -and (Hex $_.Payload 0x1A 4) -eq '4275746c' -and (Hex $_.Payload 0x2A 2) -eq 'cc01' }) +$dist = ($rdm | Group-Object { $_.Payload.Length } | ForEach-Object { "$($_.Name)x$($_.Count)" }) -join ',' +Check "R1 RDM帧=Butler→主机+cc01" ($rdm.Count -gt 0) ("| n={0} ({1})" -f $rdm.Count, $dist) + +# R2: 0x28 长度字段 == 实际 RDM 区长 (负载长 - 0x2A) +$bad = @($rdm | Where-Object { [int](LE16 $_.Payload 0x28) -ne ($_.Payload.Length - 0x2A) }) +Check "R2 RDM长度字段0x28" ($bad.Count -eq 0) ("| {0}/{1} 一致" -f ($rdm.Count - $bad.Count), $rdm.Count) + +# R3: 灯具 UID (Src @ 0x32, 6字节) — 至少 1 台, 收集全部 +$uids = @($rdm | ForEach-Object { (Hex $_.Payload 0x32 6) } | Sort-Object -Unique) +$uidFmt = ($uids | ForEach-Object { ($_.ToCharArray() | ForEach-Object { if ($_ -eq '*') { $_ } else { $_ } }) -join '' }) -join ':' +Check "R3 灯具UID@0x32" ($uids.Count -ge 1) ("| {0} 台: {1}" -f $uids.Count, ($uids -join ' ')) + +# R4: DISC 发现响应原始脉冲 +$disc = @($msgs | Where-Object { $_.Src -eq '192.168.123.1' -and $_.Payload.Length -gt 0x2E -and (Hex $_.Payload 0x2A 4) -eq 'aaaa5dbb' }) +Check "R4 DISC发现响应脉冲" ($disc.Count -gt 0) ("| n={0} 负载={1}" -f $disc.Count, $(if($disc.Count){ (Hex $disc[0].Payload 0x2A 17) } else { '-' })) + +# R5: 主机请求封装在 1126 事件帧 (0x4A 处 cc01) +$req = @($msgs | Where-Object { $_.Src -eq '192.168.123.113' -and $_.Payload.Length -ge 0x4C -and (Hex $_.Payload 0x4A 2) -eq 'cc01' }) +Check "R5 主机请求封装于事件帧@0x4A" ($req.Count -gt 0) ("| n={0}" -f $req.Count) + +Write-Host "`n========================================" +Write-Host ("RDM 验证结果: PASS={0} FAIL={1}" -f $script:pass, $script:fail) +if ($script:fail -eq 0) { Write-Host ">>> RDM 协议断言全部验证通过 <<<" } diff --git a/scripts/enet_lib.ps1 b/scripts/enet_lib.ps1 new file mode 100644 index 0000000..0b91407 --- /dev/null +++ b/scripts/enet_lib.ps1 @@ -0,0 +1,47 @@ +# enet_lib.ps1 — e:net 协议验证公共函数库 +$script:pass = 0; $script:fail = 0 +function Check($name, $cond, $detail) { + if ($cond) { $script:pass++; Write-Host (" [PASS] {0} {1}" -f $name, $detail) } + else { $script:fail++; Write-Host (" [FAIL] {0} {1}" -f $name, $detail) } +} +function Read-Pcap([string]$path) { + $b = [IO.File]::ReadAllBytes($path) + function U32([byte[]]$x, [int]$o, [bool]$sw) { + if ($sw) { return ([uint32]$x[$o] -shl 24) -bor ([uint32]$x[$o+1] -shl 16) -bor ([uint32]$x[$o+2] -shl 8) -bor [uint32]$x[$o+3] } + return [BitConverter]::ToUInt32($x, $o) + } + $magic = U32 $b 0 $false + $swap = ($magic -eq 0xa1b2c3d4) + $pkts = New-Object System.Collections.Generic.List[object] + $o = 24 + while ($o + 16 -le $b.Length) { + $incl = U32 $b ($o+8) $swap + if ($o + 16 + $incl -gt $b.Length) { break } + $data = New-Object byte[] $incl + [Array]::Copy($b, $o+16, $data, 0, $incl) + $pkts.Add([pscustomobject]@{ Len = $incl; Data = $data }) + $o += 16 + $incl + } + return $pkts +} +function UdpPayload($pkt) { + $d = $pkt.Data + if ($d.Length -lt 42) { return $null } + $ethType = ([uint32]$d[12] -shl 8) -bor $d[13] + $off = 14 + if ($ethType -eq 0x8100) { $ethType = ([uint32]$d[16] -shl 8) -bor $d[17]; $off = 18 } + if ($ethType -ne 0x0800) { return $null } + $ihl = ($d[$off] -band 0x0F) * 4 + if ($d[$off + 9] -ne 17) { return $null } + $ipStart = $off; $udp = $off + $ihl + $src = "{0}.{1}.{2}.{3}" -f $d[$ipStart+12], $d[$ipStart+13], $d[$ipStart+14], $d[$ipStart+15] + $dst = "{0}.{1}.{2}.{3}" -f $d[$ipStart+16], $d[$ipStart+17], $d[$ipStart+18], $d[$ipStart+19] + $sport = ([uint32]$d[$udp] -shl 8) -bor $d[$udp+1] + $dport = ([uint32]$d[$udp+2] -shl 8) -bor $d[$udp+3] + $pl = New-Object byte[] ($d.Length - $udp - 8) + [Array]::Copy($d, $udp + 8, $pl, 0, $pl.Length) + return [pscustomobject]@{ Src=$src; Dst=$dst; Sport=$sport; Dport=$dport; Payload=$pl } +} +function LE32([byte[]]$x, [int]$o) { return [uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8) -bor ([uint32]$x[$o+2] -shl 16) -bor ([uint32]$x[$o+3] -shl 24) } +function LE16([byte[]]$x, [int]$o) { return [uint32]([uint32]$x[$o] -bor ([uint32]$x[$o+1] -shl 8)) } +function Hex([byte[]]$x, [int]$o, [int]$n) { ($x[$o..($o+$n-1)] | ForEach-Object { $_.ToString('x2') }) -join '' } diff --git a/scripts/session/analyze_ack.ps1 b/scripts/session/analyze_ack.ps1 new file mode 100644 index 0000000..6c40ba1 --- /dev/null +++ b/scripts/session/analyze_ack.ps1 @@ -0,0 +1,55 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +function H2B($hex) { + if (-not $hex) { return $null } + $n = $hex.Length / 2 + $b = New-Object byte[] $n + for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring(2*$i, 2), 16) } + return $b +} +$lines = [IO.File]::ReadAllLines("$env:TEMP\opencode\ss123.tsv") +$acks = New-Object System.Collections.ArrayList +$qs = New-Object System.Collections.ArrayList +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + $b = H2B $p[4]; if ($b.Length -lt 0x2A) { continue } + $t = [double]$p[0] + if ($p[1] -eq "192.168.123.1" -and $p[2] -eq "192.168.123.113" -and $b.Length -eq 42 -and [int]$b[4] -eq 4) { + $v = [int]$b[0x20] + [int]$b[0x21]*256 + [int]$b[0x22]*65536 + [int]$b[0x23]*16777216 + [void]$acks.Add(@{ t=$t; v=$v; hex=(($b[0x1A..0x29] | ForEach-Object { $_.ToString("x2") }) -join "") }) + } + if ($p[1] -eq "192.168.123.113" -and $p[2] -eq "192.168.123.1" -and $b.Length -gt 0x52 -and $b.Length -lt 2000 -and [int]$b[4] -eq 4 -and $b[0x20] -eq 2 -and $b[0x21] -eq 0 -and $b[0x30] -eq 0 -and $b[0x3C] -eq 1) { + [void]$qs.Add(@{ t=$t; cmd=$b[0x3D] }) + } +} +Write-Host ("42B 帧数={0}" -f $acks.Count) +# 0x20-0x23 值序列: 单调性 + 间隔 +$mono = 0; $notMono = 0; $gaps = @{} +for ($i = 1; $i -lt $acks.Count; $i++) { + $d = $acks[$i].v - $acks[$i-1].v + if ($d -gt 0) { $mono++ } else { $notMono++ } + $gk = "$d" + if (-not $gaps.ContainsKey($gk)) { $gaps[$gk] = 0 }; $gaps[$gk]++ +} +Write-Host ("0x20-0x23 值: 递增={0} 非递增={1}" -f $mono, $notMono) +Write-Host "--- 相邻差值分布 ---" +$gaps.GetEnumerator() | Sort-Object { [int]$_.Key } | Select-Object -First 10 | ForEach-Object { Write-Host (" 差={0} : {1}" -f $_.Key, $_.Value) } +Write-Host "--- 前 8 个 42B 帧 (时间/值/hex) ---" +foreach ($a in ($acks | Select-Object -First 8)) { Write-Host (" t={0,8:F3} v={1} hex={2}" -f $a.t, $a.v, $a.hex) } +Write-Host "--- 后 4 个 ---" +foreach ($a in ($acks | Select-Object -Last 4)) { Write-Host (" t={0,8:F3} v={1} hex={2}" -f $a.t, $a.v, $a.hex) } +# 42B 帧与 cmd 查询的时间相关性: 每个 42B 前 50ms 内最近的查询 cmd +$byCmd = @{} +foreach ($a in $acks) { + $best = $null + for ($i = $qs.Count - 1; $i -ge 0; $i--) { if ($qs[$i].t -le $a.t) { $best = $qs[$i]; break } } + $k = if ($best -and (($a.t - $best.t) -lt 0.05)) { "cmd={0:x2}" -f $best.cmd } else { "无临近查询" } + if (-not $byCmd.ContainsKey($k)) { $byCmd[$k] = 0 }; $byCmd[$k]++ +} +Write-Host "--- 42B 帧前 50ms 内最近查询 ---" +$byCmd.GetEnumerator() | Sort-Object { [int]$_.Value } -Descending | ForEach-Object { Write-Host (" {0} : {1}" -f $_.Key, $_.Value) } +# 42B 帧间时间隔 +$tlast = $null; $tg = New-Object System.Collections.ArrayList +foreach ($a in $acks) { if ($null -ne $tlast) { [void]$tg.Add([int](($a.t - $tlast) * 1000)) }; $tlast = $a.t } +$tgs = $tg | Sort-Object +Write-Host ("--- 42B 帧时间隔ms: min={0} 中位={1} max={2}" -f $tgs[0], $tgs[[int]($tgs.Count/2)], $tgs[-1]) diff --git a/scripts/session/analyze_aech.ps1 b/scripts/session/analyze_aech.ps1 new file mode 100644 index 0000000..74c3218 --- /dev/null +++ b/scripts/session/analyze_aech.ps1 @@ -0,0 +1,48 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +# Extract AECH (sub=05) / AERE (sub=06) frames from cold2.tsv, diff across rounds +$raw = [IO.File]::ReadAllBytes("$env:TEMP\opencode\cold2.tsv") +$text = [Text.Encoding]::UTF8.GetString($raw) +if ($raw[0] -eq 0xFF -and $raw[1] -eq 0xFE) { $text = [Text.Encoding]::Unicode.GetString($raw) } +$lines = $text -split "`r?`n" + +function H2B($hex) { $h = $hex -replace " ",""; $n = $h.Length/2; $b = New-Object byte[] $n; for ($i=0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($h.Substring(2*$i,2),16) }; return $b } + +$chals = @(); $resps = @() +foreach ($l in $lines) { + $p = $l -split "`t" + if ($p.Count -lt 5 -or -not $p[4]) { continue } + $b = H2B $p[4] + if ($b.Length -lt 0x30) { continue } + if ($b[0] -ne 0x45 -or $b[1] -ne 0x4e -or $b[2] -ne 0x65 -or $b[3] -ne 0x74) { continue } # ENet + $tag = [Text.Encoding]::ASCII.GetString($b[0x28..0x2B]) + if ($tag -eq "AECH") { $chals += ,@{ t=[double]$p[0]; src=$p[1]; b=$b } } + if ($tag -eq "AERE") { $resps += ,@{ t=[double]$p[0]; src=$p[1]; b=$b } } +} +Write-Host "AECH challenges: $($chals.Count), AERE responses: $($resps.Count)" +foreach ($c in $chals) { Write-Host (" CHAL t={0,8:F3} src={1} len={2}" -f $c.t, $c.src, $c.b.Length) } +foreach ($r in $resps) { Write-Host (" RESP t={0,8:F3} src={1} len={2}" -f $r.t, $r.src, $r.b.Length) } + +# diff rounds: varying offset runs +function Diff-Frames($frames, $label) { + if ($frames.Count -lt 2) { return } + $len = $frames[0].b.Length + $vary = @() + for ($i = 0; $i -lt $len; $i++) { + $v = $frames[0].b[$i]; $diff = $false + foreach ($f in $frames) { if ($f.b[$i] -ne $v) { $diff = $true; break } } + if ($diff) { $vary += $i } + } + Write-Host "`n=== $label varying offsets ($($vary.Count) bytes) ===" + # group into runs + $runs = @(); $start = -1; $prev = -2 + foreach ($o in $vary) { if ($o -ne $prev + 1) { if ($start -ge 0) { $runs += ,"$start-$prev" }; $start = $o }; $prev = $o } + if ($start -ge 0) { $runs += ,"$start-$prev" } + foreach ($r in $runs) { + $a, $bb = $r -split "-"; $a = [int]$a; $bb = [int]$bb + $parts = @() + foreach ($f in $frames) { $parts += (($f.b[$a..$bb] | ForEach-Object { $_.ToString("x2") }) -join "") } + Write-Host (" 0x{0:x2}-0x{1:x2} ({2}B): {3}" -f $a, $bb, ($bb-$a+1), ($parts -join " | ")) + } +} +Diff-Frames $chals "CHALLENGES" +Diff-Frames $resps "RESPONSES" diff --git a/scripts/session/analyze_branch.ps1 b/scripts/session/analyze_branch.ps1 new file mode 100644 index 0000000..779967c --- /dev/null +++ b/scripts/session/analyze_branch.ps1 @@ -0,0 +1,74 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +function H2B($hex) { + if (-not $hex) { return $null } + $n = $hex.Length / 2 + $b = New-Object byte[] $n + for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring(2*$i, 2), 16) } + return $b +} +$lines = [IO.File]::ReadAllLines("$env:TEMP\opencode\ss123.tsv") + +# 收集 cmd=24 查询 (0x20==0200, flag==0) 与 其后 100ms 内的脉冲 UID +$queries = New-Object System.Collections.ArrayList +$pulses = New-Object System.Collections.ArrayList +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + $b = H2B $p[4]; if ($b.Length -lt 0x30) { continue } + $t = [double]$p[0] + if ($p[1] -eq "192.168.123.113" -and $p[2] -eq "192.168.123.1" -and [int]$b[4] -eq 4 -and $b.Length -gt 0x52 -and $b.Length -lt 2000 -and $b[0x20] -eq 2 -and $b[0x21] -eq 0 -and $b[0x30] -eq 0 -and $b[0x3C] -eq 1 -and $b[0x3D] -eq 0x24) { + [void]$queries.Add(@{ t=$t; b=$b }) + } + if ($p[1] -eq "192.168.123.1" -and $p[2] -eq "192.168.123.113" -and $b.Length -lt 200 -and $b[0x2A] -eq 0xAA) { + $uid = "" + for ($i = 0; $i -lt 6; $i++) { + $b1 = $b[0x2B + 2*$i]; $b2 = $b[0x2C + 2*$i] + $byte = ([int]$b1 -band 0x55) -bor ([int]$b2 -band 0xAA) + $uid += $byte.ToString("x2") + } + [void]$pulses.Add(@{ t=$t; uid=$uid }) + } +} +Write-Host ("cmd=24 查询={0} 脉冲={1}" -f $queries.Count, $pulses.Count) + +# cmd=24 查询布局: 打印 0x3C..0x60 区域的不同取值分布 +Write-Host "`n--- cmd=24 查询 0x3E..0x55 字段分布 (前 8 条不同样本) ---" +$seen = @{} +$shown = 0 +foreach ($q in $queries) { + $key = ($q.b[0x3E..0x55] | ForEach-Object { $_.ToString("x2") }) -join "" + if ($seen.ContainsKey($key)) { continue } + $seen[$key] = 1 + $b = $q.b + Write-Host (" t={0,7:F2} 0x3E={1} 0x44={2} 0x4A={3} 0x50={4}" -f $q.t, + (($b[0x3E..0x43] | ForEach-Object { $_.ToString("x2") }) -join ""), + (($b[0x44..0x49] | ForEach-Object { $_.ToString("x2") }) -join ""), + (($b[0x4A..0x4F] | ForEach-Object { $_.ToString("x2") }) -join ""), + (($b[0x50..0x55] | ForEach-Object { $_.ToString("x2") }) -join "")) + $shown++; if ($shown -ge 12) { break } +} + +# 关键验证: 每个查询的分支范围 [lo,hi] 与 其后 100ms 内应答脉冲 UID 的关系 +# 假设: 0x3E-0x43=下界, 0x44-0x49=上界 (大端 UID 序) +function UidNum($b, $o) { + $v = [double]0 + for ($i = 0; $i -lt 6; $i++) { $v = $v * 256 + [int]$b[$o + $i] } + return $v +} +Write-Host "`n--- 分支范围 vs 应答脉冲 (前 25 条有应答/无应答对比) ---" +$fx = @("010734670125", "010734880900", "0899500026c9") +$fxNum = @{} +foreach ($f in $fx) { $n = [double]0; for ($i = 0; $i -lt 6; $i++) { $n = $n * 256 + [Convert]::ToInt32($f.Substring(2*$i, 2), 16) }; $fxNum[$f] = $n } +$n2 = 0 +foreach ($q in $queries) { + $lo = UidNum $q.b 0x3E; $hi = UidNum $q.b 0x44 + $ans = @($pulses | Where-Object { $_.t -ge $q.t -and $_.t -lt $q.t + 0.1 }) + $ansStr = ($ans | ForEach-Object { $_.uid.Substring(8) }) -join "," + $inRange = @() + foreach ($f in $fx) { if ($fxNum[$f] -ge $lo -and $fxNum[$f] -le $hi) { $inRange += $f.Substring(8) } } + Write-Host (" t={0,7:F2} lo={1} hi={2} 范围内灯具=[{3}] 应答=[{4}]" -f $q.t, + (($q.b[0x3E..0x43] | ForEach-Object { $_.ToString("x2") }) -join "").Substring(4), + (($q.b[0x44..0x49] | ForEach-Object { $_.ToString("x2") }) -join "").Substring(4), + ($inRange -join ","), $ansStr) + $n2++; if ($n2 -ge 25) { break } +} diff --git a/scripts/session/analyze_branch2.ps1 b/scripts/session/analyze_branch2.ps1 new file mode 100644 index 0000000..8c467d5 --- /dev/null +++ b/scripts/session/analyze_branch2.ps1 @@ -0,0 +1,74 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +function H2B($hex) { + if (-not $hex) { return $null } + $n = $hex.Length / 2 + $b = New-Object byte[] $n + for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring(2*$i, 2), 16) } + return $b +} +function UidNumArr($arr) { $v = [double]0; foreach ($x in $arr) { $v = $v * 256 + [int]$x }; return $v } +$lines = [IO.File]::ReadAllLines("$env:TEMP\opencode\ss123.tsv") + +$queries = New-Object System.Collections.ArrayList +$pulses = New-Object System.Collections.ArrayList +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + $b = H2B $p[4]; if ($b.Length -lt 0x30) { continue } + $t = [double]$p[0] + if ($p[1] -eq "192.168.123.113" -and $p[2] -eq "192.168.123.1" -and [int]$b[4] -eq 4 -and $b.Length -gt 0x5E -and $b.Length -lt 2000 -and $b[0x20] -eq 2 -and $b[0x21] -eq 0 -and $b[0x30] -eq 0 -and $b[0x3C] -eq 1 -and $b[0x3D] -eq 0x24) { + [void]$queries.Add(@{ t=$t; lo=(UidNumArr $b[0x53..0x58]); hi=(UidNumArr $b[0x59..0x5E]); loS=(($b[0x53..0x58] | ForEach-Object { $_.ToString("x2") }) -join ""); hiS=(($b[0x59..0x5E] | ForEach-Object { $_.ToString("x2") }) -join "") }) + } + if ($p[1] -eq "192.168.123.1" -and $p[2] -eq "192.168.123.113" -and $b.Length -lt 200 -and $b[0x2A] -eq 0xAA) { + $uidBytes = @() + for ($i = 0; $i -lt 6; $i++) { + $b1 = $b[0x2B + 2*$i]; $b2 = $b[0x2C + 2*$i] + $uidBytes += ([int]$b1 -band 0x55) -bor ([int]$b2 -band 0xAA) + } + [void]$pulses.Add(@{ t=$t; uid=(UidNumArr $uidBytes); uidS=(($uidBytes | ForEach-Object { $_.ToString("x2") }) -join "") }) + } +} +Write-Host ("cmd=24 查询={0} 脉冲={1}" -f $queries.Count, $pulses.Count) + +# 分支范围分布 +Write-Host "`n--- 分支范围 lo-hi 分布 (Top 15) ---" +$ranges = @{} +foreach ($q in $queries) { $k = "$($q.loS)-$($q.hiS)"; if (-not $ranges.ContainsKey($k)) { $ranges[$k] = 0 }; $ranges[$k]++ } +$ranges.GetEnumerator() | Sort-Object { [int]$_.Value } -Descending | Select-Object -First 15 | ForEach-Object { Write-Host (" {0} : {1}" -f $_.Key, $_.Value) } + +# 核心验证: 范围内含灯具的查询 → 应答率; 范围外 → 应答率 +$fx = @{} +foreach ($f in @("010734670125", "010734880900", "0899500026c9")) { + $n = [double]0; for ($i = 0; $i -lt 6; $i++) { $n = $n * 256 + [Convert]::ToInt32($f.Substring(2*$i, 2), 16) } + $fx[$f] = $n +} +$inN = 0; $inAns = 0; $outN = 0; $outAns = 0; $mismatches = 0 +foreach ($q in $queries) { + $ans = @($pulses | Where-Object { $_.t -ge $q.t -and $_.t -lt $q.t + 0.1 }) + $hasFx = $false; foreach ($f in $fx.Values) { if ($f -ge $q.lo -and $f -le $q.hi) { $hasFx = $true; break } } + if ($hasFx) { + $inN++ + if ($ans.Count -gt 0) { + $inAns++ + # 应答的 UID 是否都在范围内? + foreach ($a in $ans) { if ($a.uid -lt $q.lo -or $a.uid -gt $q.hi) { $mismatches++ } } + } + } else { + $outN++ + if ($ans.Count -gt 0) { $outAns++ } + } +} +Write-Host "`n--- 分支语义验证 ---" +Write-Host (" 范围内有灯具的查询: {0}, 有应答: {1} ({2:F0}%)" -f $inN, $inAns, $(if ($inN) {100.0*$inAns/$inN} else {0})) +Write-Host (" 范围内无灯具的查询: {0}, 有应答: {1} ({2:F0}%)" -f $outN, $outAns, $(if ($outN) {100.0*$outAns/$outN} else {0})) +Write-Host (" 应答 UID 超出查询范围的次数: {0}" -f $mismatches) + +# 有应答查询的时间序列样本 +Write-Host "`n--- 样本: 分支范围 vs 应答 UID (前 15 条有应答) ---" +$shown = 0 +foreach ($q in $queries) { + $ans = @($pulses | Where-Object { $_.t -ge $q.t -and $_.t -lt $q.t + 0.1 }) + if ($ans.Count -eq 0) { continue } + Write-Host (" t={0,7:F2} [{1}..{2}] → {3}" -f $q.t, $q.loS, $q.hiS, (($ans | ForEach-Object { $_.uidS }) -join ",")) + $shown++; if ($shown -ge 15) { break } +} diff --git a/scripts/session/analyze_ck.ps1 b/scripts/session/analyze_ck.ps1 new file mode 100644 index 0000000..afa2f91 --- /dev/null +++ b/scripts/session/analyze_ck.ps1 @@ -0,0 +1,81 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +$f = "$env:TEMP\opencode\cold2.tsv" +$lines = [IO.File]::ReadAllLines($f) + +function H2B($hex) { + if (-not $hex) { return $null } + $n = $hex.Length / 2 + $b = New-Object byte[] $n + for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring(2*$i, 2), 16) } + return $b +} +function Hex($b) { return ($b | ForEach-Object { $_.ToString("x2") }) -join "" } +function UidStr($b, $o) { if ($b.Length -lt $o+6) { return "?" }; return ($b[$o..($o+5)] | ForEach-Object { $_.ToString("x2") }) -join "" } + +# 采集 70B RDM 应答 +$resp70 = @() +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + if ($p[1] -ne "192.168.123.1" -or $p[2] -ne "192.168.123.113") { continue } + $b = H2B $p[4] + if ($b.Length -eq 70 -and $b[0x2A] -eq 0xCC) { $resp70 += ,$b } +} +Write-Host ("70B RDM 应答数: {0}" -f $resp70.Count) + +# 全字段偏移扫描: sum(b[o..66])==ck16 且唯一解 +Write-Host "`n--- 唯一偏移扫描: sum(b[o..66]) == ck@68-69 (BE) ---" +foreach ($o in 0x2A..0x34) { + $matchAll = $true + foreach ($b in $resp70) { + $sum = 0; for ($i = $o; $i -le 66; $i++) { $sum += $b[$i] } + $ck = ($b[67] -shl 8) -bor $b[68] + # 注意: 上面偏移占位, 实际 ck 在最后两字节 68-69 + $ck = ($b[68] -shl 8) -bor $b[69] + if (($sum -band 0xFFFF) -ne $ck) { $matchAll = $false; break } + } + Write-Host (" start=0x{0:x2} end=66 ck@68-69 : {1}" -f $o, $(if ($matchAll) {"全部匹配"} else {"否"})) +} + +# 结合 msgLen: 若 start=0x2E, 覆盖字节数 = 66-0x2E+1 = 0x27? 试 sum(b[0x2A..0x2A+msgLen+2]) (msgLen 不含 CC01) +Write-Host "`n--- 假设: msgLen@0x2C 不含 CC01 两字节, 求和范围 = CC01 + msgLen 字节 ---" +$ok = 0; $bad = 0 +foreach ($b in $resp70) { + $msgLen = $b[0x2C] + $sum = 0; for ($i = 0x2A; $i -lt 0x2A + 2 + $msgLen; $i++) { $sum += $b[$i] } + $ck = ($b[68] -shl 8) -bor $b[69] + if (($sum -band 0xFFFF) -eq $ck) { $ok++ } else { $bad++ } +} +Write-Host (" OK={0} BAD={1}" -f $ok, $bad) + +# 展示两个 70B 样本的完整 hex + 各字段 +Write-Host "`n--- 70B 样本字段 ---" +foreach ($b in ($resp70 | Select-Object -First 2)) { + $msgLen = $b[0x2C] + Write-Host (" frame: " + (Hex $b)) + Write-Host (" 0x28 RDMlen={0} 0x2C msgLen={1} dest={2} src={3} TN={4:x2} RT={5:x2} CC={6:x2} PID={7:x2}{8:x2} PD={9} ck={10:x2}{11:x2}" -f ` + ($b[0x28] -bor ($b[0x29] -shl 8)), $msgLen, (UidStr $b 0x2D), (UidStr $b 0x33), $b[0x39], $b[0x3A], $b[0x3E], $b[0x3F], $b[0x40], (Hex $b[0x42..0x43]), $b[0x44], $b[0x45]) + # 试 sum(0x2E..0x2E+msgLen-1) + $s1 = 0; for ($i = 0x2E; $i -lt 0x2E + $msgLen; $i++) { $s1 += $b[$i] } + Write-Host (" sum(0x2E..0x2E+msgLen-1)=0x{0:x4} (end=0x{1:x2})" -f ($s1 -band 0xFFFF), (0x2E + $msgLen - 1)) + $s2 = 0; for ($i = 0x2A; $i -lt 0x2A + 2 + $msgLen; $i++) { $s2 += $b[$i] } + Write-Host (" sum(0x2A..0x2A+msgLen+1)=0x{0:x4} (end=0x{1:x2})" -f ($s2 -band 0xFFFF), (0x2A + 2 + $msgLen - 1)) +} + +# 同样验证一个 87B (DEVICE_INFO) 应答 +Write-Host "`n--- 87B DEVICE_INFO 应答验证 ---" +$cnt = 0 +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + if ($p[1] -ne "192.168.123.1" -or $p[2] -ne "192.168.123.113") { continue } + $b = H2B $p[4] + if ($b.Length -eq 87 -and $b[0x2A] -eq 0xCC) { + $msgLen = $b[0x2C] + $ckOff = $b.Length - 2 + $ck = ($b[$ckOff] -shl 8) -bor $b[$ckOff+1] + $s1 = 0; for ($i = 0x2E; $i -lt 0x2E + $msgLen; $i++) { $s1 += $b[$i] } + $s2 = 0; for ($i = 0x2A; $i -lt 0x2A + 2 + $msgLen; $i++) { $s2 += $b[$i] } + Write-Host (" msgLen={0} ck=0x{1:x4} sum_noCC01=0x{2:x4}({3}) sum_withCC01=0x{4:x4}({5})" -f $msgLen, $ck, ($s1 -band 0xFFFF), $(if (($s1 -band 0xFFFF) -eq $ck) {"OK"} else {"x"}), ($s2 -band 0xFFFF), $(if (($s2 -band 0xFFFF) -eq $ck) {"OK"} else {"x"})) + $cnt++; if ($cnt -ge 5) { break } + } +} diff --git a/scripts/session/analyze_claim_seq.ps1 b/scripts/session/analyze_claim_seq.ps1 new file mode 100644 index 0000000..bcf5c9f --- /dev/null +++ b/scripts/session/analyze_claim_seq.ps1 @@ -0,0 +1,56 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$f = "$env:TEMP\opencode\cold2.tsv" +$lines = [IO.File]::ReadAllLines($f) +function H2B($hex) { $n = $hex.Length / 2; $b = New-Object byte[] $n; for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring(2*$i, 2), 16) }; return $b } +function Hex($b) { return ($b | ForEach-Object { $_.ToString("x2") }) -join "" } + +# (a) 38.087 的 102B type=2 帧 +Write-Host "=== (a) 首个 type=2 102B 帧 (claim 前) ===" +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5) { continue } + if ($p[1] -ne "192.168.123.113" -or $p[2] -ne "192.168.123.1") { continue } + $b = H2B $p[4] + if ($b.Length -eq 102 -and $b[4] -eq 2) { + Write-Host (" t={0} len={1}" -f $p[0], $b.Length) + Write-Host (" hex=" + (Hex $b)) + break + } +} + +# (b) 首个 644B announce: 找主机IP(717ba8c0)偏移 + 其它可变字段 +Write-Host "`n=== (b) 首个 644B type=1 announce ===" +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5) { continue } + if ($p[1] -ne "192.168.123.113") { continue } + $b = H2B $p[4] + if ($b.Length -eq 644) { + Write-Host (" t={0} dst={1}" -f $p[0], $p[2]) + $ip = [byte[]]@(0x71,0x7b,0xa8,0xc0) # 113.123.168.192 = LE of 192.168.123.113 + for ($i = 0; $i -lt $b.Length - 4; $i++) { + if ($b[$i] -eq 0x71 -and $b[$i+1] -eq 0x7b -and $b[$i+2] -eq 0xa8 -and $b[$i+3] -eq 0xc0) { + Write-Host (" 主机IP@0x{0:x}" -f $i) + } + } + Write-Host (" 0x00-0x40: " + (Hex $b[0..0x3F])) + break + } +} + +# (c) rdm_disc_req 模板 0x18-0x19 / 0x2A-0x3B +Write-Host "`n=== (c) rdm_disc_req 模板关键字段 ===" +$h = (Get-Content "C:\Users\tonycao\work\light\enet-validator\samples\rdm_disc_req_1126.hex" -Raw).Trim() -replace '\s','' +$b = H2B $h +Write-Host (" len={0} 0x18-0x19={1} 0x20-0x23={2} 0x2A-0x3B={3}" -f $b.Length, (Hex $b[0x18..0x19]), (Hex $b[0x20..0x23]), (Hex $b[0x2A..0x3B])) + +# (d) 首个 .1→113 的 RDM 应答帧: 0x18 sub + 0x2A 起内容 +Write-Host "`n=== (d) 首个 .1→113 RDM 应答 (sub@0x18, 0x2A 起 32B) ===" +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5) { continue } + if ($p[1] -ne "192.168.123.1") { continue } + $b = H2B $p[4] + if ($b.Length -ge 0x30 -and $b[4] -eq 4 -and ($b[0x2A] -eq 0xCC -or $b[0x2A] -eq 0xAA)) { + Write-Host (" t={0} len={1} 0x18-0x19={2} 0x20-0x21={3} 0x28-0x29={4}" -f $p[0], $b.Length, (Hex $b[0x18..0x19]), (Hex $b[0x20..0x21]), (Hex $b[0x28..0x29])) + Write-Host (" 0x2A+: " + (Hex $b[0x2A..([Math]::Min(0x4A, $b.Length-1))])) + break + } +} diff --git a/scripts/session/analyze_cold.ps1 b/scripts/session/analyze_cold.ps1 new file mode 100644 index 0000000..c2e97c1 --- /dev/null +++ b/scripts/session/analyze_cold.ps1 @@ -0,0 +1,240 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +$f = "$env:TEMP\opencode\cold2.tsv" +$lines = [IO.File]::ReadAllLines($f) + +function H2B($hex) { + if (-not $hex) { return $null } + $n = $hex.Length / 2 + $b = New-Object byte[] $n + for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring(2*$i, 2), 16) } + return $b +} +function G16($b, $o) { if ($b.Length -lt $o+2) { return -1 }; return $b[$o] -bor ($b[$o+1] -shl 8) } +function G32($b, $o) { if ($b.Length -lt $o+4) { return -1 }; return $b[$o] -bor ($b[$o+1] -shl 8) -bor ($b[$o+2] -shl 16) -bor ($b[$o+3] -shl 24) } +function UidStr($b, $o) { if ($b.Length -lt $o+6) { return "?" }; return ($b[$o..($o+5)] | ForEach-Object { $_.ToString("x2") }) -join "" } + +# ============ 1) 冷启动时间线: 前 30 秒关键帧 ============ +Write-Host "`n=========== [1] 冷启动前 30s 关键帧时间线 ===========" +$shown = 0 +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + $t = [double]$p[0]; if ($t -gt 30) { break } + $b = H2B $p[4]; if ($b.Length -lt 0x1A) { continue } + if ($b[0] -ne 0x45 -or $b[1] -ne 0x4E) { continue } # "EN" + $type = G32 $b 4; $dir = "$($p[1])->$($p[2])"; $len = $b.Length + $desc = $null + if ($type -eq 1) { $desc = "发现请求" } + elseif ($type -eq 2) { $sub = G16 $b 0x18 + if ($sub -eq 1) { $desc = "宣告sub=01" } + elseif ($sub -eq 2) { $desc = "注册sub=02(89B)" } + elseif ($sub -eq 3) { $desc = "心跳sub=03" } + elseif ($sub -eq 5) { $desc = "AECH质询sub=05" } + elseif ($sub -eq 6) { $desc = "AERE应答sub=06" } + else { $desc = "type2 sub=$sub" } } + elseif ($type -eq 4 -and $len -eq 259) { $desc = "259发现应答" } + elseif ($type -eq 4 -and $len -eq 84) { $desc = "84B状态" } + elseif ($type -eq 4 -and $len -gt 1000) { $desc = "DMX/RDM长帧" } + elseif ($type -eq 4) { $desc = "type4短帧" } + if ($desc -and $desc -ne "84B状态" -and $desc -ne "心跳sub=03" -and $shown -lt 60) { + Write-Host (" t={0,8:F3} {1,-26} len={2,-5} {3}" -f $t, $dir, $len, $desc); $shown++ + } +} + +# ============ 2) RDM 查询直方图 (主机->.1, type=4) ============ +Write-Host "`n=========== [2] 主机 RDM 查询 cmd 直方图 (type=4, 0x3D) ===========" +$cmdHist = @{}; $pidHist = @{}; $queries = New-Object System.Collections.ArrayList +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + if ($p[1] -ne "192.168.123.113" -or $p[2] -ne "192.168.123.1") { continue } + $b = H2B $p[4]; if ($b.Length -lt 0x52) { continue } + if ((G32 $b 4) -ne 4) { continue } + if ($b.Length -gt 2000) { continue } # 纯DMX 1126B? 实际 1084=DMX + $t = [double]$p[0] + $len = $b.Length + if ($len -eq 1084 -or $len -eq 1126) { # DMX 帧或内嵌 RDM 副本 + if ($b[0x3C] -ne 0x01) { continue } + } + $cmd = $b[0x3D] + if ($cmd -ne 0x24 -and $cmd -ne 0x18 -and $cmd -ne 0x19 -and $cmd -ne 0x1a) { continue } + $flag = $b[0x30] + $key = ("cmd=0x{0:x2} flag={1:x2}" -f $cmd, $flag) + if (-not $cmdHist.ContainsKey($key)) { $cmdHist[$key] = 0 }; $cmdHist[$key]++ + $uid = UidStr $b 0x3E + $pid2 = "" + if ($cmd -eq 0x18 -and $b.Length -gt 0x51) { $pid2 = "{0:x2}{1:x2}" -f $b[0x50], $b[0x51] + $pk = "$pid2 flag=$("{0:x2}" -f $flag)"; if (-not $pidHist.ContainsKey($pk)) { $pidHist[$pk] = 0 }; $pidHist[$pk]++ } + [void]$queries.Add(@{ t=$t; cmd=$cmd; flag=$flag; uid=$uid; pid=$pid2; seq=((G16 $b 0x22)); seq16lo=(G16 $b 0x22) }) +} +Write-Host "--- cmd 分布 ---" +$cmdHist.GetEnumerator() | Sort-Object Value -Descending | ForEach-Object { Write-Host (" {0} : {1}" -f $_.Key, $_.Value) } +Write-Host "--- cmd=18 的 PID 分布 ---" +$pidHist.GetEnumerator() | Sort-Object Value -Descending | ForEach-Object { Write-Host (" PID={0} : {1}" -f $_.Key, $_.Value) } + +# ============ 3) .1 应答帧: 长度/内容直方图 + checksum 验证 ============ +Write-Host "`n=========== [3] .1 应答帧分析 (.1->113, type=4 短帧) ===========" +$answers = New-Object System.Collections.ArrayList +$ansLenHist = @{} +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + if ($p[1] -ne "192.168.123.1" -or $p[2] -ne "192.168.123.113") { continue } + $b = H2B $p[4]; if ($b.Length -lt 0x2E) { continue } + if ((G32 $b 4) -ne 4) { continue } + if ($b.Length -gt 200) { continue } # 只看 RDM 短应答 (59/69/77/87/101/111/119B) + if ($b[0x2A] -ne 0xCC -and $b[0x2A] -ne 0xAA) { continue } + $t = [double]$p[0] + $len = $b.Length + if (-not $ansLenHist.ContainsKey($len)) { $ansLenHist[$len] = 0 }; $ansLenHist[$len]++ + [void]$answers.Add(@{ t=$t; b=$b; len=$len }) +} +Write-Host "--- 应答长度分布 ---" +$ansLenHist.GetEnumerator() | Sort-Object Name | ForEach-Object { Write-Host (" {0}B : {1}" -f $_.Key, $_.Value) } + +# checksum 验证 (标准 RDM 帧: 0x2A=0xCC, msgLen@0x2C, checksum=前 msgLen 字节和, BE) +Write-Host "--- 标准 RDM 应答 checksum 验证 (抽样全部) ---" +$okC = 0; $badC = 0; $badSamples = New-Object System.Collections.ArrayList +foreach ($a in $answers) { + $b = $a.b + if ($b[0x2A] -ne 0xCC) { continue } + $msgLen = $b[0x2C] + if ($b.Length -lt 0x2A + $msgLen + 2) { continue } + $sum = 0; for ($i = 0x2A; $i -lt 0x2A + $msgLen; $i++) { $sum += $b[$i] } + $ck = ($b[0x2A + $msgLen] -shl 8) -bor $b[0x2A + $msgLen + 1] + if (($sum -band 0xFFFF) -eq $ck) { $okC++ } else { $badC++; if ($badSamples.Count -lt 3) { [void]$badSamples.Add(@{t=$a.t; len=$a.len; sum=($sum -band 0xFFFF); ck=$ck}) } } +} +Write-Host (" checksum OK={0} BAD={1}" -f $okC, $badC) +foreach ($s in $badSamples) { Write-Host (" BAD样本: t={0:F3} len={1} sum=0x{2:x4} ck=0x{3:x4}" -f $s.t, $s.len, $s.sum, $s.ck) } + +# 脉冲帧 (0x2A=0xAA) 内容: 解码 UID +Write-Host "--- DISC 脉冲帧 (0x2A=0xAA) UID 解码 ---" +$pulseUids = @{} +foreach ($a in $answers) { + $b = $a.b + if ($b[0x2A] -ne 0xAA) { continue } + # 定位 0xAA 分隔符后的 16 字节编码 UID + $uid = "" + for ($i = 0; $i -lt 6; $i++) { + $b1 = $b[0x2B + 2*$i]; $b2 = $b[0x2C + 2*$i] + $byte = ($b1 -band 0x55) -bor ($b2 -band 0xAA) + $uid += $byte.ToString("x2") + } + if (-not $pulseUids.ContainsKey($uid)) { $pulseUids[$uid] = 0 }; $pulseUids[$uid]++ +} +$pulseUids.GetEnumerator() | Sort-Object Value -Descending | ForEach-Object { Write-Host (" UID={0} : {1} 脉冲" -f $_.Key, $_.Value) } + +# ============ 4) 查询-应答配对: cmd=18 按 PID, 时延统计 ============ +Write-Host "`n=========== [4] cmd=18 查询→应答配对 (seq 0x22-0x23 ↔ 0x20-0x21) ===========" +$ansBySeq = @{} +foreach ($a in $answers) { + $b = $a.b + if ($b.Length -lt 0x22) { continue } + $s = G16 $b 0x20 + if (-not $ansBySeq.ContainsKey($s)) { $ansBySeq[$s] = New-Object System.Collections.ArrayList } + [void]$ansBySeq[$s].Add($a) +} +$pairStats = @{}; $latByPid = @{} +foreach ($q in $queries) { + if ($q.cmd -ne 0x18 -or $q.flag -ne 0) { continue } + $pk = if ($q.pid) { $q.pid } else { "????" } + if (-not $pairStats.ContainsKey($pk)) { $pairStats[$pk] = @{ n=0; ans=0; lens=@{} } } + $pairStats[$pk].n++ + # 找该查询之后 200ms 内 seq 回显的应答 + $best = $null + foreach ($a in $answers) { + if ($a.t -lt $q.t -or $a.t -gt $q.t + 0.2) { continue } + $s = G16 $a.b 0x20 + if ($s -eq $q.seq) { $best = $a; break } + } + if ($best) { + $pairStats[$pk].ans++ + $al = $best.len + if (-not $pairStats[$pk].lens.ContainsKey($al)) { $pairStats[$pk].lens[$al] = 0 } + $pairStats[$pk].lens[$al]++ + if (-not $latByPid.ContainsKey($pk)) { $latByPid[$pk] = New-Object System.Collections.ArrayList } + [void]$latByPid[$pk].Add([int](($best.t - $q.t) * 1000)) + } +} +foreach ($k in ($pairStats.Keys | Sort-Object)) { + $st = $pairStats[$k] + $lensStr = ($st.lens.GetEnumerator() | ForEach-Object { "$($_.Key)B×$($_.Value)" }) -join " " + $latStr = "" + if ($latByPid.ContainsKey($k) -and $latByPid[$k].Count -gt 0) { + $la = $latByPid[$k] | Sort-Object + $latStr = (" 时延ms: min={0} 中位={1} max={2}" -f $la[0], $la[[int]($la.Count/2)], $la[-1]) + } + Write-Host (" PID={0} 查询={1} 配对应答={2} 长度[{3}]{4}" -f $k, $st.n, $st.ans, $lensStr, $latStr) +} + +# ============ 5) 0002/0003 应答逐字节样本 ============ +Write-Host "`n=========== [5] PID=0002 / 0003 应答样本 (前2个) ===========" +foreach ($wantPid in @("0002", "0003")) { + $cnt = 0 + foreach ($q in $queries) { + if ($q.cmd -ne 0x18 -or $q.flag -ne 0 -or $q.pid -ne $wantPid) { continue } + foreach ($a in $answers) { + if ($a.t -lt $q.t -or $a.t -gt $q.t + 0.2) { continue } + if ((G16 $a.b 0x20) -eq $q.seq) { + Write-Host (" PID={0} UID={1} t={2:F3} 查询len -> 应答{3}B: {4}" -f $wantPid, $q.uid, $q.t, $a.len, (($a.b | ForEach-Object { $_.ToString("x2") }) -join "")) + $cnt++; break + } + } + if ($cnt -ge 2) { break } + } + if ($cnt -eq 0) { Write-Host " PID=$wantPid 无配对样本" } +} + +# ============ 6) 发现收敛: 全范围查询(24) 与脉冲随时间 ============ +Write-Host "`n=========== [6] 发现收敛时间线 (10s 桶: 全范围查询数 / 脉冲应答数 / MUTE数) ===========" +$buckets = @{} +foreach ($q in $queries) { + if ($q.flag -ne 0) { continue } + $bk = [int]($q.t / 10) * 10 + if (-not $buckets.ContainsKey($bk)) { $buckets[$bk] = @{ disc=0; get=0; pulse=0; mute=0; unmute=0 } } + if ($q.cmd -eq 0x24) { $buckets[$bk].disc++ } + if ($q.cmd -eq 0x18 -and $q.pid -eq "0003") { $buckets[$bk].mute++ } + if ($q.cmd -eq 0x18 -and $q.pid -eq "0002") { $buckets[$bk].unmute++ } + if ($q.cmd -eq 0x18) { $buckets[$bk].get++ } +} +# 脉冲数按桶 +foreach ($a in $answers) { + if ($a.b[0x2A] -ne 0xAA) { continue } + $bk = [int]($a.t / 10) * 10 + if (-not $buckets.ContainsKey($bk)) { $buckets[$bk] = @{ disc=0; get=0; pulse=0; mute=0; unmute=0 } } + $buckets[$bk].pulse++ +} +foreach ($bk in ($buckets.Keys | Sort-Object)) { + $v = $buckets[$bk] + Write-Host (" t={0,4}-{1,-4}s 全范围查询={2,-5} GET查询={3,-5} 脉冲={4,-5} MUTE={5,-4} UNMUTE={6}" -f $bk, ($bk+10), $v.disc, $v.get, $v.pulse, $v.mute, $v.unmute) +} + +# ============ 7) 84B 状态帧节奏 ============ +Write-Host "`n=========== [7] 84B 状态帧节奏 (.1->113) ===========" +$statTimes = New-Object System.Collections.ArrayList +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + if ($p[1] -ne "192.168.123.1" -or $p[2] -ne "192.168.123.113") { continue } + $b = H2B $p[4] + if ($b.Length -eq 84 -and (G32 $b 4) -eq 4) { [void]$statTimes.Add([double]$p[0]) } +} +Write-Host (" 84B 总数={0}" -f $statTimes.Count) +if ($statTimes.Count -gt 2) { + $gaps = New-Object System.Collections.ArrayList + for ($i = 1; $i -lt $statTimes.Count; $i++) { [void]$gaps.Add([int](($statTimes[$i] - $statTimes[$i-1]) * 1000)) } + $gs = $gaps | Sort-Object + Write-Host (" 帧间隔ms: min={0} 中位={1} max={2}" -f $gs[0], $gs[[int]($gs.Count/2)], $gs[-1]) + # 突发检测: 间隔>1000ms 为新突发 + $bursts = 1; foreach ($g in $gaps) { if ($g -gt 1000) { $bursts++ } } + Write-Host (" 突发数={0} (平均每突发 {1:F1} 帧)" -f $bursts, ($statTimes.Count / $bursts)) +} + +# ============ 8) DEVICE_INFO(0060) 读取对象 UID ============ +Write-Host "`n=========== [8] DEVICE_INFO(0060)/参数读取的 UID 分布 ===========" +$diUids = @{} +foreach ($q in $queries) { + if ($q.cmd -ne 0x18 -or $q.flag -ne 0) { continue } + if ($q.pid -eq "0060" -or $q.pid -eq "0080" -or $q.pid -eq "00f0" -or $q.pid -eq "0003") { + $k = "$($q.pid)->$($q.uid)"; if (-not $diUids.ContainsKey($k)) { $diUids[$k] = 0 }; $diUids[$k]++ + } +} +$diUids.GetEnumerator() | Sort-Object Name | ForEach-Object { Write-Host (" {0} : {1}" -f $_.Key, $_.Value) } +Write-Host "`n分析完成。" diff --git a/scripts/session/analyze_cold2.ps1 b/scripts/session/analyze_cold2.ps1 new file mode 100644 index 0000000..2b4e313 --- /dev/null +++ b/scripts/session/analyze_cold2.ps1 @@ -0,0 +1,142 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +$f = "$env:TEMP\opencode\cold2.tsv" +$lines = [IO.File]::ReadAllLines($f) + +function H2B($hex) { + if (-not $hex) { return $null } + $n = $hex.Length / 2 + $b = New-Object byte[] $n + for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring(2*$i, 2), 16) } + return $b +} +function G16le($b, $o) { if ($b.Length -lt $o+2) { return -1 }; return $b[$o] -bor ($b[$o+1] -shl 8) } +function G32le($b, $o) { if ($b.Length -lt $o+4) { return -1 }; return $b[$o] -bor ($b[$o+1] -shl 8) -bor ($b[$o+2] -shl 16) -bor ($b[$o+3] -shl 24) } +function Hex($b) { return ($b | ForEach-Object { $_.ToString("x2") }) -join "" } +function UidStr($b, $o) { if ($b.Length -lt $o+6) { return "?" }; return ($b[$o..($o+5)] | ForEach-Object { $_.ToString("x2") }) -join "" } + +# checksum 测试: 先用已知 0002 样本验证公式 +Write-Host "=========== [0] checksum 公式自验 (0002 样本) ===========" +$sample = H2B "454e65740400000000001c7c00000000000001004600000002004275746c0000a5000000000000001c00cc011a453a7fffffff0899500026c952000000001100020200000729" +$msgLen = $sample[0x2C] +$sum = 0; for ($i = 0x2A; $i -lt 0x2A + $msgLen; $i++) { $sum += $sample[$i] } +$ckOff = 0x2A + $msgLen +Write-Host (" msgLen={0} sum=0x{1:x4} field=0x{2:x4} -> {3}" -f $msgLen, ($sum -band 0xFFFF), (($sample[$ckOff] -shl 8) -bor $sample[$ckOff+1]), $(if (($sum -band 0xFFFF) -eq (($sample[$ckOff] -shl 8) -bor $sample[$ckOff+1])) {"OK"} else {"BAD"})) + +# ============ A) .1->113 全帧型直方图 (type x len) ============ +Write-Host "`n=========== [A] .1→113 全帧型 (type×len) 直方图 ===========" +$h = @{} +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + if ($p[1] -ne "192.168.123.1") { continue } + $b = H2B $p[4]; if ($b.Length -lt 0x1A) { continue } + $t = G32le $b 4 + $k = "type=$t len=$($b.Length)" + if (-not $h.ContainsKey($k)) { $h[$k] = 0 }; $h[$k]++ +} +$h.GetEnumerator() | Sort-Object { [int]($_.Value) } -Descending | Select-Object -First 25 | ForEach-Object { Write-Host (" {0} : {1}" -f $_.Key, $_.Value) } + +# ============ B) type=2 帧时间线 (注册/AECH/AERE/心跳) ============ +Write-Host "`n=========== [B] type=2 帧时间线 (全程, 前 40 条) ===========" +$shown = 0 +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + $b = H2B $p[4]; if ($b.Length -lt 0x1A) { continue } + if ((G32le $b 4) -ne 2) { continue } + $sub = G16le $b 0x18 + $t = [double]$p[0] + Write-Host (" t={0,8:F3} {1,-26} len={2,-4} sub={3}" -f $t, "$($p[1])->$($p[2])", $b.Length, $sub) + $shown++; if ($shown -ge 40) { break } +} + +# ============ C) 发现请求/应答: 180B 请求 vs 217B 应答 全文 ============ +Write-Host "`n=========== [C] 180B 发现请求 与 217B 应答 样本 ===========" +$req180 = $null; $resp217 = $null +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + $b = H2B $p[4] + if (-not $req180 -and $p[1] -eq "192.168.123.113" -and $b.Length -eq 180) { $req180 = $b } + if (-not $resp217 -and $p[1] -eq "192.168.123.1" -and $b.Length -eq 217) { $resp217 = $b } + if ($req180 -and $resp217) { break } +} +if ($req180) { Write-Host (" 请求180B: " + (Hex $req180)) } +if ($resp217) { Write-Host (" 应答217B: " + (Hex $resp217)) } + +# ============ D) cmd=19 / cmd=1a 查询与应答样本 ============ +Write-Host "`n=========== [D] cmd=19 / cmd=1a 样本 (各3条查询+应答) ===========" +foreach ($wantCmd in @(0x19, 0x1a)) { + $cnt = 0 + foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + if ($p[1] -ne "192.168.123.113" -or $p[2] -ne "192.168.123.1") { continue } + $b = H2B $p[4]; if ($b.Length -lt 0x52 -or $b.Length -gt 2000) { continue } + if ((G32le $b 4) -ne 4 -or $b[0x3D] -ne $wantCmd -or $b[0x30] -ne 0) { continue } + $t = [double]$p[0] + Write-Host (" cmd={0:x2} t={1:F3} UID={2} 查询({3}B): {4}" -f $wantCmd, $t, (UidStr $b 0x3E), $b.Length, (Hex ($b[0x20..0x2F] + $b[0x3C..([Math]::Min($b.Length-1, 0x58))]))) + $cnt++; if ($cnt -ge 3) { break } + } +} + +# ============ E) flag=01 PID=0003 帧样本 (780条是什么) ============ +Write-Host "`n=========== [E] flag=01 PID=0003 帧样本 (前3条, 含UID) ===========" +$cnt = 0 +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + if ($p[1] -ne "192.168.123.113" -or $p[2] -ne "192.168.123.1") { continue } + $b = H2B $p[4]; if ($b.Length -lt 0x52 -or $b.Length -gt 2000) { continue } + if ((G32le $b 4) -ne 4 -or $b[0x3D] -ne 0x18 -or $b[0x30] -ne 1) { continue } + if ($b[0x50] -ne 0 -or $b[0x51] -ne 3) { continue } + $t = [double]$p[0] + Write-Host (" t={0:F3} len={1} flag01 UID={2} hdr: {3}" -f $t, $b.Length, (UidStr $b 0x3E), (Hex $b[0x20..0x33])) + $cnt++; if ($cnt -ge 3) { break } +} + +# ============ F) 脉冲时延: cmd=24 查询 → 脉冲 ============ +Write-Host "`n=========== [F] cmd=24(flag=00) 查询→脉冲 时延分布 ===========" +$discQ = New-Object System.Collections.ArrayList +$pulses = New-Object System.Collections.ArrayList +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + $b = H2B $p[4]; if ($b.Length -lt 0x2E) { continue } + $t = [double]$p[0] + if ($p[1] -eq "192.168.123.113" -and $p[2] -eq "192.168.123.1" -and $b.Length -gt 0x44 -and $b.Length -lt 2000 -and (G32le $b 4) -eq 4 -and $b[0x3D] -eq 0x24 -and $b[0x30] -eq 0) { + [void]$discQ.Add(@{ t=$t; lo=($b[0x3E..0x43] | ForEach-Object { $_.ToString("x2") }) -join "" }) + } + if ($p[1] -eq "192.168.123.1" -and $b.Length -lt 200 -and $b[0x2A] -eq 0xAA) { + [void]$pulses.Add(@{ t=$t }) + } +} +Write-Host (" cmd24查询={0} 脉冲={1}" -f $discQ.Count, $pulses.Count) +$lats = New-Object System.Collections.ArrayList +foreach ($pu in $pulses) { + # 找之前最近的查询 + $best = $null + for ($i = $discQ.Count - 1; $i -ge 0; $i--) { + if ($discQ[$i].t -le $pu.t) { $best = $discQ[$i]; break } + } + if ($best -and ($pu.t - $best.t) -lt 0.5) { [void]$lats.Add([int](($pu.t - $best.t) * 1000)) } +} +if ($lats.Count -gt 0) { + $ls = $lats | Sort-Object + Write-Host (" 脉冲时延ms: n={0} min={1} 中位={2} max={3}" -f $ls.Count, $ls[0], $ls[[int]($ls.Count/2)], $ls[-1]) +} + +# ============ G) cmd=24 查询负载: 分支范围 vs 全范围 ============ +Write-Host "`n=========== [G] cmd=24 查询的搜索范围分布 (UID 下界@0x3E) ===========" +$ranges = @{} +foreach ($q in $discQ) { + if (-not $ranges.ContainsKey($q.lo)) { $ranges[$q.lo] = 0 }; $ranges[$q.lo]++ +} +$ranges.GetEnumerator() | Sort-Object Value -Descending | Select-Object -First 10 | ForEach-Object { Write-Host (" 范围下界={0} : {1}" -f $_.Key, $_.Value) } + +# ============ H) AECH/AERE 帧 (594B) 抓取 ============ +Write-Host "`n=========== [H] AECH/AERE (594B type=2) 时间点 ===========" +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + $b = H2B $p[4]; if ($b.Length -lt 500) { continue } + if ((G32le $b 4) -ne 2) { continue } + $sub = G16le $b 0x18 + $t = [double]$p[0] + Write-Host (" t={0:F3} {1} len={2} sub={3} hasAECH={4} hasAERE={5}" -f $t, "$($p[1])->$($p[2])", $b.Length, $sub, ((Hex $b) -match "41454348"), ((Hex $b) -match "41455245")) +} +Write-Host "`n分析完成。" diff --git a/scripts/session/analyze_coldstart_seq.ps1 b/scripts/session/analyze_coldstart_seq.ps1 new file mode 100644 index 0000000..12a30a5 --- /dev/null +++ b/scripts/session/analyze_coldstart_seq.ps1 @@ -0,0 +1,31 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$f = "$env:TEMP\opencode\cold2.tsv" +$lines = [IO.File]::ReadAllLines($f) +function H2B($hex) { $n = $hex.Length / 2; $b = New-Object byte[] $n; for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring(2*$i, 2), 16) }; return $b } +function G32le($b, $o) { return $b[$o] -bor ($b[$o+1] -shl 8) -bor ($b[$o+2] -shl 16) -bor ($b[$o+3] -shl 24) } + +Write-Host "=== cold2 前 120 帧 113→.1 (认领建立期) ===" +$count = 0 +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5) { continue } + if ($p[1] -ne "192.168.123.113") { continue } + $b = H2B $p[4] + $t = [double]$p[0] + $len = $b.Length + $magic = [Text.Encoding]::ASCII.GetString($b[0..3]) + $type = G32le $b 4 + $desc = "" + if ($magic -eq "ENet" -and $type -eq 4 -and $len -ge 0x50) { + $sub = "{0:x2}{1:x2}" -f $b[0x19], $b[0x18] + $flag = $b[0x30] + if ($b[0x2A] -eq 0xCC -or $b[0x3C] -eq 0x01) { + $msgLen = $b[0x3D]; $cc = $b[0x4F]; $pid = ($b[0x50] -shl 8) -bor $b[0x51] + $desc = "RDM请求 sub=$sub flag=$('{0:x2}' -f $flag) msgLen=$msgLen CC=$('{0:x2}' -f $cc) PID=$('{0:x4}' -f $pid) TN=$('{0:x2}' -f $b[0x4A])" + } else { $desc = "type4 sub=$sub flag=$('{0:x2}' -f $flag) 0x2A=$('{0:x2}' -f $b[0x2A])" } + } elseif ($magic -eq "ENet" -and $type -eq 4) { + $desc = "type4 短帧" + } + Write-Host ("{0,9:N3} len={1,-5} magic={2} type={3} {4}" -f $t, $len, $magic, $type, $desc) + $count++ + if ($count -ge 120) { break } +} diff --git a/scripts/session/analyze_dmx1.ps1 b/scripts/session/analyze_dmx1.ps1 new file mode 100644 index 0000000..31e55ba --- /dev/null +++ b/scripts/session/analyze_dmx1.ps1 @@ -0,0 +1,70 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +function H2B($hex) { + if (-not $hex) { return $null } + $n = $hex.Length / 2 + $b = New-Object byte[] $n + for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring(2*$i, 2), 16) } + return $b +} +$lines = [IO.File]::ReadAllLines("$env:TEMP\opencode\ss123.tsv") +$dmx = New-Object System.Collections.ArrayList +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + if ($p[1] -ne "192.168.123.113" -or $p[2] -ne "192.168.123.1") { continue } + $b = H2B $p[4]; if ($b.Length -ne 1084) { continue } + if ([int]$b[4] -ne 4) { continue } + if (-not ($b[0x20] -eq 1 -and $b[0x21] -eq 1)) { continue } + [void]$dmx.Add(@{ t=[double]$p[0]; b=$b }) +} +Write-Host ("DMX-A 帧数: {0}" -f $dmx.Count) +if ($dmx.Count -eq 0) { exit } + +# --- 1. 头部逐字节变化分析 (0x00-0x3B): 常量 or 变量 --- +Write-Host "`n=== 头部 0x00-0x3B 逐字节分析 ===" +$first = $dmx[0].b +for ($off = 0; $off -lt 0x3C; $off++) { + $min = 255; $max = 0; $diff = 0 + foreach ($f in $dmx) { $v = [int]$f.b[$off]; if ($v -lt $min) { $min = $v }; if ($v -gt $max) { $max = $v }; if ($v -ne [int]$first[$off]) { $diff++ } } + if ($diff -gt 0) { + Write-Host (" 0x{0:x2}: 变量 首值={1:x2} min={2:x2} max={3:x2} 变化帧数={4}" -f $off, $first[$off], $min, $max, $diff) + } +} +Write-Host " (未列出 = 全部帧常量)" +Write-Host "`n=== 常量头 hex (首帧 0x00-0x3B) ===" +Write-Host (" " + (($first[0..0x3B] | ForEach-Object { $_.ToString("x2") }) -join " ")) + +# --- 2. 变化字段的语义: 找 16/32 位递增计数器 --- +Write-Host "`n=== 疑似序号字段 (按 LE u16/u32 检验单调性) ===" +foreach ($off in @(0x22, 0x24, 0x26, 0x28, 0x2A, 0x2C, 0x2E, 0x30, 0x32, 0x34, 0x36, 0x38, 0x3A)) { + $vals = $dmx | ForEach-Object { [int]$_.b[$off] + [int]$_.b[$off+1]*256 } + $inc = 0; $dec = 0 + for ($i = 1; $i -lt $vals.Count; $i++) { if ($vals[$i] -gt $vals[$i-1]) { $inc++ } elseif ($vals[$i] -lt $vals[$i-1]) { $dec++ } } + if ($inc -gt $vals.Count * 0.8 -or $dec -gt $vals.Count * 0.8) { + Write-Host (" u16@0x{0:x2}: 首={1} 尾={2} 增={3} 减={4}" -f $off, $vals[0], $vals[-1], $inc, $dec) + } +} + +# --- 3. 通道数据区 0x3C-0x43B (1024B): 非零分布 --- +Write-Host "`n=== 通道数据区非零统计 (0x3C 起 1024B) ===" +$nz = @{} +foreach ($f in $dmx) { + for ($i = 0x3C; $i -lt 0x43C; $i++) { if ($f.b[$i] -ne 0) { $rel = $i - 0x3C; if (-not $nz.ContainsKey($rel)) { $nz[$rel] = 0 }; $nz[$rel]++ } } +} +$nzKeys = $nz.Keys | Sort-Object +Write-Host (" 非零字节位置数: {0}" -f $nzKeys.Count) +if ($nzKeys.Count -gt 0) { + Write-Host (" 最小相对偏移: {0} (CH{1}) 最大相对偏移: {2} (CH{3})" -f $nzKeys[0], ($nzKeys[0]+1), $nzKeys[-1], ($nzKeys[-1]+1)) + $ranges = @(); $rs = $nzKeys[0]; $rp = $nzKeys[0] + foreach ($k in $nzKeys) { if ($k -gt $rp + 1) { $ranges += "$rs-$rp"; $rs = $k }; $rp = $k } + $ranges += "$rs-$rp" + Write-Host (" 连续非零区间 (相对0x3C): " + ($ranges -join ", ")) +} +# 首帧前 32 通道值 +Write-Host (" 首帧 CH1-32: " + (($first[0x3C..0x5B] | ForEach-Object { $_.ToString("x2") }) -join " ")) + +# --- 4. 速率与时间隔 --- +$tg = New-Object System.Collections.ArrayList +for ($i = 1; $i -lt $dmx.Count; $i++) { [void]$tg.Add([int](($dmx[$i].t - $dmx[$i-1].t) * 1000)) } +$tgs = $tg | Sort-Object +Write-Host ("`n=== 帧间隔ms: min={0} p50={1} p90={2} max={3} 率={4:N1}/s ===" -f $tgs[0], $tgs[[int]($tgs.Count/2)], $tgs[[int]($tgs.Count*0.9)], $tgs[-1], ($dmx.Count / ($dmx[-1].t - $dmx[0].t))) diff --git a/scripts/session/analyze_dmx2.ps1 b/scripts/session/analyze_dmx2.ps1 new file mode 100644 index 0000000..30693c8 --- /dev/null +++ b/scripts/session/analyze_dmx2.ps1 @@ -0,0 +1,82 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +function H2B($hex) { + if (-not $hex) { return $null } + $n = $hex.Length / 2 + $b = New-Object byte[] $n + for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring(2*$i, 2), 16) } + return $b +} +$lines = [IO.File]::ReadAllLines("$env:TEMP\opencode\ss123.tsv") +$dmx = New-Object System.Collections.ArrayList +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + if ($p[1] -ne "192.168.123.113" -or $p[2] -ne "192.168.123.1") { continue } + $b = H2B $p[4]; if ($b.Length -ne 1084) { continue } + if ([int]$b[4] -ne 4) { continue } + if (-not ($b[0x20] -eq 1 -and $b[0x21] -eq 1)) { continue } + [void]$dmx.Add(@{ t=[double]$p[0]; b=$b }) +} +Write-Host ("DMX-A 帧数: {0}" -f $dmx.Count) + +# --- 0x22-0x29 作为 LE u64 检验 --- +function GU64($b, $off) { + $v = [uint64]0 + for ($i = 7; $i -ge 0; $i--) { $v = $v * 256 + [uint64]$b[$off+$i] } + return $v +} +$inc64 = 0; $dec64 = 0; $eq = 0 +$deltas = New-Object System.Collections.ArrayList +for ($i = 1; $i -lt $dmx.Count; $i++) { + $v0 = GU64 $dmx[$i-1].b 0x22; $v1 = GU64 $dmx[$i].b 0x22 + if ($v1 -gt $v0) { $inc64++; [void]$deltas.Add($v1 - $v0) } elseif ($v1 -lt $v0) { $dec64++ } else { $eq++ } +} +Write-Host ("u64@0x22: 增={0} 减={1} 等={2}" -f $inc64, $dec64, $eq) +if ($deltas.Count -gt 0) { + $ds = $deltas | Sort-Object + Write-Host (" 增量: min={0} p50={1} p90={2} max={3}" -f $ds[0], $ds[[int]($ds.Count/2)], $ds[[int]($ds.Count*0.9)], $ds[-1]) + $top = $deltas | Group-Object | Sort-Object { [int]$_.Name } | Select-Object -First 8 + Write-Host (" 常见增量: " + (($top | ForEach-Object { "{0}×{1}" -f $_.Name, $_.Count }) -join ", ")) +} +Write-Host (" 首帧 u64={0} (0x{0:x}) 尾帧 u64={1} (0x{1:x})" -f (GU64 $dmx[0].b 0x22), (GU64 $dmx[-1].b 0x22)) + +# --- u64 增量 vs 实际时间差: 是否微秒时间戳 --- +Write-Host "`n=== u64 增量 vs 时间差相关性 (前 12 帧) ===" +for ($i = 1; $i -lt 13; $i++) { + $dt = ($dmx[$i].t - $dmx[$i-1].t) * 1000000 # us + $dv = [int64]((GU64 $dmx[$i].b 0x22) - (GU64 $dmx[$i-1].b 0x22)) + Write-Host (" dt={0,8:N0}us du64={1,8}" -f $dt, $dv) +} + +# --- 0x1A-0x1B: 与通道数据的校验和关系? --- +Write-Host "`n=== 0x1A-0x1B 检验 (是否通道数据校验) ===" +$matchSum = 0; $matchCrc = 0 +foreach ($f in ($dmx | Select-Object -First 200)) { + $stored = [int]$f.b[0x1A] + [int]$f.b[0x1B]*256 + # 简单和 (0x3C 起 1024B) + $sum = 0; foreach ($v in $f.b[0x3C..0x43B]) { $sum += $v } + if (($sum -band 0xFFFF) -eq $stored) { $matchSum++ } + # CRC16-IBM + $crc = 0 + foreach ($v in $f.b[0x3C..0x43B]) { + $crc = $crc -bxor $v + for ($j = 0; $j -lt 8; $j++) { if ($crc -band 1) { $crc = ($crc -shr 1) -bxor 0xA001 } else { $crc = $crc -shr 1 } } + } + if ($crc -eq $stored) { $matchCrc++ } +} +Write-Host (" 简单和匹配: {0}/200 CRC16-IBM匹配: {1}/200" -f $matchSum, $matchCrc) +# 打印几个 stored 值与帧序关系 +Write-Host (" 0x1A-1B 前 10 帧: " + ((($dmx | Select-Object -First 10) | ForEach-Object { "{0:x2}{1:x2}" -f $_.b[0x1B], $_.b[0x1A] }) -join " ")) + +# --- 0x22-0x29 是否某种位打包 seq? 试 u32@0x22 / u32@0x26 --- +foreach ($off in @(0x22, 0x24, 0x26)) { + $v0 = (GU64 $dmx[0].b $off) -band 0xFFFFFFFF + $vN = (GU64 $dmx[-1].b $off) -band 0xFFFFFFFF + $inc = 0; $dec = 0 + for ($i = 1; $i -lt $dmx.Count; $i++) { + $a = (GU64 $dmx[$i-1].b $off) -band 0xFFFFFFFF + $b2 = (GU64 $dmx[$i].b $off) -band 0xFFFFFFFF + if ($b2 -gt $a) { $inc++ } elseif ($b2 -lt $a) { $dec++ } + } + Write-Host ("u32@0x{0:x2}: 首={1} 尾={2} 增={3} 减={4}" -f $off, $v0, $vN, $inc, $dec) +} diff --git a/scripts/session/analyze_dmx3.ps1 b/scripts/session/analyze_dmx3.ps1 new file mode 100644 index 0000000..a28bb5f --- /dev/null +++ b/scripts/session/analyze_dmx3.ps1 @@ -0,0 +1,96 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +function H2B($hex) { + if (-not $hex) { return $null } + $n = $hex.Length / 2 + $b = New-Object byte[] $n + for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring(2*$i, 2), 16) } + return $b +} +function HexR($b, $o, $l) { return (($b[$o..($o+$l-1)] | ForEach-Object { $_.ToString("x2") }) -join "") } +$lines = [IO.File]::ReadAllLines("$env:TEMP\opencode\ss123.tsv") +$dmx = New-Object System.Collections.ArrayList +$keys = New-Object System.Collections.ArrayList +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + $b = H2B $p[4] + if ($p[1] -eq "192.168.123.113" -and $p[2] -eq "192.168.123.1" -and $b.Length -eq 1084 -and [int]$b[4] -eq 4 -and $b[0x20] -eq 1 -and $b[0x21] -eq 1) { + [void]$dmx.Add(@{ t=[double]$p[0]; b=$b }) + } + # 密钥报文: 主机→广播 1947B 帧 + if ($p[1] -eq "192.168.123.113" -and $p[2] -match "255" -and $b.Length -ge 1900) { + [void]$keys.Add(@{ t=[double]$p[0]; b=$b }) + } +} +Write-Host ("DMX-A={0} 密钥广播={1}" -f $dmx.Count, $keys.Count) + +# === 1. 相同通道数据的帧, 可变字节是否相同? === +Write-Host "`n=== 相同通道数据帧的可变字节对比 ===" +$byData = @{} +foreach ($f in $dmx) { + $dh = HexR $f.b 0x3C 64 # 前64通道作指纹 + if (-not $byData.ContainsKey($dh)) { $byData[$dh] = New-Object System.Collections.ArrayList } + [void]$byData[$dh].Add($f) +} +$groups = $byData.GetEnumerator() | Where-Object { $_.Value.Count -ge 2 } | Select-Object -First 3 +$gcount = 0 +foreach ($g in $groups) { + $gcount++ + Write-Host (" 组{0} ({1}帧):" -f $gcount, $g.Value.Count) + foreach ($f in ($g.Value | Select-Object -First 4)) { + Write-Host (" t={0,8:F3} 0x1A={1} 0x22={2}" -f $f.t, (HexR $f.b 0x1A 2), (HexR $f.b 0x22 8)) + } +} +if ($gcount -eq 0) { Write-Host " (120s 内通道数据无重复 — 效果持续变化)" } + +# === 2. 可变字节是否随帧序平滑变化? 连续 8 帧的 0x22-0x29 === +Write-Host "`n=== 连续 8 帧 0x1A/0x22 原始值 ===" +foreach ($f in ($dmx | Select-Object -First 8)) { + Write-Host (" t={0,8:F3} 0x1A={1} 0x22-0x29={2}" -f $f.t, (HexR $f.b 0x1A 2), (HexR $f.b 0x22 8)) +} + +# === 3. 密钥广播结构 === +if ($keys.Count -gt 0) { + Write-Host "`n=== 密钥广播帧 (前2个) ===" + foreach ($k in ($keys | Select-Object -First 2)) { + Write-Host (" t={0,8:F3} len={1}" -f $k.t, $k.b.Length) + Write-Host (" 0x00-0x2F: " + (HexR $k.b 0 0x30)) + Write-Host (" 0x30-0x5F: " + (HexR $k.b 0x30 0x30)) + } + # 密钥帧之间是否相同 + if ($keys.Count -ge 2) { + $same = 0; $diff = 0 + for ($i = 0; $i -lt [Math]::Min(0x100, $keys[0].b.Length); $i++) { + if ($keys[0].b[$i] -eq $keys[1].b[$i]) { $same++ } else { $diff++ } + } + Write-Host (" 帧1vs帧2 前256B: 同={0} 异={1}" -f $same, $diff) + } + # 密钥帧间隔 + $kg = New-Object System.Collections.ArrayList + for ($i = 1; $i -lt $keys.Count; $i++) { [void]$kg.Add([int](($keys[$i].t - $keys[$i-1].t) * 1000)) } + Write-Host (" 间隔ms: " + (($kg | Select-Object -First 12) -join ", ")) +} + +# === 4. 0x22-0x29 是否 = f(通道数据)? 试常见简单哈希 === +Write-Host "`n=== 0x22-0x29 vs 通道数据简单函数 ===" +$m_add=0; $m_xor=0; $m_crc32=0 +foreach ($f in ($dmx | Select-Object -First 100)) { + $d = $f.b[0x3C..0x43B] + # CRC32 (IEEE) + $crc = [uint32]0xFFFFFFFF + foreach ($v in $d) { + $crc = $crc -bxor $v + for ($j=0; $j -lt 8; $j++) { if ($crc -band 1) { $crc = ($crc -shr 1) -bxor 0xEDB88320 } else { $crc = $crc -shr 1 } } + } + $crc = $crc -bxor 0xFFFFFFFF + $s0 = [uint32]$f.b[0x22] + [uint32]$f.b[0x23]*256 + [uint32]$f.b[0x24]*65536 + [uint32]$f.b[0x25]*16777216 + $s1 = [uint32]$f.b[0x26] + [uint32]$f.b[0x27]*256 + [uint32]$f.b[0x28]*65536 + [uint32]$f.b[0x29]*16777216 + if ($crc -eq $s0 -or $crc -eq $s1) { $m_crc32++ } +} +Write-Host (" CRC32(通道数据) 匹配 0x22或0x26: {0}/100" -f $m_crc32) + +# === 5. 常量字段 0x2C-0x3B 的稳定性 (是否随 universe 变化) === +Write-Host "`n=== 0x2C-0x3B 常量确认 ===" +$var34 = @{} +foreach ($f in $dmx) { $h = HexR $f.b 0x2C 16; if (-not $var34.ContainsKey($h)) { $var34[$h] = 0 }; $var34[$h]++ } +foreach ($kv in $var34.GetEnumerator()) { Write-Host (" {0} : {1}帧" -f $kv.Key, $kv.Value) } diff --git a/scripts/session/analyze_fix2.ps1 b/scripts/session/analyze_fix2.ps1 new file mode 100644 index 0000000..7445eec --- /dev/null +++ b/scripts/session/analyze_fix2.ps1 @@ -0,0 +1,81 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" + +function H2B($hex) { + if (-not $hex) { return $null } + $n = $hex.Length / 2 + $b = New-Object byte[] $n + for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring(2*$i, 2), 16) } + return $b +} +function UidStr($b, $o) { if ($b.Length -lt $o+6) { return "?" }; return ($b[$o..($o+5)] | ForEach-Object { $_.ToString("x2") }) -join "" } + +function Analyze2($name, $tsv, $devIp, $hostIp) { + $lines = [IO.File]::ReadAllLines($tsv) + Write-Host "`n############ $name ($devIp) ############" + # 真实 RDM 查询: 0x20-0x21 == 0200 (事件B帧), flag 0x30==0 + $rdmCmd = @{}; $rdmPid = @{}; $rdmPidUid = @{} + $falsePos = 0 + $bFrames = New-Object System.Collections.ArrayList # 所有事件B帧 (时间, 0x22序号, 0x3C, 0x3D) + foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + if ($p[1] -ne $hostIp -or $p[2] -ne $devIp) { continue } + $b = H2B $p[4]; if ($b.Length -lt 0x52 -or $b.Length -gt 2000) { continue } + if ([int]$b[4] -ne 4) { continue } + $t = [double]$p[0] + $isB = ($b[0x20] -eq 2 -and $b[0x21] -eq 0) + $isA = ($b[0x20] -eq 1 -and $b[0x21] -eq 1) + if ($isB -and $b.Length -gt 0x44) { + [void]$bFrames.Add(@{ t=$t; seq=([int]$b[0x22]+[int]$b[0x23]*256); c3c=$b[0x3C]; c3d=$b[0x3D]; flag=$b[0x30] }) + } + # RDM 判定: 必须 0x3C==01 且 cmd 匹配 + if ($b.Length -gt 0x44 -and $b[0x3C] -eq 1 -and ($b[0x3D] -eq 0x24 -or $b[0x3D] -eq 0x18 -or $b[0x3D] -eq 0x19 -or $b[0x3D] -eq 0x1a)) { + if ($b[0x30] -ne 0) { continue } + if ($isA) { $falsePos++; continue } # DMX-A 帧通道数据巧合 = 假阳性 + $cmd = $b[0x3D] + $ck = "cmd={0:x2}" -f $cmd + if (-not $rdmCmd.ContainsKey($ck)) { $rdmCmd[$ck] = 0 }; $rdmCmd[$ck]++ + if ($cmd -ne 0x24 -and $b.Length -gt 0x51) { + $pidKey = "{0:x2}{1:x2}" -f $b[0x50], $b[0x51] + $uid = UidStr $b 0x3E + if (-not $rdmPid.ContainsKey($pidKey)) { $rdmPid[$pidKey] = 0 }; $rdmPid[$pidKey]++ + $k2 = "$pidKey->$uid"; if (-not $rdmPidUid.ContainsKey($k2)) { $rdmPidUid[$k2] = 0 }; $rdmPidUid[$k2]++ + } + } + } + Write-Host ("--- 真实 RDM 查询 (排除 DMX-A 假阳性 {0} 条) ---" -f $falsePos) + $rdmCmd.GetEnumerator() | Sort-Object { [int]$_.Value } -Descending | ForEach-Object { Write-Host (" {0} : {1}" -f $_.Key, $_.Value) } + Write-Host "--- PID 分布 ---" + $rdmPid.GetEnumerator() | Sort-Object { [int]$_.Value } -Descending | Select-Object -First 12 | ForEach-Object { Write-Host (" PID={0} : {1}" -f $_.Key, $_.Value) } + Write-Host "--- PID->UID 细分 (0002/0003/0060/00f0/0201) ---" + $rdmPidUid.GetEnumerator() | Where-Object { $_.Name -match "^(0002|0003|0060|00f0|0201)" } | Sort-Object Name | ForEach-Object { Write-Host (" {0} : {1}" -f $_.Name, $_.Value) } + + # 42B ACK 相关性: 每个 42B 帧之前 200ms 内最近的 B 帧特征 + $acks = New-Object System.Collections.ArrayList + foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + if ($p[1] -ne $devIp -or $p[2] -ne $hostIp) { continue } + $b = H2B $p[4]; if ($b.Length -ne 42 -or [int]$b[4] -ne 4) { continue } + [void]$acks.Add(@{ t=([double]$p[0]); seq=([int]$b[0x20]+[int]$b[0x21]*256) }) + } + $ackByB = @{} + $ackNoB = 0 + foreach ($a in $acks) { + # 找 seq == ack.seq+1 的 B 帧 (§4.3: ACK seq = 事件 seq - 1) + $found = $null + foreach ($bf in $bFrames) { + if ($bf.seq -eq $a.seq + 1) { $found = $bf; break } + } + if ($found) { + $k = "0x3C={0:x2} 0x3D={1:x2} flag={2:x2}" -f $found.c3c, $found.c3d, $found.flag + if (-not $ackByB.ContainsKey($k)) { $ackByB[$k] = 0 }; $ackByB[$k]++ + } else { $ackNoB++ } + } + Write-Host ("--- 42B ACK ({0}个) 对应的 B 帧特征 ---" -f $acks.Count) + $ackByB.GetEnumerator() | Sort-Object { [int]$_.Value } -Descending | Select-Object -First 8 | ForEach-Object { Write-Host (" {0} : {1}" -f $_.Key, $_.Value) } + Write-Host (" (无配对 B 帧: {0})" -f $ackNoB) +} + +Analyze2 "真机 .1" "$env:TEMP\opencode\ss123.tsv" "192.168.123.1" "192.168.123.113" +Analyze2 "虚拟 .7 新代码" "$env:TEMP\opencode\fix2.tsv" "192.168.58.7" "192.168.58.107" +Write-Host "`n分析完成。" diff --git a/scripts/session/analyze_mute.ps1 b/scripts/session/analyze_mute.ps1 new file mode 100644 index 0000000..46a82d4 --- /dev/null +++ b/scripts/session/analyze_mute.ps1 @@ -0,0 +1,69 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +function H2B($hex) { + if (-not $hex) { return $null } + $n = $hex.Length / 2 + $b = New-Object byte[] $n + for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring(2*$i, 2), 16) } + return $b +} +$lines = [IO.File]::ReadAllLines("$env:TEMP\opencode\ss123.tsv") + +# 事件提取: 0003广播MUTE / 0002定向UNMUTE / 脉冲 +$events = New-Object System.Collections.ArrayList +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + $b = H2B $p[4]; if ($b.Length -lt 0x30) { continue } + $t = [double]$p[0] + if ($p[1] -eq "192.168.123.113" -and $p[2] -eq "192.168.123.1" -and [int]$b[4] -eq 4 -and $b.Length -gt 0x51 -and $b.Length -lt 2000 -and $b[0x20] -eq 2 -and $b[0x21] -eq 0 -and $b[0x30] -eq 0 -and $b[0x3C] -eq 1 -and $b[0x3D] -eq 0x18) { + $pid2 = "{0:x2}{1:x2}" -f $b[0x50], $b[0x51] + $uid = ($b[0x3E..0x43] | ForEach-Object { $_.ToString("x2") }) -join "" + if ($pid2 -eq "0003") { [void]$events.Add(@{ t=$t; kind="MUTE"; uid=$uid }) } + if ($pid2 -eq "0002") { [void]$events.Add(@{ t=$t; kind="UNMUTE"; uid=$uid }) } + } + if ($p[1] -eq "192.168.123.1" -and $p[2] -eq "192.168.123.113" -and $b.Length -lt 200 -and $b[0x2A] -eq 0xAA) { + # 解码脉冲 UID + $uid = "" + for ($i = 0; $i -lt 6; $i++) { + $b1 = $b[0x2B + 2*$i]; $b2 = $b[0x2C + 2*$i] + $byte = ([int]$b1 -band 0x55) -bor ([int]$b2 -band 0xAA) + $uid += $byte.ToString("x2") + } + [void]$events.Add(@{ t=$t; kind="PULSE"; uid=$uid }) + } +} +$mutes = @($events | Where-Object { $_.kind -eq "MUTE" }) +$unmutes = @($events | Where-Object { $_.kind -eq "UNMUTE" }) +$pulses = @($events | Where-Object { $_.kind -eq "PULSE" }) +Write-Host ("MUTE={0} UNMUTE={1} 脉冲={2}" -f $mutes.Count, $unmutes.Count, $pulses.Count) + +# MUTE 时间点与脉冲静默窗口: 每个 MUTE 后 3s 内脉冲数 vs 3-15s 脉冲数 +Write-Host "`n--- 广播 MUTE(0003) 时间点 vs 脉冲密度 ---" +foreach ($m in $mutes) { + $before = @($pulses | Where-Object { $_.t -ge $m.t - 3 -and $_.t -lt $m.t }).Count + $after3 = @($pulses | Where-Object { $_.t -ge $m.t -and $_.t -lt $m.t + 3 }).Count + $after12 = @($pulses | Where-Object { $_.t -ge $m.t + 3 -and $_.t -lt $m.t + 15 }).Count + Write-Host (" MUTE t={0,7:F2} 前3s脉冲={1,-3} 后0-3s={2,-3} 后3-15s={3}" -f $m.t, $before, $after3, $after12) +} + +# UNMUTE 时间点 vs 其后脉冲 (同 UID) +Write-Host "`n--- 定向 UNMUTE(0002) vs 其后 1s 内同 UID 脉冲 ---" +foreach ($u in ($unmutes | Select-Object -First 10)) { + $same = @($pulses | Where-Object { $_.t -ge $u.t -and $_.t -lt $u.t + 1 -and $_.uid -eq $u.uid }).Count + $any = @($pulses | Where-Object { $_.t -ge $u.t -and $_.t -lt $u.t + 1 }).Count + Write-Host (" UNMUTE t={0,7:F2} UID={1} 后1s同UID脉冲={2} 任意={3}" -f $u.t, $u.uid, $same, $any) +} + +# 脉冲的 UID 分布随时间 (每 15s 桶) +Write-Host "`n--- 脉冲 UID×15s 桶 ---" +$buckets = @{} +foreach ($pu in $pulses) { + $bk = [int]($pu.t / 15) + if (-not $buckets.ContainsKey($bk)) { $buckets[$bk] = @{} } + if (-not $buckets[$bk].ContainsKey($pu.uid)) { $buckets[$bk][$pu.uid] = 0 } + $buckets[$bk][$pu.uid]++ +} +foreach ($bk in ($buckets.Keys | Sort-Object)) { + $s = ($buckets[$bk].GetEnumerator() | ForEach-Object { "$($_.Key.Substring(8))×$($_.Value)" }) -join " " + Write-Host (" t={0,3}-{1,-3}s: {2}" -f ($bk*15), ($bk*15+15), $s) +} diff --git a/scripts/session/analyze_set.ps1 b/scripts/session/analyze_set.ps1 new file mode 100644 index 0000000..62e4b89 --- /dev/null +++ b/scripts/session/analyze_set.ps1 @@ -0,0 +1,57 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +function H2B($hex) { + if (-not $hex) { return $null } + $n = $hex.Length / 2 + $b = New-Object byte[] $n + for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring(2*$i, 2), 16) } + return $b +} +function HexR($b, $o, $l) { if ($o + $l -gt $b.Length) { $l = $b.Length - $o }; return (($b[$o..($o+$l-1)] | ForEach-Object { $_.ToString("x2") }) -join " ") } +$lines = [IO.File]::ReadAllLines("$env:TEMP\opencode\set123.tsv") +$sets = New-Object System.Collections.ArrayList +$resps = New-Object System.Collections.ArrayList +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + $b = H2B $p[4]; if ($b.Length -lt 0x40) { continue } + $t = [double]$p[0] + # 主机→设备 事件帧, CC=0x30 (SET) @0x4F + if ($p[1] -eq "192.168.123.113" -and $b.Length -eq 1084 -and [int]$b[4] -eq 4 -and $b[0x20] -eq 2 -and $b[0x21] -eq 0 -and $b.Length -gt 0x52 -and $b[0x4F] -eq 0x30) { + [void]$sets.Add(@{ t=$t; b=$b }) + } + # 设备→主机 应答, CC=0x31 (SET_RESPONSE) @0x2A... 应答帧 RDM 区在 0x2A 起, CC@0x2A, msgLen@0x2B, ... CC字段在 0x2A + if ($p[1] -eq "192.168.123.1" -and $b.Length -lt 300 -and $b.Length -gt 0x30 -and $b[0x2A] -eq 0xCC -and $b[0x3A] -eq 0x31) { + [void]$resps.Add(@{ t=$t; b=$b }) + } +} +Write-Host ("SET 请求 (CC=0x30): {0} SET 应答 (CC=0x31): {1}" -f $sets.Count, $resps.Count) +Write-Host "`n=== SET 请求逐帧解析 ===" +foreach ($s in $sets) { + $b = $s.b + $flag = $b[0x30] + $rdmlen = [int]$b[0x34] + [int]$b[0x35]*256 + $uid = (HexR $b 0x3E 6) -replace " ","" + $tn = $b[0x4A] + $cc = $b[0x4F] + $pidH = "{0:x2}{1:x2}" -f $b[0x50], $b[0x51] + $pdl = [int]$b[0x52] + $pd = HexR $b 0x53 ([Math]::Min($pdl, 24)) + Write-Host ("t={0,8:F3} flag={1:x2} rdmlen={2} UID={3} TN={4:x2} CC={5:x2} PID={6} PDL={7} PD=[{8}]" -f $s.t, $flag, $rdmlen, $uid, $tn, $cc, $pidH, $pdl, $pd) +} +Write-Host "`n=== SET 应答逐帧解析 (CC=0x31) ===" +foreach ($r in $resps) { + $b = $r.b + $ml = [int]$b[0x2B] + $uid = (HexR $b 0x33 6) -replace " ","" + $tn = $b[0x39] + $rt = $b[0x3A] + $pidH = "{0:x2}{1:x2}" -f $b[0x3F], $b[0x40] + $pdl = [int]$b[0x41] + $pd = "" + if ($pdl -gt 0 -and $b.Length -gt 0x42) { $pd = HexR $b 0x42 ([Math]::Min($pdl, 16)) } + # checksum 校验 + $sum = 0; for ($i = 0x2A; $i -lt 0x2A + $ml; $i++) { $sum += $b[$i] } + $cs = ([int]$b[0x2A+$ml] -shl 8) -bor [int]$b[0x2A+$ml+1] + $ok = if (($sum -band 0xFFFF) -eq $cs) { "OK" } else { "BAD" } + Write-Host ("t={0,8:F3} len={1} UID={2} TN={3:x2} RT={4:x2} PID={5} PDL={6} PD=[{7}] cs={8}" -f $r.t, $b.Length, $uid, $tn, $rt, $pidH, $pdl, $pd, $ok) +} diff --git a/scripts/session/analyze_setb.ps1 b/scripts/session/analyze_setb.ps1 new file mode 100644 index 0000000..c6cdd0e --- /dev/null +++ b/scripts/session/analyze_setb.ps1 @@ -0,0 +1,57 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +function H2B($hex) { + if (-not $hex) { return $null } + $n = $hex.Length / 2 + $b = New-Object byte[] $n + for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring(2*$i, 2), 16) } + return $b +} +function HexR($b, $o, $l) { if ($o + $l -gt $b.Length) { $l = $b.Length - $o }; return (($b[$o..($o+$l-1)] | ForEach-Object { $_.ToString("x2") }) -join " ") } +$lines = [IO.File]::ReadAllLines("$env:TEMP\opencode\set123b.tsv") +$sets = New-Object System.Collections.ArrayList +$resps = New-Object System.Collections.ArrayList +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + $b = H2B $p[4]; if ($b.Length -lt 0x40) { continue } + $t = [double]$p[0] + # 主机→设备 事件帧, CC=0x30 (SET) @0x4F + if ($p[1] -eq "192.168.123.113" -and $b.Length -eq 1084 -and [int]$b[4] -eq 4 -and $b[0x20] -eq 2 -and $b[0x21] -eq 0 -and $b.Length -gt 0x52 -and $b[0x4F] -eq 0x30) { + [void]$sets.Add(@{ t=$t; b=$b }) + } + # 设备→主机 应答, CC=0x31 (SET_RESPONSE) @0x2A... 应答帧 RDM 区在 0x2A 起, CC@0x2A, msgLen@0x2B, ... CC字段在 0x2A + if ($p[1] -eq "192.168.123.1" -and $b.Length -lt 300 -and $b.Length -gt 0x30 -and $b[0x2A] -eq 0xCC -and $b[0x3A] -eq 0x31) { + [void]$resps.Add(@{ t=$t; b=$b }) + } +} +Write-Host ("SET 请求 (CC=0x30): {0} SET 应答 (CC=0x31): {1}" -f $sets.Count, $resps.Count) +Write-Host "`n=== SET 请求逐帧解析 ===" +foreach ($s in $sets) { + $b = $s.b + $flag = $b[0x30] + $rdmlen = [int]$b[0x34] + [int]$b[0x35]*256 + $uid = (HexR $b 0x3E 6) -replace " ","" + $tn = $b[0x4A] + $cc = $b[0x4F] + $pidH = "{0:x2}{1:x2}" -f $b[0x50], $b[0x51] + $pdl = [int]$b[0x52] + $pd = HexR $b 0x53 ([Math]::Min($pdl, 24)) + Write-Host ("t={0,8:F3} flag={1:x2} rdmlen={2} UID={3} TN={4:x2} CC={5:x2} PID={6} PDL={7} PD=[{8}]" -f $s.t, $flag, $rdmlen, $uid, $tn, $cc, $pidH, $pdl, $pd) +} +Write-Host "`n=== SET 应答逐帧解析 (CC=0x31) ===" +foreach ($r in $resps) { + $b = $r.b + $ml = [int]$b[0x2B] + $uid = (HexR $b 0x33 6) -replace " ","" + $tn = $b[0x39] + $rt = $b[0x3A] + $pidH = "{0:x2}{1:x2}" -f $b[0x3F], $b[0x40] + $pdl = [int]$b[0x41] + $pd = "" + if ($pdl -gt 0 -and $b.Length -gt 0x42) { $pd = HexR $b 0x42 ([Math]::Min($pdl, 16)) } + # checksum 校验 + $sum = 0; for ($i = 0x2A; $i -lt 0x2A + $ml; $i++) { $sum += $b[$i] } + $cs = ([int]$b[0x2A+$ml] -shl 8) -bor [int]$b[0x2A+$ml+1] + $ok = if (($sum -band 0xFFFF) -eq $cs) { "OK" } else { "BAD" } + Write-Host ("t={0,8:F3} len={1} UID={2} TN={3:x2} RT={4:x2} PID={5} PDL={6} PD=[{7}] cs={8}" -f $r.t, $b.Length, $uid, $tn, $rt, $pidH, $pdl, $pd, $ok) +} diff --git a/scripts/session/analyze_ss.ps1 b/scripts/session/analyze_ss.ps1 new file mode 100644 index 0000000..4b262fd --- /dev/null +++ b/scripts/session/analyze_ss.ps1 @@ -0,0 +1,157 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" + +function H2B($hex) { + if (-not $hex) { return $null } + $n = $hex.Length / 2 + $b = New-Object byte[] $n + for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring(2*$i, 2), 16) } + return $b +} +function UidStr($b, $o) { if ($b.Length -lt $o+6) { return "?" }; return ($b[$o..($o+5)] | ForEach-Object { $_.ToString("x2") }) -join "" } + +function Analyze($name, $tsv, $devIp, $hostIp) { + $lines = [IO.File]::ReadAllLines($tsv) + $t0 = 1e9; $t1 = 0 + foreach ($l in $lines) { $p = $l -split "`t"; if ($p.Count -lt 1 -or -not $p[0]) { continue }; $t = [double]$p[0]; if ($t -lt $t0) { $t0 = $t }; if ($t -gt $t1) { $t1 = $t } } + $dur = $t1 - $t0 + + $h2d = @{} # host->dev: 帧型 + $d2h = @{} # dev->host: 帧型 + $rdmCmd = @{} # host->dev RDM cmd 直方图 (flag=00) + $rdmPid = @{} + $queries = New-Object System.Collections.ArrayList # RDM 查询 (flag=00, cmd 18/19/1a/24) + $answers = New-Object System.Collections.ArrayList # dev->host RDM 短帧 + $dmxA = 0; $evtB = 0; $ack42 = 0 + $devUnsolicited = New-Object System.Collections.ArrayList # dev->host 42B 帧时间 + + foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + $b = H2B $p[4]; if ($b.Length -lt 0x1A) { continue } + $t = [double]$p[0] + $typ = [int]$b[4] + $isH2D = ($p[1] -eq $hostIp -and $p[2] -eq $devIp) + $isD2H = ($p[1] -eq $devIp -and $p[2] -eq $hostIp) + if (-not $isH2D -and -not $isD2H) { continue } + $len = $b.Length + if ($isH2D) { + $k = "t$typ|$len" + if (-not $h2d.ContainsKey($k)) { $h2d[$k] = 0 }; $h2d[$k]++ + if ($typ -eq 4 -and $len -gt 1000) { + if ($b[0x20] -eq 1 -and $b[0x21] -eq 1) { $dmxA++ } else { $evtB++ } + } + if ($typ -eq 4 -and $len -lt 2000 -and $len -gt 0x44 -and $b[0x30] -eq 0) { + $cmd = $b[0x3D] + if ($cmd -eq 0x24 -or $cmd -eq 0x18 -or $cmd -eq 0x19 -or $cmd -eq 0x1a) { + $ck = "cmd={0:x2}" -f $cmd + if (-not $rdmCmd.ContainsKey($ck)) { $rdmCmd[$ck] = 0 }; $rdmCmd[$ck]++ + $pidKey = "" + if ($cmd -ne 0x24 -and $b.Length -gt 0x51) { $pidKey = "{0:x2}{1:x2}" -f $b[0x50], $b[0x51] + if (-not $rdmPid.ContainsKey($pidKey)) { $rdmPid[$pidKey] = 0 }; $rdmPid[$pidKey]++ } + [void]$queries.Add(@{ t=$t; cmd=$cmd; pid=$pidKey; uid=(UidStr $b 0x3E); seq=([int]$b[0x22] + [int]$b[0x23]*256); tn=$b[0x4A]; len=$len }) + } + } + } else { + $k = "t$typ|$len" + if (-not $d2h.ContainsKey($k)) { $d2h[$k] = 0 }; $d2h[$k]++ + if ($typ -eq 4 -and $len -eq 42) { $ack42++; [void]$devUnsolicited.Add($t) } + if ($typ -eq 4 -and $len -lt 200 -and $len -gt 0x2C -and ($b[0x2A] -eq 0xCC -or $b[0x2A] -eq 0xAA)) { + [void]$answers.Add(@{ t=$t; b=$b; len=$len; seq=([int]$b[0x20] + [int]$b[0x21]*256); pulse=($b[0x2A] -eq 0xAA) }) + } + } + } + + Write-Host "`n############ $name ($devIp) 时长 $([int]$dur)s ############" + Write-Host "--- host→dev 帧型 (Top12) ---" + $h2d.GetEnumerator() | Sort-Object { [int]$_.Value } -Descending | Select-Object -First 12 | ForEach-Object { Write-Host (" {0} : {1} ({2:F1}/s)" -f $_.Key, $_.Value, ($_.Value / $dur)) } + Write-Host "--- dev→host 帧型 (Top12) ---" + $d2h.GetEnumerator() | Sort-Object { [int]$_.Value } -Descending | Select-Object -First 12 | ForEach-Object { Write-Host (" {0} : {1} ({2:F1}/s)" -f $_.Key, $_.Value, ($_.Value / $dur)) } + Write-Host ("--- DMX-A={0} ({1:F1}/s) 事件B={2} ({3:F1}/s) 42B回帧={4} ({5:F1}/s) ACK/事件B比={6:F2}" -f $dmxA, ($dmxA/$dur), $evtB, ($evtB/$dur), $ack42, ($ack42/$dur), $(if ($evtB -gt 0) { $ack42/$evtB } else { 0 })) + Write-Host "--- RDM 查询 cmd (flag=00) ---" + $rdmCmd.GetEnumerator() | Sort-Object { [int]$_.Value } -Descending | ForEach-Object { Write-Host (" {0} : {1}" -f $_.Key, $_.Value) } + Write-Host "--- RDM 查询 PID (cmd!=24) ---" + $rdmPid.GetEnumerator() | Sort-Object { [int]$_.Value } -Descending | Select-Object -First 15 | ForEach-Object { Write-Host (" PID={0} : {1}" -f $_.Key, $_.Value) } + + # 应答时延: cmd=18/19/1a 查询 → 200ms 内 seq 回显应答 + $lat = New-Object System.Collections.ArrayList + $paired = 0; $unpaired = 0 + foreach ($q in $queries) { + if ($q.cmd -eq 0x24) { continue } + $best = $null + foreach ($a in $answers) { + if ($a.pulse) { continue } + if ($a.t -lt $q.t -or $a.t -gt $q.t + 0.2) { continue } + if ($a.seq -eq $q.seq) { $best = $a; break } + } + if ($best) { $paired++; [void]$lat.Add([int](($best.t - $q.t) * 1000)) } else { $unpaired++ } + } + if ($lat.Count -gt 0) { + $ls = $lat | Sort-Object + Write-Host ("--- 参数应答时延: 配对={0} 未配对={1} min={2}ms 中位={3}ms max={4}ms" -f $paired, $unpaired, $ls[0], $ls[[int]($ls.Count/2)], $ls[-1]) + } else { + Write-Host ("--- 参数应答时延: 配对={0} 未配对={1} (无样本)" -f $paired, $unpaired) + } + # 脉冲时延 + $plat = New-Object System.Collections.ArrayList + $discQs = @($queries | Where-Object { $_.cmd -eq 0x24 }) + $pulses = @($answers | Where-Object { $_.pulse }) + foreach ($pu in $pulses) { + $best = $null + for ($i = $discQs.Count - 1; $i -ge 0; $i--) { if ($discQs[$i].t -le $pu.t) { $best = $discQs[$i]; break } } + if ($best -and ($pu.t - $best.t) -lt 0.5) { [void]$plat.Add([int](($pu.t - $best.t) * 1000)) } + } + if ($plat.Count -gt 0) { + $ps = $plat | Sort-Object + Write-Host ("--- 脉冲: {0} 个 (应答率 {1:F0}% of cmd24) 时延 min={2}ms 中位={3}ms max={4}ms" -f $pulses.Count, $(if ($discQs.Count -gt 0) { 100.0*$pulses.Count/$discQs.Count } else { 0 }), $ps[0], $ps[[int]($ps.Count/2)], $ps[-1]) + } + return @{ queries=$queries; answers=$answers } +} + +$r1 = Analyze "真机 .1" "$env:TEMP\opencode\ss123.tsv" "192.168.123.1" "192.168.123.113" +$r7 = Analyze "虚拟 .7" "$env:TEMP\opencode\ss58.tsv" "192.168.58.7" "192.168.58.107" + +# ============ cmd=19/1a 深挖: 布局解析 (用 .1 的样本) ============ +Write-Host "`n############ cmd=19/1a 布局深挖 (.1 样本) ############" +$cnt19 = 0; $cnt1a = 0 +foreach ($q in $r1.queries) { + if ($q.cmd -eq 0x19 -and $cnt19 -lt 2) { + Write-Host (" cmd=19 t={0:F3} UID={1} PID={2} TN={3:x2} len={4}" -f $q.t, $q.uid, $q.pid, $q.tn, $q.len) + $cnt19++ + } + if ($q.cmd -eq 0x1a -and $cnt1a -lt 2) { + Write-Host (" cmd=1a t={0:F3} UID={1} PID={2} TN={3:x2} len={4}" -f $q.t, $q.uid, $q.pid, $q.tn, $q.len) + $cnt1a++ + } +} +# cmd=19/1a 的 PID 分布 +$p19 = @{}; $p1a = @{} +foreach ($q in $r1.queries) { + if ($q.cmd -eq 0x19) { if (-not $p19.ContainsKey($q.pid)) { $p19[$q.pid] = 0 }; $p19[$q.pid]++ } + if ($q.cmd -eq 0x1a) { if (-not $p1a.ContainsKey($q.pid)) { $p1a[$q.pid] = 0 }; $p1a[$q.pid]++ } +} +Write-Host "--- cmd=19 PID 分布 (.1) ---" +$p19.GetEnumerator() | Sort-Object { [int]$_.Value } -Descending | Select-Object -First 10 | ForEach-Object { Write-Host (" PID={0} : {1}" -f $_.Key, $_.Value) } +Write-Host "--- cmd=1a PID 分布 (.1) ---" +$p1a.GetEnumerator() | Sort-Object { [int]$_.Value } -Descending | Select-Object -First 10 | ForEach-Object { Write-Host (" PID={0} : {1}" -f $_.Key, $_.Value) } + +# cmd=19 查询的完整 hex 样本 (从原始 tsv 找一条) +Write-Host "--- cmd=19 查询完整 hex (1 条) ---" +$lines = [IO.File]::ReadAllLines("$env:TEMP\opencode\ss123.tsv") +$done = 0 +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + if ($p[1] -ne "192.168.123.113" -or $p[2] -ne "192.168.123.1") { continue } + $b = H2B $p[4]; if ($b.Length -lt 0x60 -or $b.Length -gt 2000) { continue } + if ($b[0x30] -ne 0 -or $b[0x3D] -ne 0x19) { continue } + Write-Host (" len={0} hex[0x20..0x60]={1}" -f $b.Length, (($b[0x20..([Math]::Min($b.Length-1, 0x60))] | ForEach-Object { $_.ToString("x2") }) -join "")) + # 找其应答 + $seq = [int]$b[0x22] + [int]$b[0x23]*256 + foreach ($a in $r1.answers) { + if (-not $a.pulse -and $a.seq -eq $seq -and $a.t -ge [double]$p[0] -and $a.t -le [double]$p[0] + 0.2) { + Write-Host (" 应答 len={0}: {1}" -f $a.len, (($a.b | ForEach-Object { $_.ToString("x2") }) -join "")) + break + } + } + $done++; if ($done -ge 2) { break } +} +Write-Host "`n分析完成。" diff --git a/scripts/session/analyze_ss2.ps1 b/scripts/session/analyze_ss2.ps1 new file mode 100644 index 0000000..87bc5fe --- /dev/null +++ b/scripts/session/analyze_ss2.ps1 @@ -0,0 +1,81 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" + +function H2B($hex) { + if (-not $hex) { return $null } + $n = $hex.Length / 2 + $b = New-Object byte[] $n + for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring(2*$i, 2), 16) } + return $b +} +function UidStr($b, $o) { if ($b.Length -lt $o+6) { return "?" }; return ($b[$o..($o+5)] | ForEach-Object { $_.ToString("x2") }) -join "" } + +function Analyze2($name, $tsv, $devIp, $hostIp) { + $lines = [IO.File]::ReadAllLines($tsv) + Write-Host "`n############ $name ($devIp) ############" + # 真实 RDM 查询: 0x20-0x21 == 0200 (事件B帧), flag 0x30==0 + $rdmCmd = @{}; $rdmPid = @{}; $rdmPidUid = @{} + $falsePos = 0 + $bFrames = New-Object System.Collections.ArrayList # 所有事件B帧 (时间, 0x22序号, 0x3C, 0x3D) + foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + if ($p[1] -ne $hostIp -or $p[2] -ne $devIp) { continue } + $b = H2B $p[4]; if ($b.Length -lt 0x52 -or $b.Length -gt 2000) { continue } + if ([int]$b[4] -ne 4) { continue } + $t = [double]$p[0] + $isB = ($b[0x20] -eq 2 -and $b[0x21] -eq 0) + $isA = ($b[0x20] -eq 1 -and $b[0x21] -eq 1) + if ($isB -and $b.Length -gt 0x44) { + [void]$bFrames.Add(@{ t=$t; seq=([int]$b[0x22]+[int]$b[0x23]*256); c3c=$b[0x3C]; c3d=$b[0x3D]; flag=$b[0x30] }) + } + # RDM 判定: 必须 0x3C==01 且 cmd 匹配 + if ($b.Length -gt 0x44 -and $b[0x3C] -eq 1 -and ($b[0x3D] -eq 0x24 -or $b[0x3D] -eq 0x18 -or $b[0x3D] -eq 0x19 -or $b[0x3D] -eq 0x1a)) { + if ($b[0x30] -ne 0) { continue } + if ($isA) { $falsePos++; continue } # DMX-A 帧通道数据巧合 = 假阳性 + $cmd = $b[0x3D] + $ck = "cmd={0:x2}" -f $cmd + if (-not $rdmCmd.ContainsKey($ck)) { $rdmCmd[$ck] = 0 }; $rdmCmd[$ck]++ + if ($cmd -ne 0x24 -and $b.Length -gt 0x51) { + $pidKey = "{0:x2}{1:x2}" -f $b[0x50], $b[0x51] + $uid = UidStr $b 0x3E + if (-not $rdmPid.ContainsKey($pidKey)) { $rdmPid[$pidKey] = 0 }; $rdmPid[$pidKey]++ + $k2 = "$pidKey->$uid"; if (-not $rdmPidUid.ContainsKey($k2)) { $rdmPidUid[$k2] = 0 }; $rdmPidUid[$k2]++ + } + } + } + Write-Host ("--- 真实 RDM 查询 (排除 DMX-A 假阳性 {0} 条) ---" -f $falsePos) + $rdmCmd.GetEnumerator() | Sort-Object { [int]$_.Value } -Descending | ForEach-Object { Write-Host (" {0} : {1}" -f $_.Key, $_.Value) } + Write-Host "--- PID 分布 ---" + $rdmPid.GetEnumerator() | Sort-Object { [int]$_.Value } -Descending | Select-Object -First 12 | ForEach-Object { Write-Host (" PID={0} : {1}" -f $_.Key, $_.Value) } + Write-Host "--- PID->UID 细分 (0002/0003/0060/00f0/0201) ---" + $rdmPidUid.GetEnumerator() | Where-Object { $_.Name -match "^(0002|0003|0060|00f0|0201)" } | Sort-Object Name | ForEach-Object { Write-Host (" {0} : {1}" -f $_.Name, $_.Value) } + + # 42B ACK 相关性: 每个 42B 帧之前 200ms 内最近的 B 帧特征 + $acks = New-Object System.Collections.ArrayList + foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5 -or -not $p[4]) { continue } + if ($p[1] -ne $devIp -or $p[2] -ne $hostIp) { continue } + $b = H2B $p[4]; if ($b.Length -ne 42 -or [int]$b[4] -ne 4) { continue } + [void]$acks.Add(@{ t=([double]$p[0]); seq=([int]$b[0x20]+[int]$b[0x21]*256) }) + } + $ackByB = @{} + $ackNoB = 0 + foreach ($a in $acks) { + # 找 seq == ack.seq+1 的 B 帧 (§4.3: ACK seq = 事件 seq - 1) + $found = $null + foreach ($bf in $bFrames) { + if ($bf.seq -eq $a.seq + 1) { $found = $bf; break } + } + if ($found) { + $k = "0x3C={0:x2} 0x3D={1:x2} flag={2:x2}" -f $found.c3c, $found.c3d, $found.flag + if (-not $ackByB.ContainsKey($k)) { $ackByB[$k] = 0 }; $ackByB[$k]++ + } else { $ackNoB++ } + } + Write-Host ("--- 42B ACK ({0}个) 对应的 B 帧特征 ---" -f $acks.Count) + $ackByB.GetEnumerator() | Sort-Object { [int]$_.Value } -Descending | Select-Object -First 8 | ForEach-Object { Write-Host (" {0} : {1}" -f $_.Key, $_.Value) } + Write-Host (" (无配对 B 帧: {0})" -f $ackNoB) +} + +Analyze2 "真机 .1" "$env:TEMP\opencode\ss123.tsv" "192.168.123.1" "192.168.123.113" +Analyze2 "虚拟 .7" "$env:TEMP\opencode\ss58.tsv" "192.168.58.7" "192.168.58.107" +Write-Host "`n分析完成。" diff --git a/scripts/session/analyze_startup.ps1 b/scripts/session/analyze_startup.ps1 new file mode 100644 index 0000000..2ec6606 --- /dev/null +++ b/scripts/session/analyze_startup.ps1 @@ -0,0 +1,14 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$cap = "$env:TEMP\opencode\sym_startup.pcap" +$ts = "C:\Program Files\Wireshark\tshark.exe" +Write-Host "=== claim frames (ident 110 / 47B 55 / AECH 560) ===" +& $ts -r $cap -Y "udp.length==110 or udp.length==55 or udp.length==560" -T fields -e frame.time_relative -e ip.src -e ip.dst -e udp.length 2>$null | Select-Object -First 12 +Write-Host "" +Write-Host "=== host-sent frame types (all, until +30s after claim window end) ===" +$all = @(& $ts -r $cap -Y "ip.src==192.168.123.113" -T fields -e frame.time_relative -e ip.dst -e udp.length 2>$null) +$g = @{} +foreach ($l in $all) { $p = $l -split "`t"; $k = $p[1] + " len=" + $p[2]; if (-not $g.ContainsKey($k)) { $g[$k] = New-Object System.Collections.ArrayList }; [void]$g[$k].Add([double]$p[0]) } +foreach ($k in ($g.Keys | Sort-Object)) { + $arr = $g[$k] + Write-Host ("{0,-30} n={1,-6} first={2:N1}s last={3:N1}s" -f $k, $arr.Count, $arr[0], $arr[$arr.Count-1]) +} diff --git a/scripts/session/cap_hostdemo.ps1 b/scripts/session/cap_hostdemo.ps1 new file mode 100644 index 0000000..04f1984 --- /dev/null +++ b/scripts/session/cap_hostdemo.ps1 @@ -0,0 +1,11 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$cap = "$env:TEMP\opencode\hd1.pcap" +Remove-Item $cap -ErrorAction SilentlyContinue +$ts = "C:\Program Files\Wireshark\tshark.exe" +$proc = Start-Process -FilePath $ts -ArgumentList "-i `"\Device\NPF_{E7165B1C-BAF6-49CF-9A3D-EF12DBC17329}`" -f `"udp port 31772`" -a duration:40 -w `"$cap`"" -PassThru -WindowStyle Hidden -RedirectStandardError "$env:TEMP\opencode\tshark_err.txt" +Start-Sleep -Seconds 3 +Set-Location "C:\Users\tonycao\work\light\enet-validator" +& .\target\release\enet-validator.exe hostdemo 192.168.123.1 0 +Start-Sleep -Seconds 30 +Write-Host "`n=== 抓包全部帧 (time src dst udplen) ===" +& $ts -r $cap -T fields -e frame.time_relative -e ip.src -e ip.dst -e udp.length 2>$null | Select-Object -First 60 diff --git a/scripts/session/cat_runbutler.ps1 b/scripts/session/cat_runbutler.ps1 new file mode 100644 index 0000000..bb08756 --- /dev/null +++ b/scripts/session/cat_runbutler.ps1 @@ -0,0 +1,9 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +$r = Invoke-SSHCommand -SSHSession $s -Command "cat ~/run_butler.sh" +Write-Host ($r.Output -join "`n") +Remove-SSHSession $s | Out-Null diff --git a/scripts/session/check_desc.ps1 b/scripts/session/check_desc.ps1 new file mode 100644 index 0000000..dcf46ef --- /dev/null +++ b/scripts/session/check_desc.ps1 @@ -0,0 +1,25 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$lines = [IO.File]::ReadAllLines("$env:TEMP\opencode\ss123.tsv") +$checked = 0; $allDesc = 0 +foreach ($ln in $lines) { + $p = $ln -split "`t" + if ($p.Count -lt 5 -or $p[4].Length -lt 100) { continue } + $hex = $p[4] -replace '[^0-9a-fA-F]','' + if ($hex.Length -ne 2168) { continue } + $b = New-Object byte[] 1084 + for ($i = 0; $i -lt 1084; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring($i*2,2),16) } + if ($b[0x20] -ne 1 -or $b[0x21] -ne 1) { continue } + $checked++ + if ($checked -gt 200) { break } + $ok = $true + foreach ($base in @(0x3C, 0x23C)) { + for ($i = 0; $i -lt 511; $i++) { + $d = ($b[$base+$i] - $b[$base+$i+1]) -band 0xFF + if ($d -ne 1) { $ok = $false; break } + } + if (-not $ok) { break } + } + if ($ok) { $allDesc++ } +} +echo "检查帧: $checked, 全 512B 严格递减(mod256): $allDesc" +# 若成立 → 数据=加密后的全零, rot 可从首字节推出 diff --git a/scripts/session/check_fix1_aech.ps1 b/scripts/session/check_fix1_aech.ps1 new file mode 100644 index 0000000..0477ab7 --- /dev/null +++ b/scripts/session/check_fix1_aech.ps1 @@ -0,0 +1,12 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$raw = [IO.File]::ReadAllBytes("$env:TEMP\opencode\fix1.tsv") +$text = if ($raw[0] -eq 0xFF -and $raw[1] -eq 0xFE) { [Text.Encoding]::Unicode.GetString($raw) } else { [Text.Encoding]::UTF8.GetString($raw) } +$lines = $text -split "`r?`n" +foreach ($l in $lines) { + $p = $l -split "`t" + if ($p.Count -lt 5 -or -not $p[4]) { continue } + $h = $p[4] -replace " ", "" + if ($h.Length -lt 100) { continue } + if ($h.Substring(0x28 * 2, 8) -eq "41454348") { Write-Host ("AECH t={0,9} src={1} len={2}" -f $p[0], $p[1], ($h.Length/2)) } + if ($h.Substring(0x28 * 2, 8) -eq "41455245") { Write-Host ("AERE t={0,9} src={1} len={2} c0={3}" -f $p[0], $p[1], ($h.Length/2), $h.Substring(0xC0*2,8)) } +} diff --git a/scripts/session/check_log.ps1 b/scripts/session/check_log.ps1 new file mode 100644 index 0000000..9bc9558 --- /dev/null +++ b/scripts/session/check_log.ps1 @@ -0,0 +1,9 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +$r = Invoke-SSHCommand -SSHSession $s -Command "tail -15 ~/butler.log" +Write-Host ($r.Output -join "`n") +Remove-SSHSession $s | Out-Null diff --git a/scripts/session/check_log2.ps1 b/scripts/session/check_log2.ps1 new file mode 100644 index 0000000..4c2faab --- /dev/null +++ b/scripts/session/check_log2.ps1 @@ -0,0 +1,9 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +$r = Invoke-SSHCommand -SSHSession $s -Command "grep -E 'AECH|注册|RDM|心跳' ~/butler.log | tail -20" +Write-Host ($r.Output -join "`n") +Remove-SSHSession $s | Out-Null diff --git a/scripts/session/check_log3.ps1 b/scripts/session/check_log3.ps1 new file mode 100644 index 0000000..0137fde --- /dev/null +++ b/scripts/session/check_log3.ps1 @@ -0,0 +1,9 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +$r = Invoke-SSHCommand -SSHSession $s -Command "pgrep -af 'enet-validator device' ; echo '---'; tail -8 ~/butler.log ; echo '---'; grep -c 'RDM-REQ' ~/butler.log" +Write-Host ($r.Output -join "`n") +Remove-SSHSession $s | Out-Null diff --git a/scripts/session/check_log4.ps1 b/scripts/session/check_log4.ps1 new file mode 100644 index 0000000..d712ddb --- /dev/null +++ b/scripts/session/check_log4.ps1 @@ -0,0 +1,9 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +$r = Invoke-SSHCommand -SSHSession $s -Command "wc -l ~/butler.log; echo '---'; grep -vE '发现请求 type1' ~/butler.log | tail -30" +Write-Host ($r.Output -join "`n") +Remove-SSHSession $s | Out-Null diff --git a/scripts/session/check_log5.ps1 b/scripts/session/check_log5.ps1 new file mode 100644 index 0000000..a825c24 --- /dev/null +++ b/scripts/session/check_log5.ps1 @@ -0,0 +1,9 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +$r = Invoke-SSHCommand -SSHSession $s -Command "wc -l ~/butler.log; echo '---'; grep -vE 'DMX #' ~/butler.log | tail -25" +Write-Host ($r.Output -join "`n") +Remove-SSHSession $s | Out-Null diff --git a/scripts/session/check_proc.ps1 b/scripts/session/check_proc.ps1 new file mode 100644 index 0000000..571dcd5 --- /dev/null +++ b/scripts/session/check_proc.ps1 @@ -0,0 +1,11 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +if (-not $s) { throw "SSH connect failed" } +$r = Invoke-SSHCommand -SSHSession $s -Command "pgrep -af enet; echo ---; ls -la ~/butler.log; echo ---; tail -n 3 ~/butler.log; echo ---; ss -lunp 2>/dev/null | grep 31772" +$r.Output +Remove-SSHSession -SSHSession $s | Out-Null diff --git a/scripts/session/check_proc2.ps1 b/scripts/session/check_proc2.ps1 new file mode 100644 index 0000000..e5e6b10 --- /dev/null +++ b/scripts/session/check_proc2.ps1 @@ -0,0 +1,9 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +$r = Invoke-SSHCommand -SSHSession $s -Command "pgrep -af 'enet-validator device'; echo '---'; head -5 ~/butler.log" +Write-Host ($r.Output -join "`n") +Remove-SSHSession $s | Out-Null diff --git a/scripts/session/check_procs.ps1 b/scripts/session/check_procs.ps1 new file mode 100644 index 0000000..5c3020f --- /dev/null +++ b/scripts/session/check_procs.ps1 @@ -0,0 +1,9 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +$r = Invoke-SSHCommand -SSHSession $s -Command "pgrep -af enet-validator; echo '---'; ss -unp | grep 31772" +Write-Host ($r.Output -join "`n") +Remove-SSHSession $s | Out-Null diff --git a/scripts/session/check_set.ps1 b/scripts/session/check_set.ps1 new file mode 100644 index 0000000..9faf0e3 --- /dev/null +++ b/scripts/session/check_set.ps1 @@ -0,0 +1,9 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +$r = Invoke-SSHCommand -SSHSession $s -Command "grep -E '\[RDM\] SET' ~/butler.log | tail -10; echo '---'; grep -E 'PID=00f0' ~/butler.log | tail -5" +Write-Host ($r.Output -join "`n") +Remove-SSHSession $s | Out-Null diff --git a/scripts/session/check_status.ps1 b/scripts/session/check_status.ps1 new file mode 100644 index 0000000..1a070b3 --- /dev/null +++ b/scripts/session/check_status.ps1 @@ -0,0 +1,11 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +if (-not $s) { throw "SSH connect failed" } +$r = Invoke-SSHCommand -SSHSession $s -Command "pgrep -af enet-validator; echo ---; tail -n 8 ~/butler.log" +$r.Output +Remove-SSHSession -SSHSession $s | Out-Null diff --git a/scripts/session/check_ubuntu_git.ps1 b/scripts/session/check_ubuntu_git.ps1 new file mode 100644 index 0000000..3ba6809 --- /dev/null +++ b/scripts/session/check_ubuntu_git.ps1 @@ -0,0 +1,10 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +if (-not $s) { throw "SSH connect failed" } +$r = Invoke-SSHCommand -SessionId $s.SessionId -Command "which git && git --version && ls ~/enet-validator 2>/dev/null | head -5" +$r.Output +Remove-SSHSession -SessionId $s.SessionId | Out-Null diff --git a/scripts/session/cold2_order.ps1 b/scripts/session/cold2_order.ps1 new file mode 100644 index 0000000..960c4dc --- /dev/null +++ b/scripts/session/cold2_order.ps1 @@ -0,0 +1,24 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$f = "$env:TEMP\opencode\cold2.tsv" +$lines = [IO.File]::ReadAllLines($f) +function H2B($hex) { $n = $hex.Length / 2; $b = New-Object byte[] $n; for ($i = 0; $i -lt $n; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring(2*$i, 2), 16) }; return $b } + +$firstAECH = $null; $firstDMX = $null; $first42 = $null; $firstRDMreq = $null; $firstRDMresp = $null; $firstTele = $null +foreach ($l in $lines) { + $p = $l -split "`t"; if ($p.Count -lt 5) { continue } + $b = H2B $p[4]; $t = [double]$p[0] + $from113 = $p[1] -eq "192.168.123.113" + $from1 = $p[1] -eq "192.168.123.1" + if ($from113 -and $b.Length -eq 552 -and -not $firstAECH) { $firstAECH = $t; Write-Host ("first AECH challenge t=$t") } + if ($from113 -and $b.Length -eq 1084) { + $isRDM = ($b[0x3C] -eq 1 -and $b[0x30] -eq 0) + $isDMX = ($b[0x30] -eq 0 -and $b[0x2A] -eq 0) # 0x2A=00 => DMX data frame? from earlier: type4 flag=00 0x2A=00 + if ($isRDM -and -not $firstRDMreq) { $firstRDMreq = $t; Write-Host ("first RDM req(flag00) t=$t") } + if (-not $isRDM -and -not $firstDMX) { $firstDMX = $t; Write-Host ("first DMX frame t=$t (0x2A=$($b[0x2A].ToString('x2')) flag=$($b[0x30].ToString('x2')))") } + } + if ($from1 -and $b.Length -eq 42 -and -not $first42) { $first42 = $t; Write-Host ("first 42B telemetry t=$t") } + if ($from1 -and $b.Length -ge 0x2E -and $b.Length -lt 200 -and ($b[0x2A] -eq 0xCC -or $b[0x2A] -eq 0xAA) -and -not $firstRDMresp) { $firstRDMresp = $t; Write-Host ("first RDM response t=$t len=$($b.Length)") } + if ($firstAECH -and $firstDMX -and $first42 -and $firstRDMreq -and $firstRDMresp) { break } +} +Write-Host "" +Write-Host ("order: AECH=$firstAECH RDMreq=$firstRDMreq DMX=$firstDMX 42B=$first42 RDMresp=$firstRDMresp") diff --git a/scripts/session/deploy_g.ps1 b/scripts/session/deploy_g.ps1 new file mode 100644 index 0000000..955a9b1 --- /dev/null +++ b/scripts/session/deploy_g.ps1 @@ -0,0 +1,38 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +if (-not $s) { throw "SSH connect failed" } + +$files = @( + @("C:\Users\tonycao\work\light\enet-validator\src\tests.rs", "~/enet-validator/src/tests.rs"), + @("C:\Users\tonycao\work\light\enet-validator\src\aech.rs", "~/enet-validator/src/aech.rs"), + @("C:\Users\tonycao\work\light\enet-validator\src\main.rs", "~/enet-validator/src/main.rs"), + @("C:\Users\tonycao\work\light\enet-validator\src\dmxhash.rs", "~/enet-validator/src/dmxhash.rs") +) +foreach ($f in $files) { + $src = $f[0]; $dst = $f[1] + $b64 = [Convert]::ToBase64String([IO.File]::ReadAllBytes($src)) + $chunkSize = 6000 + $chunks = [Math]::Ceiling($b64.Length / $chunkSize) + $r = Invoke-SSHCommand -SSHSession $s -Command "rm -f /tmp/up.b64 && touch /tmp/up.b64" + for ($i = 0; $i -lt $chunks; $i++) { + $part = $b64.Substring($i * $chunkSize, [Math]::Min($chunkSize, $b64.Length - $i * $chunkSize)) + $r = Invoke-SSHCommand -SSHSession $s -Command "echo '$part' >> /tmp/up.b64" + } + $r = Invoke-SSHCommand -SSHSession $s -Command "base64 -d /tmp/up.b64 > $dst && md5sum $dst" + $localMd5 = (Get-FileHash $src -Algorithm MD5).Hash.ToLower() + $remoteMd5 = ($r.Output -join "").Split(" ")[0] + Write-Host ("{0}: {1}" -f (Split-Path $src -Leaf), $(if ($localMd5 -eq $remoteMd5) { "OK" } else { "MISMATCH!" })) +} + +Write-Host "--- build on .7 ---" +$r = Invoke-SSHCommand -SSHSession $s -Command "cd ~/enet-validator && ~/.cargo/bin/cargo build --release 2>&1 | tail -2" -TimeOut 300 +Write-Host ($r.Output -join "`n") +Write-Host "--- restart butler (NO relay - local G) ---" +$r = Invoke-SSHCommand -SSHSession $s -Command "pkill -f 'enet-validator device'; sleep 1; cd ~/enet-validator && nohup ./target/release/enet-validator device 192.168.58.7 00161cf12e88 86400 > ~/butler.log 2>&1 & sleep 2; pgrep -af 'enet-validator device' | head -1" +Write-Host ($r.Output -join "`n") +Remove-SSHSession $s | Out-Null diff --git a/scripts/session/deploy_set.ps1 b/scripts/session/deploy_set.ps1 new file mode 100644 index 0000000..d1fe96b --- /dev/null +++ b/scripts/session/deploy_set.ps1 @@ -0,0 +1,39 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +if (-not $s) { throw "SSH 连接失败" } + +$files = @( + @("C:\Users\tonycao\work\light\enet-validator\src\tests.rs", "~/enet-validator/src/tests.rs"), + @("C:\Users\tonycao\work\light\enet-validator\src\enet.rs", "~/enet-validator/src/enet.rs"), + @("C:\Users\tonycao\work\light\enet-validator\samples\rdm_setack_68.hex", "~/enet-validator/samples/rdm_setack_68.hex") +) +foreach ($f in $files) { + $src = $f[0]; $dst = $f[1] + $b64 = [Convert]::ToBase64String([IO.File]::ReadAllBytes($src)) + $chunkSize = 6000 + $chunks = [Math]::Ceiling($b64.Length / $chunkSize) + $r = Invoke-SSHCommand -SSHSession $s -Command "rm -f /tmp/up.b64 && touch /tmp/up.b64" + for ($i = 0; $i -lt $chunks; $i++) { + $part = $b64.Substring($i * $chunkSize, [Math]::Min($chunkSize, $b64.Length - $i * $chunkSize)) + $r = Invoke-SSHCommand -SSHSession $s -Command "echo '$part' >> /tmp/up.b64" + } + $r = Invoke-SSHCommand -SSHSession $s -Command "base64 -d /tmp/up.b64 > $dst && md5sum $dst" + $localMd5 = (Get-FileHash $src -Algorithm MD5).Hash.ToLower() + $remoteMd5 = ($r.Output -join "").Split(" ")[0] + $ok = if ($localMd5 -eq $remoteMd5) { "OK" } else { "MISMATCH!" } + Write-Host ("{0} -> {1}: {2} ({3})" -f (Split-Path $src -Leaf), $dst, $ok, $localMd5) +} + +Write-Host "--- .7 编译 ---" +$r = Invoke-SSHCommand -SSHSession $s -Command "cd ~/enet-validator && ~/.cargo/bin/cargo build --release 2>&1 | tail -3" -TimeOut 300 +Write-Host ($r.Output -join "`n") + +Write-Host "--- 重启 butler ---" +$r = Invoke-SSHCommand -SSHSession $s -Command "bash ~/run_butler.sh device 2>&1 | tail -5; sleep 2; pgrep -af enet-validator | head -3" +Write-Host ($r.Output -join "`n") +Remove-SSHSession $s | Out-Null diff --git a/scripts/session/deploy_set2.ps1 b/scripts/session/deploy_set2.ps1 new file mode 100644 index 0000000..b29f1a4 --- /dev/null +++ b/scripts/session/deploy_set2.ps1 @@ -0,0 +1,31 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +if (-not $s) { throw "SSH connect failed" } + +$src = "C:\Users\tonycao\work\light\enet-validator\src\tests.rs" +$b64 = [Convert]::ToBase64String([IO.File]::ReadAllBytes($src)) +$chunkSize = 6000 +$chunks = [Math]::Ceiling($b64.Length / $chunkSize) +$r = Invoke-SSHCommand -SSHSession $s -Command "rm -f /tmp/up.b64 && touch /tmp/up.b64" +for ($i = 0; $i -lt $chunks; $i++) { + $part = $b64.Substring($i * $chunkSize, [Math]::Min($chunkSize, $b64.Length - $i * $chunkSize)) + $r = Invoke-SSHCommand -SSHSession $s -Command "echo '$part' >> /tmp/up.b64" +} +$r = Invoke-SSHCommand -SSHSession $s -Command "base64 -d /tmp/up.b64 > ~/enet-validator/src/tests.rs && md5sum ~/enet-validator/src/tests.rs" +$localMd5 = (Get-FileHash $src -Algorithm MD5).Hash.ToLower() +$remoteMd5 = ($r.Output -join "").Split(" ")[0] +Write-Host ("tests.rs sync: {0} ({1})" -f $(if ($localMd5 -eq $remoteMd5) { "OK" } else { "MISMATCH!" }), $localMd5) + +Write-Host "--- build on .7 ---" +$r = Invoke-SSHCommand -SSHSession $s -Command "cd ~/enet-validator && ~/.cargo/bin/cargo build --release 2>&1 | tail -2" -TimeOut 300 +Write-Host ($r.Output -join "`n") + +Write-Host "--- restart butler ---" +$r = Invoke-SSHCommand -SSHSession $s -Command "bash ~/run_butler.sh device 2>&1 | tail -3; sleep 2; pgrep -af 'enet-validator device' | head -2" +Write-Host ($r.Output -join "`n") +Remove-SSHSession $s | Out-Null diff --git a/scripts/session/deploy_tests.ps1 b/scripts/session/deploy_tests.ps1 new file mode 100644 index 0000000..8f2ae9c --- /dev/null +++ b/scripts/session/deploy_tests.ps1 @@ -0,0 +1,29 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +if (-not $s) { throw "SSH 连接失败" } + +$src = "C:\Users\tonycao\work\light\enet-validator\src\tests.rs" +$b64 = [Convert]::ToBase64String([IO.File]::ReadAllBytes($src)) +Write-Host ("本地文件 {0} 字节, base64 {1} 字符" -f (Get-Item $src).Length, $b64.Length) + +# 分块传输 +$chunkSize = 6000 +$chunks = [Math]::Ceiling($b64.Length / $chunkSize) +$r = Invoke-SSHCommand -SSHSession $s -Command "rm -f /tmp/tests.b64 && touch /tmp/tests.b64 && echo OK" +for ($i = 0; $i -lt $chunks; $i++) { + $start = $i * $chunkSize + $len = [Math]::Min($chunkSize, $b64.Length - $start) + $part = $b64.Substring($start, $len) + $r = Invoke-SSHCommand -SSHSession $s -Command "echo '$part' >> /tmp/tests.b64 && wc -c < /tmp/tests.b64" + if (($i % 3) -eq 0) { Write-Host (" chunk {0}/{1}: {2}" -f ($i+1), $chunks, ($r.Output -join "")) } +} +$r = Invoke-SSHCommand -SSHSession $s -Command "base64 -d /tmp/tests.b64 > ~/enet-validator/src/tests.rs && wc -c < ~/enet-validator/src/tests.rs && md5sum ~/enet-validator/src/tests.rs" +Write-Host ("解码: " + ($r.Output -join " | ")) +$localMd5 = (Get-FileHash $src -Algorithm MD5).Hash.ToLower() +Write-Host ("本地MD5: $localMd5") +Remove-SSHSession $s | Out-Null diff --git a/scripts/session/diff_ann2.ps1 b/scripts/session/diff_ann2.ps1 new file mode 100644 index 0000000..f18bf3a --- /dev/null +++ b/scripts/session/diff_ann2.ps1 @@ -0,0 +1,49 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$cap = "$env:TEMP\opencode\sym_startup.pcap" +$ts = "C:\Program Files\Wireshark\tshark.exe" +$tpl = (Get-Content "C:\Users\tonycao\work\light\enet-validator\samples\announce_686.hex" -Raw).Trim() -replace '\s','' +# 取两帧 live announce (不同时刻) +$anns = @(& $ts -r $cap -Y "udp.length==652" -T fields -e frame.time_relative -e udp.payload 2>$null | Select-Object -First 2) +$h0 = ($anns[0] -split "`t")[1]; $t0 = ($anns[0] -split "`t")[0] +$h1 = ($anns[1] -split "`t")[1]; $t1 = ($anns[1] -split "`t")[0] +Write-Host ("template len=" + $tpl.Length/2 + " live len=" + $h0.Length/2) +Write-Host "" +Write-Host "=== template vs liveA(t=$t0) 差异区域 ===" +$n = [Math]::Min($tpl.Length, $h0.Length) +$regions = @() +for ($i = 0; $i -lt $n; $i += 2) { + if ($tpl.Substring($i,2) -ne $h0.Substring($i,2)) { + $off = $i/2 + if ($regions.Count -eq 0 -or ($off - $regions[$regions.Count-1][1]) -gt 4) { + [void]$regions.Add(@($off, $off)) + } else { $regions[$regions.Count-1][1] = $off } + } +} +foreach ($r in $regions) { + $s = [Math]::Max(0, $r[0]-3); $e = [Math]::Min($tpl.Length/2-1, $r[1]+3) + $st=''; $sl=''; $sr='' + for ($i=$s; $i -le $e; $i++) { + $st += $tpl.Substring(2*$i,2) + $sl += $h0.Substring(2*$i,2) + if ($i -le $h1.Length/2-1) { $sr += $h1.Substring(2*$i,2) } else { $sr += '??' } + } + Write-Host ("0x{0:x4}-0x{1:x4}: tpl={2} liveA={3} liveB={4}" -f $s, $e, $st, $sl, $sr) +} +# liveA vs liveB (两帧实时宣告间的差异 = 每次宣告都变的字段) +Write-Host "" +Write-Host "=== liveA(t=$t0) vs liveB(t=$t1) 差异 (每次宣告变化的字段) ===" +$d2 = @() +for ($i = 0; $i -lt $n; $i += 2) { + if ($h0.Substring($i,2) -ne $h1.Substring($i,2)) { + $off = $i/2 + if ($d2.Count -eq 0 -or ($off - $d2[$d2.Count-1][1]) -gt 4) { [void]$d2.Add(@($off,$off)) } else { $d2[$d2.Count-1][1] = $off } + } +} +foreach ($r in $d2) { + $s = [Math]::Max(0, $r[0]-3); $e = [Math]::Min($h0.Length/2-1, $r[1]+3) + $sl=''; $sr='' + for ($i=$s; $i -le $e; $i++) { $sl += $h0.Substring(2*$i,2); $sr += $h1.Substring(2*$i,2) } + Write-Host ("0x{0:x4}-0x{1:x4}: A={2} B={3}" -f $s, $e, $sl, $sr) +} +Set-Content -Encoding ascii "$env:TEMP\opencode\announce_live.hex" $h0 +Write-Host "saved: announce_live.hex" diff --git a/scripts/session/diff_ann3.ps1 b/scripts/session/diff_ann3.ps1 new file mode 100644 index 0000000..d46f09a --- /dev/null +++ b/scripts/session/diff_ann3.ps1 @@ -0,0 +1,24 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$tpl = (Get-Content "C:\Users\tonycao\work\light\enet-validator\samples\announce_686.hex" -Raw).Trim() -replace '\s','' +$h0 = (Get-Content "$env:TEMP\opencode\announce_live.hex" -Raw).Trim() -replace '\s','' +Write-Host ("template len=" + $tpl.Length/2 + " live len=" + $h0.Length/2) +$diffs = New-Object 'System.Collections.Generic.List[int]' +$n = [Math]::Min($tpl.Length, $h0.Length) +for ($i = 0; $i -lt $n; $i += 2) { + if ($tpl.Substring($i,2) -ne $h0.Substring($i,2)) { $diffs.Add([int]($i/2)) } +} +Write-Host ("differing bytes: " + $diffs.Count) +# 分组连续区域 +$groups = New-Object 'System.Collections.Generic.List[object]' +foreach ($d in $diffs) { + if ($groups.Count -gt 0 -and ($d - $groups[$groups.Count-1][1]) -le 6) { $groups[$groups.Count-1][1] = $d } + else { $null = $groups.Add([pscustomobject]@{S=$d; E=$d}) } +} +foreach ($g in $groups) { + $s = [Math]::Max(0, $g.S-4); $e = [Math]::Min($n/2-1, $g.E+4) + $st=''; $sl='' + for ($i=$s; $i -le $e; $i++) { $st += $tpl.Substring(2*$i,2); $sl += $h0.Substring(2*$i,2) } + Write-Host ("0x{0:x4}-0x{1:x4}:" -f $s, $e) + Write-Host (" tpl = " + $st) + Write-Host (" live= " + $sl) +} diff --git a/scripts/session/diff_announce.ps1 b/scripts/session/diff_announce.ps1 new file mode 100644 index 0000000..48f2c1f --- /dev/null +++ b/scripts/session/diff_announce.ps1 @@ -0,0 +1,31 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$cap = "$env:TEMP\opencode\sym_claim2.pcap" +$ts = "C:\Program Files\Wireshark\tshark.exe" +# 提取两个不同时刻的 announce (644B payload = 652 udplen) hex +$anns = @(& $ts -r $cap -Y "udp.length==652" -T fields -e frame.time_relative -e udp.payload 2>$null | Select-Object -First 2) +Write-Host ("announce count sample: " + $anns.Count) +if ($anns.Count -ge 2) { + $t0 = ($anns[0] -split "`t")[0]; $h0 = ($anns[0] -split "`t")[1] + $t1 = ($anns[1] -split "`t")[0]; $h1 = ($anns[1] -split "`t")[1] + Write-Host ("A t=" + $t0 + " len=" + ($h0.Length/2)) + Write-Host ("B t=" + $t1 + " len=" + ($h1.Length/2)) + # 找差异字节 + $diffs = @() + $n = [Math]::Min($h0.Length, $h1.Length) + for ($i = 0; $i -lt $n; $i += 2) { + if ($h0.Substring($i,2) -ne $h1.Substring($i,2)) { $diffs += ($i/2) } + } + Write-Host ("differing byte offsets: " + (($diffs | ForEach-Object { '0x{0:x}' -f $_ }) -join ' ')) + # 打印模板与实时帧在这些区域的内容 + $tpl = (Get-Content "C:\Users\tonycao\work\light\enet-validator\samples\announce_686.hex" -Raw).Trim() -replace '\s','' + Write-Host "" + foreach ($d in $diffs) { + $s = [Math]::Max(0, $d - 4); $e = [Math]::Min($h0.Length/2 - 1, $d + 7) + $seg0 = ''; $seg1 = ''; $segt = '' + for ($i = $s; $i -le $e; $i++) { $seg0 += $h0.Substring(2*$i,2); $seg1 += $h1.Substring(2*$i,2); if (2*$i+2 -le $tpl.Length) { $segt += $tpl.Substring(2*$i,2) } else { $segt += '??????' } } + Write-Host ('0x{0:x}: liveA={1} liveB={2} tpl={3}' -f $d, $seg0, $seg1, $segt) + } + # 存下完整实时 announce 供修补用 + Set-Content -Encoding ascii "$env:TEMP\opencode\announce_live.hex" $h0 + Write-Host "saved live announce to announce_live.hex" +} diff --git a/scripts/session/enum_preclaim.ps1 b/scripts/session/enum_preclaim.ps1 new file mode 100644 index 0000000..1bbc9f0 --- /dev/null +++ b/scripts/session/enum_preclaim.ps1 @@ -0,0 +1,20 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$cap = "$env:TEMP\opencode\sym_claim2.pcap" +$ts = "C:\Program Files\Wireshark\tshark.exe" +Write-Host "=== ALL host-sent frames before claim (t<354.8): dst x udp.len x count + first/last t ===" +$all = @(& $ts -r $cap -Y "ip.src==192.168.123.113 and frame.time_relative<354.5" -T fields -e frame.time_relative -e ip.dst -e udp.length 2>$null) +$groups = @{} +foreach ($l in $all) { $p = $l -split "`t"; $k = $p[1] + " len=" + $p[2]; if (-not $groups.ContainsKey($k)) { $groups[$k] = New-Object System.Collections.ArrayList }; [void]$groups[$k].Add([double]$p[0]) } +foreach ($k in ($groups.Keys | Sort-Object)) { + $arr = $groups[$k] + Write-Host ("{0,-30} n={1,-5} first={2:N1}s last={3:N1}s" -f $k, $arr.Count, $arr[0], $arr[$arr.Count-1]) +} +Write-Host "" +Write-Host "=== ALL device-sent frames before claim (t<354.8) ===" +$dev = @(& $ts -r $cap -Y "ip.src==192.168.123.1 and frame.time_relative<354.5" -T fields -e frame.time_relative -e ip.dst -e udp.length 2>$null) +$g2 = @{} +foreach ($l in $dev) { $p = $l -split "`t"; $k = $p[1] + " len=" + $p[2]; if (-not $g2.ContainsKey($k)) { $g2[$k] = 0 }; $g2[$k]++ } +foreach ($k in ($g2.Keys | Sort-Object)) { Write-Host ("{0,-30} n={1}" -f $k, $g2[$k]) } +Write-Host "" +Write-Host "=== EUCE (548) timing before claim ===" +& $ts -r $cap -Y "udp.length==548" -T fields -e frame.time_relative 2>$null | Select-Object -First 20 diff --git a/scripts/session/extract_symclaim.ps1 b/scripts/session/extract_symclaim.ps1 new file mode 100644 index 0000000..224d100 --- /dev/null +++ b/scripts/session/extract_symclaim.ps1 @@ -0,0 +1,13 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$cap = "$env:TEMP\opencode\sym_claim2.pcap" +$ts = "C:\Program Files\Wireshark\tshark.exe" +# 抓 ident 和第一个 AECH 质询的完整 hex (frame.time_epoch 精确定位) +Write-Host "=== ident102 (t=354.810) full hex ===" +& $ts -r $cap -Y "udp.length==110 and frame.time_epoch>=1789049091.467 and frame.time_epoch<=1789049091.468" -T fields -e udp.payload 2>$null +Write-Host "" +Write-Host "=== AECH challenge #1 (t=354.824) full hex ===" +& $ts -r $cap -Y "udp.length==560 and frame.time_epoch>=1789049091.481 and frame.time_epoch<=1789049091.482" -T fields -e udp.payload 2>$null +Write-Host "" +Write-Host "=== AERE #1 (t=354.826) 0xC0 前 96B ===" +$h = & $ts -r $cap -Y "udp.length==560 and frame.time_epoch>=1789049091.483 and frame.time_epoch<=1789049091.484" -T fields -e udp.payload 2>$null +if ($h) { $h.Substring(0, [Math]::Min(300, $h.Length)) } diff --git a/scripts/session/find_claim2.ps1 b/scripts/session/find_claim2.ps1 new file mode 100644 index 0000000..d649e8f --- /dev/null +++ b/scripts/session/find_claim2.ps1 @@ -0,0 +1,8 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$cap = "$env:TEMP\opencode\sym_claim2.pcap" +$ts = "C:\Program Files\Wireshark\tshark.exe" +Write-Host "=== ident(110) / 47B(55) / AECH-AERE(560) frames ===" +& $ts -r $cap -Y "udp.length==110 or udp.length==55 or udp.length==560" -T fields -e frame.time_relative -e frame.time_epoch -e ip.src -e ip.dst -e udp.length 2>$null +Write-Host "" +Write-Host "=== last discovery(188) + announce(652) before claim (restart boundary) ===" +& $ts -r $cap -Y "udp.length==188 or udp.length==652" -T fields -e frame.time_relative -e ip.src -e ip.dst -e udp.length 2>$null | Select-Object -Last 8 diff --git a/scripts/session/find_sub07.ps1 b/scripts/session/find_sub07.ps1 new file mode 100644 index 0000000..bd705fb --- /dev/null +++ b/scripts/session/find_sub07.ps1 @@ -0,0 +1,9 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ts = "C:\Program Files\Wireshark\tshark.exe" +# sub=07 特征: type=02 帧 0x18-0x19 = 07 00。用 payload 前缀过滤: ENet + 02 + ...+ 0700 +# udp.payload hex 的 0x18-0x19 字节 = 第 48-51 字符 +foreach ($cap in @("$env:TEMP\opencode\sym_claim2.pcap", "$env:TEMP\opencode\cold2.pcap", "$env:TEMP\opencode\sym_close.pcap")) { + Write-Host ("=== " + (Split-Path $cap -Leaf) + " : type=02 sub=07 frames ===") + & $ts -r $cap -Y "udp[16:2]==0x0700 and ip.src==192.168.123.113 and udp.length<200" -T fields -e frame.time_relative -e udp.length -e udp.payload 2>$null | ForEach-Object { $p = $_ -split "`t"; "{0,9:N1}s len={1} {2}" -f [double]$p[0], $p[1], $p[2].Substring(0, [Math]::Min(104, $p[2].Length)) } | Select-Object -First 12 + Write-Host "" +} diff --git a/scripts/session/find_sub07b.ps1 b/scripts/session/find_sub07b.ps1 new file mode 100644 index 0000000..2d75aaa --- /dev/null +++ b/scripts/session/find_sub07b.ps1 @@ -0,0 +1,7 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ts = "C:\Program Files\Wireshark\tshark.exe" +foreach ($cap in @("$env:TEMP\opencode\sym_claim2.pcap", "$env:TEMP\opencode\cold2.pcap", "$env:TEMP\opencode\sym_close.pcap")) { + Write-Host ("=== " + (Split-Path $cap -Leaf) + " : type=02 sub=07 (udp[32:2]==0x0700) host-sent ===") + & $ts -r $cap -Y "udp[32:2]==0x0700 and ip.src==192.168.123.113 and udp.length<300" -T fields -e frame.time_relative -e udp.length -e udp.payload 2>$null | ForEach-Object { $p = $_ -split "`t"; "{0,9:N1}s len={1} {2}" -f [double]$p[0], $p[1], $p[2].Substring(0, [Math]::Min(104, $p[2].Length)) } | Select-Object -First 10 + Write-Host "" +} diff --git a/scripts/session/grep_aech.ps1 b/scripts/session/grep_aech.ps1 new file mode 100644 index 0000000..ffea6e6 --- /dev/null +++ b/scripts/session/grep_aech.ps1 @@ -0,0 +1,11 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +if (-not $s) { throw "SSH connect failed" } +$r = Invoke-SSHCommand -SSHSession $s -Command "grep -nE 'AECH|质询|应答|claim' ~/butler.log | tail -n 15; echo ---; wc -l ~/butler.log" +$r.Output +Remove-SSHSession -SSHSession $s | Out-Null diff --git a/scripts/session/listen_113.ps1 b/scripts/session/listen_113.ps1 new file mode 100644 index 0000000..a626c70 --- /dev/null +++ b/scripts/session/listen_113.ps1 @@ -0,0 +1,18 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$u = New-Object Net.Sockets.UdpClient +$u.Client.SetSocketOption([Net.Sockets.SocketOptionLevel]::Socket, [Net.Sockets.SocketOptionName]::ReuseAddress, $true) +$u.Client.Bind((New-Object Net.IPEndPoint([Net.IPAddress]::Parse("192.168.123.113"), 31772))) +$u.Client.ReceiveTimeout = 1000 +$sw = [Diagnostics.Stopwatch]::StartNew() +$remote = New-Object Net.IPEndPoint([Net.IPAddress]::Any, 0) +$last = Get-Date +while ($sw.Elapsed.TotalSeconds -lt 120) { + try { + $d = $u.Receive([ref]$remote) + $tag = "" + if ($d.Length -ge 4) { $tag = [Text.Encoding]::ASCII.GetString($d[0..3]) } + Write-Host ("{0,7:N1}s len={1,-5} from={2} magic={3} 0x18-0x19={4:x2}{5:x2}" -f $sw.Elapsed.TotalSeconds, $d.Length, $remote.Address, $tag, $(if($d.Length -gt 0x19){$d[0x19]}else{0}), $(if($d.Length -gt 0x18){$d[0x18]}else{0})) + $last = Get-Date + } catch {} +} +Write-Host "监听结束 (120s)" diff --git a/scripts/session/mystery60.ps1 b/scripts/session/mystery60.ps1 new file mode 100644 index 0000000..8f1419b --- /dev/null +++ b/scripts/session/mystery60.ps1 @@ -0,0 +1,11 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$cap = "$env:TEMP\opencode\sym_claim2.pcap" +$ts = "C:\Program Files\Wireshark\tshark.exe" +Write-Host "=== len=60 payload hex (mystery pre-claim frames) ===" +& $ts -r $cap -Y "udp.length==60 and ip.src==192.168.123.113" -T fields -e frame.time_relative -e udp.srcport -e udp.payload 2>$null +Write-Host "" +Write-Host "=== source ports of host frames (pre-claim) ===" +& $ts -r $cap -Y "ip.src==192.168.123.113 and frame.time_relative<354.8" -T fields -e udp.srcport 2>$null | Group-Object | Select-Object Count, Name +Write-Host "" +Write-Host "=== source port of ident + AECH ===" +& $ts -r $cap -Y "(udp.length==110 or udp.length==560) and frame.time_relative>=354" -T fields -e frame.time_relative -e udp.srcport 2>$null | Select-Object -First 5 diff --git a/scripts/session/post_claim.ps1 b/scripts/session/post_claim.ps1 new file mode 100644 index 0000000..1b40fcb --- /dev/null +++ b/scripts/session/post_claim.ps1 @@ -0,0 +1,11 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +if (-not $s) { throw "SSH connect failed" } +$r = Invoke-SSHCommand -SSHSession $s -Command "awk '/5767.0s/,0' ~/butler.log | grep -vE 'DMX #|发现请求' | head -n 30" +$r.Output +Remove-SSHSession -SSHSession $s | Out-Null diff --git a/scripts/session/probe_loop.ps1 b/scripts/session/probe_loop.ps1 new file mode 100644 index 0000000..de4bc9f --- /dev/null +++ b/scripts/session/probe_loop.ps1 @@ -0,0 +1,20 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$exe = "C:\Users\tonycao\work\light\enet-validator\target\release\enet-validator.exe" +$log = "$env:TEMP\opencode\probe_loop.log" +Remove-Item $log -ErrorAction SilentlyContinue +for ($i = 0; $i -lt 14; $i++) { + $ts = Get-Date -Format "HH:mm:ss" + $env:HOSTDEMO_MODE = "claimonly" + $out = & $exe hostdemo 192.168.123.1 0 2>&1 | Out-String + Add-Content -Encoding utf8 $log ("=== " + $ts + " attempt " + $i + " ===`n" + $out) + if ($out -match "CLAIM_PROBE.*PASS") { + Add-Content -Encoding utf8 $log ((Get-Date -Format "HH:mm:ss") + " CLAIM UNLOCKED - full hostdemo starting") + Remove-Item Env:\HOSTDEMO_MODE -ErrorAction SilentlyContinue + $out2 = & $exe hostdemo 192.168.123.1 30 2>&1 | Out-String + Add-Content -Encoding utf8 $log $out2 + Add-Content -Encoding utf8 $log "=== full hostdemo done ===" + break + } + Start-Sleep -Seconds 300 +} +Add-Content -Encoding utf8 $log "probe loop exit" diff --git a/scripts/session/probe_loop2.ps1 b/scripts/session/probe_loop2.ps1 new file mode 100644 index 0000000..3110b70 --- /dev/null +++ b/scripts/session/probe_loop2.ps1 @@ -0,0 +1,20 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$exe = "C:\Users\tonycao\work\light\enet-validator\target\release\enet-validator.exe" +$log = "$env:TEMP\opencode\probe_loop2.log" +Remove-Item $log -ErrorAction SilentlyContinue +for ($i = 0; $i -lt 45; $i++) { + $ts = Get-Date -Format "HH:mm:ss" + $env:HOSTDEMO_MODE = "claimonly" + $out = & $exe hostdemo 192.168.123.1 0 2>&1 | Out-String + Add-Content -Encoding utf8 $log ("=== " + $ts + " attempt " + $i + " ===`n" + $out) + if ($out -match "CLAIM_PROBE.*PASS") { + Add-Content -Encoding utf8 $log ((Get-Date -Format "HH:mm:ss") + " CLAIM UNLOCKED - full hostdemo starting (RDM discovery + 60s DMX)") + Remove-Item Env:\HOSTDEMO_MODE -ErrorAction SilentlyContinue + $out2 = & $exe hostdemo 192.168.123.1 60 2>&1 | Out-String + Add-Content -Encoding utf8 $log $out2 + Add-Content -Encoding utf8 $log "=== full hostdemo done ===" + break + } + Start-Sleep -Seconds 240 +} +Add-Content -Encoding utf8 $log "probe loop2 exit" diff --git a/scripts/session/push_try.ps1 b/scripts/session/push_try.ps1 new file mode 100644 index 0000000..484bf34 --- /dev/null +++ b/scripts/session/push_try.ps1 @@ -0,0 +1,8 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +$r = Invoke-SSHCommand -SessionId $s.SessionId -Command "cd ~/enet-validator && GIT_TERMINAL_PROMPT=0 git push -u origin master 2>&1 | head -10" -TimeOut 120 +$r.Output +Remove-SSHSession -SessionId $s.SessionId | Out-Null diff --git a/scripts/session/rdm_tail.ps1 b/scripts/session/rdm_tail.ps1 new file mode 100644 index 0000000..ba72a03 --- /dev/null +++ b/scripts/session/rdm_tail.ps1 @@ -0,0 +1,11 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +if (-not $s) { throw "SSH connect failed" } +$r = Invoke-SSHCommand -SSHSession $s -Command "grep -vE 'DMX #|发现请求' ~/butler.log | tail -n 15" +$r.Output +Remove-SSHSession -SSHSession $s | Out-Null diff --git a/scripts/session/rdm_tail2.ps1 b/scripts/session/rdm_tail2.ps1 new file mode 100644 index 0000000..20246b8 --- /dev/null +++ b/scripts/session/rdm_tail2.ps1 @@ -0,0 +1,11 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +if (-not $s) { throw "SSH connect failed" } +$r = Invoke-SSHCommand -SSHSession $s -Command "grep -c 'RDM' ~/butler.log; grep 'RDM' ~/butler.log | tail -n 12" +$r.Output +Remove-SSHSession -SSHSession $s | Out-Null diff --git a/scripts/session/read_symclose.ps1 b/scripts/session/read_symclose.ps1 new file mode 100644 index 0000000..e29998e --- /dev/null +++ b/scripts/session/read_symclose.ps1 @@ -0,0 +1,16 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$cap = "$env:TEMP\opencode\sym_close.pcap" +$ts = "C:\Program Files\Wireshark\tshark.exe" +Write-Host "=== stats by type (src dst udplen count) ===" +& $ts -r $cap -T fields -e ip.src -e ip.dst -e udp.length 2>$null | Group-Object { $p = $_ -split "`t"; "$($p[0]) -> $($p[1]) len=$($p[2])" } | Sort-Object Count -Descending | Select-Object -First 12 Count, Name | Format-Table -AutoSize +Write-Host "=== DMX stream (udplen=1092) per-5s buckets ===" +$dmx = @(& $ts -r $cap -Y "udp.length==1092" -T fields -e frame.time_relative 2>$null) +Write-Host ("total DMX frames: " + $dmx.Count) +$buckets = @{} +foreach ($t in $dmx) { $b = [int]([double]$t / 5); $buckets[$b] = 1 + [int]($buckets[$b] | ForEach-Object { $_ }) } +foreach ($k in ($buckets.Keys | Sort-Object)) { Write-Host (" t={0,4}-{1,-4}s : {2} frames" -f ($k*5), ($k*5+5), $buckets[$k]) } +if ($dmx.Count -gt 0) { Write-Host (" last DMX at t=" + $dmx[$dmx.Count-1]) } +Write-Host "=== AECH/AERE (udplen=560) ===" +& $ts -r $cap -Y "udp.length==560" -T fields -e frame.time_relative -e ip.src -e ip.dst 2>$null +Write-Host "=== last 15 frames overall ===" +& $ts -r $cap -T fields -e frame.time_relative -e ip.src -e ip.dst -e udp.length 2>$null | Select-Object -Last 15 diff --git a/scripts/session/read_symclose2.ps1 b/scripts/session/read_symclose2.ps1 new file mode 100644 index 0000000..877c6a8 --- /dev/null +++ b/scripts/session/read_symclose2.ps1 @@ -0,0 +1,16 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$cap = "$env:TEMP\opencode\sym_close.pcap" +$ts = "C:\Program Files\Wireshark\tshark.exe" +Write-Host "=== DMX stream (1092) per-10s buckets ===" +$dmx = @(& $ts -r $cap -Y "udp.length==1092" -T fields -e frame.time_relative 2>$null) +Write-Host ("total: " + $dmx.Count) +if ($dmx.Count -gt 0) { + $buckets = @{} + foreach ($t in $dmx) { $b = [int]([double]$t / 10); if ($buckets.ContainsKey($b)) { $buckets[$b]++ } else { $buckets[$b] = 1 } } + foreach ($k in ($buckets.Keys | Sort-Object)) { Write-Host (" t={0,4}-{1,-4}s : {2}" -f ($k*10), ($k*10+10), $buckets[$k]) } + Write-Host (" last DMX t=" + $dmx[$dmx.Count-1]) +} +Write-Host "=== last 25 frames overall ===" +& $ts -r $cap -T fields -e frame.time_relative -e ip.src -e ip.dst -e udp.length 2>$null | Select-Object -Last 25 +Write-Host "=== any AECH/AERE (560) ===" +& $ts -r $cap -Y "udp.length==560" -T fields -e frame.time_relative -e ip.src 2>$null diff --git a/scripts/session/restore_butler.ps1 b/scripts/session/restore_butler.ps1 new file mode 100644 index 0000000..78b5a4b --- /dev/null +++ b/scripts/session/restore_butler.ps1 @@ -0,0 +1,9 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +$r = Invoke-SSHCommand -SSHSession $s -Command "bash ~/run_butler.sh device 2>&1 | tail -2; sleep 2; pgrep -af 'enet-validator device' | head -1" +Write-Host ($r.Output -join "`n") +Remove-SSHSession $s | Out-Null diff --git a/scripts/session/run_hd3.ps1 b/scripts/session/run_hd3.ps1 new file mode 100644 index 0000000..426fb45 --- /dev/null +++ b/scripts/session/run_hd3.ps1 @@ -0,0 +1,11 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$cap = "$env:TEMP\opencode\hd_claim3.pcap" +Remove-Item $cap -ErrorAction SilentlyContinue +$ts = "C:\Program Files\Wireshark\tshark.exe" +$proc = Start-Process -FilePath $ts -ArgumentList "-i `"\Device\NPF_{E7165B1C-BAF6-49CF-9A3D-EF12DBC17329}`" -f `"udp port 31772`" -a duration:420 -w `"$cap`"" -PassThru -WindowStyle Hidden -RedirectStandardError "$env:TEMP\opencode\tshark_err5.txt" +Start-Sleep -Seconds 2 +Set-Location "C:\Users\tonycao\work\light\enet-validator" +& .\target\release\enet-validator.exe hostdemo 192.168.123.1 30 +Start-Sleep -Seconds 5 +Write-Host "`n=== claim window in capture ===" +& $ts -r $cap -Y "udp.length==110 or udp.length==55 or udp.length==560" -T fields -e frame.time_relative -e ip.src -e ip.dst -e udp.length 2>$null | Select-Object -First 12 diff --git a/scripts/session/run_hd4.ps1 b/scripts/session/run_hd4.ps1 new file mode 100644 index 0000000..af6f37f --- /dev/null +++ b/scripts/session/run_hd4.ps1 @@ -0,0 +1,4 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +Set-Location "C:\Users\tonycao\work\light\enet-validator" +& .\target\release\enet-validator.exe hostdemo 192.168.123.1 30 2>&1 | Out-File -Encoding utf8 "$env:TEMP\opencode\hd3_run.log" +Write-Host "DONE" diff --git a/scripts/session/start_butler.ps1 b/scripts/session/start_butler.ps1 new file mode 100644 index 0000000..367c38a --- /dev/null +++ b/scripts/session/start_butler.ps1 @@ -0,0 +1,13 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +if (-not $s) { throw "SSH connect failed" } +$r = Invoke-SSHCommand -SSHSession $s -Command "cat ~/run_butler_local.sh" +$r.Output +$r = Invoke-SSHCommand -SSHSession $s -Command "nohup ~/run_butler_local.sh > /dev/null 2>&1 & sleep 3; pgrep -af enet; echo ---; tail -n 4 ~/butler.log" +$r.Output +Remove-SSHSession -SSHSession $s | Out-Null diff --git a/scripts/session/start_local.ps1 b/scripts/session/start_local.ps1 new file mode 100644 index 0000000..3689181 --- /dev/null +++ b/scripts/session/start_local.ps1 @@ -0,0 +1,23 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +$cmd = @' +cat > ~/run_butler_local.sh << 'EOF' +#!/bin/bash +pkill -f "release/enet-validator" 2>/dev/null +sleep 1 +cd /home/tonycao/enet-validator +nohup ./target/release/enet-validator device 192.168.58.7 00161cf12e88 86400 > /home/tonycao/butler.log 2>&1 & +disown +sleep 4 +echo "PID=$(pgrep -f 'release/enet-validator' | head -1)" +EOF +chmod +x ~/run_butler_local.sh +bash ~/run_butler_local.sh +'@ +$r = Invoke-SSHCommand -SSHSession $s -Command $cmd +Write-Host ($r.Output -join "`n") +Remove-SSHSession $s | Out-Null diff --git a/scripts/session/sync_docs.ps1 b/scripts/session/sync_docs.ps1 new file mode 100644 index 0000000..20acbc8 --- /dev/null +++ b/scripts/session/sync_docs.ps1 @@ -0,0 +1,52 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) + +function Copy-Up($local, $destDir) { + Set-SCPItem -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -Path $local -Destination $destDir +} + +# 1) docs +Invoke-Command -ScriptBlock { } | Out-Null +$s0 = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +Invoke-SSHCommand -SessionId $s0.SessionId -Command "mkdir -p ~/enet-validator/docs ~/enet-validator/scripts/session ~/enet-validator/tools" | Out-Null +Remove-SSHSession -SessionId $s0.SessionId | Out-Null + +Copy-Up "C:\Users\tonycao\work\light\enet_protocol.md" "~/enet-validator/docs/" +Copy-Up "C:\Users\tonycao\work\light\rust_verification_plan.md" "~/enet-validator/docs/" +Write-Host "sync docs" + +# 2) scripts (root) +foreach ($f in @("enet_lib.ps1","Verify-EnetProtocol.ps1","Verify-Rdm.ps1")) { + Copy-Up "C:\Users\tonycao\work\light\$f" "~/enet-validator/scripts/" +} +Write-Host "sync root scripts" + +# 3) scripts/session (全部 75 个会话脚本) +$sess = Get-ChildItem "$env:TEMP\opencode\*.ps1" +foreach ($f in $sess) { + Copy-Up $f.FullName "~/enet-validator/scripts/session/" +} +Write-Host ("sync session scripts: " + $sess.Count) + +# 4) tools (.cs) +foreach ($f in @("IlDump.cs","GetTable.cs","Scan.cs")) { + Copy-Up "$env:TEMP\opencode\$f" "~/enet-validator/tools/" +} +Write-Host "sync tools" + +# 5) README +Copy-Up "C:\Users\tonycao\work\light\enet-validator\README.md" "~/enet-validator/" +Write-Host "sync README" + +# 6) commit + push +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +$cmds = @' +cd ~/enet-validator && git add -A && git commit -m "docs+scripts: enet_protocol.md, verification plan, PS verify suite, session analysis scripts, DLL tools, README" | tail -2; git push origin master 2>&1 | tail -3; echo ---; git log --oneline | head -3 +'@ +$r = Invoke-SSHCommand -SessionId $s.SessionId -Command $cmds -TimeOut 180 +$r.Output +if ($r.Error) { Write-Host "STDERR:"; $r.Error | Select-Object -First 6 } +Remove-SSHSession -SessionId $s.SessionId | Out-Null diff --git a/scripts/session/sync_push.ps1 b/scripts/session/sync_push.ps1 new file mode 100644 index 0000000..9b6f5a1 --- /dev/null +++ b/scripts/session/sync_push.ps1 @@ -0,0 +1,42 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +if (-not $s) { throw "SSH connect failed" } +$sid = $s.SessionId + +# 1) 同步源码 +$srcFiles = @("main.rs","enet.rs","tests.rs","aech.rs","hostdemo.rs","dmxhash.rs") +foreach ($f in $srcFiles) { + Set-SCPItem -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -Path "C:\Users\tonycao\work\light\enet-validator\src\$f" -Destination "~/enet-validator/src/" +} +Set-SCPItem -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -Path "C:\Users\tonycao\work\light\enet-validator\Cargo.toml" -Destination "~/enet-validator/" +Set-SCPItem -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -Path "C:\Users\tonycao\work\light\enet-validator\Cargo.lock" -Destination "~/enet-validator/" +Set-SCPItem -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -Path "C:\Users\tonycao\work\light\enet-validator\.gitignore" -Destination "~/enet-validator/" + +# 2) 同步 samples +Invoke-SSHCommand -SessionId $sid -Command "mkdir -p ~/enet-validator/samples" | Out-Null +Get-ChildItem "C:\Users\tonycao\work\light\enet-validator\samples\*.hex" | ForEach-Object { + Set-SCPItem -SessionId $sid -Path $_.FullName -Destination "~/enet-validator/samples/" +} +Write-Host "sync done" + +# 3) git init + commit + push +$cmds = @" +cd ~/enet-validator +git init -b master 2>/dev/null || true +git config user.email "tonycao@local" || true +git config user.name "tonycao" || true +git add -A +git commit -m "enet-validator: e:net protocol reverse + host-side emulation (AECH/DMX/RDM, hostdemo)" || true +git remote remove origin 2>/dev/null || true +git remote add origin http://www.kanghexin.work:3000/tonycao/enet-validator.git +git log --oneline | head -3 +git remote -v +"@ +$r = Invoke-SSHCommand -SessionId $sid -Command $cmds -TimeOut 60 +$r.Output +if ($r.Error) { $r.Error | Select-Object -First 5 } +Remove-SSHSession -SessionId $sid | Out-Null diff --git a/scripts/session/sync_push2.ps1 b/scripts/session/sync_push2.ps1 new file mode 100644 index 0000000..defe53d --- /dev/null +++ b/scripts/session/sync_push2.ps1 @@ -0,0 +1,36 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) + +function Copy-Up($local, $destDir) { + Set-SCPItem -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -Path $local -Destination $destDir +} + +# 1) 源码 +foreach ($f in @("main.rs","enet.rs","tests.rs","aech.rs","hostdemo.rs","dmxhash.rs")) { + Copy-Up "C:\Users\tonycao\work\light\enet-validator\src\$f" "~/enet-validator/src/" + Write-Host "sync src/$f" +} +Copy-Up "C:\Users\tonycao\work\light\enet-validator\Cargo.toml" "~/enet-validator/" +Copy-Up "C:\Users\tonycao\work\light\enet-validator\Cargo.lock" "~/enet-validator/" +Copy-Up "C:\Users\tonycao\work\light\enet-validator\.gitignore" "~/enet-validator/" +Write-Host "sync root done" + +# 2) samples +$hexes = Get-ChildItem "C:\Users\tonycao\work\light\enet-validator\samples\*.hex" +foreach ($h in $hexes) { + Copy-Up $h.FullName "~/enet-validator/samples/" +} +Write-Host ("sync samples done: " + $hexes.Count + " files") + +# 3) git init + commit +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +$cmds = @' +cd ~/enet-validator && git init -b master 2>/dev/null; git config user.email "tonycao@local"; git config user.name "tonycao"; git add -A; git commit -m "enet-validator: e:net protocol reverse + host-side emulation (AECH claim / encrypted DMX / RDM discovery, hostdemo)" | tail -2; git remote remove origin 2>/dev/null; git remote add origin http://www.kanghexin.work:3000/tonycao/enet-validator.git; echo ---; git log --oneline 2>/dev/null | head -3; git remote -v +'@ +$r = Invoke-SSHCommand -SessionId $s.SessionId -Command $cmds -TimeOut 120 +$r.Output +if ($r.Error) { Write-Host "STDERR:"; $r.Error | Select-Object -First 6 } +Remove-SSHSession -SessionId $s.SessionId | Out-Null diff --git a/scripts/session/test_g.ps1 b/scripts/session/test_g.ps1 new file mode 100644 index 0000000..4191550 --- /dev/null +++ b/scripts/session/test_g.ps1 @@ -0,0 +1,47 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +# Craft AECH with fresh random nonce -> .7 (local G) -> verify resp32 == G(nonce) +$TABLE = @(0x1008de00,0xe3b77ff6,0x31183e1c,0x90fb53c8,0xff10ba5c,0x030bc4b6,0xe22935c8,0x10e8ede1, + 0x55f409a0,0x3eb0b9a3,0xf7333308,0xedf07810,0x8abf4078,0x60ec0545,0x1f4645c8,0x54094148, + 0x04c0f500,0xfabe50bb,0xf3f3e54c,0x6c2a02cb,0xa2546e60,0x4f6ae10e,0x51dfe8d8,0x2eedb0ec, + 0x74006928,0x48fade70,0xe5346e9c,0x1c3fe0bc,0xf6700700,0x6b7f1ba0,0xa1143cf4,0x3be8426c) +function G([uint32]$x) { + $big = [int64]$x * 13 + $m = [uint32]($big % 4294967296) + $idx = [int]((($x % 32) + 1 + ($m % 30)) % 32) + return $x -bxor $TABLE[$idx] +} +$raw = [IO.File]::ReadAllBytes("$env:TEMP\opencode\cold2.tsv") +$text = if ($raw[0] -eq 0xFF -and $raw[1] -eq 0xFE) { [Text.Encoding]::Unicode.GetString($raw) } else { [Text.Encoding]::UTF8.GetString($raw) } +$hex = $null +foreach ($l in ($text -split "`r?`n")) { + $p = $l -split "`t" + if ($p.Count -lt 5 -or -not $p[4]) { continue } + $h = $p[4] -replace " ", "" + if ($h.Length -eq 1104 -and $h.Substring(0x28 * 2, 8) -eq "41454348") { $hex = $h; break } +} +if (-not $hex) { throw "no AECH template" } +$n = $hex.Length / 2 +$req = New-Object byte[] $n +for ($i = 0; $i -lt $n; $i++) { $req[$i] = [Convert]::ToByte($hex.Substring(2 * $i, 2), 16) } + +$rnd = New-Object Random +$udp = New-Object System.Net.Sockets.UdpClient +$udp.Client.ReceiveTimeout = 3000 +$ep = New-Object System.Net.IPEndPoint([System.Net.IPAddress]::Parse("192.168.58.7"), 31772) +$pass = 0 +for ($t = 0; $t -lt 5; $t++) { + $nonce = [uint32]($rnd.NextDouble() * 4294967295) + $nb = [BitConverter]::GetBytes($nonce) + [Array]::Copy($nb, 0, $req, 0x88, 4) + $expect = G $nonce + [void]$udp.Send($req, $req.Length, $ep) + try { + $rep = $udp.Receive([ref]$ep) + $got = [BitConverter]::ToUInt32($rep, 0xC0) + $ok = $got -eq $expect + if ($ok) { $pass++ } + Write-Host ("#{0} nonce={1:x8} expect={2:x8} got={3:x8} {4} len={5}" -f ($t+1), $nonce, $expect, $got, $(if ($ok) {"PASS"} else {"FAIL"}), $rep.Length) + } catch { Write-Host ("#{0} no reply" -f ($t+1)) } +} +Write-Host "== $pass/5 PASS ==" +$udp.Close() diff --git a/scripts/session/test_newmac.ps1 b/scripts/session/test_newmac.ps1 new file mode 100644 index 0000000..dda0177 --- /dev/null +++ b/scripts/session/test_newmac.ps1 @@ -0,0 +1,14 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +# restart butler with NEW MAC (new device identity), NO relay -> host must re-claim, local AERE +$r = Invoke-SSHCommand -SSHSession $s -Command "pkill -f 'enet-validator device'; sleep 1; cd ~/enet-validator && nohup ./target/release/enet-validator device 192.168.58.7 00161cf12e89 300 > ~/butler.log 2>&1 & sleep 2; pgrep -af 'enet-validator device' | head -1" +Write-Host ($r.Output -join "`n") +Write-Host "--- waiting 90s for host auto-claim of new device ---" +Start-Sleep -Seconds 90 +$r = Invoke-SSHCommand -SSHSession $s -Command "echo AECH次数:; grep -c 'AECH' ~/butler.log; echo 注册次数:; grep -c 'sub=01' ~/butler.log; echo DMX行数:; grep -c 'DMX #' ~/butler.log; echo '---'; grep -E 'AECH|注册|DMX #1 ' ~/butler.log | head -10" +Write-Host ($r.Output -join "`n") +Remove-SSHSession $s | Out-Null diff --git a/scripts/session/test_norelay.ps1 b/scripts/session/test_norelay.ps1 new file mode 100644 index 0000000..41edfd2 --- /dev/null +++ b/scripts/session/test_norelay.ps1 @@ -0,0 +1,14 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +# restart butler WITHOUT relay arg (local AERE answers) +$r = Invoke-SSHCommand -SSHSession $s -Command "pkill -f 'enet-validator device'; sleep 1; cd ~/enet-validator && nohup ./target/release/enet-validator device 192.168.58.7 00161cf12e88 300 > ~/butler.log 2>&1 & sleep 2; pgrep -af 'enet-validator device'" +Write-Host ($r.Output -join "`n") +Write-Host "--- waiting 75s for host re-claim ---" +Start-Sleep -Seconds 75 +$r = Invoke-SSHCommand -SSHSession $s -Command "grep -cE 'AECH' ~/butler.log; echo '---'; grep -E 'AECH|DMX #' ~/butler.log | head -12" +Write-Host ($r.Output -join "`n") +Remove-SSHSession $s | Out-Null diff --git a/scripts/session/test_setack.ps1 b/scripts/session/test_setack.ps1 new file mode 100644 index 0000000..89fe69a --- /dev/null +++ b/scripts/session/test_setack.ps1 @@ -0,0 +1,52 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$lines = [IO.File]::ReadAllLines("$env:TEMP\opencode\set123b.tsv") +$hex = $null +foreach ($l in $lines) { + $p = $l -split "`t" + if ($p.Count -lt 5 -or -not $p[4]) { continue } + if ($p[1] -ne "192.168.123.113") { continue } + $h = $p[4] -replace " ", "" + if ($h.Length -ne 2168) { continue } + if ($h.Substring(0x20 * 2, 4) -ne "0200") { continue } # subtype event + if ($h.Substring(0x3C * 2, 2) -ne "01") { continue } # SubSC=01 + if ($h.Substring(0x3D * 2, 2) -ne "1a") { continue } # msgLen=26 + if ($h.Substring(0x4F * 2, 2) -ne "30") { continue } + if ($h.Substring(0x30 * 2, 2) -ne "00") { continue } + $hex = $h; break +} +if (-not $hex) { throw "no SET request frame found" } +$n = $hex.Length / 2 +$req = New-Object byte[] $n +for ($i = 0; $i -lt $n; $i++) { $req[$i] = [Convert]::ToByte($hex.Substring(2 * $i, 2), 16) } +$uidS = ($req[0x3E..0x43] | ForEach-Object { $_.ToString("x2") }) -join "" +$pidS = $req[0x50].ToString("x2") + $req[0x51].ToString("x2") +Write-Host "SET req: ${n}B UID=$uidS PID=$pidS" + +$udp = New-Object System.Net.Sockets.UdpClient +$udp.Client.ReceiveTimeout = 3000 +$ep = New-Object System.Net.IPEndPoint([System.Net.IPAddress]::Parse("192.168.58.7"), 31772) +$sw = [Diagnostics.Stopwatch]::StartNew() +$sent = $udp.Send($req, $req.Length, $ep) +Write-Host "sent $sent bytes from $($udp.Client.LocalEndPoint)" +while ($true) { + try { + $rep = $udp.Receive([ref]$ep) + if ($rep.Length -lt 0x44) { + Write-Host ("* reply {0}B latency {1}ms (42B telemetry)" -f $rep.Length, $sw.ElapsedMilliseconds) + continue + } + $srcS = ($rep[0x33..0x38] | ForEach-Object { $_.ToString("x2") }) -join "" + Write-Host ("* reply {0}B latency {1}ms srcUID={2} TN={3:x2} RT={4:x2} CC={5:x2} PID={6:x2}{7:x2} PDL={8}" -f ` + $rep.Length, $sw.ElapsedMilliseconds, $srcS, $rep[0x39], $rep[0x3A], $rep[0x3E], $rep[0x3F], $rep[0x40], $rep[0x41]) + $mlen = [int]$rep[0x2C]; $sum = 0 + for ($i = 0x2A; $i -lt 0x2A + $mlen; $i++) { $sum += $rep[$i] } + $ckv = ([int]$rep[0x2A + $mlen] -shl 8) -bor $rep[0x2A + $mlen + 1] + $v = if (($sum -band 0xFFFF) -eq $ckv) { "OK" } else { "FAIL" } + Write-Host (" checksum calc={0:x4} frame={1:x4} {2}" -f ($sum -band 0xFFFF), $ckv, $v) + break + } catch { + Write-Host "no reply within 3s" + break + } +} +$udp.Close() diff --git a/scripts/session/test_setget.ps1 b/scripts/session/test_setget.ps1 new file mode 100644 index 0000000..12fdae5 --- /dev/null +++ b/scripts/session/test_setget.ps1 @@ -0,0 +1,60 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +# E2E: replay SET frame patched to virtual fixture 130 (addr 200), then GET verify +$lines = [IO.File]::ReadAllLines("$env:TEMP\opencode\set123b.tsv") +$hex = $null +foreach ($l in $lines) { + $p = $l -split "`t" + if ($p.Count -lt 5 -or -not $p[4]) { continue } + if ($p[1] -ne "192.168.123.113") { continue } + $h = $p[4] -replace " ", "" + if ($h.Length -ne 2168) { continue } + if ($h.Substring(0x20 * 2, 4) -ne "0200") { continue } + if ($h.Substring(0x3C * 2, 2) -ne "01") { continue } + if ($h.Substring(0x3D * 2, 2) -ne "1a") { continue } + if ($h.Substring(0x4F * 2, 2) -ne "30") { continue } + if ($h.Substring(0x30 * 2, 2) -ne "00") { continue } + $hex = $h; break +} +if (-not $hex) { throw "no SET frame" } +$n = $hex.Length / 2 +$req = New-Object byte[] $n +for ($i = 0; $i -lt $n; $i++) { $req[$i] = [Convert]::ToByte($hex.Substring(2 * $i, 2), 16) } +# patch: dest UID @0x3E = 010734670130, PD @0x53 = 00 C8 (addr 200) +$vuid = [byte[]](0x01, 0x07, 0x34, 0x67, 0x01, 0x30) +[Array]::Copy($vuid, 0, $req, 0x3E, 6) +$req[0x53] = 0x00; $req[0x54] = 0xC8 + +$udp = New-Object System.Net.Sockets.UdpClient +$udp.Client.ReceiveTimeout = 3000 +$ep = New-Object System.Net.IPEndPoint([System.Net.IPAddress]::Parse("192.168.58.7"), 31772) + +function Send-Recv($frame, $label) { + $sw = [Diagnostics.Stopwatch]::StartNew() + [void]$udp.Send($frame, $frame.Length, $ep) + while ($true) { + try { + $rep = $udp.Receive([ref]$ep) + if ($rep.Length -lt 0x44) { continue } # skip 42B telemetry + $srcS = ($rep[0x33..0x38] | ForEach-Object { $_.ToString("x2") }) -join "" + $pdl = [int]$rep[0x41] + $pdS = "" + if ($pdl -gt 0 -and $rep.Length -ge 0x42 + $pdl) { $pdS = ($rep[0x42..(0x41 + $pdl)] | ForEach-Object { $_.ToString("x2") }) -join "" } + $mlen = [int]$rep[0x2C]; $sum = 0 + for ($i = 0x2A; $i -lt 0x2A + $mlen; $i++) { $sum += $rep[$i] } + $ckv = ([int]$rep[0x2A + $mlen] -shl 8) -bor $rep[0x2A + $mlen + 1] + $v = if (($sum -band 0xFFFF) -eq $ckv) { "OK" } else { "FAIL" } + Write-Host ("{0}: {1}B {2}ms srcUID={3} CC={4:x2} PID={5:x2}{6:x2} PDL={7} PD={8} ck={9}" -f ` + $label, $rep.Length, $sw.ElapsedMilliseconds, $srcS, $rep[0x3E], $rep[0x3F], $rep[0x40], $pdl, $pdS, $v) + return + } catch { Write-Host "${label}: no reply"; return } + } +} + +Send-Recv $req "SET 130 addr=200" +Start-Sleep -Milliseconds 100 +# GET: same frame, msgLen=0x18, CC=0x20, PDL=0 +$get = [byte[]]$req.Clone() +$get[0x3D] = 0x18; $get[0x4F] = 0x20; $get[0x52] = 0x00 +$get[0x22] = $get[0x22] + 1 # bump query seq for clarity +Send-Recv $get "GET 130 addr " +$udp.Close() diff --git a/scripts/session/verify_dmxsend.ps1 b/scripts/session/verify_dmxsend.ps1 new file mode 100644 index 0000000..8d31c15 --- /dev/null +++ b/scripts/session/verify_dmxsend.ps1 @@ -0,0 +1,11 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +if (-not $s) { throw "SSH connect failed" } +$r = Invoke-SSHCommand -SSHSession $s -Command "grep 'aa5501' ~/butler.log | tail -n 5; echo ---; grep -c 'DMX #' ~/butler.log; tail -n 4 ~/butler.log" +$r.Output +Remove-SSHSession -SSHSession $s | Out-Null diff --git a/scripts/session/verify_frame.ps1 b/scripts/session/verify_frame.ps1 new file mode 100644 index 0000000..cb2c250 --- /dev/null +++ b/scripts/session/verify_frame.ps1 @@ -0,0 +1,27 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$lines = [IO.File]::ReadAllLines("$env:TEMP\opencode\ss123.tsv") +$n = 0; $guardHit = 0; $msbZero = 0; $msbZero2 = 0; $first = $null +foreach ($ln in $lines) { + $p = $ln -split "`t" + if ($p.Count -lt 5 -or $p[4].Length -lt 100) { continue } + $hex = $p[4] -replace '[^0-9a-fA-F]','' + if ($hex.Length -ne 2168) { continue } # 1084B = 2168 hex chars + $b = New-Object byte[] 1084 + for ($i = 0; $i -lt 1084; $i++) { $b[$i] = [Convert]::ToByte($hex.Substring($i*2,2),16) } + if ($b[0x20] -ne 1 -or $b[0x21] -ne 1) { continue } # DMX subtype + $n++ + if ($null -eq $first) { $first = $b } + $ch1 = $b[0x3C]; $ch513 = $b[0x23C] + if ($b[0x22] -eq (($ch1 - 1) -bxor 0x70)) { $guardHit++ } + if ($b[0x25] -lt 0x80) { $msbZero++ } + if ($b[0x29] -lt 0x80) { $msbZero2++ } +} +echo "DMX frames: $n" +echo "g1==(CH1-1)^0x70: $guardHit/$n" +echo "byte0x25<0x80 (MSB=0, Random.Next 特征): $msbZero/$n" +echo "byte0x29<0x80: $msbZero2/$n" +if ($first) { + $hdr = ($first[0x18..0x3F] | ForEach-Object { $_.ToString("x2") }) -join ' ' + echo "首帧 0x18-0x3F: $hdr" + echo "首帧 CH1-8: $(($first[0x3C..0x43] | ForEach-Object { $_.ToString('x2') }) -join ' ')" +} diff --git a/scripts/session/watch_claim.ps1 b/scripts/session/watch_claim.ps1 new file mode 100644 index 0000000..3aea1b8 --- /dev/null +++ b/scripts/session/watch_claim.ps1 @@ -0,0 +1,11 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$ErrorActionPreference = "Stop" +Set-ExecutionPolicy -Scope Process Bypass -Force +Import-Module Posh-SSH +$sec = ConvertTo-SecureString "0" -AsPlainText -Force +$cred = New-Object PSCredential("tonycao", $sec) +$s = New-SSHSession -ComputerName 192.168.58.7 -Credential $cred -AcceptKey -ConnectionTimeout 15 +if (-not $s) { throw "SSH connect failed" } +$r = Invoke-SSHCommand -SSHSession $s -Command "tail -n 400 ~/butler.log | grep -nE 'AECH|认领|注册|claim|register|89B' | tail -n 20; echo ---TAIL---; tail -n 5 ~/butler.log" +$r.Output +Remove-SSHSession -SSHSession $s | Out-Null diff --git a/scripts/session/watch_sym_claim.ps1 b/scripts/session/watch_sym_claim.ps1 new file mode 100644 index 0000000..0bbfdf1 --- /dev/null +++ b/scripts/session/watch_sym_claim.ps1 @@ -0,0 +1,16 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +$cap = "$env:TEMP\opencode\sym_claim.pcap" +Remove-Item $cap -ErrorAction SilentlyContinue +$ts = "C:\Program Files\Wireshark\tshark.exe" +$proc = Start-Process -FilePath $ts -ArgumentList "-i `"\Device\NPF_{E7165B1C-BAF6-49CF-9A3D-EF12DBC17329}`" -f `"udp port 31772`" -a duration:90 -w `"$cap`"" -PassThru -WindowStyle Hidden -RedirectStandardError "$env:TEMP\opencode\tshark_err2.txt" +Start-Sleep -Seconds 2 +Start-Process -FilePath "C:\Program Files\ecue\Sympholight 5.5\Sympholight.exe" +Write-Host "SymphoLight started, capturing 90s..." +Start-Sleep -Seconds 88 +Write-Host "=== frame type stats (src dst udplen count) ===" +& $ts -r $cap -T fields -e ip.src -e ip.dst -e udp.length 2>$null | Group-Object { $p = $_ -split "`t"; "$($p[0]) -> $($p[1]) len=$($p[2])" } | Sort-Object Count -Descending | Select-Object Count, Name | Format-Table -AutoSize +Write-Host "=== AERE replies (udplen=560, .1->113) times ===" +& $ts -r $cap -Y "ip.src==192.168.123.1 && udp.length==560" -T fields -e frame.time_relative 2>$null +Write-Host "=== DMX stream (udplen=1092, 113->.1) first/last ===" +$dmx = @(& $ts -r $cap -Y "ip.dst==192.168.123.1 && udp.length==1092" -T fields -e frame.time_relative 2>$null) +if ($dmx.Count -gt 0) { Write-Host (" first t=" + $dmx[0] + " last t=" + $dmx[$dmx.Count-1] + " total " + $dmx.Count) } else { Write-Host " no DMX stream" } diff --git a/scripts/session/webui_probe.ps1 b/scripts/session/webui_probe.ps1 new file mode 100644 index 0000000..f10db5a --- /dev/null +++ b/scripts/session/webui_probe.ps1 @@ -0,0 +1,11 @@ +[Console]::OutputEncoding = [Text.Encoding]::UTF8 +foreach ($u in @("setup.htm","config.htm","reboot.htm","reset.htm","status.htm")) { + try { + $r = Invoke-WebRequest -Uri "http://192.168.123.1/$u" -TimeoutSec 4 -UseBasicParsing + Write-Host ("== " + $u + " (" + $r.StatusCode + ") ==") + [regex]::Matches($r.Content, '<(form|input|select|a)[^>]*>') | ForEach-Object { $_.Value } | Select-Object -First 20 + Write-Host "" + } catch { + Write-Host ("== " + $u + " : " + $_.Exception.Message) + } +} diff --git a/tools/GetTable.cs b/tools/GetTable.cs new file mode 100644 index 0000000..5603a4d --- /dev/null +++ b/tools/GetTable.cs @@ -0,0 +1,28 @@ +using System; +using System.Linq; +using Mono.Cecil; + +class GetTable { + static void Main(string[] args) { + string dll = args[0]; + string fname = args[1]; // e.g. 0A8F2B10... + var asm = AssemblyDefinition.ReadAssembly(dll); + foreach (var mod in asm.Modules) { + foreach (var t in mod.Types) { + if (!t.FullName.Contains("PrivateImplementationDetails")) continue; + foreach (var f in t.Fields) { + if (f.Name != fname) continue; + byte[] iv = f.InitialValue; + Console.WriteLine("field " + f.Name + " len=" + iv.Length); + for (int i = 0; i + 4 <= iv.Length; i += 4) { + uint v = BitConverter.ToUInt32(iv, i); + Console.WriteLine(" [{0,2}] = 0x{1:x8}", i / 4, v); + } + Console.WriteLine("hex: " + BitConverter.ToString(iv).Replace("-", "")); + return; + } + } + } + Console.WriteLine("NOT FOUND"); + } +} diff --git a/tools/IlDump.cs b/tools/IlDump.cs new file mode 100644 index 0000000..41d1fe1 --- /dev/null +++ b/tools/IlDump.cs @@ -0,0 +1,71 @@ +using System; +using System.IO; +using System.Linq; +using Mono.Cecil; +using Mono.Cecil.Cil; + +class IlDump { + static void Main(string[] args) { + string dll = args[0]; + string mode = args.Length > 1 ? args[1] : "types"; + string pat = args.Length > 2 ? args[2].ToLower() : ""; + var rp = new ReaderParameters { ReadSymbols = false }; + var asm = AssemblyDefinition.ReadAssembly(dll, rp); + foreach (var mod in asm.Modules) { + foreach (var t in mod.Types) { + DumpType(t, mode, pat, 0); + } + } + } + static void DumpType(TypeDefinition t, string mode, string pat, int depth) { + bool tmatch = pat.Length == 0 || t.FullName.ToLower().Contains(pat); + if (mode == "types") { + bool anyMethod = t.Methods.Any(m => m.Name.ToLower().Contains(pat)); + if (tmatch || anyMethod) { + Console.WriteLine("TYPE " + t.FullName); + foreach (var m in t.Methods) { + if (tmatch || m.Name.ToLower().Contains(pat)) + Console.WriteLine(" M " + m.FullName); + } + foreach (var f in t.Fields) { + if (tmatch) Console.WriteLine(" F " + f.FieldType.Name + " " + f.Name); + } + } + } else if (mode == "il") { + foreach (var m in t.Methods) { + if (!(t.FullName.ToLower().Contains(pat) || m.Name.ToLower().Contains(pat))) continue; + if (!m.HasBody) continue; + Console.WriteLine("=== " + m.FullName + " ==="); + foreach (var v in m.Body.Variables) + Console.WriteLine(" .local " + v.VariableType.Name + " V_" + v.Index); + foreach (var ins in m.Body.Instructions) { + string op = ins.Operand == null ? "" : " " + OpText(ins.Operand); + Console.WriteLine(string.Format(" IL_{0:x4}: {1}{2}", ins.Offset, ins.OpCode.Name, op)); + } + } + } else if (mode == "xref") { + // 找所有引用了匹配名称的方法/字段的方法体 + foreach (var m in t.Methods) { + if (!m.HasBody) continue; + bool hit = false; + foreach (var ins in m.Body.Instructions) { + string s = ins.Operand == null ? "" : ins.Operand.ToString(); + if (s.ToLower().Contains(pat)) { hit = true; break; } + } + if (hit) Console.WriteLine("XREF " + m.FullName); + } + } + foreach (var nt in t.NestedTypes) DumpType(nt, mode, pat, depth + 1); + } + static string OpText(object o) { + var mr = o as MethodReference; if (mr != null) return mr.FullName; + var fr = o as FieldReference; if (fr != null) return fr.FullName; + var s = o as string; if (s != null) return "\"" + s + "\""; + var ins = o as Instruction; if (ins != null) return "IL_" + ins.Offset.ToString("x4"); + var arr = o as Instruction[]; if (arr != null) return string.Join(",", arr.Select(i => "IL_" + i.Offset.ToString("x4")).ToArray()); + var p = o as ParameterReference; if (p != null) return "arg" + p.Index; + var vr = o as VariableReference; if (vr != null) return "V_" + vr.Index; + var tr = o as TypeReference; if (tr != null) return tr.FullName; + return o == null ? "" : o.ToString(); + } +} diff --git a/tools/Scan.cs b/tools/Scan.cs new file mode 100644 index 0000000..b22c470 --- /dev/null +++ b/tools/Scan.cs @@ -0,0 +1,30 @@ +using System; +using System.IO; + +class Scan { + static int Count(byte[] hay, byte[] needle) { + int n = 0; + for (int i = 0; i + needle.Length <= hay.Length; i++) { + bool m = true; + for (int j = 0; j < needle.Length; j++) if (hay[i + j] != needle[j]) { m = false; break; } + if (m) n++; + } + return n; + } + static void Main(string[] args) { + string dir = args[0]; + string[] pats = { "AECH", "AERE", "AEREP", "Claim", "challenge" }; + foreach (string f in Directory.GetFiles(dir, "*.dll")) { + byte[] b = File.ReadAllBytes(f); + string hits = ""; + foreach (string p in pats) { + byte[] a = System.Text.Encoding.ASCII.GetBytes(p); + byte[] u = System.Text.Encoding.Unicode.GetBytes(p); + int ca = Count(b, a), cu = Count(b, u); + if (ca > 0 || cu > 0) hits += string.Format(" {0}(ascii={1},utf16={2})", p, ca, cu); + } + if (hits.Length > 0) Console.WriteLine(Path.GetFileName(f) + ":" + hits); + } + Console.WriteLine("scan done"); + } +} From fcb20a7fa30d10fbafdc5bcbf6b43f122f082e71 Mon Sep 17 00:00:00 2001 From: tonycao Date: Fri, 11 Sep 2026 08:12:06 +0800 Subject: [PATCH 3/7] =?UTF-8?q?hostdemo:=20RDM=20discovery=20+=20DMX=20con?= =?UTF-8?q?trol=20on=20real=20Butler=20.1=20=E2=80=94=20FULL=20WORKING=20(?= =?UTF-8?q?slfull=20recipe)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hostdemo.rs | 449 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 446 insertions(+), 3 deletions(-) diff --git a/src/hostdemo.rs b/src/hostdemo.rs index bece818..1ae698a 100644 --- a/src/hostdemo.rs +++ b/src/hostdemo.rs @@ -158,6 +158,442 @@ pub fn run(target: &str, bind_ip: &str, secs: u64) { } if !dev_ok { println!("[1] 发现无应答, 终止"); return; } + // replay: verbatim 重放 SL 的完整会话帧流 (sym_startup t=101.3..136), 双阶段定位 RDM 门控 + if std::env::var("HOSTDEMO_MODE").unwrap_or_default() == "replay" { + let path = std::env::var("REPLAY_FILE") + .unwrap_or_else(|_| "C:/Users/tonycao/AppData/Local/Temp/opencode/replay_frames.txt".into()); + let data = std::fs::read_to_string(&path).expect("replay file"); + let mut frames: Vec<(f32, String, u16, Vec)> = Vec::new(); + for line in data.lines() { + let p: Vec<&str> = line.split('|').collect(); + if p.len() < 4 { continue; } + let t: f32 = p[0].parse().unwrap(); + let dst = p[1].to_string(); + let port: u16 = p[2].parse().unwrap(); + let hex = &p[3]; + let mut b = Vec::with_capacity(hex.len() / 2); + for i in (0..hex.len()).step_by(2) { b.push(u8::from_str_radix(&hex[i..i + 2], 16).unwrap()); } + frames.push((t, dst, port, b)); + } + let t0 = frames[0].0; + for phase in ["A(无认领 verbatim)", "B(先认领再 verbatim)"] { + println!("[R] === 阶段 {}: 重放 {} 帧 / {:.1}s ===", phase, frames.len(), frames[frames.len() - 1].0 - t0); + if phase.starts_with('B') { + // 快速认领 + let o2: Vec = bind_ip.split('.').map(|x| x.parse().unwrap()).collect(); + let ident = load_template("announce144"); + sock.send_to(&ident, &dst).unwrap(); + std::thread::sleep(Duration::from_millis(300)); + let nonce = rng.next(); + let bc = rng.next(); + let mut ch = load_template("claim_h112"); + ch[0x1A..0x1E].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]); + ch[0x88..0x8C].copy_from_slice(&nonce.to_le_bytes()); + ch[0x8C..0x90].copy_from_slice(&bc.to_le_bytes()); + sock.send_to(&ch, &dst).unwrap(); + std::thread::sleep(Duration::from_millis(500)); + println!("[R] 认领已发 (B12=0x{:08x})", bc); + } + let start = Instant::now(); + let mut recv_n = 0u64; + let mut fi = 0; + while start.elapsed().as_secs_f32() < frames[frames.len() - 1].0 - t0 + 3.0 { + let now = start.elapsed().as_secs_f32(); + while fi < frames.len() && frames[fi].0 - t0 <= now { + let (_, d, port, b) = &frames[fi]; + let target_addr = format!("{}:{}", d, port); + let _ = sock.send_to(b, &target_addr); + fi += 1; + } + while let Ok((n, src)) = sock.recv_from(&mut buf) { + recv_n += 1; + let b = &buf[..n]; + println!(" [R] ← len={} src={} head={}", n, src, hexs(&b[..std::cmp::min(20, n)])); + } + std::thread::sleep(Duration::from_millis(2)); + } + println!("[R] 阶段 {} 结束: 收到 {} 帧", phase, recv_n); + std::thread::sleep(Duration::from_millis(1500)); + // 排空残留 + while sock.recv_from(&mut buf).is_ok() {} + } + return; + } + + // sls: session-continue — 不认领! 直接续用 SL 会话 (B12=0x4366937a), SL 实帧做模板 + if std::env::var("HOSTDEMO_MODE").unwrap_or_default() == "sls" { + let path = std::env::var("REPLAY_FILE") + .unwrap_or_else(|_| "C:/Users/tonycao/AppData/Local/Temp/opencode/replay_frames.txt".into()); + let data = std::fs::read_to_string(&path).expect("replay file"); + let sb12: u32 = 0x4366937a; // SL 22:52 会话 B12 (Butler 会话状态绑定的值) + // 从重放清单提取模板: DMX 数据帧 / RDM branch 请求 + let mut base_dmx: Option> = None; + let mut base_rdm: Option> = None; + let mut max_ctr: u32 = 0; + for line in data.lines() { + let p: Vec<&str> = line.split('|').collect(); + if p.len() < 4 { continue; } + let hex = p[3]; + if hex.len() < 0x50 * 2 { continue; } + let b = |o: usize| u8::from_str_radix(&hex[o * 2..o * 2 + 2], 16).unwrap(); + if hex.len() == 1084 * 2 && b(0x18) == 1 && b(0x20) == 1 && b(0x21) == 1 && b(0x3c) != 1 { + let ctr = g32(&hex.as_bytes(), 0); // placeholder never used + let _ = ctr; + let mut fb = Vec::with_capacity(1084); + for i in (0..hex.len()).step_by(2) { fb.push(u8::from_str_radix(&hex[i..i + 2], 16).unwrap()); } + let c = u32::from_le_bytes([fb[0x1A], fb[0x1B], fb[0x1C], fb[0x1D]]); + if c > max_ctr { max_ctr = c; } + if base_dmx.is_none() { base_dmx = Some(fb); } + } + if hex.len() == 1084 * 2 && b(0x3c) == 1 && b(0x30) == 0 && b(0x50) == 0 && b(0x51) == 1 && base_rdm.is_none() { + let mut fb = Vec::with_capacity(1084); + for i in (0..hex.len()).step_by(2) { fb.push(u8::from_str_radix(&hex[i..i + 2], 16).unwrap()); } + base_rdm = Some(fb); + } + } + let dmx_t = base_dmx.expect("no DMX template in replay file"); + let rdm_t = base_rdm.expect("no RDM template in replay file"); + let ctr0 = max_ctr + 10; + println!("[SLS] 会话续用: B12=0x{:08x}, DMX模板 counter 基线 {}", sb12, ctr0); + println!("[SLS] DMX模板 0x2E-0x3B: {}", hexs(&dmx_t[0x2E..0x3C])); + let known: [[u8; 6]; 3] = [ + [0x01, 0x07, 0x34, 0x67, 0x01, 0x25], + [0x01, 0x07, 0x34, 0x88, 0x09, 0x00], + [0x08, 0x99, 0x50, 0x00, 0x26, 0xc9], + ]; + let start = Instant::now(); + let mut ctr = ctr0; + let mut last_hb = Instant::now() - Duration::from_secs(10); + let mut last_rdm = Instant::now() - Duration::from_secs(10); + let mut step = 0usize; // 0=UN_MUTE, 1..=3=窄分支, 4+=GET + let mut addr_list: Vec = Vec::new(); + let mut resp_n = 0u64; + while start.elapsed() < Duration::from_secs(secs.max(60)) { + let t = start.elapsed().as_secs_f32(); + if last_hb.elapsed() >= Duration::from_secs(6) { + let _ = sock.send_to(&load_template("heartbeat"), &dst); + last_hb = Instant::now(); + } + // RDM 任务每 ~700ms 一步 + if t > 1.0 && last_rdm.elapsed() >= Duration::from_millis(700) { + last_rdm = Instant::now(); + let mut f = rdm_t.clone(); + f[0x1A..0x1E].copy_from_slice(&ctr.to_le_bytes()); + let mut pd: Vec = Vec::new(); + let mut pid: u16 = 0x0001; + let mut dest = [0xffu8; 6]; + match step { + 0 => { pid = 0x0003; } // UN_MUTE 广播 + 1..=3 => { + let u = known[step - 1]; + let v = uid_u64(&u); + pd.extend_from_slice(&u64_uid(v - 1)); + pd.extend_from_slice(&u64_uid(v + 1)); + } + _ => { + // GET DEVICE_INFO 轮询已发现地址 + if (step - 4) < known.len() { + dest = known[step - 4]; + pid = 0x0060; + } else { step = 3; } // 循环重试 GET + } + } + let msg_len = 24 + pd.len(); + f[0x3C] = 0x01; + f[0x3D] = msg_len as u8; + f[0x3E..0x44].copy_from_slice(&dest); + f[0x4A] = (step * 7 + 1) as u8; + f[0x4F] = if pid == 0x0001 { 0x10 } else if pid == 0x0060 { 0x20 } else { 0x10 }; + f[0x50] = (pid >> 8) as u8; + f[0x51] = (pid & 0xFF) as u8; + f[0x52] = pd.len() as u8; + f[0x53..0x53 + pd.len()].copy_from_slice(&pd); + let mut sum: u16 = 0xCC; + for i in 0x3C..0x3C + msg_len { sum = sum.wrapping_add(f[i] as u16); } + let cs = 0x53 + pd.len(); + f[cs] = (sum >> 8) as u8; + f[cs + 1] = (sum & 0xFF) as u8; + let mut f2 = f.clone(); + f2[0x30] = 0x01; + f2[0x49] = f2[0x49].wrapping_sub(1); + let _ = sock.send_to(&f, &dst); + let _ = sock.send_to(&f2, &dst); + println!("[SLS] RDM step={} pid={:04x} 已发 (ctr={})", step, pid, ctr); + step += 1; + } + // 收包 + while let Ok((n, src)) = sock.recv_from(&mut buf) { + if src.ip().to_string() != target { continue; } + let b = &buf[..n]; + resp_n += 1; + if let Some((is_pulse, d)) = rdm_region(b) { + if is_pulse { + if let Some(uid) = decode_pulse(d) { + println!(" [SLS] ★脉冲 UID={}", uid_s(&uid)); + } else { + println!(" [SLS] 脉冲(冲突/损坏)"); + } + } else if d.len() >= 0x18 + 19 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0x60] { + let a = u16::from_be_bytes([d[0x18 + 14], d[0x18 + 15]]); + let fp = u16::from_be_bytes([d[0x18 + 10], d[0x18 + 11]]); + println!(" [SLS] ★DEVICE_INFO srcUID={} DMX地址={} 通道={}", uid_s(&[d[9], d[10], d[11], d[12], d[13], d[14]]), a, fp); + if !addr_list.contains(&a) { addr_list.push(a); } + } else { + println!(" [SLS] RDM应答 len={}", d.len()); + } + } + } + // DMX @30fps: 发现完成前黑场, 之后呼吸 + let mut p1 = [0u8; 512]; + if !addr_list.is_empty() { + for (k, &a) in addr_list.iter().enumerate() { + let phase = t * 1.2 + k as f32 * 2.1; + let v = (((phase.sin() * 0.5 + 0.5) * 255.0) * ((t - 1.0) * 0.5).min(1.0).max(0.0)) as u8; + let ch = a.max(1) - 1; + if ch < 512 { p1[ch as usize] = v; } + } + } + let r1 = rng.next(); + let r2 = rng.next(); + let mut f = dmx_t.clone(); + f[0x1A..0x1E].copy_from_slice(&ctr.to_le_bytes()); + f[0x22..0x26].copy_from_slice(&r1.to_le_bytes()); + f[0x26..0x2A].copy_from_slice(&r2.to_le_bytes()); + f[0x3C..0x23C].copy_from_slice(&agios_dmx_encrypt(&p1, r1, sb12)); + f[0x23C..0x43C].copy_from_slice(&agios_dmx_encrypt(&[0u8; 512], r2, sb12)); + let _ = sock.send_to(&f, &dst); + ctr += 1; + std::thread::sleep(Duration::from_millis(33)); + } + println!("[SLS] 结束: 应答 {} 帧, 控制地址 {:?}", resp_n, addr_list); + return; + } + + // hybrid: 认领 + 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::new(); // verbatim DMX 帧 + let mut base_rdm: Option> = None; + for line in data.lines() { + let p: Vec<&str> = line.split('|').collect(); + if p.len() < 4 { continue; } + let hex = p[3]; + if hex.len() != 1084 * 2 { continue; } + let b = |o: usize| u8::from_str_radix(&hex[o * 2..o * 2 + 2], 16).unwrap(); + if b(0x18) == 1 && b(0x20) == 1 && b(0x21) == 1 && b(0x3c) != 1 { + let mut fb = Vec::new(); + for i in (0..hex.len()).step_by(2) { fb.push(u8::from_str_radix(&hex[i..i + 2], 16).unwrap()); } + dmxf.push(fb); + } + if base_rdm.is_none() && b(0x3c) == 1 && b(0x30) == 0 && b(0x50) == 0 && b(0x51) == 1 { + let mut fb = Vec::new(); + for i in (0..hex.len()).step_by(2) { fb.push(u8::from_str_radix(&hex[i..i + 2], 16).unwrap()); } + base_rdm = Some(fb); + } + } + // 认领 (announce144 → AECH) + let o2: Vec = bind_ip.split('.').map(|x| x.parse().unwrap()).collect(); + sock.send_to(&load_template("announce144"), &dst).unwrap(); + std::thread::sleep(Duration::from_millis(300)); + let nonce = rng.next(); + let bc = rng.next(); + let mut ch = load_template("claim_h112"); + ch[0x1A..0x1E].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]); + ch[0x88..0x8C].copy_from_slice(&nonce.to_le_bytes()); + ch[0x8C..0x90].copy_from_slice(&bc.to_le_bytes()); + sock.send_to(&ch, &dst).unwrap(); + std::thread::sleep(Duration::from_millis(400)); + println!("[H] 认领完成 B12=0x{:08x}; verbatim DMX {} 帧 + 我们的 RDM", bc, dmxf.len()); + let rdm_t = base_rdm.expect("no RDM template"); + let known: [[u8; 6]; 3] = [ + [0x01, 0x07, 0x34, 0x67, 0x01, 0x25], + [0x01, 0x07, 0x34, 0x88, 0x09, 0x00], + [0x08, 0x99, 0x50, 0x00, 0x26, 0xc9], + ]; + let start = Instant::now(); + let mut di = 0usize; + let mut last_rdm = Instant::now() - Duration::from_secs(10); + let mut step = 0usize; + let mut addr_list: Vec = Vec::new(); + let mut resp_n = 0u64; + while start.elapsed() < Duration::from_secs(secs.max(90)) { + let t = start.elapsed().as_secs_f32(); + // verbatim DMX 循环 + let f = &dmxf[di % dmxf.len()]; + di += 1; + let _ = sock.send_to(f, &dst); + if di % 30 == 0 { let _ = sock.send_to(&load_template("heartbeat"), &dst); } + // RDM 任务 + if t > 2.0 && last_rdm.elapsed() >= Duration::from_millis(700) { + last_rdm = Instant::now(); + let mut f = rdm_t.clone(); + let mut pd: Vec = Vec::new(); + let mut pid: u16 = 0x0001; + let mut dest = [0xffu8; 6]; + let mut cc = 0x10u8; + match step { + 0 => { pid = 0x0003; } + 1..=3 => { + let v = uid_u64(&known[step - 1]); + pd.extend_from_slice(&u64_uid(v - 1)); + pd.extend_from_slice(&u64_uid(v + 1)); + } + _ => { + if (step - 4) < known.len() { dest = known[step - 4]; pid = 0x0060; cc = 0x20; } + else { step = 3; } + } + } + let msg_len = 24 + pd.len(); + f[0x1A..0x1E].copy_from_slice(&(di as u32).to_le_bytes()); + f[0x3D] = msg_len as u8; + f[0x3E..0x44].copy_from_slice(&dest); + f[0x4A] = (step * 7 + 1) as u8; + f[0x4F] = cc; + f[0x50] = (pid >> 8) as u8; + f[0x51] = (pid & 0xFF) as u8; + f[0x52] = pd.len() as u8; + f[0x53..0x53 + pd.len()].copy_from_slice(&pd); + let mut sum: u16 = 0xCC; + for i in 0x3C..0x3C + msg_len { sum = sum.wrapping_add(f[i] as u16); } + let cs = 0x53 + pd.len(); + f[cs] = (sum >> 8) as u8; + f[cs + 1] = (sum & 0xFF) as u8; + let mut f2 = f.clone(); + f2[0x30] = 0x01; + f2[0x49] = f2[0x49].wrapping_sub(1); + let _ = sock.send_to(&f, &dst); + let _ = sock.send_to(&f2, &dst); + println!("[H] RDM step={} pid={:04x}", step, pid); + step += 1; + } + while let Ok((n, src)) = sock.recv_from(&mut buf) { + if src.ip().to_string() != target { continue; } + let b = &buf[..n]; + resp_n += 1; + if let Some((is_pulse, d)) = rdm_region(b) { + if is_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()); + } + } + } + std::thread::sleep(Duration::from_millis(33)); + } + println!("[H] 结束: 应答 {} 帧, 地址 {:?}", resp_n, addr_list); + return; + } + + // slfull: 认领 + verbatim 重放 SL 会话(循环) + 被动解码 RDM 应答 → 发现灯具并打印地址 + // (绕过未破解的变长 RDM 帧结构: 用 SL 原帧当查询, 我们解码应答) + if std::env::var("HOSTDEMO_MODE").unwrap_or_default() == "slfull" { + let path = std::env::var("REPLAY_FILE") + .unwrap_or_else(|_| "C:/Users/tonycao/AppData/Local/Temp/opencode/replay_frames.txt".into()); + let data = std::fs::read_to_string(&path).expect("replay file"); + let mut frames: Vec<(f32, String, u16, Vec)> = Vec::new(); + for line in data.lines() { + let p: Vec<&str> = line.split('|').collect(); + if p.len() < 4 { continue; } + let t: f32 = p[0].parse().unwrap(); + let dst = p[1].to_string(); + let port: u16 = p[2].parse().unwrap(); + let hex = p[3]; + let mut b = Vec::new(); + for i in (0..hex.len()).step_by(2) { b.push(u8::from_str_radix(&hex[i..i + 2], 16).unwrap()); } + frames.push((t, dst, port, b)); + } + let t0 = frames[0].0; + let dur = frames[frames.len() - 1].0 - t0; + // 认领 + let o2: Vec = bind_ip.split('.').map(|x| x.parse().unwrap()).collect(); + sock.send_to(&load_template("announce144"), &dst).unwrap(); + std::thread::sleep(Duration::from_millis(300)); + let nonce = rng.next(); + let bc = rng.next(); + let mut ch = load_template("claim_h112"); + ch[0x1A..0x1E].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]); + ch[0x88..0x8C].copy_from_slice(&nonce.to_le_bytes()); + ch[0x8C..0x90].copy_from_slice(&bc.to_le_bytes()); + sock.send_to(&ch, &dst).unwrap(); + std::thread::sleep(Duration::from_millis(500)); + println!("[F] 认领完成; 开始循环重放 {} 帧 ({:.1}s/轮), 被动解码 RDM 应答", frames.len(), dur); + let start = Instant::now(); + let mut seen_uids: Vec<[u8; 6]> = Vec::new(); + let mut addr_map: Vec<([u8; 6], u16)> = Vec::new(); + let mut resp_n = 0u64; + let mut pat_ctr: u32 = 0; + while start.elapsed() < Duration::from_secs(secs.max(120)) { + let play_start = Instant::now(); + for (t, d, port, b) in frames.iter() { + let target_t = Duration::from_secs_f32(t - t0); + let now = play_start.elapsed(); + if target_t > now { std::thread::sleep(target_t - now); } + let addr = format!("{}:{}", d, port); + // ★DMX 数据帧: 替换为我们自己的呼吸图案 (用认领 B12 加密 → Butler 解密后上灯) + let mut fb = b.clone(); + if fb.len() == 1084 && fb[0x18] == 1 && fb[0x20] == 1 && fb[0x21] == 1 && fb[0x3C] != 1 { + pat_ctr += 1; + let tt = pat_ctr as f32 / 30.0; + let mut p1 = [0u8; 512]; + for (k, &a) in [101u16, 123, 1].iter().enumerate() { + let phase = tt * 1.0 + k as f32 * 2.1; + let v = (((phase.sin() * 0.5 + 0.5) * 220.0) * (tt * 0.3).min(1.0)) as u8; + p1[(a - 1) as usize] = v; + } + let r1 = rng.next(); + let r2 = rng.next(); + fb[0x22..0x26].copy_from_slice(&r1.to_le_bytes()); + fb[0x26..0x2A].copy_from_slice(&r2.to_le_bytes()); + fb[0x3C..0x23C].copy_from_slice(&agios_dmx_encrypt(&p1, r1, bc)); + fb[0x23C..0x43C].copy_from_slice(&agios_dmx_encrypt(&[0u8; 512], r2, bc)); + } + let _ = sock.send_to(&fb, &addr); + // 收包 + while let Ok((n, src)) = sock.recv_from(&mut buf) { + if src.ip().to_string() != target { continue; } + let bb = &buf[..n]; + resp_n += 1; + if let Some((is_pulse, dd)) = rdm_region(bb) { + if is_pulse { + if let Some(uid) = decode_pulse(dd) { + if !seen_uids.contains(&uid) { + seen_uids.push(uid); + println!(" [F] ★发现灯具 UID={}", uid_s(&uid)); + } + } + } else if dd.len() >= 0x18 + 19 && dd[0] == 0xCC && dd[0x15..0x17] == [0x00, 0x60] { + let src_uid = [dd[9], dd[10], dd[11], dd[12], dd[13], dd[14]]; + let a = u16::from_be_bytes([dd[0x18 + 14], dd[0x18 + 15]]); + let fp = u16::from_be_bytes([dd[0x18 + 10], dd[0x18 + 11]]); + if !addr_map.iter().any(|(u, _)| *u == src_uid) { + addr_map.push((src_uid, a)); + println!(" [F] ★{} → DMX地址 {} ({}通道)", uid_s(&src_uid), a, fp); + } + } else if dd.len() >= 0x18 + 2 && dd[0] == 0xCC && dd[0x15..0x17] == [0x00, 0xF0] { + let src_uid = [dd[9], dd[10], dd[11], dd[12], dd[13], dd[14]]; + let a = u16::from_be_bytes([dd[0x18], dd[0x19]]); + if !addr_map.iter().any(|(u, _)| *u == src_uid) { + addr_map.push((src_uid, a)); + println!(" [F] ★{} → DMX_START_ADDRESS={}", uid_s(&src_uid), a); + } + } + } + } + } + } + println!("[F] 结束: 应答 {} 帧; 灯具 {} 台: {:?}; 地址: {:?}", resp_n, seen_uids.len(), + seen_uids.iter().map(uid_s).collect::>(), addr_map); + return; + } + let mut b12 = rng.next(); let mut claim_ok = false; @@ -723,8 +1159,12 @@ fn session_loop(sock: &UdpSocket, dst: &str, target: &str, rng: &mut Rng, b12: u let mut tn: u8 = rng.byte(); let mut qseq: u16 = 1; let mut seq: u32 = 1; - // 发现任务队列: (dest, cc, pid, pd) — None = 分支查询由 walk 状态机生成 + // 发现任务队列: ★窄分支优先 (已知灯具各一个只含自己的分支 — Butler 可能拒转全范围查询) let mut stack: Vec<(u64, u64)> = vec![(0, 0xFFFF_FFFF_FFFF)]; + for u in known.iter() { + let v = uid_u64(u); + stack.push((v - 1, v + 1)); + } let mut get_queue: Vec<[u8; 6]> = Vec::new(); let mut muted: Vec<[u8; 6]> = Vec::new(); let start = Instant::now(); @@ -757,8 +1197,8 @@ fn session_loop(sock: &UdpSocket, dst: &str, target: &str, rng: &mut Rng, b12: u last_key_t = t; } - // --- RDM 任务 (每 ~100ms 一发; ★SL 实测: 认领后 27.5s 纯 DMX, RDM 过早发可能重置武装计时) --- - if discovering && t > 30.0 && last_rdm.elapsed() >= Duration::from_millis(100) { + // --- RDM 任务 (每 ~100ms 一发; 窄分支优先, t>3s 即开始快速定位问题) --- + if discovering && t > 3.0 && last_rdm.elapsed() >= Duration::from_millis(100) { last_rdm = Instant::now(); if collecting && Instant::now() >= collect_until { // 本分支收集结束 → 判定 @@ -804,6 +1244,7 @@ fn session_loop(sock: &UdpSocket, dst: &str, target: &str, rng: &mut Rng, b12: u let _ = sock.send_to(&f, dst); let mut f2 = f.clone(); f2[0x30] = 0x01; + f2[0x49] = f2[0x49].wrapping_sub(1); let _ = sock.send_to(&f2, dst); } } @@ -830,6 +1271,8 @@ fn session_loop(sock: &UdpSocket, dst: &str, target: &str, rng: &mut Rng, b12: u let _ = sock.send_to(&f, dst); let mut f2 = f.clone(); f2[0x30] = 0x01; + // flag=01 副本的 srcUID 末字节 = fe (SL 实测: ff/fe 成对) + f2[0x49] = f2[0x49].wrapping_sub(1); let _ = sock.send_to(&f2, dst); current_branch = Some((lo, hi)); collecting = true; From 3056a6974c8841c727cb695c2c8a521553169bbb Mon Sep 17 00:00:00 2001 From: tonycao Date: Fri, 11 Sep 2026 08:12:07 +0800 Subject: [PATCH 4/7] docs: 11.7 final working recipe + session-level findings --- docs/enet_protocol.md | 55 +++++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/docs/enet_protocol.md b/docs/enet_protocol.md index 19d865f..0e4a922 100644 --- a/docs/enet_protocol.md +++ b/docs/enet_protocol.md @@ -637,29 +637,44 @@ mute 亦无碍);cmd=19/1a 中 SET 类厂商 PID(0x0200 族)写路径未 全链路(认领→注册→加密→解密)闭环自证。对真机 .1 仿冒的唯一额外约束: AECH 质询须源自 192.168.123.113(真机固件过滤,§3.7)。 -### 11.7 hostdemo:真机 .1 主机侧仿真(2026-09-10 晚)★进行中 +### 11.7 hostdemo:真机 .1 主机侧仿真(2026-09-10 晚)★★✅ 全链路完成(RDM 发现 + DMX 控制) -`hostdemo [目标IP] [秒]` 子命令(src/hostdemo.rs):完整主机等效流程——发现 → -冷启动前奏(discovery/announce 广播 ×10s + EUCE)→ 102B 身份帧 → AECH×3 → -DISC_UN_MUTE 广播 → UNIQUE_BRANCH 二分遍历(含 DISC 脉冲解码,§5.4 逆运算) -→ 逐 UID DISC_MUTE → GET DEVICE_INFO/DMX_START_ADDRESS → 30fps 加密 DMX 呼吸控制。 +`hostdemo [目标IP] [秒]` 子命令(src/hostdemo.rs)。当晚会话历经多次假设-证伪, +**最终工作配方(HOSTDEMO_MODE=slfull,2026-09-11 00:3x 灯光实测确认)**: -**实测进展**: -- ✅ 发现应答(217B)每次都正常——设备在任何状态下都答发现 -- ✅ **AECH 认领真机 3 次完整通过**(3 轮 G(nonce) 验证 PASS,nonce/B12 自选随机) -- ⏳ RDM 发现/控制段:尚未跑到(受下方认领门控阻断) -- cold2 时序佐证:首个 RDM 请求在认领后 54ms 发出;首个 RDM 应答与首个 42B - 遥测同步出现于认领后 1.37s(此前主机已连发 ~14 次 UNIQUE_BRANCH—— - 冷启动应答延迟大属正常,hostdemo 每分支按 4 次 ×250ms 重试) +``` +1. 发现(type=1 → 217B) +2. 认领: announce144(102B 身份帧) 单播 → 47B 注册应答 → AECH 质询 → AERE=G(nonce) ✓ + (★announce144 即 ident102 同帧; 完整冷启动前奏/绑定帧/EUCE 反而无效) +3. verbatim 重放 SL 真实会话帧流(replay_merged.txt, 32540 帧/126.7s 循环): + - DMX 数据帧: 保留 SL 原帧结构, 仅替换 r1/r2 + 密文数据段 + (用本会话认领的 B12 重加密呼吸图案 → Butler 解密上灯) + - RDM 请求: SL 原帧(UN_MUTE/UNIQUE_BRANCH/GET, 含 ff/fe 成对与可变偏移结构) +4. 被动解码应答: DISC 脉冲→UID, DEVICE_INFO/START_ADDRESS→DMX 地址 +``` -**★新发现:真机认领门控/锁死行为(§3.7 补遗)** -连续 3 次成功认领但**不发任何会话流量**(无 DMX/心跳)后,.1 进入锁定态: -- 对 ident102 不回 47B 注册应答、对 AECH 质询完全不答—— - **连 cold2 原始 ident/质询的逐字节重放也一并忽略**(设备端状态,非帧构造问题) -- 发现请求(type=1)仍正常应答(225B) -- SymphoLight 于锁定约 55-70 分钟后认领成功(19:2x,期间它先广播了 >90s 才发起) -- SymphoLight 关闭(20:04,无释放帧、无 AECH,DMX 流直接消失)后 15 分钟仍锁定 -- 暂定模型:**会话保持 ≈ 55-60 分钟**(自最后一次会话流量起算),期间拒答一切新认领 +**实测结果(灯光肉眼确认呼吸)**: +- ✅ RDM 发现 3/3: `010734670125`(NFS1K2D)@101/2ch、`010734880900`(NFM680S)@123、 + `0899500026c9`(HMKJ)@1/32ch(应答 15.5k 帧) +- ✅ DMX 控制: ch101/ch123/ch1 异相呼吸(fade-in 渐入)**灯光确认** + +**★会话机制深层发现(本晚核心)**: +- **AECH 握手成功 ≠ 会话数据通道打通**——认领(47B/AERE 全过)后,自构造的 + type=4 帧(DMX/RDM)仍被静默丢弃;而 verbatim 重放 SL 的历史会话帧立即全通 + (遥测/RDM 应答恢复)→ Butler 在帧级校验某种我们尚未破解的会话一致性 +- **RDM 请求帧的消息起始位置可变**(UN_MUTE@0x3A vs UNIQUE_BRANCH@0x3C, + 前+len16 字段位置随之浮动 2B)——固定偏移构造的 RDM 帧必被拒;变长规则未破解 + (实用绕过: 直接重放 SL 原帧) +- 1947 端口"密钥报文"实为 **40B ASCII 许可令牌**(`BlZRQphA…`,恒定值,双目标 + 21 帧 @200ms 每 ~34s 一轮);EUCE 实为全程每 6s 的许可信标 +- 真机认领门控行为(当晚早段观察): 灯具 MUTE 态跨会话持久(SL 遗留的 mute + 挡住后续 branch 查询);SymphoLight 关闭无释放帧,DMX 流直接消失 + +**遗留**:RDM 帧变长结构规则(破解后可用纯自构造帧完成发现,摆脱对 SL 原帧的依赖)。 + +**历史过程记录(假设-证伪链,供复盘)**:完整冷启动前奏(45s)/绑定帧×4/EUCE 6s 节奏/ +新鲜时间戳宣告/1947 密钥/会话保持 2h 模型——均单因素排除;最终由 verbatim 重放 +二分(阶段 A 无认领 vs 阶段 B 先认领)锁定"认领 + 帧级会话一致性"双条件。 --- From a4a3ed916fddeabfccf89ea70ecfdf645140ee06 Mon Sep 17 00:00:00 2001 From: tonycao Date: Fri, 11 Sep 2026 08:20:53 +0800 Subject: [PATCH 5/7] docs: key inventory (3.7), session recap (hostdemo_recap), 11.7 key-sources table --- docs/enet_protocol.md | 15 ++++++ docs/hostdemo_recap_2026-09-10.md | 80 +++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 docs/hostdemo_recap_2026-09-10.md diff --git a/docs/enet_protocol.md b/docs/enet_protocol.md index 0e4a922..0f8b8df 100644 --- a/docs/enet_protocol.md +++ b/docs/enet_protocol.md @@ -167,6 +167,21 @@ TABLE = 32×u32 常量表, 内嵌于 DLL 静态数组 (0A8F2B10..., 128B): **实测过滤行为**:真机 .1 只应答**源 IP=192.168.123.113** 的 AECH(任意源端口均可), 其他源 IP 的质询直接丢弃(这是真机固件行为;虚拟设备无需此过滤)。 +**★密钥来源与可变性盘点(2026-09-11,hostdemo 实战确认)**: + +| 密钥/敏感量 | 来源 | 可变性 | 实测 | +|---|---|---|---| +| nonce(0x88-0x8B) | 主机每次质询随机生成 | ✅ 完全自由 | 每轮不同,均 PASS | +| B12(0x8C-0x8F,DMX 混淆基值) | 主机认领时随机生成,设备经 AECH 登记为会话基值 | ✅ 完全自由 | 每次认领不同,DMX 均被接受 | +| G 表(32×u32) | 内嵌 Ecue.Devices.dll 静态数组 | ❌ 全产品固定常量 | 10/10 样本命中 | +| 1947 令牌(40B ASCII `BlZRQphA…`) | SymphoLight 安装许可证绑定 | ⚠️ 安装级常量(跨天稳定,换 license 才变) | 从 SL 抓包捕获,随重放广播;**是否必需未做消融** | +| AECH 帧内 92B+148B 随机填充 | 任意(黑盒实证设备完全忽略) | ✅ 完全自由 | 模板陈旧值亦可 | + +结论:**核心密钥(nonce/B12)完全自主可控、逐会话随机,无外部密钥依赖**; +外部依赖仅两项——1947 许可令牌(安装级,可从任意一次 SL 会话重抓)与 +RDM 帧变长结构(当前以重放 SL 原帧绕过,见 §11.7)。 + + 认领通过后:主机开始 DMX 会话(§4),心跳 sub=03(36B,每 ~6s,**设备不应答**)。 --- diff --git a/docs/hostdemo_recap_2026-09-10.md b/docs/hostdemo_recap_2026-09-10.md new file mode 100644 index 0000000..22cf358 --- /dev/null +++ b/docs/hostdemo_recap_2026-09-10.md @@ -0,0 +1,80 @@ +# hostdemo 攻坚总结 — 真机 Butler .1 主机侧仿真全链路打通 + +**日期**:2026-09-10 晚 ~ 09-11 凌晨 +**结论**:✅ 认领 + RDM 设备发现(3/3)+ DMX 呼吸控制(灯光肉眼确认) +**代码**:`HOSTDEMO_MODE=slfull`(src/hostdemo.rs),commit fcb20a7/3056a69 + +--- + +## 一、最终工作配方 + +``` +1. 发现: type=1 (180B) → 217B 应答 +2. 认领: announce144 (=102B 身份帧) 单播 → 47B 注册应答 + → AECH 质询 (nonce/B12 自选随机) → AERE = G(nonce) ✓ +3. 循环 verbatim 重放 SL 真实会话帧流 (32540 帧 / 126.7s): + - DMX 数据帧: 保留 SL 原帧全部结构, 仅替换 r1/r2 + 密文段 + (呼吸图案用本次认领的 B12 重加密 → Butler 解密上灯) + - RDM 请求: SL 原帧原样 (UN_MUTE / UNIQUE_BRANCH / GET, ff+fe 成对) +4. 被动解码应答: DISC 脉冲 → UID; DEVICE_INFO / START_ADDRESS → DMX 地址 +``` + +**实测**:3/3 灯具发现(NFS1K2D@101/2ch、NFM680S@123、HMKJ@1/32ch, +应答 15.5k 帧)+ 三色异相呼吸(fade-in),**灯光肉眼确认**。 + +运行方式: +``` +set HOSTDEMO_MODE=slfull +set REPLAY_FILE=<重放清单.txt> (tshark 提取: time|dst|dstport|payload) +enet-validator hostdemo 192.168.123.1 480 +``` + +--- + +## 二、假设-证伪链(当晚完整过程) + +| # | 假设 | 实验 | 结果 | +|---|---|---|---| +| 1 | 认领后需长前奏 | 45s discovery/announce 广播 | ❌ 无效 | +| 2 | 需要 sub=07 绑定帧 | 按实测节奏重放 ×4 | ❌ 无效 | +| 3 | 宣告时间戳过期被拒 | live 模板 + 新鲜时间戳 | ❌ 无效 | +| 4 | 会话保持 ~2h 需等待 | 45→86 分钟轮询探针 | ❌ 超时后仍拒 | +| 5 | 1947 密钥/EUCE 6s 节奏门控 | 会话内补发 | ❌ 单独无效 | +| 6 | RDM 帧字段差异 | 模板 vs 实帧 0 差异 | ❌ 排除 | +| 7 | **verbatim 重放二分** | 阶段 A(无认领)/B(先认领) | ✅ **B 全通** | + +**教训**:帧级逐字节比对全通过后仍被拒时,问题在会话状态层—— +用 verbatim 重放做二分是定位此类问题的最快路径。 + +--- + +## 三、深层发现(已写入协议文档 §3.7/§11.7) + +1. **AECH 握手成功 ≠ 会话通道打通**:47B/AERE 全过但自构造 type=4 帧被静默丢弃; + verbatim 的 SL 历史帧立即全通 → Butler 在帧级校验某种会话一致性(规则未破解)。 +2. **RDM 请求帧消息起始位置可变**:UN_MUTE@0x3A vs UNIQUE_BRANCH@0x3C + (前置 len16 字段浮动 2B)→ 固定偏移构造必被拒 → 重放绕过。 +3. **announce144 == ident102**(同一帧两种称呼);真机冷启动序列: + 广播前奏 → 身份帧 → 47B → AECH×3 → DMX+RDM(首应答延迟 ~1.4s 属正常)。 +4. **1947 端口"密钥报文"** = 40B ASCII 许可令牌(安装级常量,双目标 21 帧 @200ms + 每 ~34s 一轮);**EUCE** = 全程每 6s 的许可信标。 +5. **灯具 MUTE 态跨会话持久**(SL 遗留 mute 挡后续 branch 查询)。 +6. **密钥盘点**:nonce/B12 逐会话随机自主可控;G 表固定;1947 令牌安装级常量 + (未做必需性消融)。详见协议文档 §3.7 密钥盘点表。 + +--- + +## 四、遗留事项 + +- [ ] RDM 帧变长结构规则(破解后可纯自构造帧发现,摆脱对 SL 原帧依赖) +- [ ] 1947 令牌必需性消融实验 +- [ ] NFM680S 通道数应答漂移(2ch vs 1ch,疑查询时机差异) +- [ ] 会话级校验的具体算法(当前只知道"verbatim 能过") + +## 五、产物清单 + +- 代码:`src/hostdemo.rs`(slfull/slclaim/sls/hybrid/replay 五模式) +- 重放清单:`replay_merged.txt`(sym_startup 101-520s + rdm3 全程,32540 帧) +- 抓包:`sym_startup.pcap`(认领+发现全程)、`sym_claim2.pcap`(接管+重启)、 + `allports.pcap`(全端口,1947 发现)、`hd_claim3.pcap` 等 +- 文档:`enet_protocol.md` §3.7/§11.7 本夜更新 From b644bdae7945c04613d6317b07e1769b5bc692d2 Mon Sep 17 00:00:00 2001 From: tonycao Date: Fri, 11 Sep 2026 08:47:42 +0800 Subject: [PATCH 6/7] =?UTF-8?q?fix:=20variable-offset=20theory=20falsified?= =?UTF-8?q?=20(fixed=20layout=20@0x3C/0x34/0x30=20confirmed);=20single-fra?= =?UTF-8?q?me=20verbatim=20rejected=20outside=20stream=20context=20?= =?UTF-8?q?=E9=88=A5=3Fgating=20is=20stream-level?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/enet_protocol.md | 13 +++-- src/hostdemo.rs | 113 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 4 deletions(-) diff --git a/docs/enet_protocol.md b/docs/enet_protocol.md index 0f8b8df..8ad1f92 100644 --- a/docs/enet_protocol.md +++ b/docs/enet_protocol.md @@ -677,15 +677,20 @@ AECH 质询须源自 192.168.123.113(真机固件过滤,§3.7)。 - **AECH 握手成功 ≠ 会话数据通道打通**——认领(47B/AERE 全过)后,自构造的 type=4 帧(DMX/RDM)仍被静默丢弃;而 verbatim 重放 SL 的历史会话帧立即全通 (遥测/RDM 应答恢复)→ Butler 在帧级校验某种我们尚未破解的会话一致性 -- **RDM 请求帧的消息起始位置可变**(UN_MUTE@0x3A vs UNIQUE_BRANCH@0x3C, - 前+len16 字段位置随之浮动 2B)——固定偏移构造的 RDM 帧必被拒;变长规则未破解 - (实用绕过: 直接重放 SL 原帧) +- **~~RDM 请求帧消息起始位置可变~~(09-11 更正:解析错误,实为固定)**: + 深夜复核证实所有 RDM 请求帧布局恒定——SubSC/msgLen@0x3C、len16(msgLen+1)@0x34、 + flag@0x30(00 独立/01 副本,配对 srcUID 末字节 ff/fe,UN_MUTE 同样成对)、 + counter@0x1A、qseq@0x22、TN@0x4A。此前"0x3A vs 0x3C 变长"系手工解析错位 +- **★单帧 verbatim 在流外仍被拒**(slbisect 突变实验:SL 原版 GET→NFS 帧单独 + 发送 → 零应答;同一帧在完整流重放中 → 正常应答)→ **门控在会话流上下文** + (疑似流级防重放指纹/状态机,具体算法未破解)——实用方案即完整流重放 - 1947 端口"密钥报文"实为 **40B ASCII 许可令牌**(`BlZRQphA…`,恒定值,双目标 21 帧 @200ms 每 ~34s 一轮);EUCE 实为全程每 6s 的许可信标 - 真机认领门控行为(当晚早段观察): 灯具 MUTE 态跨会话持久(SL 遗留的 mute 挡住后续 branch 查询);SymphoLight 关闭无释放帧,DMX 流直接消失 -**遗留**:RDM 帧变长结构规则(破解后可用纯自构造帧完成发现,摆脱对 SL 原帧的依赖)。 +**遗留**:会话流上下文门控的具体算法(破解后可用纯自构造帧完成发现,摆脱对 +SL 原帧重放的依赖——当前 slfull 重放方案已可复现全部功能,不影响使用)。 **历史过程记录(假设-证伪链,供复盘)**:完整冷启动前奏(45s)/绑定帧×4/EUCE 6s 节奏/ 新鲜时间戳宣告/1947 密钥/会话保持 2h 模型——均单因素排除;最终由 verbatim 重放 diff --git a/src/hostdemo.rs b/src/hostdemo.rs index 1ae698a..5634c8a 100644 --- a/src/hostdemo.rs +++ b/src/hostdemo.rs @@ -594,6 +594,119 @@ pub fn run(target: &str, bind_ip: &str, secs: u64) { return; } + // slbisect: SL 原帧逐字段突变 — 定位 Butler 拒收我们 RDM 帧的确切字段 + // 突变序列: verbatim(对照) → counter → TN → qseq → 全部 → 纯我们构造 + if std::env::var("HOSTDEMO_MODE").unwrap_or_default() == "slbisect" { + // 认领 + let o2: Vec = bind_ip.split('.').map(|x| x.parse().unwrap()).collect(); + sock.send_to(&load_template("announce144"), &dst).unwrap(); + std::thread::sleep(Duration::from_millis(300)); + let nonce = rng.next(); + let bc = rng.next(); + let mut ch = load_template("claim_h112"); + ch[0x1A..0x1E].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]); + ch[0x88..0x8C].copy_from_slice(&nonce.to_le_bytes()); + ch[0x8C..0x90].copy_from_slice(&bc.to_le_bytes()); + sock.send_to(&ch, &dst).unwrap(); + std::thread::sleep(Duration::from_millis(500)); + println!("[B] 认领完成 B12=0x{:08x}", bc); + // verbatim DMX 背景流 + 突变 branch 查询 + let path = std::env::var("REPLAY_FILE") + .unwrap_or_else(|_| "C:/Users/tonycao/AppData/Local/Temp/opencode/replay_frames.txt".into()); + let data = std::fs::read_to_string(&path).expect("replay file"); + let mut dmxf: Vec> = Vec::new(); + for line in data.lines() { + let p: Vec<&str> = line.split('|').collect(); + if p.len() < 4 { continue; } + let hex = p[3]; + if hex.len() != 1084 * 2 { continue; } + let byte = |o: usize| u8::from_str_radix(&hex[o * 2..o * 2 + 2], 16).unwrap(); + if byte(0x18) == 1 && byte(0x20) == 1 && byte(0x21) == 1 && byte(0x3c) != 1 { + let mut fb = Vec::new(); + for i in (0..hex.len()).step_by(2) { fb.push(u8::from_str_radix(&hex[i..i + 2], 16).unwrap()); } + dmxf.push(fb); + } + } + let sl_branch = std::fs::read_to_string("C:/Users/tonycao/AppData/Local/Temp/opencode/sl_branch_exact.hex") + .unwrap_or_else(|_| String::new()); + let mut slb: Vec = Vec::new(); + { + let h = sl_branch.trim(); + for i in (0..h.len()).step_by(2) { slb.push(u8::from_str_radix(&h[i..i + 2], 16).unwrap()); } + } + // NFS1K2D GET DEVICE_INFO 探针 (被 MUTE 的设备也须应答 GET — 应答可观测) + let sl_get = std::fs::read_to_string("C:/Users/tonycao/AppData/Local/Temp/opencode/sl_get_exact.hex") + .unwrap_or_default(); + let mut slg: Vec = Vec::new(); + { + let h = sl_get.trim(); + for i in (0..h.len()).step_by(2) { slg.push(u8::from_str_radix(&h[i..i + 2], 16).unwrap()); } + } + let known: [[u8; 6]; 3] = [ + [0x01, 0x07, 0x34, 0x67, 0x01, 0x25], + [0x01, 0x07, 0x34, 0x88, 0x09, 0x00], + [0x08, 0x99, 0x50, 0x00, 0x26, 0xc9], + ]; + println!("[B] 开始突变序列 (GET→NFS; 每步 1s DMX 背景 + 发送 + 500ms 侦听)"); + let mut di = 0usize; + let mutations = ["verbatim对照", "counter=ours", "TN=ours", "qseq=ours", "全突变"]; + for round in 0..2 { + for (mi, mname) in mutations.iter().enumerate() { + let seg = Instant::now(); + while seg.elapsed() < Duration::from_millis(1000) { + let f = &dmxf[di % dmxf.len()]; + di += 1; + let _ = sock.send_to(f, &dst); + if di % 30 == 0 { let _ = sock.send_to(&load_template("heartbeat"), &dst); } + std::thread::sleep(Duration::from_millis(33)); + while sock.recv_from(&mut buf).is_ok() {} + } + let mut f = slg.clone(); + let ctr = (di as u32).wrapping_add(9000); + match mi { + 1 => { f[0x1A..0x1E].copy_from_slice(&ctr.to_le_bytes()); } + 2 => { f[0x4A] = 0x77; } + 3 => { f[0x22] = 0x55; f[0x23] = 0x00; } + 4 => { + f[0x1A..0x1E].copy_from_slice(&ctr.to_le_bytes()); + f[0x4A] = 0x78; + f[0x22] = 0x56; f[0x23] = 0x00; + } + _ => {} + } + let _ = sock.send_to(&f, &dst); + let mut f2 = f.clone(); + f2[0x30] = 0x01; + f2[0x49] = f2[0x49].wrapping_sub(1); + let _ = sock.send_to(&f2, &dst); + let mut info = false; + let mut any = 0usize; + let w = Instant::now(); + // ★边发 DMX 边侦听 (Butler 仅在 DMX 流持续时吐应答, 停流即饿死) + while w.elapsed() < Duration::from_millis(500) { + let f = &dmxf[di % dmxf.len()]; + di += 1; + let _ = sock.send_to(f, &dst); + if di % 30 == 0 { let _ = sock.send_to(&load_template("heartbeat"), &dst); } + while let Ok((n, src)) = sock.recv_from(&mut buf) { + if src.ip().to_string() != target { continue; } + let b = &buf[..n]; + any += 1; + if let Some((false, d)) = rdm_region(b) { + if d.len() >= 0x18 + 19 && d[0] == 0xCC && d[0x15..0x17] == [0x00, 0x60] { + info = true; + } + } + } + std::thread::sleep(Duration::from_millis(33)); + } + println!(" [B] r{} {:<14} → 收帧 {} DEVICE_INFO={}", round, mname, any, info); + } + } + let _ = known; + return; + } + let mut b12 = rng.next(); let mut claim_ok = false; From 9580613ab200522914585148a867a5ee5730748e Mon Sep 17 00:00:00 2001 From: tonycao Date: Fri, 11 Sep 2026 10:08:03 +0800 Subject: [PATCH 7/7] =?UTF-8?q?fix:=20variable-offset=20theory=20falsified?= =?UTF-8?q?=20(fixed=20layout=20@0x3C/0x34/0x30=20confirmed);=20single-fra?= =?UTF-8?q?me=20verbatim=20rejected=20outside=20stream=20context=20?= =?UTF-8?q?=E9=88=A5=3Fgating=20is=20stream-level?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/enet_protocol.md | 31 +++++++++++++++++++++++++++++-- src/hostdemo.rs | 28 ++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/docs/enet_protocol.md b/docs/enet_protocol.md index 8ad1f92..b167456 100644 --- a/docs/enet_protocol.md +++ b/docs/enet_protocol.md @@ -689,8 +689,35 @@ AECH 质询须源自 192.168.123.113(真机固件过滤,§3.7)。 - 真机认领门控行为(当晚早段观察): 灯具 MUTE 态跨会话持久(SL 遗留的 mute 挡住后续 branch 查询);SymphoLight 关闭无释放帧,DMX 流直接消失 -**遗留**:会话流上下文门控的具体算法(破解后可用纯自构造帧完成发现,摆脱对 -SL 原帧重放的依赖——当前 slfull 重放方案已可复现全部功能,不影响使用)。 +**★09-11 凌晨终极突破:RDM 代理开关 = 周期宣告/发现广播(消融矩阵实证)** + +| 重放/发送配置 | RDM 发现 | +|---|---| +| 全量流 | ✅ | +| 去令牌 / 去心跳 / 去 EUCE / 去 DMX 数据 | ✅ 各自 3/3 | +| **去宣告+发现广播** | ❌ 0 | +| 仅 RDM / RDM+DMX | ❌ 0 | + +→ **Butler 只为持续周期广播宣告(644B)/发现(188B) 的主机转发 RDM**(~2-3s 周期, +SL 天然满足)。SL 关闭后 RDM 哑、重开即通、我们的会话环无广播即哑——全部现象 +由此统一解释(含"会话保持"假象)。 + +**★最终工作配方 v2(hostdemo hybrid 模式,100% 自构造帧)**: +``` +认领(announce144 → 47B → AECH) → 循环@30fps: + - 广播: discovery + announce 每 2.5s ← RDM 代理开关 + - DMX: 自构造帧, r1/r2 随机, 数据=呼吸图案(认领 B12 加密) + - RDM: 自构造 GET DEVICE_INFO / UNIQUE_BRANCH (窄分支/全范围均可) + - 心跳: 36B sub=03 每 ~1s +``` +实测: 3/3 灯具 DEVICE_INFO 持续应答(101/123/1),零 SL 原帧依赖 +(DMX 骨架帧的常量字段 0x2E-0x3B 暂取自 SL 模板)。 +注: 灯具断开时 Butler 仍代答 discovery/GET(设备表缓存)——分支/GET 应答 +不能作为灯具物理在线的证据,须以 DEVICE_LABEL/SENSOR 等实测量确认。 + +**遗留**:DMX 帧骨架常量字段 0x2E-0x3B 的语义(当前照抄 SL 值);流级门控 +与广播门控的关系(广播已证实充分, 是否还需其它条件未知——纯自构造全帧已通, +说明广播+认领即充分)。 **历史过程记录(假设-证伪链,供复盘)**:完整冷启动前奏(45s)/绑定帧×4/EUCE 6s 节奏/ 新鲜时间戳宣告/1947 密钥/会话保持 2h 模型——均单因素排除;最终由 verbatim 重放 diff --git a/src/hostdemo.rs b/src/hostdemo.rs index 5634c8a..4646b42 100644 --- a/src/hostdemo.rs +++ b/src/hostdemo.rs @@ -418,12 +418,36 @@ pub fn run(target: &str, bind_ip: &str, secs: u64) { let mut step = 0usize; let mut addr_list: Vec = Vec::new(); let mut resp_n = 0u64; + // ★消融实证: 周期宣告/发现广播是 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)) { let t = start.elapsed().as_secs_f32(); + if last_bcast.elapsed() >= Duration::from_millis(2500) { + let _ = sock.send_to(&disc, "255.255.255.255:31772"); + let _ = sock.send_to(&ann, &format!("{}:31772", target.rsplitn(2, '.').nth(1).unwrap_or("192.168.123").to_string() + ".255")); + last_bcast = Instant::now(); + } // verbatim DMX 循环 - let f = &dmxf[di % dmxf.len()]; + // ★纯自构造 DMX 帧 (认领 B12 加密呼吸图案) — 彻底摆脱 SL 原帧依赖 + 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; + } + 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)); 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); } // RDM 任务 if t > 2.0 && last_rdm.elapsed() >= Duration::from_millis(700) {