refactor: SDK 包结构——transports/firmware 分包、bridge_server 入包+ppclock-bridge 入口、fw 工具入 SDK 域,151 测试全绿
This commit is contained in:
1 parent
c13958db10
commit
a613c76114
14 files changed
+126
-69
No files matched your search
@@ -17,6 +17,7 @@ dev = ["pytest>=8"]
|
|||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
ppclock = "ppclock.cli:main"
|
ppclock = "ppclock.cli:main"
|
||||||
|
ppclock-bridge = "ppclock.bridge_server:run"
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["src"]
|
where = ["src"]
|
||||||
|
|||||||
@@ -325,8 +325,13 @@ async def main():
|
|||||||
await srv.serve_forever()
|
await srv.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
def run():
|
||||||
|
"""console_script 入口:ppclock-bridge。"""
|
||||||
try:
|
try:
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run()
|
||||||
+6
-6
@@ -42,13 +42,13 @@ def _emit(args, cmd, data=None, error=None):
|
|||||||
|
|
||||||
def _make_transport(args):
|
def _make_transport(args):
|
||||||
"""工厂函数——测试用 monkeypatch 替换为 FakeTransport。"""
|
"""工厂函数——测试用 monkeypatch 替换为 FakeTransport。"""
|
||||||
from .bridge import BridgeTransport, parse_via
|
from .transports.bridge import BridgeTransport, parse_via
|
||||||
via = parse_via(getattr(args, "via", None))
|
via = parse_via(getattr(args, "via", None))
|
||||||
if via:
|
if via:
|
||||||
host, port = via
|
host, port = via
|
||||||
return BridgeTransport(address=args.mac, timeout=args.timeout,
|
return BridgeTransport(address=args.mac, timeout=args.timeout,
|
||||||
host=host, port=port)
|
host=host, port=port)
|
||||||
from .transport import BLETransport
|
from .transports.local import BLETransport
|
||||||
return BLETransport(address=args.mac, timeout=args.timeout)
|
return BLETransport(address=args.mac, timeout=args.timeout)
|
||||||
|
|
||||||
|
|
||||||
@@ -316,12 +316,12 @@ async def _run_batch(args) -> list:
|
|||||||
|
|
||||||
async def _dispatch(args) -> dict | list | None:
|
async def _dispatch(args) -> dict | list | None:
|
||||||
if args.cmd == "scan":
|
if args.cmd == "scan":
|
||||||
from .bridge import parse_via
|
from .transports.bridge import parse_via
|
||||||
via = parse_via(getattr(args, "via", None))
|
via = parse_via(getattr(args, "via", None))
|
||||||
if via:
|
if via:
|
||||||
from .bridge import scan as bridge_scan
|
from .transports.bridge import scan as bridge_scan
|
||||||
return {"devices": await bridge_scan(via[0], via[1], timeout=args.timeout)}
|
return {"devices": await bridge_scan(via[0], via[1], timeout=args.timeout)}
|
||||||
from .transport import scan
|
from .transports.local import scan
|
||||||
return {"devices": await scan(timeout=args.timeout)}
|
return {"devices": await scan(timeout=args.timeout)}
|
||||||
if args.cmd == "batch":
|
if args.cmd == "batch":
|
||||||
return {"results": await _run_batch(args)}
|
return {"results": await _run_batch(args)}
|
||||||
@@ -338,7 +338,7 @@ def main(argv=None) -> int:
|
|||||||
_emit(args, args.cmd, error=str(e))
|
_emit(args, args.cmd, error=str(e))
|
||||||
return EXIT_USAGE
|
return EXIT_USAGE
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
from .transport import TransportError
|
from .transports.base import TransportError
|
||||||
code = EXIT_TRANSPORT if isinstance(e, TransportError) else EXIT_ERROR
|
code = EXIT_TRANSPORT if isinstance(e, TransportError) else EXIT_ERROR
|
||||||
_emit(args, args.cmd, error=f"{type(e).__name__}: {e}")
|
_emit(args, args.cmd, error=f"{type(e).__name__}: {e}")
|
||||||
return code
|
return code
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
"""固件域:DA14585 镜像解析/重打包(image)与 SUOTA 升级(ota)。"""
|
||||||
|
from .image import parse_image, unpack_body, pack_image
|
||||||
|
from .ota import run_ota, OtaError, package_image, xor_checksum
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"parse_image", "unpack_body", "pack_image",
|
||||||
|
"run_ota", "OtaError", "package_image", "xor_checksum",
|
||||||
|
]
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"""DA14585 SUOTA 镜像解析/重打包(tools 交付物)。
|
||||||
|
格式(证据:app/ 样本 + CRC 反推验证):
|
||||||
|
[0:2] magic 'pQ' = 0x7051(LE)
|
||||||
|
[2:4] flags = 0x01AA
|
||||||
|
[4:8] body 长度(= 文件长 - 64)
|
||||||
|
[8:12] CRC32(body)(zlib 多项式,LE)
|
||||||
|
[12:64] 0xFF 填充
|
||||||
|
[64:] 镜像体,起始为 Cortex-M 向量表(SP, Reset, ...),加载基址 0x07FC0000
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import struct
|
||||||
|
import zlib
|
||||||
|
|
||||||
|
MAGIC_BYTES = b"pQ" # 文件头两字节 70 51
|
||||||
|
MAGIC = 0x7051
|
||||||
|
FLAGS_DEFAULT = 0x01AA
|
||||||
|
HEADER_SIZE = 64
|
||||||
|
LOAD_BASE = 0x07FC0000
|
||||||
|
|
||||||
|
|
||||||
|
def parse_image(data: bytes) -> dict:
|
||||||
|
if len(data) < HEADER_SIZE:
|
||||||
|
raise ValueError("文件小于 64 字节头")
|
||||||
|
if data[0:2] != MAGIC_BYTES:
|
||||||
|
raise ValueError(f"magic 错误: {data[0:2].hex()}(期望 {MAGIC_BYTES.hex()}='pQ')")
|
||||||
|
flags, body_size, crc = struct.unpack_from("<HII", data, 2)
|
||||||
|
body = data[HEADER_SIZE:HEADER_SIZE + body_size]
|
||||||
|
sp, reset = struct.unpack_from("<II", body, 0)
|
||||||
|
return {
|
||||||
|
"magic": MAGIC,
|
||||||
|
"flags": flags,
|
||||||
|
"body_size": body_size,
|
||||||
|
"crc_stored": crc,
|
||||||
|
"crc_valid": (zlib.crc32(body) & 0xFFFFFFFF) == crc,
|
||||||
|
"initial_sp": sp,
|
||||||
|
"reset_handler": reset,
|
||||||
|
"load_base": LOAD_BASE,
|
||||||
|
"file_size": len(data),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def unpack_body(data: bytes) -> bytes:
|
||||||
|
info = parse_image(data)
|
||||||
|
return data[HEADER_SIZE:HEADER_SIZE + info["body_size"]]
|
||||||
|
|
||||||
|
|
||||||
|
def pack_image(body: bytes, flags: int = FLAGS_DEFAULT) -> bytes:
|
||||||
|
header = MAGIC_BYTES + struct.pack("<HII", flags, len(body),
|
||||||
|
zlib.crc32(body) & 0xFFFFFFFF)
|
||||||
|
# 填充:黄金样本 0x20 处为 0x00(含义未知,保留以字节级一致),其余 0xFF
|
||||||
|
pad = bytearray(b"\xFF" * (HEADER_SIZE - len(header)))
|
||||||
|
pad[0x20 - len(header)] = 0x00
|
||||||
|
return header + bytes(pad) + body
|
||||||
File renamed without changes.
@@ -0,0 +1,10 @@
|
|||||||
|
"""传输层实现集合。"""
|
||||||
|
from .base import Transport, TransportError
|
||||||
|
from .local import BLETransport, scan
|
||||||
|
from .bridge import BridgeTransport, BridgeError, parse_via
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Transport", "TransportError",
|
||||||
|
"BLETransport", "scan",
|
||||||
|
"BridgeTransport", "BridgeError", "parse_via",
|
||||||
|
]
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
"""传输层接口协议(SDK 语义层统一依赖此接口)。
|
||||||
|
任何实现以下方法的类均可作为 PPClient 的传输层:
|
||||||
|
write_epd(data, response=True) / write_rxtx(data, response=True)
|
||||||
|
read_rxtx() / request_device_id(timeout) / delay(seconds) / run_ota(image, on_progress)
|
||||||
|
异步上下文管理 __aenter__/__aexit__
|
||||||
|
内置实现:transports.local.BLETransport(本机 bleak)、transports.bridge.BridgeTransport(RPC 桥)。
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Protocol, runtime_checkable
|
||||||
|
|
||||||
|
|
||||||
|
class TransportError(Exception):
|
||||||
|
"""传输层通用错误(连接失败/设备不可达/超时等)。"""
|
||||||
|
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class Transport(Protocol):
|
||||||
|
address: str | None
|
||||||
|
|
||||||
|
async def __aenter__(self): ...
|
||||||
|
async def __aexit__(self, *exc): ...
|
||||||
|
async def write_epd(self, data: bytes, response: bool = True): ...
|
||||||
|
async def write_rxtx(self, data: bytes, response: bool = True): ...
|
||||||
|
async def read_rxtx(self) -> bytes: ...
|
||||||
|
async def request_device_id(self, timeout: float = 8.0) -> str: ...
|
||||||
|
async def delay(self, seconds: float): ...
|
||||||
|
async def run_ota(self, image: bytes, on_progress=None): ...
|
||||||
File renamed without changes.
@@ -6,7 +6,7 @@ import asyncio
|
|||||||
from bleak import BleakClient, BleakScanner
|
from bleak import BleakClient, BleakScanner
|
||||||
from bleak.exc import BleakError
|
from bleak.exc import BleakError
|
||||||
|
|
||||||
from . import protocol as P
|
from .. import protocol as P
|
||||||
|
|
||||||
|
|
||||||
class TransportError(Exception):
|
class TransportError(Exception):
|
||||||
@@ -92,5 +92,5 @@ class BLETransport:
|
|||||||
|
|
||||||
async def run_ota(self, image: bytes, on_progress=None):
|
async def run_ota(self, image: bytes, on_progress=None):
|
||||||
"""SUOTA 固件升级(证据 analysis/apk/apk-analysis.md §e)。"""
|
"""SUOTA 固件升级(证据 analysis/apk/apk-analysis.md §e)。"""
|
||||||
from . import ota
|
from ..firmware import ota
|
||||||
return await ota.run_ota(self._client, image, on_progress)
|
return await ota.run_ota(self._client, image, on_progress)
|
||||||
@@ -5,7 +5,7 @@ import json
|
|||||||
import pytest
|
import pytest
|
||||||
import pytest_asyncio
|
import pytest_asyncio
|
||||||
|
|
||||||
from ppclock import bridge
|
from ppclock.transports import bridge
|
||||||
|
|
||||||
|
|
||||||
class FakeBridgeServer:
|
class FakeBridgeServer:
|
||||||
|
|||||||
+5
-5
@@ -154,7 +154,7 @@ class TestCommands:
|
|||||||
assert FakeTransport.instances[0].epd_writes == [bytes([0x01])]
|
assert FakeTransport.instances[0].epd_writes == [bytes([0x01])]
|
||||||
|
|
||||||
def test_scan(self, capsys, monkeypatch):
|
def test_scan(self, capsys, monkeypatch):
|
||||||
import ppclock.transport as tr
|
import ppclock.transports.local as tr
|
||||||
async def fake_scan(timeout=5.0, name_prefix="NRF-"):
|
async def fake_scan(timeout=5.0, name_prefix="NRF-"):
|
||||||
return [{"name": "NRF-TEST", "address": "AA:BB", "rssi": -50}]
|
return [{"name": "NRF-TEST", "address": "AA:BB", "rssi": -50}]
|
||||||
monkeypatch.setattr(tr, "scan", fake_scan)
|
monkeypatch.setattr(tr, "scan", fake_scan)
|
||||||
@@ -242,7 +242,7 @@ _ORIG_MAKE = cli._make_transport
|
|||||||
class TestViaBridge:
|
class TestViaBridge:
|
||||||
def test_via_bridge_creates_bridge_transport(self):
|
def test_via_bridge_creates_bridge_transport(self):
|
||||||
import argparse
|
import argparse
|
||||||
from ppclock.bridge import BridgeTransport
|
from ppclock.transports.bridge import BridgeTransport
|
||||||
args = argparse.Namespace(mac="AA:BB", timeout=5.0, via="bridge://192.168.61.35:8971")
|
args = argparse.Namespace(mac="AA:BB", timeout=5.0, via="bridge://192.168.61.35:8971")
|
||||||
t = _ORIG_MAKE(args)
|
t = _ORIG_MAKE(args)
|
||||||
assert isinstance(t, BridgeTransport)
|
assert isinstance(t, BridgeTransport)
|
||||||
@@ -257,12 +257,12 @@ class TestViaBridge:
|
|||||||
|
|
||||||
def test_via_local_creates_ble(self):
|
def test_via_local_creates_ble(self):
|
||||||
import argparse
|
import argparse
|
||||||
from ppclock.transport import BLETransport
|
from ppclock.transports.local import BLETransport
|
||||||
args = argparse.Namespace(mac=None, timeout=5.0, via=None)
|
args = argparse.Namespace(mac=None, timeout=5.0, via=None)
|
||||||
assert isinstance(_ORIG_MAKE(args), BLETransport)
|
assert isinstance(_ORIG_MAKE(args), BLETransport)
|
||||||
|
|
||||||
def test_scan_uses_bridge(self, capsys, monkeypatch):
|
def test_scan_uses_bridge(self, capsys, monkeypatch):
|
||||||
import ppclock.bridge as br
|
import ppclock.transports.bridge as br
|
||||||
called = {}
|
called = {}
|
||||||
|
|
||||||
async def fake_scan(host, port, timeout=5.0):
|
async def fake_scan(host, port, timeout=5.0):
|
||||||
@@ -275,7 +275,7 @@ class TestViaBridge:
|
|||||||
assert out["data"]["devices"][0]["name"] == "NRF-BR"
|
assert out["data"]["devices"][0]["name"] == "NRF-BR"
|
||||||
|
|
||||||
def test_env_var_fallback(self, capsys, monkeypatch):
|
def test_env_var_fallback(self, capsys, monkeypatch):
|
||||||
import ppclock.bridge as br
|
import ppclock.transports.bridge as br
|
||||||
called = {}
|
called = {}
|
||||||
|
|
||||||
async def fake_scan(host, port, timeout=5.0):
|
async def fake_scan(host, port, timeout=5.0):
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
"""OTA/SUOTA 打包逻辑测试(证据:analysis/apk/apk-analysis.md §e,z7.h0/z7.b0)。"""
|
"""OTA/SUOTA 打包逻辑测试(证据:analysis/apk/apk-analysis.md §e,z7.h0/z7.b0)。"""
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from ppclock import ota
|
from ppclock.firmware import ota
|
||||||
|
|
||||||
|
|
||||||
class TestPackaging:
|
class TestPackaging:
|
||||||
|
|||||||
+4
-53
@@ -1,54 +1,5 @@
|
|||||||
"""DA14585 SUOTA 镜像解析/重打包(tools 交付物)。
|
"""兼容垫片:fw 工具已迁入 ppclock.firmware.image(SDK 域)。
|
||||||
格式(证据:app/ 样本 + CRC 反推验证):
|
保留本模块以兼容 tools/field_test.py 的 `from tools.fw import ...`。
|
||||||
[0:2] magic 'pQ' = 0x7051(LE)
|
|
||||||
[2:4] flags = 0x01AA
|
|
||||||
[4:8] body 长度(= 文件长 - 64)
|
|
||||||
[8:12] CRC32(body)(zlib 多项式,LE)
|
|
||||||
[12:64] 0xFF 填充
|
|
||||||
[64:] 镜像体,起始为 Cortex-M 向量表(SP, Reset, ...),加载基址 0x07FC0000
|
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from ppclock.firmware.image import * # noqa: F401,F403
|
||||||
|
from ppclock.firmware.image import MAGIC, MAGIC_BYTES, FLAGS_DEFAULT, HEADER_SIZE, LOAD_BASE # noqa: F401
|
||||||
import struct
|
|
||||||
import zlib
|
|
||||||
|
|
||||||
MAGIC_BYTES = b"pQ" # 文件头两字节 70 51
|
|
||||||
MAGIC = 0x7051
|
|
||||||
FLAGS_DEFAULT = 0x01AA
|
|
||||||
HEADER_SIZE = 64
|
|
||||||
LOAD_BASE = 0x07FC0000
|
|
||||||
|
|
||||||
|
|
||||||
def parse_image(data: bytes) -> dict:
|
|
||||||
if len(data) < HEADER_SIZE:
|
|
||||||
raise ValueError("文件小于 64 字节头")
|
|
||||||
if data[0:2] != MAGIC_BYTES:
|
|
||||||
raise ValueError(f"magic 错误: {data[0:2].hex()}(期望 {MAGIC_BYTES.hex()}='pQ')")
|
|
||||||
flags, body_size, crc = struct.unpack_from("<HII", data, 2)
|
|
||||||
body = data[HEADER_SIZE:HEADER_SIZE + body_size]
|
|
||||||
sp, reset = struct.unpack_from("<II", body, 0)
|
|
||||||
return {
|
|
||||||
"magic": MAGIC,
|
|
||||||
"flags": flags,
|
|
||||||
"body_size": body_size,
|
|
||||||
"crc_stored": crc,
|
|
||||||
"crc_valid": (zlib.crc32(body) & 0xFFFFFFFF) == crc,
|
|
||||||
"initial_sp": sp,
|
|
||||||
"reset_handler": reset,
|
|
||||||
"load_base": LOAD_BASE,
|
|
||||||
"file_size": len(data),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def unpack_body(data: bytes) -> bytes:
|
|
||||||
info = parse_image(data)
|
|
||||||
return data[HEADER_SIZE:HEADER_SIZE + info["body_size"]]
|
|
||||||
|
|
||||||
|
|
||||||
def pack_image(body: bytes, flags: int = FLAGS_DEFAULT) -> bytes:
|
|
||||||
header = MAGIC_BYTES + struct.pack("<HII", flags, len(body),
|
|
||||||
zlib.crc32(body) & 0xFFFFFFFF)
|
|
||||||
# 填充:黄金样本 0x20 处为 0x00(含义未知,保留以字节级一致),其余 0xFF
|
|
||||||
pad = bytearray(b"\xFF" * (HEADER_SIZE - len(header)))
|
|
||||||
pad[0x20 - len(header)] = 0x00
|
|
||||||
return header + bytes(pad) + body
|
|
||||||
Reference in new issue
Block a user