|
7 | 7 |
|
8 | 8 | import json |
9 | 9 | import re |
| 10 | +import gc |
10 | 11 | import ttboard.util.time as time |
11 | 12 | from ttboard.pins.pins import Pins |
12 | 13 | from ttboard.boot.rom import ChipROM |
13 | 14 | from ttboard.boot.shuttle_properties import HardcodedShuttle |
14 | 15 | import ttboard.logging as logging |
15 | 16 | log = logging.getLogger(__name__) |
16 | 17 |
|
| 18 | + |
17 | 19 | ''' |
18 | 20 | Fetched with |
19 | 21 | https://index.tinytapeout.com/tt03p5.json?fields=repo,address,commit,clock_hz,title |
20 | 22 | https://index.tinytapeout.com/tt04.json?fields=repo,address,commit,clock_hz,title |
21 | 23 |
|
22 | 24 | ''' |
23 | 25 | class Design: |
24 | | - BadCharsRe = re.compile(r'[^\w\d\s]+') |
25 | | - SpaceCharsRe = re.compile(r'\s+') |
26 | | - def __init__(self, projectMux, projindex:int, info:dict): |
| 26 | + def __init__(self, projectMux, projname:str, projindex:int, info:dict): |
27 | 27 | self.mux = projectMux |
28 | | - self.project_index = projindex |
29 | 28 | self.count = int(projindex) |
| 29 | + self.name = projname |
30 | 30 | self.macro = info['macro'] |
31 | | - self.name = info['macro'] |
32 | | - self.repo = info['repo'] |
33 | | - self.commit = info['commit'] |
| 31 | + self.repo = '' |
| 32 | + if 'repo' in info: |
| 33 | + self.repo = info['repo'] |
| 34 | + |
| 35 | + self.commit = '' |
| 36 | + if 'commit' in info: |
| 37 | + self.commit = info['commit'] |
34 | 38 | self.clock_hz = info['clock_hz'] |
35 | | - # special cleanup for wokwi gen'ed names |
36 | | - if self.name.startswith('tt_um_wokwi') and 'title' in info and len(info['title']): |
37 | | - new_name = self.SpaceCharsRe.sub('_', self.BadCharsRe.sub('', info['title'])).lower() |
38 | | - if len(new_name): |
39 | | - self.name = f'wokwi_{new_name}' |
40 | | - |
41 | 39 | self._all = info |
42 | 40 |
|
| 41 | + @property |
| 42 | + def project_index(self): |
| 43 | + return self.count |
| 44 | + |
43 | 45 | def enable(self): |
44 | 46 | self.mux.enable(self) |
45 | 47 |
|
46 | 48 | def disable(self): |
47 | 49 | self.mux.disable() |
48 | 50 |
|
49 | 51 | def __str__(self): |
50 | | - return f'{self.name} ({self.project_index}) @ {self.repo}' |
| 52 | + return f'{self.name} ({self.count}) @ {self.repo}' |
51 | 53 |
|
52 | 54 | def __repr__(self): |
53 | | - return f'<Design {self.project_index}: {self.name}>' |
| 55 | + return f'<Design {self.count}: {self.name}>' |
54 | 56 |
|
| 57 | + |
| 58 | +class DesignStub: |
| 59 | + ''' |
| 60 | + A yet-to-be-loaded design, just a pointer that will |
| 61 | + auto-load the design if accessed. |
| 62 | + Has a side effect of replacing itself as an attribute |
| 63 | + in the design index so this only happens once. |
| 64 | + ''' |
| 65 | + def __init__(self, design_index, projname): |
| 66 | + self.design_index = design_index |
| 67 | + self.name = projname |
| 68 | + self._des = None |
| 69 | + |
| 70 | + def _lazy_load(self): |
| 71 | + des = self.design_index.load_project(self.name) |
| 72 | + setattr(self.design_index, self.name, des) |
| 73 | + self._des = des |
| 74 | + return des |
| 75 | + |
| 76 | + def __getattr__(self, name:str): |
| 77 | + if hasattr(self, '_des') and self._des is not None: |
| 78 | + des = self._des |
| 79 | + else: |
| 80 | + des = self._lazy_load() |
| 81 | + return getattr(des, name) |
| 82 | + |
| 83 | + def __dir__(self): |
| 84 | + des = self._lazy_load() |
| 85 | + return dir(des) |
| 86 | + |
| 87 | + def __repr__(self): |
| 88 | + return f'<Design {self.name} (uninit)>' |
| 89 | + |
55 | 90 | class DesignIndex: |
| 91 | + |
| 92 | + BadCharsRe = re.compile(r'[^\w\d\s]+') |
| 93 | + SpaceCharsRe = re.compile(r'\s+') |
| 94 | + |
56 | 95 | def __init__(self, projectMux, src_JSON_file:str='shuttle_index.json'): |
57 | | - self._shuttle_index = dict() |
| 96 | + self._src_json = src_JSON_file |
| 97 | + self._project_mux = projectMux |
58 | 98 | self._project_count = 0 |
| 99 | + self.load_available(src_JSON_file) |
| 100 | + |
| 101 | + def load_available(self, src_JSON_file:str=None): |
| 102 | + if src_JSON_file is None: |
| 103 | + src_JSON_file = self._src_json |
| 104 | + |
| 105 | + self._shuttle_index = dict() |
| 106 | + self._available_projects = dict() |
59 | 107 | try: |
60 | 108 | with open(src_JSON_file) as fh: |
61 | 109 | index = json.load(fh) |
62 | | - for project in index["projects"]: |
63 | | - des = Design(projectMux, project["address"], project) |
64 | | - attrib_name = des.name |
65 | | - if attrib_name in self._shuttle_index: |
| 110 | + for project in index['projects']: |
| 111 | + attrib_name = project['macro'] |
| 112 | + project_address = int(project['address']) |
| 113 | + |
| 114 | + if attrib_name in self._available_projects: |
66 | 115 | log.info(f'Already have a "{attrib_name}" here...') |
67 | 116 | attempt = 1 |
68 | 117 | augmented_name = f'{attrib_name}_{attempt}' |
69 | | - while augmented_name in self._shuttle_index: |
| 118 | + while augmented_name in self._available_projects: |
70 | 119 | attempt += 1 |
71 | 120 | augmented_name = f'{attrib_name}_{attempt}' |
72 | 121 |
|
73 | 122 | attrib_name = augmented_name |
74 | | - des.name = augmented_name |
75 | | - self._shuttle_index[attrib_name] = des |
76 | | - setattr(self, attrib_name, des) |
| 123 | + |
| 124 | + attrib_name = self._wokwi_name_cleanup(attrib_name, project) |
| 125 | + self._available_projects[attrib_name] = int(project_address) |
| 126 | + setattr(self, attrib_name, DesignStub(self, attrib_name)) |
77 | 127 | self._project_count += 1 |
| 128 | + |
78 | 129 | except OSError: |
79 | 130 | log.error(f'Could not open shuttle index {src_JSON_file}') |
| 131 | + |
| 132 | + gc.collect() |
| 133 | + |
80 | 134 |
|
81 | 135 |
|
| 136 | + |
| 137 | + def _wokwi_name_cleanup(self, name:str, info:dict): |
| 138 | + |
| 139 | + # special cleanup for wokwi gen'ed names |
| 140 | + if name.startswith('tt_um_wokwi') and 'title' in info and len(info['title']): |
| 141 | + new_name = self.SpaceCharsRe.sub('_', self.BadCharsRe.sub('', info['title'])).lower() |
| 142 | + if len(new_name): |
| 143 | + name = f'wokwi_{new_name}' |
| 144 | + |
| 145 | + return name |
82 | 146 | @property |
83 | 147 | def count(self): |
84 | 148 | return self._project_count |
85 | 149 |
|
86 | 150 | @property |
87 | 151 | def names(self): |
88 | | - return sorted(self._shuttle_index.keys()) |
89 | | - |
| 152 | + return sorted(self._available_projects.keys()) |
90 | 153 | @property |
91 | 154 | def all(self): |
| 155 | + ''' |
| 156 | + all available projects in the shuttle, whether loaded or not |
| 157 | + ''' |
| 158 | + return list(map(lambda p: getattr(self, p), sorted(self._available_projects.keys()))) |
| 159 | + @property |
| 160 | + def all_loaded(self): |
| 161 | + ''' |
| 162 | + all the projects that have been lazy-loaded, basically |
| 163 | + anything you've actually enabled or accessed in any way. |
| 164 | + ''' |
92 | 165 | return sorted(self._shuttle_index.values(), key=lambda p: p.name) |
93 | 166 |
|
| 167 | + |
94 | 168 | def get(self, project_name:str) -> Design: |
| 169 | + if not self.is_available(project_name): |
| 170 | + # not in list of available, maybe it's an integer? |
| 171 | + try: |
| 172 | + des_idx = int(project_name) |
| 173 | + for des in self._available_projects.items(): |
| 174 | + if des[1] == des_idx: |
| 175 | + return self.get(des[0]) |
| 176 | + except ValueError: |
| 177 | + pass |
| 178 | + raise AttributeError(f'Unknown project "{project_name}"') |
| 179 | + |
| 180 | + if hasattr(self, project_name): |
| 181 | + return getattr(self, project_name) |
| 182 | + |
95 | 183 | if project_name in self._shuttle_index: |
96 | 184 | return self._shuttle_index[project_name] |
97 | 185 |
|
98 | | - # maybe it's an integer? |
99 | | - try: |
100 | | - des_idx = int(project_name) |
101 | | - for des in self.all: |
102 | | - if des.count == des_idx: |
103 | | - return des |
104 | | - except ValueError: |
105 | | - pass |
| 186 | + return self.load_project(project_name) |
| 187 | + |
| 188 | + def load_project(self, project_name:str) -> Design: |
| 189 | + |
| 190 | + # neither a know integer nor a loaded project, but is avail |
| 191 | + project_address = self._available_projects[project_name] |
| 192 | + try: |
| 193 | + with open(self._src_json) as fh: |
| 194 | + index = json.load(fh) |
| 195 | + for project in index['projects']: |
| 196 | + if int(project['address']) == project_address: |
| 197 | + # this is our guy |
| 198 | + des = Design(self._project_mux, project_name, project["address"], project) |
| 199 | + self._shuttle_index[des.name] = des |
| 200 | + gc.collect() |
| 201 | + return des |
| 202 | + |
| 203 | + |
| 204 | + except OSError: |
| 205 | + log.error(f'Could not open shuttle index {self._src_json}') |
106 | 206 |
|
107 | | - raise ValueError(f'Unknown project "{project_name}"') |
| 207 | + raise AttributeError(f'Unknown project "{project_name}"') |
108 | 208 |
|
| 209 | + def is_available(self, project_name:str): |
| 210 | + return project_name in self._available_projects |
| 211 | + |
109 | 212 | def __len__(self): |
110 | | - return len(self._shuttle_index) |
| 213 | + return len(self._available_projects) |
| 214 | + def __getattr__(self, name:str): |
| 215 | + if hasattr(self, '_shuttle_index') and name in self._shuttle_index: |
| 216 | + return self._shuttle_index[name] |
| 217 | + |
| 218 | + return self.get(name) |
111 | 219 |
|
112 | 220 | def __getitem__(self, idx:int) -> Design: |
113 | 221 | return self.get(idx) |
| 222 | + |
| 223 | + def __dir__(self): |
| 224 | + return list(self._available_projects.keys()) |
114 | 225 |
|
115 | 226 | def __repr__(self): |
116 | 227 | return f'<DesignIndex {len(self)} projects>' |
@@ -248,18 +359,28 @@ def find(self, search:str) -> list: |
248 | 359 | return list(filter(lambda p: p.name.find(search) >= 0, self.all)) |
249 | 360 |
|
250 | 361 | def __getattr__(self, name): |
251 | | - if hasattr(self, 'projects') and hasattr(self.projects, name): |
252 | | - return getattr(self.projects, name) |
| 362 | + if hasattr(self, 'projects'): |
| 363 | + if self.projects.is_available(name) or hasattr(self.projects, name): |
| 364 | + return getattr(self.projects, name) |
253 | 365 | raise AttributeError(f"What is '{name}'?") |
254 | 366 |
|
255 | 367 | def __getitem__(self, key) -> Design: |
256 | 368 | if hasattr(self, 'projects'): |
257 | 369 | return self.projects[key] |
258 | 370 | raise None |
259 | 371 |
|
| 372 | + def __dir__(self): |
| 373 | + # this doesn't seem to do what I want in uPython? |
| 374 | + if hasattr(self, 'projects'): |
| 375 | + return self.projects.names |
| 376 | + return [] |
| 377 | + |
| 378 | + |
| 379 | + def __len__(self): |
| 380 | + return len(self.projects) |
260 | 381 |
|
261 | 382 | def __str__(self): |
262 | | - return f'Shuttle {self.run}\n{self.all}' |
| 383 | + return f'Shuttle {self.run}' |
263 | 384 |
|
264 | 385 | def __repr__(self): |
265 | 386 | des_idx = self.projects |
|
0 commit comments