fix(protocol): RXTX 命令通道切换到 331f(实机证据:1f1f 仅ACK不执行)+ 设备ID反解参数序校正(实机向量),150 测试全绿
This commit is contained in:
1 parent
4db823f207
commit
aebe0ad317
6 files changed
+33
-18
No files matched your search
@@ -0,0 +1,3 @@
|
|||||||
|
{"argv": ["activate", "--show-id"]}
|
||||||
|
{"argv": ["raw", "e10201", "--channel", "alt"]}
|
||||||
|
{"argv": ["raw", "e2", "--channel", "alt"]}
|
||||||
@@ -143,7 +143,9 @@ class BridgeTransport:
|
|||||||
await self._rpc("write", char="epd", data=data.hex(), response=response)
|
await self._rpc("write", char="epd", data=data.hex(), response=response)
|
||||||
|
|
||||||
async def write_rxtx(self, data: bytes, response: bool = True):
|
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):
|
async def write_alt(self, data: bytes, response: bool = True):
|
||||||
"""经 331f 通道写(实机证据:1f1f 仅 ATT-ACK,331f 为活命令通道)。"""
|
"""经 331f 通道写(实机证据:1f1f 仅 ATT-ACK,331f 为活命令通道)。"""
|
||||||
|
|||||||
+10
-7
@@ -4,11 +4,12 @@ from __future__ import annotations
|
|||||||
import datetime
|
import datetime
|
||||||
from typing import Iterator
|
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_SERVICE_UUID = "13187b10-eba9-a3ba-044e-83d3217d9a38"
|
||||||
EPD_CHAR_UUID = "4b646063-6264-f3a7-8941-e65356ea82fe"
|
EPD_CHAR_UUID = "4b646063-6264-f3a7-8941-e65356ea82fe"
|
||||||
RXTX_SERVICE_UUID = "00001f10-0000-1000-8000-00805f9b34fb"
|
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-"
|
DEVICE_NAME_PREFIX = "NRF-"
|
||||||
|
|
||||||
BLOCK_PAYLOAD = 240 # my_step=480 hex 字符 = 240 数据字节
|
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:
|
def recover_mac_from_device_id(device_id: str) -> bytes:
|
||||||
"""14hex 设备 ID → 6B MAC。
|
"""14hex 设备 ID → 6B MAC(NVDS 字节序)。
|
||||||
ID = %02X(|MAC4-0x99|,|MAC5-0x99|,|MAC3-0x99|,|MAC2-0x99|,|MAC1-0x99|,|MAC0-0x99|,flags),
|
ID = %02X(|MAC5-0x99|,|MAC4-0x99|,|MAC3-0x99|,|MAC2-0x99|,|MAC1-0x99|,|MAC0-0x99|,flags),
|
||||||
flags bit i = MAC[i] ≥ 0x9A(证据 §4.2 生成函数 0x07FCA22C)。"""
|
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:
|
if len(device_id) != 14:
|
||||||
raise ValueError("设备 ID 必须 14 个 hex 字符")
|
raise ValueError("设备 ID 必须 14 个 hex 字符")
|
||||||
try:
|
try:
|
||||||
@@ -189,8 +192,8 @@ def recover_mac_from_device_id(device_id: str) -> bytes:
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
raise ValueError("设备 ID 含非 hex 字符") from None
|
raise ValueError("设备 ID 含非 hex 字符") from None
|
||||||
*v, flags = vals
|
*v, flags = vals
|
||||||
# v 顺序:MAC4,MAC5,MAC3,MAC2,MAC1,MAC0 → 反映射
|
# v 顺序:MAC5,MAC4,MAC3,MAC2,MAC1,MAC0 → 反映射
|
||||||
order = [4, 5, 3, 2, 1, 0]
|
order = [5, 4, 3, 2, 1, 0]
|
||||||
mac = [0] * 6
|
mac = [0] * 6
|
||||||
for vi, idx in zip(v, order):
|
for vi, idx in zip(v, order):
|
||||||
if flags & (1 << idx):
|
if flags & (1 << idx):
|
||||||
|
|||||||
@@ -99,7 +99,8 @@ async def test_write_channels_and_hex(server):
|
|||||||
await t.write_rxtx(bytes([0xE2]), response=False)
|
await t.write_rxtx(bytes([0xE2]), response=False)
|
||||||
w = [r for r in s.received if r["op"] == "write"]
|
w = [r for r in s.received if r["op"] == "write"]
|
||||||
assert w[0]["char"] == "epd" and w[0]["data"] == "03ff"
|
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
|
@pytest.mark.asyncio
|
||||||
|
|||||||
+3
-3
@@ -213,11 +213,11 @@ class TestOta:
|
|||||||
|
|
||||||
class TestActivateAuto:
|
class TestActivateAuto:
|
||||||
def test_activate_auto_via_device_id(self, capsys):
|
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):
|
class IdTransport(FakeTransport):
|
||||||
async def request_device_id(self, timeout=8.0):
|
async def request_device_id(self, timeout=8.0):
|
||||||
self.rxtx_writes.append(bytes([0xEF, 0xEF]))
|
self.rxtx_writes.append(bytes([0xEF, 0xEF]))
|
||||||
return "89794900980101"
|
return "81233F3C267112"
|
||||||
FakeTransport.instances.clear()
|
FakeTransport.instances.clear()
|
||||||
import ppclock.cli as c
|
import ppclock.cli as c
|
||||||
orig = c._make_transport
|
orig = c._make_transport
|
||||||
@@ -231,7 +231,7 @@ class TestActivateAuto:
|
|||||||
assert t.rxtx_writes[0] == bytes([0xEF, 0xEF])
|
assert t.rxtx_writes[0] == bytes([0xEF, 0xEF])
|
||||||
# keygen(9A,01,99,50,10,20):逐字节 rol2(((m<<1)&FF)^EF)%101
|
# keygen(9A,01,99,50,10,20):逐字节 rol2(((m<<1)&FF)^EF)%101
|
||||||
from ppclock.protocol import keygen_code
|
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 t.rxtx_writes[1] == bytes([0xEF]) + expected
|
||||||
assert out["data"]["code"] == expected.hex()
|
assert out["data"]["code"] == expected.hex()
|
||||||
|
|
||||||
|
|||||||
+12
-6
@@ -24,16 +24,22 @@ class TestKeygen:
|
|||||||
|
|
||||||
|
|
||||||
class TestRecoverMac:
|
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):
|
def test_recover_vector(self):
|
||||||
"""MAC [9A,01,99,50,10,20] → ID '89794900980101'(|MAC4-99|,|MAC5-99|,|MAC3-99|,
|
"""MAC [9A,01,99,50,10,20] → ID '79894900980101'(|MAC5-99|,|MAC4-99|,|MAC3-99|,
|
||||||
|MAC2-99|,|MAC1-99|,|MAC0-99|,flags;flags=各字节≥0x9A 位掩码)"""
|
|MAC2-99|,|MAC1-99|,|MAC0-99|,flags;flags=各字节≥0x9A 位掩码=0x01)"""
|
||||||
mac = P.recover_mac_from_device_id("89794900980101")
|
mac = P.recover_mac_from_device_id("79894900980101")
|
||||||
assert list(mac) == [0x9A, 0x01, 0x99, 0x50, 0x10, 0x20]
|
assert list(mac) == [0x9A, 0x01, 0x99, 0x50, 0x10, 0x20]
|
||||||
|
|
||||||
def test_roundtrip_all_high(self):
|
def test_roundtrip_all_high(self):
|
||||||
# 全 ≥0x9A:flags=0x3F;v=|m-0x99|
|
# 全 ≥0x9A:flags=0x3F;v=|m-0x99|
|
||||||
mac = bytes([0x9A, 0xB0, 0xC1, 0xFF, 0xA0, 0x9B])
|
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)]
|
abs(mac[2] - 0x99), abs(mac[1] - 0x99), abs(mac[0] - 0x99)]
|
||||||
dev_id = "".join(f"{v:02X}" for v in vals) + "3F"
|
dev_id = "".join(f"{v:02X}" for v in vals) + "3F"
|
||||||
assert P.recover_mac_from_device_id(dev_id) == mac
|
assert P.recover_mac_from_device_id(dev_id) == mac
|
||||||
@@ -44,6 +50,6 @@ class TestRecoverMac:
|
|||||||
|
|
||||||
def test_keygen_after_recover_matches(self):
|
def test_keygen_after_recover_matches(self):
|
||||||
"""recovered MAC → keygen:与直接对原 MAC keygen 一致"""
|
"""recovered MAC → keygen:与直接对原 MAC keygen 一致"""
|
||||||
mac = bytes([0x9A, 0x01, 0x99, 0x50, 0x10, 0x20])
|
mac = bytes([0x28, 0xBF, 0x5D, 0x5A, 0xBC, 0x18])
|
||||||
recovered = P.recover_mac_from_device_id("89794900980101")
|
recovered = P.recover_mac_from_device_id("81233F3C267112")
|
||||||
assert P.keygen_code(recovered) == P.keygen_code(mac)
|
assert P.keygen_code(recovered) == P.keygen_code(mac)
|
||||||
Reference in new issue
Block a user