|
| 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