Skip to content

Commit 4824b0f

Browse files
committed
Add new remote access behavior
maybe useful for #59
1 parent ce37150 commit 4824b0f

File tree

2 files changed

+346
-24
lines changed

2 files changed

+346
-24
lines changed

cmdebug/svd_fetcher.py

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
"""
2+
This file is part of PyCortexMDebug
3+
4+
PyCortexMDebug is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
PyCortexMDebug is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License
15+
along with PyCortexMDebug. If not, see <http://www.gnu.org/licenses/>.
16+
17+
18+
19+
Fetch SVD files from the cmsis-svd GitHub repository without cloning it.
20+
"""
21+
22+
import requests
23+
from typing import List, Dict, Optional
24+
from pathlib import Path
25+
import json
26+
import os
27+
import dataclasses
28+
29+
_REPO_OWNER = "cmsis-svd"
30+
_REPO_NAME = "cmsis-svd-data"
31+
_BRANCH = "main"
32+
_BASE_API_URL = f"https://api.github.com/repos/{_REPO_OWNER}/{_REPO_NAME}"
33+
_DEFAULT_CACHE_PATH = Path.home() / ".cache" / "cmsis-svd"
34+
35+
_VERBOSE = True
36+
37+
def _log(s: str):
38+
if _VERBOSE:
39+
print(s)
40+
41+
@dataclasses.dataclass
42+
class RemoteSVDFile:
43+
vendor: str
44+
name: str
45+
download_url: str
46+
size: str
47+
48+
def as_dict(self) -> Dict[str, str]:
49+
return {"vendor": self.vendor, "name": self.name, "download_url": self.download_url, "size": self.size}
50+
51+
@staticmethod
52+
def from_dict(d: Dict[str, str]) -> "RemoteSVDFile":
53+
return RemoteSVDFile(d["vendor"], d["name"], d["download_url"], d["size"])
54+
55+
56+
class SVDFetcher:
57+
"""Fetch SVD files from cmsis-svd repository via GitHub API."""
58+
59+
def __init__(self, cache_dir: Optional[Path] = None):
60+
"""
61+
Initialize the SVD fetcher.
62+
63+
Args:
64+
cache_dir: Directory to cache downloaded SVD files.
65+
Defaults to ~/.cache/svd-fetcher/
66+
"""
67+
if cache_dir is None:
68+
cache_dir = _DEFAULT_CACHE_PATH
69+
self.cache_dir = Path(cache_dir)
70+
self.cache_dir.mkdir(parents=True, exist_ok=True)
71+
72+
self.session = requests.Session()
73+
# Add GitHub token if available via environment variable
74+
# This increases rate limits from 60/hr to 5000/hr
75+
if token := os.environ.get("GITHUB_TOKEN"):
76+
self.session.headers["Authorization"] = f"token {token}"
77+
78+
def save(self, vendors: Dict[str, List[RemoteSVDFile]]):
79+
cache_file = self.cache_dir / "devices.json"
80+
81+
all_tuples = [(vendor, [c.as_dict() for c in chips]) for vendor, chips in vendors.items()]
82+
serializable_dict: Dict[str, List[Dict[str, str]]] = {}
83+
for (vendor, chips) in all_tuples:
84+
serializable_dict[vendor] = chips
85+
86+
open(str(cache_file), "w").write(json.dumps(serializable_dict))
87+
88+
def restore(self) -> Optional[Dict[str, List[RemoteSVDFile]]]:
89+
cache_file = self.cache_dir / "devices.json"
90+
91+
if cache_file.exists():
92+
raw: Dict[str, List[Dict[str, str]]] = json.loads(open(str(cache_file)).read())
93+
good_dict: Dict[str, List[RemoteSVDFile]] = dict()
94+
# Raw _should_ be dict of vendors with a list of chips under each
95+
for (vendor, chips_dicts) in raw.items():
96+
chips = [RemoteSVDFile.from_dict(chip_dict) for chip_dict in chips_dicts]
97+
good_dict[vendor] = chips
98+
return good_dict
99+
100+
return None
101+
102+
103+
104+
105+
106+
107+
108+
def get_vendors(self) -> List[str]:
109+
"""
110+
Get list of available vendors.
111+
112+
Returns:
113+
Sorted list of vendor names
114+
"""
115+
url = f"{_BASE_API_URL}/contents/data"
116+
params = {"ref": _BRANCH}
117+
118+
response = self.session.get(url, params=params)
119+
response.raise_for_status()
120+
121+
contents = response.json()
122+
vendors = [item["name"] for item in contents if item["type"] == "dir"]
123+
return sorted(vendors)
124+
125+
def get_chips_for_vendor(self, vendor: str) -> List[RemoteSVDFile]:
126+
"""
127+
Get list of available chips for a specific vendor.
128+
129+
Args:
130+
vendor: Vendor name (e.g., "STMicro", "NXP")
131+
132+
Returns:
133+
List of dicts with keys: 'name', 'download_url', 'size'
134+
"""
135+
url = f"{_BASE_API_URL}/contents/data/{vendor}"
136+
params = {"ref": _BRANCH}
137+
138+
response = self.session.get(url, params=params)
139+
response.raise_for_status()
140+
141+
contents = response.json()
142+
chips: List[RemoteSVDFile] = []
143+
for item in contents:
144+
if item["type"] == "file" and item["name"].endswith(".svd"):
145+
chips.append(
146+
RemoteSVDFile(
147+
vendor=vendor,
148+
name=item["name"],
149+
download_url=item["download_url"],
150+
size=item["size"]))
151+
return sorted(chips, key=lambda x: x.name)
152+
153+
def get_all_chips(self) -> Dict[str, List[RemoteSVDFile]]:
154+
"""
155+
Get all vendors and their chips with download URLs.
156+
157+
Returns:
158+
Dict mapping vendor name to list of chip info dicts
159+
"""
160+
vendors = self.get_vendors()
161+
all_data = {}
162+
163+
for vendor in vendors:
164+
chips = self.get_chips_for_vendor(vendor)
165+
all_data[vendor] = chips
166+
167+
return all_data
168+
169+
def download_svd(self, chip: RemoteSVDFile, force: bool = False) -> Path:
170+
"""
171+
Download an SVD file for a specific chip.
172+
173+
Args:
174+
chip: The RemoteSVDFile for the chip
175+
force: If True, re-download even if cached
176+
177+
Returns:
178+
Path to the downloaded SVD file
179+
"""
180+
# Check cache first
181+
cached_file = self.cache_dir / chip.vendor / chip.name
182+
if cached_file.exists() and not force:
183+
_log(f"Using cached file {chip.vendor}->{chip.name} at {cached_file}")
184+
return cached_file
185+
186+
# Download the file
187+
_log(f"Downloading {chip.vendor}->{chip.name}, url {chip.download_url}, to {cached_file}")
188+
response = self.session.get(chip.download_url)
189+
response.raise_for_status()
190+
191+
# Save to cache
192+
cached_file.parent.mkdir(parents=True, exist_ok=True)
193+
cached_file.write_bytes(response.content)
194+
195+
return cached_file
196+
197+
def search_chip(self, query: str) -> List[RemoteSVDFile]:
198+
"""
199+
Search for chips matching a query string.
200+
201+
Args:
202+
query: Search string (case-insensitive)
203+
204+
Returns:
205+
List of matching chips with vendor, name, and download_url
206+
"""
207+
query_lower = query.lower()
208+
matches = []
209+
210+
all_chips = self.get_all_chips()
211+
for vendor, chips in all_chips.items():
212+
for chip in chips:
213+
if query_lower in chip.name.lower():
214+
matches.append(chip)
215+
216+
return matches
217+
218+
219+
def main():
220+
"""Example usage."""
221+
fetcher = SVDFetcher()
222+
223+
# List vendors
224+
print("Available vendors:")
225+
vendors = fetcher.get_vendors()
226+
for vendor in vendors:
227+
print(f" {vendor}")
228+
229+
print(f"\nTotal vendors: {len(vendors)}")
230+
231+
# Show chips for first vendor as example
232+
if vendors:
233+
example_vendor = vendors[0]
234+
print(f"\nChips for {example_vendor}:")
235+
chips = fetcher.get_chips_for_vendor(example_vendor)
236+
for chip in chips[:5]:
237+
print(f" {chip.name}")
238+
print(f" ... ({len(chips)} total)")
239+
240+
# Example: search for STM32F4 chips
241+
print("\nSearching for 'STM32F4' chips:")
242+
results = fetcher.search_chip("STM32F4")
243+
for result in results[:10]:
244+
print(f" {result.vendor}/{result.name}")
245+
246+
# Example: download a specific chip
247+
if results:
248+
first = results[0]
249+
print(f"\nDownloading {first.vendor}/{first.name}...")
250+
path = fetcher.download_svd(first)
251+
print(f"Saved to: {path}")
252+
253+
254+
if __name__ == "__main__":
255+
main()

0 commit comments

Comments
 (0)