ht32-code-mcp 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ht32_code_mcp-0.1.0/.gitignore +8 -0
- ht32_code_mcp-0.1.0/.gitkeep +0 -0
- ht32_code_mcp-0.1.0/PKG-INFO +15 -0
- ht32_code_mcp-0.1.0/README.md +5 -0
- ht32_code_mcp-0.1.0/e2e_scenario_a.py +142 -0
- ht32_code_mcp-0.1.0/e2e_scenario_b.py +76 -0
- ht32_code_mcp-0.1.0/pyproject.toml +35 -0
- ht32_code_mcp-0.1.0/smoke_stdio.py +58 -0
- ht32_code_mcp-0.1.0/src/ht32_code_mcp/__init__.py +3 -0
- ht32_code_mcp-0.1.0/src/ht32_code_mcp/__main__.py +4 -0
- ht32_code_mcp-0.1.0/src/ht32_code_mcp/exporter.py +166 -0
- ht32_code_mcp-0.1.0/src/ht32_code_mcp/index.py +246 -0
- ht32_code_mcp-0.1.0/src/ht32_code_mcp/knowledge.py +100 -0
- ht32_code_mcp-0.1.0/src/ht32_code_mcp/server.py +327 -0
- ht32_code_mcp-0.1.0/tests/conftest.py +10 -0
- ht32_code_mcp-0.1.0/tests/test_tools.py +237 -0
- ht32_code_mcp-0.1.0/uv.lock +1626 -0
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ht32-code-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: HT32 Code MCP Server - 为 AI Agent 提供 HT32 函数库/例程/模板检索与工程导出
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Keywords: firmware,holtek,ht32,mcp,mcu
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Topic :: Software Development :: Embedded Systems
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
Requires-Dist: fastmcp>=0.5.0
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""P0-4 场景 a 端到端驱动:双 MCP 本地 stdio 真实子进程跑通新建工程链路。
|
|
2
|
+
|
|
3
|
+
流程(对应 ht32-skills/ht32-project-create):
|
|
4
|
+
wiki-mcp: get_model + get_chip_info(HT32F52352 规格核对)
|
|
5
|
+
code-mcp: tools/list(7 工具)→ get_project_manifest → export_project_bundle → 落盘核对
|
|
6
|
+
|
|
7
|
+
用法:
|
|
8
|
+
<code-venv-python> e2e_scenario_a.py [--chip HT32F52352]
|
|
9
|
+
输出:
|
|
10
|
+
%TEMP%/ht32_p0_4_e2e/wiki_chip_info.txt | manifest.txt | export.txt
|
|
11
|
+
%TEMP%/ht32_p0_4_e2e/stage/<chip>_demo/ 导出产物(编译对象)
|
|
12
|
+
退出码 0 全通过;1 失败。
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import asyncio
|
|
18
|
+
import os
|
|
19
|
+
import shutil
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from fastmcp import Client
|
|
24
|
+
from fastmcp.client.transports import StdioTransport
|
|
25
|
+
|
|
26
|
+
CODE_MCP_DIR = Path(__file__).resolve().parent
|
|
27
|
+
WIKI_MCP_DIR = Path(r"d:\HoltekWork\WorkDesktop\ht32_ai_mcp\ht32-wiki\.work\mcp-server")
|
|
28
|
+
WIKI_ROOT = WIKI_MCP_DIR.parents[1]
|
|
29
|
+
|
|
30
|
+
CODE_PY = CODE_MCP_DIR / ".venv" / "Scripts" / "python.exe"
|
|
31
|
+
WIKI_PY = WIKI_MCP_DIR / ".venv" / "Scripts" / "python.exe"
|
|
32
|
+
|
|
33
|
+
EXPECTED_TOOLS = {
|
|
34
|
+
"get_code_version", "list_libraries", "find_example", "get_file",
|
|
35
|
+
"search_code_knowledge", "get_project_manifest", "export_project_bundle",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
WORK = Path(os.environ.get("TEMP", ".")) / "ht32_p0_4_e2e"
|
|
39
|
+
|
|
40
|
+
failed = 0
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def check(name: str, cond: bool, detail: str = "") -> bool:
|
|
44
|
+
global failed
|
|
45
|
+
suffix = f" — {detail}" if detail else ""
|
|
46
|
+
print(f"{'✅' if cond else '❌'} [{name}]{suffix}")
|
|
47
|
+
if not cond:
|
|
48
|
+
failed += 1
|
|
49
|
+
return cond
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def call_text(client: Client, tool: str, args: dict) -> str:
|
|
53
|
+
r = await client.call_tool(tool, args)
|
|
54
|
+
parts = [c.text for c in r.content if getattr(c, "text", None)]
|
|
55
|
+
return "\n".join(parts)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def wiki_transport() -> StdioTransport:
|
|
59
|
+
env = dict(os.environ)
|
|
60
|
+
env.update({"WIKI_ROOT": str(WIKI_ROOT), "MCP_TRANSPORT": "stdio",
|
|
61
|
+
"MCP_WARMUP": "1", "DS_RAG_AUTOBUILD": "0"})
|
|
62
|
+
for key in ("EMBED_API_KEY", "SILICONFLOW_API_KEY", "RERANK_API_KEY"):
|
|
63
|
+
env.pop(key, None)
|
|
64
|
+
return StdioTransport(str(WIKI_PY), ["-m", "ht32_wiki_mcp"],
|
|
65
|
+
env=env, cwd=str(WIKI_MCP_DIR))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def code_transport() -> StdioTransport:
|
|
69
|
+
env = dict(os.environ)
|
|
70
|
+
env["CODE_ROOT"] = str(CODE_MCP_DIR.parents[1])
|
|
71
|
+
return StdioTransport(str(CODE_PY), ["-m", "ht32_code_mcp"],
|
|
72
|
+
env=env, cwd=str(CODE_MCP_DIR))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def phase_wiki(chip: str) -> str:
|
|
76
|
+
print("\n========== 阶段 1 · wiki-mcp 规格核对 ==========")
|
|
77
|
+
async with Client(wiki_transport()) as client:
|
|
78
|
+
model_text = await call_text(client, "get_model", {"model": chip})
|
|
79
|
+
check("get_model 返回系列映射", "系列:" in model_text,
|
|
80
|
+
model_text.splitlines()[0] if model_text else "(空)")
|
|
81
|
+
chip_text = await call_text(client, "get_chip_info", {"model": chip})
|
|
82
|
+
check("get_chip_info 命中系列页", "未找到" not in chip_text and len(chip_text) > 200,
|
|
83
|
+
f"{len(chip_text)} 字符")
|
|
84
|
+
(WORK / "wiki_chip_info.txt").write_text(
|
|
85
|
+
f"== get_model({chip}) ==\n{model_text}\n\n== get_chip_info({chip}) ==\n{chip_text}",
|
|
86
|
+
encoding="utf-8")
|
|
87
|
+
print(f"规格核对全文已存 {WORK / 'wiki_chip_info.txt'}")
|
|
88
|
+
return chip_text
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
async def phase_code(chip: str) -> Path:
|
|
92
|
+
print("\n========== 阶段 2 · code-mcp manifest + export ==========")
|
|
93
|
+
stage = WORK / "stage" / f"{chip.lower()}_demo"
|
|
94
|
+
if stage.exists():
|
|
95
|
+
shutil.rmtree(stage)
|
|
96
|
+
stage.parent.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
|
|
98
|
+
async with Client(code_transport()) as client:
|
|
99
|
+
tools = {t.name for t in await client.list_tools()}
|
|
100
|
+
check("7 工具注册齐全", EXPECTED_TOOLS <= tools,
|
|
101
|
+
f"{len(tools)} 个" if EXPECTED_TOOLS <= tools else f"缺 {EXPECTED_TOOLS - tools}")
|
|
102
|
+
|
|
103
|
+
manifest = await call_text(client, "get_project_manifest", {})
|
|
104
|
+
check("manifest 返回模板清单", "工程模板清单" in manifest,
|
|
105
|
+
manifest.splitlines()[0] if manifest else "(空)")
|
|
106
|
+
(WORK / "manifest.txt").write_text(manifest, encoding="utf-8")
|
|
107
|
+
|
|
108
|
+
export = await call_text(client, "export_project_bundle",
|
|
109
|
+
{"chip": chip, "out_dir": str(stage)})
|
|
110
|
+
check("export_project_bundle 成功", "导出失败" not in export and stage.is_dir(),
|
|
111
|
+
export.splitlines()[0] if export else "(空)")
|
|
112
|
+
(WORK / "export.txt").write_text(export, encoding="utf-8")
|
|
113
|
+
|
|
114
|
+
uvprojx = list(stage.rglob("*.uvprojx"))
|
|
115
|
+
check("落盘核对:.uvprojx 存在", len(uvprojx) == 1, str(uvprojx))
|
|
116
|
+
files = [p for p in stage.rglob("*") if p.is_file()]
|
|
117
|
+
check("落盘核对:工程文件数 > 20", len(files) > 20, f"{len(files)} 个文件")
|
|
118
|
+
print(f"导出产物:{stage}")
|
|
119
|
+
return stage
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
async def main() -> int:
|
|
123
|
+
parser = argparse.ArgumentParser()
|
|
124
|
+
parser.add_argument("--chip", default="HT32F52352")
|
|
125
|
+
args = parser.parse_args()
|
|
126
|
+
WORK.mkdir(parents=True, exist_ok=True)
|
|
127
|
+
if not (WIKI_PY.is_file() and CODE_PY.is_file()):
|
|
128
|
+
print("❌ 找不到 venv 解释器", file=sys.stderr)
|
|
129
|
+
return 1
|
|
130
|
+
|
|
131
|
+
await phase_wiki(args.chip)
|
|
132
|
+
stage = await phase_code(args.chip)
|
|
133
|
+
|
|
134
|
+
print("\n========== 摘要 ==========")
|
|
135
|
+
print(f"工作目录: {WORK}")
|
|
136
|
+
print(f"导出工程: {stage}")
|
|
137
|
+
print(f"结果: {'✅ 全部通过' if failed == 0 else f'❌ {failed} 项失败'}")
|
|
138
|
+
return 0 if failed == 0 else 1
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
if __name__ == "__main__":
|
|
142
|
+
raise SystemExit(asyncio.run(main()))
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""P0-4 场景 b 冒烟:检索辅助写码三工具各一次,确认返回真实库内容。
|
|
2
|
+
|
|
3
|
+
search_code_knowledge("UART 用法") → 知识页命中
|
|
4
|
+
find_example(peripheral="UART", chip="HT32F52352") → 官方例程路径
|
|
5
|
+
get_file(例程 main.c) → 官方库真实源码
|
|
6
|
+
|
|
7
|
+
用法:
|
|
8
|
+
<code-venv-python> e2e_scenario_b.py
|
|
9
|
+
退出码 0 全通过;1 失败。
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from fastmcp import Client
|
|
20
|
+
from fastmcp.client.transports import StdioTransport
|
|
21
|
+
|
|
22
|
+
CODE_MCP_DIR = Path(__file__).resolve().parent
|
|
23
|
+
CODE_PY = CODE_MCP_DIR / ".venv" / "Scripts" / "python.exe"
|
|
24
|
+
|
|
25
|
+
failed = 0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def check(name: str, cond: bool, detail: str = "") -> bool:
|
|
29
|
+
global failed
|
|
30
|
+
suffix = f" — {detail}" if detail else ""
|
|
31
|
+
print(f"{'✅' if cond else '❌'} [{name}]{suffix}")
|
|
32
|
+
if not cond:
|
|
33
|
+
failed += 1
|
|
34
|
+
return cond
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def call_text(client: Client, tool: str, args: dict) -> str:
|
|
38
|
+
r = await client.call_tool(tool, args)
|
|
39
|
+
return "\n".join(c.text for c in r.content if getattr(c, "text", None))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def main() -> int:
|
|
43
|
+
env = dict(os.environ)
|
|
44
|
+
env["CODE_ROOT"] = str(CODE_MCP_DIR.parents[1])
|
|
45
|
+
transport = StdioTransport(str(CODE_PY), ["-m", "ht32_code_mcp"],
|
|
46
|
+
env=env, cwd=str(CODE_MCP_DIR))
|
|
47
|
+
|
|
48
|
+
async with Client(transport) as client:
|
|
49
|
+
print("== 场景 b 冒烟(检索辅助写码)==")
|
|
50
|
+
|
|
51
|
+
s = await call_text(client, "search_code_knowledge", {"query": "UART 用法 中断 发送", "limit": 3})
|
|
52
|
+
check("search_code_knowledge 命中知识页", "找到" in s and "entities/" in s,
|
|
53
|
+
s.splitlines()[0] if s else "(空)")
|
|
54
|
+
print("\n".join(s.splitlines()[:12]), "…\n")
|
|
55
|
+
|
|
56
|
+
# 官方例程目录名为 USART(无 UART 目录):find_example 词法匹配不含 uart→usart 别名
|
|
57
|
+
f = await call_text(client, "find_example",
|
|
58
|
+
{"peripheral": "USART", "chip": "HT32F52352", "limit": 3})
|
|
59
|
+
check("find_example 返回 USART 例程", "找到" in f and "example" in f,
|
|
60
|
+
f.splitlines()[0] if f else "(空)")
|
|
61
|
+
m = re.search(r"路径: (raw/\S+)", f)
|
|
62
|
+
check("例程含 main.c 路径", bool(m), m.group(1) if m else "(未匹配)")
|
|
63
|
+
print("\n".join(f.splitlines()[:10]), "…\n")
|
|
64
|
+
|
|
65
|
+
if m:
|
|
66
|
+
g = await call_text(client, "get_file", {"path": f"{m.group(1)}/main.c", "max_length": 4000})
|
|
67
|
+
has_api = ("HT_" in g) and ("UART" in g or "USART" in g)
|
|
68
|
+
check("get_file 返回官方库真实源码", has_api, f"{len(g)} 字符")
|
|
69
|
+
print("\n".join(g.splitlines()[:10]), "…")
|
|
70
|
+
|
|
71
|
+
print(f"\n结果: {'✅ 场景 b 冒烟通过' if failed == 0 else f'❌ {failed} 项失败'}")
|
|
72
|
+
return 0 if failed == 0 else 1
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
raise SystemExit(asyncio.run(main()))
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ht32-code-mcp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "HT32 Code MCP Server - 为 AI Agent 提供 HT32 函数库/例程/模板检索与工程导出"
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
keywords = ["ht32", "mcu", "mcp", "firmware", "holtek"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.11",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Topic :: Software Development :: Embedded Systems",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"fastmcp>=0.5.0",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
dev = [
|
|
25
|
+
"pytest>=8.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
ht32-code-mcp = "ht32_code_mcp.server:main"
|
|
30
|
+
|
|
31
|
+
[tool.hatch.build.targets.wheel]
|
|
32
|
+
packages = ["src/ht32_code_mcp"]
|
|
33
|
+
|
|
34
|
+
[tool.pytest.ini_options]
|
|
35
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""stdio 冒烟测试:以子进程 stdio 启动 server,验证 7 工具全部可调用。
|
|
2
|
+
|
|
3
|
+
用法(从 .work/mcp-server/ 目录,需已安装依赖):
|
|
4
|
+
python smoke_stdio.py
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
14
|
+
SRC = SCRIPT_DIR / "src"
|
|
15
|
+
sys.path.insert(0, str(SRC))
|
|
16
|
+
|
|
17
|
+
from fastmcp import Client # noqa: E402
|
|
18
|
+
from fastmcp.client.transports import StdioTransport # noqa: E402
|
|
19
|
+
|
|
20
|
+
EXPECTED_TOOLS = {
|
|
21
|
+
"get_code_version", "list_libraries", "find_example", "get_file",
|
|
22
|
+
"search_code_knowledge", "get_project_manifest", "export_project_bundle",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def run() -> int:
|
|
27
|
+
env = dict(os.environ)
|
|
28
|
+
env["PYTHONPATH"] = str(SRC) + os.pathsep + env.get("PYTHONPATH", "")
|
|
29
|
+
transport = StdioTransport(sys.executable, ["-m", "ht32_code_mcp"], env=env)
|
|
30
|
+
async with Client(transport) as client:
|
|
31
|
+
tools = {t.name for t in await client.list_tools()}
|
|
32
|
+
missing = EXPECTED_TOOLS - tools
|
|
33
|
+
print(f"工具清单({len(tools)}):{sorted(tools)}")
|
|
34
|
+
if missing:
|
|
35
|
+
print(f"❌ 缺失工具:{missing}")
|
|
36
|
+
return 1
|
|
37
|
+
print(f"✅ 7 工具注册齐全")
|
|
38
|
+
|
|
39
|
+
r = await client.call_tool("get_code_version", {})
|
|
40
|
+
print("✅ get_code_version:", (r.content[0].text or "").splitlines()[0])
|
|
41
|
+
|
|
42
|
+
r = await client.call_tool("find_example", {"peripheral": "GPIO", "limit": 3})
|
|
43
|
+
text = r.content[0].text or ""
|
|
44
|
+
assert "example/GPIO" in text, text
|
|
45
|
+
print(f"✅ find_example(GPIO): {text.splitlines()[0]}")
|
|
46
|
+
|
|
47
|
+
r = await client.call_tool("get_file", {
|
|
48
|
+
"path": "raw/HT32_STD_5xxxx_FWLib_V1.21.1/library/HT32F5xxxx_Driver/inc/ht32f5xxxx_gpio.h",
|
|
49
|
+
"max_length": -1,
|
|
50
|
+
})
|
|
51
|
+
assert "GPIO_DirectionConfig" in (r.content[0].text or "")
|
|
52
|
+
print("✅ get_file(gpio.h) 内容正确")
|
|
53
|
+
print("✅ stdio 冒烟全部通过")
|
|
54
|
+
return 0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
raise SystemExit(asyncio.run(run()))
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""export_project_bundle:从模板 + 官方库源码组装自包含可编译 Keil 工程到本地目录。
|
|
2
|
+
|
|
3
|
+
本阶段(P0-2)不做 token 下载 URL,云端阶段再扩展。
|
|
4
|
+
参数化依据:raw/templates/ht32f5-keil/devices.json(device/cdef/adef/sys/startup)。
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
import shutil
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
_REDIRECT_RE = re.compile(r"#ifdef\s+(USE_\w+)\s*\n\s*#define\s+(USE_\w+)")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ExportError(Exception):
|
|
16
|
+
"""导出失败(型号未知 / 源文件缺失 / 目标目录非空等)。"""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _read_text(path: Path) -> str:
|
|
20
|
+
return path.read_text(encoding="utf-8", errors="replace")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ProjectExporter:
|
|
24
|
+
def __init__(self, code_root: Path) -> None:
|
|
25
|
+
self.root = Path(code_root)
|
|
26
|
+
self.template_dir = self.root / "raw" / "templates" / "ht32f5-keil"
|
|
27
|
+
self.fwlib_dir = self.root / "raw" / "HT32_STD_5xxxx_FWLib_V1.21.1"
|
|
28
|
+
|
|
29
|
+
# ------------------------------------------------------------ 主流程
|
|
30
|
+
def export(self, chip: str, out_dir: str) -> str:
|
|
31
|
+
if not self.template_dir.is_dir():
|
|
32
|
+
raise ExportError(f"模板目录不存在:{self.template_dir}")
|
|
33
|
+
device, entry = self._resolve_device(chip)
|
|
34
|
+
|
|
35
|
+
# 1. 预检目标目录
|
|
36
|
+
out = Path(out_dir).expanduser().resolve()
|
|
37
|
+
if out.exists() and any(out.iterdir()):
|
|
38
|
+
raise ExportError(f"目标目录非空,拒绝覆盖:{out}(请指定新目录或先清空)")
|
|
39
|
+
|
|
40
|
+
# 2. 预检官方源文件(先验证后落盘,失败不产生半成品)
|
|
41
|
+
sys_c = self.fwlib_dir / "library" / "Device" / "Holtek" / "HT32F5xxxx" / "Source" / entry["sys"]
|
|
42
|
+
sys_h = sys_c.parent.parent / "Include" / (sys_c.stem + ".h")
|
|
43
|
+
startup = (self.fwlib_dir / "library" / "Device" / "Holtek" / "HT32F5xxxx"
|
|
44
|
+
/ "Source" / "ARM" / entry["startup"])
|
|
45
|
+
libcfg_name = self._resolve_libcfg(entry["cdef"])
|
|
46
|
+
libcfg = (self.fwlib_dir / "library" / "HT32F5xxxx_Driver" / "inc" / libcfg_name
|
|
47
|
+
if libcfg_name else None)
|
|
48
|
+
missing = [str(p) for p in (sys_c, sys_h, startup, libcfg) if p is not None and not p.is_file()]
|
|
49
|
+
if missing:
|
|
50
|
+
raise ExportError("官方库缺少所需源文件:\n- " + "\n- ".join(missing))
|
|
51
|
+
|
|
52
|
+
# 3. 复制模板树
|
|
53
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
shutil.copytree(self.template_dir, out, dirs_exist_ok=True)
|
|
55
|
+
|
|
56
|
+
# 4. 参数化 uvprojx
|
|
57
|
+
uvprojx = out / "MDK-ARM" / "ht32f5-template.uvprojx"
|
|
58
|
+
old_sys_stem = "system_ht32f5xxxx_01"
|
|
59
|
+
old_startup_stem = "startup_ht32f5xxxx_01"
|
|
60
|
+
text = _read_text(uvprojx)
|
|
61
|
+
text = re.sub(r"(<Device>)[^<]*(</Device>)", rf"\g<1>{device}\g<2>", text, count=1)
|
|
62
|
+
text = _replace_define(text, "USE_HT32_DRIVER", entry["cdef"])
|
|
63
|
+
text = _replace_define(text, "USE_HT32_CHIP", entry["adef"])
|
|
64
|
+
text = text.replace(old_sys_stem, sys_c.stem).replace(old_startup_stem, startup.stem)
|
|
65
|
+
with open(uvprojx, "w", encoding="utf-8", newline="") as f:
|
|
66
|
+
f.write(text)
|
|
67
|
+
|
|
68
|
+
# 5. 替换设备相关文件
|
|
69
|
+
replaced: list[str] = []
|
|
70
|
+
for old in out.glob("CMSIS/system_ht32f5xxxx_*.c"):
|
|
71
|
+
old.unlink()
|
|
72
|
+
for old in out.glob("CMSIS/system_ht32f5xxxx_*.h"):
|
|
73
|
+
old.unlink()
|
|
74
|
+
shutil.copyfile(sys_c, out / "CMSIS" / sys_c.name)
|
|
75
|
+
shutil.copyfile(sys_h, out / "CMSIS" / sys_h.name)
|
|
76
|
+
replaced += [f"CMSIS/{sys_c.name}", f"CMSIS/{sys_h.name}"]
|
|
77
|
+
|
|
78
|
+
for old in out.glob("Startup/startup_*.s"):
|
|
79
|
+
old.unlink()
|
|
80
|
+
shutil.copyfile(startup, out / "Startup" / startup.name)
|
|
81
|
+
replaced.append(f"Startup/{startup.name}")
|
|
82
|
+
|
|
83
|
+
if libcfg is not None:
|
|
84
|
+
for old in out.glob("Drivers/*_libcfg.h"):
|
|
85
|
+
old.unlink()
|
|
86
|
+
shutil.copyfile(libcfg, out / "Drivers" / libcfg.name)
|
|
87
|
+
replaced.append(f"Drivers/{libcfg.name}")
|
|
88
|
+
|
|
89
|
+
return self._report(out, device, entry, replaced, libcfg_name)
|
|
90
|
+
|
|
91
|
+
# ------------------------------------------------------------ 辅助
|
|
92
|
+
def _resolve_device(self, chip: str) -> tuple[str, dict]:
|
|
93
|
+
import json
|
|
94
|
+
dj = self.template_dir / "devices.json"
|
|
95
|
+
if not dj.is_file():
|
|
96
|
+
raise ExportError(f"模板型号参数表缺失:{dj}")
|
|
97
|
+
meta = json.loads(dj.read_text(encoding="utf-8"))
|
|
98
|
+
want = chip.strip().upper()
|
|
99
|
+
for d in meta.get("devices", []):
|
|
100
|
+
if d.get("device", "").upper() == want:
|
|
101
|
+
return d["device"], d # 重名取首条(官方板级变体)
|
|
102
|
+
# 宽松匹配:型号主体包含(HT32F52352 含于 HT32F52352_SK 场景已由上面精确处理)
|
|
103
|
+
for d in meta.get("devices", []):
|
|
104
|
+
if want in d.get("device", "").upper():
|
|
105
|
+
return d["device"], d
|
|
106
|
+
names = [d.get("device", "") for d in meta.get("devices", [])][:20]
|
|
107
|
+
raise ExportError(
|
|
108
|
+
f"型号 '{chip}' 不在模板参数表(共 {meta.get('count', '?')} 个型号)。\n"
|
|
109
|
+
f"可用型号(前 20):{', '.join(names)}…\n"
|
|
110
|
+
"完整清单:get_file('raw/templates/ht32f5-keil/devices.json') 或 get_project_manifest()"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def _resolve_libcfg(self, cdef: str) -> str | None:
|
|
114
|
+
"""从 cdef 宏解析 libcfg 文件名,沿 ht32_config.h 重定向链(USE_HT50F32002 → USE_HT32F50220_30)。"""
|
|
115
|
+
inc_dir = self.fwlib_dir / "library" / "HT32F5xxxx_Driver" / "inc"
|
|
116
|
+
config = (self.template_dir / "CMSIS" / "ht32_config.h")
|
|
117
|
+
redirects: dict[str, str] = {}
|
|
118
|
+
if config.is_file():
|
|
119
|
+
redirects = {m.group(1): m.group(2)
|
|
120
|
+
for m in _REDIRECT_RE.finditer(_read_text(config))}
|
|
121
|
+
for token in [t.strip() for t in cdef.split(",")]:
|
|
122
|
+
if not token.startswith("USE_"):
|
|
123
|
+
continue
|
|
124
|
+
cur, seen = token, set()
|
|
125
|
+
while cur and cur not in seen:
|
|
126
|
+
seen.add(cur)
|
|
127
|
+
candidate = inc_dir / (cur[4:].lower() + "_libcfg.h")
|
|
128
|
+
if candidate.is_file():
|
|
129
|
+
return candidate.name
|
|
130
|
+
cur = redirects.get(cur, "")
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
def _report(self, out: Path, device: str, entry: dict,
|
|
134
|
+
replaced: list[str], libcfg_name: str | None) -> str:
|
|
135
|
+
n_files = sum(1 for p in out.rglob("*") if p.is_file())
|
|
136
|
+
libcfg_note = (f"Drivers/{libcfg_name}" if libcfg_name
|
|
137
|
+
else "(未解析出 libcfg,保留模板默认,请人工核对)")
|
|
138
|
+
lines = [
|
|
139
|
+
f"自包含 Keil 工程已导出:{out}",
|
|
140
|
+
f"目标芯片:{device}({entry.get('sys')} / {entry.get('startup')})",
|
|
141
|
+
f"C 宏(Cads):{entry['cdef']}",
|
|
142
|
+
f"汇编宏(Aads):{entry['adef']}",
|
|
143
|
+
f"libcfg:{libcfg_note}",
|
|
144
|
+
f"工程文件:MDK-ARM/ht32f5-template.uvprojx(已按上述宏参数化)",
|
|
145
|
+
f"文件总数:{n_files}",
|
|
146
|
+
"",
|
|
147
|
+
"按目标芯片替换/注入的文件:",
|
|
148
|
+
*[f"- {r}" for r in replaced],
|
|
149
|
+
"",
|
|
150
|
+
"目录结构:Application(main/it/conf)、CMSIS(设备头+system)、"
|
|
151
|
+
"Drivers(最小驱动集+libcfg)、Startup、MDK-ARM(工程文件)、README.md、devices.json",
|
|
152
|
+
"",
|
|
153
|
+
"后续步骤:",
|
|
154
|
+
"1. Keil MDK 打开 MDK-ARM/ht32f5-template.uvprojx(需 Holtek HT32_DFP pack ≥1.0.67)",
|
|
155
|
+
"2. 如需精确 IROM/IRAM 内存布局,在 Options → Device 重新选择该型号由 DFP 刷新",
|
|
156
|
+
"3. 基线编译验证(P0-4.2 将用 UV4 命令行复核 0 Error 0 Warning)",
|
|
157
|
+
]
|
|
158
|
+
return "\n".join(lines)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _replace_define(text: str, anchor: str, new_value: str) -> str:
|
|
162
|
+
"""替换 uvprojx 中包含 anchor 的 <Define> 行内容(Cads/Aads 各一条)。"""
|
|
163
|
+
pattern = re.compile(r"(<Define>)[^<]*" + re.escape(anchor) + r"[^<]*(</Define>)")
|
|
164
|
+
if not pattern.search(text):
|
|
165
|
+
raise ExportError(f"uvprojx 中未找到含 {anchor} 的 <Define>,模板结构可能已变化")
|
|
166
|
+
return pattern.sub(rf"\g<1>{new_value}\g<2>", text, count=1)
|