feat(spi_link): 添加SPI Link v2 PC自测框架并优化协议稳定性
SPI Link v2 Self-Test / selftest (push) Has been cancelled
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通信的可靠性和内存使用效率。
This commit is contained in:
@@ -1,81 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
from pathlib import Path
|
||||
import re
|
||||
from collections import defaultdict
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
text = (ROOT / "build" / "ESP32_COMM.map").read_text(encoding="utf-8", errors="replace")
|
||||
rows = []
|
||||
lines = text.splitlines()
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
m = re.match(r"^\s*\.bss\.(\S+)\s*$", line)
|
||||
m2 = re.match(r"^\s*\.bss\.(\S+)\s+(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)", line)
|
||||
if m2:
|
||||
rows.append((int(m2.group(3), 16), m2.group(1), ""))
|
||||
elif m:
|
||||
name = m.group(1)
|
||||
j = i + 1
|
||||
while j < len(lines) and j < i + 5:
|
||||
l2 = lines[j]
|
||||
mm = re.match(r"^\s+(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+(\S+)", l2)
|
||||
if mm:
|
||||
rows.append((int(mm.group(2), 16), name, mm.group(3)))
|
||||
break
|
||||
if re.match(r"^\s*\.", l2) and not l2.strip().startswith(".bss"):
|
||||
break
|
||||
j += 1
|
||||
i += 1
|
||||
|
||||
|
||||
def demangle(n: str) -> str:
|
||||
n = n.replace("_ZN12_GLOBAL__N_1L", "")
|
||||
n = n.replace("_ZL", "")
|
||||
if n.startswith("_ZZ"):
|
||||
n = "local:" + n[3:50]
|
||||
return n[:55]
|
||||
|
||||
|
||||
cats = defaultdict(int)
|
||||
app = []
|
||||
for size, name, obj in rows:
|
||||
o = (obj or "").lower()
|
||||
if "libmain" in o:
|
||||
cat = "app(main)"
|
||||
app.append((size, name, obj))
|
||||
elif "libfreertos" in o:
|
||||
cat = "freertos"
|
||||
elif "liblwip" in o:
|
||||
cat = "lwip"
|
||||
elif any(x in o for x in ("libnet80211", "libpp", "libwpa", "libmesh")):
|
||||
cat = "wifi/mesh"
|
||||
elif any(x in o for x in ("libnimble", "libbt", "bt.", "ble_")):
|
||||
cat = "ble"
|
||||
elif any(
|
||||
x in name
|
||||
for x in ("g_cnx", "gWpa", "g_ic", "g_pm", "gChm", "gScan", "g_mesh", "mesh_")
|
||||
):
|
||||
cat = "wifi/mesh(sym)"
|
||||
else:
|
||||
cat = "other/idf"
|
||||
cats[cat] += size
|
||||
|
||||
print("=== category ===")
|
||||
for k, v in sorted(cats.items(), key=lambda x: -x[1]):
|
||||
print(f"{v/1024:6.2f} KB {k}")
|
||||
print(f"{sum(cats.values())/1024:6.2f} KB TOTAL")
|
||||
|
||||
print("\n=== app(main) ===")
|
||||
app.sort(reverse=True)
|
||||
s = 0
|
||||
for size, name, obj in app:
|
||||
s += size
|
||||
print(f"{size:5d} {demangle(name)}")
|
||||
print(f"app sum {s} ({s/1024:.2f} KB)")
|
||||
|
||||
print("\n=== top 20 overall ===")
|
||||
rows.sort(reverse=True)
|
||||
for size, name, obj in rows[:20]:
|
||||
short = obj.split("/")[-1] if obj else "?"
|
||||
print(f"{size:5d} ({size/1024:5.2f}K) {demangle(name):45s} {short[:28]}")
|
||||
@@ -1,21 +0,0 @@
|
||||
Threshold: >128 bytes (prefix+body+newline, UTF-8)
|
||||
Candidates: 5
|
||||
|
||||
# est body pre lv tag location
|
||||
----------------------------------------------------------------------------------------------------
|
||||
1 176 157 18 DBG TAG main/server/ble_ota_data.cpp:100
|
||||
fmt: BLEp(%s) %.1f-%.1f ce=%u-%u rc=%d
|
||||
args: %s鈫?6:windows_compatible ? "Win" : "Hi"
|
||||
2 153 134 18 WARN kTag main/monitor/can_feedback_task.cpp:122
|
||||
fmt: CH%u s=%lu e=%lu T=%u R=%u st=%02X
|
||||
3 153 134 18 DBG TAG main/server/ble_ota_data.cpp:69
|
||||
fmt: %sitv=%.2f lat=%u sup=%u
|
||||
args: %s鈫?8:phase
|
||||
4 141 122 18 INFO TAG main/server/ble_ota_host.cpp:545
|
||||
fmt: PHY %d %s/%s
|
||||
args: %s鈫?8:rx_phy; %s鈫?8:tx_phy
|
||||
5 135 116 18 ERR TAG main/ota/esp32_ota_core.cpp:227
|
||||
fmt: ota_write %s n=%u off=%u free=%lu
|
||||
args: %s鈫?2:esp_err_to_name(err)
|
||||
|
||||
Total over 128B: 5
|
||||
@@ -1,37 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
# Match DEBUG_*(tag, "..." PRI... "..." ...)
|
||||
pat = re.compile(
|
||||
r'DEBUG_(ERR|WARN|INFO|DBG)\s*\(\s*([^,]+)\s*,\s*'
|
||||
r'((?:"(?:\\.|[^"\\])*"\s*(?:PRIu\d+|PRIx\d+|PRId\d+|PRIX\d+)?\s*)+)',
|
||||
re.M,
|
||||
)
|
||||
|
||||
rows = []
|
||||
for path in (ROOT / "main").rglob("*.cpp"):
|
||||
t = path.read_text(encoding="utf-8")
|
||||
for m in pat.finditer(t):
|
||||
raw = m.group(3)
|
||||
parts = re.findall(r'"((?:\\.|[^"\\])*)"', raw)
|
||||
if not parts:
|
||||
continue
|
||||
joined = "".join(parts)
|
||||
# replace PRI holes with %u placeholder length 10 each
|
||||
pri_count = len(re.findall(r"PRI[udxX]\d+", raw))
|
||||
# rough: joined has empty holes between lits around PRI
|
||||
body = len(joined.encode()) + pri_count * 10
|
||||
# also % in joined
|
||||
body += joined.count("%") * 8 # remaining %
|
||||
if body < 100:
|
||||
continue
|
||||
ln = t.count("\n", 0, m.start()) + 1
|
||||
rows.append((body, path.relative_to(ROOT).as_posix(), ln, joined[:100], pri_count))
|
||||
|
||||
rows.sort(reverse=True)
|
||||
print(f"Long concat fmt candidates: {len(rows)}")
|
||||
for b, f, ln, j, pc in rows[:40]:
|
||||
print(f"{b:4d} {f}:{ln} pri={pc}")
|
||||
print(f" {j}")
|
||||
@@ -1,207 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Scan DEBUG_* with realistic arg widths (actual-ish max line size)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
THRESHOLD = 128
|
||||
|
||||
PAT = re.compile(
|
||||
r'DEBUG_(ERR|WARN|INFO|DBG)\s*\(\s*([^,]+)\s*,\s*"((?:\\.|[^"\\])*)"\s*(?:,|\))',
|
||||
re.M,
|
||||
)
|
||||
|
||||
# Known TAG macros
|
||||
KNOWN_TAGS = {
|
||||
"TAG": 7, # worst of BLE_OTA/OTA_CORE/OTA_COORD
|
||||
"kTag": 8,
|
||||
}
|
||||
|
||||
|
||||
def unescape(fmt: str) -> str:
|
||||
return fmt.replace("\\n", "\n").replace("\\t", "\t").replace('\\"', '"')
|
||||
|
||||
|
||||
def parse_args(text: str, fmt_end: int) -> list[str]:
|
||||
i = fmt_end
|
||||
while i < len(text) and text[i] in " \t\r\n":
|
||||
i += 1
|
||||
if i < len(text) and text[i] == ")":
|
||||
return []
|
||||
if i < len(text) and text[i] == ",":
|
||||
i += 1
|
||||
args, depth, start = [], 0, i
|
||||
in_str, esc = False, False
|
||||
while i < len(text):
|
||||
c = text[i]
|
||||
if in_str:
|
||||
if esc:
|
||||
esc = False
|
||||
elif c == "\\":
|
||||
esc = True
|
||||
elif c == '"':
|
||||
in_str = False
|
||||
i += 1
|
||||
continue
|
||||
if c == '"':
|
||||
in_str = True
|
||||
i += 1
|
||||
continue
|
||||
if c in "([{":
|
||||
depth += 1
|
||||
elif c in ")]}":
|
||||
if depth == 0 and c == ")":
|
||||
part = text[start:i].strip()
|
||||
if part:
|
||||
args.append(part)
|
||||
break
|
||||
depth -= 1
|
||||
elif c == "," and depth == 0:
|
||||
part = text[start:i].strip()
|
||||
if part:
|
||||
args.append(part)
|
||||
start = i + 1
|
||||
i += 1
|
||||
if i - start > 500:
|
||||
break
|
||||
return args
|
||||
|
||||
|
||||
def s_est(arg: str) -> int:
|
||||
a = arg.strip()
|
||||
if re.search(r"frontend_sha|device_hex|device_sha|sha_hex", a, re.I):
|
||||
return 64
|
||||
if re.search(r"resp_hex", a, re.I):
|
||||
return 64 # should not dump full anymore
|
||||
if "esp_err_to_name" in a:
|
||||
return 24
|
||||
if re.search(r"stateToString|phase|label|name|rx_phy|tx_phy", a, re.I):
|
||||
return 16
|
||||
m = re.match(r'^"((?:\\.|[^"\\])*)"$', a)
|
||||
if m:
|
||||
return len(unescape(m.group(1)).encode())
|
||||
if "?" in a and ":" in a:
|
||||
# ternary of string lits
|
||||
parts = re.findall(r'"((?:\\.|[^"\\])*)"', a)
|
||||
if parts:
|
||||
return max(len(unescape(p).encode()) for p in parts)
|
||||
return 8
|
||||
return 16
|
||||
|
||||
|
||||
def body_est(fmt: str, args: list[str]) -> int:
|
||||
i, n, out, ai = 0, len(fmt), 0, 0
|
||||
|
||||
def nexta() -> str:
|
||||
nonlocal ai
|
||||
if ai < len(args):
|
||||
a = args[ai]
|
||||
ai += 1
|
||||
return a
|
||||
return ""
|
||||
|
||||
while i < n:
|
||||
if fmt[i] != "%":
|
||||
j = fmt.find("%", i)
|
||||
if j < 0:
|
||||
out += len(fmt[i:].encode())
|
||||
break
|
||||
out += len(fmt[i:j].encode())
|
||||
i = j
|
||||
continue
|
||||
if i + 1 < n and fmt[i + 1] == "%":
|
||||
out += 1
|
||||
i += 2
|
||||
continue
|
||||
i += 1
|
||||
while i < n and fmt[i] in "-+ #0":
|
||||
i += 1
|
||||
if i < n and fmt[i] == "*":
|
||||
nexta()
|
||||
i += 1
|
||||
else:
|
||||
while i < n and fmt[i].isdigit():
|
||||
i += 1
|
||||
if i < n and fmt[i] == ".":
|
||||
i += 1
|
||||
if i < n and fmt[i] == "*":
|
||||
nexta()
|
||||
i += 1
|
||||
else:
|
||||
while i < n and fmt[i].isdigit():
|
||||
i += 1
|
||||
while i < n and fmt[i] in "hlLzjtq":
|
||||
i += 1
|
||||
if i >= n:
|
||||
break
|
||||
conv = fmt[i]
|
||||
i += 1
|
||||
arg = nexta()
|
||||
if conv in "diouxX":
|
||||
out += 10 # realistic
|
||||
elif conv in "fFeEgG":
|
||||
out += 8
|
||||
elif conv == "s":
|
||||
out += s_est(arg)
|
||||
elif conv == "c":
|
||||
out += 1
|
||||
elif conv == "p":
|
||||
out += 10
|
||||
else:
|
||||
out += 8
|
||||
return out
|
||||
|
||||
|
||||
def tag_len(expr: str) -> tuple[int, str]:
|
||||
e = expr.strip()
|
||||
m = re.match(r'^"([^"]*)"$', e)
|
||||
if m:
|
||||
return len(m.group(1).encode()), m.group(1)
|
||||
if e in KNOWN_TAGS:
|
||||
return KNOWN_TAGS[e], e
|
||||
return 8, e
|
||||
|
||||
|
||||
def main() -> int:
|
||||
rows = []
|
||||
for path in ROOT.rglob("*"):
|
||||
if path.suffix not in {".cpp", ".c", ".h"}:
|
||||
continue
|
||||
if "build" in path.parts or ".git" in path.parts:
|
||||
continue
|
||||
if "components" in path.parts:
|
||||
continue
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8", errors="replace")
|
||||
except OSError:
|
||||
continue
|
||||
rel = path.relative_to(ROOT).as_posix()
|
||||
for m in PAT.finditer(text):
|
||||
level, tag_expr, fmt_raw = m.group(1), m.group(2), m.group(3)
|
||||
fmt = unescape(fmt_raw).rstrip("\n\r")
|
||||
args = parse_args(text, m.end(3) + 1)
|
||||
body = body_est(fmt, args)
|
||||
tlen, tag = tag_len(tag_expr)
|
||||
lc = {"ERR": "E", "WARN": "W", "INFO": "I", "DBG": "D"}[level]
|
||||
if tag_expr.strip().startswith('"'):
|
||||
prefix = len(f"[{lc}][{tag}] ".encode())
|
||||
else:
|
||||
prefix = len(f"[{lc}][".encode()) + tlen + len("] ".encode())
|
||||
total = prefix + body + 1
|
||||
if total <= THRESHOLD:
|
||||
continue
|
||||
ln = text.count("\n", 0, m.start()) + 1
|
||||
rows.append((total, body, prefix, level, tag, rel, ln, fmt))
|
||||
rows.sort(reverse=True)
|
||||
print(f"Realistic threshold >{THRESHOLD}B: {len(rows)} candidates\n")
|
||||
for i, (total, body, pre, lv, tag, rel, ln, fmt) in enumerate(rows, 1):
|
||||
fs = fmt if len(fmt) <= 90 else fmt[:87] + "..."
|
||||
print(f"{i:3d} {total:4d} body={body:3d} pre={pre:2d} {lv:4s} {str(tag)[:10]:10s} {rel}:{ln}")
|
||||
print(f" {fs}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,130 +0,0 @@
|
||||
"Archive File","Total Size","DRAM",".bss",".data",".text","Flash
|
||||
Code",".text","Flash Data",".tdata",".rodata",".appdesc","RTC
|
||||
SLOW",".rtc_reserved",".force_slow"
|
||||
"libnet80211.a","154301","12686","7678","936","4072","128198","128198","13417","
|
||||
0","13417","0","0","0","0"
|
||||
"libesp_stdio.a","114154","16","16","0","0","402","402","113736","0","113736","0
|
||||
","0","0","0"
|
||||
"libbtdm_app.a","98094","20783","684","481","19618","72152","72152","5159","0","
|
||||
5159","0","0","0","0"
|
||||
"liblwip.a","88946","2602","2590","12","0","82916","82916","3428","0","3428","0"
|
||||
,"0","0","0"
|
||||
"libmain.a","85209","26097","22673","3264","160","57804","57804","1308","0","130
|
||||
8","0","0","0","0"
|
||||
"libpp.a","68522","19794","1262","2644","15888","44814","44814","3914","0","3914
|
||||
","0","0","0","0"
|
||||
"libmbed-builtin.a","57570","60","12","48","0","45710","45710","11800","0","1180
|
||||
0","0","0","0","0"
|
||||
"libbt.a","56078","1060","139","233","688","51916","51916","3102","0","3102","0"
|
||||
,"0","0","0"
|
||||
"libwpa_supplicant.a","37603","1138","1138","0","0","36390","36390","75","0","75
|
||||
","0","0","0","0"
|
||||
"libphy.a","36923","5801","87","1312","4402","31122","31122","0","0","0","0","0"
|
||||
,"0","0"
|
||||
"libtfpsacrypto.a","24703","562","400","80","82","23844","23844","297","0","297"
|
||||
,"0","0","0","0"
|
||||
"libfreertos.a","20942","17688","2560","16","15112","1690","1690","1564","0","15
|
||||
64","0","0","0","0"
|
||||
"libesp_hw_support.a","20186","6251","145","160","5946","13170","13170","705","0
|
||||
","705","0","60","24","36"
|
||||
"libspi_flash.a","17438","15081","10","5867","9204","1322","1322","1035","0","10
|
||||
35","0","0","0","0"
|
||||
"libc.a","14702","4","0","4","0","13610","13610","1088","0","1088","0","0","0","
|
||||
0"
|
||||
"libnvs_flash.a","14264","24","24","0","0","14108","14108","132","0","132","0","
|
||||
0","0","0"
|
||||
"libtransport.a","14237","1271","50","1145","76","11942","11942","1024","0","102
|
||||
4","0","0","0","0"
|
||||
"libhttp_parser.a","12694","0","0","0","0","11980","11980","714","0","714","0","
|
||||
0","0","0"
|
||||
"libesp_driver_uart.a","11468","225","25","200","0","10782","10782","461","0","4
|
||||
61","0","0","0","0"
|
||||
"libesp_system.a","11009","1595","142","49","1404","8866","8866","548","0","548"
|
||||
,"0","0","0","0"
|
||||
"libesp_http_server.a","10867","0","0","0","0","10528","10528","339","0","339","
|
||||
0","0","0","0"
|
||||
"libheap.a","10856","6894","8","4","6882","3240","3240","722","0","722","0","0",
|
||||
"0","0"
|
||||
"libesp_driver_spi.a","10004","3507","8","131","3368","6068","6068","429","0","4
|
||||
29","0","0","0","0"
|
||||
"libesp_netif.a","7572","37","29","8","0","7304","7304","231","0","231","0","0",
|
||||
"0","0"
|
||||
"libcoexist.a","6051","461","6","301","154","4316","4316","1274","0","1274","0",
|
||||
"0","0","0"
|
||||
"libesp_driver_gpio.a","6008","216","0","36","180","5238","5238","554","0","554"
|
||||
,"0","0","0","0"
|
||||
"libesp_libc.a","5358","3584","564","324","2696","1622","1622","152","16","136",
|
||||
"0","0","0","0"
|
||||
"libesp_wifi.a","5144","703","43","480","180","3618","3618","823","0","823","0",
|
||||
"0","0","0"
|
||||
"libcjson.a","5116","20","8","12","0","5096","5096","0","0","0","0","0","0","0"
|
||||
"libvfs.a","5051","236","44","192","0","4676","4676","139","0","139","0","0","0"
|
||||
,"0"
|
||||
"libesp_ringbuf.a","4860","2894","0","0","2894","1492","1492","474","0","474","0
|
||||
","0","0","0"
|
||||
"libesp_hal_mspi.a","4366","3792","0","36","3756","574","574","0","0","0","0","0
|
||||
","0","0"
|
||||
"libbootloader_support.a","4284","686","8","0","678","3510","3510","88","0","88"
|
||||
,"0","0","0","0"
|
||||
"libesp_mm.a","4183","1325","48","103","1174","2644","2644","214","0","214","0",
|
||||
"0","0","0"
|
||||
"libesp_event.a","3691","4","4","0","0","3546","3546","141","0","141","0","0","0
|
||||
","0"
|
||||
"libesp_phy.a","3455","248","51","9","188","2908","2908","299","0","299","0","0"
|
||||
,"0","0"
|
||||
"libesp_timer.a","3035","1364","36","16","1312","1580","1580","91","0","91","0",
|
||||
"0","0","0"
|
||||
"libapp_update.a","2996","12","12","0","0","2922","2922","62","0","62","0","0","
|
||||
0","0"
|
||||
"libbtbb.a","2868","0","0","0","0","2868","2868","0","0","0","0","0","0","0"
|
||||
"libesp_partition.a","2796","8","8","0","0","2620","2620","168","0","168","0","0
|
||||
","0","0"
|
||||
"libesp_driver_dma.a","2774","140","0","12","128","2532","2532","102","0","102",
|
||||
"0","0","0","0"
|
||||
"libhal.a","2564","2212","5","235","1972","352","352","0","0","0","0","0","0","0
|
||||
"
|
||||
"libesp_hal_gpspi.a","2550","2194","0","20","2174","284","284","72","0","72","0"
|
||||
,"0","0","0"
|
||||
"libefuse.a","2461","320","4","316","0","1654","1654","487","0","487","0","0","0
|
||||
","0"
|
||||
"libesp_driver_usb_serial_jtag.a","2172","157","17","48","92","1874","1874","141
|
||||
","0","141","0","0","0","0"
|
||||
"libstdc++.a","2056","33","25","8","0","1664","1664","359","0","359","0","0","0"
|
||||
,"0"
|
||||
"libesp_driver_tsens.a","1942","8","4","4","0","1714","1714","220","0","220","0"
|
||||
,"0","0","0"
|
||||
"liblog.a","1894","700","276","8","416","1146","1146","48","0","48","0","0","0",
|
||||
"0"
|
||||
"libesp_common.a","1774","0","0","0","0","48","48","1726","0","1726","0","0","0"
|
||||
,"0"
|
||||
"libesp_hal_ana_conv.a","1559","9","0","9","0","1450","1450","100","0","100","0"
|
||||
,"0","0","0"
|
||||
"libriscv.a","1294","1112","256","0","856","132","132","50","0","50","0","0","0"
|
||||
,"0"
|
||||
"libesp_hal_security.a","1260","0","0","0","0","1260","1260","0","0","0","0","0"
|
||||
,"0","0"
|
||||
"libpthread.a","1223","4","4","0","0","1130","1130","89","0","89","0","0","0","0
|
||||
"
|
||||
"libesp_hal_dma.a","1022","366","0","4","362","656","656","0","0","0","0","0","0
|
||||
","0"
|
||||
"libesp_hal_wdt.a","1005","1005","0","21","984","0","0","0","0","0","0","0","0",
|
||||
"0"
|
||||
"libesp_security.a","922","12","12","0","0","902","902","8","0","8","0","0","0",
|
||||
"0"
|
||||
"libesp_app_format.a","770","10","10","0","0","496","496","264","0","8","256","0
|
||||
","0","0"
|
||||
"libcxx.a","740","16","16","0","0","724","724","0","0","0","0","0","0","0"
|
||||
"libesp_hal_uart.a","674","0","0","0","0","618","618","56","0","56","0","0","0",
|
||||
"0"
|
||||
"libesp_hal_gpio.a","468","0","0","0","0","440","440","28","0","28","0","0","0",
|
||||
"0"
|
||||
"libesp_coex.a","428","154","0","80","74","274","274","0","0","0","0","0","0","0
|
||||
"
|
||||
"libesp_hal_clock.a","404","0","0","0","0","404","404","0","0","0","0","0","0","
|
||||
0"
|
||||
"libcore.a","366","9","9","0","0","314","314","43","0","43","0","0","0","0"
|
||||
"libsoc.a","364","0","0","0","0","0","0","364","0","364","0","0","0","0"
|
||||
"libmesh.a","64","4","4","0","0","60","60","0","0","0","0","0","0","0"
|
||||
"libmbedtls.a","32","0","0","0","0","24","24","8","0","8","0","0","0","0"
|
||||
"libespnow.a","3","3","0","3","0","0","0","0","0","0","0","0","0","0"
|
||||
"libnvs_sec_provider.a","2","0","0","0","0","2","2","0","0","0","0","0","0","0"
|
||||
|
@@ -1,217 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Scan DEBUG_* format strings and estimate max expanded line length (not LOG_RING_SIZE)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
PAT = re.compile(
|
||||
r"DEBUG_(ERR|WARN|INFO|DBG)\s*\(\s*([^,]+)\s*,\s*\"((?:\\.|[^\"\\])*)\"",
|
||||
re.M,
|
||||
)
|
||||
|
||||
|
||||
def unescape_fmt(fmt: str) -> str:
|
||||
try:
|
||||
return bytes(fmt, "utf-8").decode("unicode_escape")
|
||||
except Exception:
|
||||
return (
|
||||
fmt.replace("\\n", "\n")
|
||||
.replace("\\r", "\r")
|
||||
.replace("\\t", "\t")
|
||||
.replace('\\"', '"')
|
||||
)
|
||||
|
||||
|
||||
def fmt_max_len(fmt: str) -> tuple[int, bool]:
|
||||
"""Return (max_len, unbounded_string) for fmt body (without our [L][tag] prefix)."""
|
||||
i = 0
|
||||
n = len(fmt)
|
||||
out = 0
|
||||
unbounded = False
|
||||
while i < n:
|
||||
if fmt[i] != "%":
|
||||
j = fmt.find("%", i)
|
||||
if j < 0:
|
||||
out += n - i
|
||||
break
|
||||
out += j - i
|
||||
i = j
|
||||
continue
|
||||
if i + 1 < n and fmt[i + 1] == "%":
|
||||
out += 1
|
||||
i += 2
|
||||
continue
|
||||
i += 1
|
||||
while i < n and fmt[i] in "-+ #0":
|
||||
i += 1
|
||||
width = 0
|
||||
if i < n and fmt[i] == "*":
|
||||
i += 1
|
||||
else:
|
||||
while i < n and fmt[i].isdigit():
|
||||
width = width * 10 + int(fmt[i])
|
||||
i += 1
|
||||
prec = None
|
||||
if i < n and fmt[i] == ".":
|
||||
i += 1
|
||||
if i < n and fmt[i] == "*":
|
||||
prec = 0
|
||||
i += 1
|
||||
else:
|
||||
prec = 0
|
||||
while i < n and fmt[i].isdigit():
|
||||
prec = prec * 10 + int(fmt[i])
|
||||
i += 1
|
||||
while i < n and fmt[i] in "hlLzjtq":
|
||||
i += 1
|
||||
if i >= n:
|
||||
break
|
||||
conv = fmt[i]
|
||||
i += 1
|
||||
if conv in "diouxX":
|
||||
est = 20 if conv in "diou" else 16
|
||||
if prec is not None:
|
||||
est = max(est, prec)
|
||||
if width:
|
||||
est = max(est, width)
|
||||
out += est
|
||||
elif conv in "fFeEgG":
|
||||
est = 48 # long double-ish
|
||||
if prec is not None:
|
||||
est = max(est, prec + 16)
|
||||
if width:
|
||||
est = max(est, width)
|
||||
out += est
|
||||
elif conv == "s":
|
||||
if prec is not None:
|
||||
est = prec
|
||||
else:
|
||||
# no precision: could be long; use 128 as aggressive scan bound + flag
|
||||
est = 128
|
||||
unbounded = True
|
||||
if width:
|
||||
est = max(est, width)
|
||||
out += est
|
||||
elif conv == "c":
|
||||
out += max(1, width or 1)
|
||||
elif conv == "p":
|
||||
out += 18
|
||||
elif conv == "PRIu32" or True:
|
||||
# leftover / unknown after PRI macros already expanded in source rarely
|
||||
if conv not in "diouxXfFeEgGscp%":
|
||||
out += 12
|
||||
else:
|
||||
out += 8
|
||||
else:
|
||||
out += 8
|
||||
return out, unbounded
|
||||
|
||||
|
||||
def tag_len(tag_expr: str) -> tuple[int, str]:
|
||||
tag_expr = tag_expr.strip()
|
||||
m = re.match(r'^"([^"]*)"$', tag_expr)
|
||||
if m:
|
||||
return len(m.group(1)), m.group(1)
|
||||
# TAG macro / variable: measure common names
|
||||
if tag_expr in ("TAG", "kTag", "tag"):
|
||||
return 12, tag_expr
|
||||
return max(len(tag_expr), 8), tag_expr
|
||||
|
||||
|
||||
def main() -> int:
|
||||
rows = []
|
||||
for path in ROOT.rglob("*"):
|
||||
if path.suffix not in {".cpp", ".c", ".h"}:
|
||||
continue
|
||||
parts = path.parts
|
||||
if "build" in parts or ".git" in parts:
|
||||
continue
|
||||
if "components" in parts and "cjson" in parts:
|
||||
continue
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8", errors="replace")
|
||||
except OSError:
|
||||
continue
|
||||
rel = path.relative_to(ROOT).as_posix()
|
||||
for m in PAT.finditer(text):
|
||||
level, tag_expr, fmt_raw = m.group(1), m.group(2), m.group(3)
|
||||
fmt = unescape_fmt(fmt_raw)
|
||||
# strip trailing newline in fmt for body math; we add +1 for forced \n
|
||||
body_src = fmt.rstrip("\n\r")
|
||||
body, unb = fmt_max_len(body_src)
|
||||
tl, tag = tag_len(tag_expr)
|
||||
prefix = 6 + tl # [L][tag]_ e.g. [I][WIFI]
|
||||
total = prefix + body + 1 # + newline
|
||||
# line number
|
||||
line_no = text.count("\n", 0, m.start()) + 1
|
||||
rows.append(
|
||||
{
|
||||
"total": total,
|
||||
"body": body,
|
||||
"prefix": prefix,
|
||||
"unbounded": unb,
|
||||
"level": level,
|
||||
"tag": tag,
|
||||
"fmt": body_src,
|
||||
"file": rel,
|
||||
"line": line_no,
|
||||
}
|
||||
)
|
||||
|
||||
rows.sort(key=lambda r: (-r["total"], r["file"], r["line"]))
|
||||
print(f"DEBUG_* sites: {len(rows)}")
|
||||
print(f"design cap LOG_RING_SIZE=128 → usable max line = 127 bytes\n")
|
||||
print("--- Top 50 by estimated max total ([L][tag]+body+\\n) ---")
|
||||
print(f"{'est':>5} {'body':>5} {'pre':>3} {'>%159':>5} level tag file:line")
|
||||
for r in rows[:50]:
|
||||
over = "YES" if r["total"] > 159 else ""
|
||||
unb = "*" if r["unbounded"] else " "
|
||||
fmt_short = r["fmt"].replace("\n", "\\n")
|
||||
if len(fmt_short) > 70:
|
||||
fmt_short = fmt_short[:67] + "..."
|
||||
print(
|
||||
f"{r['total']:5d}{unb}{r['body']:5d} {r['prefix']:3d} {over:>5} "
|
||||
f"{r['level']:4s} {r['tag'][:14]:14s} {r['file']}:{r['line']}"
|
||||
)
|
||||
print(f" {fmt_short}")
|
||||
|
||||
print("\n--- length histogram (estimated total) ---")
|
||||
bins = [(0, 80), (81, 110), (111, 127), (128, 160), (161, 200), (201, 99999)]
|
||||
for lo, hi in bins:
|
||||
c = sum(1 for r in rows if lo <= r["total"] <= hi)
|
||||
print(f" {lo:5d}-{hi:5d}: {c:4d}")
|
||||
|
||||
over = [r for r in rows if r["total"] > 159]
|
||||
unb = [r for r in rows if r["unbounded"]]
|
||||
print(f"\nSites with est > 159 (would truncate in ring): {len(over)}")
|
||||
print(f"Sites with unbounded %s (est used 128 for %s): {len(unb)}")
|
||||
|
||||
# Also report pure fmt string lengths (no conversion expansion)
|
||||
print("\n--- Top 20 by format string literal length only ---")
|
||||
by_lit = sorted(rows, key=lambda r: (-len(r["fmt"]), r["file"]))
|
||||
for r in by_lit[:20]:
|
||||
print(f" lit={len(r['fmt']):3d} est={r['total']:4d} {r['file']}:{r['line']} {r['fmt'][:80]!r}")
|
||||
|
||||
# max among sites without unbounded %s
|
||||
bounded = [r for r in rows if not r["unbounded"]]
|
||||
if bounded:
|
||||
m = max(bounded, key=lambda r: r["total"])
|
||||
print(
|
||||
f"\nMax est among bounded formats (no open %s): "
|
||||
f"{m['total']} B @ {m['file']}:{m['line']} tag={m['tag']}"
|
||||
)
|
||||
print(f" fmt={m['fmt'][:160]!r}")
|
||||
|
||||
# For unbounded, try better estimate: common %s sources
|
||||
print("\n--- Unbounded %s samples (need runtime arg max) ---")
|
||||
for r in unb[:15]:
|
||||
print(f" est~{r['total']} {r['file']}:{r['line']} {r['fmt'][:100]!r}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,294 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""List DEBUG_* call sites that can exceed 128 bytes (UTF-8), longest first.
|
||||
|
||||
Uses realistic bounds for known %s sources (SHA 64, SPI hex, esp_err_to_name, etc.).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
THRESHOLD = 128 # bytes, including [L][TAG] prefix + body + \\n
|
||||
|
||||
PAT = re.compile(
|
||||
r"DEBUG_(ERR|WARN|INFO|DBG)\s*\(\s*([^,]+)\s*,\s*\"((?:\\.|[^\"\\])*)\"\s*(?:,|\))",
|
||||
re.M,
|
||||
)
|
||||
|
||||
# When we can see following args in a window, classify %s
|
||||
ARG_WIN = 200
|
||||
|
||||
|
||||
def unescape_fmt(fmt: str) -> str:
|
||||
try:
|
||||
return bytes(fmt, "utf-8").decode("unicode_escape")
|
||||
except Exception:
|
||||
return fmt.replace("\\n", "\n").replace("\\r", "\r").replace("\\t", "\t")
|
||||
|
||||
|
||||
def tag_info(tag_expr: str) -> tuple[int, str]:
|
||||
tag_expr = tag_expr.strip()
|
||||
m = re.match(r'^"([^"]*)"$', tag_expr)
|
||||
if m:
|
||||
return len(m.group(1).encode("utf-8")), m.group(1)
|
||||
# TAG / kTag variable — common values up to ~12
|
||||
return 12, tag_expr
|
||||
|
||||
|
||||
def classify_s_arg(arg: str) -> int:
|
||||
"""Max bytes for a %s argument expression (UTF-8)."""
|
||||
a = arg.strip()
|
||||
# SHA hex buffers
|
||||
if re.search(r"sha|hex|device_hex|frontend_sha", a, re.I):
|
||||
if "64" in a or "sha" in a.lower() or "hex" in a.lower():
|
||||
# device_hex / frontend_sha_hex are 64-char C strings
|
||||
if re.search(r"frontend_sha|device_hex|device_sha|out_device", a, re.I):
|
||||
return 64
|
||||
if re.search(r"resp_hex|payload_hex|hex\[", a, re.I):
|
||||
return 512 # worst unless capped in call site
|
||||
return 64
|
||||
if re.search(r"resp_hex", a, re.I):
|
||||
return 512 # SPI_OTA_FRAME_MAX*2
|
||||
if re.search(r"\bhex\b", a, re.I):
|
||||
# local hex[] in ota_relay often 64–72 chars
|
||||
return 72
|
||||
if "esp_err_to_name" in a:
|
||||
return 32
|
||||
if re.search(r"FW_VERSION|GIT_HASH|BUILD_TIME|version", a, re.I):
|
||||
return 16
|
||||
if re.search(r"partition|label|name|pcTaskName|task", a, re.I):
|
||||
return 16
|
||||
if re.search(r"stage|TAG|tag", a, re.I):
|
||||
return 24
|
||||
if re.search(r"ssid|ip_str|ip", a, re.I):
|
||||
return 32
|
||||
# string literals
|
||||
m = re.match(r'^"((?:\\.|[^"\\])*)"$', a)
|
||||
if m:
|
||||
return len(unescape_fmt(m.group(1)).encode("utf-8"))
|
||||
# ternary "...":"..."
|
||||
if "?" in a and ":" in a:
|
||||
return 16
|
||||
return 48 # generic unknown %s
|
||||
|
||||
|
||||
def parse_args_after_fmt(text: str, fmt_end: int) -> list[str]:
|
||||
"""Rough parse of C args after the format string until closing paren of DEBUG_*."""
|
||||
# fmt ends at quote; skip ", then args
|
||||
i = fmt_end
|
||||
# find the comma after format or )
|
||||
while i < len(text) and text[i] in " \t\r\n":
|
||||
i += 1
|
||||
if i < len(text) and text[i] == ")":
|
||||
return []
|
||||
if i < len(text) and text[i] == ",":
|
||||
i += 1
|
||||
args: list[str] = []
|
||||
depth = 0
|
||||
start = i
|
||||
in_str = False
|
||||
esc = False
|
||||
while i < len(text):
|
||||
c = text[i]
|
||||
if in_str:
|
||||
if esc:
|
||||
esc = False
|
||||
elif c == "\\":
|
||||
esc = True
|
||||
elif c == '"':
|
||||
in_str = False
|
||||
i += 1
|
||||
continue
|
||||
if c == '"':
|
||||
in_str = True
|
||||
i += 1
|
||||
continue
|
||||
if c in "([{":
|
||||
depth += 1
|
||||
elif c in ")]}":
|
||||
if depth == 0 and c == ")":
|
||||
part = text[start:i].strip()
|
||||
if part:
|
||||
args.append(part)
|
||||
break
|
||||
depth -= 1
|
||||
elif c == "," and depth == 0:
|
||||
part = text[start:i].strip()
|
||||
if part:
|
||||
args.append(part)
|
||||
start = i + 1
|
||||
i += 1
|
||||
if i - start > 500:
|
||||
break
|
||||
return args
|
||||
|
||||
|
||||
def body_max_bytes(fmt: str, args: list[str]) -> tuple[int, list[str]]:
|
||||
"""Walk fmt and consume args for max expanded body length in UTF-8 bytes."""
|
||||
# Work on UTF-8: literal parts as utf-8 len
|
||||
i = 0
|
||||
n = len(fmt)
|
||||
out = 0
|
||||
arg_i = 0
|
||||
notes: list[str] = []
|
||||
|
||||
def next_arg() -> str:
|
||||
nonlocal arg_i
|
||||
if arg_i < len(args):
|
||||
a = args[arg_i]
|
||||
arg_i += 1
|
||||
return a
|
||||
return ""
|
||||
|
||||
while i < n:
|
||||
if fmt[i] != "%":
|
||||
j = fmt.find("%", i)
|
||||
if j < 0:
|
||||
out += len(fmt[i:].encode("utf-8"))
|
||||
break
|
||||
out += len(fmt[i:j].encode("utf-8"))
|
||||
i = j
|
||||
continue
|
||||
if i + 1 < n and fmt[i + 1] == "%":
|
||||
out += 1
|
||||
i += 2
|
||||
continue
|
||||
i += 1
|
||||
while i < n and fmt[i] in "-+ #0":
|
||||
i += 1
|
||||
width = 0
|
||||
if i < n and fmt[i] == "*":
|
||||
next_arg() # width arg
|
||||
i += 1
|
||||
else:
|
||||
while i < n and fmt[i].isdigit():
|
||||
width = width * 10 + int(fmt[i])
|
||||
i += 1
|
||||
prec = None
|
||||
if i < n and fmt[i] == ".":
|
||||
i += 1
|
||||
if i < n and fmt[i] == "*":
|
||||
next_arg()
|
||||
prec = 0
|
||||
i += 1
|
||||
else:
|
||||
prec = 0
|
||||
while i < n and fmt[i].isdigit():
|
||||
prec = prec * 10 + int(fmt[i])
|
||||
i += 1
|
||||
# length
|
||||
while i < n and fmt[i] in "hlLzjtq":
|
||||
i += 1
|
||||
# PRI macros might appear as broken sequence in source - rare
|
||||
if i >= n:
|
||||
break
|
||||
conv = fmt[i]
|
||||
i += 1
|
||||
arg = next_arg()
|
||||
if conv in "diouxX":
|
||||
est = 20 if conv in "diou" else 16
|
||||
if prec is not None:
|
||||
est = max(est, prec)
|
||||
if width:
|
||||
est = max(est, width)
|
||||
out += est
|
||||
elif conv in "fFeEgG":
|
||||
est = 32
|
||||
if prec is not None:
|
||||
est = max(est, prec + 8)
|
||||
if width:
|
||||
est = max(est, width)
|
||||
out += est
|
||||
elif conv == "s":
|
||||
if prec is not None:
|
||||
est = prec
|
||||
else:
|
||||
est = classify_s_arg(arg)
|
||||
notes.append(f"%s←{est}:{arg[:40]}")
|
||||
if width:
|
||||
est = max(est, width)
|
||||
out += est
|
||||
elif conv == "c":
|
||||
out += max(1, width or 1)
|
||||
elif conv == "p":
|
||||
out += 18
|
||||
else:
|
||||
# maybe start of PRIu32 in string - treat as literal leftover
|
||||
out += 12
|
||||
return out, notes
|
||||
|
||||
|
||||
def main() -> int:
|
||||
rows = []
|
||||
for path in ROOT.rglob("*"):
|
||||
if path.suffix not in {".cpp", ".c", ".h"}:
|
||||
continue
|
||||
if "build" in path.parts or ".git" in path.parts:
|
||||
continue
|
||||
if "components" in path.parts and "cjson" in path.parts:
|
||||
continue
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8", errors="replace")
|
||||
except OSError:
|
||||
continue
|
||||
rel = path.relative_to(ROOT).as_posix()
|
||||
for m in PAT.finditer(text):
|
||||
level, tag_expr, fmt_raw = m.group(1), m.group(2), m.group(3)
|
||||
fmt = unescape_fmt(fmt_raw).rstrip("\n\r")
|
||||
# end of format string in source: position after closing quote of fmt
|
||||
# m.end() is after last "; use group 3 span
|
||||
fmt_end = m.end(3) + 1 # past closing "
|
||||
args = parse_args_after_fmt(text, fmt_end)
|
||||
body, notes = body_max_bytes(fmt, args)
|
||||
tlen, tag = tag_info(tag_expr)
|
||||
prefix = len(f"[{level[0] if False else ''}]") # placeholder
|
||||
# real prefix: [E]/[W]/[I]/[D] + [tag] + space
|
||||
level_c = {"ERR": "E", "WARN": "W", "INFO": "I", "DBG": "D"}.get(level, "?")
|
||||
prefix = len(f"[{level_c}][{tag}] ".encode("utf-8")) if tag_expr.strip().startswith('"') else (6 + tlen)
|
||||
if not tag_expr.strip().startswith('"'):
|
||||
# use tlen for tag byte len estimate (ASCII tags)
|
||||
prefix = 6 + tlen
|
||||
else:
|
||||
prefix = len(f"[{level_c}][{tag}] ".encode("utf-8"))
|
||||
total = prefix + body + 1 # + \\n
|
||||
if total <= THRESHOLD:
|
||||
continue
|
||||
line_no = text.count("\n", 0, m.start()) + 1
|
||||
rows.append(
|
||||
{
|
||||
"total": total,
|
||||
"body": body,
|
||||
"prefix": prefix,
|
||||
"level": level,
|
||||
"tag": tag if tag_expr.strip().startswith('"') else tag_expr.strip(),
|
||||
"fmt": fmt,
|
||||
"file": rel,
|
||||
"line": line_no,
|
||||
"notes": notes,
|
||||
"args": args,
|
||||
}
|
||||
)
|
||||
|
||||
rows.sort(key=lambda r: (-r["total"], r["file"], r["line"]))
|
||||
print(f"Threshold: >{THRESHOLD} bytes (prefix+body+newline, UTF-8)")
|
||||
print(f"Candidates: {len(rows)}\n")
|
||||
print(f"{'#':>3} {'est':>5} {'body':>5} {'pre':>3} {'lv':4} {'tag':12} location")
|
||||
print("-" * 100)
|
||||
for i, r in enumerate(rows, 1):
|
||||
print(
|
||||
f"{i:3d} {r['total']:5d} {r['body']:5d} {r['prefix']:3d} "
|
||||
f"{r['level']:4s} {str(r['tag'])[:12]:12s} {r['file']}:{r['line']}"
|
||||
)
|
||||
fmt_show = r["fmt"].replace("\n", "\\n")
|
||||
if len(fmt_show) > 100:
|
||||
fmt_show = fmt_show[:97] + "..."
|
||||
print(f" fmt: {fmt_show}")
|
||||
if r["notes"]:
|
||||
print(f" args: {'; '.join(r['notes'][:6])}")
|
||||
print(f"\nTotal over {THRESHOLD}B: {len(rows)}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,167 @@
|
||||
#!/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())
|
||||
+6
-6
@@ -4,11 +4,11 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ESP32 CAN 通信控制台</title>
|
||||
<link rel="stylesheet" href="css/base.css?v=20260717x">
|
||||
<link rel="stylesheet" href="css/layout.css?v=20260717x">
|
||||
<link rel="stylesheet" href="css/components.css?v=20260717x">
|
||||
<link rel="stylesheet" href="css/can.css?v=20260719s">
|
||||
<link rel="stylesheet" href="css/responsive.css?v=20260717x">
|
||||
<link rel="stylesheet" href="css/base.css?v=7d5f4eef">
|
||||
<link rel="stylesheet" href="css/layout.css?v=a3e0b7e2">
|
||||
<link rel="stylesheet" href="css/components.css?v=475fa1ac">
|
||||
<link rel="stylesheet" href="css/can.css?v=8a9342b7">
|
||||
<link rel="stylesheet" href="css/responsive.css?v=61b66182">
|
||||
</head>
|
||||
<body class="can-page">
|
||||
<div class="container can-container">
|
||||
@@ -318,6 +318,6 @@
|
||||
<span id="toastMsg">操作成功</span>
|
||||
</div>
|
||||
|
||||
<script type="module" src="js/can.js?v=20260720b"></script>
|
||||
<script type="module" src="js/can.js?v=fad32c3a"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ESP32 CAN 接收监控</title>
|
||||
<link rel="stylesheet" href="css/base.css?v=20260716s">
|
||||
<link rel="stylesheet" href="css/layout.css?v=20260716s">
|
||||
<link rel="stylesheet" href="css/components.css?v=20260716s">
|
||||
<link rel="stylesheet" href="css/can.css?v=20260719s">
|
||||
<link rel="stylesheet" href="css/responsive.css?v=20260716s">
|
||||
<link rel="stylesheet" href="css/base.css?v=7d5f4eef">
|
||||
<link rel="stylesheet" href="css/layout.css?v=a3e0b7e2">
|
||||
<link rel="stylesheet" href="css/components.css?v=475fa1ac">
|
||||
<link rel="stylesheet" href="css/can.css?v=8a9342b7">
|
||||
<link rel="stylesheet" href="css/responsive.css?v=61b66182">
|
||||
</head>
|
||||
<body class="can-page">
|
||||
<div class="container can-container">
|
||||
@@ -182,6 +182,6 @@
|
||||
<span id="toastMsg">操作成功</span>
|
||||
</div>
|
||||
|
||||
<script type="module" src="js/can_monitor.js?v=20260719s"></script>
|
||||
<script type="module" src="js/can_monitor.js?v=ef00eb03"></script>
|
||||
</body>
|
||||
</html>
|
||||
+107
-29
@@ -1,28 +1,74 @@
|
||||
/* === base.css — 基础变量与容器 === */
|
||||
/* ===== ESP32 OTA 控制台 — 工业调试台风格 ===== */
|
||||
/* ============================================================
|
||||
base.css — DCU Mission Control · 设计令牌与全局基底
|
||||
风格:深色测控台(deep blue-slate),多色子系统语义配色
|
||||
子系统色:ESP32=青绿 · GD32=琥珀 · BLE=蓝 · CAN=绿 · 内存/调试=紫
|
||||
============================================================ */
|
||||
|
||||
:root {
|
||||
--bg-root: #090e15;
|
||||
--bg-panel: #0f161f;
|
||||
--bg-card: #141d28;
|
||||
--bg-elevated: #1a2432;
|
||||
--bg-input: #0d131b;
|
||||
--border: #1d2a3a;
|
||||
--border-focus: #22d3ee;
|
||||
--text: #e2e8f0;
|
||||
--text-dim: #7b8fa8;
|
||||
--text-label: #5a6d82;
|
||||
--accent: #22d3ee;
|
||||
--accent-dim: rgba(34,211,238,.15);
|
||||
--green: #22c55e;
|
||||
--amber: #f59e0b;
|
||||
--red: #ef4444;
|
||||
--radius-sm: 6px;
|
||||
--radius: 10px;
|
||||
--radius-lg: 14px;
|
||||
/* ---- 底色层次:深蓝灰,层层抬升,拒绝纯黑 ---- */
|
||||
--bg-root: #0a0f1a;
|
||||
--bg-panel: #0e1624;
|
||||
--bg-card: #131d2e;
|
||||
--bg-elevated: #1a2740;
|
||||
--bg-input: #0b1220;
|
||||
--bg-hover: #20304c;
|
||||
|
||||
/* ---- 边框 ---- */
|
||||
--border: #21304a;
|
||||
--border-strong: #2e4262;
|
||||
--border-subtle: #1a2740;
|
||||
|
||||
/* ---- 文字 ---- */
|
||||
--text: #e9f0fa;
|
||||
--text-dim: #93a8c4;
|
||||
--text-label: #617a99;
|
||||
|
||||
/* ---- 子系统语义色 ---- */
|
||||
--esp32: #2fd8c1; /* 青绿:ESP32 / 主操作 / WiFi OTA */
|
||||
--esp32-hi: #6ff2e0;
|
||||
--esp32-dim: rgba(47,216,193,.14);
|
||||
--gd32: #fcbf3f; /* 琥珀:GD32 / 中继 OTA */
|
||||
--gd32-hi: #ffd97a;
|
||||
--gd32-dim: rgba(252,191,63,.14);
|
||||
--ble: #5fa8ff; /* 蓝:BLE 蓝牙通道 */
|
||||
--ble-hi: #93c6ff;
|
||||
--ble-dim: rgba(95,168,255,.14);
|
||||
--can: #43d97b; /* 绿:CAN 总线 / 健康 */
|
||||
--can-hi: #7deda6;
|
||||
--can-dim: rgba(67,217,123,.14);
|
||||
--mem: #a88bfa; /* 紫:内存 / 调试 / 日志 */
|
||||
--mem-hi: #c7b3ff;
|
||||
--mem-dim: rgba(168,139,250,.14);
|
||||
|
||||
/* ---- 状态语义色 ---- */
|
||||
--green: #3ddc8e;
|
||||
--amber: #fcbf3f;
|
||||
--red: #ff6b6b;
|
||||
--red-dim: rgba(255,107,107,.14);
|
||||
|
||||
/* ---- 兼容旧令牌(大量旧样式引用,勿删)---- */
|
||||
--accent: var(--esp32);
|
||||
--accent-dim: var(--esp32-dim);
|
||||
--border-focus: var(--esp32);
|
||||
|
||||
/* ---- 圆角 ---- */
|
||||
--radius-sm: 8px;
|
||||
--radius: 12px;
|
||||
--radius-lg: 16px;
|
||||
|
||||
/* ---- 字体 ----
|
||||
display:Chakra Petch(棱角科技感,用于标题/大数字/按钮;
|
||||
无中文字形,中文自动回退苹方/雅黑)
|
||||
mono:JetBrains Mono(SHA / HEX / 日志 / 数值)
|
||||
body:系统中文字体优先,保证正文可读性 */
|
||||
--font-display: "Chakra Petch","PingFang SC","Microsoft YaHei","Segoe UI",sans-serif;
|
||||
--font: "PingFang SC","Microsoft YaHei","Segoe UI",system-ui,sans-serif;
|
||||
--font-mono: "SF Mono","Fira Code","Cascadia Code","Consolas",monospace;
|
||||
--gap: 16px;
|
||||
--font-mono: "JetBrains Mono","SF Mono","Cascadia Code","Consolas",monospace;
|
||||
|
||||
/* ---- 间距 / 阴影 ---- */
|
||||
--gap: 18px;
|
||||
--shadow-card: 0 10px 30px rgba(3,7,16,.45), 0 1px 0 rgba(255,255,255,.03) inset;
|
||||
--shadow-pop: 0 18px 48px rgba(3,7,16,.6);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
@@ -33,23 +79,55 @@ html, body {
|
||||
color: var(--text);
|
||||
font-family: var(--font);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
line-height: 1.55;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* subtle noise-grid background */
|
||||
/* ---- 环境光背景:细网格 + 双色氛围辉光 + 顶部扫光 ----
|
||||
网格提供"工程图纸"质感;左上青绿、右下琥珀的两团极淡辉光
|
||||
暗示 ESP32 / GD32 双核子系统,随视口固定,营造纵深。 */
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed; inset: 0; z-index: 0; pointer-events: none;
|
||||
background-image:
|
||||
linear-gradient(rgba(255,255,255,.015) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255,255,255,.015) 1px, transparent 1px);
|
||||
background-size: 40px 40px;
|
||||
mask-image: radial-gradient(ellipse at 50% 0%, black 40%, transparent 70%);
|
||||
linear-gradient(rgba(148,180,226,.035) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(148,180,226,.035) 1px, transparent 1px);
|
||||
background-size: 44px 44px;
|
||||
mask-image: radial-gradient(ellipse 120% 90% at 50% -8%, black 30%, transparent 78%);
|
||||
}
|
||||
body::after {
|
||||
content: "";
|
||||
position: fixed; inset: 0; z-index: 0; pointer-events: none;
|
||||
background:
|
||||
radial-gradient(900px 520px at 8% -10%, rgba(47,216,193,.10), transparent 60%),
|
||||
radial-gradient(820px 560px at 100% 112%, rgba(252,191,63,.07), transparent 62%),
|
||||
radial-gradient(700px 480px at 88% -6%, rgba(95,168,255,.06), transparent 60%);
|
||||
}
|
||||
|
||||
.container {
|
||||
position: relative; z-index: 1;
|
||||
max-width: 1480px; margin: 0 auto; padding: 24px 32px 40px;
|
||||
max-width: 1480px; margin: 0 auto; padding: 26px 32px 44px;
|
||||
}
|
||||
|
||||
/* ---- 全局滚动条 ---- */
|
||||
::-webkit-scrollbar { width: 9px; height: 9px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--border-strong); border-radius: 99px;
|
||||
border: 2px solid var(--bg-root);
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover { background: #3a5378; }
|
||||
|
||||
/* ---- 选区 ---- */
|
||||
::selection { background: rgba(47,216,193,.28); color: #eafffb; }
|
||||
|
||||
/* ---- 通用等宽数字(让指标/百分比对齐、不跳动)---- */
|
||||
.tabular { font-variant-numeric: tabular-nums; }
|
||||
|
||||
/* ---- focus 可见性(键盘可达性)---- */
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--esp32);
|
||||
outline-offset: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
+470
-923
File diff suppressed because it is too large
Load Diff
+256
-155
@@ -1,241 +1,342 @@
|
||||
/* === layout.css — 头部/主布局/卡片/表单/按钮/连接状态 === */
|
||||
/* === HEADER — compact status bar === */
|
||||
/* ============================================================
|
||||
layout.css — 命令栏头部 / 主布局 / 卡片 / 表单 / 按钮 / 侧滑抽屉
|
||||
============================================================ */
|
||||
|
||||
/* ================= HEADER — 命令栏 ================= */
|
||||
.header-bar {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
padding: 14px 20px; margin-bottom: 24px;
|
||||
background: var(--bg-panel);
|
||||
position: relative;
|
||||
display: flex; align-items: center; gap: 16px;
|
||||
padding: 16px 22px; margin-bottom: 26px;
|
||||
background: linear-gradient(160deg, var(--bg-panel) 0%, var(--bg-card) 70%);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-card);
|
||||
flex-wrap: wrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
/* 顶部三色霓虹描边:ESP32 青绿 → BLE 蓝 → GD32 琥珀,一条细线点出子系统谱系 */
|
||||
.header-bar::before {
|
||||
content: "";
|
||||
position: absolute; top: 0; left: 0; right: 0; height: 2px;
|
||||
background: linear-gradient(90deg,
|
||||
var(--esp32) 0%, var(--ble) 38%, var(--gd32) 78%, transparent 100%);
|
||||
opacity: .85;
|
||||
}
|
||||
/* 右上角极淡的芯片电路装饰 */
|
||||
.header-bar::after {
|
||||
content: "";
|
||||
position: absolute; right: -40px; top: -40px;
|
||||
width: 220px; height: 220px; pointer-events: none;
|
||||
background:
|
||||
radial-gradient(circle at 30% 30%, rgba(47,216,193,.10), transparent 55%);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.header-brand {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
padding-right: 20px; border-right: 1px solid var(--border);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.brand-logo {
|
||||
width: 40px; height: 40px; flex-shrink: 0;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
border-radius: 11px;
|
||||
background: linear-gradient(145deg, rgba(47,216,193,.16), rgba(95,168,255,.10));
|
||||
border: 1px solid rgba(47,216,193,.3);
|
||||
box-shadow: 0 0 18px rgba(47,216,193,.18), inset 0 0 12px rgba(47,216,193,.08);
|
||||
color: var(--esp32);
|
||||
}
|
||||
.brand-logo svg { width: 24px; height: 24px; display: block; }
|
||||
.brand-text { display: flex; flex-direction: column; line-height: 1.15; }
|
||||
.brand-text strong {
|
||||
font-family: var(--font-display);
|
||||
font-size: 17px; font-weight: 700; letter-spacing: .02em;
|
||||
background: linear-gradient(92deg, var(--text) 30%, var(--esp32-hi) 100%);
|
||||
-webkit-background-clip: text; background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
.brand-sub {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px; letter-spacing: .22em; text-transform: uppercase;
|
||||
color: var(--text-label); margin-top: 2px;
|
||||
}
|
||||
/* 兼容旧结构:独立 dot-mark(无 brand-logo 时)仍显示为一颗呼吸 LED */
|
||||
.header-brand .dot-mark {
|
||||
width: 10px; height: 10px; border-radius: 50%; background: var(--accent);
|
||||
box-shadow: 0 0 8px var(--accent-dim);
|
||||
width: 10px; height: 10px; border-radius: 50%;
|
||||
background: var(--esp32);
|
||||
box-shadow: 0 0 10px var(--esp32-dim);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.header-brand strong { font-size: 15px; font-weight: 700; }
|
||||
.header-brand strong { font-size: 16px; font-weight: 700; }
|
||||
|
||||
.header-status {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.header-sub {
|
||||
display: flex; gap: 10px; margin-left: auto;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* ---- 状态胶囊 ---- */
|
||||
.pill {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
padding: 5px 12px; border-radius: 20px; font-size: 12px; font-weight: 500;
|
||||
display: inline-flex; align-items: center; gap: 7px;
|
||||
padding: 6px 13px; border-radius: 999px;
|
||||
font-size: 12px; font-weight: 500;
|
||||
font-family: var(--font-mono);
|
||||
background: var(--bg-elevated); border: 1px solid var(--border);
|
||||
color: var(--text-dim);
|
||||
white-space: nowrap;
|
||||
transition: border-color .2s, background .2s, transform .2s;
|
||||
}
|
||||
.pill .indicator {
|
||||
width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0;
|
||||
background: var(--text-label);
|
||||
}
|
||||
.pill.online .indicator { background: var(--green); }
|
||||
.pill.offline .indicator { background: var(--red); }
|
||||
.pill.processing .indicator { background: var(--amber); animation: blink 1s infinite; }
|
||||
.pill.online .indicator { background: var(--green); box-shadow: 0 0 8px rgba(61,220,142,.6); }
|
||||
.pill.offline .indicator { background: var(--red); box-shadow: 0 0 8px rgba(255,107,107,.5); }
|
||||
.pill.processing .indicator,
|
||||
.pill.live .indicator { background: var(--amber); animation: blink 1s infinite; }
|
||||
.pill.idle .indicator { background: var(--text-dim); }
|
||||
@keyframes blink { 50% { opacity: .35; } }
|
||||
.pill.online { color: var(--can-hi); border-color: rgba(61,220,142,.35); }
|
||||
.pill.offline { color: #ffb3b3; border-color: rgba(255,107,107,.3); }
|
||||
.pill.processing,
|
||||
.pill.live { color: var(--gd32-hi); border-color: rgba(252,191,63,.35); }
|
||||
@keyframes blink { 50% { opacity: .3; } }
|
||||
|
||||
/* === MAIN LAYOUT === */
|
||||
.pill-val {
|
||||
font-weight: 700; color: var(--text);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
/* ================= 主布局 ================= */
|
||||
.main-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 380px 1fr;
|
||||
grid-template-columns: 400px 1fr;
|
||||
gap: var(--gap);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
/* === 右侧滑出抽屉:堆与任务栈 ===
|
||||
* 总宽 = peek 把手 + 面板;默认 translateX(面板宽) 只露把手,hover 整板探出 */
|
||||
.sys-drawer {
|
||||
--sys-drawer-panel-w: min(380px, calc(100vw - 48px));
|
||||
--sys-drawer-peek: 36px;
|
||||
position: fixed;
|
||||
top: 72px;
|
||||
right: 0;
|
||||
z-index: 80;
|
||||
width: calc(var(--sys-drawer-panel-w) + var(--sys-drawer-peek));
|
||||
height: calc(100vh - 88px);
|
||||
max-height: calc(100vh - 88px);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
transform: translateX(var(--sys-drawer-panel-w));
|
||||
transition: transform .28s cubic-bezier(.22, 1, .36, 1);
|
||||
filter: drop-shadow(-6px 0 18px rgba(0, 0, 0, .35));
|
||||
}
|
||||
.sys-drawer:hover,
|
||||
.sys-drawer:focus-within {
|
||||
transform: translateX(0);
|
||||
}
|
||||
/* 平时露出的竖条把手 */
|
||||
.sys-drawer-rail {
|
||||
flex: 0 0 var(--sys-drawer-peek);
|
||||
width: var(--sys-drawer-peek);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 14px 4px;
|
||||
background: linear-gradient(180deg, #1a2a3a 0%, var(--bg-card) 100%);
|
||||
border: 1px solid var(--border);
|
||||
border-right: none;
|
||||
border-radius: var(--radius) 0 0 var(--radius);
|
||||
cursor: default;
|
||||
}
|
||||
.sys-drawer-rail-ico {
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
.sys-drawer-rail-text {
|
||||
writing-mode: vertical-rl;
|
||||
text-orientation: mixed;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: .12em;
|
||||
color: var(--text-dim);
|
||||
text-transform: none;
|
||||
}
|
||||
.sys-drawer:hover .sys-drawer-rail-text,
|
||||
.sys-drawer:focus-within .sys-drawer-rail-text {
|
||||
color: var(--accent);
|
||||
}
|
||||
.sys-drawer-panel {
|
||||
flex: 1 1 var(--sys-drawer-panel-w);
|
||||
width: var(--sys-drawer-panel-w);
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
margin: 0 !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 0;
|
||||
overflow: hidden;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
.sys-drawer-head {
|
||||
cursor: default;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.sys-drawer-head .card-head-btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
.sys-drawer-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
/* === CARDS === */
|
||||
/* ================= 卡片 ================= */
|
||||
.card {
|
||||
background: var(--bg-card);
|
||||
position: relative;
|
||||
background: linear-gradient(170deg, var(--bg-card) 0%, rgba(15,23,38,.92) 100%);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
.card + .card { margin-top: var(--gap); }
|
||||
|
||||
/* 子系统色带:卡片顶部 2px 渐变,由修饰类决定颜色 */
|
||||
.card::before {
|
||||
content: "";
|
||||
position: absolute; top: 0; left: 0; right: 0; height: 2px;
|
||||
background: linear-gradient(90deg, var(--card-accent, var(--esp32)), transparent 85%);
|
||||
opacity: .7; pointer-events: none;
|
||||
}
|
||||
.card--esp32 { --card-accent: var(--esp32); --card-accent-dim: var(--esp32-dim); }
|
||||
.card--gd32 { --card-accent: var(--gd32); --card-accent-dim: var(--gd32-dim); }
|
||||
.card--ble { --card-accent: var(--ble); --card-accent-dim: var(--ble-dim); }
|
||||
.card--can { --card-accent: var(--can); --card-accent-dim: var(--can-dim); }
|
||||
.card--mem { --card-accent: var(--mem); --card-accent-dim: var(--mem-dim); }
|
||||
.card--status { --card-accent: var(--green); --card-accent-dim: var(--can-dim); }
|
||||
|
||||
.card-head {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
font-family: var(--font-display);
|
||||
font-size: 13px; font-weight: 600;
|
||||
color: var(--text-dim);
|
||||
text-transform: uppercase; letter-spacing: .06em;
|
||||
color: var(--text);
|
||||
letter-spacing: .05em;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.card-head .ico { font-size: 15px; }
|
||||
.card-head-btn {
|
||||
margin-left: auto;
|
||||
padding: 4px 10px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
/* 图标芯片:圆角方块底色取自卡片子系统色 */
|
||||
.card-head .ico {
|
||||
width: 28px; height: 28px; flex-shrink: 0;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
font-size: 15px;
|
||||
border-radius: 8px;
|
||||
background: var(--card-accent-dim, var(--esp32-dim));
|
||||
border: 1px solid color-mix(in srgb, var(--card-accent, var(--esp32)) 30%, transparent);
|
||||
}
|
||||
.card-body { padding: 16px 18px; }
|
||||
.card-head-btn { margin-left: auto; padding: 4px 10px; position: relative; z-index: 2; }
|
||||
.card-body { padding: 17px 18px; }
|
||||
.card.collapsed .card-body { display: none; }
|
||||
.card-head { cursor: pointer; }
|
||||
.card-head .collapse-arrow {
|
||||
margin-left: auto; font-size: 10px; opacity: .45;
|
||||
margin-left: auto; font-size: 10px; color: var(--text-label);
|
||||
transition: transform .2s;
|
||||
}
|
||||
.card.collapsed .collapse-arrow { transform: rotate(-90deg); }
|
||||
|
||||
/* === FORM ELEMENTS === */
|
||||
/* ================= 表单 ================= */
|
||||
.form-row { margin-bottom: 14px; }
|
||||
.form-row:last-child { margin-bottom: 0; }
|
||||
.form-label {
|
||||
display: block; font-size: 11px; color: var(--text-label);
|
||||
text-transform: uppercase; letter-spacing: .08em; margin-bottom: 6px;
|
||||
display: block;
|
||||
font-family: var(--font-display);
|
||||
font-size: 11px; color: var(--text-label);
|
||||
text-transform: uppercase; letter-spacing: .12em;
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
.input, select, textarea {
|
||||
width: 100%; padding: 10px 12px; font-size: 13px; font-family: inherit;
|
||||
width: 100%; padding: 10px 13px;
|
||||
font-size: 13px; font-family: var(--font-mono);
|
||||
background: var(--bg-input); color: var(--text);
|
||||
border: 1px solid var(--border); border-radius: var(--radius-sm);
|
||||
outline: none; transition: border-color .15s;
|
||||
outline: none;
|
||||
transition: border-color .18s, box-shadow .18s, background .18s;
|
||||
}
|
||||
.input::placeholder { color: var(--text-label); }
|
||||
.input:focus, select:focus, textarea:focus {
|
||||
border-color: var(--border-focus);
|
||||
box-shadow: 0 0 0 3px var(--accent-dim);
|
||||
border-color: var(--esp32);
|
||||
background: color-mix(in srgb, var(--esp32) 5%, var(--bg-input));
|
||||
box-shadow: 0 0 0 3px var(--esp32-dim);
|
||||
}
|
||||
select { cursor: pointer; }
|
||||
select option { background: var(--bg-panel); color: var(--text); }
|
||||
.input-group { display: flex; gap: 8px; }
|
||||
.input-group .input { flex: 1; }
|
||||
|
||||
/* === BUTTONS === */
|
||||
/* ================= 按钮 ================= */
|
||||
.btn {
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
|
||||
padding: 9px 16px; border: none; border-radius: var(--radius-sm);
|
||||
font-size: 13px; font-weight: 600; cursor: pointer;
|
||||
transition: opacity .15s, transform .15s;
|
||||
font-family: inherit; white-space: nowrap;
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
||||
padding: 9px 17px; border: none; border-radius: var(--radius-sm);
|
||||
font-family: var(--font-display);
|
||||
font-size: 13px; font-weight: 600; letter-spacing: .03em;
|
||||
cursor: pointer; white-space: nowrap;
|
||||
transition: transform .16s, box-shadow .2s, background .2s, border-color .2s, color .2s, opacity .2s;
|
||||
}
|
||||
.btn:hover:not(:disabled) { transform: translateY(-1px); }
|
||||
.btn:disabled { opacity: .4; cursor: not-allowed; transform: none; }
|
||||
.btn-primary { background: var(--accent); color: #07121c; }
|
||||
.btn-primary:hover:not(:disabled) { opacity: .88; }
|
||||
.btn-success { background: var(--green); color: #051a0c; }
|
||||
.btn-danger { background: var(--red); color: #fff; }
|
||||
.btn:active:not(:disabled) { transform: translateY(0) scale(.97); }
|
||||
.btn:disabled { opacity: .38; cursor: not-allowed; transform: none; }
|
||||
|
||||
/* 主操作 = ESP32 青绿 */
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--esp32) 0%, #14b8a6 100%);
|
||||
color: #04231e;
|
||||
box-shadow: 0 4px 16px rgba(47,216,193,.28), inset 0 1px 0 rgba(255,255,255,.25);
|
||||
}
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
box-shadow: 0 6px 22px rgba(47,216,193,.4), inset 0 1px 0 rgba(255,255,255,.3);
|
||||
}
|
||||
.btn-success {
|
||||
background: linear-gradient(135deg, var(--green) 0%, #16a374 100%);
|
||||
color: #03231a;
|
||||
box-shadow: 0 4px 16px rgba(61,220,142,.25), inset 0 1px 0 rgba(255,255,255,.22);
|
||||
}
|
||||
.btn-danger {
|
||||
background: linear-gradient(135deg, var(--red) 0%, #dc4c4c 100%);
|
||||
color: #fff;
|
||||
box-shadow: 0 4px 16px rgba(255,107,107,.25), inset 0 1px 0 rgba(255,255,255,.2);
|
||||
}
|
||||
.btn-outline {
|
||||
background: transparent; color: var(--text-dim);
|
||||
border: 1px solid var(--border);
|
||||
background: rgba(255,255,255,.02); color: var(--text-dim);
|
||||
border: 1px solid var(--border-strong);
|
||||
}
|
||||
.btn-outline:hover:not(:disabled) { background: var(--bg-elevated); color: var(--text); }
|
||||
.btn-ghost {
|
||||
background: transparent; color: var(--text-dim);
|
||||
.btn-outline:hover:not(:disabled) {
|
||||
background: var(--bg-hover); color: var(--text);
|
||||
border-color: #3d5a84;
|
||||
}
|
||||
.btn-ghost:hover:not(:disabled) { background: var(--bg-elevated); color: var(--text); }
|
||||
.btn-ghost { background: transparent; color: var(--text-dim); }
|
||||
.btn-ghost:hover:not(:disabled) { background: var(--bg-hover); color: var(--text); }
|
||||
.btn-block { width: 100%; justify-content: center; }
|
||||
.btn-sm { padding: 6px 12px; font-size: 12px; }
|
||||
|
||||
/* === CONNECTION STATUS ROW === */
|
||||
/* ---- 多色子系统页签(关键)----
|
||||
console-mode.js 用 btn-primary / btn-outline 表达"选中/未选中",
|
||||
这里按按钮 id 把"选中态"染成各自子系统色:
|
||||
WiFi=青绿 · BLE=蓝 · ESP32=青绿 · GD32=琥珀。 */
|
||||
#modeBleBtn.btn-primary {
|
||||
background: linear-gradient(135deg, var(--ble) 0%, #3b82f6 100%);
|
||||
color: #04142b;
|
||||
box-shadow: 0 4px 16px rgba(95,168,255,.3), inset 0 1px 0 rgba(255,255,255,.25);
|
||||
}
|
||||
#targetGd32Tab.btn-primary {
|
||||
background: linear-gradient(135deg, var(--gd32) 0%, #f59e0b 100%);
|
||||
color: #2b1d02;
|
||||
box-shadow: 0 4px 16px rgba(252,191,63,.3), inset 0 1px 0 rgba(255,255,255,.3);
|
||||
}
|
||||
#targetEsp32Tab.btn-primary {
|
||||
background: linear-gradient(135deg, var(--esp32) 0%, #14b8a6 100%);
|
||||
color: #04231e;
|
||||
}
|
||||
/* GD32 OTA 大按钮 = 琥珀(区别于 ESP32 青绿) */
|
||||
#uploadGd32Btn {
|
||||
background: linear-gradient(135deg, var(--gd32) 0%, #f59e0b 100%);
|
||||
color: #2b1d02;
|
||||
box-shadow: 0 6px 20px rgba(252,191,63,.3), inset 0 1px 0 rgba(255,255,255,.3);
|
||||
}
|
||||
#uploadEsp32Btn {
|
||||
box-shadow: 0 6px 20px rgba(47,216,193,.32), inset 0 1px 0 rgba(255,255,255,.28);
|
||||
}
|
||||
|
||||
/* ================= 连接状态条 / 提示 ================= */
|
||||
.conn-indicator {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 12px 16px; border-radius: var(--radius-sm);
|
||||
background: var(--bg-elevated); border: 1px solid var(--border);
|
||||
font-size: 13px; font-weight: 500;
|
||||
}
|
||||
.conn-indicator .light {
|
||||
width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0;
|
||||
}
|
||||
.conn-indicator.ok .light { background: var(--green); }
|
||||
.conn-indicator.err .light { background: var(--red); }
|
||||
.conn-indicator .light { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
|
||||
.conn-indicator.ok .light { background: var(--green); box-shadow: 0 0 8px rgba(61,220,142,.6); }
|
||||
.conn-indicator.err .light { background: var(--red); box-shadow: 0 0 8px rgba(255,107,107,.5); }
|
||||
.conn-indicator.warn .light { background: var(--amber); }
|
||||
|
||||
.helper-note {
|
||||
margin-top: 10px;
|
||||
padding: 10px 12px;
|
||||
margin-top: 10px; padding: 10px 13px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-elevated);
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
font-size: 12px; line-height: 1.65; color: var(--text-dim);
|
||||
}
|
||||
.helper-note.ok { border-color: rgba(61,220,142,.3); background: rgba(61,220,142,.07); color: #a9f0c6; }
|
||||
.helper-note.warn { border-color: rgba(252,191,63,.3); background: rgba(252,191,63,.07); color: #fbe3ae; }
|
||||
.helper-note.err { border-color: rgba(255,107,107,.3); background: rgba(255,107,107,.07); color: #ffc4c4; }
|
||||
.helper-note code { color: var(--esp32-hi); font-family: var(--font-mono); }
|
||||
|
||||
/* ================= 右侧滑出抽屉:堆与任务栈 ================= */
|
||||
.sys-drawer {
|
||||
--sys-drawer-panel-w: min(390px, calc(100vw - 48px));
|
||||
--sys-drawer-peek: 38px;
|
||||
position: fixed; top: 76px; right: 0; z-index: 80;
|
||||
width: calc(var(--sys-drawer-panel-w) + var(--sys-drawer-peek));
|
||||
height: calc(100vh - 92px); max-height: calc(100vh - 92px);
|
||||
display: flex; flex-direction: row; align-items: stretch;
|
||||
transform: translateX(var(--sys-drawer-panel-w));
|
||||
transition: transform .3s cubic-bezier(.22, 1, .36, 1);
|
||||
filter: drop-shadow(-8px 0 24px rgba(2,5,12,.5));
|
||||
}
|
||||
.sys-drawer:hover, .sys-drawer:focus-within { transform: translateX(0); }
|
||||
.sys-drawer-rail {
|
||||
flex: 0 0 var(--sys-drawer-peek); width: var(--sys-drawer-peek);
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
gap: 12px; padding: 14px 4px;
|
||||
background: linear-gradient(180deg, var(--bg-elevated) 0%, var(--bg-card) 100%);
|
||||
border: 1px solid var(--border); border-right: none;
|
||||
border-radius: var(--radius) 0 0 var(--radius);
|
||||
cursor: default;
|
||||
}
|
||||
.sys-drawer-rail-ico { font-size: 16px; line-height: 1; }
|
||||
.sys-drawer-rail-text {
|
||||
writing-mode: vertical-rl; text-orientation: mixed;
|
||||
font-family: var(--font-display);
|
||||
font-size: 11px; font-weight: 600; letter-spacing: .18em;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.helper-note.ok { border-color: rgba(34,197,94,.25); color: #b8f5cb; }
|
||||
.helper-note.warn { border-color: rgba(245,158,11,.25); color: #fbd38d; }
|
||||
.helper-note.err { border-color: rgba(239,68,68,.25); color: #fecaca; }
|
||||
.helper-note code { color: #d7fbff; font-family: var(--font-mono); }
|
||||
.sys-drawer:hover .sys-drawer-rail-text,
|
||||
.sys-drawer:focus-within .sys-drawer-rail-text { color: var(--mem-hi); }
|
||||
.sys-drawer-panel {
|
||||
flex: 1 1 var(--sys-drawer-panel-w);
|
||||
width: var(--sys-drawer-panel-w); min-width: 0; height: 100%;
|
||||
margin: 0 !important;
|
||||
display: flex; flex-direction: column;
|
||||
border-radius: 0; overflow: hidden;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border); border-left: none; border-right: none;
|
||||
}
|
||||
.sys-drawer-head { cursor: default; flex-shrink: 0; }
|
||||
.sys-drawer-head .card-head-btn { margin-left: auto; }
|
||||
.sys-drawer-body { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
|
||||
|
||||
+8
-8
@@ -9,11 +9,11 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ESP32 OTA 中继控制台</title>
|
||||
<link rel="stylesheet" href="css/base.css?v=20260718b">
|
||||
<link rel="stylesheet" href="css/layout.css?v=20260718b">
|
||||
<link rel="stylesheet" href="css/components.css?v=20260719x">
|
||||
<link rel="stylesheet" href="css/can.css?v=20260719s">
|
||||
<link rel="stylesheet" href="css/responsive.css?v=20260718b">
|
||||
<link rel="stylesheet" href="css/base.css?v=7d5f4eef">
|
||||
<link rel="stylesheet" href="css/layout.css?v=a3e0b7e2">
|
||||
<link rel="stylesheet" href="css/components.css?v=475fa1ac">
|
||||
<link rel="stylesheet" href="css/can.css?v=8a9342b7">
|
||||
<link rel="stylesheet" href="css/responsive.css?v=61b66182">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
@@ -469,8 +469,8 @@
|
||||
<span id="toastMsg">操作成功</span>
|
||||
</div>
|
||||
|
||||
<script type="module" src="js/app.js?v=20260720ae"></script>
|
||||
<script type="module" src="js/ota/console-mode.js?v=20260720ae"></script>
|
||||
<script type="module" src="js/ble_ota.js?v=20260720ae"></script>
|
||||
<script type="module" src="js/app.js?v=953c41e9"></script>
|
||||
<script type="module" src="js/ota/console-mode.js?v=06b7189e"></script>
|
||||
<script type="module" src="js/ble_ota.js?v=01d3cb4e"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -741,7 +741,7 @@ function buildHeapLowRowHtml(r, idx, segTag) {
|
||||
'</div>' +
|
||||
'<span>分配类别: <b>' + escapeHtml(r.hook_task || '--') + '</b> <span class="heap-low-caller-hint">net?=小中块/疑网络 · bulk=大块 · tiny=更小</span></span>' +
|
||||
'<span>本次alloc: <b>' + (Number.isFinite(r.hook_alloc_size) && Number(r.hook_alloc_size) > 0 ? formatBytes(r.hook_alloc_size) : '--') + '</b></span>' +
|
||||
'<span>alloc后est空闲: <b>' + (Number.isFinite(r.hook_free_after) && Number(r.hook_free_after) > 0 ? formatBytes(r.hook_free_after) : '--') + '</b></span>' +
|
||||
'<span>alloc后est空闲: <b>' + (Number.isFinite(r.hook_est_free_after) && Number(r.hook_est_free_after) > 0 ? formatBytes(r.hook_est_free_after) : (Number.isFinite(r.hook_free_after) && Number(r.hook_free_after) > 0 ? formatBytes(r.hook_free_after) : '--')) + '</b>' + (r.hook_confidence === 'est_low_watermark' ? ' <span class="heap-low-caller-hint">(estimated 低水位近似)</span>' : '') + '</span>' +
|
||||
'<span>调用者地址: <b class="heap-low-caller">0x' + (Number.isFinite(r.hook_caller_addr) && Number(r.hook_caller_addr) > 0 ? r.hook_caller_addr.toString(16).toUpperCase() : '--') + '</b>' +
|
||||
' <span class="heap-low-caller-hint">(addr2line)</span></span>' +
|
||||
'</div>' +
|
||||
|
||||
@@ -1,226 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Print ESP32-C3 static memory breakdown from build map + ELF (nm).
|
||||
|
||||
Usage (from repo or any cwd):
|
||||
python tools/parse_map_static_mem.py
|
||||
python tools/parse_map_static_mem.py --elf build/ESP32_COMM.elf
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_MAP = ROOT / "build" / "ESP32_COMM.map"
|
||||
DEFAULT_ELF = ROOT / "build" / "ESP32_COMM.elf"
|
||||
|
||||
# ESP32-C3 DRAM / IRAM windows used by app (after 16KB i-cache)
|
||||
DRAM_LO, DRAM_HI = 0x3FC80000, 0x3FCE0000
|
||||
IRAM_LO, IRAM_HI = 0x40380000, 0x403E0000
|
||||
|
||||
|
||||
def demangle(name: str) -> str:
|
||||
"""Lightweight Itanium demangle for common ESP app symbols."""
|
||||
# _ZL13s_status_json -> s_status_json
|
||||
m = re.match(r"^_ZL\d+(.+)$", name)
|
||||
if m:
|
||||
return m.group(1)
|
||||
# _ZZ20can_feedback_handler...E15s_feedback_json
|
||||
m = re.search(r"E\d+([A-Za-z_][A-Za-z0-9_]*)$", name)
|
||||
if m and name.startswith("_ZZ"):
|
||||
return m.group(1) + " (fn-static)"
|
||||
# _ZN12_GLOBAL__N_1L6s_workE
|
||||
m = re.match(r"^_ZN\d+_GLOBAL__N_\d+L\d+(.+)E$", name)
|
||||
if m:
|
||||
return m.group(1) + " (anon)"
|
||||
# _ZZN12Esp32OtaCore23computeShaFromPartitionEvE14s_sha_read_buf
|
||||
m = re.search(r"E\d+([A-Za-z_][A-Za-z0-9_]*)$", name)
|
||||
if m and name.startswith("_ZZN"):
|
||||
return m.group(1) + " (method-static)"
|
||||
# singleton: _ZZN8DebugLog8instanceEvE4inst
|
||||
m = re.match(r"^_ZZN(\d+)(.+)$", name)
|
||||
if m and "instanceEvE" in name:
|
||||
# extract class-ish
|
||||
body = name
|
||||
cm = re.search(r"_ZZN\d+([A-Za-z0-9_]+)\d+instance", body)
|
||||
if cm:
|
||||
return cm.group(1) + "::instance"
|
||||
return "singleton inst"
|
||||
return name
|
||||
|
||||
|
||||
def find_nm() -> str | None:
|
||||
which = shutil.which("riscv32-esp-elf-nm")
|
||||
if which:
|
||||
return which
|
||||
roots = [
|
||||
Path(r"C:\Espressif\tools\riscv32-esp-elf"),
|
||||
Path(r"F:\software\.espressif\tools\riscv32-esp-elf"),
|
||||
]
|
||||
for root in roots:
|
||||
if not root.exists():
|
||||
continue
|
||||
matches = sorted(root.glob("**/riscv32-esp-elf-nm.exe"))
|
||||
if matches:
|
||||
return str(matches[-1])
|
||||
return None
|
||||
|
||||
|
||||
def parse_syms(map_text: str) -> dict[str, int]:
|
||||
out = {}
|
||||
for name in (
|
||||
"_iram_start",
|
||||
"_iram_end",
|
||||
"_data_start",
|
||||
"_data_end",
|
||||
"_bss_start",
|
||||
"_bss_end",
|
||||
"_heap_start",
|
||||
):
|
||||
m = re.search(rf"0x([0-9a-fA-F]+)\s+{name}\s*=", map_text)
|
||||
if m:
|
||||
out[name] = int(m.group(1), 16)
|
||||
return out
|
||||
|
||||
|
||||
def run_nm(nm: str, elf: Path) -> list[tuple[int, int, str, str]]:
|
||||
"""Return list of (addr, size, type, name)."""
|
||||
# -S size, --size-sort, -t x hex
|
||||
r = subprocess.run(
|
||||
[nm, "-S", "--size-sort", "-t", "x", str(elf)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
check=False,
|
||||
)
|
||||
rows = []
|
||||
for line in r.stdout.splitlines():
|
||||
parts = line.split()
|
||||
if len(parts) < 4:
|
||||
continue
|
||||
try:
|
||||
addr = int(parts[0], 16)
|
||||
size = int(parts[1], 16)
|
||||
except ValueError:
|
||||
continue
|
||||
typ = parts[2]
|
||||
name = parts[3]
|
||||
rows.append((addr, size, typ, name))
|
||||
return rows
|
||||
|
||||
|
||||
def top_in_range(
|
||||
rows: list[tuple[int, int, str, str]],
|
||||
lo: int,
|
||||
hi: int,
|
||||
types: set[str],
|
||||
n: int = 20,
|
||||
min_size: int = 64,
|
||||
) -> list[tuple[int, str, str]]:
|
||||
picked = []
|
||||
for addr, size, typ, name in rows:
|
||||
if typ not in types:
|
||||
continue
|
||||
if size < min_size:
|
||||
continue
|
||||
if not (lo <= addr < hi):
|
||||
continue
|
||||
# skip linker bookkeeping extremes
|
||||
if name.startswith("_") and name.endswith(("_start", "_end", "_length")):
|
||||
if size > 0x10000:
|
||||
continue
|
||||
picked.append((size, typ, name))
|
||||
picked.sort(key=lambda x: -x[0])
|
||||
return picked[:n]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("--map", type=Path, default=DEFAULT_MAP)
|
||||
ap.add_argument("--elf", type=Path, default=DEFAULT_ELF)
|
||||
ap.add_argument("-n", type=int, default=18, help="top N symbols per section")
|
||||
args = ap.parse_args()
|
||||
|
||||
if not args.map.exists():
|
||||
print(f"map not found: {args.map}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
text = args.map.read_text(encoding="utf-8", errors="replace")
|
||||
syms = parse_syms(text)
|
||||
|
||||
def span(a: str, b: str) -> int:
|
||||
if a not in syms or b not in syms:
|
||||
return 0
|
||||
return max(0, syms[b] - syms[a])
|
||||
|
||||
iram = span("_iram_start", "_iram_end")
|
||||
data = span("_data_start", "_data_end")
|
||||
bss = span("_bss_start", "_bss_end")
|
||||
heap_start = syms.get("_heap_start", 0)
|
||||
static_to_heap = max(0, heap_start - DRAM_LO) if heap_start else iram + data + bss
|
||||
|
||||
print("=== Linker section totals (authoritative) ===")
|
||||
print(f" IRAM : {iram:7d} ({iram/1024:6.1f} KB) code in RAM (WiFi/BT/ISR/tlsf…)")
|
||||
print(f" .data : {data:7d} ({data/1024:6.1f} KB) initialized globals/singletons")
|
||||
print(f" .bss : {bss:7d} ({bss/1024:6.1f} KB) zeroed static buffers")
|
||||
print(f" static→heap : {static_to_heap:7d} ({static_to_heap/1024:6.1f} KB)")
|
||||
if heap_start:
|
||||
print(f" _heap_start : 0x{heap_start:08x}")
|
||||
room = max(0, DRAM_HI - heap_start)
|
||||
print(f" DRAM window after heap_start (to SOC_DRAM_HIGH): {room} ({room/1024:.1f} KB)")
|
||||
print(f" sum check IRAM+data+bss = {iram+data+bss} (should ≈ static→heap on C3)")
|
||||
|
||||
nm = find_nm()
|
||||
if not args.elf.exists():
|
||||
print(f"\nELF not found: {args.elf} (skip symbol tops)")
|
||||
return 0
|
||||
if not nm:
|
||||
print("\nriscv32-esp-elf-nm not found (skip symbol tops)")
|
||||
return 0
|
||||
|
||||
print(f"\nusing nm: {nm}")
|
||||
rows = run_nm(nm, args.elf)
|
||||
|
||||
sections = [
|
||||
("IRAM code (largest functions)", IRAM_LO, IRAM_HI, set("tT")),
|
||||
(".data (largest objects)", DRAM_LO, DRAM_HI, set("dD")),
|
||||
(".bss (largest objects)", DRAM_LO, DRAM_HI, set("bB")),
|
||||
]
|
||||
for title, lo, hi, types in sections:
|
||||
print(f"\n=== Top {args.n} {title} ===")
|
||||
tops = top_in_range(rows, lo, hi, types, n=args.n)
|
||||
# for .bss prefer heap_start range if known
|
||||
if "b" in {t.lower() for t in types} and heap_start:
|
||||
bss_lo = syms.get("_bss_start", DRAM_LO)
|
||||
bss_hi = heap_start
|
||||
tops = top_in_range(rows, bss_lo, bss_hi, types, n=args.n)
|
||||
if "d" in {t.lower() for t in types} and "_data_start" in syms:
|
||||
tops = top_in_range(
|
||||
rows,
|
||||
syms["_data_start"],
|
||||
syms.get("_data_end", DRAM_HI),
|
||||
types,
|
||||
n=args.n,
|
||||
)
|
||||
total = sum(s for s, _, _ in tops)
|
||||
for size, typ, name in tops:
|
||||
print(
|
||||
f" {size:6d} {size/1024:5.1f}KB {typ} "
|
||||
f"{demangle(name):40s} {name}"
|
||||
)
|
||||
print(f" (listed sum {total} / {total/1024:.1f} KB)")
|
||||
|
||||
print(
|
||||
"\nTip: runtime /system/status memory.{iram,data,bss} matches linker totals; "
|
||||
"symbol tops are build-time only."
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user