feat(spi_link): 添加SPI Link v2 PC自测框架并优化协议稳定性
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:
小龙虾
2026-07-21 01:16:43 +08:00
parent 00b716c8db
commit 770ae14156
29 changed files with 1712 additions and 2428 deletions
-81
View File
@@ -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]}")
-21
View File
@@ -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
-37
View File
@@ -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}")
-207
View File
@@ -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())
-130
View File
@@ -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 Archive File Total Size DRAM .bss .data .text Flash Code .text Flash Data .tdata .rodata .appdesc RTC SLOW .rtc_reserved .force_slow
2 libnet80211.a 154301 12686 7678 936 4072 128198 128198 13417 0 13417 0 0 0 0
3 libesp_stdio.a 114154 16 16 0 0 402 402 113736 0 113736 0 0 0 0
4 libbtdm_app.a 98094 20783 684 481 19618 72152 72152 5159 0 5159 0 0 0 0
5 liblwip.a 88946 2602 2590 12 0 82916 82916 3428 0 3428 0
6 0 0 0
7 libmain.a 85209 26097 22673 3264 160 57804 57804 1308 0 130 8 0 0 0 0
8 libpp.a 68522 19794 1262 2644 15888 44814 44814 3914 0 3914 0 0 0 0
9 libmbed-builtin.a 57570 60 12 48 0 45710 45710 11800 0 1180 0 0 0 0 0
10 libbt.a 56078 1060 139 233 688 51916 51916 3102 0 3102 0
11 0 0 0
12 libwpa_supplicant.a 37603 1138 1138 0 0 36390 36390 75 0 75 0 0 0 0
13 libphy.a 36923 5801 87 1312 4402 31122 31122 0 0 0 0 0
14 0 0
15 libtfpsacrypto.a 24703 562 400 80 82 23844 23844 297 0 297
16 0 0 0 0
17 libfreertos.a 20942 17688 2560 16 15112 1690 1690 1564 0 15 64 0 0 0 0
18 libesp_hw_support.a 20186 6251 145 160 5946 13170 13170 705 0 705 0 60 24 36
19 libspi_flash.a 17438 15081 10 5867 9204 1322 1322 1035 0 10 35 0 0 0 0
20 libc.a 14702 4 0 4 0 13610 13610 1088 0 1088 0 0 0 0
21 libnvs_flash.a 14264 24 24 0 0 14108 14108 132 0 132 0 0 0 0
22 libtransport.a 14237 1271 50 1145 76 11942 11942 1024 0 102 4 0 0 0 0
23 libhttp_parser.a 12694 0 0 0 0 11980 11980 714 0 714 0 0 0 0
24 libesp_driver_uart.a 11468 225 25 200 0 10782 10782 461 0 4 61 0 0 0 0
25 libesp_system.a 11009 1595 142 49 1404 8866 8866 548 0 548
26 0 0 0 0
27 libesp_http_server.a 10867 0 0 0 0 10528 10528 339 0 339 0 0 0 0
28 libheap.a 10856 6894 8 4 6882 3240 3240 722 0 722 0 0
29 0 0
30 libesp_driver_spi.a 10004 3507 8 131 3368 6068 6068 429 0 4 29 0 0 0 0
31 libesp_netif.a 7572 37 29 8 0 7304 7304 231 0 231 0 0
32 0 0
33 libcoexist.a 6051 461 6 301 154 4316 4316 1274 0 1274 0
34 0 0 0
35 libesp_driver_gpio.a 6008 216 0 36 180 5238 5238 554 0 554
36 0 0 0 0
37 libesp_libc.a 5358 3584 564 324 2696 1622 1622 152 16 136
38 0 0 0 0
39 libesp_wifi.a 5144 703 43 480 180 3618 3618 823 0 823 0
40 0 0 0
41 libcjson.a 5116 20 8 12 0 5096 5096 0 0 0 0 0 0 0
42 libvfs.a 5051 236 44 192 0 4676 4676 139 0 139 0 0 0
43 0
44 libesp_ringbuf.a 4860 2894 0 0 2894 1492 1492 474 0 474 0 0 0 0
45 libesp_hal_mspi.a 4366 3792 0 36 3756 574 574 0 0 0 0 0 0 0
46 libbootloader_support.a 4284 686 8 0 678 3510 3510 88 0 88
47 0 0 0 0
48 libesp_mm.a 4183 1325 48 103 1174 2644 2644 214 0 214 0
49 0 0 0
50 libesp_event.a 3691 4 4 0 0 3546 3546 141 0 141 0 0 0 0
51 libesp_phy.a 3455 248 51 9 188 2908 2908 299 0 299 0 0
52 0 0
53 libesp_timer.a 3035 1364 36 16 1312 1580 1580 91 0 91 0
54 0 0 0
55 libapp_update.a 2996 12 12 0 0 2922 2922 62 0 62 0 0 0 0
56 libbtbb.a 2868 0 0 0 0 2868 2868 0 0 0 0 0 0 0
57 libesp_partition.a 2796 8 8 0 0 2620 2620 168 0 168 0 0 0 0
58 libesp_driver_dma.a 2774 140 0 12 128 2532 2532 102 0 102
59 0 0 0 0
60 libhal.a 2564 2212 5 235 1972 352 352 0 0 0 0 0 0 0
61 libesp_hal_gpspi.a 2550 2194 0 20 2174 284 284 72 0 72 0
62 0 0 0
63 libefuse.a 2461 320 4 316 0 1654 1654 487 0 487 0 0 0 0
64 libesp_driver_usb_serial_jtag.a 2172 157 17 48 92 1874 1874 141 0 141 0 0 0 0
65 libstdc++.a 2056 33 25 8 0 1664 1664 359 0 359 0 0 0
66 0
67 libesp_driver_tsens.a 1942 8 4 4 0 1714 1714 220 0 220 0
68 0 0 0
69 liblog.a 1894 700 276 8 416 1146 1146 48 0 48 0 0 0
70 0
71 libesp_common.a 1774 0 0 0 0 48 48 1726 0 1726 0 0 0
72 0
73 libesp_hal_ana_conv.a 1559 9 0 9 0 1450 1450 100 0 100 0
74 0 0 0
75 libriscv.a 1294 1112 256 0 856 132 132 50 0 50 0 0 0
76 0
77 libesp_hal_security.a 1260 0 0 0 0 1260 1260 0 0 0 0 0
78 0 0
79 libpthread.a 1223 4 4 0 0 1130 1130 89 0 89 0 0 0 0
80 libesp_hal_dma.a 1022 366 0 4 362 656 656 0 0 0 0 0 0 0
81 libesp_hal_wdt.a 1005 1005 0 21 984 0 0 0 0 0 0 0 0
82 0
83 libesp_security.a 922 12 12 0 0 902 902 8 0 8 0 0 0
84 0
85 libesp_app_format.a 770 10 10 0 0 496 496 264 0 8 256 0 0 0
86 libcxx.a 740 16 16 0 0 724 724 0 0 0 0 0 0 0
87 libesp_hal_uart.a 674 0 0 0 0 618 618 56 0 56 0 0 0
88 0
89 libesp_hal_gpio.a 468 0 0 0 0 440 440 28 0 28 0 0 0
90 0
91 libesp_coex.a 428 154 0 80 74 274 274 0 0 0 0 0 0 0
92 libesp_hal_clock.a 404 0 0 0 0 404 404 0 0 0 0 0 0 0
93 libcore.a 366 9 9 0 0 314 314 43 0 43 0 0 0 0
94 libsoc.a 364 0 0 0 0 0 0 364 0 364 0 0 0 0
95 libmesh.a 64 4 4 0 0 60 60 0 0 0 0 0 0 0
96 libmbedtls.a 32 0 0 0 0 24 24 8 0 8 0 0 0 0
97 libespnow.a 3 3 0 3 0 0 0 0 0 0 0 0 0 0
98 libnvs_sec_provider.a 2 0 0 0 0 2 2 0 0 0 0 0 0 0
View File
View File
-217
View File
@@ -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())
-294
View File
@@ -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())
+167
View File
@@ -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
View File
@@ -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>
+6 -6
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+256 -155
View File
@@ -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
View File
@@ -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>
+1 -1
View File
@@ -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>' +
-226
View File
@@ -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())