SPI Link v2 Self-Test / selftest (push) Has been cancelled
添加SPI Link v2的PC端自测功能,包含交叉校验基准测试,覆盖HELLO/OTA/CAN/PING等 多种协议场景。同时重构了health monitor中的多字段读取逻辑,消除了volatile变量 的撕裂读问题,并增强了BLE OTA的数据传输可靠性。 refactor(health_monitor): 修复heap hook多字段撕裂读问题 采用portMUX_CRITICAL临界区实现多字段一致快照读取,消除任务上下文与heap hook 之间的数据竞争。修改了HookSnap结构体设计,通过一次性拷贝volatile变量避免 跨字段组合时的数据不一致。JSON输出中明确标注estimated值的置信度。 fix(ble_ota): 实现DATA_IN丢包负反馈机制 当malloc失败或队列满时发送缺口ACK通知前端按completed_bytes续传,避免 静默丢包导致的8秒超时重传风暴。增加节流机制防止负ACK在重传叠加时打爆 NimBLE mbuf池,提升低内存环境下的OTA传输稳定性。 refactor(spi_master): 从堆分配改为固定Job池管理 将SPI worker的动态堆分配Job改为固定大小的静态池管理,避免频繁的 calloc/free操作影响实时性。新增acquireJobSlot/releaseJobSlot机制 管理池槽生命周期,提升SPI通信的可靠性和内存使用效率。
167 lines
6.3 KiB
Python
167 lines
6.3 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
bump_asset_versions.py — 按本地静态资源内容 hash 自动更新 HTML 里的 ?v= 版本号。
|
||
|
||
动机
|
||
----
|
||
ESP32_COMM/tools 下多个 HTML(can.html / can_monitor.html / index.html)手工维护
|
||
<link>/<script> 引用的 `?v=...` query 版本号。components.css 修改后部分页面没 bump,
|
||
浏览器继续加载旧 CSS(审计 finding WEB-001)。
|
||
|
||
本脚本扫描所有 HTML 中形如
|
||
<link ... href="css/foo.css?v=OLD">
|
||
<script ... src="js/bar.js?v=OLD">
|
||
的本地资源引用,按文件内容计算短 hash(sha1 前 8 位十六进制),把 OLD 替换成 hash。
|
||
内容不变就不改,内容变了就 bump,所有页面引用同一资源时自动收敛到同一版本号。
|
||
|
||
用法
|
||
----
|
||
python bump_asset_versions.py # 默认 dry-run,只打印将要改动的行
|
||
python bump_asset_versions.py --write # 实际写回 HTML
|
||
python bump_asset_versions.py --check # CI 模式,发现任何不一致即返回非 0
|
||
python bump_asset_versions.py --root PATH # 指定 tools 目录(默认脚本所在目录)
|
||
|
||
退出码
|
||
------
|
||
0 全部一致(dry-run 无改动 或 --write 成功 或 --check 通过)
|
||
1 发现需要 bump 的引用(dry-run/--check 时)
|
||
2 参数错误或文件 IO 错误
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import hashlib
|
||
import re
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
# 匹配 <link href="...?v=..."> 与 <script src="...?v=...">,仅本地相对路径(无 scheme/host)。
|
||
# 路径里允许字母/数字/_-./ 子目录。query 部分仅捕获 v= 的值,保留其它 query key(若有)。
|
||
ASSET_REF_RE = re.compile(
|
||
r"""
|
||
(?P<tag>(?:href|src)\s*=\s*") # 属性名 + =" 开头
|
||
(?P<path>[A-Za-z0-9_./-]+\.(?:css|js)) # 本地资源路径(.css 或 .js)
|
||
\?v=(?P<old>[^"&\s]+) # ?v= 版本号
|
||
(?P<rest>"?) # 收尾引号(保留)
|
||
""",
|
||
re.VERBOSE,
|
||
)
|
||
|
||
|
||
def short_hash(content: bytes) -> str:
|
||
"""sha1 前 8 位十六进制;8 位足够区分日常 bump,且 URL 友好。"""
|
||
return hashlib.sha1(content).hexdigest()[:8]
|
||
|
||
|
||
def find_html_files(root: Path) -> list[Path]:
|
||
return sorted(p for p in root.glob("*.html") if p.is_file())
|
||
|
||
|
||
def process_html(html_path: Path) -> tuple[list[str], str]:
|
||
"""
|
||
返回 (new_lines, new_text)。
|
||
new_lines 是"将被改动"的描述列表(仅 dry-run/日志用),空列表表示无改动。
|
||
new_text 是替换后的完整文本(无论是否有改动都返回)。
|
||
"""
|
||
text = html_path.read_text(encoding="utf-8")
|
||
lines = text.splitlines(keepends=True)
|
||
changes: list[str] = []
|
||
|
||
def replace_in_line(line: str, line_no: int) -> str:
|
||
def _sub(m: re.Match[str]) -> str:
|
||
rel_path = m.group("path")
|
||
old_ver = m.group("old")
|
||
asset_path = html_path.parent / rel_path
|
||
if not asset_path.is_file():
|
||
# 资源缺失:不动,交给调用方在校验阶段报错
|
||
return m.group(0)
|
||
new_ver = short_hash(asset_path.read_bytes())
|
||
if new_ver == old_ver:
|
||
return m.group(0)
|
||
changes.append(
|
||
f" L{line_no}: {rel_path} {old_ver} -> {new_ver}"
|
||
)
|
||
return f'{m.group("tag")}{rel_path}?v={new_ver}{m.group("rest")}'
|
||
|
||
return ASSET_REF_RE.sub(_sub, line)
|
||
|
||
new_lines_text = [replace_in_line(line, i + 1) for i, line in enumerate(lines)]
|
||
return changes, "".join(new_lines_text)
|
||
|
||
|
||
def collect_missing_assets(html_files: list[Path]) -> list[str]:
|
||
"""收集所有 HTML 引用但磁盘上不存在的本地资源,供 --check 报错。"""
|
||
missing: set[str] = set()
|
||
for html_path in html_files:
|
||
text = html_path.read_text(encoding="utf-8")
|
||
for m in ASSET_REF_RE.finditer(text):
|
||
rel = m.group("path")
|
||
if not (html_path.parent / rel).is_file():
|
||
missing.add(f"{html_path.name}: {rel}")
|
||
return sorted(missing)
|
||
|
||
|
||
def main(argv: list[str] | None = None) -> int:
|
||
parser = argparse.ArgumentParser(
|
||
description="按本地静态资源内容 hash 自动更新 HTML 里的 ?v= 版本号。"
|
||
)
|
||
parser.add_argument(
|
||
"--root", type=Path, default=None,
|
||
help="tools 目录;默认为脚本所在目录。",
|
||
)
|
||
mode = parser.add_mutually_exclusive_group()
|
||
mode.add_argument("--write", action="store_true", help="实际写回 HTML(默认 dry-run)。")
|
||
mode.add_argument("--check", action="store_true", help="CI 模式:有改动或资源缺失即非 0 退出。")
|
||
args = parser.parse_args(argv)
|
||
|
||
root = (args.root if args.root is not None else Path(__file__).resolve().parent)
|
||
if not root.is_dir():
|
||
print(f"ERROR: root 不是目录: {root}", file=sys.stderr)
|
||
return 2
|
||
|
||
html_files = find_html_files(root)
|
||
if not html_files:
|
||
print(f"[bump] 没有找到 HTML 文件: {root}", file=sys.stderr)
|
||
return 2
|
||
|
||
mode_label = "check" if args.check else ("write" if args.write else "dry-run")
|
||
print(f"[bump] root={root} mode={mode_label} html={len(html_files)}")
|
||
|
||
missing = collect_missing_assets(html_files)
|
||
if missing:
|
||
print("[bump] 引用了不存在的本地资源:", file=sys.stderr)
|
||
for item in missing:
|
||
print(f" {item}", file=sys.stderr)
|
||
return 2
|
||
|
||
total_changes = 0
|
||
for html_path in html_files:
|
||
changes, new_text = process_html(html_path)
|
||
if not changes:
|
||
continue
|
||
total_changes += len(changes)
|
||
rel = html_path.relative_to(root)
|
||
print(f"[bump] {rel}({len(changes)} 处):")
|
||
for line in changes:
|
||
print(line)
|
||
if args.write:
|
||
html_path.write_text(new_text, encoding="utf-8")
|
||
|
||
if total_changes == 0:
|
||
print("[bump] 全部一致,无需 bump。")
|
||
return 0
|
||
|
||
if args.write:
|
||
print(f"[bump] 已写回 {total_changes} 处版本号。")
|
||
return 0
|
||
if args.check:
|
||
print(f"[bump] CHECK FAIL:{total_changes} 处版本号与内容不一致。", file=sys.stderr)
|
||
return 1
|
||
print(f"[bump] dry-run:{total_changes} 处需要 bump(加 --write 写回)。")
|
||
return 1
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main()) |