From d5bbfade80f1927f2512b9f5bbb723255ec8926d Mon Sep 17 00:00:00 2001 From: jaseg Date: Sat, 29 Jan 2022 21:05:31 +0100 Subject: Fix IPC-356 tests --- gerbonara/gerber/ipc356.py | 137 +++++++++++++++++++++++++++++++++------------ 1 file changed, 100 insertions(+), 37 deletions(-) (limited to 'gerbonara/gerber/ipc356.py') diff --git a/gerbonara/gerber/ipc356.py b/gerbonara/gerber/ipc356.py index 940ed72..043f2ab 100644 --- a/gerbonara/gerber/ipc356.py +++ b/gerbonara/gerber/ipc356.py @@ -39,21 +39,28 @@ class Netlist(CamFile): self.adjacency = adjacency or {} self.params = params or {} - def merge(self, other, our_net_prefix=None, their_net_prefix=None): + def merge(self, other, our_prefix=None, their_prefix=None): ''' Merge other netlist into this netlist. The respective net names are prefixed with the given prefixes (default: None). Garbles other. ''' if not isinstance(other, Netlist): raise TypeError(f'Can only merge Netlist with other Netlist, not {type(other)}') - self.prefix_nets(our_net_prefix) - other.prefix_nets(our_net_prefix) + self.prefix_nets(our_prefix) + other.prefix_nets(our_prefix) self.test_records.extend(other.test_records) self.conductors.extend(other.conductors) self.outlines.extend(other.outlines) self.comments.extend(other.comments) self.adjacency.update(other.adjacency) - self.params.extend(other.params) + self.params.update(other.params) + + self.params['JOB'] = 'Gerbonara IPC-356 merge' + self.params['TITLE'] = 'Gerbonara IPC-356 merge' + + for key in 'CODE', 'NUM', 'REV', 'VER': + if key in self.params: + del self.params[key] def prefix_nets(self, prefix): if not prefix: @@ -68,10 +75,18 @@ class Netlist(CamFile): conductor.net_name = prefix + conductor.net_name new_adjacency = {} - for key in adjacency: - new_adjacency[prefix + key] = [ prefix + name for name in adjacency[key] ] + for key in self.adjacency: + new_adjacency[prefix + key] = [ prefix + name for name in self.adjacency[key] ] self.adjacency = new_adjacency + def offset(self, dx=0, dy=0, unit=MM): + # FIXME + pass + + def rotate(self, angle:'radian', center=(0,0), unit=MM): + # FIXME + pass + @property def objects(self): yield from self.test_records @@ -117,18 +132,18 @@ class Netlist(CamFile): yield f'P {name} {value!s}' net_name_map = { - f'NNAME{i}': name for i, name in enumerate( + name: f'NNAME{i}' for i, name in enumerate( name for name in self.net_names() if len(name) > 14 ) } yield 'C' yield 'C Net name mapping:' yield 'C' - for name, value in net_name_map.items(): - yield f'P {name} {value!s}' + for name, alias in net_name_map.items(): + yield f'P {alias} {name}' yield 'C' - yield 'C Test records:' + yield 'C Test records:' yield 'C' for record in self.test_records: @@ -136,21 +151,21 @@ class Netlist(CamFile): if self.conductors: yield 'C' - yield 'C Conductors:' + yield 'C Conductors:' yield 'C' for conductor in self.conductors: yield from conductor.format(settings, net_name_map) if self.outlines: yield 'C' - yield 'C Outlines:' + yield 'C Outlines:' yield 'C' for outline in self.outlines: yield from outline.format(settings) if self.adjacency: yield 'C' - yield 'C Adjacency data:' + yield 'C Adjacency data:' yield 'C' done = set() for net, others in self.adjacency.items(): @@ -217,6 +232,7 @@ class NetlistParser(object): self.adjacency = {} self.outlines = [] self.eof = False + self.generator = None def warn(self, msg, kls=SyntaxWarning): warnings.warn(f'{self.filename}:{self.start_line}: {msg}', kls) @@ -255,16 +271,34 @@ class NetlistParser(object): return if self.eof: - warnings.warn('Data following IPC-356 End Of File marker') + self.warn('Data following IPC-356 End Of File marker') if line[0] == 'C': line = line[2:].strip() + # +-- sic! + # v + if 'Ouptut' in line and 'Allegro' in line: + self.generator = 'allegro' + + elif 'Ouptut' not in line and 'Allegro' in line: + self.warn('This seems to be a file generated by a newer allegro version. Please raise an issue on our ' + 'issue tracker with your Allegro version and if possible please provide an example file ' + 'so we can improve Gerbonara!') + + elif 'EAGLE' in line and 'CadSoft' in line: + self.generator = 'eagle' if line.strip().startswith('NNAME'): name, *value = line.strip().split() value = ' '.join(value) - warnings.warn('File contains non-standard Allegro-style net name alias definitions in comments.') - self.net_names[name] = value + self.warn('File contains non-standard Allegro-style net name alias definitions in comments.') + if self.generator == 'allegro': + # it's amazing how allegro always seems to have found a way to do the same thing everyone else is + # doing just in a different, slightly more messed up, completely incompatible way. + self.net_names[name] = value[5:] # strip NNAME because Allegro + + else: + self.net_names[name] = value else: self.comments.append(line) @@ -294,7 +328,11 @@ class NetlistParser(object): raise SyntaxError(f'Unsupported IPC-356 netlist unit specification "{line}"') elif name.startswith('NNAME'): - self.net_names[name] = value + if self.generator == 'allegro': + self.net_names[name] = value[5:] + + else: + self.net_names[name] = value else: self.params[name] = value @@ -319,10 +357,10 @@ class NetlistParser(object): elif line[0:3] == '389': self.assert_unit() - self.outlines.append(Outline.parse(line, self.settings)) + self.outlines.extend(Outline.parse(line, self.settings)) else: - warnings.warn(f'Unknown IPC-356 record type {line[0:3]}') + self.warn(f'Unknown IPC-356 record type {line[0:3]}') class PadType(Enum): @@ -345,6 +383,7 @@ class TestRecord: __test__ = False # tell pytest to ignore this class pad_type : PadType = None net_name : str = None + is_connected : bool = True # None, True or False. ref_des : str = None # part reference designator, e.g. "C1" or "U69" is_via : bool = False pin_num : int = None @@ -356,10 +395,11 @@ class TestRecord: y : float = None w : float = None h : float = None - rotation : float = None + rotation : float = 0 solder_mask : SoldermaskInfo = None lefover : str = None - unit: KW_ONLY = None + _ : KW_ONLY + unit : LengthUnit = None def __str__(self): x = self.unit.format(self.x) @@ -373,9 +413,23 @@ class TestRecord: obj.unit = settings.unit obj.pad_type = PadType(int(line[1])) + net_name = line[3:17].strip() or None - obj.net_name = net_name_map.get(net_name, net_name) - obj.ref_des = line[20:26].strip() or None + if net_name == 'N/C': + obj.net_name = None + obj.is_connected = False + else: + obj.net_name = net_name_map.get(net_name, net_name) + obj.is_connected = True + + ref_des = line[20:26].strip() or None + if ref_des == 'VIA': + obj.is_via = True + obj.ref_des = None + else: + obj.is_via = False + obj.ref_des = ref_des + obj.pin = line[27:31].strip() or None if line[31] == 'M': @@ -395,21 +449,26 @@ class TestRecord: if line[62] == 'Y': obj.h = settings.parse_ipc_length(line[63:67]) if line[67] == 'R': - obj.h = math.radians(int(line[68:71])) + obj.rotation = math.radians(int(line[68:71])) + else: + obj.rotation = 0 if line[72] == 'S': obj.solder_mask = SoldermaskInfo(int(line[73])) obj.leftover = line[74:].strip() or None return obj - def format(self, settings, net_name_map): + def format(self, settings, net_name_map={}): x = settings.unit(self.x, self.unit) y = settings.unit(self.y, self.unit) w = settings.unit(self.w, self.unit) h = settings.unit(self.h, self.unit) # TODO: raise warning if any string is too long ref_des = 'VIA' if self.is_via else (self.ref_des or '') - net_name = net_name_map.get(self.net_name, self.net_name) + if self.is_connected: + net_name = net_name_map.get(self.net_name, self.net_name) + else: + net_name = 'N/C' yield ''.join(( '3', @@ -427,7 +486,7 @@ class TestRecord: settings.format_ipc_length(self.x, 6, 'X', self.unit, sign=True), settings.format_ipc_length(self.y, 6, 'Y', self.unit, sign=True), settings.format_ipc_length(self.w, 4, 'X', self.unit), - settings.format_ipc_length(self.y, 4, 'Y', self.unit), + settings.format_ipc_length(self.h, 4, 'Y', self.unit), settings.format_ipc_number(math.degrees(self.rotation) if self.rotation is not None else None, 3, 'R'), ' ', settings.format_ipc_number(self.solder_mask, 1, 'S'), @@ -445,7 +504,7 @@ def parse_coord_chain(line, settings): for segment in line.split('*'): coords = [] for coord in segment.strip().split(): - if not (m := re.match(r'(X[+-]?[0-9]+)?(Y[+-]?[0-9]+)?', coord)): + if not (match := re.match(r'(X[+-]?[0-9]+)?(Y[+-]?[0-9]+)?', coord)): raise SyntaxError(f'Invalid IPC-356 coordinate {coord}') x = settings.parse_ipc_length(match[1], x) @@ -457,17 +516,17 @@ def parse_coord_chain(line, settings): coords.append((x, y)) yield coords -def format_coord_chain(line, settings, coords, cont): +def format_coord_chain(line, settings, coords, cont, unit): for x, y in coords: - coord = settings.format_ipc_length(x, 6, 'X', unit=self.unit, sign=True) - coord += settings.format_ipc_length(y, 6, 'Y', unit=self.unit, sign=True) + coord = settings.format_ipc_length(x, 6, 'X', unit=unit, sign=True) + coord += settings.format_ipc_length(y, 6, 'Y', unit=unit, sign=True) if len(line) + len(coord) <= 80: - line += coord + line = (line + coord + ' ')[:80] else: yield line - line = f'{cont} {coord}' + line = f'{cont} {coord} ' yield line @@ -475,17 +534,20 @@ def format_coord_chain(line, settings, coords, cont): class Outline: outline_type : OutlineType outline : [(float,)] - unit : KW_ONLY + _ : KW_ONLY + unit : LengthUnit = None @classmethod def parse(kls, line, settings): + print('parsing outline', line) outline_type = OutlineType[line[3:17].strip()] for outline in parse_coord_chain(line[22:], settings): + print(' ->', outline) yield kls(outline_type, outline, unit=settings.unit) def format(self, settings): line = f'389{self.outline_type.name:<14} ' - yield from format_coord_chain(line, settings, self.outline, '089') + yield from format_coord_chain(line, settings, self.outline, '089', self.unit) def __str__(self): return f'' @@ -497,7 +559,8 @@ class Conductor: layer : int aperture : (float,) coords : [(float,)] - unit : KW_ONLY + _ : KW_ONLY + unit : LengthUnit = None @classmethod def parse(kls, line, settings, net_name_map={}): @@ -520,7 +583,7 @@ class Conductor: net_name = net_name_map.get(self.net_name, self.net_name) net_name = f'{net_name:<14}[:14]' line = f'378{net_name} L{self.layer:02d} ' - yield from format_coord_chain(line, settings, self.outline, '078') + yield from format_coord_chain(line, settings, self.outline, '078', self.unit) def __str__(self): return f'' -- cgit