summaryrefslogtreecommitdiff
path: root/gerbonara/excellon.py
diff options
context:
space:
mode:
authorjaseg <git@jaseg.de>2022-02-06 01:10:17 +0100
committerjaseg <git@jaseg.de>2022-02-06 01:10:17 +0100
commit263617ca754fc7b3fc550a33934fcbcbe5ef8acf (patch)
tree48bee309f4311a030cae5ed518d6499a93edfb8b /gerbonara/excellon.py
parent5c4e18031623207b31855ff58be6b4c907962474 (diff)
downloadgerbonara-263617ca754fc7b3fc550a33934fcbcbe5ef8acf.tar.gz
gerbonara-263617ca754fc7b3fc550a33934fcbcbe5ef8acf.tar.bz2
gerbonara-263617ca754fc7b3fc550a33934fcbcbe5ef8acf.zip
Add Zuken tests and fix parsing
Diffstat (limited to 'gerbonara/excellon.py')
-rwxr-xr-xgerbonara/excellon.py93
1 files changed, 64 insertions, 29 deletions
diff --git a/gerbonara/excellon.py b/gerbonara/excellon.py
index 8b8744f..5f0db44 100755
--- a/gerbonara/excellon.py
+++ b/gerbonara/excellon.py
@@ -161,6 +161,30 @@ def parse_allegro_logfile(data):
found_tools[index1] = ExcellonTool(diameter=diameter, plated=is_plated, unit=unit)
return found_tools
+def parse_zuken_logfile(data):
+ lines = [ line.strip() for line in data.splitlines() ]
+ if '***** DRILL LIST *****' not in lines:
+ return # likely not a Zuken CR-8000 logfile
+
+ params = {}
+ for line in lines:
+ key, colon, value = line.partition(':')
+ if colon and value:
+ params[key.strip()] = value.strip()
+
+ if not (fmt := params.get('Coordinate Format')):
+ return None
+
+ integer, _, decimal = fmt.partition('V')
+ settings = FileSettings(number_format=(int(integer), int(decimal)))
+
+ if (supp := params.get('Zero Suppress')):
+ supp, _1, _2 = supp.partition(' ')
+ settings.zeros = supp.lower()
+
+ return settings
+
+
class ExcellonFile(CamFile):
""" Excellon drill file.
@@ -253,7 +277,7 @@ class ExcellonFile(CamFile):
self.import_settings = None
@classmethod
- def open(kls, filename, plated=None, settings=None):
+ def open(kls, filename, plated=None, settings=None, external_tools=None):
""" Load an Excellon file from the file system.
Certain CAD tools do not put any information on decimal points into the actual excellon file, and instead put
@@ -272,36 +296,46 @@ class ExcellonFile(CamFile):
"""
filename = Path(filename)
- logfile_tools = None
+ external_tools = None
- # Parse allegro parameter files.
- # Prefer nc_param.txt over ncparam.log since the txt is the machine-readable one.
if settings is None:
+ # Parse allegro parameter files for settings.
+ # Prefer nc_param.txt over ncparam.log since the txt is the machine-readable one.
for fn in 'nc_param.txt', 'ncdrill.log':
if (param_file := filename.parent / fn).is_file():
settings = parse_allegro_ncparam(param_file.read_text())
warnings.warn(f'Loaded allegro-style excellon settings file {param_file}')
break
+ # Parse Zuken log file for settings
+ if filename.name.endswith('.fdr'):
+ logfile = filename.with_suffix('.fdl')
+ if logfile.is_file():
+ settings = parse_zuken_logfile(logfile.read_text())
+ warnings.warn(f'Loaded zuken-style excellon log file {logfile}: {settings}')
+
+ if external_tools is None:
+ # Parse allegro log files for tools.
# TODO add try/except aronud this
log_file = filename.parent / 'ncdrill.log'
if log_file.is_file():
- logfile_tools = parse_allegro_logfile(log_file.read_text())
+ external_tools = parse_allegro_logfile(log_file.read_text())
+
return kls.from_string(filename.read_text(), settings=settings,
- filename=filename, plated=plated, logfile_tools=logfile_tools)
+ filename=filename, plated=plated, external_tools=external_tools)
@classmethod
- def from_string(kls, data, settings=None, filename=None, plated=None, logfile_tools=None):
+ def from_string(kls, data, settings=None, filename=None, plated=None, external_tools=None):
""" Parse the given string as an Excellon file. Note that often, Excellon files do not contain any information
on which number format (integer/decimal places, zeros suppression) is used. In case Gerbonara cannot determine
this with certainty, this function *will* error out. Use :py:meth:`~.ExcellonFile.open` if you want Gerbonara to
parse this metadata from the non-standardized text files many CAD packages produce in addition to drill files.
"""
- parser = ExcellonParser(settings, logfile_tools=logfile_tools)
+ parser = ExcellonParser(settings, external_tools=external_tools)
parser.do_parse(data, filename=filename)
- return kls(objects=parser.objects, comments=parser.comments, import_settings=settings,
+ return kls(objects=parser.objects, comments=parser.comments, import_settings=parser.settings,
generator_hints=parser.generator_hints, original_path=filename)
def _generate_statements(self, settings, drop_comments=True):
@@ -478,7 +512,7 @@ class ProgramState(Enum):
class ExcellonParser(object):
""" Internal helper class that contains all the actual Excellon format parsing logic. """
- def __init__(self, settings=None, logfile_tools=None):
+ def __init__(self, settings=None, external_tools=None):
# NOTE XNC files do not contain an explicit number format specification, but all values have decimal points.
# Thus, we set the default number format to (None, None). If the file does not contain an explicit specification
# and FileSettings.parse_gerber_value encounters a number without an explicit decimal point, it will throw a
@@ -500,7 +534,7 @@ class ExcellonParser(object):
self.generator_hints = []
self.lineno = None
self.filename = None
- self.logfile_tools = logfile_tools or {}
+ self.external_tools = external_tools or {}
def warn(self, msg):
warnings.warn(f'{self.filename}:{self.lineno} "{self.line}": {msg}', SyntaxWarning)
@@ -610,11 +644,11 @@ class ExcellonParser(object):
if index == 0: # T0 is used as END marker, just ignore
return
elif index not in self.tools:
- if not self.tools and index in self.logfile_tools:
+ if not self.tools and index in self.external_tools:
# allegro is just wonderful.
self.warn(f'Undefined tool index {index} selected. We found an allegro drill log file next to this, so '
'we will use tool definitions from there.')
- self.active_tool = self.logfile_tools[index]
+ self.active_tool = self.external_tools[index]
else:
raise SyntaxError(f'Undefined tool index {index} selected.')
@@ -670,15 +704,6 @@ class ExcellonParser(object):
def handle_end_header(self, match):
self.program_state = ProgramState.DRILLING
- @exprs.match('M00')
- def handle_next_tool(self, match):
- #FIXME is this correct? Shouldn't this be "end of program"?
- if self.active_tool:
- self.active_tool = self.tools[self.tools.index(self.active_tool) + 1]
-
- else:
- self.warn('M00 statement found before first tool selection statement.')
-
@exprs.match('M15')
def handle_drill_down(self, match):
self.drill_down = True
@@ -688,14 +713,16 @@ class ExcellonParser(object):
self.drill_down = False
- @exprs.match('M30')
+ @exprs.match('M30|M00')
def handle_end_of_program(self, match):
if self.program_state in (None, ProgramState.HEADER):
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.
+ if match[0] == 'M00':
+ self.generator_hints.append('zuken')
+
def do_move(self, match=None, x='X', y='Y'):
x = self.settings.parse_gerber_value(match['X'])
y = self.settings.parse_gerber_value(match['Y'])
@@ -839,11 +866,15 @@ class ExcellonParser(object):
@exprs.match('(FMAT|VER),?([0-9]*)')
def handle_command_format(self, match):
- # We do not support integer/fractional decimals specification via FMAT because that's stupid. If you need this,
- # please raise an issue on our issue tracker, provide a sample file and tell us where on earth you found that
- # file.
- if match[2] not in ('', '2'):
- raise SyntaxError(f'Unsupported FMAT format version {match["version"]}')
+ if match[1] == 'FMAT':
+ # We do not support integer/fractional decimals specification via FMAT because that's stupid. If you need this,
+ # please raise an issue on our issue tracker, provide a sample file and tell us where on earth you found that
+ # file.
+ if match[2] not in ('', '2'):
+ raise SyntaxError(f'Unsupported FMAT format version {match[2]}')
+
+ else: # VER
+ self.generator_hints.append('zuken')
@exprs.match(r'G40|G41|G42|F[0-9]+')
def handle_unhandled(self, match):
@@ -880,6 +911,10 @@ class ExcellonParser(object):
else:
self.warn('Bare coordinate after end of file')
+ @exprs.match(r'DETECT,ON|ATC,ON|M06')
+ def parse_zuken_legacy_statements(self, match):
+ self.generator_hints.append('zuken')
+
@exprs.match(r'; Format\s*: ([0-9]+\.[0-9]+) / (Absolute|Incremental) / (Inch|MM) / (Leading|Trailing)')
def parse_siemens_format(self, match):
x, _, y = match[1].partition('.')