diff --git a/examples/decisive_batch.jsonl b/examples/decisive_batch.jsonl new file mode 100644 index 0000000..8804c1a --- /dev/null +++ b/examples/decisive_batch.jsonl @@ -0,0 +1,3 @@ +{"argv": ["activate", "--show-id"]} +{"argv": ["raw", "e10201", "--channel", "alt"]} +{"argv": ["raw", "e2", "--channel", "alt"]} diff --git a/src/ppclock/bridge.py b/src/ppclock/bridge.py index bd80cdc..ed6fd58 100644 --- a/src/ppclock/bridge.py +++ b/src/ppclock/bridge.py @@ -143,7 +143,9 @@ class BridgeTransport: await self._rpc("write", char="epd", data=data.hex(), response=response) async def write_rxtx(self, data: bytes, response: bool = True): - await self._rpc("write", char="rxtx", data=data.hex(), response=response) + # 实机证据(2026-07-28):活命令通道是 331f(bridge 服务端 "alt"), + # 1f1f 仅 ATT-ACK 不执行 → RXTX 写统一走 alt + await self._rpc("write", char="alt", data=data.hex(), response=response) async def write_alt(self, data: bytes, response: bool = True): """经 331f 通道写(实机证据:1f1f 仅 ATT-ACK,331f 为活命令通道)。""" diff --git a/src/ppclock/protocol.py b/src/ppclock/protocol.py index ed0a39d..7aea6eb 100644 --- a/src/ppclock/protocol.py +++ b/src/ppclock/protocol.py @@ -4,11 +4,12 @@ from __future__ import annotations import datetime from typing import Iterator -# GATT(docs/protocol.md §1) +# GATT(docs/protocol.md §1;实机 GATT 表 2026-07-28 经 bridge 诊断确认) EPD_SERVICE_UUID = "13187b10-eba9-a3ba-044e-83d3217d9a38" EPD_CHAR_UUID = "4b646063-6264-f3a7-8941-e65356ea82fe" RXTX_SERVICE_UUID = "00001f10-0000-1000-8000-00805f9b34fb" -RXTX_CHAR_UUID = "00001f1f-0000-1000-8000-00805f9b34fb" +RXTX_CHAR_UUID = "0000331f-0000-1000-8000-00805f9b34fb" # 活命令通道(h=57, notify+write+read) +RXTX_CHAR_UUID_LEGACY = "00001f1f-0000-1000-8000-00805f9b34fb" # h=53 仅write,本设备不执行命令 DEVICE_NAME_PREFIX = "NRF-" BLOCK_PAYLOAD = 240 # my_step=480 hex 字符 = 240 数据字节 @@ -179,9 +180,11 @@ def keygen_code(mac: bytes) -> bytes: def recover_mac_from_device_id(device_id: str) -> bytes: - """14hex 设备 ID → 6B MAC。 - ID = %02X(|MAC4-0x99|,|MAC5-0x99|,|MAC3-0x99|,|MAC2-0x99|,|MAC1-0x99|,|MAC0-0x99|,flags), - flags bit i = MAC[i] ≥ 0x9A(证据 §4.2 生成函数 0x07FCA22C)。""" + """14hex 设备 ID → 6B MAC(NVDS 字节序)。 + ID = %02X(|MAC5-0x99|,|MAC4-0x99|,|MAC3-0x99|,|MAC2-0x99|,|MAC1-0x99|,|MAC0-0x99|,flags), + flags bit i = MAC[i] ≥ 0x9A。 + 证据:实机 2026-07-28 ID '81233F3C267112' ↔ 广播 18:BC:5A:5D:BF:28(校正 + firmware-analysis §4.2 中前两个参数的顺序,固件 snprintf 实参序为 MAC5,MAC4)。""" if len(device_id) != 14: raise ValueError("设备 ID 必须 14 个 hex 字符") try: @@ -189,8 +192,8 @@ def recover_mac_from_device_id(device_id: str) -> bytes: except ValueError: raise ValueError("设备 ID 含非 hex 字符") from None *v, flags = vals - # v 顺序:MAC4,MAC5,MAC3,MAC2,MAC1,MAC0 → 反映射 - order = [4, 5, 3, 2, 1, 0] + # v 顺序:MAC5,MAC4,MAC3,MAC2,MAC1,MAC0 → 反映射 + order = [5, 4, 3, 2, 1, 0] mac = [0] * 6 for vi, idx in zip(v, order): if flags & (1 << idx): diff --git a/tests/test_bridge.py b/tests/test_bridge.py index db8bf9e..8b39b5f 100644 --- a/tests/test_bridge.py +++ b/tests/test_bridge.py @@ -99,7 +99,8 @@ async def test_write_channels_and_hex(server): await t.write_rxtx(bytes([0xE2]), response=False) w = [r for r in s.received if r["op"] == "write"] assert w[0]["char"] == "epd" and w[0]["data"] == "03ff" - assert w[1]["char"] == "rxtx" and w[1]["data"] == "e2" and w[1]["response"] is False + # RXTX 写走 alt(331f) 活通道(实机证据) + assert w[1]["char"] == "alt" and w[1]["data"] == "e2" and w[1]["response"] is False @pytest.mark.asyncio diff --git a/tests/test_cli.py b/tests/test_cli.py index ef8012e..604a153 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -213,11 +213,11 @@ class TestOta: class TestActivateAuto: def test_activate_auto_via_device_id(self, capsys): - """EFEF 应答 ID '89794900980101' → MAC 9A:01:99:50:10:20 → keygen → EF 下发""" + """EFEF 应答 ID '81233F3C267112' → MAC 28:BF:5D:5A:BC:18 → keygen → EF 下发""" class IdTransport(FakeTransport): async def request_device_id(self, timeout=8.0): self.rxtx_writes.append(bytes([0xEF, 0xEF])) - return "89794900980101" + return "81233F3C267112" FakeTransport.instances.clear() import ppclock.cli as c orig = c._make_transport @@ -231,7 +231,7 @@ class TestActivateAuto: assert t.rxtx_writes[0] == bytes([0xEF, 0xEF]) # keygen(9A,01,99,50,10,20):逐字节 rol2(((m<<1)&FF)^EF)%101 from ppclock.protocol import keygen_code - expected = keygen_code(bytes([0x9A, 0x01, 0x99, 0x50, 0x10, 0x20])) + expected = keygen_code(bytes([0x28, 0xBF, 0x5D, 0x5A, 0xBC, 0x18])) assert t.rxtx_writes[1] == bytes([0xEF]) + expected assert out["data"]["code"] == expected.hex() diff --git a/tests/test_keygen.py b/tests/test_keygen.py index 5dadcc3..45fbf79 100644 --- a/tests/test_keygen.py +++ b/tests/test_keygen.py @@ -24,16 +24,22 @@ class TestKeygen: class TestRecoverMac: + def test_recover_real_device(self): + """实机向量:ID '81233F3C267112' ↔ 广播 18:BC:5A:5D:BF:28(NVDS 序=显示序反转)""" + mac = P.recover_mac_from_device_id("81233F3C267112") + assert mac == bytes([0x28, 0xBF, 0x5D, 0x5A, 0xBC, 0x18]) + assert mac[::-1] == bytes.fromhex("18BC5A5DBF28") + def test_recover_vector(self): - """MAC [9A,01,99,50,10,20] → ID '89794900980101'(|MAC4-99|,|MAC5-99|,|MAC3-99|, - |MAC2-99|,|MAC1-99|,|MAC0-99|,flags;flags=各字节≥0x9A 位掩码)""" - mac = P.recover_mac_from_device_id("89794900980101") + """MAC [9A,01,99,50,10,20] → ID '79894900980101'(|MAC5-99|,|MAC4-99|,|MAC3-99|, + |MAC2-99|,|MAC1-99|,|MAC0-99|,flags;flags=各字节≥0x9A 位掩码=0x01)""" + mac = P.recover_mac_from_device_id("79894900980101") assert list(mac) == [0x9A, 0x01, 0x99, 0x50, 0x10, 0x20] def test_roundtrip_all_high(self): # 全 ≥0x9A:flags=0x3F;v=|m-0x99| mac = bytes([0x9A, 0xB0, 0xC1, 0xFF, 0xA0, 0x9B]) - vals = [abs(mac[4] - 0x99), abs(mac[5] - 0x99), abs(mac[3] - 0x99), + vals = [abs(mac[5] - 0x99), abs(mac[4] - 0x99), abs(mac[3] - 0x99), abs(mac[2] - 0x99), abs(mac[1] - 0x99), abs(mac[0] - 0x99)] dev_id = "".join(f"{v:02X}" for v in vals) + "3F" assert P.recover_mac_from_device_id(dev_id) == mac @@ -44,6 +50,6 @@ class TestRecoverMac: def test_keygen_after_recover_matches(self): """recovered MAC → keygen:与直接对原 MAC keygen 一致""" - mac = bytes([0x9A, 0x01, 0x99, 0x50, 0x10, 0x20]) - recovered = P.recover_mac_from_device_id("89794900980101") + mac = bytes([0x28, 0xBF, 0x5D, 0x5A, 0xBC, 0x18]) + recovered = P.recover_mac_from_device_id("81233F3C267112") assert P.keygen_code(recovered) == P.keygen_code(mac)