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 |