diff --git a/configs/Kossel_K25000.cfg b/configs/Kossel_K25000.cfg index ca46c75c..5f1a371d 100644 --- a/configs/Kossel_K25000.cfg +++ b/configs/Kossel_K25000.cfg @@ -1,6 +1,7 @@ [System] machine_type = Kossel_K25000 +version = 2.0 [Geometry] # Delta diff --git a/configs/debrew.cfg b/configs/debrew.cfg index d33acc42..3701f436 100644 --- a/configs/debrew.cfg +++ b/configs/debrew.cfg @@ -1,3 +1,7 @@ +[System] + +version = 2.0 + [Geometry] # Delta axis_config = 3 diff --git a/configs/kossel_mini.cfg b/configs/kossel_mini.cfg index 05b29a1a..4507934b 100644 --- a/configs/kossel_mini.cfg +++ b/configs/kossel_mini.cfg @@ -1,6 +1,7 @@ [System] machine_type = Kossel_mini +version = 2.0 [Geometry] # Delta diff --git a/configs/makerbot_cupcake.cfg b/configs/makerbot_cupcake.cfg index bc952136..c6825565 100644 --- a/configs/makerbot_cupcake.cfg +++ b/configs/makerbot_cupcake.cfg @@ -1,6 +1,7 @@ [System] machine_type = Makerbot_Cupcake +version = 2.0 [Geometry] # 0 - Cartesian diff --git a/configs/maxcorexy.cfg b/configs/maxcorexy.cfg index 120ab533..cc21eed1 100644 --- a/configs/maxcorexy.cfg +++ b/configs/maxcorexy.cfg @@ -1,5 +1,7 @@ [System] +version = 2.0 + [Geometry] # Core-XY axis_config = 2 diff --git a/configs/mendelmax.cfg b/configs/mendelmax.cfg index c0524896..4941de6a 100644 --- a/configs/mendelmax.cfg +++ b/configs/mendelmax.cfg @@ -1,5 +1,7 @@ [System] +version = 2.0 + [Geometry] # Cartesian XY axis_config = 0 diff --git a/configs/prusa_i3.cfg b/configs/prusa_i3.cfg index 0f5762c2..5c425443 100644 --- a/configs/prusa_i3.cfg +++ b/configs/prusa_i3.cfg @@ -1,6 +1,7 @@ [System] machine_type = Prusa_I3 +version = 2.0 [Geometry] offset_x = -0.19 diff --git a/configs/prusa_i3_quad.cfg b/configs/prusa_i3_quad.cfg index 6e656cbd..1547dd77 100644 --- a/configs/prusa_i3_quad.cfg +++ b/configs/prusa_i3_quad.cfg @@ -1,6 +1,7 @@ [System] machine_type = Prusa_I3 +version = 2.0 [Geometry] offset_x = -0.19 diff --git a/configs/rostock_max_v2.cfg b/configs/rostock_max_v2.cfg index 37f5e7d9..b64a69ee 100644 --- a/configs/rostock_max_v2.cfg +++ b/configs/rostock_max_v2.cfg @@ -2,6 +2,7 @@ [System] machine_type = Roskock_Max_v2 +version = 2.0 [Geometry] # Delta diff --git a/configs/series1.cfg b/configs/series1.cfg index 23c77d20..d54d7903 100644 --- a/configs/series1.cfg +++ b/configs/series1.cfg @@ -8,6 +8,7 @@ loglevel = 10 # Machine type is used by M115 # to identify the machine connected. machine_type = Series 1 Pro +version = 2.0 [Geometry] diff --git a/configs/testing_rev_A.cfg b/configs/testing_rev_A.cfg index 2533b449..974f9ef6 100644 --- a/configs/testing_rev_A.cfg +++ b/configs/testing_rev_A.cfg @@ -3,6 +3,7 @@ [System] loglevel = 10 +version = 2.0 [Geometry] axis_config = 0 diff --git a/configs/testing_rev_B.cfg b/configs/testing_rev_B.cfg index 13412c79..0ea90135 100644 --- a/configs/testing_rev_B.cfg +++ b/configs/testing_rev_B.cfg @@ -3,6 +3,7 @@ [System] loglevel = 10 +version = 2.0 [Geometry] axis_config = 0 diff --git a/configs/thing.cfg b/configs/thing.cfg index e7c3fe4c..bf0693ea 100644 --- a/configs/thing.cfg +++ b/configs/thing.cfg @@ -1,5 +1,7 @@ [System] +version = 2.0 + [Geometry] # Thing has H-belt axis_config = 1 diff --git a/configs/thing_delta.cfg b/configs/thing_delta.cfg index d46dbbd1..5ecdbcd1 100644 --- a/configs/thing_delta.cfg +++ b/configs/thing_delta.cfg @@ -1,6 +1,7 @@ [System] machine_type = Thing delta +version = 2.0 [Geometry] # Delta diff --git a/configs/ultimaker_original.cfg b/configs/ultimaker_original.cfg index 295863d9..3d60d69c 100644 --- a/configs/ultimaker_original.cfg +++ b/configs/ultimaker_original.cfg @@ -1,3 +1,7 @@ +[System] + +version = 2.0 + [Geometry] travel_x = 0.2 travel_y = -0.2 diff --git a/redeem/CascadingConfigParser.py b/redeem/CascadingConfigParser.py deleted file mode 100644 index 983f48c9..00000000 --- a/redeem/CascadingConfigParser.py +++ /dev/null @@ -1,189 +0,0 @@ -""" -Author: Elias Bakken -email: elias(dot)bakken(at)gmail(dot)com -Website: http://www.thing-printer.com -License: GNU GPL v3: http://www.gnu.org/copyleft/gpl.html - - Redeem is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Redeem 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 General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Redeem. If not, see . -""" - -import ConfigParser -import os -import logging -import struct - - -class CascadingConfigParser(ConfigParser.SafeConfigParser): - def __init__(self, config_files): - - ConfigParser.SafeConfigParser.__init__(self) - - # Write options in the case it was read. - # self.optionxform = str - - # Parse to real path - self.config_files = [] - for config_file in config_files: - self.config_files.append(os.path.realpath(config_file)) - self.config_location = os.path.dirname(os.path.realpath(config_file)) - - # Parse all config files in list - for config_file in self.config_files: - if os.path.isfile(config_file): - logging.info("Using config file " + config_file) - self.readfp(open(config_file)) - else: - logging.warning("Missing config file " + config_file) - # Might also add command line options for overriding stuff - - def timestamp(self): - """ Get the largest (newest) timestamp for all the config files. """ - ts = 0 - for config_file in self.config_files: - if os.path.isfile(config_file): - ts = max(ts, os.path.getmtime(config_file)) - - printer_cfg = os.path.join(self.config_location, "printer.cfg") - if os.path.islink(printer_cfg): - ts = max(ts, os.lstat(printer_cfg).st_mtime) - return ts - - def parse_capes(self): - """ Read the name and revision of each cape on the BeagleBone """ - self.replicape_revision = None - self.reach_revision = None - - import glob - paths = glob.glob("/sys/bus/i2c/devices/[1-2]-005[4-7]/*/nvmem") - paths.extend(glob.glob("/sys/bus/i2c/devices/[1-2]-005[4-7]/nvmem/at24-[1-4]/nvmem")) - #paths.append(glob.glob("/sys/bus/i2c/devices/[1-2]-005[4-7]/eeprom")) - for i, path in enumerate(paths): - try: - with open(path, "rb") as f: - data = f.read(120) - name = data[58:74].strip() - if name == "BB-BONE-REPLICAP": - self.replicape_revision = data[38:42] - self.replicape_data = data - self.replicape_path = path - elif name[:13] == "BB-BONE-REACH": - self.reach_revision = data[38:42] - self.reach_data = data - self.reach_path = path - if self.replicape_revision != None and self.reach_revision != None: - break - except IOError as e: - pass - return - - def get_default_settings(self): - fs = [] - for config_file in self.config_files: - if os.path.isfile(config_file): - c_file = os.path.basename(config_file) - cp = ConfigParser.SafeConfigParser() - cp.readfp(open(config_file)) - fs.append((c_file, cp)) - - lines = [] - for section in self.sections(): - for option in self.options(section): - for (name, cp) in fs: - if cp.has_option(section, option): - line = [name, section, option, cp.get(section, option)] - lines.append(line) - - return lines - - - def save(self, filename): - """ Save the changed settings to local.cfg """ - current = CascadingConfigParser(self.config_files) - - # Get list of changed values - to_save = [] - for section in self.sections(): - #logging.debug(section) - for option in self.options(section): - if self.get(section, option) != current.get(section, option): - old = current.get(section, option) - val = self.get(section, option) - to_save.append((section, option, val, old)) - - # Update local config with changed values - local = ConfigParser.SafeConfigParser() - local.readfp(open(filename, "r")) - for opt in to_save: - (section, option, value, old) = opt - if not local.has_section(section): - local.add_section(section) - local.set(section, option, value) - logging.info("Update setting: {} from {} to {} ".format(option, old, value)) - - - # Save changed values to file - local.write(open(filename, "w+")) - - - def check(self, filename): - """ Check the settings currently set against default.cfg """ - default = ConfigParser.SafeConfigParser() - default.readfp(open(os.path.join(self.config_location, "default.cfg"))) - local = ConfigParser.SafeConfigParser() - local.readfp(open(filename)) - - local_ok = True - diff = set(local.sections())-set(default.sections()) - for section in diff: - logging.warning("Section {} does not exist in {}".format(section, "default.cfg")) - local_ok = False - for section in local.sections(): - if not default.has_section(section): - continue - diff = set(local.options(section))-set(default.options(section)) - for option in diff: - logging.warning("Option {} in section {} does not exist in {}".format(option, section, "default.cfg")) - local_ok = False - if local_ok: - logging.info("{} is OK".format(filename)) - else: - logging.warning("{} contains errors.".format(filename)) - return local_ok - - def get_key(self): - """ Get the generated key from the config or create one """ - self.replicape_key = "".join(struct.unpack('20c', self.replicape_data[100:120])) - logging.debug("Found Replicape key: '"+self.replicape_key+"'") - if self.replicape_key == '\x00'*20: - logging.debug("Replicape key invalid") - import random - import string - self.replicape_key = ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(20)) - self.replicape_data = self.replicape_data[:100] + self.replicape_key - logging.debug("New Replicape key: '"+self.replicape_key+"'") - #logging.debug("".join(struct.unpack('20c', self.new_replicape_data[100:120]))) - try: - with open(self.replicape_path, "wb") as f: - f.write(self.replicape_data[:120]) - except IOError as e: - logging.warning("Unable to write new key to EEPROM") - return self.replicape_key - - -if __name__ == '__main__': - logging.basicConfig(level=logging.DEBUG, - format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s', - datefmt='%m-%d %H:%M') - c = CascadingConfigParser(["/etc/redeem/default.cfg", "/etc/redeem/printer.cfg", "/etc/redeem/local.cfg"]) - print(c.get_default_settings()) diff --git a/redeem/Printer.py b/redeem/Printer.py index 88bc5b12..b9bdc455 100755 --- a/redeem/Printer.py +++ b/redeem/Printer.py @@ -26,10 +26,11 @@ from Delta import Delta from PruInterface import PruInterface from SDCardManager import SDCardManager -import os import json from six import iteritems +from configuration.RedeemConfig import RedeemConfig + class Printer: AXES = "XYZEHABC" @@ -108,6 +109,11 @@ def __init__(self): self.sd_card_manager = SDCardManager() + # default. should be initialized later + # TODO : these should be passed into from constructor + self.config = RedeemConfig() + self.replicape_key = None + def add_slave(self, master, slave): ''' Make an axis copy the movement of another. the slave will get the same position as the axis''' diff --git a/redeem/Redeem.py b/redeem/Redeem.py index 4901e53b..e96fbbbd 100755 --- a/redeem/Redeem.py +++ b/redeem/Redeem.py @@ -52,7 +52,6 @@ from Gcode import Gcode from ColdEnd import ColdEnd from PruFirmware import PruFirmware -from CascadingConfigParser import CascadingConfigParser from Printer import Printer from GCodeProcessor import GCodeProcessor from PluginsController import PluginsController @@ -69,6 +68,9 @@ from _version import __version__, __release_name__ # Global vars +from redeem.configuration import get_config_factory +from redeem.configuration.factories.ConfigFactoryV20 import ConfigFactoryV20 + printer = None # Default logging level is set to debug @@ -101,12 +103,6 @@ def __init__(self, config_location="/etc/redeem"): Alarm.executor = AlarmExecutor() alarm = Alarm(Alarm.ALARM_TEST, "Alarm framework operational") - # check for config files - file_path = os.path.join(config_location,"default.cfg") - if not os.path.exists(file_path): - logging.error(file_path + " does not exist, this file is required for operation") - sys.exit() # maybe use something more graceful? - local_path = os.path.join(config_location,"local.cfg") if not os.path.exists(local_path): logging.info(local_path + " does not exist, Creating one") @@ -114,16 +110,13 @@ def __init__(self, config_location="/etc/redeem"): os.chmod(local_path, 0o777) # Parse the config files. - printer.config = CascadingConfigParser( - [os.path.join(config_location,'default.cfg'), - os.path.join(config_location,'printer.cfg'), - os.path.join(config_location,'local.cfg')]) - - # Check the local and printer files - printer_path = os.path.join(config_location,"printer.cfg") - if os.path.exists(printer_path): - printer.config.check(printer_path) - printer.config.check(os.path.join(config_location,'local.cfg')) + config_files = [ + os.path.join(config_location, 'printer.cfg'), + os.path.join(config_location, 'local.cfg') + ] + + config_factory = get_config_factory(config_files) + printer.config = config_factory.hydrate_config(config_files=config_files) # Get the revision and loglevel from the Config file level = self.printer.config.getint('System', 'loglevel') diff --git a/redeem/configuration/RedeemConfig.py b/redeem/configuration/RedeemConfig.py new file mode 100644 index 00000000..9451e5b9 --- /dev/null +++ b/redeem/configuration/RedeemConfig.py @@ -0,0 +1,149 @@ +import struct +import logging +import random +import string + +from redeem.configuration.exceptions import InvalidConfigSectionException +from redeem.configuration.utils import clean_key as _ + +from redeem.configuration.sections.alarms import AlarmsConfig +from redeem.configuration.sections.coldends import ColdendsConfig +from redeem.configuration.sections.delta import DeltaConfig +from redeem.configuration.sections.endstops import EndstopsConfig +from redeem.configuration.sections.fans import FansConfig +from redeem.configuration.sections.filamentsensors import FilamentSensorsConfig +from redeem.configuration.sections.geometry import GeometryConfig +from redeem.configuration.sections.heaters import HeatersConfig +from redeem.configuration.sections.homing import HomingConfig +from redeem.configuration.sections.macros import MacrosConfig +from redeem.configuration.sections.planner import PlannerConfig +from redeem.configuration.sections.plugins import HPX2MaxPluginConfig, DualServoPluginConfig +from redeem.configuration.sections.probe import ProbeConfig +from redeem.configuration.sections.rotaryencoders import RotaryEncodersConfig +from redeem.configuration.sections.servos import ServosConfig +from redeem.configuration.sections.steppers import SteppersConfig +from redeem.configuration.sections.system import SystemConfig +from redeem.configuration.sections.watchdog import WatchdogConfig + + +class RedeemConfig(object): + """match the 'interface' for the ConfigParser to minimize the need + to make changes where `printer.config` is accessed in the code base. + """ + + alarms = AlarmsConfig() + cold_ends = ColdendsConfig() + delta = DeltaConfig() + endstops = EndstopsConfig() + fans = FansConfig() + filament_sensors = FilamentSensorsConfig() + geometry = GeometryConfig() + heaters = HeatersConfig() + homing = HomingConfig() + macros = MacrosConfig() + planner = PlannerConfig() + hpx2maxplugin = HPX2MaxPluginConfig() + dualservoplugin = DualServoPluginConfig() + probe = ProbeConfig() + rotary_encoders = RotaryEncodersConfig() + servos = ServosConfig() + steppers = SteppersConfig() + system = SystemConfig() + watchdog = WatchdogConfig() + + replicape_revision = None + replicape_data = None + replicape_path = None + replicape_key = None + + reach_revision = None + reach_data = None + reach_path = None + + def get(self, section, key, default=None): + if hasattr(self, _(section)): + return getattr(self, _(section)).get(key) + return default + + def has(self, section, key): + return hasattr(self, _(section)) and getattr(self, _(section)).has(key) + + # alias + def has_option(self, section, key): + return self.has(section, key) + + def getint(self, section, key, default=None): + if hasattr(self, _(section)): + return getattr(self, _(section)).getint(key) + return default + + def getfloat(self, section, key, default=None): + if hasattr(self, _(section)): + return getattr(self, _(section)).getfloat(key) + return default + + def getboolean(self, section, key, default=None): + if hasattr(self, section.replace('-','_').lower()): + return getattr(self, _(section)).getboolean(key) + return default + + def set(self, section, key, val): + if not hasattr(self, _(section)): + raise InvalidConfigSectionException() + getattr(self, _(section)).set(key, val) + + def parse_capes(self): + """ Read the name and revision of each cape on the BeagleBone """ + self.replicape_revision = None + self.reach_revision = None + + import glob + paths = glob.glob("/sys/bus/i2c/devices/[1-2]-005[4-7]/*/nvmem") + paths.extend(glob.glob("/sys/bus/i2c/devices/[1-2]-005[4-7]/nvmem/at24-[1-4]/nvmem")) + # paths.append(glob.glob("/sys/bus/i2c/devices/[1-2]-005[4-7]/eeprom")) + for i, path in enumerate(paths): + try: + with open(path, "rb") as f: + data = f.read(120) + name = data[58:74].strip() + if name == "BB-BONE-REPLICAP": + self.replicape_revision = data[38:42] + self.replicape_data = data + self.replicape_path = path + elif name[:13] == "BB-BONE-REACH": + self.reach_revision = data[38:42] + self.reach_data = data + self.reach_path = path + if self.replicape_revision is not None and self.reach_revision is not None: + break + except IOError as e: + pass + + def save(self, filename): + raise NotImplemented("not yet implemented") + + def _gen_key(self): + """ Used to generate a key when one is not found """ + return ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(20)) + + def get_key(self): + """ Get the generated key from the config or create one """ + + self.replicape_key = "".join(struct.unpack('20c', self.replicape_data[100:120])) + + logging.debug("Found Replicape key: '"+self.replicape_key+"'") + + if self.replicape_key == '\x00'*20: + logging.debug("Replicape key invalid") + + self.replicape_key = self._gen_key() + self.replicape_data = self.replicape_data[:100] + self.replicape_key + + logging.debug("New Replicape key: '"+self.replicape_key+"'") + + try: + with open(self.replicape_path, "wb") as f: + f.write(self.replicape_data[:120]) + except IOError as e: + logging.warning("Unable to write new key to EEPROM") + return self.replicape_key diff --git a/redeem/configuration/__init__.py b/redeem/configuration/__init__.py new file mode 100644 index 00000000..cefebea3 --- /dev/null +++ b/redeem/configuration/__init__.py @@ -0,0 +1,23 @@ +try: # TODO: remove when migrate to python 3 + from configparser import ConfigParser +except ImportError: + from ConfigParser import RawConfigParser as ConfigParser + +from redeem.configuration.factories.ConfigFactoryV19 import ConfigFactoryV19 +from redeem.configuration.factories.ConfigFactoryV20 import ConfigFactoryV20 + + +def get_config_factory(config_files): + + config_parser = ConfigParser() + config_parser.read(config_files) + + version = None + if config_parser.has_option('System', 'version'): + version = config_parser.get('System', 'version') + + factory = ConfigFactoryV19() + if version == 2.0: + factory = ConfigFactoryV20() + + return factory diff --git a/redeem/configuration/exceptions.py b/redeem/configuration/exceptions.py new file mode 100644 index 00000000..e5155b05 --- /dev/null +++ b/redeem/configuration/exceptions.py @@ -0,0 +1,6 @@ +class InvalidConfigSectionException(Exception): + pass + + +class InvalidConfigOptionException(Exception): + pass diff --git a/redeem/configuration/factories/ConfigFactoryV19.py b/redeem/configuration/factories/ConfigFactoryV19.py new file mode 100644 index 00000000..a76399e1 --- /dev/null +++ b/redeem/configuration/factories/ConfigFactoryV19.py @@ -0,0 +1,157 @@ +import numpy as np + +from configuration import ConfigFactoryV20 +from redeem.configuration.sections.delta import DeltaConfig + + +def _getfloat(config_parser, section, option, default): + if config_parser.has_option(section, option): + return config_parser.getfloat(section, option) + return default + + +def _radiansToDegrees(radians): + return radians * 180 / np.pi + + +class ConfigFactoryV19(ConfigFactoryV20): + + def _calc_old_column_position(self, r, + ae, be, ce, + a_tangential, b_tangential, c_tangential, + a_radial, b_radial, c_radial): + """from https://github.com/intelligent-agent/redeem/blob/5f225ddf3ab806ef8996e1431bbef6c454a60f48/redeem/path_planner/Delta.cpp""" # noqa + + # Column theta + At = np.pi / 2.0 + Bt = 7.0 * np.pi / 6.0 + Ct = 11.0 * np.pi / 6.0 + + # Calculate the column tangential offsets + Apxe = a_tangential # Tower A doesn't require a separate y componen + Apye = 0.00 + Bpxe = b_tangential / 2.0 + Bpye = np.sqrt(3.0)*(-b_tangential/2.0) + Cpxe = np.sqrt(3.0)*(c_tangential/2.0) + Cpye = c_tangential/2.0 + + # Calculate the column positions + Apx = (a_radial + r) * np.cos(At) + Apxe + Apy = (a_radial + r) * np.sin(At) + Apye + Bpx = (b_radial + r) * np.cos(Bt) + Bpxe + Bpy = (b_radial + r) * np.sin(Bt) + Bpye + Cpx = (c_radial + r) * np.cos(Ct) + Cpxe + Cpy = (c_radial + r) * np.sin(Ct) + Cpye + + # Calculate the effector positions + Aex = ae * np.cos(At) + Aey = ae * np.sin(At) + Bex = be * np.cos(Bt) + Bey = be * np.sin(Bt) + Cex = ce * np.cos(Ct) + Cey = ce * np.sin(Ct) + + # Calculate the virtual column positions + Avx = Apx - Aex + Avy = Apy - Aey + Bvx = Bpx - Bex + Bvy = Bpy - Bey + Cvx = Cpx - Cex + Cvy = Cpy - Cey + + return Avx, Avy, Bvx, Bvy, Cvx, Cvy + + def _calc_new_column_position(self, r, Avx, Avy, Bvx, Bvy, Cvx, Cvy): + """ + from new calcs + + + 1) Avx = (A_radial + r)*cos(At); + 2) Avy = (A_radial + r)*sin(At); + + solve for a_radial + 3) (Avx / cos(At)) - r = A_radial + 4) (Avy / sin(At)) - r = A_radial + + set 3 equal to 4 + 5) Avx / cos(At) = Avy / sin(At) + 6) Avx * sin(At) = cos(At) * Avy + 6) Avy / Avx = sin(At) / cos(At) = tan(At) + + solve for At + 7) arctan(Avy/Avx) = At + + a_radial from 7 into 1 + """ + + At = np.arctan(Avy / Avx) + Bt = np.arctan(Bvy / Bvx) + Ct = np.arctan(Cvy / Cvx) + + a_radial = (Avx / np.cos(At)) - r + b_radial = (Bvx / np.cos(Bt)) + r + c_radial = (Cvx / np.cos(Ct)) - r + + '''from new calcs + At = degreesToRadians(90.0 + A_angular) + Bt = degreesToRadians(210.0 + B_angular) + Ct = degreesToRadians(330.0 + C_angular)''' + + # solve for _angular + a_angular = _radiansToDegrees(At) - 90 + b_angular = _radiansToDegrees(Bt) - 30 + c_angular = _radiansToDegrees(Ct) + 30 + + return a_radial, b_radial, c_radial, a_angular, b_angular, c_angular + + def hydrate_deltaconfig(self, config_parser): + """ + + 1.9 used ae, be, ce along with a/b/c_tangential to calculate position of each tower + + 2.0 users angular and radial dimensions + + + also r in 1.9 was just radius to edge of effector, instead of the center as in 2.0 + + """ + cfg = DeltaConfig() + + # if this isn't a Delta config, skip transformations + if config_parser.getint('Geometry', 'axis_config') != 3: + return cfg + + # length of rod same in both + if config_parser.has_option('Delta', 'L'): + cfg.l = config_parser.getfloat('Delta', 'L') + + # radius2.0 = radius1.9 + Ae + if config_parser.has_option('Delta', 'r'): + cfg.r = config_parser.getfloat('Delta', 'r') + + if config_parser.has_option('Delta', 'Ae'): + cfg.r -= config_parser.getfloat('Delta', 'Ae') + + r = _getfloat(config_parser, 'Delta', 'r', 0.0) + ae = _getfloat(config_parser, 'Delta', 'Ae', 0.0) + be = _getfloat(config_parser, 'Delta', 'Be', 0.0) + ce = _getfloat(config_parser, 'Delta', 'Ce', 0.0) + a_radial = _getfloat(config_parser, 'Delta', 'A_radial', 0.0) + b_radial = _getfloat(config_parser, 'Delta', 'B_radial', 0.0) + c_radial = _getfloat(config_parser, 'Delta', 'C_radial', 0.0) + a_tangential = _getfloat(config_parser, 'Delta', 'A_tangential', 0.0) + b_tangential = _getfloat(config_parser, 'Delta', 'B_tangential', 0.0) + c_tangential = _getfloat(config_parser, 'Delta', 'C_tangential', 0.0) + + Avx, Avy, Bvx, Bvy, CvX, Cvy = self._calc_old_column_position(r, + ae, be, ce, + a_tangential, b_tangential, c_tangential, + a_radial, b_radial, c_radial) + + (cfg.a_radial, cfg.b_radial, cfg.c_radial, + cfg.a_angular, cfg.b_angular, cfg.c_angular) = self._calc_new_column_position(cfg.r, + Avx, Avy, + Bvx, Bvy, + CvX, Cvy) + + return cfg diff --git a/redeem/configuration/factories/ConfigFactoryV20.py b/redeem/configuration/factories/ConfigFactoryV20.py new file mode 100644 index 00000000..0045dbcf --- /dev/null +++ b/redeem/configuration/factories/ConfigFactoryV20.py @@ -0,0 +1,5 @@ +from redeem.configuration.factories import ConfigFactory + + +class ConfigFactoryV20(ConfigFactory): + pass diff --git a/redeem/configuration/factories/__init__.py b/redeem/configuration/factories/__init__.py new file mode 100644 index 00000000..a2b8dda4 --- /dev/null +++ b/redeem/configuration/factories/__init__.py @@ -0,0 +1,100 @@ +try: # TODO: remove when migrate to python 3 + from configparser import ConfigParser +except ImportError: + from ConfigParser import SafeConfigParser as ConfigParser + + +from redeem.configuration.RedeemConfig import RedeemConfig +from redeem.configuration.sections.alarms import AlarmsConfig +from redeem.configuration.sections.coldends import ColdendsConfig +from redeem.configuration.sections.delta import DeltaConfig +from redeem.configuration.sections.endstops import EndstopsConfig +from redeem.configuration.sections.fans import FansConfig +from redeem.configuration.sections.filamentsensors import FilamentSensorsConfig +from redeem.configuration.sections.geometry import GeometryConfig +from redeem.configuration.sections.heaters import HeatersConfig +from redeem.configuration.sections.homing import HomingConfig +from redeem.configuration.sections.macros import MacrosConfig +from redeem.configuration.sections.planner import PlannerConfig +from redeem.configuration.sections.plugins import HPX2MaxPluginConfig, DualServoPluginConfig +from redeem.configuration.sections.probe import ProbeConfig +from redeem.configuration.sections.rotaryencoders import RotaryEncodersConfig +from redeem.configuration.sections.servos import ServosConfig +from redeem.configuration.sections.steppers import SteppersConfig +from redeem.configuration.sections.system import SystemConfig +from redeem.configuration.sections.watchdog import WatchdogConfig + +import logging + + +def _clean(key): + return key.replace('-', '_').lower() + + +class ConfigFactory(object): + + sections = { + 'Alarms': AlarmsConfig, + 'Cold-ends': ColdendsConfig, + 'Delta': DeltaConfig, + 'Endstops': EndstopsConfig, + 'Fans': FansConfig, + 'Filament-sensors': FilamentSensorsConfig, + 'Geometry': GeometryConfig, + 'Heaters': HeatersConfig, + 'Homing': HomingConfig, + 'Macros': MacrosConfig, + 'Planner': PlannerConfig, + 'HPX2MaxPlugin': HPX2MaxPluginConfig, + 'DualServoPlugin': DualServoPluginConfig, + 'Probe': ProbeConfig, + 'Rotary-encoders': RotaryEncodersConfig, + 'Servos': ServosConfig, + 'Steppers': SteppersConfig, + 'System': SystemConfig, + 'Watchdog': WatchdogConfig + } + + def __init__(self): + pass + + def hydrate_config(self, config_file=None, config_files=()): + """Use default mapper, unless another one is specified by subclass""" + config_parser = ConfigParser() + + if config_file is not None and len(config_files) > 0: + raise Exception("cannot provide both single and list of config files") + + if config_file: + config_parser.read([config_file, ]) + elif len(config_files) > 0: + num_files = config_parser.read(config_files) + if num_files < len(config_files): + logging.warn("number of files loaded less than provided: {} vs. {}".format(num_files, len(config_files))) + + redeem_config = RedeemConfig() + + for section in config_parser.sections(): + if section not in self.sections.keys(): + logging.warn("[{}] does not match known section".format(section)) + continue + section_cls = self.sections[section] + hydration_name = 'hydrate_' + section_cls.__name__.lower() + if hasattr(self, hydration_name): + config_func = getattr(self, hydration_name) + config = config_func(config_parser) + else: + config = self.hydrate_section_config(config_parser, section, section_cls) + assert(hasattr(redeem_config, _clean(section))) + setattr(redeem_config, _clean(section), config) + return redeem_config + + def hydrate_section_config(self, config_parser, section, config_cls): + """A simple one-to-one mapper from ini to config class""" + config = config_cls() + for option in config_parser.options(section): + if not config.has(option): + logging.warn("[{}] '{}' does not match known option".format(section, option)) + continue + setattr(config, option, config_parser.get(section, option)) + return config diff --git a/redeem/configuration/sections/__init__.py b/redeem/configuration/sections/__init__.py new file mode 100644 index 00000000..e9ba7b6c --- /dev/null +++ b/redeem/configuration/sections/__init__.py @@ -0,0 +1,40 @@ +from redeem.configuration.utils import clean_key as _ +from redeem.configuration.exceptions import InvalidConfigOptionException + + +class BaseConfig(object): + """Superclass of all config 'sections'""" + + def has(self, key): + return hasattr(self, _(key)) + + def get(self, key): + if not hasattr(self, _(key)): + return None + return getattr(self, _(key)) + + def set(self, key, val): + if not hasattr(self, _(key)): + raise InvalidConfigOptionException() + setattr(self, _(key), val) + + def getfloat(self, key): + val = self.get(_(key)) + try: + val = float(val) + except ValueError: + return None + return val + + def getint(self, key): + val = self.get(_(key)) + try: + val = int(val) + except ValueError: + return None + return val + + def getboolean(self, key): + val = self.get(_(key)) + return val in ['True', 'true', True] + diff --git a/redeem/configuration/sections/alarms.py b/redeem/configuration/sections/alarms.py new file mode 100644 index 00000000..0cb6cc2d --- /dev/null +++ b/redeem/configuration/sections/alarms.py @@ -0,0 +1,5 @@ +from redeem.configuration.sections import BaseConfig + + +class AlarmsConfig(BaseConfig): + pass diff --git a/redeem/configuration/sections/coldends.py b/redeem/configuration/sections/coldends.py new file mode 100644 index 00000000..23dd982d --- /dev/null +++ b/redeem/configuration/sections/coldends.py @@ -0,0 +1,45 @@ +from redeem.configuration.sections import BaseConfig + + +class ColdendsConfig(BaseConfig): + # To use the DS18B20 temp sensors, connect them like this, enable by setting to True + connect_ds18b20_0_fan_0 = False + connect_ds18b20_1_fan_0 = False + connect_ds18b20_0_fan_1 = False + + # This list is for connecting thermistors to fans, so they are controlled automatically when reaching 60 degrees. + connect_therm_e_fan_0 = False + connect_therm_e_fan_1 = False + connect_therm_e_fan_2 = False + connect_therm_e_fan_3 = False + connect_therm_h_fan_0 = False + connect_therm_h_fan_1 = False + connect_therm_h_fan_2 = False + connect_therm_h_fan_3 = False + connect_therm_a_fan_0 = False + connect_therm_a_fan_1 = False + connect_therm_a_fan_2 = False + connect_therm_a_fan_3 = False + connect_therm_b_fan_0 = False + connect_therm_b_fan_1 = False + connect_therm_b_fan_2 = False + connect_therm_b_fan_3 = False + connect_therm_c_fan_0 = False + connect_therm_c_fan_1 = False + connect_therm_c_fan_2 = False + connect_therm_c_fan_3 = False + connect_therm_hbp_fan_0 = False + connect_therm_hbp_fan_1 = False + connect_therm_hbp_fan_2 = False + connect_therm_hbp_fan_3 = False + + add_fan_0_to_m106 = False + add_fan_1_to_m106 = False + add_fan_2_to_m106 = False + add_fan_3_to_m106 = False + + cooler_0_target_temp = 60 # If you want coolers to have a different 'keep' temp, list it here. + + # If you want the fan-thermitor connetions to have a + # different temperature: + # therm-e-fan-0-target_temp = 70 \ No newline at end of file diff --git a/redeem/configuration/sections/delta.py b/redeem/configuration/sections/delta.py new file mode 100644 index 00000000..1339b62f --- /dev/null +++ b/redeem/configuration/sections/delta.py @@ -0,0 +1,15 @@ +from redeem.configuration.sections import BaseConfig + + +class DeltaConfig(BaseConfig): + + l = 0.135 # Length of the rod (m) + r = 0.144 # Radius of the columns (m) + + # Compensation for positional error of the columns + # Radial offsets of the columns, positive values move the tower away from the center of the printer (m) + a_radial, b_radial, c_radial = 0.0, 0.0, 0.0 + + # Angular offsets of the columns + # Positive values move the tower counter-clockwise, as seen from above (degrees) + a_angular, b_angular, c_angular = 0.0, 0.0, 0.0 diff --git a/redeem/configuration/sections/endstops.py b/redeem/configuration/sections/endstops.py new file mode 100644 index 00000000..cde24f80 --- /dev/null +++ b/redeem/configuration/sections/endstops.py @@ -0,0 +1,90 @@ +from redeem.configuration.sections import BaseConfig + + +class EndstopsConfig(BaseConfig): + + # Which axis should be homed. + has_x = True + has_y = True + has_z = True + has_e = False + has_h = False + has_a = False + has_b = False + has_c = False + + inputdev = '/dev/input/by-path/platform-ocp:gpio_keys-event' + + # Number of cycles to wait between checking + # end stops. CPU frequency is 200 MHz + end_stop_delay_cycles = 1000 + + # Invert = + # True means endstop is connected as Normally Open (NO) or not connected + # False means endstop is connected as Normally Closed (NC) + invert_x1 = False + invert_x2 = False + invert_y1 = False + invert_y2 = False + invert_z1 = False + invert_z2 = False + + pin_x1 = 'GPIO3_21' + pin_x2 = 'GPIO0_30' + pin_y1 = 'GPIO1_17' + pin_y2 = 'GPIO3_17' + pin_z1 = 'GPIO0_31' + pin_z2 = 'GPIO0_4' + + keycode_x1 = 112 + keycode_x2 = 113 + keycode_y1 = 114 + keycode_y2 = 115 + keycode_z1 = 116 + keycode_z2 = 117 + + # If one endstop is hit, which steppers and directions are masked. + # The list is comma separated and has format + # x_cw = stepper x clockwise (independent of direction_x) + # x_ccw = stepper x counter clockwise (independent of direction_x) + # x_neg = setpper x negative direction (affected by direction_x) + # x_pos = setpper x positive direction (affected by direction_x) + # Steppers e and h (and a, b, c for reach) can also be masked. + # + # For a list of steppers to stop, use this format: x_cw, y_ccw + # For Simple XYZ bot, the usual practice would be + # end_stop_X1_stops = x_neg, end_stop_X2_stops = x_pos, ... + # For CoreXY and similar, two steppers should be stopped if an end stop is hit. + # similarly for a delta probe should stop x, y and z. + end_stop_x1_stops = '' + end_stop_y1_stops = '' + end_stop_z1_stops = '' + end_stop_x2_stops = '' + end_stop_y2_stops = '' + end_stop_z2_stops = '' + + # if an endstop should only be used for homing or probing, then add it to + # homing_only_endstops in comma separated format. + # Example: homing_only_endstops = Z1, Z2 + # this will make sure that endstop Z1 and Z2 are only used during homing or probing + # NOTE: Be very careful with this option. + + homing_only_endstops = '' + + soft_end_stop_min_x = -0.5 + soft_end_stop_min_y = -0.5 + soft_end_stop_min_z = -0.5 + soft_end_stop_min_e = -1000.0 + soft_end_stop_min_h = -1000.0 + soft_end_stop_min_a = -1000.0 + soft_end_stop_min_b = -1000.0 + soft_end_stop_min_c = -1000.0 + + soft_end_stop_max_x = 0.5 + soft_end_stop_max_y = 0.5 + soft_end_stop_max_z = 0.5 + soft_end_stop_max_e = 1000.0 + soft_end_stop_max_h = 1000.0 + soft_end_stop_max_a = 1000.0 + soft_end_stop_max_b = 1000.0 + soft_end_stop_max_c = 1000.0 diff --git a/redeem/configuration/sections/fans.py b/redeem/configuration/sections/fans.py new file mode 100644 index 00000000..486a5003 --- /dev/null +++ b/redeem/configuration/sections/fans.py @@ -0,0 +1,14 @@ +from redeem.configuration.sections import BaseConfig + + +class FansConfig(BaseConfig): + default_fan_0_value = 0.0 + default_fan_1_value = 0.0 + default_fan_2_value = 0.0 + default_fan_3_value = 0.0 + default_fan_4_value = 0.0 + default_fan_5_value = 0.0 + default_fan_6_value = 0.0 + default_fan_7_value = 0.0 + default_fan_8_value = 0.0 + default_fan_9_value = 0.0 diff --git a/redeem/configuration/sections/filamentsensors.py b/redeem/configuration/sections/filamentsensors.py new file mode 100644 index 00000000..95202985 --- /dev/null +++ b/redeem/configuration/sections/filamentsensors.py @@ -0,0 +1,7 @@ +from redeem.configuration.sections import BaseConfig + + +class FilamentSensorsConfig(BaseConfig): + + # If the error is > 1 cm, sound the alarm + alarm_level_e = 0.01 diff --git a/redeem/configuration/sections/geometry.py b/redeem/configuration/sections/geometry.py new file mode 100644 index 00000000..7533f03c --- /dev/null +++ b/redeem/configuration/sections/geometry.py @@ -0,0 +1,17 @@ +from redeem.configuration.sections import BaseConfig + + +class GeometryConfig(BaseConfig): + axis_config = 0 # 0 - Cartesian, 1 - H-belt, 2 - Core XY, 3 - Delta + + # The total length each axis can travel, this affects the homing endstop searching length. + travel_x, travel_y, travel_z = 0.2, 0.2, 0.2 + travel_e, travel_h = 0.2, 0.2 + travel_a, travel_b, travel_c = 0.0, 0.0, 0.0 + + # Define the origin of the build plate in relation to the endstops + offset_x, offset_y, offset_z = 0.0, 0.0, 0.0 + offset_e, offset_h = 0.0, 0.0 + offset_a, offset_b, offset_c = 0.0, 0.0, 0.0 + + bed_compensation_matrix = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]] diff --git a/redeem/configuration/sections/heaters.py b/redeem/configuration/sections/heaters.py new file mode 100644 index 00000000..12ea7609 --- /dev/null +++ b/redeem/configuration/sections/heaters.py @@ -0,0 +1,95 @@ +from redeem.configuration.sections import BaseConfig + + +class HeatersConfig(BaseConfig): + # For list of available temp charts, look in temp_chart.py + + sensor_e = 'B57560G104F' + pid_kp_e = 0.1 + pid_ti_e = 100.0 + pid_td_e = 0.3 + ok_range_e = 4.0 + max_rise_temp_e = 10.0 + max_fall_temp_e = 10.0 + min_temp_e = 20.0 + max_temp_e = 250.0 + path_adc_e = '/sys/bus/iio/devices/iio:device0/in_voltage4_raw' + mosfet_e = 5 + onoff_e = False + prefix_e = 'T0' + max_power_e = 1.0 + + sensor_h = 'B57560G104F' + pid_kp_h = 0.1 + pid_ti_h = 0.01 + pid_td_h = 0.3 + ok_range_h = 4.0 + max_rise_temp_h = 10.0 + max_fall_temp_h = 10.0 + min_temp_h = 20.0 + max_temp_h = 250.0 + path_adc_h = '/sys/bus/iio/devices/iio:device0/in_voltage5_raw' + mosfet_h = 3 + onoff_h = False + prefix_h = 'T1' + max_power_h = 1.0 + + sensor_a = 'B57560G104F' + pid_kp_a = 0.1 + pid_ti_a = 0.01 + pid_td_a = 0.3 + ok_range_a = 4.0 + max_rise_temp_a = 10.0 + max_fall_temp_a = 10.0 + min_temp_a = 20.0 + max_temp_a = 250.0 + path_adc_a = '/sys/bus/iio/devices/iio:device0/in_voltage0_raw' + mosfet_a = 11 + onoff_a = False + prefix_a = 'T2' + max_power_a = 1.0 + + sensor_b = 'B57560G104F' + pid_kp_b = 0.1 + pid_ti_b = 0.01 + pid_td_b = 0.3 + ok_range_b = 4.0 + max_rise_temp_b = 10.0 + max_fall_temp_b = 10.0 + min_temp_b = 20.0 + max_temp_b = 250.0 + path_adc_b = '/sys/bus/iio/devices/iio:device0/in_voltage3_raw' + mosfet_b = 12 + onoff_b = False + prefix_b = 'T3' + max_power_b = 1.0 + + sensor_c = 'B57560G104F' + pid_kp_c = 0.1 + pid_ti_c = 0.01 + pid_td_c = 0.3 + ok_range_c = 4.0 + max_rise_temp_c = 10.0 + max_fall_temp_c = 10.0 + min_temp_c = 20.0 + max_temp_c = 250.0 + path_adc_c = '/sys/bus/iio/devices/iio:device0/in_voltage2_raw' + mosfet_c = 13 + onoff_c = False + prefix_c = 'T4' + max_power_c = 1.0 + + sensor_hbp = 'B57560G104F' + pid_kp_hbp = 0.1 + pid_ti_hbp = 0.01 + pid_td_hbp = 0.3 + ok_range_hbp = 4.0 + max_rise_temp_hbp = 10.0 + max_fall_temp_hbp = 10.0 + min_temp_hbp = 20.0 + max_temp_hbp = 250.0 + path_adc_hbp = '/sys/bus/iio/devices/iio:device0/in_voltage6_raw' + mosfet_hbp = 4 + onoff_hbp = False + prefix_hbp = 'B' + max_power_hbp = 1.0 diff --git a/redeem/configuration/sections/homing.py b/redeem/configuration/sections/homing.py new file mode 100644 index 00000000..05a993ee --- /dev/null +++ b/redeem/configuration/sections/homing.py @@ -0,0 +1,53 @@ +from redeem.configuration.sections import BaseConfig + + +class HomingConfig(BaseConfig): + # default G28 homing axes + g28_default_axes = "X,Y,Z,E,H,A,B,C" + + # Homing speed for the steppers in m/s + # Search to minimum ends by default. Negative value for searching to maximum ends. + home_speed_x = 0.1 + home_speed_y = 0.1 + home_speed_z = 0.1 + home_speed_e = 0.01 + home_speed_h = 0.01 + home_speed_a = 0.01 + home_speed_b = 0.01 + home_speed_c = 0.01 + + # homing backoff speed + home_backoff_speed_x = 0.01 + home_backoff_speed_y = 0.01 + home_backoff_speed_z = 0.01 + home_backoff_speed_e = 0.01 + home_backoff_speed_h = 0.01 + home_backoff_speed_a = 0.01 + home_backoff_speed_b = 0.01 + home_backoff_speed_c = 0.01 + + # homing backoff dist + home_backoff_offset_x = 0.01 + home_backoff_offset_y = 0.01 + home_backoff_offset_z = 0.01 + home_backoff_offset_e = 0.01 + home_backoff_offset_h = 0.01 + home_backoff_offset_a = 0.01 + home_backoff_offset_b = 0.01 + home_backoff_offset_c = 0.01 + + # Where should the printer goes after homing. + # The default is to stay at the offset position. + # This setting is useful if you have a delta printer + # and want it to stay at the top position after homing, instead + # of moving down to the center of the plate. + # In that case, use home_z and set that to the same as the offset values + # for X, Y, and Z, only with different sign. + home_x = 0.0 + home_y = 0.0 + home_z = 0.0 + home_e = 0.0 + home_h = 0.0 + home_a = 0.0 + home_b = 0.0 + home_c = 0.0 diff --git a/redeem/configuration/sections/macros.py b/redeem/configuration/sections/macros.py new file mode 100644 index 00000000..89aee6b6 --- /dev/null +++ b/redeem/configuration/sections/macros.py @@ -0,0 +1,46 @@ +from redeem.configuration.sections import BaseConfig + + +class MacrosConfig(BaseConfig): + g29 = """ + M561 ; Reset the bed level matrix + M558 P0 ; Set probe type to Servo with switch + M557 P0 X10 Y20 ; Set probe point 0 + M557 P1 X10 Y180 ; Set probe point 1 + M557 P2 X180 Y100 ; Set probe point 2 + G28 X0 Y0 ; Home X Y + + G28 Z0 ; Home Z + G0 Z12 ; Move Z up to allow space for probe + G32 ; Undock probe + G92 Z0 ; Reset Z height to 0 + G30 P0 S ; Probe point 0 + G0 Z0 ; Move the Z up + G31 ; Dock probe + + G28 Z0 ; Home Z + G0 Z12 ; Move Z up to allow space for probe + G32 ; Undock probe + G92 Z0 ; Reset Z height to 0 + G30 P1 S ; Probe point 1 + G0 Z0 ; Move the Z up + G31 ; Dock probe + + G28 Z0 ; Home Z + G0 Z12 ; Move Z up to allow space for probe + G32 ; Undock probe + G92 Z0 ; Reset Z height to 0 + G30 P2 S ; Probe point 2 + G0 Z0 ; Move the Z up + G31 ; Dock probe + + G28 X0 Y0 ; Home X Y + """ + + g31 = """ + M280 P0 S320 F3000 ; Probe up (Dock sled) + """ + + g32 = """ + M280 P0 S-60 F3000 ; Probe down (Undock sled) + """ diff --git a/redeem/configuration/sections/planner.py b/redeem/configuration/sections/planner.py new file mode 100644 index 00000000..8d6237f3 --- /dev/null +++ b/redeem/configuration/sections/planner.py @@ -0,0 +1,30 @@ +from redeem.configuration.sections import BaseConfig + + +class PlannerConfig(BaseConfig): + + move_cache_size = 1024 # size of the path planning cache + print_move_buffer_wait = 250 # time to wait for buffer to fill, (ms) + + # total buffered move time should not exceed this much (ms) + max_buffered_move_time = 1000 + + acceleration_x, acceleration_y, acceleration_z = 0.5, 0.5, 0.5 + acceleration_e, acceleration_h = 0.5, 0.5 + acceleration_a, acceleration_b, acceleration_c = 0.5, 0.5, 0.5 + + max_jerk_x, max_jerk_y, max_jerk_z = 0.01, 0.01, 0.01 + max_jerk_e, max_jerk_h = 0.01, 0.01 + max_jerk_a, max_jerk_b, max_jerk_c = 0.01, 0.01, 0.01 + + # Max speed for the steppers in m/s + max_speed_x, max_speed_y, max_speed_z = 0.2, 0.2, 0.02 + max_speed_e, max_speed_h = 0.2, 0.2 + max_speed_a, max_speed_b, max_speed_c = 0.2, 0.2, 0.2 + + arc_segment_length = 0.001 # for arc commands, seperate into segments of length in m + + # When true, movements on the E axis (eg, G1, G92) will apply + # to the active tool (similar to other firmwares). When false, + # such movements will only apply to the E axis. + e_axis_active = True diff --git a/redeem/configuration/sections/plugins.py b/redeem/configuration/sections/plugins.py new file mode 100644 index 00000000..b5348f61 --- /dev/null +++ b/redeem/configuration/sections/plugins.py @@ -0,0 +1,30 @@ +from redeem.configuration.sections import BaseConfig + + +class HPX2MaxPluginConfig(BaseConfig): + + # Configuration for the HPX2Max plugin (if loaded) + # The channel on which the servo is connected. The numbering correspond to the Fan number + servo_channel = 1 + + # Extruder 0 angle to set the servo when extruder 0 is selected, in degree + extruder_0_angle = 20 + + # Extruder 1 angle to set the servo when extruder 1 is selected, in degree + extruder_1_angle = 175 + + +class DualServoPluginConfig(BaseConfig): + + # Configuration for the Dual extruder by servo plugin + # This config is only used if loaded. + + # The pin name of where the servo is located + servo_channel = "P9_14" + pulse_min = 0.001 + pulse_max = 0.002 + angle_min = -90 + angle_max = 90 + extruder_0_angle = -5 + extruder_1_angle = 5 + diff --git a/redeem/configuration/sections/probe.py b/redeem/configuration/sections/probe.py new file mode 100644 index 00000000..1fd3f687 --- /dev/null +++ b/redeem/configuration/sections/probe.py @@ -0,0 +1,11 @@ +from redeem.configuration.sections import BaseConfig + + +class ProbeConfig(BaseConfig): + + length = 0.01 + speed = 0.05 + accel = 0.1 + offset_x = 0.0 + offset_y = 0.0 + offset_z = 0.0 diff --git a/redeem/configuration/sections/rotaryencoders.py b/redeem/configuration/sections/rotaryencoders.py new file mode 100644 index 00000000..5960834f --- /dev/null +++ b/redeem/configuration/sections/rotaryencoders.py @@ -0,0 +1,8 @@ +from redeem.configuration.sections import BaseConfig + + +class RotaryEncodersConfig(BaseConfig): + enable_e = False + event_e = "/dev/input/event1" + cpr_e = -360 + diameter_e = 0.003 diff --git a/redeem/configuration/sections/servos.py b/redeem/configuration/sections/servos.py new file mode 100644 index 00000000..31ddd147 --- /dev/null +++ b/redeem/configuration/sections/servos.py @@ -0,0 +1,19 @@ +from redeem.configuration.sections import BaseConfig + + +class ServosConfig(BaseConfig): + + # Example servo for Rev A4A, connected to channel 14 on the PWM chip + # For Rev B, servo is either P9_14 or P9_16. + # Not enabled for now, just kept here for reference. + # Angle init is the angle the servo is set to when redeem starts. + # pulse min and max is the pulse with for min and max position, as always in SI unit Seconds. + # So 0.001 is 1 ms. + # Angle min and max is what angles those pulses correspond to. + servo_0_enable = False + servo_0_channel = "P9_14" + servo_0_angle_init = 90 + servo_0_angle_min = -90 + servo_0_angle_max = 90 + servo_0_pulse_min = 0.001 + servo_0_pulse_max = 0.002 diff --git a/redeem/configuration/sections/steppers.py b/redeem/configuration/sections/steppers.py new file mode 100644 index 00000000..8076e223 --- /dev/null +++ b/redeem/configuration/sections/steppers.py @@ -0,0 +1,49 @@ +from redeem.configuration.sections import BaseConfig + + +class SteppersConfig(BaseConfig): + + microstepping_x, microstepping_y, microstepping_z = 3, 3, 3 + microstepping_e, microstepping_h = 3, 3 + microstepping_a, microstepping_b, microstepping_c = 3, 3, 3 + + current_x, current_y, current_z = 0.5, 0.5, 0.5 + current_e, current_h = 0.5, 0.5 + current_a, current_b, current_c = 0.5, 0.5, 0.5 + + # full steps per 1 mm, ignore microstepping settings + steps_pr_mm_x, steps_pr_mm_y, steps_pr_mm_z = 4.0, 4.0, 50.0 + steps_pr_mm_e, steps_pr_mm_h = 6.0, 6.0 + steps_pr_mm_a, steps_pr_mm_b, steps_pr_mm_c = 6.0, 6.0, 6.0 + + backlash_x, backlash_y, backlash_z = 0.0, 0.0, 0.0 + backlash_e, backlash_h = 0.0, 0.0 + backlash_a, backlash_b, backlash_c = 0.0, 0.0, 0.0 + + # Which steppers are enabled + in_use_x, in_use_y, in_use_z = True, True, True + in_use_e, in_use_h = True, True + in_use_a, in_use_b, in_use_c = False, False, False + + # Set to -1 if axis is inverted + direction_x, direction_y, direction_z = 1, 1, 1 + direction_e, direction_h = 1, 1 + direction_a, direction_b, direction_c = 1, 1, 1 + + # Set to True if slow decay mode is needed + slow_decay_x, slow_decay_y, slow_decay_z = 0, 0, 0 + slow_decay_e, slow_decay_h = 0, 0 + slow_decay_a, slow_decay_b, slow_decay_c = 0, 0, 0 + + # A stepper controller can operate in slave mode, + # meaning that it will mirror the position of the + # specified stepper. Typically, H will mirror Y or Z, + # in the case of the former, write this: slave_y = H. + slave_x, slave_y, slave_z = '', '', '' + slave_e, slave_h = '', '' + slave_a, slave_b, slave_c = '', '', '' + + # Stepper timout + use_timeout = True + timeout_seconds = 500 + diff --git a/redeem/configuration/sections/system.py b/redeem/configuration/sections/system.py new file mode 100644 index 00000000..24c20f6a --- /dev/null +++ b/redeem/configuration/sections/system.py @@ -0,0 +1,11 @@ +from redeem.configuration.sections import BaseConfig + + +class SystemConfig(BaseConfig): + + loglevel = 20 # CRITICAL=50, # ERROR=40, # WARNING=30, INFO=20, DEBUG=10, NOTSET=0 + log_to_file = True + logfile = '/home/octo/.octoprint/logs/plugin_redeem.log' + data_path = "/etc/redeem" # location to look for data files (temperature charts, etc) + plugins = '' # Plugin to load for redeem, comma separated (i.e. HPX2Max,plugin2,plugin3) + machine_type = 'Unknown' # Machine type is used by M115 to identify the machine connected. diff --git a/redeem/configuration/sections/watchdog.py b/redeem/configuration/sections/watchdog.py new file mode 100644 index 00000000..35e4a2b5 --- /dev/null +++ b/redeem/configuration/sections/watchdog.py @@ -0,0 +1,5 @@ +from redeem.configuration.sections import BaseConfig + + +class WatchdogConfig(BaseConfig): + enable_watchdog = True diff --git a/redeem/configuration/utils.py b/redeem/configuration/utils.py new file mode 100644 index 00000000..3b2cd009 --- /dev/null +++ b/redeem/configuration/utils.py @@ -0,0 +1,3 @@ +def clean_key(key): + return key.replace('-', '_').lower() + diff --git a/tests/core/resources/default.1.9.cfg b/tests/core/resources/default.1.9.cfg new file mode 100644 index 00000000..d1b36681 --- /dev/null +++ b/tests/core/resources/default.1.9.cfg @@ -0,0 +1,616 @@ +[System] + +# CRITICAL=50, # ERROR=40, # WARNING=30, INFO=20, DEBUG=10, NOTSET=0 +loglevel = 20 + +# If set to True, also log to file. +log_to_file = True + +# Default file to log to, this can be viewed from octoprint +logfile = /home/octo/.octoprint/logs/plugin_redeem.log + +# location to look for data files (temperature charts, etc) +data_path = /etc/redeem + +# Plugin to load for redeem, comma separated (i.e. HPX2Max,plugin2,plugin3) +plugins = + +# Machine type is used by M115 +# to identify the machine connected. +machine_type = Unknown + +[Geometry] +# 0 - Cartesian +# 1 - H-belt +# 2 - Core XY +# 3 - Delta +axis_config = 0 + +# The total length each axis can travel +# This affects the homing endstop searching length. +travel_x = 0.2 +travel_y = 0.2 +travel_z = 0.2 +travel_e = 0.2 +travel_h = 0.2 +travel_a = 0.0 +travel_b = 0.0 +travel_c = 0.0 + +# Define the origin in relation to the endstops. +# The offset that the origin of the build plate has +# from the end stop. +offset_x = 0.0 +offset_y = 0.0 +offset_z = 0.0 +offset_e = 0.0 +offset_h = 0.0 +offset_a = 0.0 +offset_b = 0.0 +offset_c = 0.0 + + +bed_compensation_matrix = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]] + +[Delta] +# Distance head extends below the effector. +Hez = 0.0 +# Length of the rod +L = 0.135 +# Radius of the columns +r = 0.144 +# Effector offset +Ae = 0.026 +Be = 0.026 +Ce = 0.026 +# Carriage offset +A_radial = 0.0 +B_radial = 0.0 +C_radial = 0.0 + +# Compensation for positional error of the columns +# (For details, read: https://github.com/hercek/Marlin/blob/Marlin_v1/calibration.wxm) +# Positive values move the tower to the right, in the +X direction, tangent to it's radius +A_tangential = 0.0 +B_tangential = 0.0 +C_tangential = 0.0 + +# Stepper e is ext 1, h is ext 2 +[Steppers] + +microstepping_x = 3 +microstepping_y = 3 +microstepping_z = 3 +microstepping_e = 3 +microstepping_h = 3 +microstepping_a = 3 +microstepping_b = 3 +microstepping_c = 3 + +current_x = 0.5 +current_y = 0.5 +current_z = 0.5 +current_e = 0.5 +current_h = 0.5 +current_a = 0.5 +current_b = 0.5 +current_c = 0.5 + +# steps per mm: +# Defined how many stepper full steps needed to move 1mm. +# Do not factor in microstepping settings. +# For example: If the axis will travel 10mm in one revolution and +# angle per step in 1.8deg (200step/rev), steps_pr_mm is 20. +steps_pr_mm_x = 4.0 +steps_pr_mm_y = 4.0 +steps_pr_mm_z = 50.0 +steps_pr_mm_e = 6.0 +steps_pr_mm_h = 6.0 +steps_pr_mm_a = 6.0 +steps_pr_mm_b = 6.0 +steps_pr_mm_c = 6.0 + +backlash_x = 0.0 +backlash_y = 0.0 +backlash_z = 0.0 +backlash_e = 0.0 +backlash_h = 0.0 +backlash_a = 0.0 +backlash_b = 0.0 +backlash_c = 0.0 + + +# Which steppers are enabled +in_use_x = True +in_use_y = True +in_use_z = True +in_use_e = True +in_use_h = True +in_use_a = False +in_use_b = False +in_use_c = False + +# Set to -1 if axis is inverted +direction_x = 1 +direction_y = 1 +direction_z = 1 +direction_e = 1 +direction_h = 1 +direction_a = 1 +direction_b = 1 +direction_c = 1 + +# Set to True if slow decay mode is needed +slow_decay_x = 0 +slow_decay_y = 0 +slow_decay_z = 0 +slow_decay_e = 0 +slow_decay_h = 0 +slow_decay_a = 0 +slow_decay_b = 0 +slow_decay_c = 0 + + +# A stepper controller can operate in slave mode, +# meaning that it will mirror the position of the +# specified stepper. Typically, H will mirror Y or Z, +# in the case of the former, write this: slave_y = H. +slave_x = +slave_y = +slave_z = +slave_e = +slave_h = +slave_a = +slave_b = +slave_c = + +# Stepper timout +use_timeout = True +timeout_seconds = 500 + +[Planner] + +# size of the path planning cache +move_cache_size = 1024 + +# time to wait for buffer to fill, (ms) +print_move_buffer_wait = 250 + +# if total buffered time gets below (min_buffered_move_time) then wait for (print_move_buffer_wait) before moving again, (ms) +min_buffered_move_time = 100 + +# total buffered move time should not exceed this much (ms) +max_buffered_move_time = 1000 + +# max segment length +max_length = 0.001 + +acceleration_x = 0.5 +acceleration_y = 0.5 +acceleration_z = 0.5 +acceleration_e = 0.5 +acceleration_h = 0.5 +acceleration_a = 0.5 +acceleration_b = 0.5 +acceleration_c = 0.5 + +max_jerk_x = 0.01 +max_jerk_y = 0.01 +max_jerk_z = 0.01 +max_jerk_e = 0.01 +max_jerk_h = 0.01 +max_jerk_a = 0.01 +max_jerk_b = 0.01 +max_jerk_c = 0.01 + +# Max speed for the steppers in m/s +max_speed_x = 0.2 +max_speed_y = 0.2 +max_speed_z = 0.02 +max_speed_e = 0.2 +max_speed_h = 0.2 +max_speed_a = 0.2 +max_speed_b = 0.2 +max_speed_c = 0.2 + +# Max speed for the steppers in m/s +min_speed_x = 0.005 +min_speed_y = 0.005 +min_speed_z = 0.005 +min_speed_e = 0.01 +min_speed_h = 0.01 +min_speed_a = 0.01 +min_speed_b = 0.01 +min_speed_c = 0.01 + +# When true, movements on the E axis (eg, G1, G92) will apply +# to the active tool (similar to other firmwares). When false, +# such movements will only apply to the E axis. +e_axis_active = True + +[Cold-ends] +# To use the DS18B20 temp sensors, connect them like this. +# Enable by setting to True +connect-ds18b20-0-fan-0 = False +connect-ds18b20-1-fan-0 = False +connect-ds18b20-0-fan-1 = False + +# This list is for connecting thermistors to fans, +# so they are controlled automatically when reaching 60 degrees. +connect-therm-E-fan-0 = False +connect-therm-E-fan-1 = False +connect-therm-E-fan-2 = False +connect-therm-E-fan-3 = False +connect-therm-H-fan-0 = False +connect-therm-H-fan-1 = False +connect-therm-H-fan-2 = False +connect-therm-H-fan-3 = False +connect-therm-A-fan-0 = False +connect-therm-A-fan-1 = False +connect-therm-A-fan-2 = False +connect-therm-A-fan-3 = False +connect-therm-B-fan-0 = False +connect-therm-B-fan-1 = False +connect-therm-B-fan-2 = False +connect-therm-B-fan-3 = False +connect-therm-C-fan-0 = False +connect-therm-C-fan-1 = False +connect-therm-C-fan-2 = False +connect-therm-C-fan-3 = False +connect-therm-HBP-fan-0 = False +connect-therm-HBP-fan-1 = False +connect-therm-HBP-fan-2 = False +connect-therm-HBP-fan-3 = False + +add-fan-0-to-M106 = False +add-fan-1-to-M106 = False +add-fan-2-to-M106 = False +add-fan-3-to-M106 = False + +# If you want coolers to +# have a different 'keep' temp, list it here. +cooler_0_target_temp = 60 + +# If you want the fan-thermitor connetions to have a +# different temperature: +# therm-e-fan-0-target_temp = 70 + +[Fans] +default-fan-0-value = 0.0 +default-fan-1-value = 0.0 +default-fan-2-value = 0.0 +default-fan-3-value = 0.0 + +[Heaters] +# For list of available temp charts, look in temp_chart.py + +sensor_E = B57560G104F +pid_Kp_E = 0.1 +pid_Ti_E = 100.0 +pid_Td_E = 0.3 +ok_range_E = 4.0 +max_rise_temp_E = 10.0 +max_fall_temp_E = 10.0 +min_temp_E = 20.0 +max_temp_E = 250.0 +path_adc_E = /sys/bus/iio/devices/iio:device0/in_voltage4_raw +mosfet_E = 5 +onoff_E = False +prefix_E = T0 +max_power_E = 1.0 + +sensor_H = B57560G104F +pid_Kp_H = 0.1 +pid_Ti_H = 0.01 +pid_Td_H = 0.3 +ok_range_H = 4.0 +max_rise_temp_H = 10.0 +max_fall_temp_H = 10.0 +min_temp_H = 20.0 +max_temp_H = 250.0 +path_adc_H = /sys/bus/iio/devices/iio:device0/in_voltage5_raw +mosfet_H = 3 +onoff_H = False +prefix_H = T1 +max_power_H = 1.0 + +sensor_A = B57560G104F +pid_Kp_A = 0.1 +pid_Ti_A = 0.01 +pid_Td_A = 0.3 +ok_range_A = 4.0 +max_rise_temp_A = 10.0 +max_fall_temp_A = 10.0 +min_temp_A = 20.0 +max_temp_A = 250.0 +path_adc_A = /sys/bus/iio/devices/iio:device0/in_voltage0_raw +mosfet_A = 11 +onoff_A = False +prefix_A = T2 +max_power_A = 1.0 + +sensor_B = B57560G104F +pid_Kp_B = 0.1 +pid_Ti_B = 0.01 +pid_Td_B = 0.3 +ok_range_B = 4.0 +max_rise_temp_B = 10.0 +max_fall_temp_B = 10.0 +min_temp_B = 20.0 +max_temp_B = 250.0 +path_adc_B = /sys/bus/iio/devices/iio:device0/in_voltage3_raw +mosfet_B = 12 +onoff_B = False +prefix_B = T3 +max_power_B = 1.0 + +sensor_C = B57560G104F +pid_Kp_C = 0.1 +pid_Ti_C = 0.01 +pid_Td_C = 0.3 +ok_range_C = 4.0 +max_rise_temp_C = 10.0 +max_fall_temp_C = 10.0 +min_temp_C = 20.0 +max_temp_C = 250.0 +path_adc_C = /sys/bus/iio/devices/iio:device0/in_voltage2_raw +mosfet_C = 13 +onoff_C = False +prefix_C = T4 +max_power_C = 1.0 + +sensor_HBP = B57560G104F +pid_Kp_HBP = 0.1 +pid_Ti_HBP = 0.01 +pid_Td_HBP = 0.3 +ok_range_HBP = 4.0 +max_rise_temp_HBP = 10.0 +max_fall_temp_HBP = 10.0 +min_temp_HBP = 20.0 +max_temp_HBP = 250.0 +path_adc_HBP = /sys/bus/iio/devices/iio:device0/in_voltage6_raw +mosfet_HBP = 4 +onoff_HBP = False +prefix_HBP = B +max_power_HBP = 1.0 + +[Endstops] +# Which axis should be homed. +has_x = True +has_y = True +has_z = True +has_e = False +has_h = False +has_a = False +has_b = False +has_c = False + +inputdev = /dev/input/by-path/platform-ocp:gpio_keys-event + +# Number of cycles to wait between checking +# end stops. CPU frequency is 200 MHz +end_stop_delay_cycles = 1000 + +# Invert = +# True means endstop is connected as Normally Open (NO) or not connected +# False means endstop is connected as Normally Closed (NC) +invert_X1 = False +invert_X2 = False +invert_Y1 = False +invert_Y2 = False +invert_Z1 = False +invert_Z2 = False + +pin_X1 = GPIO3_21 +pin_X2 = GPIO0_30 +pin_Y1 = GPIO1_17 +pin_Y2 = GPIO3_17 +pin_Z1 = GPIO0_31 +pin_Z2 = GPIO0_4 + +keycode_X1 = 112 +keycode_X2 = 113 +keycode_Y1 = 114 +keycode_Y2 = 115 +keycode_Z1 = 116 +keycode_Z2 = 117 + +# If one endstop is hit, which steppers and directions are masked. +# The list is comma separated and has format +# x_cw = stepper x clockwise (independent of direction_x) +# x_ccw = stepper x counter clockwise (independent of direction_x) +# x_neg = setpper x negative direction (affected by direction_x) +# x_pos = setpper x positive direction (affected by direction_x) +# Steppers e and h (and a, b, c for reach) can also be masked. +# +# For a list of steppers to stop, use this format: x_cw, y_ccw +# For Simple XYZ bot, the usual practice would be +# end_stop_X1_stops = x_neg, end_stop_X2_stops = x_pos, ... +# For CoreXY and similar, two steppers should be stopped if an end stop is hit. +# similarly for a delta probe should stop x, y and z. +end_stop_X1_stops = +end_stop_Y1_stops = +end_stop_Z1_stops = +end_stop_X2_stops = +end_stop_Y2_stops = +end_stop_Z2_stops = + +# if an endstop should only be used for homing or probing, then add it to +# homing_only_endstops in comma separated format. +# Example: homing_only_endstops = Z1, Z2 +# this will make sure that endstop Z1 and Z2 are only used during homing or probing +# NOTE: Be very careful with this option. + +homing_only_endstops = + +soft_end_stop_min_x = -0.5 +soft_end_stop_min_y = -0.5 +soft_end_stop_min_z = -0.5 +soft_end_stop_min_e = -1000.0 +soft_end_stop_min_h = -1000.0 +soft_end_stop_min_a = -1000.0 +soft_end_stop_min_b = -1000.0 +soft_end_stop_min_c = -1000.0 + +soft_end_stop_max_x = 0.5 +soft_end_stop_max_y = 0.5 +soft_end_stop_max_z = 0.5 +soft_end_stop_max_e = 1000.0 +soft_end_stop_max_h = 1000.0 +soft_end_stop_max_a = 1000.0 +soft_end_stop_max_b = 1000.0 +soft_end_stop_max_c = 1000.0 + +[Homing] + +# Homing speed for the steppers in m/s +# Search to minimum ends by default. Negative value for searching to maximum ends. +home_speed_x = 0.1 +home_speed_y = 0.1 +home_speed_z = 0.1 +home_speed_e = 0.01 +home_speed_h = 0.01 +home_speed_a = 0.01 +home_speed_b = 0.01 +home_speed_c = 0.01 + +# homing backoff speed +home_backoff_speed_x = 0.01 +home_backoff_speed_y = 0.01 +home_backoff_speed_z = 0.01 +home_backoff_speed_e = 0.01 +home_backoff_speed_h = 0.01 +home_backoff_speed_a = 0.01 +home_backoff_speed_b = 0.01 +home_backoff_speed_c = 0.01 + +# homing backoff dist +home_backoff_offset_x = 0.01 +home_backoff_offset_y = 0.01 +home_backoff_offset_z = 0.01 +home_backoff_offset_e = 0.01 +home_backoff_offset_h = 0.01 +home_backoff_offset_a = 0.01 +home_backoff_offset_b = 0.01 +home_backoff_offset_c = 0.01 + +# Where should the printer goes after homing. +# The default is to stay at the offset position. +# This setting is useful if you have a delta printer +# and want it to stay at the top position after homing, instead +# of moving down to the center of the plate. +# In that case, use home_z and set that to the same as the offset values +# for X, Y, and Z, only with different sign. +home_x = 0.0 +home_y = 0.0 +home_z = 0.0 +home_e = 0.0 +home_h = 0.0 +home_a = 0.0 +home_b = 0.0 +home_c = 0.0 + +[Servos] +# Example servo for Rev A4A, connected to channel 14 on the PWM chip +# For Rev B, servo is either P9_14 or P9_16. +# Not enabled for now, just kept here for reference. +# Angle init is the angle the servo is set to when redeem starts. +# pulse min and max is the pulse with for min and max position, as always in SI unit Seconds. +# So 0.001 is 1 ms. +# Angle min and max is what angles those pulses correspond to. +servo_0_enable = False +servo_0_channel = P9_14 +servo_0_angle_init = 90 +servo_0_angle_min = -90 +servo_0_angle_max = 90 +servo_0_pulse_min = 0.001 +servo_0_pulse_max = 0.002 + +[Probe] +length = 0.01 +speed = 0.05 +accel = 0.1 +offset_x = 0.0 +offset_y = 0.0 +offset_z = 0.0 + +[Rotary-encoders] +enable-e = False +event-e = /dev/input/event1 +cpr-e = -360 +diameter-e = 0.003 + +[Filament-sensors] +# If the error is > 1 cm, sound the alarm +alarm-level-e = 0.01 + +[Watchdog] +enable_watchdog = True + +[Macros] +G29 = + M561 ; Reset the bed level matrix + M558 P0 ; Set probe type to Servo with switch + M557 P0 X10 Y20 ; Set probe point 0 + M557 P1 X10 Y180 ; Set probe point 1 + M557 P2 X180 Y100 ; Set probe point 2 + G28 X0 Y0 ; Home X Y + + G28 Z0 ; Home Z + G0 Z12 ; Move Z up to allow space for probe + G32 ; Undock probe + G92 Z0 ; Reset Z height to 0 + G30 P0 S ; Probe point 0 + G0 Z0 ; Move the Z up + G31 ; Dock probe + + G28 Z0 ; Home Z + G0 Z12 ; Move Z up to allow space for probe + G32 ; Undock probe + G92 Z0 ; Reset Z height to 0 + G30 P1 S ; Probe point 1 + G0 Z0 ; Move the Z up + G31 ; Dock probe + + G28 Z0 ; Home Z + G0 Z12 ; Move Z up to allow space for probe + G32 ; Undock probe + G92 Z0 ; Reset Z height to 0 + G30 P2 S ; Probe point 2 + G0 Z0 ; Move the Z up + G31 ; Dock probe + + G28 X0 Y0 ; Home X Y + +G31 = + M280 P0 S320 F3000 ; Probe up (Dock sled) + +G32 = + M280 P0 S-60 F3000 ; Probe down (Undock sled) + + +# Configuration for the HPX2Max plugin (if loaded) +[HPX2MaxPlugin] + +# The channel on which the servo is connected. The numbering correspond to the Fan number +servo_channel = 1 + +# Extruder 0 angle to set the servo when extruder 0 is selected, in degree +extruder_0_angle = 20 + +# Extruder 1 angle to set the servo when extruder 1 is selected, in degree +extruder_1_angle = 175 + + + +# Configuration for the Dual extruder by servo plugin +# This config is only used if loaded. +[DualServoPlugin] +# The pin name of where the servo is located +servo_channel = P9_14 +pulse_min = 0.001 +pulse_max = 0.002 +angle_min = -90 +angle_max = 90 +extruder_0_angle = -5 +extruder_1_angle = 5 diff --git a/configs/default.cfg b/tests/core/resources/default.2.0.cfg similarity index 100% rename from configs/default.cfg rename to tests/core/resources/default.2.0.cfg diff --git a/tests/core/resources/delta_local1.9.cfg b/tests/core/resources/delta_local1.9.cfg new file mode 100644 index 00000000..0a7f355c --- /dev/null +++ b/tests/core/resources/delta_local1.9.cfg @@ -0,0 +1,24 @@ +[Delta] + +Hez = 0.0 + +L = 0.135 + +r = 0.144 + +Ae = 0.026 +Be = 0.026 +Ce = 0.026 + +# Carriage offset +A_radial = 0.001 +B_radial = 0.002 +C_radial = 0.003 + +# Compensation for positional error of the columns +# (For details, read: https://github.com/hercek/Marlin/blob/Marlin_v1/calibration.wxm) +# Positive values move the tower to the right, in the +X direction, tangent to it's radius +A_tangential = 0.004 +B_tangential = 0.005 +C_tangential = 0.006 + diff --git a/tests/core/resources/delta_printer1.9.cfg b/tests/core/resources/delta_printer1.9.cfg new file mode 100644 index 00000000..6f436856 --- /dev/null +++ b/tests/core/resources/delta_printer1.9.cfg @@ -0,0 +1,205 @@ + +[System] + +machine_type = Roskock_Max_v2 + +[Geometry] +# Delta +axis_config = 3 + +# Set the total length each axis can travel +travel_x = -0.6 +travel_y = -0.6 +travel_z = -0.6 + +# Define the origin in relation to the endstops +# Starting point befor calibration. +offset_x = -0.381 +offset_y = -0.381 +offset_z = -0.381 + +[Delta] +# Distance head extends below the effector. +Hez = 0.013 +# Length of the rod +l = 0.2908 +# Radius of the columns +#Increase to lower nozzle in relation to outer measurements. +#Decrease to raise nozzle in relation to outer measurements. +r = 0.19825 + +#Combined Effector and Carriage offset +Ae = 0.0714 +Be = 0.0714 +Ce = 0.0714 + +#Carriage offset set to 0 and added to Effector offset +A_radial = 0 +B_radial = 0 +C_radial = 0 + +#Original Effector and Carriage Numbers +# Effector offset +#Ae = 0.033 +#Be = 0.033 +#Ce = 0.033 + +# Carriage offset (the distance from the column to the carriage's center of the rods' joints) +#A_radial = 0.0384 +#B_radial = 0.0384 +#C_radial = 0.0384 + +# Stepper e is ext 1, h is ext 2 +[Steppers] +current_x = 0.735 +current_y = 0.735 +current_z = 0.735 +current_e = 0.650 +current_h = 0.650 + +steps_pr_mm_x = 5.0 +steps_pr_mm_y = 5.0 +steps_pr_mm_z = 5.0 +steps_pr_mm_e = 5.79 +steps_pr_mm_h = 5.79 + +# Which steppers are enabled +in_use_x = True +in_use_y = True +in_use_z = True +in_use_e = True + +slow_decay_x = 1 +slow_decay_y = 1 +slow_decay_z = 1 +slow_decay_e = 1 +slow_decay_h = 1 + +microstepping_x = 6 +microstepping_y = 6 +microstepping_z = 6 +microstepping_e = 6 + +[Heaters] +# For list of available temp charts, look in temp_chart.py + +# E3D v6 Hot end +temp_chart_E = SEMITEC-104GT-2 +pid_p_E = 0.1 +pid_i_E = 0.01 +pid_d_E = 0.3 +ok_range_E = 4.0 + +#Heated Bed +temp_chart_HBP = SEMITEC-104GT-2 +pid_p_HBP = 0.1 +pid_i_HBP = 0.01 +pid_d_HBP = 0.9 +ok_range_HBP = 4.0 + +[Endstops] + +end_stop_X1_stops = x_ccw +end_stop_Y1_stops = y_ccw +end_stop_Z1_stops = z_ccw + +soft_end_stop_min_x = -0.05 +soft_end_stop_min_y = -0.05 +soft_end_stop_min_z = -0.001 + +soft_end_stop_max_x = 0.05 +soft_end_stop_max_y = 0.05 +soft_end_stop_max_z = 0.6 + +has_x = True +has_y = True +has_z = True + +# Invert = +# True means endstop is connected as Normally Open (NO) or not connected +# False means endstop is connected as Normally Closed (NC)invert_X1 = True +invert_Y1 = True +invert_Z1 = True +invert_X2 = True +invert_Y2 = True +invert_Z2 = True + +[Homing] +#Where the ptinter should go for homing +home_x = 0 +home_y = 0 +home_z = 370 +home_e = 0.0 + +#Homing speed for the steppers in M/s +home_speed_x = 0.150 +home_speed_y = 0.150 +home_speed_z = 0.150 + +#Homing backoff speed +home_backoff_speed_x = 0.025 +home_backoff_speed_y = 0.025 +home_backoff_speed_z = 0.025 + +# homing backoff dist +home_backoff_offset_x = 0.01 +home_backoff_offset_y = 0.01 +home_backoff_offset_z = 0.01 + +[Cold-ends] +# We want the E3D fan to turn on when the thermistor goes above 50 + +connect-therm-E-fan-1 = True +add-fan-0-to-M106 = True +add-fan-3-to-M106 = True + +# If you want coolers to +# have a different 'keep' temp, list it here. +cooler_0_target_temp = 50 + +[Planner] +# Max speed for the steppers in m/s +max_speed_x = 0.4 +max_speed_y = 0.4 +max_speed_z = 0.4 +max_speed_e = 0.4 +max_speed_h = 0.4 + +#Max Acceleration for Printing moves M/s^2 (Taken from Repeiter.confg) +#acceleration_x = 1.850 +#acceleration_y = 1.850 +#acceleration_z = 1.850 + +[Probe] +offset_x = 0.0 +offset_y = 0.0 + +[Macros] +#Copied form kossel_mini.cfg I never tried it on a RostockMax +g29 = + M557 P0 X0 Y0 Z5 + M557 P1 X50 Y0 Z5 ; Set probe point + M557 P2 X0 Y50 Z5 ; Set probe point + M557 P3 X-50 Y0 Z5 ; Set probe point + M557 P4 X0 Y-40 Z5 ; Set probe point + M557 P5 X25 Y0 Z5 + M557 P6 X0 Y25 Z5 + M557 P7 X-25 Y0 Z5 + M557 P8 X0 Y-25 Z5 + G32 ; Undock probe + G28 ; Home steppers + G30 P0 S + G30 P1 S ; Probe point 1 + G30 P2 S ; Probe point 2 + G30 P3 S ; Probe point 3 + G30 P4 S ; Probe point 4 + G30 P5 S + G30 P6 S + G30 P7 S + G30 P8 S + G31 ; Dock probe +G32 = + M106 P2 S255 ; Turn on power to probe. + +G31 = + M106 P2 S0 ; Turn off power to probe. diff --git a/tests/core/resources/local.cfg b/tests/core/resources/local.cfg new file mode 100644 index 00000000..e1ffd4f8 --- /dev/null +++ b/tests/core/resources/local.cfg @@ -0,0 +1,2 @@ +[System] +loglevel = 30 diff --git a/tests/core/resources/printer.cfg b/tests/core/resources/printer.cfg new file mode 100644 index 00000000..37f5e7d9 --- /dev/null +++ b/tests/core/resources/printer.cfg @@ -0,0 +1,187 @@ + +[System] + +machine_type = Roskock_Max_v2 + +[Geometry] +# Delta +axis_config = 3 + +# Set the total length each axis can travel +travel_x = -0.6 +travel_y = -0.6 +travel_z = -0.6 + +# Define the origin in relation to the endstops +# Starting point befor calibration. +offset_x = -0.381 +offset_y = -0.381 +offset_z = -0.381 + +[Delta] +# Length of the rod +l = 0.2908 +# Radius of the columns +#Increase to lower nozzle in relation to outer measurements. +#Decrease to raise nozzle in relation to outer measurements. +r = 0.12685 + +# Carriage offset (the distance from the column to the carriage's center of the rods' joints) +#A_radial = 0.0384 +#B_radial = 0.0384 +#C_radial = 0.0384 + +# Stepper e is ext 1, h is ext 2 +[Steppers] +current_x = 0.735 +current_y = 0.735 +current_z = 0.735 +current_e = 0.650 +current_h = 0.650 + +steps_pr_mm_x = 5.0 +steps_pr_mm_y = 5.0 +steps_pr_mm_z = 5.0 +steps_pr_mm_e = 5.79 +steps_pr_mm_h = 5.79 + +# Which steppers are enabled +in_use_x = True +in_use_y = True +in_use_z = True +in_use_e = True + +slow_decay_x = 1 +slow_decay_y = 1 +slow_decay_z = 1 +slow_decay_e = 1 +slow_decay_h = 1 + +microstepping_x = 6 +microstepping_y = 6 +microstepping_z = 6 +microstepping_e = 6 + +[Heaters] +# For list of available temp charts, look in temp_chart.py + +# E3D v6 Hot end +temp_chart_E = SEMITEC-104GT-2 +pid_p_E = 0.1 +pid_i_E = 0.01 +pid_d_E = 0.3 +ok_range_E = 4.0 + +#Heated Bed +temp_chart_HBP = SEMITEC-104GT-2 +pid_p_HBP = 0.1 +pid_i_HBP = 0.01 +pid_d_HBP = 0.9 +ok_range_HBP = 4.0 + +[Endstops] + +end_stop_X1_stops = x_ccw +end_stop_Y1_stops = y_ccw +end_stop_Z1_stops = z_ccw + +soft_end_stop_min_x = -0.05 +soft_end_stop_min_y = -0.05 +soft_end_stop_min_z = -0.001 + +soft_end_stop_max_x = 0.05 +soft_end_stop_max_y = 0.05 +soft_end_stop_max_z = 0.6 + +has_x = True +has_y = True +has_z = True + +# Invert = +# True means endstop is connected as Normally Open (NO) or not connected +# False means endstop is connected as Normally Closed (NC)invert_X1 = True +invert_Y1 = True +invert_Z1 = True +invert_X2 = True +invert_Y2 = True +invert_Z2 = True + +[Homing] +#Where the ptinter should go for homing +home_x = 0 +home_y = 0 +home_z = 370 +home_e = 0.0 + +#Homing speed for the steppers in M/s +home_speed_x = 0.150 +home_speed_y = 0.150 +home_speed_z = 0.150 + +#Homing backoff speed +home_backoff_speed_x = 0.025 +home_backoff_speed_y = 0.025 +home_backoff_speed_z = 0.025 + +# homing backoff dist +home_backoff_offset_x = 0.01 +home_backoff_offset_y = 0.01 +home_backoff_offset_z = 0.01 + +[Cold-ends] +# We want the E3D fan to turn on when the thermistor goes above 50 + +connect-therm-E-fan-1 = True +add-fan-0-to-M106 = True +add-fan-3-to-M106 = True + +# If you want coolers to +# have a different 'keep' temp, list it here. +cooler_0_target_temp = 50 + +[Planner] +# Max speed for the steppers in m/s +max_speed_x = 0.4 +max_speed_y = 0.4 +max_speed_z = 0.4 +max_speed_e = 0.4 +max_speed_h = 0.4 + +#Max Acceleration for Printing moves M/s^2 (Taken from Repeiter.confg) +#acceleration_x = 1.850 +#acceleration_y = 1.850 +#acceleration_z = 1.850 + +[Probe] +offset_x = 0.0 +offset_y = 0.0 + +[Macros] +#Copied form kossel_mini.cfg I never tried it on a RostockMax +g29 = + M557 P0 X0 Y0 Z5 + M557 P1 X50 Y0 Z5 ; Set probe point + M557 P2 X0 Y50 Z5 ; Set probe point + M557 P3 X-50 Y0 Z5 ; Set probe point + M557 P4 X0 Y-40 Z5 ; Set probe point + M557 P5 X25 Y0 Z5 + M557 P6 X0 Y25 Z5 + M557 P7 X-25 Y0 Z5 + M557 P8 X0 Y-25 Z5 + G32 ; Undock probe + G28 ; Home steppers + G30 P0 S + G30 P1 S ; Probe point 1 + G30 P2 S ; Probe point 2 + G30 P3 S ; Probe point 3 + G30 P4 S ; Probe point 4 + G30 P5 S + G30 P6 S + G30 P7 S + G30 P8 S + G31 ; Dock probe +G32 = + M106 P2 S255 ; Turn on power to probe. + +G31 = + M106 P2 S0 ; Turn off power to probe. diff --git a/tests/core/test_configs.py b/tests/core/test_configs.py new file mode 100644 index 00000000..5bd3df41 --- /dev/null +++ b/tests/core/test_configs.py @@ -0,0 +1,126 @@ +import unittest +from ConfigParser import SafeConfigParser, RawConfigParser + +import os + +import re + +from redeem.configuration.RedeemConfig import RedeemConfig +from redeem.configuration.factories.ConfigFactoryV19 import ConfigFactoryV19 +from redeem.configuration.factories.ConfigFactoryV20 import ConfigFactoryV20 +from tests.logger_test import LogTestCase + +current_path = os.path.dirname(os.path.abspath(__file__)) + + +class ConfigTests(unittest.TestCase): + + def _compare_configs(self, cp, redeem_config): + + for section in cp.sections(): + for option in cp.options(section): + self.assertTrue(redeem_config.has(section, option), 'option {}/{} is missing'.format(section, option)) + + if section == "Macros": + continue + + config_val = str(cp.get(section, option)) + redeem_val = str(redeem_config.get(section, option)) + self.assertEqual(config_val, redeem_val, "option {}/{} does not match: '{}' vs '{}'".format(section, option, config_val, redeem_val)) + + def test_all_keys_in_config(self): + + default_cfg = os.path.join(current_path, 'resources/default.2.0.cfg') + cp = RawConfigParser() + self.assertEqual(len(cp.read(default_cfg)), 1) + + redeem_config = RedeemConfig() + self._compare_configs(cp, redeem_config) + + def test_basic_config_factory(self): + + factory = ConfigFactoryV20() + redeem_config = factory.hydrate_config(config_file=os.path.join(current_path, 'resources/default.2.0.cfg')) + + default_cfg = os.path.join(current_path, 'resources/default.2.0.cfg') + cp = RawConfigParser() + cp.read(default_cfg) + self._compare_configs(cp, redeem_config) + + +class ConfigWarningTests(LogTestCase): + + known_19_mismatches = ('hez', 'ae', 'be', 'ce', 'a_tangential', 'b_tangential', 'c_tangential', 'min_buffered_move_time', 'max_length', 'min_speed_x', 'min_speed_y', 'min_speed_z', 'min_speed_e', 'min_speed_h', 'min_speed_a', 'min_speed_b', 'min_speed_c',) + + def test_old_config_factory(self): + """ test to make sure that 1.9 config mismatches the above list""" + + factory = ConfigFactoryV20() + + with self.assertLogs('', level='WARN') as cm: + factory.hydrate_config(config_file=os.path.join(current_path, 'resources/default.1.9.cfg')) + for warning in cm.output: + m = re.search(r'.*?\'([a-z_]+)\'', warning) + self.assertIsNotNone(m, "needs to match: {}".format(warning)) + self.assertIn(m.group(1), self.known_19_mismatches) + + +class ConfigV19toV20Tests(LogTestCase): + + def test_delta(self): + """test to make sure delta corrections are zero when tangential and angular are zero""" + + factory = ConfigFactoryV19() + + files = [ + os.path.join(current_path, 'resources/delta_printer1.9.cfg') + ] + + redeem_config = factory.hydrate_config(config_files=files) + + self.assertAlmostEqual(redeem_config.getfloat('delta', 'a_radial'), 0.0) + self.assertAlmostEqual(redeem_config.getfloat('delta', 'a_angular'), 0.0) + + self.assertAlmostEqual(redeem_config.getfloat('delta', 'b_radial'), 0.0) + self.assertAlmostEqual(redeem_config.getfloat('delta', 'b_angular'), 0.0) + + self.assertAlmostEqual(redeem_config.getfloat('delta', 'c_radial'), 0.0) + self.assertAlmostEqual(redeem_config.getfloat('delta', 'c_angular'), 0.0) + + def test_old_config_into_new(self): + """test to make sure delta corrections are converted correctly from 1.9 format to 2.0 format""" + + factory = ConfigFactoryV19() + + files = [ + os.path.join(current_path, 'resources/delta_printer1.9.cfg'), + os.path.join(current_path, 'resources/delta_local1.9.cfg') + ] + + redeem_config = factory.hydrate_config(config_files=files) + + self.assertAlmostEqual(redeem_config.getfloat('delta', 'a_radial'), 0.0010672079121700762) + self.assertAlmostEqual(redeem_config.getfloat('delta', 'a_angular'), -1.9251837083231607) + + self.assertAlmostEqual(redeem_config.getfloat('delta', 'b_radial'), -0.00210412149464) + self.assertAlmostEqual(redeem_config.getfloat('delta', 'b_angular'), 2.38594403039) + + self.assertAlmostEqual(redeem_config.getfloat('delta', 'c_radial'), 0.00610882321576) + self.assertAlmostEqual(redeem_config.getfloat('delta', 'c_angular'), 2.39954455253) + + +class LoadMultipleConfigs(LogTestCase): + + def test_printer_and_local(self): + + files = [ + os.path.join(current_path, 'resources/printer.cfg'), + os.path.join(current_path, 'resources/local.cfg') + ] + + factory = ConfigFactoryV20() + redeem_config = factory.hydrate_config(config_files=files) + + # make sure local takes precedence + self.assertEqual(redeem_config.getint('System', 'loglevel'), 30) + diff --git a/tests/gcode/MockPrinter.py b/tests/gcode/MockPrinter.py index 6ad3775e..7a85d2b4 100644 --- a/tests/gcode/MockPrinter.py +++ b/tests/gcode/MockPrinter.py @@ -3,11 +3,12 @@ import unittest import mock import sys - -from redeem.Extruder import Heater +import os +import numpy as np sys.modules['evdev'] = mock.Mock() sys.modules['spidev'] = mock.MagicMock() + sys.modules['redeem.RotaryEncoder'] = mock.Mock() sys.modules['redeem.Watchdog'] = mock.Mock() sys.modules['redeem.GPIO'] = mock.Mock() @@ -15,6 +16,7 @@ sys.modules['redeem.Key_pin'] = mock.Mock() sys.modules['redeem.DAC'] = mock.Mock() sys.modules['redeem.ShiftRegister.py'] = mock.Mock() + sys.modules['Adafruit_BBIO'] = mock.Mock() sys.modules['Adafruit_BBIO.GPIO'] = mock.Mock() sys.modules['Adafruit_GPIO'] = mock.Mock() @@ -31,23 +33,13 @@ sys.modules['redeem.USB'] = mock.Mock() sys.modules['redeem.Ethernet'] = mock.Mock() sys.modules['redeem.Pipe'] = mock.Mock() -sys.modules['redeem.Fan'] = mock.Mock() -sys.modules['redeem.Mosfet'] = mock.Mock() -sys.modules['redeem.PWM'] = mock.Mock() - -from redeem.CascadingConfigParser import CascadingConfigParser -from redeem.Redeem import * +from redeem.Redeem import Redeem +from redeem.PathPlanner import PathPlanner from redeem.EndStop import EndStop - -""" -Override CascadingConfigParser methods to set self. variables -""" -class CascadingConfigParserWedge(CascadingConfigParser): - def parse_capes(self): - self.replicape_revision = "0A4A" # Fake. No hardware involved in these tests (Redundant?) - self.reach_revision = "00A0" # Fake. No hardware involved in these tests (Redundant?) +from redeem.Path import Path +from redeem.Gcode import Gcode class MockPrinter(unittest.TestCase): @@ -90,7 +82,6 @@ def setUpConfigFiles(cls, path): @classmethod @mock.patch.object(EndStop, "_wait_for_event", new=None) @mock.patch.object(PathPlanner, "_init_path_planner") - @mock.patch("redeem.CascadingConfigParser", new=CascadingConfigParserWedge) def setUpClass(cls, mock_init_path_planner): """ @@ -110,6 +101,7 @@ def bypass_init_path_planner(self): mock.patch('redeem.Extruder.Extruder.enable', new=disabled_extruder_enable).start() mock.patch('redeem.Extruder.HBP.enable', new=disabled_hbp_enable).start() mock.patch('redeem.PathPlanner.PathPlanner._init_path_planner', new=bypass_init_path_planner) + mock.patch("redeem.PWM.PWM.i2c").start() cfg_path = "../configs" cls.setUpConfigFiles(cfg_path) @@ -118,6 +110,9 @@ def bypass_init_path_planner(self): cls.printer = cls.R.printer cls.printer.replicape_key = "TESTING_DUMMY_KEY" + # cls.printer.reach_revision = "00B0" + + cls.setUpPatch() cls.gcodes = cls.printer.processor.gcodes diff --git a/tests/gcode/test_M114.py b/tests/gcode/test_M114.py index 9a9106f0..5fe3b9f6 100644 --- a/tests/gcode/test_M114.py +++ b/tests/gcode/test_M114.py @@ -33,7 +33,9 @@ def test_gcodes_M114(self): g = Gcode({"message": "M114"}) self.printer.processor.gcodes[g.gcode].execute(g) self.printer.path_planner.get_current_pos.assert_called_with(ideal=True, mm=True) # kinda redundant, but hey. + self.assertEqual(g.answer, "ok C: X:{:.1f} Y:{:.1f} Z:{:.1f} E:{:.1f} A:{:.1f} B:{:.1f} C:{:.1f} H:{:.1f}".format( X, Y, Z, E, A, B, C, H )) + diff --git a/tests/gcode/test_M115.py b/tests/gcode/test_M115.py index 6d604b56..1f1dc00f 100644 --- a/tests/gcode/test_M115.py +++ b/tests/gcode/test_M115.py @@ -16,7 +16,7 @@ def test_gcodes_M115(self): self.assertRegexpMatches(g.answer, "REPLICAPE_KEY:TESTING_DUMMY_KEY") self.assertRegexpMatches(g.answer, "FIRMWARE_NAME:Redeem") self.assertRegexpMatches(g.answer, "FIRMWARE_VERSION:{}\s".format(re.escape(self.printer.firmware_version))) - self.assertRegexpMatches(g.answer, "FIRMWARE_URL:https?%3A\S+") + self.assertRegexpMatches(g.answer, r"FIRMWARE_URL:https://") self.assertRegexpMatches(g.answer, "MACHINE_TYPE:{}\s".format( re.escape(self.printer.config.get('System', 'machine_type')))) self.assertRegexpMatches(g.answer, "EXTRUDER_COUNT:{}".format(self.printer.NUM_AXES - 3)) diff --git a/tests/gcode/test_M19.py b/tests/gcode/test_M19.py index c15d7045..7b629a5e 100644 --- a/tests/gcode/test_M19.py +++ b/tests/gcode/test_M19.py @@ -5,7 +5,6 @@ from six import iteritems from redeem.Stepper import Stepper_00B3 - class M19_Tests(MockPrinter): @mock.patch.object(Stepper_00B3, "reset") diff --git a/tests/logger_test.py b/tests/logger_test.py new file mode 100644 index 00000000..7fb2e3c7 --- /dev/null +++ b/tests/logger_test.py @@ -0,0 +1,105 @@ +# logger_test.py +# this file contains the base class containing the newly added method +# assertLogs +import collections +import logging +import unittest + +_LoggingWatcher = collections.namedtuple("_LoggingWatcher", + ["records", "output"]) + +class _BaseTestCaseContext(object): + + def __init__(self, test_case): + self.test_case = test_case + + def _raiseFailure(self, standardMsg): + msg = self.test_case._formatMessage(self.msg, standardMsg) + raise self.test_case.failureException(msg) + + +class _CapturingHandler(logging.Handler): + """ + A logging handler capturing all (raw and formatted) logging output. + """ + + def __init__(self): + logging.Handler.__init__(self) + self.watcher = _LoggingWatcher([], []) + + def flush(self): + pass + + def emit(self, record): + self.watcher.records.append(record) + msg = self.format(record) + self.watcher.output.append(msg) + + +class _AssertLogsContext(_BaseTestCaseContext): + """A context manager used to implement TestCase.assertLogs().""" + + LOGGING_FORMAT = "%(levelname)s:%(name)s:%(message)s" + + def __init__(self, test_case, logger_name, level): + _BaseTestCaseContext.__init__(self, test_case) + self.logger_name = logger_name + if level: + self.level = logging._levelNames.get(level, level) + else: + self.level = logging.INFO + self.msg = None + + def __enter__(self): + if isinstance(self.logger_name, logging.Logger): + logger = self.logger = self.logger_name + else: + logger = self.logger = logging.getLogger(self.logger_name) + formatter = logging.Formatter(self.LOGGING_FORMAT) + handler = _CapturingHandler() + handler.setFormatter(formatter) + self.watcher = handler.watcher + self.old_handlers = logger.handlers[:] + self.old_level = logger.level + self.old_propagate = logger.propagate + logger.handlers = [handler] + logger.setLevel(self.level) + logger.propagate = False + return handler.watcher + + def __exit__(self, exc_type, exc_value, tb): + self.logger.handlers = self.old_handlers + self.logger.propagate = self.old_propagate + self.logger.setLevel(self.old_level) + if exc_type is not None: + # let unexpected exceptions pass through + return False + if len(self.watcher.records) == 0: + self._raiseFailure( + "no logs of level {} or higher triggered on {}" + .format(logging.getLevelName(self.level), self.logger.name)) + + +class LogTestCase(unittest.TestCase): + + def assertLogs(self, logger=None, level=None): + """Fail unless a log message of level *level* or higher is emitted + on *logger_name* or its children. If omitted, *level* defaults to + INFO and *logger* defaults to the root logger. + + This method must be used as a context manager, and will yield + a recording object with two attributes: `output` and `records`. + At the end of the context manager, the `output` attribute will + be a list of the matching formatted log messages and the + `records` attribute will be a list of the corresponding LogRecord + objects. + + Example:: + + with self.assertLogs('foo', level='INFO') as cm: + logging.getLogger('foo').info('first message') + logging.getLogger('foo.bar').error('second message') + self.assertEqual(cm.output, ['INFO:foo:first message', + 'ERROR:foo.bar:second message']) + """ + return _AssertLogsContext(self, logger, level)