begin of PCB footprint parser 2
Stefan Schuermans

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