Stefan Schuermans commited on 2021-02-17 19:13:39
Showing 1 changed files, with 147 additions and 20 deletions.
... | ... |
@@ -2,6 +2,20 @@ |
2 | 2 |
|
3 | 3 |
import ezdxf |
4 | 4 |
import re |
5 |
+import sys |
|
6 |
+ |
|
7 |
+ |
|
8 |
+class ParseError(Exception): |
|
9 |
+ def __init__(self, msg: str, pos: str): |
|
10 |
+ super().__init__(self, msg, pos) |
|
11 |
+ self.msg = msg |
|
12 |
+ self.pos = pos |
|
13 |
+ |
|
14 |
+ def __repr__(self) -> str: |
|
15 |
+ return f'ParseError({repr(self.msg):s}, {repr(self.pos):s})' |
|
16 |
+ |
|
17 |
+ def __str__(self) -> str: |
|
18 |
+ return f'ParseError: {self.msg:s} at {self.pos:s}' |
|
5 | 19 |
|
6 | 20 |
|
7 | 21 |
class StringBuffer: |
... | ... |
@@ -67,10 +81,10 @@ class StringBuffer: |
67 | 81 |
""" |
68 | 82 |
Check that string s follows index. |
69 | 83 |
If so, advance. |
70 |
- If not, throw ValueError. |
|
84 |
+ If not, throw ParseError. |
|
71 | 85 |
""" |
72 | 86 |
if not self.check(s): |
73 |
- raise ValueError |
|
87 |
+ raise ParseError(f'expected {repr(s):s}', self.pos()) |
|
74 | 88 |
|
75 | 89 |
def get(self, length: int) -> str: |
76 | 90 |
""" |
... | ... |
@@ -83,11 +97,12 @@ class StringBuffer: |
83 | 97 |
def getRe(self, ptrn: re.Pattern): |
84 | 98 |
""" |
85 | 99 |
Return the matching the regular expression pattern and advance |
86 |
- index or throw ValueError. |
|
100 |
+ index or throw ParseError. |
|
87 | 101 |
""" |
88 | 102 |
m = ptrn.match(self._string[self._index:]) |
89 | 103 |
if not m: |
90 |
- raise ValueError() |
|
104 |
+ raise ParseError(f'expected pattern {repr(ptrn.pattern):s}', |
|
105 |
+ self.pos()) |
|
91 | 106 |
self._index += len(m.group(0)) |
92 | 107 |
return m |
93 | 108 |
|
... | ... |
@@ -97,6 +112,17 @@ class StringBuffer: |
97 | 112 |
""" |
98 | 113 |
return self._string[self._index:self._index + length] |
99 | 114 |
|
115 |
+ def pos(self) -> str: |
|
116 |
+ """ |
|
117 |
+ Get string describing current position. |
|
118 |
+ """ |
|
119 |
+ before = self._string[:self._index].split('\n') |
|
120 |
+ after = self._string[self._index:].split('\n') |
|
121 |
+ line_no = len(before) |
|
122 |
+ column_no = len(before[-1]) + 1 |
|
123 |
+ rest = after[0] |
|
124 |
+ return f'line {line_no:d} column {column_no:d} {repr(rest):s}' |
|
125 |
+ |
|
100 | 126 |
|
101 | 127 |
class PcbBaseParser(StringBuffer): |
102 | 128 |
""" |
... | ... |
@@ -110,6 +136,11 @@ class PcbBaseParser(StringBuffer): |
110 | 136 |
re_space = re.compile(r'^\s+', re.MULTILINE) |
111 | 137 |
re_space_opt = re.compile(r'^\s*', re.MULTILINE) |
112 | 138 |
|
139 |
+ known_string_flags = set([ |
|
140 |
+ 'edge2', 'hole', 'nopaste', 'octagon', 'onsolder', 'pin', 'showname', |
|
141 |
+ 'square', 'via' |
|
142 |
+ ]) |
|
143 |
+ |
|
113 | 144 |
def parseCoord(self) -> float: |
114 | 145 |
""" |
115 | 146 |
Parse a coordinate. |
... | ... |
@@ -127,22 +158,47 @@ class PcbBaseParser(StringBuffer): |
127 | 158 |
Parse a floating point value. |
128 | 159 |
""" |
129 | 160 |
with self: |
130 |
- return float(self.getRe(self.re_float).group(0)) |
|
161 |
+ s = self.getRe(self.re_float).group(0) |
|
162 |
+ try: |
|
163 |
+ return float(s) |
|
164 |
+ except ValueError as exc: |
|
165 |
+ raise ParseError(f'invalid floating point value {repr(s):s}', |
|
166 |
+ self.pos()) |
|
131 | 167 |
|
132 | 168 |
def parseInt(self) -> int: |
133 | 169 |
""" |
134 | 170 |
Parse a decimal integer. |
135 | 171 |
""" |
136 | 172 |
with self: |
137 |
- return int(self.getRe(self.re_int).group(0)) |
|
173 |
+ s = self.getRe(self.re_int).group(0) |
|
174 |
+ try: |
|
175 |
+ return int(s) |
|
176 |
+ except ValueError as exc: |
|
177 |
+ raise ParseError('invalid integer value {repr(s):s}', |
|
178 |
+ self.pos()) |
|
138 | 179 |
|
139 |
- def parseQuoted(self): |
|
180 |
+ def parseQuoted(self) -> str: |
|
140 | 181 |
""" |
141 |
- Parse quoted string. |
|
182 |
+ Parse a quoted string. |
|
142 | 183 |
""" |
143 | 184 |
with self: |
144 | 185 |
return self.getRe(self.re_quoted).group(1) |
145 | 186 |
|
187 |
+ def parseStringFlags(self) -> list: |
|
188 |
+ """ |
|
189 |
+ Parse string flags. |
|
190 |
+ """ |
|
191 |
+ with self: |
|
192 |
+ q = self.parseQuoted() |
|
193 |
+ fs = set(q.split(',')) |
|
194 |
+ fs -= {''} |
|
195 |
+ unk = fs - self.known_string_flags |
|
196 |
+ if unk: |
|
197 |
+ unknown = ','.join(sorted(unk)) |
|
198 |
+ raise ParseError(f'unknown string flags {unknown:s}', |
|
199 |
+ self.pos()) |
|
200 |
+ return sorted(fs) |
|
201 |
+ |
|
146 | 202 |
def parseSpace(self): |
147 | 203 |
""" |
148 | 204 |
Parse at least one whitespace character or more. |
... | ... |
@@ -171,22 +227,28 @@ class PcbFootprintParser(PcbBaseParser): |
171 | 227 |
with self: |
172 | 228 |
self.parseOpen() |
173 | 229 |
while True: |
230 |
+ excs = [] # collect parse errors |
|
174 | 231 |
# end of body |
175 | 232 |
try: |
176 | 233 |
self.parseClose() |
177 | 234 |
break |
178 |
- except ValueError: |
|
179 |
- pass |
|
235 |
+ except ParseError as exc: |
|
236 |
+ excs.append(exc) |
|
237 |
+ # not end of body, try options |
|
180 | 238 |
# try options |
181 | 239 |
for c in contents: |
182 | 240 |
try: |
183 | 241 |
c() |
184 |
- break # option succeded, go to next content |
|
185 |
- except ValueError: |
|
186 |
- pass # an option failed, try next option |
|
242 |
+ # option succeded, go to next content |
|
243 |
+ break |
|
244 |
+ except ParseError as exc: |
|
245 |
+ # an option failed, try next option |
|
246 |
+ excs.append(exc) |
|
187 | 247 |
# all options failed |
188 | 248 |
else: |
189 |
- raise ValueError() |
|
249 |
+ raise ParseError( |
|
250 |
+ 'invalid syntax, all options failed: ' + |
|
251 |
+ ', '.join([exc.msg for exc in excs]), self.pos()) |
|
190 | 252 |
|
191 | 253 |
def parseClose(self): |
192 | 254 |
""" |
... | ... |
@@ -195,6 +257,34 @@ class PcbFootprintParser(PcbBaseParser): |
195 | 257 |
with self: |
196 | 258 |
self.parseSpaceOpt() |
197 | 259 |
self.expect(')') |
260 |
+ self.parseSpaceOpt() |
|
261 |
+ |
|
262 |
+ def parseElementArcClause(self): |
|
263 |
+ """ |
|
264 |
+ Parse a GNU PCB element arc clause. |
|
265 |
+ """ |
|
266 |
+ with self: |
|
267 |
+ self.parseSpaceOpt() |
|
268 |
+ self.expect('ElementArc') |
|
269 |
+ self.parseSpaceOpt() |
|
270 |
+ self.expect('[') |
|
271 |
+ self.parseSpaceOpt() |
|
272 |
+ r_x = self.parseCoord() |
|
273 |
+ self.parseSpace() |
|
274 |
+ r_y = self.parseCoord() |
|
275 |
+ self.parseSpace() |
|
276 |
+ width = self.parseCoord() |
|
277 |
+ self.parseSpace() |
|
278 |
+ height = self.parseCoord() |
|
279 |
+ self.parseSpace() |
|
280 |
+ start_angle = self.parseFloat() |
|
281 |
+ self.parseSpace() |
|
282 |
+ end_angle = self.parseFloat() |
|
283 |
+ self.parseSpace() |
|
284 |
+ thickness = self.parseCoord() |
|
285 |
+ self.parseSpaceOpt() |
|
286 |
+ self.expect(']') |
|
287 |
+ self.parseSpaceOpt() |
|
198 | 288 |
|
199 | 289 |
def parseElementBlock(self): |
200 | 290 |
""" |
... | ... |
@@ -203,11 +293,13 @@ class PcbFootprintParser(PcbBaseParser): |
203 | 293 |
with self: |
204 | 294 |
self.parseElementClause() |
205 | 295 |
self.parseBody([ |
296 |
+ # parse options |
|
297 |
+ lambda: self.parseElementArcClause(), |
|
206 | 298 |
lambda: self.parseElementLineClause(), |
207 |
- lambda: self.parsePadClause() |
|
299 |
+ lambda: self.parsePadClause(), |
|
300 |
+ lambda: self.parsePinClause() |
|
208 | 301 |
]) |
209 | 302 |
|
210 |
- |
|
211 | 303 |
def parseElementClause(self): |
212 | 304 |
""" |
213 | 305 |
Parse a GNU PCB element clause. |
... | ... |
@@ -218,7 +310,7 @@ class PcbFootprintParser(PcbBaseParser): |
218 | 310 |
self.parseSpaceOpt() |
219 | 311 |
self.expect('[') |
220 | 312 |
self.parseSpaceOpt() |
221 |
- s_flags = self.parseQuoted() |
|
313 |
+ s_flags = self.parseStringFlags() |
|
222 | 314 |
self.parseSpace() |
223 | 315 |
desc = self.parseQuoted() |
224 | 316 |
self.parseSpace() |
... | ... |
@@ -238,9 +330,10 @@ class PcbFootprintParser(PcbBaseParser): |
238 | 330 |
self.parseSpace() |
239 | 331 |
t_scale = self.parseInt() |
240 | 332 |
self.parseSpace() |
241 |
- t_s_flags = self.parseQuoted() |
|
333 |
+ t_s_flags = self.parseStringFlags() |
|
242 | 334 |
self.parseSpaceOpt() |
243 | 335 |
self.expect(']') |
336 |
+ self.parseSpaceOpt() |
|
244 | 337 |
|
245 | 338 |
def parseElementLineClause(self): |
246 | 339 |
""" |
... | ... |
@@ -263,6 +356,7 @@ class PcbFootprintParser(PcbBaseParser): |
263 | 356 |
thickness = self.parseCoord() |
264 | 357 |
self.parseSpaceOpt() |
265 | 358 |
self.expect(']') |
359 |
+ self.parseSpaceOpt() |
|
266 | 360 |
|
267 | 361 |
def parseOpen(self): |
268 | 362 |
""" |
... | ... |
@@ -271,6 +365,7 @@ class PcbFootprintParser(PcbBaseParser): |
271 | 365 |
with self: |
272 | 366 |
self.parseSpaceOpt() |
273 | 367 |
self.expect('(') |
368 |
+ self.parseSpaceOpt() |
|
274 | 369 |
|
275 | 370 |
def parsePadClause(self): |
276 | 371 |
""" |
... | ... |
@@ -300,9 +395,41 @@ class PcbFootprintParser(PcbBaseParser): |
300 | 395 |
self.parseSpace() |
301 | 396 |
number = self.parseQuoted() |
302 | 397 |
self.parseSpace() |
303 |
- s_flags = self.parseQuoted() |
|
398 |
+ s_flags = self.parseStringFlags() |
|
399 |
+ self.parseSpaceOpt() |
|
400 |
+ self.expect(']') |
|
401 |
+ self.parseSpaceOpt() |
|
402 |
+ |
|
403 |
+ def parsePinClause(self): |
|
404 |
+ """ |
|
405 |
+ Parse a GNU PCB pin clause. |
|
406 |
+ """ |
|
407 |
+ with self: |
|
408 |
+ self.parseSpaceOpt() |
|
409 |
+ self.expect('Pin') |
|
410 |
+ self.parseSpaceOpt() |
|
411 |
+ self.expect('[') |
|
412 |
+ self.parseSpaceOpt() |
|
413 |
+ r_x = self.parseCoord() |
|
414 |
+ self.parseSpace() |
|
415 |
+ r_y = self.parseCoord() |
|
416 |
+ self.parseSpace() |
|
417 |
+ thickness = self.parseCoord() |
|
418 |
+ self.parseSpace() |
|
419 |
+ clearance = self.parseCoord() |
|
420 |
+ self.parseSpace() |
|
421 |
+ mask = self.parseCoord() |
|
422 |
+ self.parseSpace() |
|
423 |
+ drill = self.parseCoord() |
|
424 |
+ self.parseSpace() |
|
425 |
+ name = self.parseQuoted() |
|
426 |
+ self.parseSpace() |
|
427 |
+ number = self.parseQuoted() |
|
428 |
+ self.parseSpace() |
|
429 |
+ s_flags = self.parseStringFlags() |
|
304 | 430 |
self.parseSpaceOpt() |
305 | 431 |
self.expect(']') |
432 |
+ self.parseSpaceOpt() |
|
306 | 433 |
|
307 | 434 |
|
308 | 435 |
class PcbFootprint: |
... | ... |
@@ -336,7 +463,7 @@ def write_dxf(file_name: str): |
336 | 463 |
|
337 | 464 |
def main(): |
338 | 465 |
fp = PcbFootprint() |
339 |
- fp.readPcb('../pcb_footpr/SMD_1206') |
|
466 |
+ fp.readPcb(sys.argv[1]) |
|
340 | 467 |
|
341 | 468 |
|
342 | 469 |
if __name__ == '__main__': |
343 | 470 |