################################################################################ # # # This file is part of the Buildbotics firmware. # # # # Copyright (c) 2015 - 2018, Buildbotics LLC # # All rights reserved. # # # # This file ("the software") is free software: you can redistribute it # # and/or modify it under the terms of the GNU General Public License, # # version 2 as published by the Free Software Foundation. You should # # have received a copy of the GNU General Public License, version 2 # # along with the software. If not, see . # # # # The software is distributed in the hope that it will be useful, but # # WITHOUT ANY WARRANTY; without even the implied warranty of # # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # # Lesser General Public License for more details. # # # # You should have received a copy of the GNU Lesser General Public # # License along with the software. If not, see # # . # # # # For information regarding this software email: # # "Joseph Coffland" # # # ################################################################################ import os import json import pkg_resources from pkg_resources import Requirement, resource_filename def get_resource(path): return resource_filename(Requirement.parse('bbctrl'), 'bbctrl/' + path) class Config(object): def __init__(self, ctrl): self.ctrl = ctrl self.log = ctrl.log.get('Config') self.values = {} try: self.version = "1.6.7" # Load config template with open(get_resource('http/config-template.json'), 'r', encoding = 'utf-8') as f: self.template = json.load(f) except Exception: self.log.exception('Internal error: Failed to load config template') def load(self): path = self.ctrl.get_path('config.json') try: if os.path.exists(path): with open(path, 'r') as f: config = json.load(f) else: config = {'version': self.version} try: self._upgrade(config) except Exception: self.log.exception('Internal error: Failed to upgrade config') except Exception as e: self.log.warning('%s', e) config = {'version': self.version} self._defaults(config) return config def reload(self): self._update(self.load(), True) def get(self, name, default = None): return self.values.get(name, default) def set(self, name, default = None): path = self.ctrl.get_path('config.json') try: if os.path.exists(path): with open(path, 'r') as f: config_data = json.load(f) else: config_data = {'version': self.version} if name in config_data: existing_value = config_data[name] if isinstance(existing_value, dict) and isinstance(default, dict): config_data[name] = {**existing_value, **default} # existing_value.update(default) elif isinstance(existing_value, list) and isinstance(default, list): config_data[name].extend(default) elif isinstance(existing_value, list): config_data[name].append(default) else: config_data[name] = default else: config_data[name] = default self.save(config_data) except Exception: self.log.exception('Internal error: Failed to upgrade config') def save(self, config): self._upgrade(config) self._update(config, False) with open(self.ctrl.get_path('config.json'), 'w') as f: json.dump(config, f, indent=2) os.sync() self.ctrl.preplanner.invalidate_all() self.log.info('Saved') def reset(self): if os.path.exists('config.json'): os.unlink('config.json') self.reload() self.ctrl.preplanner.invalidate_all() def _valid_value(self, template, value): type = template['type'] try: if type == 'int': value = int(value) if type == 'float': value = float(value) if type == 'text': value = str(value) if type == 'bool': value = bool(value) except: return False if 'values' in template and value not in template['values']: return False return True def __defaults(self, config, name, template): if 'type' in template: if (not name in config or not self._valid_value(template, config[name])): config[name] = template['default'] elif 'max' in template and template['max'] < config[name]: config[name] = template['max'] elif 'min' in template and config[name] < template['min']: config[name] = template['min'] if template['type'] == 'list': if 'index' in template: config = config[name] for i in range(len(template['index'])): if len(config) <= i: config.append({}) for name, tmpl in template['template'].items(): self.__defaults(config[i], name, tmpl) else: for name, tmpl in template.items(): self.__defaults(config, name, tmpl) def _defaults(self, config): for name, tmpl in self.template.items(): if not 'type' in tmpl: if not name in config: config[name] = {} conf = config[name] else: conf = config self.__defaults(conf, name, tmpl) def _upgrade(self, config): version = config['version'] version = version.split('b')[0] # Strip off any "beta" suffix version = version.split('-')[0] version = tuple(map(int, version.split('.'))) # Break it into a tuple of integers if version < (1, 0, 7): config['settings']['max-deviation'] = 0.001 config['settings']['junction-accel'] = 200000 config['settings']['probing-prompts'] = True config['probe']['probe-fast-seek'] = 75 config['probe']['probe-slow-seek'] = 25 for motor in config['motors']: motor['stall-microstep'] = 8 motor['stall-current'] = 1 motor['idle-current'] = 1 motor['max-accel'] = 750 motor['latch-backoff'] = 5 if motor['axis'] == 'X' or motor['axis'] == 'Y': motor['search-velocity'] = 1.688 motor['max-velocity'] = 10 motor['max-jerk'] = 1000 motor['zero-backoff'] = 1.5 if motor['axis'] == 'Z': motor['search-velocity'] = 0.675 motor['max-velocity'] = 3 motor['max-jerk'] = 1000 motor['zero-backoff'] = 1 if version < (1, 0, 8): config['settings']['max-deviation'] = 0.05 config['settings']['junction-accel'] = 200000 if version < (1, 0, 9): with open(get_resource('http/onefinity_defaults.json'), 'r', encoding = 'utf-8') as f: defaults = json.load(f) config['selected-tool-settings'] = defaults['selected-tool-settings']; # Auxiliary axis nomenclature: rename W -> A in macro names and # filenames. The auxcnc-driven stepper has been integrated into # gplan as A since the option-b migration; old configs may # still carry W Down/W Up macro entries pointing at # w_down.nc/w_up.nc which were renamed on disk to a_down.nc / # a_up.nc. Migrate idempotently on every load so a stale # in-memory copy can never reintroduce the old names. macros = config.get('macros') if isinstance(config, dict) else None if isinstance(macros, list): renames = { 'w_down.nc': 'a_down.nc', 'w_up.nc': 'a_up.nc', } display_renames = { 'W Down': 'A Down', 'W Up': 'A Up', } for m in macros: if not isinstance(m, dict): continue fn = m.get('file_name') if isinstance(fn, str) and fn in renames: m['file_name'] = renames[fn] nm = m.get('name') if isinstance(nm, str) and nm in display_renames: m['name'] = display_renames[nm] config['version'] = self.version.split('b')[0] config['full_version'] = self.version def _encode(self, name, index, config, tmpl, with_defaults): # Handle category if not 'type' in tmpl: for name, entry in tmpl.items(): if 'type' in entry and config is not None: conf = config.get(name, None) else: conf = config self._encode(name, index, conf, entry, with_defaults) return # Handle defaults if config is not None: value = config elif with_defaults: value = tmpl['default'] else: return # Handle list if tmpl['type'] == 'list': if 'index' in tmpl: for i in range(len(tmpl['index'])): if config is not None and i < len(config): conf = config[i] else: conf = None self._encode(name, index + tmpl['index'][i], conf, tmpl['template'], with_defaults) else: self.values[name]=value self.ctrl.state.config(name,value) return # Update config values if index: if not name in self.values: self.values[name] = {} self.values[name][index] = value else: self.values[name] = value # Update state variable if not 'code' in tmpl: return if tmpl['type'] == 'enum': if value in tmpl['values']: value = tmpl['values'].index(value) else: value = tmpl['default'] elif tmpl['type'] == 'bool': value = 1 if value else 0 elif tmpl['type'] == 'percent': value /= 100.0 self.ctrl.state.config(index + tmpl['code'], value) def _update(self, config, with_defaults): for name, tmpl in self.template.items(): conf = config.get(name, None) self._encode(name, '', conf, tmpl, with_defaults)