summaryrefslogtreecommitdiff
path: root/gerbonara/gerber/excellon.py
diff options
context:
space:
mode:
authorjaseg <git@jaseg.de>2022-01-24 14:19:01 +0100
committerjaseg <git@jaseg.de>2022-01-24 14:19:01 +0100
commita1c1d6d971257f03f1926db9dc44e659d2773d2d (patch)
tree443af73413f52b47fbbddf185bb497fb483c93c0 /gerbonara/gerber/excellon.py
parent4a6d76c557caf7263ab57e5fe840d28aa3356621 (diff)
downloadgerbonara-a1c1d6d971257f03f1926db9dc44e659d2773d2d.tar.gz
gerbonara-a1c1d6d971257f03f1926db9dc44e659d2773d2d.tar.bz2
gerbonara-a1c1d6d971257f03f1926db9dc44e659d2773d2d.zip
Add more tests
Diffstat (limited to 'gerbonara/gerber/excellon.py')
-rwxr-xr-xgerbonara/gerber/excellon.py73
1 files changed, 42 insertions, 31 deletions
diff --git a/gerbonara/gerber/excellon.py b/gerbonara/gerber/excellon.py
index 02e63d7..3931660 100755
--- a/gerbonara/gerber/excellon.py
+++ b/gerbonara/gerber/excellon.py
@@ -52,10 +52,14 @@ class ExcellonContext:
def route_mode(self, unit, x, y):
x, y = self.settings.unit(x, unit), self.settings.unit(y, unit)
- if self.mode == ProgramState.ROUTING and (self.x, self.y) == (x, y):
- return # nothing to do
+ if self.mode == ProgramState.ROUTING:
+ if (self.x, self.y) == (x, y):
+ return # nothing to do
+ else:
+ yield 'M16' # drill up
yield 'G00' + 'X' + self.settings.write_excellon_value(x) + 'Y' + self.settings.write_excellon_value(y)
+ yield 'M15' # drill down
def set_current_point(self, unit, x, y):
self.current_point = self.settings.unit(x, unit), self.settings.unit(y, unit)
@@ -179,13 +183,13 @@ class ExcellonFile(CamFile):
return kls(objects=parser.objects, comments=parser.comments, import_settings=settings,
generator_hints=parser.generator_hints, filename=filename)
- def _generate_statements(self, settings):
+ def _generate_statements(self, settings, drop_comments=True):
yield '; XNC file generated by gerbonara'
- if self.comments:
+ if self.comments and not drop_comments:
yield '; Comments found in original file:'
- for comment in self.comments:
- yield ';' + comment
+ for comment in self.comments:
+ yield ';' + comment
yield 'M48'
yield 'METRIC' if settings.unit == MM else 'INCH'
@@ -219,7 +223,7 @@ class ExcellonFile(CamFile):
yield 'M30'
- def to_excellon(self, settings=None):
+ def to_excellon(self, settings=None, drop_comments=True):
''' Export to Excellon format. This function always generates XNC, which is a well-defined subset of Excellon.
'''
if settings is None:
@@ -229,11 +233,11 @@ class ExcellonFile(CamFile):
settings = FileSettings()
settings.zeros = None
settings.number_format = (3,5)
- return '\n'.join(self._generate_statements(settings))
+ return '\n'.join(self._generate_statements(settings, drop_comments=drop_comments))
- def save(self, filename, settings=None):
+ def save(self, filename, settings=None, drop_comments=True):
with open(filename, 'w') as f:
- f.write(self.to_excellon(settings))
+ f.write(self.to_excellon(settings, drop_comments=drop_comments))
def offset(self, x=0, y=0, unit=MM):
self.objects = [ obj.with_offset(x, y, unit) for obj in self.objects ]
@@ -346,14 +350,20 @@ class ExcellonParser(object):
self.is_plated = None
self.comments = []
self.generator_hints = []
+ self.lineno = None
+ self.filename = None
+
+ def warn(self, msg):
+ warnings.warn(f'{self.filename}:{self.lineno} "{self.line}": {msg}', SyntaxWarning)
def do_parse(self, data, filename=None):
# filename arg is for error messages
- filename = filename or '<unknown>'
+ self.filename = filename = filename or '<unknown>'
leftover = None
for lineno, line in enumerate(data.splitlines(), start=1):
line = line.strip()
+ self.lineno, self.line = lineno, line # for warnings
if not line:
continue
@@ -361,7 +371,7 @@ class ExcellonParser(object):
# Coordinates of G00 and G01 may be on the next line
if line == 'G00' or line == 'G01':
if leftover:
- warnings.warn('Two consecutive G00/G01 commands without coordinates. Ignoring first.', SyntaxWarning)
+ self.warn('Two consecutive G00/G01 commands without coordinates. Ignoring first.')
leftover = line
continue
@@ -370,10 +380,11 @@ class ExcellonParser(object):
leftover = None
if line and self.program_state == ProgramState.FINISHED:
- warnings.warn('Commands found following end of program statement.', SyntaxWarning)
+ self.warn('Commands found following end of program statement.')
# TODO check first command in file is "start of header" command.
try:
+ #print(f'{lineno} "{line}"', end=' ')
if not self.exprs.handle(self, line):
raise ValueError('Unknown excellon statement:', line)
except Exception as e:
@@ -392,7 +403,7 @@ class ExcellonParser(object):
raise SyntaxError('BUG: Allegro excellon tool def has mismatching tool indices. Please file a bug report on our issue tracker and provide this file!')
if index in self.tools:
- warnings.warn('Re-definition of tool index {index}, overwriting old definition.', SyntaxWarning)
+ self.warn('Re-definition of tool index {index}, overwriting old definition.')
# NOTE: We map "optionally" plated holes to plated holes for API simplicity. If you hit a case where that's a
# problem, please raise an issue on our issue tracker, explain why you need this and provide an example file.
@@ -407,9 +418,9 @@ class ExcellonParser(object):
unit = MM
if unit != self.settings.unit:
- warnings.warn('Allegro Excellon drill file tool definitions in {unit.name}, but file parameters say the '
+ self.warn('Allegro Excellon drill file tool definitions in {unit.name}, but file parameters say the '
'file should be in {settings.unit.name}. Please double-check that this is correct, and if it is, '
- 'please raise an issue on our issue tracker.', SyntaxWarning)
+ 'please raise an issue on our issue tracker.')
self.tools[index] = ExcellonTool(diameter=diameter, plated=is_plated, unit=unit)
@@ -420,7 +431,7 @@ class ExcellonParser(object):
tool = ExcellonTool(diameter=float(match['diameter']), unit=unit, plated=self.is_plated)
if (index := int(match['index'])) in self.tools:
- warnings.warn('Re-definition of tool index {index}, overwriting old definition.', SyntaxWarning)
+ self.warn('Re-definition of tool index {index}, overwriting old definition.')
self.tools[index] = tool
self.generator_hints.append('easyeda')
@@ -431,7 +442,7 @@ class ExcellonParser(object):
# not a parser for the type of Excellon files a CAM program sends to the machine.
if (index := int(match[1])) in self.tools:
- warnings.warn('Re-definition of tool index {index}, overwriting old definition.', SyntaxWarning)
+ self.warn('Re-definition of tool index {index}, overwriting old definition.')
params = { m[0]: self.settings.parse_gerber_value(m[1:]) for m in re.findall('[BCFHSTZ][.0-9]+', match[2]) }
@@ -481,9 +492,9 @@ class ExcellonParser(object):
def wrapper(self, *args, **kwargs):
nonlocal name
if self.program_state is None:
- warnings.warn(f'{name} header statement found before start of header', SyntaxWarning)
+ self.warn(f'{name} header statement found before start of header')
elif self.program_state != ProgramState.HEADER:
- warnings.warn(f'{name} header statement found after end of header', SyntaxWarning)
+ self.warn(f'{name} header statement found after end of header')
fun(self, *args, **kwargs)
return wrapper
return wrap
@@ -495,7 +506,7 @@ class ExcellonParser(object):
# of the file.
self.generator_hints.append('fritzing')
elif self.program_state is not None:
- warnings.warn(f'M48 "header start" statement found in the middle of the file, currently in {self.program_state}', SyntaxWarning)
+ self.warn(f'M48 "header start" statement found in the middle of the file, currently in {self.program_state}')
self.program_state = ProgramState.HEADER
@exprs.match('M95')
@@ -510,7 +521,7 @@ class ExcellonParser(object):
self.active_tool = self.tools[self.tools.index(self.active_tool) + 1]
else:
- warnings.warn('M00 statement found before first tool selection statement.', SyntaxWarning)
+ self.warn('M00 statement found before first tool selection statement.')
@exprs.match('M15')
def handle_drill_down(self, match):
@@ -524,7 +535,7 @@ class ExcellonParser(object):
@exprs.match('M30')
def handle_end_of_program(self, match):
if self.program_state in (None, ProgramState.HEADER):
- warnings.warn('M30 statement found before end of header.', SyntaxWarning)
+ self.warn('M30 statement found before end of header.')
self.program_state = ProgramState.FINISHED
# ignore.
# TODO: maybe add warning if this is followed by other commands.
@@ -551,7 +562,7 @@ class ExcellonParser(object):
@exprs.match('G00' + xy_coord)
def handle_start_routing(self, match):
if self.program_state is None:
- warnings.warn('Routing mode command found before header.', SyntaxWarning)
+ self.warn('Routing mode command found before header.')
self.program_state = ProgramState.ROUTING
self.do_move(match)
@@ -572,7 +583,7 @@ class ExcellonParser(object):
if self.active_tool:
return self.active_tool
- warnings.warn('Routing command found before first tool definition.', SyntaxWarning)
+ self.warn('Routing command found before first tool definition.')
return None
@exprs.match('(?P<mode>G01|G02|G03)' + xy_coord + coord('A') + coord('I') + coord('J'))
@@ -598,19 +609,19 @@ class ExcellonParser(object):
if self.interpolation_mode == InterpMode.LINEAR:
if a or i or j:
- warnings.warn('A/I/J arc coordinates found in linear mode.', SyntaxWarning)
+ self.warn('A/I/J arc coordinates found in linear mode.')
self.objects.append(Line(*start, *end, self.active_tool, unit=self.settings.unit))
else:
if (x or y) and not (a or i or j):
- warnings.warn('Arc without radius found.', SyntaxWarning)
+ self.warn('Arc without radius found.')
clockwise = (self.interpolation_mode == InterpMode.CIRCULAR_CW)
if a: # radius given
if i or j:
- warnings.warn('Arc without both radius and center specified.', SyntaxWarning)
+ self.warn('Arc without both radius and center specified.')
# Convert endpoint-radius-endpoint notation to endpoint-center-endpoint notation. We always use the
# smaller arc here.
@@ -651,7 +662,7 @@ class ExcellonParser(object):
self.settings.number_format = len(integer), len(fractional)
elif self.settings.number_format == (None, None) and not metric:
- warnings.warn('Using implicit number format from naked "INCH" statement. This is normal for Fritzing, Diptrace, Geda and pcb-rnd.', SyntaxWarning)
+ self.warn('Using implicit number format from naked "INCH" statement. This is normal for Fritzing, Diptrace, Geda and pcb-rnd.')
self.settings.number_format = (2,4)
@exprs.match('G90')
@@ -680,11 +691,11 @@ class ExcellonParser(object):
@exprs.match('G40|G41|G42|{coord("F")}')
def handle_unhandled(self, match):
- warnings.warn(f'{match[0]} excellon command intended for CAM tools found in EDA file.', SyntaxWarning)
+ self.warn(f'{match[0]} excellon command intended for CAM tools found in EDA file.')
@exprs.match(coord('X', 'x1') + coord('Y', 'y1') + 'G85' + coord('X', 'x2') + coord('Y', 'y2'))
def handle_slot_dotted(self, match):
- warnings.warn('Weird G85 excellon slot command used. Please raise an issue on our issue tracker and provide this file for testing.', SyntaxWarning)
+ self.warn('Weird G85 excellon slot command used. Please raise an issue on our issue tracker and provide this file for testing.')
self.do_move(match, 'X1', 'Y1')
start, end = self.do_move(match, 'X2', 'Y2')